diff --git a/.env b/.env index e3ececc2e54..d5cffcec0aa 100644 --- a/.env +++ b/.env @@ -1,4 +1,5 @@ APP_IMAGE=gdcc/dataverse:unstable -POSTGRES_VERSION=13 +POSTGRES_VERSION=16 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.3.0 +SKIP_DEPLOY=0 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 7e6995d76d9..3dba7d52109 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -14,7 +14,7 @@ Thank you for contributing to the Dataverse Project through the creation of a bu WARNING: If this is a security issue it should be reported privately to security@dataverse.org More information on bug issues and contributions can be found in the "Contributing to Dataverse" page: -https://github.com/IQSS/dataverse/blob/develop/CONTRIBUTING.md#bug-reportsissues +https://guides.dataverse.org/en/latest/contributor/index.html Please fill out as much of the template as you can. Start below this comment section. @@ -44,7 +44,6 @@ Start below this comment section. **Any related open or closed issues to this bug report?** - **Screenshots:** No matter the issue, screenshots are always welcome. @@ -53,3 +52,7 @@ To add a screenshot, please use one of the following formats and/or methods desc * https://help.github.com/en/articles/file-attachments-on-issues-and-pull-requests * + + +**Are you thinking about creating a pull request for this issue?** +Help is always welcome, is this bug something you or your organization plan to fix? diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index d6248537418..7365cb4317c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,7 @@ --- name: Feature request about: Suggest an idea or new feature for the Dataverse software! -title: 'Feature Request/Idea:' +title: 'Feature Request:' labels: 'Type: Feature' assignees: '' @@ -11,7 +11,7 @@ assignees: '' Thank you for contributing to the Dataverse Project through the creation of a feature request! More information on ideas/feature requests and contributions can be found in the "Contributing to Dataverse" page: -https://github.com/IQSS/dataverse/blob/develop/CONTRIBUTING.md#ideasfeature-requests +https://guides.dataverse.org/en/latest/contributor/index.html Please fill out as much of the template as you can. Start below this comment section. @@ -34,3 +34,6 @@ Start below this comment section. **Any open or closed issues related to this feature request?** + +**Are you thinking about creating a pull request for this feature?** +Help is always welcome, is this feature something you or your organization plan to implement? diff --git a/.github/ISSUE_TEMPLATE/idea_proposal.md b/.github/ISSUE_TEMPLATE/idea_proposal.md new file mode 100644 index 00000000000..8cb6c7bfafe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/idea_proposal.md @@ -0,0 +1,40 @@ +--- +name: Idea proposal +about: Propose a new idea for discussion to improve the Dataverse software! +title: 'Suggestion:' +labels: 'Type: Suggestion' +assignees: '' + +--- + + + +**Overview of the Suggestion** + + +**What kind of user is the suggestion intended for?** +(Example users roles: API User, Curator, Depositor, Guest, Superuser, Sysadmin) + + +**What inspired this idea?** + + +**What existing behavior do you want changed?** + + +**Any brand new behavior do you want to add to Dataverse?** + + +**Any open or closed issues related to this suggestion?** + + +**Are you thinking about creating a pull request for this issue?** +Help is always welcome, is this idea something you or your organization plan to implement? diff --git a/.github/actions/setup-maven/action.yml b/.github/actions/setup-maven/action.yml new file mode 100644 index 00000000000..4cf09f34231 --- /dev/null +++ b/.github/actions/setup-maven/action.yml @@ -0,0 +1,37 @@ +--- +name: "Setup Maven and Caches" +description: "Determine Java version and setup Maven, including necessary caches." +inputs: + git-reference: + description: 'The git reference (branch/tag) to check out' + required: false + default: '${{ github.ref }}' + pom-paths: + description: "List of paths to Maven POM(s) for cache dependency setup" + required: false + default: 'pom.xml' +runs: + using: composite + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ inputs.git-reference }} + - name: Determine Java version by reading the Maven property + shell: bash + run: | + echo "JAVA_VERSION=$(grep '' ${GITHUB_WORKSPACE}/modules/dataverse-parent/pom.xml | cut -f2 -d'>' | cut -f1 -d'<')" | tee -a ${GITHUB_ENV} + - name: Set up JDK ${{ env.JAVA_VERSION }} + id: setup-java + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: 'temurin' + cache: 'maven' + cache-dependency-path: ${{ inputs.pom-paths }} + - name: Download common cache on branch cache miss + if: ${{ steps.setup-java.outputs.cache-hit != 'true' }} + uses: actions/cache/restore@v4 + with: + key: dataverse-maven-cache + path: ~/.m2/repository diff --git a/.github/workflows/check_property_files.yml b/.github/workflows/check_property_files.yml new file mode 100644 index 00000000000..505310aab35 --- /dev/null +++ b/.github/workflows/check_property_files.yml @@ -0,0 +1,32 @@ +name: "Properties Check" +on: + pull_request: + paths: + - "src/**/*.properties" + - "scripts/api/data/metadatablocks/*" +jobs: + duplicate_keys: + name: Duplicate Keys + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run duplicates detection script + shell: bash + run: tests/check_duplicate_properties.sh + + metadata_blocks_properties: + name: Metadata Blocks Properties + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup GraalVM + Native Image + uses: graalvm/setup-graalvm@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + java-version: '21' + distribution: 'graalvm-community' + - name: Setup JBang + uses: jbangdev/setup-jbang@main + - name: Run metadata block properties verification script + shell: bash + run: tests/verify_mdb_properties.sh diff --git a/.github/workflows/container_app_push.yml b/.github/workflows/container_app_push.yml index b3e247e376c..3b7ce066d73 100644 --- a/.github/workflows/container_app_push.yml +++ b/.github/workflows/container_app_push.yml @@ -5,6 +5,12 @@ 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: + inputs: + base-image-ref: + type: string + description: "Reference of the base image to build on in full qualified form [/]/:" + required: false + default: "gdcc/base:unstable" pull_request: branches: - develop @@ -16,7 +22,6 @@ on: env: IMAGE_TAG: unstable - BASE_IMAGE_TAG: unstable REGISTRY: "" # Empty means default to Docker Hub PLATFORMS: "linux/amd64,linux/arm64" MASTER_BRANCH_TAG: alpha @@ -33,20 +38,24 @@ jobs: if: ${{ github.repository_owner == 'IQSS' }} steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Set up JDK - uses: actions/setup-java@v3 + - name: Checkout and Setup Maven + uses: IQSS/dataverse/.github/actions/setup-maven@develop with: - java-version: "17" - distribution: temurin - cache: maven + pom-paths: | + pom.xml + modules/container-configbaker/pom.xml + modules/dataverse-parent/pom.xml + + # TODO: Add a filter step here, that avoids building the image if this is a PR and there are other files touched than declared above. + # Use https://github.com/dorny/paths-filter to solve this. This will ensure we do not run this twice if this workflow + # will be triggered by the other workflows already (base image or java changes) + # To become a part of #10618. - 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 + $( [[ -n "${{ inputs.base-image-ref }}" ]] && echo "-Dbase.image=${{ inputs.base-image-ref }}" ) install # TODO: add smoke / integration testing here (add "-Pct -DskipIntegrationTests=false") @@ -106,11 +115,13 @@ jobs: if: needs.check-secrets.outputs.available == 'true' && ( github.event_name != 'push' || ( github.event_name == 'push' && contains(fromJSON('["develop", "master"]'), github.ref_name))) steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 + - name: Checkout and Setup Maven + uses: IQSS/dataverse/.github/actions/setup-maven@develop with: - java-version: "17" - distribution: temurin + pom-paths: | + pom.xml + modules/container-configbaker/pom.xml + modules/dataverse-parent/pom.xml # Depending on context, we push to different targets. Login accordingly. - if: github.event_name != 'pull_request' @@ -146,11 +157,13 @@ jobs: run: > mvn -B -f modules/dataverse-parent -P ct -pl edu.harvard.iq:dataverse -am + $( [[ -n "${{ inputs.base-image-ref }}" ]] && echo "-Dbase.image=${{ inputs.base-image-ref }}" ) 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 }} + -Dapp.image.tag=${{ env.IMAGE_TAG }} + $( [[ -n "${{ inputs.base-image-ref }}" ]] && echo "-Dbase.image=${{ inputs.base-image-ref }}" ) ${{ env.REGISTRY }} -Ddocker.platforms=${{ env.PLATFORMS }} -P ct deploy diff --git a/.github/workflows/container_base_push.yml b/.github/workflows/container_base_push.yml index b938851f816..c2340576c78 100644 --- a/.github/workflows/container_base_push.yml +++ b/.github/workflows/container_base_push.yml @@ -1,99 +1,130 @@ --- -name: Base Container Image +name: Container Images Releasing on: push: + tags: + - 'v[6-9].**' branches: - 'develop' - - 'master' + # "Path filters are not evaluated for pushes of tags" https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#onpushpull_requestpull_request_targetpathspaths-ignore paths: - 'modules/container-base/**' + - '!modules/container-base/src/backports/**' + - '!modules/container-base/README.md' - 'modules/dataverse-parent/pom.xml' - '.github/workflows/container_base_push.yml' - pull_request: - branches: - - 'develop' - - 'master' - paths: - - '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 + + # These TODOs are left for #10618 + # TODO: we are missing a workflow_call option here, so we can trigger this flow from pr comments and maven tests (keep the secrets availability in mind!) + # TODO: we are missing a pull_request option here (filter for stuff that would trigger the maven runs!) so we can trigger preview builds for them when coming from the main repo (keep the secrets availability in mind!) env: - IMAGE_TAG: unstable PLATFORMS: linux/amd64,linux/arm64 + DEVELOPMENT_BRANCH: develop jobs: build: - name: Build image + name: Base Image runs-on: ubuntu-latest permissions: contents: read packages: read - strategy: - matrix: - jdk: [ '17' ] # Only run in upstream repo - avoid unnecessary runs in forks if: ${{ github.repository_owner == 'IQSS' }} + outputs: + base-image-ref: ${{ steps.finalize.outputs.base-image-ref }} steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Set up JDK ${{ matrix.jdk }} - uses: actions/setup-java@v3 + - name: Checkout and Setup Maven + uses: IQSS/dataverse/.github/actions/setup-maven@develop with: - java-version: ${{ matrix.jdk }} - distribution: 'adopt' - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - - name: Build base container image with local architecture - run: mvn -f modules/container-base -Pct package + pom-paths: modules/container-base/pom.xml - # Run anything below only if this is not a pull request. - # Accessing, pushing tags etc. to DockerHub will only succeed in upstream because secrets. - - - if: ${{ github.event_name == 'push' && github.ref_name == 'develop' }} - name: Push description to DockerHub - uses: peter-evans/dockerhub-description@v3 + # Note: Accessing, pushing tags etc. to DockerHub will only succeed in upstream and + # on events in context of upstream because secrets. PRs run in context of forks by default! + - name: Log in to the Container registry + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - repository: gdcc/base - short-description: "Dataverse Base Container image providing Payara application server and optimized configuration" - readme-filepath: ./modules/container-base/README.md - - if: ${{ github.event_name != 'pull_request' }} - name: Log in to the Container registry - uses: docker/login-action@v2 + # In case this is a push to develop, we care about buildtime. + # Configure a remote ARM64 build host in addition to the local AMD64 in two steps. + - name: Setup SSH agent + if: ${{ github.event_name != 'schedule' }} + uses: webfactory/ssh-agent@v0.9.0 with: - registry: ${{ env.REGISTRY }} - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - if: ${{ github.event_name != 'pull_request' }} - name: Set up QEMU for multi-arch builds - uses: docker/setup-qemu-action@v2 - - name: Re-set image tag based on branch - if: ${{ github.ref_name == 'master' }} - 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 }} -Ddocker.platforms=${{ env.PLATFORMS }} + ssh-private-key: ${{ secrets.BUILDER_ARM64_SSH_PRIVATE_KEY }} + - name: Provide the known hosts key and the builder config + if: ${{ github.event_name != 'schedule' }} + run: | + echo "${{ secrets.BUILDER_ARM64_SSH_HOST_KEY }}" > ~/.ssh/known_hosts + mkdir -p modules/container-base/target/buildx-state/buildx/instances + cat > modules/container-base/target/buildx-state/buildx/instances/maven << EOF + { "Name": "maven", + "Driver": "docker-container", + "Dynamic": false, + "Nodes": [{"Name": "maven0", + "Endpoint": "unix:///var/run/docker.sock", + "Platforms": [{"os": "linux", "architecture": "amd64"}], + "DriverOpts": null, + "Flags": ["--allow-insecure-entitlement=network.host"], + "Files": null}, + {"Name": "maven1", + "Endpoint": "ssh://${{ secrets.BUILDER_ARM64_SSH_CONNECTION }}", + "Platforms": [{"os": "linux", "architecture": "arm64"}], + "DriverOpts": null, + "Flags": ["--allow-insecure-entitlement=network.host"], + "Files": null}]} + EOF + + # Determine the base image name we are going to use from here on + - name: Determine base image name + run: | + if [[ "${{ github.ref_name }}" = "${{ env.DEVELOPMENT_BRANCH }}" ]]; then + echo "BASE_IMAGE=$( mvn initialize help:evaluate -Pct -f modules/container-base -Dexpression=base.image -q -DforceStdout )" | tee -a "${GITHUB_ENV}" + echo "BASE_IMAGE_UPCOMING=$( mvn initialize help:evaluate -Pct -f modules/container-base -Dexpression=base.image -Dbase.image.tag.suffix="" -q -DforceStdout )" | tee -a "${GITHUB_ENV}" + else + echo "BASE_IMAGE=$( mvn initialize help:evaluate -Pct -f modules/container-base -Dexpression=base.image -Dbase.image.tag.suffix="" -q -DforceStdout )" | tee -a "${GITHUB_ENV}" + fi + - name: Calculate revision number for immutable tag (on release branches only) + if: ${{ github.ref_name != env.DEVELOPMENT_BRANCH }} + id: revision-tag + uses: ./.github/actions/get-image-revision + with: + image-ref: ${{ env.BASE_IMAGE }} + tag-options-prefix: "-Dbase.image.tag.suffix='' -Ddocker.tags.revision=" + - name: Configure update of "latest" tag for development branch + id: develop-tag + if: ${{ github.ref_name == env.DEVELOPMENT_BRANCH }} + run: | + echo "tag-options=-Ddocker.tags.develop=unstable -Ddocker.tags.upcoming=${BASE_IMAGE_UPCOMING#*:}" | tee -a "${GITHUB_OUTPUT}" + + - name: Deploy multi-arch base container image to Docker Hub + id: build + run: | + mvn -f modules/container-base -Pct deploy -Ddocker.noCache -Ddocker.platforms=${{ env.PLATFORMS }} \ + -Ddocker.imagePropertyConfiguration=override ${{ steps.develop-tag.outputs.tag-options }} ${{ steps.revision-tag.outputs.tag-options }} + + - name: Determine appropriate base image ref for app image + id: finalize + run: | + if [[ "${{ github.ref_name }}" = "${{ env.DEVELOPMENT_BRANCH }}" ]]; then + echo "base-image-ref=${BASE_IMAGE_UPCOMING}" | tee -a "$GITHUB_OUTPUT" + else + echo "base-image-ref=gdcc/base:${{ steps.revision-tag.outputs.revision-tag }}" | tee -a "$GITHUB_OUTPUT" + fi + 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 + needs: + - build + uses: ./.github/workflows/container_app_push.yml + with: + base-image-ref: ${{ needs.build.outputs.base-image-ref }} diff --git a/.github/workflows/container_maintenance.yml b/.github/workflows/container_maintenance.yml new file mode 100644 index 00000000000..986fe25cdf5 --- /dev/null +++ b/.github/workflows/container_maintenance.yml @@ -0,0 +1,119 @@ +--- +name: Container Images Scheduled Maintenance + +on: + # TODO: think about adding a (filtered) push event trigger here in case we change the patches + # --- + # Allow manual workflow triggers in case we need to repair images on Docker Hub (build and replace) + workflow_dispatch: + inputs: + force_build: + type: boolean + required: false + default: false + description: "Build and deploy even if no newer Java images or package updates are found." + schedule: + - cron: '23 3 * * 0' # Run for 'develop' every Sunday at 03:23 UTC + +env: + PLATFORMS: linux/amd64,linux/arm64 + NUM_PAST_RELEASES: 3 + +jobs: + build: + name: Base Image Matrix Build + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + # Only run in upstream repo - avoid unnecessary runs in forks + if: ${{ github.repository_owner == 'IQSS' }} + outputs: + supported_tag_matrix: ${{ steps.execute.outputs.supported_tag_matrix }} + rebuilt_base_images: ${{ steps.execute.outputs.rebuilt_base_images }} + + steps: + - name: Checkout and Setup Maven + uses: IQSS/dataverse/.github/actions/setup-maven@develop + with: + pom-paths: modules/container-base/pom.xml + + # Note: Accessing, pushing tags etc. to DockerHub will only succeed in upstream and + # on events in context of upstream because secrets. PRs run in context of forks by default! + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up QEMU for multi-arch builds + uses: docker/setup-qemu-action@v3 + with: + platforms: ${{ env.PLATFORMS }} + + # Discover the releases we want to maintain + - name: Discover maintained releases + id: discover + run: | + echo "FORCE_BUILD=$( [[ "${{ inputs.force_build }}" = "true" ]] && echo 1 || echo 0 )" | tee -a "$GITHUB_ENV" + DEVELOPMENT_BRANCH=$( curl -f -sS https://api.github.com/repos/${{ github.repository }} | jq -r '.default_branch' ) + echo "DEVELOPMENT_BRANCH=$DEVELOPMENT_BRANCH" | tee -a "$GITHUB_ENV" + echo "branches=$( curl -f -sS https://api.github.com/repos/IQSS/dataverse/releases | jq -r " .[0:${{ env.NUM_PAST_RELEASES }}] | .[].tag_name, \"${DEVELOPMENT_BRANCH}\" " | tr "\n" " " )" | tee -a "${GITHUB_OUTPUT}" + + # Execute matrix build for the discovered branches + - name: Execute build matrix script + id: execute + run: | + .github/workflows/scripts/maintenance-job.sh ${{ steps.discover.outputs.branches }} + + # TODO: Use the needs.build.outputs.rebuilt_base_images with fromJSON() to create a matrix job. + # Must be a single rank matrix (vector), the branch and base image tag information ships as "branch=tag" string + # Will be part of working on #10618, app image versioned tags. + #push-app-img: + # name: "Rebase & Publish App Image" + # permissions: + # contents: read + # packages: write + # pull-requests: write + # secrets: inherit + # needs: + # - build + # strategy: + # fail-fast: false + # matrix: + # branch: ${{ fromJson(needs.discover.outputs.branches) }} + # uses: ./.github/workflows/container_app_push.yml + # with: + # branch: ${{ matrix.branch }} + + hub-description: + name: Push description to DockerHub + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + needs: build + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Render README + id: render + run: | + TAGS_JSON='${{ needs.build.outputs.supported_tag_matrix }}' + echo "$TAGS_JSON" | jq -r 'keys | sort | reverse | .[]' | + while IFS= read -r branch; do + echo \ + "- \`$( echo "$TAGS_JSON" | jq --arg v "$branch" -r '.[$v] | join("`, `")' )\`" \ + "([Dockerfile](https://github.com/IQSS/dataverse/blob/${branch}/modules/container-base/src/main/docker/Dockerfile)," \ + "[Patches](https://github.com/IQSS/dataverse/blob/develop/modules/container-base/src/backports/${branch}))" \ + | tee -a "${GITHUB_WORKSPACE}/tags.md" + done + sed -i -e "/<\!-- TAG BLOCK HERE -->/r ${GITHUB_WORKSPACE}/tags.md" "./modules/container-base/README.md" + + - name: Push description to DockerHub + uses: peter-evans/dockerhub-description@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: gdcc/base + short-description: "Dataverse Base Container image providing Payara application server and optimized configuration" + readme-filepath: ./modules/container-base/README.md \ No newline at end of file diff --git a/.github/workflows/maven_cache_management.yml b/.github/workflows/maven_cache_management.yml new file mode 100644 index 00000000000..fedf63b7c54 --- /dev/null +++ b/.github/workflows/maven_cache_management.yml @@ -0,0 +1,101 @@ +name: Maven Cache Management + +on: + # Every push to develop should trigger cache rejuvenation (dependencies might have changed) + push: + branches: + - develop + # According to https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy + # all caches are deleted after 7 days of no access. Make sure we rejuvenate every 7 days to keep it available. + schedule: + - cron: '23 2 * * 0' # Run for 'develop' every Sunday at 02:23 UTC (3:23 CET, 21:23 ET) + # Enable manual cache management + workflow_dispatch: + # Delete branch caches once a PR is merged + pull_request: + types: + - closed + +env: + COMMON_CACHE_KEY: "dataverse-maven-cache" + COMMON_CACHE_PATH: "~/.m2/repository" + +jobs: + seed: + name: Drop and Re-Seed Local Repository + runs-on: ubuntu-latest + if: ${{ github.event_name != 'pull_request' }} + permissions: + # Write permission needed to delete caches + # See also: https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28#delete-a-github-actions-cache-for-a-repository-using-a-cache-id + actions: write + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Determine Java version from Parent POM + run: echo "JAVA_VERSION=$(grep '' modules/dataverse-parent/pom.xml | cut -f2 -d'>' | cut -f1 -d'<')" >> ${GITHUB_ENV} + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: temurin + - name: Seed common cache + run: | + mvn -B -f modules/dataverse-parent dependency:go-offline dependency:resolve-plugins + # This non-obvious order is due to the fact that the download via Maven above will take a very long time (7-8 min). + # Jobs should not be left without a cache. Deleting and saving in one go leaves only a small chance for a cache miss. + - name: Drop common cache + run: | + gh extension install actions/gh-actions-cache + echo "🛒 Fetching list of cache keys" + cacheKeys=$(gh actions-cache list -R ${{ github.repository }} -B develop | cut -f 1 ) + + ## Setting this to not fail the workflow while deleting cache keys. + set +e + echo "🗑️ Deleting caches..." + for cacheKey in $cacheKeys + do + gh actions-cache delete $cacheKey -R ${{ github.repository }} -B develop --confirm + done + echo "✅ Done" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Save the common cache + uses: actions/cache@v4 + with: + path: ${{ env.COMMON_CACHE_PATH }} + key: ${{ env.COMMON_CACHE_KEY }} + enableCrossOsArchive: true + + # Let's delete feature branch caches once their PR is merged - we only have 10 GB of space before eviction kicks in + deplete: + name: Deplete feature branch caches + runs-on: ubuntu-latest + if: ${{ github.event_name == 'pull_request' }} + permissions: + # `actions:write` permission is required to delete caches + # See also: https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28#delete-a-github-actions-cache-for-a-repository-using-a-cache-id + actions: write + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Cleanup caches + run: | + gh extension install actions/gh-actions-cache + + BRANCH=refs/pull/${{ github.event.pull_request.number }}/merge + echo "🛒 Fetching list of cache keys" + cacheKeysForPR=$(gh actions-cache list -R ${{ github.repository }} -B $BRANCH | cut -f 1 ) + + ## Setting this to not fail the workflow while deleting cache keys. + set +e + echo "🗑️ Deleting caches..." + for cacheKey in $cacheKeysForPR + do + gh actions-cache delete $cacheKey -R ${{ github.repository }} -B $BRANCH --confirm + done + echo "✅ Done" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/maven_unit_test.yml b/.github/workflows/maven_unit_test.yml index a70c55fc31d..a94b17a67ba 100644 --- a/.github/workflows/maven_unit_test.yml +++ b/.github/workflows/maven_unit_test.yml @@ -4,6 +4,7 @@ on: push: paths: - "**.java" + - "**.sql" - "pom.xml" - "modules/**/pom.xml" - "!modules/container-base/**" @@ -11,6 +12,7 @@ on: pull_request: paths: - "**.java" + - "**.sql" - "pom.xml" - "modules/**/pom.xml" - "!modules/container-base/**" @@ -28,6 +30,7 @@ jobs: continue-on-error: ${{ matrix.experimental }} runs-on: ubuntu-latest steps: + # TODO: As part of #10618 change to setup-maven custom action # Basic setup chores - uses: actions/checkout@v3 - name: Set up JDK ${{ matrix.jdk }} @@ -93,6 +96,7 @@ jobs: # status: "Experimental" continue-on-error: ${{ matrix.experimental }} steps: + # TODO: As part of #10618 change to setup-maven custom action # Basic setup chores - uses: actions/checkout@v3 - name: Set up JDK ${{ matrix.jdk }} @@ -126,6 +130,7 @@ jobs: needs: integration-test name: Coverage Report Submission steps: + # TODO: As part of #10618 change to setup-maven custom action # Basic setup chores - uses: actions/checkout@v3 - uses: actions/setup-java@v3 @@ -154,6 +159,11 @@ jobs: # NOTE: this may be extended with adding a report to the build output, leave a comment, send to Sonarcloud, ... + # TODO: Add a filter step here, that avoids calling the app image release workflow if there are changes to the base image. + # Use https://github.com/dorny/paths-filter to solve this. Will require and additional job or adding to integration-test job. + # This way we ensure that we're not running the app image flow with a non-matching base image. + # To become a part of #10618. + push-app-img: name: Publish App Image permissions: diff --git a/.github/workflows/scripts/maintenance-job.sh b/.github/workflows/scripts/maintenance-job.sh new file mode 100755 index 00000000000..370988b9812 --- /dev/null +++ b/.github/workflows/scripts/maintenance-job.sh @@ -0,0 +1,180 @@ +#!/bin/bash + +# A matrix-like job to maintain a number of releases as well as the latest snap of Dataverse. + +# PREREQUISITES: +# - You have Java, Maven, QEMU and Docker all setup and ready to go +# - You obviously checked out the develop branch, otherwise you'd not be executing this script +# - You added all the branch names you want to run maintenance for as arguments +# Optional, but recommended: +# - You added a DEVELOPMENT_BRANCH env var to your runner/job env with the name of the development branch +# - You added a FORCE_BUILD=0|1 env var to indicate if the base image build should be forced +# - You added a PLATFORMS env var with all the target platforms you want to build for + +# NOTE: +# This script is a culmination of Github Action steps into a single script. +# The reason to put all of this in here is due to the complexity of the Github Action and the limitation of the +# matrix support in Github actions, where outputs cannot be aggregated or otherwise used further. + +set -euo pipefail + +# Get all the inputs +# If not within a runner, just print to stdout (duplicating the output in case of tee usage, but that's ok for testing) +GITHUB_OUTPUT=${GITHUB_OUTPUT:-"/proc/self/fd/1"} +GITHUB_ENV=${GITHUB_ENV:-"/proc/self/fd/1"} +GITHUB_WORKSPACE=${GITHUB_WORKSPACE:-"$(pwd)"} +GITHUB_SERVER_URL=${GITHUB_SERVER_URL:-"https://github.com"} +GITHUB_REPOSITORY=${GITHUB_REPOSITORY:-"IQSS/dataverse"} + +MAINTENANCE_WORKSPACE="${GITHUB_WORKSPACE}/maintenance-job" + +DEVELOPMENT_BRANCH="${DEVELOPMENT_BRANCH:-"develop"}" +FORCE_BUILD="${FORCE_BUILD:-"0"}" +PLATFORMS="${PLATFORMS:-"linux/amd64,linux/arm64"}" + +# Setup and validation +if [[ -z "$*" ]]; then + >&2 echo "You must give a list of branch names as arguments" + exit 1; +fi + +source "$( dirname "$0" )/utils.sh" + +# Delete old stuff if present +rm -rf "$MAINTENANCE_WORKSPACE" +mkdir -p "$MAINTENANCE_WORKSPACE" + +# Store the image tags we maintain in this array (same order as branches array!) +# This list will be used to build the support matrix within the Docker Hub image description +SUPPORTED_ROLLING_TAGS=() +# Store the tags of base images we are actually rebuilding to base new app images upon +# Takes the from "branch-name=base-image-ref" +REBUILT_BASE_IMAGES=() + +for BRANCH in "$@"; do + echo "::group::Running maintenance for $BRANCH" + + # 0. Determine if this is a development branch and the most current release + IS_DEV=0 + if [[ "$BRANCH" = "$DEVELOPMENT_BRANCH" ]]; then + IS_DEV=1 + fi + IS_CURRENT_RELEASE=0 + if [[ "$BRANCH" = $( curl -f -sS "https://api.github.com/repos/$GITHUB_REPOSITORY/releases" | jq -r '.[0].tag_name' ) ]]; then + IS_CURRENT_RELEASE=1 + fi + + # 1. Let's get the maintained sources + git clone -c advice.detachedHead=false --depth 1 --branch "$BRANCH" "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" "$MAINTENANCE_WORKSPACE/$BRANCH" + # Switch context + cd "$MAINTENANCE_WORKSPACE/$BRANCH" + + # 2. Now let's apply the patches (we have them checked out in $GITHUB_WORKSPACE, not necessarily in this local checkout) + echo "Checking for patches..." + if [[ -d ${GITHUB_WORKSPACE}/modules/container-base/src/backports/$BRANCH ]]; then + echo "Applying patches now." + find "${GITHUB_WORKSPACE}/modules/container-base/src/backports/$BRANCH" -type f -name '*.patch' -print0 | xargs -0 -n1 patch -p1 -s -i + fi + + # 3. Determine the base image ref (/:) + BASE_IMAGE_REF="" + # For the dev branch we want to full flexi stack tag, to detect stack upgrades requiring new build + if (( IS_DEV )); then + BASE_IMAGE_REF=$( mvn initialize help:evaluate -Pct -f modules/container-base -Dexpression=base.image -q -DforceStdout ) + else + BASE_IMAGE_REF=$( mvn initialize help:evaluate -Pct -f modules/container-base -Dexpression=base.image -Dbase.image.tag.suffix="" -q -DforceStdout ) + fi + echo "Determined BASE_IMAGE_REF=$BASE_IMAGE_REF from Maven" + + # 4. Check for Temurin image updates + JAVA_IMAGE_REF=$( mvn help:evaluate -Pct -f modules/container-base -Dexpression=java.image -q -DforceStdout ) + echo "Determined JAVA_IMAGE_REF=$JAVA_IMAGE_REF from Maven" + NEWER_JAVA_IMAGE=0 + if check_newer_parent "$JAVA_IMAGE_REF" "$BASE_IMAGE_REF"; then + NEWER_JAVA_IMAGE=1 + fi + + # 5. Check for package updates in base image + PKGS="$( grep "ARG PKGS" modules/container-base/src/main/docker/Dockerfile | cut -f2 -d= | tr -d '"' )" + echo "Determined installed packages=\"$PKGS\" from Maven" + NEWER_PKGS=0 + # Don't bother with package checks if the java image is newer already + if ! (( NEWER_JAVA_IMAGE )); then + if check_newer_pkgs "$BASE_IMAGE_REF" "$PKGS"; then + NEWER_PKGS=1 + fi + fi + + # 6. Get current immutable revision tag if not on the dev branch + REV=$( current_revision "$BASE_IMAGE_REF" ) + CURRENT_REV_TAG="${BASE_IMAGE_REF#*:}-r$REV" + NEXT_REV_TAG="${BASE_IMAGE_REF#*:}-r$(( REV + 1 ))" + + # 7. Let's put together what tags we want added to this build run + TAG_OPTIONS="" + if ! (( IS_DEV )); then + TAG_OPTIONS="-Dbase.image=$BASE_IMAGE_REF -Ddocker.tags.revision=$NEXT_REV_TAG" + # In case of the current release, add the "latest" tag as well. + if (( IS_CURRENT_RELEASE )); then + TAG_OPTIONS="$TAG_OPTIONS -Ddocker.tags.latest=latest" + fi + else + UPCOMING_TAG=$( mvn initialize help:evaluate -Pct -f modules/container-base -Dexpression=base.image.tag -Dbase.image.tag.suffix="" -q -DforceStdout ) + TAG_OPTIONS="-Ddocker.tags.develop=unstable -Ddocker.tags.upcoming=$UPCOMING_TAG" + + # For the dev branch we only have rolling tags and can add them now already + SUPPORTED_ROLLING_TAGS+=("[\"unstable\", \"$UPCOMING_TAG\", \"${BASE_IMAGE_REF#*:}\"]") + fi + echo "Determined these additional Maven tag options: $TAG_OPTIONS" + + # 8. Let's build the base image if necessary + NEWER_IMAGE=0 + if (( NEWER_JAVA_IMAGE + NEWER_PKGS + FORCE_BUILD > 0 )); then + mvn -Pct -f modules/container-base deploy -Ddocker.noCache -Ddocker.platforms="${PLATFORMS}" \ + -Ddocker.imagePropertyConfiguration=override $TAG_OPTIONS + NEWER_IMAGE=1 + # Save the information about the immutable or rolling tag we just built + if ! (( IS_DEV )); then + REBUILT_BASE_IMAGES+=("$BRANCH=${BASE_IMAGE_REF%:*}:$NEXT_REV_TAG") + else + REBUILT_BASE_IMAGES+=("$BRANCH=$BASE_IMAGE_REF") + fi + else + echo "No rebuild necessary, we're done here." + fi + + # 9. Add list of rolling and immutable tags for release builds + if ! (( IS_DEV )); then + RELEASE_TAGS_LIST="[" + if (( IS_CURRENT_RELEASE )); then + RELEASE_TAGS_LIST+="\"latest\", " + fi + RELEASE_TAGS_LIST+="\"${BASE_IMAGE_REF#*:}\", " + if (( NEWER_IMAGE )); then + RELEASE_TAGS_LIST+="\"$NEXT_REV_TAG\"]" + else + RELEASE_TAGS_LIST+="\"$CURRENT_REV_TAG\"]" + fi + SUPPORTED_ROLLING_TAGS+=("${RELEASE_TAGS_LIST}") + fi + + echo "::endgroup::" +done + +# Built the output which base images have actually been rebuilt as JSON +REBUILT_IMAGES="[" +for IMAGE in "${REBUILT_BASE_IMAGES[@]}"; do + REBUILT_IMAGES+=" \"$IMAGE\" " +done +REBUILT_IMAGES+="]" +echo "rebuilt_base_images=${REBUILT_IMAGES// /, }" | tee -a "${GITHUB_OUTPUT}" + +# Built the supported rolling tags matrix as JSON +SUPPORTED_TAGS="{" +for (( i=0; i < ${#SUPPORTED_ROLLING_TAGS[@]} ; i++ )); do + j=$((i+1)) + SUPPORTED_TAGS+="\"${!j}\": ${SUPPORTED_ROLLING_TAGS[$i]}" + (( i < ${#SUPPORTED_ROLLING_TAGS[@]}-1 )) && SUPPORTED_TAGS+=", " +done +SUPPORTED_TAGS+="}" +echo "supported_tag_matrix=$SUPPORTED_TAGS" | tee -a "$GITHUB_OUTPUT" diff --git a/.github/workflows/scripts/utils.sh b/.github/workflows/scripts/utils.sh new file mode 100644 index 00000000000..987b58d8bb5 --- /dev/null +++ b/.github/workflows/scripts/utils.sh @@ -0,0 +1,108 @@ +#!/bin/bash + +set -euo pipefail + +function check_newer_parent() { + PARENT_IMAGE="$1" + # Get namespace, default to "library" if not found + PARENT_IMAGE_NS="${PARENT_IMAGE%/*}" + if [[ "$PARENT_IMAGE_NS" = "${PARENT_IMAGE}" ]]; then + PARENT_IMAGE_NS="library" + fi + PARENT_IMAGE_REPO="${PARENT_IMAGE%:*}" + PARENT_IMAGE_TAG="${PARENT_IMAGE#*:}" + + PARENT_IMAGE_LAST_UPDATE="$( curl -sS "https://hub.docker.com/v2/namespaces/${PARENT_IMAGE_NS}/repositories/${PARENT_IMAGE_REPO}/tags/${PARENT_IMAGE_TAG}" | jq -r .last_updated )" + if [[ "$PARENT_IMAGE_LAST_UPDATE" = "null" ]]; then + echo "::error title='Invalid PARENT Image'::Could not find ${PARENT_IMAGE} in the registry" + exit 1 + fi + + DERIVED_IMAGE="$2" + # Get namespace, default to "library" if not found + DERIVED_IMAGE_NS="${DERIVED_IMAGE%/*}" + if [[ "${DERIVED_IMAGE_NS}" = "${DERIVED_IMAGE}" ]]; then + DERIVED_IMAGE_NS="library" + fi + DERIVED_IMAGE_REPO="$( echo "${DERIVED_IMAGE%:*}" | cut -f2 -d/ )" + DERIVED_IMAGE_TAG="${DERIVED_IMAGE#*:}" + + DERIVED_IMAGE_LAST_UPDATE="$( curl -sS "https://hub.docker.com/v2/namespaces/${DERIVED_IMAGE_NS}/repositories/${DERIVED_IMAGE_REPO}/tags/${DERIVED_IMAGE_TAG}" | jq -r .last_updated )" + if [[ "$DERIVED_IMAGE_LAST_UPDATE" = "null" || "$DERIVED_IMAGE_LAST_UPDATE" < "$PARENT_IMAGE_LAST_UPDATE" ]]; then + echo "Parent image $PARENT_IMAGE has a newer release ($PARENT_IMAGE_LAST_UPDATE), which is more recent than $DERIVED_IMAGE ($DERIVED_IMAGE_LAST_UPDATE)" + return 0 + else + echo "Parent image $PARENT_IMAGE ($PARENT_IMAGE_LAST_UPDATE) is older than $DERIVED_IMAGE ($DERIVED_IMAGE_LAST_UPDATE)" + return 1 + fi +} + +function check_newer_pkgs() { + IMAGE="$1" + PKGS="$2" + + docker run --rm -u 0 "${IMAGE}" sh -c "apt update >/dev/null 2>&1 && apt install -s ${PKGS}" | tee /proc/self/fd/2 | grep -q "0 upgraded" + STATUS=$? + + if [[ $STATUS -eq 0 ]]; then + echo "Base image $IMAGE has no updates for our custom installed packages" + return 1 + else + echo "Base image $IMAGE needs updates for our custom installed packages" + return 0 + fi + + # TODO: In a future version of this script, we might want to include checking for other security updates, + # not just updates to the packages we installed. + # grep security /etc/apt/sources.list > /tmp/security.list + # apt-get update -oDir::Etc::Sourcelist=/tmp/security.list + # apt-get dist-upgrade -y -oDir::Etc::Sourcelist=/tmp/security.list -oDir::Etc::SourceParts=/bin/false -s + +} + +function current_revision() { + IMAGE="$1" + IMAGE_NS_REPO="${IMAGE%:*}" + IMAGE_TAG="${IMAGE#*:}" + + if [[ "$IMAGE_TAG" = "$IMAGE_NS_REPO" ]]; then + >&2 echo "You must provide an image reference in the format [/]:" + exit 1 + fi + + case "$IMAGE_NS_REPO" in + */*) :;; # namespace/repository syntax, leave as is + *) IMAGE_NS_REPO="library/$IMAGE_NS_REPO";; # bare repository name (docker official image); must convert to namespace/repository syntax + esac + + # Without such a token we may run into rate limits + # OB 2024-09-16: for some reason using this token stopped working. Let's go without and see if we really fall into rate limits. + # token=$( curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$IMAGE_NS_REPO:pull" ) + + ALL_TAGS="$( + i=0 + while [ $? == 0 ]; do + i=$((i+1)) + # OB 2024-09-16: for some reason using this token stopped working. Let's go without and see if we really fall into rate limits. + # RESULT=$( curl -s -H "Authorization: Bearer $token" "https://registry.hub.docker.com/v2/repositories/$IMAGE_NS_REPO/tags/?page=$i&page_size=100" ) + RESULT=$( curl -s "https://registry.hub.docker.com/v2/repositories/$IMAGE_NS_REPO/tags/?page=$i&page_size=100" ) + if [[ $( echo "$RESULT" | jq '.message' ) != "null" ]]; then + # If we run into an error on the first attempt, that means we have a problem. + if [[ "$i" == "1" ]]; then + >&2 echo "Error when retrieving tag data: $( echo "$RESULT" | jq '.message' )" + exit 2 + # Otherwise it will just mean we reached the last page already + else + break + fi + else + echo "$RESULT" | jq -r '."results"[]["name"]' + # DEBUG: + #echo "$RESULT" | >&2 jq -r '."results"[]["name"]' + fi + done + )" + + # Note: if a former tag could not be found, it just might not exist already. Start new series with rev 0 + echo "$ALL_TAGS" | grep "${IMAGE_TAG}-r" | sed -e "s#${IMAGE_TAG}-r##" | sort -h | tail -n1 || echo "-1" +} diff --git a/.github/workflows/shellspec.yml b/.github/workflows/shellspec.yml index 227a74fa00f..3320d9d08a4 100644 --- a/.github/workflows/shellspec.yml +++ b/.github/workflows/shellspec.yml @@ -24,28 +24,11 @@ jobs: run: | cd tests/shell shellspec - shellspec-centos7: - name: "CentOS 7" + shellspec-rocky9: + name: "RockyLinux 9" runs-on: ubuntu-latest container: - image: centos:7 - steps: - - uses: actions/checkout@v2 - - name: Install shellspec - run: | - curl -fsSL https://github.com/shellspec/shellspec/releases/download/${{ env.SHELLSPEC_VERSION }}/shellspec-dist.tar.gz | tar -xz -C /usr/share - ln -s /usr/share/shellspec/shellspec /usr/bin/shellspec - - name: Install dependencies - run: yum install -y ed - - name: Run shellspec - run: | - cd tests/shell - shellspec - shellspec-rocky8: - name: "RockyLinux 8" - runs-on: ubuntu-latest - container: - image: rockylinux/rockylinux:8 + image: rockylinux/rockylinux:9 steps: - uses: actions/checkout@v2 - name: Install shellspec diff --git a/.gitignore b/.gitignore index 7f0d3a2b466..514f82116de 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ oauth-credentials.md /src/main/webapp/oauth2/newAccount.html scripts/api/setup-all.sh* scripts/api/setup-all.*.log +src/main/resources/edu/harvard/iq/dataverse/openapi/ # ctags generated tag file tags @@ -61,3 +62,4 @@ src/main/webapp/resources/images/dataverseproject.png.thumb140 # Docker development volumes /docker-dev-volumes +/.vs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1430ba951a6..4fa6e955b70 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,67 +1,7 @@ # Contributing to Dataverse -Thank you for your interest in contributing to Dataverse! We are open to contributions from everyone. You don't need permission to participate. Just jump in. If you have questions, please reach out using one or more of the channels described below. +Thank you for your interest in contributing to Dataverse! We are open to contributions from everyone. -We aren't just looking for developers. There are many ways to contribute to Dataverse. We welcome contributions of ideas, bug reports, usability research/feedback, documentation, code, and more! +Please see our [Contributor Guide][] for how you can help! -## Ideas/Feature Requests - -Your idea or feature request might already be captured in the Dataverse [issue tracker] on GitHub but if not, the best way to bring it to the community's attention is by posting on the [dataverse-community Google Group][] or bringing it up on a [Community Call][]. You're also welcome to make some noise in [chat.dataverse.org][] or cram your idea into 280 characters and mention [@dataverseorg][] on Twitter. To discuss your idea privately, please email it to support@dataverse.org - -There's a chance your idea is already on our roadmap, which is available at https://www.iq.harvard.edu/roadmap-dataverse-project - -[chat.dataverse.org]: http://chat.dataverse.org -[issue tracker]: https://github.com/IQSS/dataverse/issues -[@dataverseorg]: https://twitter.com/dataverseorg - -## Usability testing - -Please email us at support@dataverse.org if you are interested in participating in usability testing. - -## Bug Reports/Issues - -An issue is a bug (a feature is no longer behaving the way it should) or a feature (something new to Dataverse that helps users complete tasks). You can browse the Dataverse [issue tracker] on GitHub by open or closed issues or by milestones. - -Before submitting an issue, please search the existing issues by using the search bar at the top of the page. If there is an existing open issue that matches the issue you want to report, please add a comment to it. - -If there is no pre-existing issue or it has been closed, please click on the "New Issue" button, log in, and write in what the issue is (unless it is a security issue which should be reported privately to security@dataverse.org). - -If you do not receive a reply to your new issue or comment in a timely manner, please email support@dataverse.org with a link to the issue. - -### Writing an Issue - -For the subject of an issue, please start it by writing the feature or functionality it relates to, i.e. "Create Account:..." or "Dataset Page:...". In the body of the issue, please outline the issue you are reporting with as much detail as possible. In order for the Dataverse development team to best respond to the issue, we need as much information about the issue as you can provide. Include steps to reproduce bugs. Indicate which version you're using, which is shown at the bottom of the page. We love screenshots! - -### Issue Attachments - -You can attach certain files (images, screenshots, logs, etc.) by dragging and dropping, selecting them, or pasting from the clipboard. Files must be one of GitHub's [supported attachment formats] such as png, gif, jpg, txt, pdf, zip, etc. (Pro tip: A file ending in .log can be renamed to .txt so you can upload it.) If there's no easy way to attach your file, please include a URL that points to the file in question. - -[supported attachment formats]: https://help.github.com/articles/file-attachments-on-issues-and-pull-requests/ - -## Documentation - -The source for the documentation at http://guides.dataverse.org/en/latest/ is in the GitHub repo under the "[doc][]" folder. If you find a typo or inaccuracy or something to clarify, please send us a pull request! For more on the tools used to write docs, please see the [documentation][] section of the Developer Guide. - -[doc]: https://github.com/IQSS/dataverse/tree/develop/doc/sphinx-guides/source -[documentation]: http://guides.dataverse.org/en/latest/developers/documentation.html - -## Code/Pull Requests - -We love code contributions. Developers are not limited to the main Dataverse code in this git repo. You can help with API client libraries in your favorite language that are mentioned in the [API Guide][] or create a new library. You can help work on configuration management code that's mentioned in the [Installation Guide][]. The Installation Guide also covers a relatively new concept called "external tools" that allows developers to create their own tools that are available from within an installation of Dataverse. - -[API Guide]: http://guides.dataverse.org/en/latest/api -[Installation Guide]: http://guides.dataverse.org/en/latest/installation - -If you are interested in working on the main Dataverse code, great! Before you start coding, please reach out to us either on the [dataverse-community Google Group][], the [dataverse-dev Google Group][], [chat.dataverse.org][], or via support@dataverse.org to make sure the effort is well coordinated and we avoid merge conflicts. We maintain a list of [community contributors][] and [dev efforts][] the community is working on so please let us know if you'd like to be added or removed from either list. - -Please read http://guides.dataverse.org/en/latest/developers/version-control.html to understand how we use the "git flow" model of development and how we will encourage you to create a GitHub issue (if it doesn't exist already) to associate with your pull request. That page also includes tips on making a pull request. - -After making your pull request, your goal should be to help it advance through our kanban board at https://github.com/orgs/IQSS/projects/34 . If no one has moved your pull request to the code review column in a timely manner, please reach out. Note that once a pull request is created for an issue, we'll remove the issue from the board so that we only track one card (the pull request). - -Thanks for your contribution! - -[dataverse-community Google Group]: https://groups.google.com/group/dataverse-community -[Community Call]: https://dataverse.org/community-calls -[dataverse-dev Google Group]: https://groups.google.com/group/dataverse-dev -[community contributors]: https://docs.google.com/spreadsheets/d/1o9DD-MQ0WkrYaEFTD5rF_NtyL8aUISgURsAXSL7Budk/edit?usp=sharing -[dev efforts]: https://github.com/orgs/IQSS/projects/34/views/6 +[Contributor Guide]: https://guides.dataverse.org/en/latest/contributor/index.html diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index b0864a0c55f..00000000000 --- a/Dockerfile +++ /dev/null @@ -1 +0,0 @@ -# See http://guides.dataverse.org/en/latest/developers/containers.html diff --git a/README.md b/README.md index 831dbfed5ff..77720453d5f 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,11 @@ Dataverse® Dataverse is an [open source][] software platform for sharing, finding, citing, and preserving research data (developed by the [Dataverse team](https://dataverse.org/about) at the [Institute for Quantitative Social Science](https://iq.harvard.edu/) and the [Dataverse community][]). -[dataverse.org][] is our home on the web and shows a map of Dataverse installations around the world, a list of [features][], [integrations][] that have been made possible through [REST APIs][], our development [roadmap][], and more. +[dataverse.org][] is our home on the web and shows a map of Dataverse installations around the world, a list of [features][], [integrations][] that have been made possible through [REST APIs][], our [project board][], our development [roadmap][], and more. We maintain a demo site at [demo.dataverse.org][] which you are welcome to use for testing and evaluating Dataverse. -To install Dataverse, please see our [Installation Guide][] which will prompt you to download our [latest release][]. +To install Dataverse, please see our [Installation Guide][] which will prompt you to download our [latest release][]. Docker users should consult the [Container Guide][]. To discuss Dataverse with the community, please join our [mailing list][], participate in a [community call][], chat with us at [chat.dataverse.org][], or attend our annual [Dataverse Community Meeting][]. @@ -28,7 +28,9 @@ Dataverse is a trademark of President and Fellows of Harvard College and is regi [Dataverse community]: https://dataverse.org/developers [Installation Guide]: https://guides.dataverse.org/en/latest/installation/index.html [latest release]: https://github.com/IQSS/dataverse/releases +[Container Guide]: https://guides.dataverse.org/en/latest/container/index.html [features]: https://dataverse.org/software-features +[project board]: https://github.com/orgs/IQSS/projects/34 [roadmap]: https://www.iq.harvard.edu/roadmap-dataverse-project [integrations]: https://dataverse.org/integrations [REST APIs]: https://guides.dataverse.org/en/latest/api/index.html diff --git a/conf/proxy/Caddyfile b/conf/proxy/Caddyfile new file mode 100644 index 00000000000..70e6904d26e --- /dev/null +++ b/conf/proxy/Caddyfile @@ -0,0 +1,12 @@ +# This configuration is intended to be used with Caddy, a very small high perf proxy. +# It will serve the application containers Payara Admin GUI via HTTP instead of HTTPS, +# avoiding the trouble of self signed certificates for local development. + +:4848 { + reverse_proxy https://dataverse:4848 { + transport http { + tls_insecure_skip_verify + } + header_down Location "^https://" "http://" + } +} diff --git a/conf/solr/9.3.0/schema.xml b/conf/solr/schema.xml similarity index 98% rename from conf/solr/9.3.0/schema.xml rename to conf/solr/schema.xml index 3711ffeddba..2aed50e9998 100644 --- a/conf/solr/9.3.0/schema.xml +++ b/conf/solr/schema.xml @@ -142,6 +142,7 @@ + @@ -157,7 +158,8 @@ - + + @@ -204,6 +206,7 @@ + @@ -229,6 +232,8 @@ + + @@ -562,11 +570,12 @@ + - + @@ -585,6 +594,7 @@ + @@ -609,7 +619,7 @@ - + diff --git a/conf/solr/9.3.0/solrconfig.xml b/conf/solr/solrconfig.xml similarity index 99% rename from conf/solr/9.3.0/solrconfig.xml rename to conf/solr/solrconfig.xml index 36ed4f23390..34386375fe1 100644 --- a/conf/solr/9.3.0/solrconfig.xml +++ b/conf/solr/solrconfig.xml @@ -290,7 +290,7 @@ have some sort of hard autoCommit to limit the log size. --> - ${solr.autoCommit.maxTime:15000} + ${solr.autoCommit.maxTime:30000} false @@ -301,7 +301,7 @@ --> - ${solr.autoSoftCommit.maxTime:-1} + ${solr.autoSoftCommit.maxTime:1000} + +All of them are rolling tags, except those ending with `-r`, which are the most recent immutable tags. +The `unstable` tags are the current development branch snapshot. +We strongly recommend using only immutable tags for production use cases. Within the main repository, you may find the base image files at `/modules/container-base`. 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. +You may use, extend, or alter this image to your liking and/or host in some different registry if you want to under the terms of the Apache 2.0 license. **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). diff --git a/modules/container-base/pom.xml b/modules/container-base/pom.xml index fc672696df4..6417b5b91fa 100644 --- a/modules/container-base/pom.xml +++ b/modules/container-base/pom.xml @@ -39,9 +39,13 @@ ct docker-build + gdcc/base:${base.image.tag} - unstable - eclipse-temurin:${target.java.version}-jre + + ${base.image.version}-${java.image.flavor}${base.image.tag.suffix} + -p${payara.version}-j${target.java.version} + eclipse-temurin:${target.java.version}-jre-${java.image.flavor} + noble 1000 1000 linux/amd64,linux/arm64 @@ -95,7 +99,6 @@ - ${docker.platforms} ${project.build.directory}/buildx-state diff --git a/modules/container-base/src/backports/v6.1/001-pom.xml.patch b/modules/container-base/src/backports/v6.1/001-pom.xml.patch new file mode 100644 index 00000000000..6498f972889 --- /dev/null +++ b/modules/container-base/src/backports/v6.1/001-pom.xml.patch @@ -0,0 +1,26 @@ +--- a/modules/container-base/pom.xml 2024-08-26 21:53:55.985584815 +0200 ++++ b/modules/container-base/pom.xml 2024-08-26 21:38:09.925910314 +0200 +@@ -40,8 +42,11 @@ + + docker-build + gdcc/base:${base.image.tag} +- unstable +- eclipse-temurin:${target.java.version}-jre ++ gdcc/base:${base.image.tag} ++ ${base.image.version}-${java.image.flavor}${base.image.tag.suffix} ++ -p${payara.version}-j${target.java.version} ++ eclipse-temurin:${target.java.version}-jre-${java.image.flavor} ++ jammy + 1000 + 1000 + linux/amd64,linux/arm64 +@@ -110,6 +113,9 @@ + + assembly.xml + ++ ++ ${base.image.tag.revision} ++ + + + diff --git a/modules/container-base/src/backports/v6.1/002-Dockerfile.patch b/modules/container-base/src/backports/v6.1/002-Dockerfile.patch new file mode 100644 index 00000000000..4bb7a1eac91 --- /dev/null +++ b/modules/container-base/src/backports/v6.1/002-Dockerfile.patch @@ -0,0 +1,10 @@ +--- a/modules/container-base/src/main/docker/Dockerfile ++++ b/modules/container-base/src/main/docker/Dockerfile +@@ -233,4 +233,6 @@ LABEL org.opencontainers.image.created="@git.build.time@" \ + org.opencontainers.image.vendor="Global Dataverse Community Consortium" \ + org.opencontainers.image.licenses="Apache-2.0" \ + org.opencontainers.image.title="Dataverse Base Image" \ +- org.opencontainers.image.description="This container image provides an application server tuned for Dataverse software" ++ org.opencontainers.image.description="This container image provides an application server tuned for Dataverse software" \ ++ org.opencontainers.image.base.name="@java.image@" \ ++ org.dataverse.deps.payara.version="@payara.version@" diff --git a/modules/container-base/src/backports/v6.1/003-parent-pom.xml.patch b/modules/container-base/src/backports/v6.1/003-parent-pom.xml.patch new file mode 100644 index 00000000000..a69cfd43ea7 --- /dev/null +++ b/modules/container-base/src/backports/v6.1/003-parent-pom.xml.patch @@ -0,0 +1,11 @@ +--- a/modules/dataverse-parent/pom.xml ++++ b/modules/dataverse-parent/pom.xml +@@ -457,7 +457,8 @@ + + ++ ${revision} + + + diff --git a/modules/container-base/src/backports/v6.2/001-pom.xml.patch b/modules/container-base/src/backports/v6.2/001-pom.xml.patch new file mode 100644 index 00000000000..6498f972889 --- /dev/null +++ b/modules/container-base/src/backports/v6.2/001-pom.xml.patch @@ -0,0 +1,26 @@ +--- a/modules/container-base/pom.xml 2024-08-26 21:53:55.985584815 +0200 ++++ b/modules/container-base/pom.xml 2024-08-26 21:38:09.925910314 +0200 +@@ -40,8 +42,11 @@ + + docker-build + gdcc/base:${base.image.tag} +- unstable +- eclipse-temurin:${target.java.version}-jre ++ gdcc/base:${base.image.tag} ++ ${base.image.version}-${java.image.flavor}${base.image.tag.suffix} ++ -p${payara.version}-j${target.java.version} ++ eclipse-temurin:${target.java.version}-jre-${java.image.flavor} ++ jammy + 1000 + 1000 + linux/amd64,linux/arm64 +@@ -110,6 +113,9 @@ + + assembly.xml + ++ ++ ${base.image.tag.revision} ++ + + + diff --git a/modules/container-base/src/backports/v6.2/002-Dockerfile.labels.patch b/modules/container-base/src/backports/v6.2/002-Dockerfile.labels.patch new file mode 100644 index 00000000000..fbb7f80c4ce --- /dev/null +++ b/modules/container-base/src/backports/v6.2/002-Dockerfile.labels.patch @@ -0,0 +1,10 @@ +--- a/modules/container-base/src/main/docker/Dockerfile ++++ b/modules/container-base/src/main/docker/Dockerfile +@@ -242,4 +242,6 @@ LABEL org.opencontainers.image.created="@git.build.time@" \ + org.opencontainers.image.vendor="Global Dataverse Community Consortium" \ + org.opencontainers.image.licenses="Apache-2.0" \ + org.opencontainers.image.title="Dataverse Base Image" \ +- org.opencontainers.image.description="This container image provides an application server tuned for Dataverse software" ++ org.opencontainers.image.description="This container image provides an application server tuned for Dataverse software" \ ++ org.opencontainers.image.base.name="@java.image@" \ ++ org.dataverse.deps.payara.version="@payara.version@" diff --git a/modules/container-base/src/backports/v6.2/003-Dockerfile.security.patch b/modules/container-base/src/backports/v6.2/003-Dockerfile.security.patch new file mode 100644 index 00000000000..1ab4c3a980a --- /dev/null +++ b/modules/container-base/src/backports/v6.2/003-Dockerfile.security.patch @@ -0,0 +1,10 @@ +--- a/modules/container-base/src/main/docker/Dockerfile ++++ b/modules/container-base/src/main/docker/Dockerfile +@@ -226,6 +226,7 @@ USER root + RUN true && \ + chgrp -R 0 "${DOMAIN_DIR}" && \ + chmod -R g=u "${DOMAIN_DIR}" ++USER payara + + # Set the entrypoint to tini (as a process supervisor) + ENTRYPOINT ["/usr/bin/dumb-init", "--"] diff --git a/modules/container-base/src/backports/v6.2/004-parent-pom.xml.patch b/modules/container-base/src/backports/v6.2/004-parent-pom.xml.patch new file mode 100644 index 00000000000..a69cfd43ea7 --- /dev/null +++ b/modules/container-base/src/backports/v6.2/004-parent-pom.xml.patch @@ -0,0 +1,11 @@ +--- a/modules/dataverse-parent/pom.xml ++++ b/modules/dataverse-parent/pom.xml +@@ -457,7 +457,8 @@ + + ++ ${revision} + + + diff --git a/modules/container-base/src/backports/v6.3/001-pom.xml.patch b/modules/container-base/src/backports/v6.3/001-pom.xml.patch new file mode 100644 index 00000000000..6498f972889 --- /dev/null +++ b/modules/container-base/src/backports/v6.3/001-pom.xml.patch @@ -0,0 +1,26 @@ +--- a/modules/container-base/pom.xml 2024-08-26 21:53:55.985584815 +0200 ++++ b/modules/container-base/pom.xml 2024-08-26 21:38:09.925910314 +0200 +@@ -40,8 +42,11 @@ + + docker-build + gdcc/base:${base.image.tag} +- unstable +- eclipse-temurin:${target.java.version}-jre ++ gdcc/base:${base.image.tag} ++ ${base.image.version}-${java.image.flavor}${base.image.tag.suffix} ++ -p${payara.version}-j${target.java.version} ++ eclipse-temurin:${target.java.version}-jre-${java.image.flavor} ++ jammy + 1000 + 1000 + linux/amd64,linux/arm64 +@@ -110,6 +113,9 @@ + + assembly.xml + ++ ++ ${base.image.tag.revision} ++ + + + diff --git a/modules/container-base/src/backports/v6.3/002-Dockerfile.labels.patch b/modules/container-base/src/backports/v6.3/002-Dockerfile.labels.patch new file mode 100644 index 00000000000..c7744882153 --- /dev/null +++ b/modules/container-base/src/backports/v6.3/002-Dockerfile.labels.patch @@ -0,0 +1,10 @@ +--- a/modules/container-base/src/main/docker/Dockerfile ++++ b/modules/container-base/src/main/docker/Dockerfile +@@ -240,4 +241,6 @@ LABEL org.opencontainers.image.created="@git.build.time@" \ + org.opencontainers.image.vendor="Global Dataverse Community Consortium" \ + org.opencontainers.image.licenses="Apache-2.0" \ + org.opencontainers.image.title="Dataverse Base Image" \ +- org.opencontainers.image.description="This container image provides an application server tuned for Dataverse software" ++ org.opencontainers.image.description="This container image provides an application server tuned for Dataverse software" \ ++ org.opencontainers.image.base.name="@java.image@" \ ++ org.dataverse.deps.payara.version="@payara.version@" diff --git a/modules/container-base/src/backports/v6.3/003-Dockerfile.security.patch b/modules/container-base/src/backports/v6.3/003-Dockerfile.security.patch new file mode 100644 index 00000000000..d8487b3aacc --- /dev/null +++ b/modules/container-base/src/backports/v6.3/003-Dockerfile.security.patch @@ -0,0 +1,7 @@ +--- a/modules/container-base/src/main/docker/Dockerfile ++++ b/modules/container-base/src/main/docker/Dockerfile +@@ -224,6 +224,7 @@ USER root + RUN true && \ + chgrp -R 0 "${DOMAIN_DIR}" && \ + chmod -R g=u "${DOMAIN_DIR}" ++USER payara diff --git a/modules/container-base/src/backports/v6.3/004-parent-pom.xml.patch b/modules/container-base/src/backports/v6.3/004-parent-pom.xml.patch new file mode 100644 index 00000000000..a69cfd43ea7 --- /dev/null +++ b/modules/container-base/src/backports/v6.3/004-parent-pom.xml.patch @@ -0,0 +1,11 @@ +--- a/modules/dataverse-parent/pom.xml ++++ b/modules/dataverse-parent/pom.xml +@@ -457,7 +457,8 @@ + + ++ ${revision} + + + diff --git a/modules/container-base/src/main/docker/Dockerfile b/modules/container-base/src/main/docker/Dockerfile index 97aa4cd2792..802db62e5e4 100644 --- a/modules/container-base/src/main/docker/Dockerfile +++ b/modules/container-base/src/main/docker/Dockerfile @@ -22,7 +22,7 @@ # # Make the Java base image and version configurable (useful for trying newer Java versions and flavors) -ARG JAVA_IMAGE="eclipse-temurin:11-jre" +ARG JAVA_IMAGE="eclipse-temurin:17-jre" FROM $JAVA_IMAGE # Default payara ports to expose @@ -41,16 +41,21 @@ ENV PAYARA_DIR="${HOME_DIR}/appserver" \ STORAGE_DIR="/dv" \ SECRETS_DIR="/secrets" \ DUMPS_DIR="/dumps" \ - PASSWORD_FILE="${HOME_DIR}/passwordFile" \ - ADMIN_USER="admin" \ - ADMIN_PASSWORD="admin" \ + PAYARA_ADMIN_USER="admin" \ + # This is a public default, easy to change via this env var at runtime + PAYARA_ADMIN_PASSWORD="admin" \ DOMAIN_NAME="domain1" \ - PAYARA_ARGS="" + # This is the public default as per https://docs.payara.fish/community/docs/Technical%20Documentation/Payara%20Server%20Documentation/Security%20Guide/Administering%20System%20Security.html#to-change-the-master-password + # Can be changed at runtime via this env var + DOMAIN_PASSWORD="changeit" \ + PAYARA_ARGS="" \ + LINUX_USER="payara" \ + LINUX_GROUP="payara" \ + # This is a public default and can be changed at runtime using this env var + LINUX_PASSWORD="payara" ENV PATH="${PATH}:${PAYARA_DIR}/bin:${SCRIPT_DIR}" \ DOMAIN_DIR="${PAYARA_DIR}/glassfish/domains/${DOMAIN_NAME}" \ DEPLOY_PROPS="" \ - PREBOOT_COMMANDS="${CONFIG_DIR}/pre-boot-commands.asadmin" \ - POSTBOOT_COMMANDS="${CONFIG_DIR}/post-boot-commands.asadmin" \ JVM_ARGS="" \ MEM_MAX_RAM_PERCENTAGE="70.0" \ MEM_XSS="512k" \ @@ -65,11 +70,16 @@ ENV PATH="${PATH}:${PAYARA_DIR}/bin:${SCRIPT_DIR}" \ JVM_DUMPS_ARG="-XX:+HeapDumpOnOutOfMemoryError" \ ENABLE_JMX=0 \ ENABLE_JDWP=0 \ - ENABLE_RELOAD=0 + ENABLE_RELOAD=0 \ + SKIP_DEPLOY=0 ### PART 1: SYSTEM ### ARG UID=1000 ARG GID=1000 +# Auto-populated by BuildKit / buildx +#ARG TARGETARCH="amd64" +ARG TARGETARCH + USER root WORKDIR / SHELL ["/bin/bash", "-euo", "pipefail", "-c"] @@ -79,20 +89,25 @@ RUN <> /tmp/password-change-file.txt - echo "AS_ADMIN_PASSWORD=${ADMIN_PASSWORD}" >> ${PASSWORD_FILE} - asadmin --user=${ADMIN_USER} --passwordfile=/tmp/password-change-file.txt change-admin-password --domain_name=${DOMAIN_NAME} + echo "AS_ADMIN_NEWPASSWORD=${PAYARA_ADMIN_PASSWORD}" >> /tmp/password-change-file.txt + asadmin --user=${PAYARA_ADMIN_USER} --passwordfile=/tmp/password-change-file.txt change-admin-password --domain_name=${DOMAIN_NAME} + + # Prepare shorthand + PASSWORD_FILE=$(mktemp) + echo "AS_ADMIN_PASSWORD=${PAYARA_ADMIN_PASSWORD}" >> ${PASSWORD_FILE} + ASADMIN="${PAYARA_DIR}/bin/asadmin --user=${PAYARA_ADMIN_USER} --passwordfile=${PASSWORD_FILE}" + # Start domain for configuration ${ASADMIN} start-domain ${DOMAIN_NAME} # Allow access to admin with password only @@ -154,7 +177,7 @@ RUN < "$PASSWORD_FILE" + echo "AS_ADMIN_NEWPASSWORD=${PAYARA_ADMIN_PASSWORD}" >> "$PASSWORD_FILE" + asadmin --user="${PAYARA_ADMIN_USER}" --passwordfile="$PASSWORD_FILE" change-admin-password --domain_name="${DOMAIN_NAME}" + rm "$PASSWORD_FILE" +else + echo "IMPORTANT: THIS CONTAINER USES THE DEFAULT PASSWORD FOR PAYARA ADMIN \"${PAYARA_ADMIN_USER}\"! ('admin')" + echo " To change the password, set the PAYARA_ADMIN_PASSWORD env var." +fi + +# Change the domain master password if necessary +# > The master password is not tied to a user account, and it is not used for authentication. +# > Instead, Payara Server strictly uses the master password to ONLY encrypt the keystore and truststore used to store keys and certificates for the DAS and instances usage. +# It will be requested when booting the application server! +# https://docs.payara.fish/community/docs/Technical%20Documentation/Payara%20Server%20Documentation/Security%20Guide/Administering%20System%20Security.html#to-change-the-master-password +if [ "$DOMAIN_PASSWORD" != "changeit" ]; then + PASSWORD_FILE=$(mktemp) + echo "AS_ADMIN_MASTERPASSWORD=changeit" >> "$PASSWORD_FILE" + echo "AS_ADMIN_NEWMASTERPASSWORD=${DOMAIN_PASSWORD}" >> "$PASSWORD_FILE" + asadmin --user="${PAYARA_ADMIN_USER}" --passwordfile="$PASSWORD_FILE" change-master-password --savemasterpassword false "${DOMAIN_NAME}" + rm "$PASSWORD_FILE" +else + echo "IMPORTANT: THIS CONTAINER USES THE DEFAULT DOMAIN \"MASTER\" PASSWORD! ('changeit')" + echo " To change the password, set the DOMAIN_PASSWORD env var." +fi diff --git a/modules/container-base/src/main/docker/scripts/init_1_generate_deploy_commands.sh b/modules/container-base/src/main/docker/scripts/init_1_generate_deploy_commands.sh index 8729f78e466..622ea82d6f6 100644 --- a/modules/container-base/src/main/docker/scripts/init_1_generate_deploy_commands.sh +++ b/modules/container-base/src/main/docker/scripts/init_1_generate_deploy_commands.sh @@ -31,14 +31,15 @@ # ########################################################################################################## +set -euo pipefail + # Check required variables are set if [ -z "$DEPLOY_DIR" ]; then echo "Variable DEPLOY_DIR is not set."; exit 1; fi -if [ -z "$PREBOOT_COMMANDS" ]; then echo "Variable PREBOOT_COMMANDS is not set."; exit 1; fi -if [ -z "$POSTBOOT_COMMANDS" ]; then echo "Variable POSTBOOT_COMMANDS is not set."; exit 1; fi - -# Create pre and post boot command files if they don't exist -touch "$POSTBOOT_COMMANDS" -touch "$PREBOOT_COMMANDS" +if [ -z "$PREBOOT_COMMANDS_FILE" ]; then echo "Variable PREBOOT_COMMANDS_FILE is not set."; exit 1; fi +if [ -z "$POSTBOOT_COMMANDS_FILE" ]; then echo "Variable POSTBOOT_COMMANDS_FILE is not set."; exit 1; fi +# Test if files are writeable for us, exit otherwise +touch "$PREBOOT_COMMANDS_FILE" || exit 1 +touch "$POSTBOOT_COMMANDS_FILE" || exit 1 deploy() { @@ -48,11 +49,15 @@ deploy() { fi DEPLOY_STATEMENT="deploy $DEPLOY_PROPS $1" - if grep -q "$1" "$POSTBOOT_COMMANDS"; then - echo "post boot commands already deploys $1"; + if grep -q "$1" "$POSTBOOT_COMMANDS_FILE"; then + echo "Post boot commands already deploys $1, skip adding"; else - echo "Adding deployment target $1 to post boot commands"; - echo "$DEPLOY_STATEMENT" >> "$POSTBOOT_COMMANDS"; + if [ -n "$SKIP_DEPLOY" ] && { [ "$SKIP_DEPLOY" = "1" ] || [ "$SKIP_DEPLOY" = "true" ]; }; then + echo "Skipping deployment of $1 as requested."; + else + echo "Adding deployment target $1 to post boot commands"; + echo "$DEPLOY_STATEMENT" >> "$POSTBOOT_COMMANDS_FILE"; + fi fi } diff --git a/modules/container-base/src/main/docker/scripts/init_1_generate_devmode_commands.sh b/modules/container-base/src/main/docker/scripts/init_1_generate_devmode_commands.sh index bb0984332f7..608113d1cf7 100644 --- a/modules/container-base/src/main/docker/scripts/init_1_generate_devmode_commands.sh +++ b/modules/container-base/src/main/docker/scripts/init_1_generate_devmode_commands.sh @@ -11,39 +11,49 @@ set -euo pipefail # for the parent shell before executing Payara. ###### ###### ###### ###### ###### ###### ###### ###### ###### ###### ###### +if [ -z "$PREBOOT_COMMANDS_FILE" ]; then echo "Variable PREBOOT_COMMANDS_FILE is not set."; exit 1; fi +# Test if preboot file is writeable for us, exit otherwise +touch "$PREBOOT_COMMANDS_FILE" || exit 1 + # 0. Init variables ENABLE_JMX=${ENABLE_JMX:-0} ENABLE_JDWP=${ENABLE_JDWP:-0} ENABLE_RELOAD=${ENABLE_RELOAD:-0} -DV_PREBOOT=${PAYARA_DIR}/dataverse_preboot -echo "# Dataverse preboot configuration for Payara" > "${DV_PREBOOT}" +function inject() { + if [ -z "$1" ]; then echo "No line specified"; exit 1; fi + # If the line is not yet in the file, try to add it + if ! grep -q "$1" "$PREBOOT_COMMANDS_FILE"; then + # Check if the line is still not in the file when splitting at the first = + if ! grep -q "$(echo "$1" | cut -f1 -d"=")" "$PREBOOT_COMMANDS_FILE"; then + echo "$1" >> "$PREBOOT_COMMANDS_FILE" + fi + fi +} # 1. Configure JMX (enabled by default on port 8686, but requires SSL) # See also https://blog.payara.fish/monitoring-payara-server-with-jconsole # To still use it, you can use a sidecar container proxying or using JMX via localhost without SSL. if [ "${ENABLE_JMX}" = "1" ]; then echo "Enabling unsecured JMX on 0.0.0.0:8686, enabling AMX and tuning monitoring levels to HIGH. You'll need a sidecar for this, as access is allowed from same machine only (without SSL)." - { \ - echo "set configs.config.server-config.amx-configuration.enabled=true" - echo "set configs.config.server-config.monitoring-service.module-monitoring-levels.jvm=HIGH" - echo "set configs.config.server-config.monitoring-service.module-monitoring-levels.connector-service=HIGH" - echo "set configs.config.server-config.monitoring-service.module-monitoring-levels.connector-connection-pool=HIGH" - echo "set configs.config.server-config.monitoring-service.module-monitoring-levels.jdbc-connection-pool=HIGH" - echo "set configs.config.server-config.monitoring-service.module-monitoring-levels.web-services-container=HIGH" - echo "set configs.config.server-config.monitoring-service.module-monitoring-levels.ejb-container=HIGH" - echo "set configs.config.server-config.monitoring-service.module-monitoring-levels.thread-pool=HIGH" - echo "set configs.config.server-config.monitoring-service.module-monitoring-levels.http-service=HIGH" - echo "set configs.config.server-config.monitoring-service.module-monitoring-levels.security=HIGH" - echo "set configs.config.server-config.monitoring-service.module-monitoring-levels.jms-service=HIGH" - echo "set configs.config.server-config.monitoring-service.module-monitoring-levels.jersey=HIGH" - echo "set configs.config.server-config.monitoring-service.module-monitoring-levels.transaction-service=HIGH" - echo "set configs.config.server-config.monitoring-service.module-monitoring-levels.jpa=HIGH" - echo "set configs.config.server-config.monitoring-service.module-monitoring-levels.web-container=HIGH" - echo "set configs.config.server-config.monitoring-service.module-monitoring-levels.orb=HIGH" - echo "set configs.config.server-config.monitoring-service.module-monitoring-levels.deployment=HIGH" - echo "set configs.config.server-config.admin-service.jmx-connector.system.security-enabled=false" - } >> "${DV_PREBOOT}" + inject "set configs.config.server-config.amx-configuration.enabled=true" + inject "set configs.config.server-config.monitoring-service.module-monitoring-levels.jvm=HIGH" + inject "set configs.config.server-config.monitoring-service.module-monitoring-levels.connector-service=HIGH" + inject "set configs.config.server-config.monitoring-service.module-monitoring-levels.connector-connection-pool=HIGH" + inject "set configs.config.server-config.monitoring-service.module-monitoring-levels.jdbc-connection-pool=HIGH" + inject "set configs.config.server-config.monitoring-service.module-monitoring-levels.web-services-container=HIGH" + inject "set configs.config.server-config.monitoring-service.module-monitoring-levels.ejb-container=HIGH" + inject "set configs.config.server-config.monitoring-service.module-monitoring-levels.thread-pool=HIGH" + inject "set configs.config.server-config.monitoring-service.module-monitoring-levels.http-service=HIGH" + inject "set configs.config.server-config.monitoring-service.module-monitoring-levels.security=HIGH" + inject "set configs.config.server-config.monitoring-service.module-monitoring-levels.jms-service=HIGH" + inject "set configs.config.server-config.monitoring-service.module-monitoring-levels.jersey=HIGH" + inject "set configs.config.server-config.monitoring-service.module-monitoring-levels.transaction-service=HIGH" + inject "set configs.config.server-config.monitoring-service.module-monitoring-levels.jpa=HIGH" + inject "set configs.config.server-config.monitoring-service.module-monitoring-levels.web-container=HIGH" + inject "set configs.config.server-config.monitoring-service.module-monitoring-levels.orb=HIGH" + inject "set configs.config.server-config.monitoring-service.module-monitoring-levels.deployment=HIGH" + inject "set configs.config.server-config.admin-service.jmx-connector.system.security-enabled=false" fi # 2. Enable JDWP via debugging switch @@ -55,14 +65,12 @@ fi # 3. Enable hot reload if [ "${ENABLE_RELOAD}" = "1" ]; then echo "Enabling hot reload of deployments." - echo "set configs.config.server-config.admin-service.das-config.dynamic-reload-enabled=true" >> "${DV_PREBOOT}" + inject "set configs.config.server-config.admin-service.das-config.dynamic-reload-enabled=true" + inject "set configs.config.server-config.admin-service.das-config.autodeploy-enabled=true" fi # 4. Add the commands to the existing preboot file, but insert BEFORE deployment -TMP_PREBOOT=$(mktemp) -cat "${DV_PREBOOT}" "${PREBOOT_COMMANDS}" > "${TMP_PREBOOT}" -mv "${TMP_PREBOOT}" "${PREBOOT_COMMANDS}" -echo "DEBUG: preboot contains the following commands:" +echo "DEBUG: preboot contains now the following commands:" +echo "--------------------------------------------------" +cat "${PREBOOT_COMMANDS_FILE}" echo "--------------------------------------------------" -cat "${PREBOOT_COMMANDS}" -echo "--------------------------------------------------" \ No newline at end of file diff --git a/modules/container-base/src/main/docker/scripts/startInForeground.sh b/modules/container-base/src/main/docker/scripts/startInForeground.sh index 4843f6ae055..fa7d533b0d1 100644 --- a/modules/container-base/src/main/docker/scripts/startInForeground.sh +++ b/modules/container-base/src/main/docker/scripts/startInForeground.sh @@ -32,10 +32,11 @@ ########################################################################################################## # Check required variables are set -if [ -z "$ADMIN_USER" ]; then echo "Variable ADMIN_USER is not set."; exit 1; fi -if [ -z "$PASSWORD_FILE" ]; then echo "Variable PASSWORD_FILE is not set."; exit 1; fi -if [ -z "$PREBOOT_COMMANDS" ]; then echo "Variable PREBOOT_COMMANDS is not set."; exit 1; fi -if [ -z "$POSTBOOT_COMMANDS" ]; then echo "Variable POSTBOOT_COMMANDS is not set."; exit 1; fi +if [ -z "$PAYARA_ADMIN_USER" ]; then echo "Variable ADMIN_USER is not set."; exit 1; fi +if [ -z "$PAYARA_ADMIN_PASSWORD" ]; then echo "Variable ADMIN_PASSWORD is not set."; exit 1; fi +if [ -z "$DOMAIN_PASSWORD" ]; then echo "Variable DOMAIN_PASSWORD is not set."; exit 1; fi +if [ -z "$PREBOOT_COMMANDS_FILE" ]; then echo "Variable PREBOOT_COMMANDS_FILE is not set."; exit 1; fi +if [ -z "$POSTBOOT_COMMANDS_FILE" ]; then echo "Variable POSTBOOT_COMMANDS_FILE is not set."; exit 1; fi if [ -z "$DOMAIN_NAME" ]; then echo "Variable DOMAIN_NAME is not set."; exit 1; fi # Check if dumps are enabled - add arg to JVM_ARGS in this case @@ -43,6 +44,13 @@ if [ -n "${ENABLE_DUMPS}" ] && [ "${ENABLE_DUMPS}" = "1" ]; then JVM_ARGS="${JVM_DUMPS_ARG} ${JVM_ARGS}" fi +# For safety reasons, do no longer expose the passwords - malicious code could extract it! +# (We need to save the master password for booting the server though) +MASTER_PASSWORD="${DOMAIN_PASSWORD}" +export LINUX_PASSWORD="have-some-scrambled-eggs" +export PAYARA_ADMIN_PASSWORD="have-some-scrambled-eggs" +export DOMAIN_PASSWORD="have-some-scrambled-eggs" + # The following command gets the command line to be executed by start-domain # - print the command line to the server with --dry-run, each argument on a separate line # - remove -read-string argument @@ -50,19 +58,25 @@ fi # - remove lines before and after the command line and squash commands on a single line # Create pre and post boot command files if they don't exist -touch "$POSTBOOT_COMMANDS" -touch "$PREBOOT_COMMANDS" +touch "$POSTBOOT_COMMANDS_FILE" || exit 1 +touch "$PREBOOT_COMMANDS_FILE" || exit 1 +# This workaround is necessary due to limitations of asadmin +PASSWORD_FILE=$(mktemp) +echo "AS_ADMIN_MASTERPASSWORD=$MASTER_PASSWORD" > "$PASSWORD_FILE" # shellcheck disable=SC2068 # -- Using $@ is necessary here as asadmin cannot deal with options enclosed in ""! -OUTPUT=$("${PAYARA_DIR}"/bin/asadmin --user="${ADMIN_USER}" --passwordfile="${PASSWORD_FILE}" start-domain --dry-run --prebootcommandfile="${PREBOOT_COMMANDS}" --postbootcommandfile="${POSTBOOT_COMMANDS}" $@ "$DOMAIN_NAME") +OUTPUT=$("${PAYARA_DIR}"/bin/asadmin --user="${PAYARA_ADMIN_USER}" --passwordfile="$PASSWORD_FILE" start-domain --dry-run --prebootcommandfile="${PREBOOT_COMMANDS_FILE}" --postbootcommandfile="${POSTBOOT_COMMANDS_FILE}" $@ "$DOMAIN_NAME") STATUS=$? +rm "$PASSWORD_FILE" if [ "$STATUS" -ne 0 ] then echo ERROR: "$OUTPUT" >&2 exit 1 fi +echo "Booting now..." + COMMAND=$(echo "$OUTPUT"\ | sed -n -e '2,/^$/p'\ | sed "s|glassfish.jar|glassfish.jar $JVM_ARGS |g") @@ -72,18 +86,6 @@ echo "$COMMAND" | tr ' ' '\n' echo # Run the server in foreground - read master password from variable or file or use the default "changeit" password - -set +x -if test "$AS_ADMIN_MASTERPASSWORD"x = x -a -f "$PASSWORD_FILE" - then - # shellcheck disable=SC1090 - source "$PASSWORD_FILE" -fi -if test "$AS_ADMIN_MASTERPASSWORD"x = x - then - AS_ADMIN_MASTERPASSWORD=changeit -fi -echo "AS_ADMIN_MASTERPASSWORD=$AS_ADMIN_MASTERPASSWORD" > /tmp/masterpwdfile # shellcheck disable=SC2086 # -- Unquoted exec var is necessary, as otherwise things get escaped that may not be escaped (parameters for Java) -exec ${COMMAND} < /tmp/masterpwdfile +exec ${COMMAND} < <(echo "AS_ADMIN_MASTERPASSWORD=$MASTER_PASSWORD") diff --git a/modules/container-configbaker/Dockerfile b/modules/container-configbaker/Dockerfile index 9b98334d72b..351425a17ba 100644 --- a/modules/container-configbaker/Dockerfile +++ b/modules/container-configbaker/Dockerfile @@ -10,7 +10,10 @@ ARG SOLR_VERSION FROM solr:${SOLR_VERSION} AS solr # Let's build us a baker -FROM alpine:3 +# WARNING: +# Do not upgrade the tag to :3 or :3.19 until https://pkgs.alpinelinux.org/package/v3.19/main/x86_64/c-ares is at v1.26.0+! +# See https://github.com/IQSS/dataverse/issues/10413 for more information. +FROM alpine:3.18 ENV SCRIPT_DIR="/scripts" \ SECRETS_DIR="/secrets" \ @@ -18,7 +21,7 @@ ENV SCRIPT_DIR="/scripts" \ ENV PATH="${PATH}:${SCRIPT_DIR}" \ BOOTSTRAP_DIR="${SCRIPT_DIR}/bootstrap" -ARG APK_PACKAGES="curl bind-tools netcat-openbsd jq bash dumb-init wait4x ed" +ARG APK_PACKAGES="curl bind-tools netcat-openbsd jq bash dumb-init wait4x ed postgresql-client aws-cli" RUN true && \ # Install necessary software and tools diff --git a/modules/container-configbaker/assembly.xml b/modules/container-configbaker/assembly.xml index 3285eef510a..3c55e6d64b6 100644 --- a/modules/container-configbaker/assembly.xml +++ b/modules/container-configbaker/assembly.xml @@ -8,7 +8,7 @@ - conf/solr/9.3.0 + conf/solr solr diff --git a/modules/container-configbaker/scripts/bootstrap/demo/init.sh b/modules/container-configbaker/scripts/bootstrap/demo/init.sh new file mode 100644 index 00000000000..e8d1d07dd2d --- /dev/null +++ b/modules/container-configbaker/scripts/bootstrap/demo/init.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +set -euo pipefail + +# Set some defaults +DATAVERSE_URL=${DATAVERSE_URL:-"http://dataverse:8080"} +export DATAVERSE_URL + +BLOCKED_API_KEY=${BLOCKED_API_KEY:-"unblockme"} +export BLOCKED_API_KEY + +# --insecure is used so we can configure a few things but +# later in this script we'll apply the changes as if we had +# run the script without --insecure. +echo "Running base setup-all.sh..." +"${BOOTSTRAP_DIR}"/base/setup-all.sh --insecure -p=admin1 | tee /tmp/setup-all.sh.out + +echo "" +echo "Setting DOI provider to \"FAKE\"..." +curl -sS -X PUT -d FAKE "${DATAVERSE_URL}/api/admin/settings/:DoiProvider" + +echo "" +echo "Revoke the key that allows for creation of builtin users..." +curl -sS -X DELETE "${DATAVERSE_URL}/api/admin/settings/BuiltinUsers.KEY" + +echo "" +echo "Set key for accessing blocked API endpoints..." +curl -sS -X PUT -d "$BLOCKED_API_KEY" "${DATAVERSE_URL}/api/admin/settings/:BlockedApiKey" + +echo "" +echo "Set policy to only allow access to admin APIs with with a key..." +curl -sS -X PUT -d unblock-key "${DATAVERSE_URL}/api/admin/settings/:BlockedApiPolicy" + +echo "" +echo "Block admin and other sensitive API endpoints..." +curl -sS -X PUT -d 'admin,builtin-users' "${DATAVERSE_URL}/api/admin/settings/:BlockedApiEndpoints" + +echo "" +echo "Done, your instance has been configured for demo or eval. Have a nice day!" diff --git a/modules/container-configbaker/scripts/bootstrap/dev/init.sh b/modules/container-configbaker/scripts/bootstrap/dev/init.sh index efdaee3d0c3..f8770436652 100644 --- a/modules/container-configbaker/scripts/bootstrap/dev/init.sh +++ b/modules/container-configbaker/scripts/bootstrap/dev/init.sh @@ -9,9 +9,6 @@ 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 diff --git a/modules/dataverse-parent/pom.xml b/modules/dataverse-parent/pom.xml index 386d4934cb1..5abf2763128 100644 --- a/modules/dataverse-parent/pom.xml +++ b/modules/dataverse-parent/pom.xml @@ -131,7 +131,7 @@ - 6.1 + 6.4 17 UTF-8 @@ -148,19 +148,18 @@ -Duser.timezone=${project.timezone} -Dfile.encoding=${project.build.sourceEncoding} -Duser.language=${project.language} -Duser.region=${project.region} - 6.2023.8 - 42.6.0 - 9.3.0 - 1.12.290 + 6.2024.6 + 42.7.2 + 9.4.1 + 1.12.748 26.30.0 - 8.0.0 1.7.35 - 2.11.0 + 2.15.1 1.2 3.12.0 - 1.21 + 1.26.0 4.5.13 4.4.14 @@ -168,11 +167,11 @@ 5.2.0 - 1.19.0 - 2.10.1 - 5.10.0 - 5.4.0 - 0.8.10 + 1.19.7 + 3.7.1 + 5.10.2 + 5.11.0 + 0.8.11 9.3 @@ -182,8 +181,8 @@ 3.3.2 3.5.0 3.1.1 - 3.1.0 - 3.1.0 + 3.2.5 + 3.2.5 3.6.0 3.3.1 3.0.0-M7 @@ -199,7 +198,7 @@ 1.7.0 - 0.43.4 + 0.45.0 @@ -386,18 +385,6 @@ false - - - payara-patched-externals - Payara Patched Externals - https://raw.github.com/payara/Payara_PatchedProjects/master - - true - - - false - - central-repo Central Repository @@ -454,10 +441,13 @@ ct - - + ${parsedVersion.majorVersion}.${parsedVersion.nextMinorVersion} + @@ -483,6 +473,21 @@ false + + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.0 + + + parse-version + + parse-version + + initialize + + + diff --git a/modules/nginx/Dockerfile b/modules/nginx/Dockerfile deleted file mode 100644 index 3900076599f..00000000000 --- a/modules/nginx/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM nginx:latest - -# Remove the default NGINX configuration file -RUN rm /etc/nginx/conf.d/default.conf - -# Copy the contents of the local default.conf to the container -COPY default.conf /etc/nginx/conf.d/ - -EXPOSE 4849 \ No newline at end of file diff --git a/modules/nginx/README.md b/modules/nginx/README.md deleted file mode 100644 index 9d2ff785577..00000000000 --- a/modules/nginx/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# nginx proxy - -nginx can be used to proxy various services at other ports/protocols from docker. - -Currently, this is used to work around a problem with the IntelliJ Payara plugin, which doesn't allow remote redeployment in case the Payara admin is served via HTTPS using a self-signed certificate, which is the case of the default dataverse container installation. This configuration provides an HTTP endpoint at port 4849, and proxies requests to the Payara admin console's HTTPS 4848 endpoint. From the IntelliJ Payara plugin one has to specify the localhost 4849 port (without SSL). - -![img.png](img.png) diff --git a/modules/nginx/default.conf b/modules/nginx/default.conf deleted file mode 100644 index 8381a66c19a..00000000000 --- a/modules/nginx/default.conf +++ /dev/null @@ -1,12 +0,0 @@ -server { - listen 4849; - - # Make it big, so that .war files can be submitted - client_max_body_size 300M; - - location / { - proxy_pass https://dataverse:4848; - proxy_ssl_verify off; - proxy_ssl_server_name on; - } -} diff --git a/pom.xml b/pom.xml index b04d8a9e10a..b7c8d79fe99 100644 --- a/pom.xml +++ b/pom.xml @@ -32,6 +32,12 @@ 5.2.1 2.9.1 5.5.3 + + Dataverse API + ${project.version} + Open source research data repository software. + + ${project.build.outputDirectory}/META-INF org.apache.james @@ -68,7 +85,7 @@ In case the dependency is both transitive and direct (e. g. some common lib for logging), manage the version above and add the direct dependency here WITHOUT version tag, too. --> - + - ${payara.version} @@ -221,6 +245,18 @@ provided + + + jakarta.json.bind + jakarta.json.bind-api + + + + org.eclipse + yasson + test + + org.glassfish @@ -294,7 +330,7 @@ org.apache.solr solr-solrj - 9.3.0 + 9.4.1 colt @@ -525,6 +561,11 @@ opennlp-tools 1.9.1 + + org.xmlunit + xmlunit-core + 2.9.1 + com.google.cloud google-cloud-storage @@ -543,6 +584,12 @@ java-json-canonicalization 1.1 + + + io.gdcc + sitemapgen4j + 2.1.2 + edu.ucar cdm-core @@ -553,6 +600,10 @@ dataverse-spi 2.0.0 + + javax.cache + cache-api + org.junit.jupiter @@ -664,6 +715,11 @@ 3.9.0 test + + com.hazelcast + hazelcast + test + @@ -693,6 +749,7 @@ **/firstNames/*.* **/*.xsl **/services/* + **/*.map @@ -732,6 +789,7 @@ true false + ${project.build.directory}/${project.artifactId} true @@ -896,6 +954,30 @@ true + + io.smallrye + smallrye-open-api-maven-plugin + 3.10.0 + + + + generate-schema + + + process-classes + + ${openapi.outputDirectory} + openapi + ${openapi.infoTitle} + ${openapi.infoVersion} + ${openapi.infoDescription} + CLASS_METHOD + edu.harvard.iq.dataverse + true + + + + @@ -926,8 +1008,12 @@ gdcc/dataverse:${app.image.tag} unstable + false gdcc/base:${base.image.tag} - unstable + + noble + + ${base.image.version}-${base.image.flavor}-p${payara.version}-j${target.java.version} gdcc/configbaker:${conf.image.tag} ${app.image.tag} @@ -938,6 +1024,7 @@ ${postgresql.server.version} ${solr.version} dataverse + ${app.skipDeploy} @@ -1023,14 +1110,6 @@ true - - - gdcc/dev_nginx:unstable - - ${project.basedir}/modules/nginx - - - true 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 1118ed98a03..151c4732ad7 100644 --- a/scripts/api/data/dataset-create-new-all-default-fields.json +++ b/scripts/api/data/dataset-create-new-all-default-fields.json @@ -231,6 +231,12 @@ "typeClass": "primitive", "value": "KeywordTerm1" }, + "keywordTermURI": { + "typeName": "keywordTermURI", + "multiple": false, + "typeClass": "primitive", + "value": "http://keywordTermURI1.org" + }, "keywordVocabulary": { "typeName": "keywordVocabulary", "multiple": false, @@ -251,6 +257,12 @@ "typeClass": "primitive", "value": "KeywordTerm2" }, + "keywordTermURI": { + "typeName": "keywordTermURI", + "multiple": false, + "typeClass": "primitive", + "value": "http://keywordTermURI2.org" + }, "keywordVocabulary": { "typeName": "keywordVocabulary", "multiple": false, @@ -319,6 +331,12 @@ "typeClass": "compound", "value": [ { + "publicationRelationType" : { + "typeName" : "publicationRelationType", + "multiple" : false, + "typeClass" : "controlledVocabulary", + "value" : "IsSupplementTo" + }, "publicationCitation": { "typeName": "publicationCitation", "multiple": false, @@ -907,14 +925,14 @@ "typeClass": "primitive", "value": "-70" }, - "northLongitude": { - "typeName": "northLongitude", + "northLatitude": { + "typeName": "northLatitude", "multiple": false, "typeClass": "primitive", "value": "43" }, - "southLongitude": { - "typeName": "southLongitude", + "southLatitude": { + "typeName": "southLatitude", "multiple": false, "typeClass": "primitive", "value": "42" @@ -933,14 +951,14 @@ "typeClass": "primitive", "value": "-13" }, - "northLongitude": { - "typeName": "northLongitude", + "northLatitude": { + "typeName": "northLatitude", "multiple": false, "typeClass": "primitive", "value": "29" }, - "southLongitude": { - "typeName": "southLongitude", + "southLatitude": { + "typeName": "southLatitude", "multiple": false, "typeClass": "primitive", "value": "28" diff --git a/scripts/api/data/licenses/licenseApache-2.0.json b/scripts/api/data/licenses/licenseApache-2.0.json new file mode 100644 index 00000000000..5b7c3cf5c95 --- /dev/null +++ b/scripts/api/data/licenses/licenseApache-2.0.json @@ -0,0 +1,8 @@ +{ + "name": "Apache-2.0", + "uri": "http://www.apache.org/licenses/LICENSE-2.0", + "shortDescription": "Apache License 2.0", + "active": true, + "sortOrder": 9 + } + \ No newline at end of file diff --git a/scripts/api/data/licenses/licenseMIT.json b/scripts/api/data/licenses/licenseMIT.json new file mode 100644 index 00000000000..a879e8a5595 --- /dev/null +++ b/scripts/api/data/licenses/licenseMIT.json @@ -0,0 +1,7 @@ +{ + "name": "MIT", + "uri": "https://opensource.org/licenses/MIT", + "shortDescription": "MIT License", + "active": true, + "sortOrder": 8 +} diff --git a/scripts/api/data/metadatablocks/astrophysics.tsv b/scripts/api/data/metadatablocks/astrophysics.tsv index 4039d32cb75..92792d404c9 100644 --- a/scripts/api/data/metadatablocks/astrophysics.tsv +++ b/scripts/api/data/metadatablocks/astrophysics.tsv @@ -2,13 +2,13 @@ astrophysics Astronomy and Astrophysics Metadata #datasetField name title description watermark fieldType displayOrder displayFormat advancedSearchField allowControlledVocabulary allowmultiples facetable displayoncreate required parent metadatablock_id astroType Type The nature or genre of the content of the files in the dataset. text 0 TRUE TRUE TRUE TRUE FALSE FALSE astrophysics - astroFacility Facility The observatory or facility where the data was obtained. text 1 TRUE TRUE TRUE TRUE FALSE FALSE astrophysics - astroInstrument Instrument The instrument used to collect the data. text 2 TRUE TRUE TRUE TRUE FALSE FALSE astrophysics + astroFacility Facility The observatory or facility where the data was obtained. text 1 TRUE FALSE TRUE TRUE FALSE FALSE astrophysics + astroInstrument Instrument The instrument used to collect the data. text 2 TRUE FALSE TRUE TRUE FALSE FALSE astrophysics astroObject Object Astronomical Objects represented in the data (Given as SIMBAD recognizable names preferred). text 3 TRUE FALSE TRUE TRUE FALSE FALSE astrophysics resolution.Spatial Spatial Resolution The spatial (angular) resolution that is typical of the observations, in decimal degrees. text 4 TRUE FALSE FALSE TRUE FALSE FALSE astrophysics resolution.Spectral Spectral Resolution The spectral resolution that is typical of the observations, given as the ratio \u03bb/\u0394\u03bb. text 5 TRUE FALSE FALSE TRUE FALSE FALSE astrophysics resolution.Temporal Time Resolution The temporal resolution that is typical of the observations, given in seconds. text 6 FALSE FALSE FALSE FALSE FALSE FALSE astrophysics - coverage.Spectral.Bandpass Bandpass Conventional bandpass name text 7 TRUE TRUE TRUE TRUE FALSE FALSE astrophysics + coverage.Spectral.Bandpass Bandpass Conventional bandpass name text 7 TRUE FALSE TRUE TRUE FALSE FALSE astrophysics coverage.Spectral.CentralWavelength Central Wavelength (m) The central wavelength of the spectral bandpass, in meters. Enter a floating-point number. float 8 TRUE FALSE TRUE TRUE FALSE FALSE astrophysics coverage.Spectral.Wavelength Wavelength Range The minimum and maximum wavelength of the spectral bandpass. Enter a floating-point number. none 9 FALSE FALSE TRUE FALSE FALSE FALSE astrophysics coverage.Spectral.MinimumWavelength Minimum (m) The minimum wavelength of the spectral bandpass, in meters. Enter a floating-point number. float 10 TRUE FALSE FALSE TRUE FALSE FALSE coverage.Spectral.Wavelength astrophysics diff --git a/scripts/api/data/metadatablocks/biomedical.tsv b/scripts/api/data/metadatablocks/biomedical.tsv index 28d59130c34..06f1ebec1b4 100644 --- a/scripts/api/data/metadatablocks/biomedical.tsv +++ b/scripts/api/data/metadatablocks/biomedical.tsv @@ -13,7 +13,7 @@ studyAssayOtherTechnologyType Other Technology Type If Other was selected in Technology Type, list any other technology types that were used in this Dataset. text 9 TRUE FALSE TRUE TRUE FALSE FALSE biomedical studyAssayPlatform Technology Platform The manufacturer and name of the technology platform used in the assay (e.g. Bruker AVANCE). text 10 TRUE TRUE TRUE TRUE FALSE FALSE biomedical studyAssayOtherPlatform Other Technology Platform If Other was selected in Technology Platform, list any other technology platforms that were used in this Dataset. text 11 TRUE FALSE TRUE TRUE FALSE FALSE biomedical - studyAssayCellType Cell Type The name of the cell line from which the source or sample derives. text 12 TRUE TRUE TRUE TRUE FALSE FALSE biomedical + studyAssayCellType Cell Type The name of the cell line from which the source or sample derives. text 12 TRUE FALSE TRUE TRUE FALSE FALSE biomedical #controlledVocabulary DatasetField Value identifier displayOrder studyDesignType Case Control EFO_0001427 0 studyDesignType Cross Sectional EFO_0001428 1 @@ -45,6 +45,7 @@ studyFactorType Treatment Compound EFO_0000369 17 studyFactorType Treatment Type EFO_0000727 18 studyFactorType Other OTHER_FACTOR 19 + studyAssayMeasurementType cell counting ERO_0001899 0 studyAssayMeasurementType cell sorting CHMO_0001085 1 studyAssayMeasurementType clinical chemistry analysis OBI_0000520 2 studyAssayMeasurementType copy number variation profiling OBI_0000537 3 diff --git a/scripts/api/data/metadatablocks/citation.tsv b/scripts/api/data/metadatablocks/citation.tsv index b21b6bcce57..abc09465603 100644 --- a/scripts/api/data/metadatablocks/citation.tsv +++ b/scripts/api/data/metadatablocks/citation.tsv @@ -23,62 +23,64 @@ subject Subject The area of study relevant to the Dataset text 19 TRUE TRUE TRUE TRUE TRUE TRUE citation http://purl.org/dc/terms/subject keyword Keyword A key term that describes an important aspect of the Dataset and information about any controlled vocabulary used none 20 FALSE FALSE TRUE FALSE TRUE FALSE citation keywordValue Term A key term that describes important aspects of the Dataset text 21 #VALUE TRUE FALSE FALSE TRUE TRUE FALSE keyword citation - keywordVocabulary Controlled Vocabulary Name The controlled vocabulary used for the keyword term (e.g. LCSH, MeSH) text 22 (#VALUE) FALSE FALSE FALSE FALSE TRUE FALSE keyword citation - keywordVocabularyURI Controlled Vocabulary URL The URL where one can access information about the term's controlled vocabulary https:// url 23 #VALUE FALSE FALSE FALSE FALSE TRUE FALSE keyword citation - topicClassification Topic Classification Indicates a broad, important topic or subject that the Dataset covers and information about any controlled vocabulary used none 24 FALSE FALSE TRUE FALSE FALSE FALSE citation - topicClassValue Term A topic or subject term text 25 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE topicClassification citation - topicClassVocab Controlled Vocabulary Name The controlled vocabulary used for the keyword term (e.g. LCSH, MeSH) text 26 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE topicClassification citation - topicClassVocabURI Controlled Vocabulary URL The URL where one can access information about the term's controlled vocabulary https:// url 27 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE topicClassification citation - publication Related Publication The article or report that uses the data in the Dataset. The full list of related publications will be displayed on the metadata tab none 28 FALSE FALSE TRUE FALSE TRUE FALSE citation http://purl.org/dc/terms/isReferencedBy - publicationCitation Citation The full bibliographic citation for the related publication textbox 29 #VALUE TRUE FALSE FALSE FALSE TRUE FALSE publication citation http://purl.org/dc/terms/bibliographicCitation - publicationIDType Identifier Type The type of identifier that uniquely identifies a related publication text 30 #VALUE: TRUE TRUE FALSE FALSE TRUE FALSE publication citation http://purl.org/spar/datacite/ResourceIdentifierScheme - publicationIDNumber Identifier The identifier for a related publication text 31 #VALUE TRUE FALSE FALSE FALSE TRUE FALSE publication citation http://purl.org/spar/datacite/ResourceIdentifier - publicationURL URL The URL form of the identifier entered in the Identifier field, e.g. the DOI URL if a DOI was entered in the Identifier field. Used to display what was entered in the ID Type and ID Number fields as a link. If what was entered in the Identifier field has no URL form, the URL of the publication webpage is used, e.g. a journal article webpage https:// url 32 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE publication citation https://schema.org/distribution - notesText Notes Additional information about the Dataset textbox 33 FALSE FALSE FALSE FALSE TRUE FALSE citation - language Language A language that the Dataset's files is written in text 34 TRUE TRUE TRUE TRUE FALSE FALSE citation http://purl.org/dc/terms/language - producer Producer The entity, such a person or organization, managing the finances or other administrative processes involved in the creation of the Dataset none 35 FALSE FALSE TRUE FALSE FALSE FALSE citation - producerName Name The name of the entity, e.g. the person's name or the name of an organization 1) FamilyName, GivenName or 2) Organization text 36 #VALUE TRUE FALSE FALSE TRUE FALSE TRUE producer citation - producerAffiliation Affiliation The name of the entity affiliated with the producer, e.g. an organization's name Organization XYZ text 37 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE producer citation - producerAbbreviation Abbreviated Name The producer's abbreviated name (e.g. IQSS, ICPSR) text 38 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE producer citation - producerURL URL The URL of the producer's website https:// url 39 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE producer citation - producerLogoURL Logo URL The URL of the producer's logo https:// url 40
FALSE FALSE FALSE FALSE FALSE FALSE producer citation - productionDate Production Date The date when the data were produced (not distributed, published, or archived) YYYY-MM-DD date 41 TRUE FALSE FALSE TRUE FALSE FALSE citation - productionPlace Production Location The location where the data and any related materials were produced or collected text 42 TRUE FALSE TRUE TRUE FALSE FALSE citation - contributor Contributor The entity, such as a person or organization, responsible for collecting, managing, or otherwise contributing to the development of the Dataset none 43 : FALSE FALSE TRUE FALSE FALSE FALSE citation http://purl.org/dc/terms/contributor - contributorType Type Indicates the type of contribution made to the dataset text 44 #VALUE TRUE TRUE FALSE TRUE FALSE FALSE contributor citation - contributorName Name The name of the contributor, e.g. the person's name or the name of an organization 1) FamilyName, GivenName or 2) Organization text 45 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE contributor citation - grantNumber Funding Information Information about the Dataset's financial support none 46 : FALSE FALSE TRUE FALSE FALSE FALSE citation https://schema.org/sponsor - grantNumberAgency Agency The agency that provided financial support for the Dataset Organization XYZ text 47 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE grantNumber citation - grantNumberValue Identifier The grant identifier or contract identifier of the agency that provided financial support for the Dataset text 48 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE grantNumber citation - distributor Distributor The entity, such as a person or organization, designated to generate copies of the Dataset, including any editions or revisions none 49 FALSE FALSE TRUE FALSE FALSE FALSE citation - distributorName Name The name of the entity, e.g. the person's name or the name of an organization 1) FamilyName, GivenName or 2) Organization text 50 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE distributor citation - distributorAffiliation Affiliation The name of the entity affiliated with the distributor, e.g. an organization's name Organization XYZ text 51 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE distributor citation - distributorAbbreviation Abbreviated Name The distributor's abbreviated name (e.g. IQSS, ICPSR) text 52 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE distributor citation - distributorURL URL The URL of the distributor's webpage https:// url 53 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE distributor citation - distributorLogoURL Logo URL The URL of the distributor's logo image, used to show the image on the Dataset's page https:// url 54
FALSE FALSE FALSE FALSE FALSE FALSE distributor citation - distributionDate Distribution Date The date when the Dataset was made available for distribution/presentation YYYY-MM-DD date 55 TRUE FALSE FALSE TRUE FALSE FALSE citation - depositor Depositor The entity, such as a person or organization, that deposited the Dataset in the repository 1) FamilyName, GivenName or 2) Organization text 56 FALSE FALSE FALSE FALSE FALSE FALSE citation - dateOfDeposit Deposit Date The date when the Dataset was deposited into the repository YYYY-MM-DD date 57 FALSE FALSE FALSE TRUE FALSE FALSE citation http://purl.org/dc/terms/dateSubmitted - timePeriodCovered Time Period The time period that the data refer to. Also known as span. This is the time period covered by the data, not the dates of coding, collecting data, or making documents machine-readable none 58 ; FALSE FALSE TRUE FALSE FALSE FALSE citation https://schema.org/temporalCoverage - timePeriodCoveredStart Start Date The start date of the time period that the data refer to YYYY-MM-DD date 59 #NAME: #VALUE TRUE FALSE FALSE TRUE FALSE FALSE timePeriodCovered citation - timePeriodCoveredEnd End Date The end date of the time period that the data refer to YYYY-MM-DD date 60 #NAME: #VALUE TRUE FALSE FALSE TRUE FALSE FALSE timePeriodCovered citation - dateOfCollection Date of Collection The dates when the data were collected or generated none 61 ; FALSE FALSE TRUE FALSE FALSE FALSE citation - 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 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 - softwareName Name The name of software used to generate the Dataset text 69 #VALUE FALSE TRUE FALSE FALSE FALSE FALSE software citation - softwareVersion Version The version of the software used to generate the Dataset, e.g. 4.11 text 70 #NAME: #VALUE FALSE FALSE FALSE FALSE FALSE FALSE software citation - relatedMaterial Related Material Information, such as a persistent ID or citation, about the material related to the Dataset, such as appendices or sampling information available outside of the Dataset textbox 71 FALSE FALSE TRUE FALSE FALSE FALSE citation - relatedDatasets Related Dataset Information, such as a persistent ID or citation, about a related dataset, such as previous research on the Dataset's subject textbox 72 FALSE FALSE TRUE FALSE FALSE FALSE citation http://purl.org/dc/terms/relation - otherReferences Other Reference Information, such as a persistent ID or citation, about another type of resource that provides background or supporting material to the Dataset text 73 FALSE FALSE TRUE FALSE FALSE FALSE citation http://purl.org/dc/terms/references - dataSources Data Source Information, such as a persistent ID or citation, about sources of the Dataset (e.g. a book, article, serial, or machine-readable data file) textbox 74 FALSE FALSE TRUE FALSE FALSE FALSE citation https://www.w3.org/TR/prov-o/#wasDerivedFrom - originOfSources Origin of Historical Sources For historical sources, the origin and any rules followed in establishing them as sources textbox 75 FALSE FALSE FALSE FALSE FALSE FALSE citation - characteristicOfSources Characteristic of Sources Characteristics not already noted elsewhere textbox 76 FALSE FALSE FALSE FALSE FALSE FALSE citation - accessToSources Documentation and Access to Sources 1) Methods or procedures for accessing data sources and 2) any special permissions needed for access textbox 77 FALSE FALSE FALSE FALSE FALSE FALSE citation + keywordTermURI Term URI A URI that points to the web presence of the Keyword Term https:// url 22 #VALUE FALSE FALSE FALSE FALSE TRUE FALSE keyword citation + keywordVocabulary Controlled Vocabulary Name The controlled vocabulary used for the keyword term (e.g. LCSH, MeSH) text 23 (#VALUE) FALSE FALSE FALSE FALSE TRUE FALSE keyword citation + keywordVocabularyURI Controlled Vocabulary URL The URL where one can access information about the term's controlled vocabulary https:// url 24 #VALUE FALSE FALSE FALSE FALSE TRUE FALSE keyword citation + topicClassification Topic Classification Indicates a broad, important topic or subject that the Dataset covers and information about any controlled vocabulary used none 25 FALSE FALSE TRUE FALSE FALSE FALSE citation + topicClassValue Term A topic or subject term text 26 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE topicClassification citation + topicClassVocab Controlled Vocabulary Name The controlled vocabulary used for the keyword term (e.g. LCSH, MeSH) text 27 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE topicClassification citation + topicClassVocabURI Controlled Vocabulary URL The URL where one can access information about the term's controlled vocabulary https:// url 28 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE topicClassification citation + publication Related Publication The article or report that uses the data in the Dataset. The full list of related publications will be displayed on the metadata tab none 29 FALSE FALSE TRUE FALSE TRUE FALSE citation http://purl.org/dc/terms/isReferencedBy + publicationRelationType Relation Type The nature of the relationship between this Dataset and the related publication text 30 #VALUE: TRUE TRUE FALSE FALSE TRUE FALSE publication citation http://datacite.org/schema/kernel-4/simpleTypes#relationType + publicationCitation Citation The full bibliographic citation for the related publication textbox 31 #VALUE TRUE FALSE FALSE FALSE TRUE FALSE publication citation http://purl.org/dc/terms/bibliographicCitation + publicationIDType Identifier Type The type of identifier that uniquely identifies a related publication text 32 #VALUE: TRUE TRUE FALSE FALSE TRUE FALSE publication citation http://purl.org/spar/datacite/ResourceIdentifierScheme + publicationIDNumber Identifier The identifier for a related publication text 33 #VALUE TRUE FALSE FALSE FALSE TRUE FALSE publication citation http://purl.org/spar/datacite/ResourceIdentifier + publicationURL URL The URL form of the identifier entered in the Identifier field, e.g. the DOI URL if a DOI was entered in the Identifier field. Used to display what was entered in the ID Type and ID Number fields as a link. If what was entered in the Identifier field has no URL form, the URL of the publication webpage is used, e.g. a journal article webpage https:// url 34 #VALUE FALSE FALSE FALSE FALSE TRUE FALSE publication citation https://schema.org/distribution + notesText Notes Additional information about the Dataset textbox 35 FALSE FALSE FALSE FALSE TRUE FALSE citation + language Language A language that the Dataset's files is written in text 36 TRUE TRUE TRUE TRUE FALSE FALSE citation http://purl.org/dc/terms/language + producer Producer The entity, such a person or organization, managing the finances or other administrative processes involved in the creation of the Dataset none 37 FALSE FALSE TRUE FALSE FALSE FALSE citation + producerName Name The name of the entity, e.g. the person's name or the name of an organization 1) FamilyName, GivenName or 2) Organization text 38 #VALUE TRUE FALSE FALSE TRUE FALSE TRUE producer citation + producerAffiliation Affiliation The name of the entity affiliated with the producer, e.g. an organization's name Organization XYZ text 39 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE producer citation + producerAbbreviation Abbreviated Name The producer's abbreviated name (e.g. IQSS, ICPSR) text 40 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE producer citation + producerURL URL The URL of the producer's website https:// url 41 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE producer citation + producerLogoURL Logo URL The URL of the producer's logo https:// url 42
FALSE FALSE FALSE FALSE FALSE FALSE producer citation + productionDate Production Date The date when the data were produced (not distributed, published, or archived) YYYY-MM-DD date 43 TRUE FALSE FALSE TRUE FALSE FALSE citation + productionPlace Production Location The location where the data and any related materials were produced or collected text 44 TRUE FALSE TRUE TRUE FALSE FALSE citation + contributor Contributor The entity, such as a person or organization, responsible for collecting, managing, or otherwise contributing to the development of the Dataset none 45 : FALSE FALSE TRUE FALSE FALSE FALSE citation http://purl.org/dc/terms/contributor + contributorType Type Indicates the type of contribution made to the dataset text 46 #VALUE TRUE TRUE FALSE TRUE FALSE FALSE contributor citation + contributorName Name The name of the contributor, e.g. the person's name or the name of an organization 1) FamilyName, GivenName or 2) Organization text 47 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE contributor citation + grantNumber Funding Information Information about the Dataset's financial support none 48 : FALSE FALSE TRUE FALSE FALSE FALSE citation https://schema.org/sponsor + grantNumberAgency Agency The agency that provided financial support for the Dataset Organization XYZ text 49 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE grantNumber citation + grantNumberValue Identifier The grant identifier or contract identifier of the agency that provided financial support for the Dataset text 50 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE grantNumber citation + distributor Distributor The entity, such as a person or organization, designated to generate copies of the Dataset, including any editions or revisions none 51 FALSE FALSE TRUE FALSE FALSE FALSE citation + distributorName Name The name of the entity, e.g. the person's name or the name of an organization 1) FamilyName, GivenName or 2) Organization text 52 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE distributor citation + distributorAffiliation Affiliation The name of the entity affiliated with the distributor, e.g. an organization's name Organization XYZ text 53 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE distributor citation + distributorAbbreviation Abbreviated Name The distributor's abbreviated name (e.g. IQSS, ICPSR) text 54 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE distributor citation + distributorURL URL The URL of the distributor's webpage https:// url 55 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE distributor citation + distributorLogoURL Logo URL The URL of the distributor's logo image, used to show the image on the Dataset's page https:// url 56
FALSE FALSE FALSE FALSE FALSE FALSE distributor citation + distributionDate Distribution Date The date when the Dataset was made available for distribution/presentation YYYY-MM-DD date 57 TRUE FALSE FALSE TRUE FALSE FALSE citation + depositor Depositor The entity, such as a person or organization, that deposited the Dataset in the repository 1) FamilyName, GivenName or 2) Organization text 58 FALSE FALSE FALSE FALSE TRUE FALSE citation + dateOfDeposit Deposit Date The date when the Dataset was deposited into the repository YYYY-MM-DD date 59 FALSE FALSE FALSE TRUE TRUE FALSE citation http://purl.org/dc/terms/dateSubmitted + timePeriodCovered Time Period The time period that the data refer to. Also known as span. This is the time period covered by the data, not the dates of coding, collecting data, or making documents machine-readable none 60 ; FALSE FALSE TRUE FALSE FALSE FALSE citation https://schema.org/temporalCoverage + timePeriodCoveredStart Start Date The start date of the time period that the data refer to YYYY-MM-DD date 61 #NAME: #VALUE TRUE FALSE FALSE TRUE FALSE FALSE timePeriodCovered citation + timePeriodCoveredEnd End Date The end date of the time period that the data refer to YYYY-MM-DD date 62 #NAME: #VALUE TRUE FALSE FALSE TRUE FALSE FALSE timePeriodCovered citation + dateOfCollection Date of Collection The dates when the data were collected or generated none 63 ; FALSE FALSE TRUE FALSE FALSE FALSE citation + dateOfCollectionStart Start Date The date when the data collection started YYYY-MM-DD date 64 #NAME: #VALUE FALSE FALSE FALSE FALSE FALSE FALSE dateOfCollection citation + dateOfCollectionEnd End Date The date when the data collection ended YYYY-MM-DD date 65 #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 66 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 67 : FALSE FALSE TRUE FALSE FALSE FALSE citation + seriesName Name The name of the dataset series text 68 #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 69 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE series citation + software Software Information about the software used to generate the Dataset none 70 , FALSE FALSE TRUE FALSE FALSE FALSE citation https://www.w3.org/TR/prov-o/#wasGeneratedBy + softwareName Name The name of software used to generate the Dataset text 71 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE software citation + softwareVersion Version The version of the software used to generate the Dataset, e.g. 4.11 text 72 #NAME: #VALUE FALSE FALSE FALSE FALSE FALSE FALSE software citation + relatedMaterial Related Material Information, such as a persistent ID or citation, about the material related to the Dataset, such as appendices or sampling information available outside of the Dataset textbox 73 FALSE FALSE TRUE FALSE FALSE FALSE citation + relatedDatasets Related Dataset Information, such as a persistent ID or citation, about a related dataset, such as previous research on the Dataset's subject textbox 74 FALSE FALSE TRUE FALSE FALSE FALSE citation http://purl.org/dc/terms/relation + otherReferences Other Reference Information, such as a persistent ID or citation, about another type of resource that provides background or supporting material to the Dataset text 75 FALSE FALSE TRUE FALSE FALSE FALSE citation http://purl.org/dc/terms/references + dataSources Data Source Information, such as a persistent ID or citation, about sources of the Dataset (e.g. a book, article, serial, or machine-readable data file) textbox 76 FALSE FALSE TRUE FALSE FALSE FALSE citation https://www.w3.org/TR/prov-o/#wasDerivedFrom + originOfSources Origin of Historical Sources For historical sources, the origin and any rules followed in establishing them as sources textbox 77 FALSE FALSE FALSE FALSE FALSE FALSE citation + characteristicOfSources Characteristic of Sources Characteristics not already noted elsewhere textbox 78 FALSE FALSE FALSE FALSE FALSE FALSE citation + accessToSources Documentation and Access to Sources 1) Methods or procedures for accessing data sources and 2) any special permissions needed for access textbox 79 FALSE FALSE FALSE FALSE FALSE FALSE citation #controlledVocabulary DatasetField Value identifier displayOrder subject Agricultural Sciences D01 0 subject Arts and Humanities D0 1 @@ -138,189 +140,7925 @@ authorIdentifierScheme DAI 5 authorIdentifierScheme ResearcherID 6 authorIdentifierScheme ScopusID 7 - language Abkhaz 0 - language Afar 1 aar aa - language Afrikaans 2 afr af - language Akan 3 aka ak - language Albanian 4 sqi alb sq - language Amharic 5 amh am - language Arabic 6 ara ar - language Aragonese 7 arg an - language Armenian 8 hye arm hy - language Assamese 9 asm as - language Avaric 10 ava av - language Avestan 11 ave ae - language Aymara 12 aym ay - language Azerbaijani 13 aze az - language Bambara 14 bam bm - language Bashkir 15 bak ba - language Basque 16 eus baq eu - language Belarusian 17 bel be - language Bengali, Bangla 18 ben bn - language Bihari 19 bih bh - language Bislama 20 bis bi - language Bosnian 21 bos bs - language Breton 22 bre br - language Bulgarian 23 bul bg - language Burmese 24 mya bur my - language Catalan,Valencian 25 cat ca - language Chamorro 26 cha ch - language Chechen 27 che ce - language Chichewa, Chewa, Nyanja 28 nya ny - language Chinese 29 zho chi zh - language Chuvash 30 chv cv - language Cornish 31 cor kw - language Corsican 32 cos co - language Cree 33 cre cr - language Croatian 34 hrv src hr - language Czech 35 ces cze cs - language Danish 36 dan da - language Divehi, Dhivehi, Maldivian 37 div dv - language Dutch 38 nld dut nl - language Dzongkha 39 dzo dz - language English 40 eng en - language Esperanto 41 epo eo - language Estonian 42 est et - language Ewe 43 ewe ee - language Faroese 44 fao fo - language Fijian 45 fij fj - language Finnish 46 fin fi - language French 47 fra fre fr - language Fula, Fulah, Pulaar, Pular 48 ful ff - language Galician 49 glg gl - language Georgian 50 kat geo ka - language German 51 deu ger de - language Greek (modern) 52 gre ell el - language Guaraní 53 grn gn - language Gujarati 54 guj gu - language Haitian, Haitian Creole 55 hat ht - language Hausa 56 hau ha - language Hebrew (modern) 57 heb he - language Herero 58 her hz - language Hindi 59 hin hi - language Hiri Motu 60 hmo ho - language Hungarian 61 hun hu - language Interlingua 62 ina ia - language Indonesian 63 ind id - language Interlingue 64 ile ie - language Irish 65 gle ga - language Igbo 66 ibo ig - language Inupiaq 67 ipk ik - language Ido 68 ido io - language Icelandic 69 isl ice is - language Italian 70 ita it - language Inuktitut 71 iku iu - language Japanese 72 jpn ja - language Javanese 73 jav jv - language Kalaallisut, Greenlandic 74 kal kl - language Kannada 75 kan kn - language Kanuri 76 kau kr - language Kashmiri 77 kas ks - language Kazakh 78 kaz kk - language Khmer 79 khm km - language Kikuyu, Gikuyu 80 kik ki - language Kinyarwanda 81 kin rw - language Kyrgyz 82 - language Komi 83 kom kv - language Kongo 84 kon kg - language Korean 85 kor ko - language Kurdish 86 kur ku - language Kwanyama, Kuanyama 87 kua kj - language Latin 88 lat la - language Luxembourgish, Letzeburgesch 89 ltz lb - language Ganda 90 lug lg - language Limburgish, Limburgan, Limburger 91 lim li - language Lingala 92 lin ln - language Lao 93 lao lo - language Lithuanian 94 lit lt - language Luba-Katanga 95 lub lu - language Latvian 96 lav lv - language Manx 97 glv gv - language Macedonian 98 mkd mac mk - language Malagasy 99 mlg mg - language Malay 100 may msa ms - language Malayalam 101 mal ml - language Maltese 102 mlt mt - language Māori 103 mao mri mi - language Marathi (Marāṭhī) 104 mar mr - language Marshallese 105 mah mh - language Mixtepec Mixtec 106 mix - language Mongolian 107 mon mn - language Nauru 108 nau na - language Navajo, Navaho 109 nav nv - language Northern Ndebele 110 nde nd - language Nepali 111 nep ne - language Ndonga 112 ndo ng - language Norwegian Bokmål 113 nob nb - language Norwegian Nynorsk 114 nno nn - language Norwegian 115 nor no - language Nuosu 116 - language Southern Ndebele 117 nbl nr - language Occitan 118 oci oc - language Ojibwe, Ojibwa 119 oji oj - language Old Church Slavonic,Church Slavonic,Old Bulgarian 120 chu cu - language Oromo 121 orm om - language Oriya 122 ori or - language Ossetian, Ossetic 123 oss os - language Panjabi, Punjabi 124 pan pa - language Pāli 125 pli pi - language Persian (Farsi) 126 per fas fa - language Polish 127 pol pl - language Pashto, Pushto 128 pus ps - language Portuguese 129 por pt - language Quechua 130 que qu - language Romansh 131 roh rm - language Kirundi 132 run rn - language Romanian 133 ron rum ro - language Russian 134 rus ru - language Sanskrit (Saṁskṛta) 135 san sa - language Sardinian 136 srd sc - language Sindhi 137 snd sd - language Northern Sami 138 sme se - language Samoan 139 smo sm - language Sango 140 sag sg - language Serbian 141 srp scc sr - language Scottish Gaelic, Gaelic 142 gla gd - language Shona 143 sna sn - language Sinhala, Sinhalese 144 sin si - language Slovak 145 slk slo sk - language Slovene 146 slv sl - language Somali 147 som so - language Southern Sotho 148 sot st - language Spanish, Castilian 149 spa es - language Sundanese 150 sun su - language Swahili 151 swa sw - language Swati 152 ssw ss - language Swedish 153 swe sv - language Tamil 154 tam ta - language Telugu 155 tel te - language Tajik 156 tgk tg - language Thai 157 tha th - language Tigrinya 158 tir ti - language Tibetan Standard, Tibetan, Central 159 tib bod bo - language Turkmen 160 tuk tk - language Tagalog 161 tgl tl - language Tswana 162 tsn tn - language Tonga (Tonga Islands) 163 ton to - language Turkish 164 tur tr - language Tsonga 165 tso ts - language Tatar 166 tat tt - language Twi 167 twi tw - language Tahitian 168 tah ty - language Uyghur, Uighur 169 uig ug - language Ukrainian 170 ukr uk - language Urdu 171 urd ur - language Uzbek 172 uzb uz - language Venda 173 ven ve - language Vietnamese 174 vie vi - language Volapük 175 vol vo - language Walloon 176 wln wa - language Welsh 177 cym wel cy - language Wolof 178 wol wo - language Western Frisian 179 fry fy - language Xhosa 180 xho xh - language Yiddish 181 yid yi - language Yoruba 182 yor yo - language Zhuang, Chuang 183 zha za - language Zulu 184 zul zu - language Not applicable 185 + language 'Are'are alu 0 alu + language 'Auhelawa kud 1 kud + language A'ou aou 2 aou + language A-Pucikwar apq 3 apq + language Aari aiw 4 aiw + language Aasáx aas 5 aas + language Abadi kbt 6 kbt + language Abaga abg 7 abg + language Abai Sungai abf 8 abf + language Abanyom abm 9 abm + language Abar mij 10 mij + language Abau aau 11 aau + language Abaza abq 12 abq + language Abellen Ayta abp 13 abp + language Abidji abi 14 abi + language Abinomn bsa 15 bsa + language Abipon axb 16 axb + language Abishira ash 17 ash + language Abkhaz abk 18 abk ab Abkhaz Abkhazian + language Abom aob 19 aob + language Abon abo 20 abo + language Abron abr 21 abr + language Abu ado 22 ado + language Abu' Arapesh aah 23 aah + language Abua abn 24 abn + language Abui abz 25 abz + language Abun kgr 26 kgr + language Abure abu 27 abu + language Abureni mgj 28 mgj + language Abé aba 29 aba + language Acatepec Me'phaa tpx 30 tpx + language Achagua aca 31 aca + language Achang acn 32 acn + language Ache yif 33 yif guq Aché + language Acheron acz 34 acz + language Achi acr 35 acr + language Achinese ace 36 ace + language Achterhoeks act 37 act + language Achuar-Shiwiar acu 38 acu + language Achumawi acv 39 acv + language Acoli ach 41 ach + language Acroá acs 42 acs + language Adai xad 43 xad + language Adamawa Fulfulde fub 44 fub + language Adamorobe Sign Language ads 45 ads + language Adang adn 46 adn + language Adangbe adq 47 adq + language Adangme ada 48 ada + language Adara kad 49 kad + language Adasen tiu 50 tiu + language Adele ade 51 ade + language Adhola adh 52 adh + language Adi adi 53 adi + language Adilabad Gondi wsg 54 wsg + language Adioukrou adj 55 adj + language Adithinngithigh dth 56 dth + language Adivasi Oriya ort 57 ort + language Adiwasi Garasia gas 58 gas + language Adnyamathanha adt 59 adt + language Adonara adr 60 adr + language Aduge adu 61 adu + language Adyghe ady 62 ady + language Adzera adz 63 adz + language Aeka aez 64 aez + language Aekyom awi 65 awi + language Aequian xae 66 xae + language Aer aeq 67 aeq + language Afade aal 68 aal + language Afar aar 69 aar aa + language Afghan Sign Language afg 70 afg + language Afitti aft 71 aft + language Afrihili afh 72 afh + language Afrikaans afr 73 afr af + language Afro-Seminole Creole afs 74 afs + language Agarabi agd 75 agd + language Agariya agi 76 agi + language Agatu agc 77 agc + language Agavotaguerra avo 78 avo + language Aghem agq 79 agq + language Aghu ahh 80 ahh + language Aghu-Tharnggala gtu 81 gtu + language Aghul agx 82 agx + language Aghwan xag 83 xag + language Agi aif 84 aif + language Agob kit 85 kit + language Agoi ibm 86 ibm + language Aguacateco agu 87 agu + language Aguano aga 88 aga + language Aguaruna agr 89 agr + language Aguna aug 90 aug + language Agusan Manobo msm 91 msm + language Agutaynen agn 92 agn + language Agwagwune yay 93 yay + language Ahanta aha 94 aha + language Aheri Gondi esg 95 esg + language Aheu thm 96 thm + language Ahirani ahr 97 ahr + language Ahom aho 98 aho + language Ahtena aht 99 aht + language Ahwai nfd 100 nfd + language Ai-Cham aih 101 aih + language Aighon aix 102 aix + language Aikanã tba 103 tba + language Aiklep mwg 104 mwg + language Aimaq aiq 105 aiq + language Aimele ail 106 ail + language Aimol aim 107 aim + language Ainbai aic 108 aic + language Ainu (China) aib 109 aib + language Ainu (Japan) ain 110 ain + language Aiome aki 111 aki + language Airoran air 112 air + language Aiton aio 113 aio + language Aja (Benin) ajg 114 ajg + language Aja (South Sudan) aja 115 aja + language Ajawa ajw 116 ajw + language Ajië aji 117 aji + language Ajumbu muc 118 muc + language Ajyíninka Apurucayali cpc 119 cpc + language Ak akq 120 akq + language Aka soh 121 soh + language Aka-Bea abj 122 abj + language Aka-Bo akm 123 akm + language Aka-Cari aci 124 aci + language Aka-Jeru akj 125 akj + language Aka-Kede akx 126 akx + language Aka-Kol aky 127 aky + language Aka-Kora ack 128 ack + language Akan aka 129 aka ak + language Akar-Bale acl 130 acl + language Akaselem aks 131 aks + language Akawaio ake 132 ake + language Ake aik 133 aik + language Akebu keu 134 keu + language Akei tsr 135 tsr + language Akeu aeu 136 aeu + language Akha ahk 137 ahk + language Akhvakh akv 138 akv + language Akkadian akk 139 akk + language Akkala Sami sia 140 sia + language Aklanon akl 141 akl + language Akolet akt 142 akt + language Akoose bss 143 bss + language Akoye miw 144 miw + language Akpa akf 145 akf + language Akpes ibe 146 ibe + language Akrukay afi 147 afi + language Akukem spm 148 spm + language Akuku ayk 149 ayk + language Akum aku 150 aku + language Akuntsu aqz 151 aqz + language Akurio ako 152 ako + language Akwa akw 153 akw + language Akyaung Ari Naga nqy 154 nqy + language Al-Sayyid Bedouin Sign Language syy 155 syy + language Alaba-K'abeena alw 156 alw + language Alabama akz 157 akz + language Alabat Island Agta dul 158 dul + language Alacatlatzala Mixtec mim 159 mim + language Alago ala 160 ala + language Alagwa wbj 161 wbj + language Alak alk 162 alk + language Alamblak amp 163 amp + language Alangan alj 164 alj + language Alanic xln 165 xln + language Alapmunte apv 166 apv + language Alawa alh 167 alh + language Albanian sqi 168 sqi alb sq + language Albanian Sign Language sqk 169 sqk + language Albarradas Sign Language lsc 170 lsc + language Alcozauca Mixtec xta 171 xta + language Alege alf 172 alf + language Alekano gah 173 gah + language Aleut ale 174 ale + language Algerian Arabic arq 175 arq + language Algerian Jewish Sign Language ajs 176 ajs + language Algerian Saharan Arabic aao 177 aao + language Algerian Sign Language asp 178 asp + language Algonquin alq 179 alq + language Ali aiy 180 aiy + language Alladian ald 181 ald + language Allar all 182 all + language Alngith aid 183 aid + language Alo Phola ypo 184 ypo + language Alor aol 185 aol + language Aloápam Zapotec zaq 186 zaq + language Alsea aes 187 aes + language Alu Kurumba xua 188 xua + language Alugu aub 189 aub + language Alumu-Tesu aab 190 aab + language Alune alp 191 alp + language Aluo yna 192 yna + language Alur alz 193 alz + language Alutor alr 194 alr + language Alviri-Vidari avd 195 avd + language Alyawarr aly 196 aly + language Ama (Papua New Guinea) amm 197 amm + language Ama (Sudan) nyi 198 nyi + language Amahai amq 199 amq + language Amahuaca amc 200 amc + language Amaimon ali 201 ali + language Amal aad 202 aad + language Amami Koniya Sign Language jks 203 jks + language Amanab amn 204 amn + language Amanayé ama 205 ama + language Amara aie 206 aie + language Amarakaeri amr 207 amr + language Amarasi aaz 208 aaz + language Amatlán Zapotec zpo 209 zpo + language Amba (Solomon Islands) utp 210 utp + language Amba (Uganda) rwm 211 rwm + language Ambai amk 212 amk + language Ambakich aew 213 aew + language Ambala Ayta abc 214 abc + language Ambelau amv 215 amv + language Ambele ael 216 ael + language Amblong alm 217 alm + language Ambo amb 218 amb + language Ambo-Pasco Quechua qva 219 qva + language Ambonese Malay abs 220 abs + language Ambrak aag 221 aag + language Ambul apo 222 apo + language Ambulas abt 223 abt + language Amdang amj 224 amj + language Amdo Tibetan adx 225 adx + language Amele aey 226 aey + language American Sign Language ase 227 ase + language Amganad Ifugao ifa 228 ifa + language Amharic amh 229 amh am + language Ami amy 230 amy + language Amis ami 231 ami + language Amo amo 232 amo + language Amol alx 233 alx + language Amoltepec Mixtec mbz 234 mbz + language Ampanang apg 235 apg + language Ampari Dogon aqd 236 aqd + language Amri Karbi ajz 237 ajz + language Amto amt 238 amt + language Amundava adw 239 adw + language Amurdak amg 240 amg + language Ana Tinga Dogon dti 241 dti + language Anaang anw 242 anw + language Anakalangu akg 243 akg + language Anal anm 244 anm + language Anam pda 245 pda + language Anambé aan 246 aan + language Anamgura imi 247 imi + language Anasi bpo 248 bpo + language Ancient Greek (to 1453) grc 249 grc + language Ancient Hebrew hbo 250 hbo + language Ancient Macedonian xmk 251 xmk + language Ancient North Arabian xna 252 xna + language Ancient Zapotec xzp 253 xzp + language Andaandi dgl 254 dgl + language Andai afd 255 afd + language Andajin ajn 256 ajn + language Andalusian Arabic xaa 257 xaa + language Andaman Creole Hindi hca 258 hca + language Andaqui ana 259 ana + language Andarum aod 260 aod + language Andegerebinha adg 261 adg + language Andh anr 262 anr + language Andi ani 263 ani + language Andio bzb 264 bzb + language Andoa anb 265 anb + language Andoque ano 266 ano + language Andra-Hus anx 267 anx + language Aneityum aty 268 aty + language Anem anz 269 anz + language Aneme Wake aby 270 aby + language Anfillo myo 271 myo + language Angaataha agm 272 agm + language Angaité aqt 273 aqt + language Angal age 274 age + language Angal Enen aoe 275 aoe + language Angal Heneng akh 276 akh + language Angami Naga njm 277 njm + language Angguruk Yali yli 278 yli + language Angika anp 279 anp + language Angkamuthi avm 280 avm + language Anglo-Norman xno 281 xno + language Angloromani rme 282 rme + language Angolar aoa 283 aoa + language Angor agg 284 agg + language Angoram aog 285 aog + language Angosturas Tunebo tnd 286 tnd + language Anguthimri awg 287 awg + language Ani Phowa ypn 288 ypn + language Anii blo 289 blo + language Animere anf 290 anf + language Anindilyakwa aoi 291 aoi + language Aninka aqk 292 aqk + language Anjam boj 293 boj + language Ankave aak 294 aak + language Anmatyerre amx 295 amx + language Anong nun 296 nun + language Anor anj 297 anj + language Anserma ans 298 ans + language Ansus and 299 and + language Antakarinya ant 300 ant + language Antankarana Malagasy xmv 301 xmv + language Antigua and Barbuda Creole English aig 302 aig + language Anu-Hkongso Chin anl 303 anl + language Anuak anu 304 anu + language Anufo cko 305 cko + language Anuki aui 306 aui + language Anus auq 307 auq + language Anuta aud 308 aud + language Anyin any 309 any + language Anyin Morofo mtb 310 mtb + language Ao Naga njo 311 njo + language Aoheng pni 312 pni + language Aore aor 313 aor + language Ap Ma kbx 314 kbx + language Apalachee xap 315 xap + language Apalaí apy 316 apy + language Apali ena 317 ena + language Apasco-Apoala Mixtec mip 318 mip + language Apatani apt 319 apt + language Apiaká api 320 api + language Apinayé apn 321 apn + language Apma app 322 app + language Aproumu Aizi ahp 323 ahp + language Apurinã apu 324 apu + language Aputai apx 325 apx + language Aquitanian xaq 326 xaq + language Arabana ard 327 ard + language Arabela arl 328 arl + language Arabic ara 329 ara ar + language Aragonese arg 330 arg an + language Araki akr 331 akr + language Arakwal rkw 332 rkw + language Aralle-Tabulahan atq 333 atq + language Arammba stk 334 stk + language Aranadan aaf 335 aaf + language Aranama-Tamique xrt 336 xrt + language Arandai jbj 337 jbj + language Araona aro 338 aro + language Arapaho arp 339 arp + language Arapaso arj 340 arj + language Ararandewára xaj 341 xaj + language Arawak arw 342 arw + language Araweté awt 343 awt + language Arawum awm 344 awm + language Arbore arv 345 arv + language Arbëreshë Albanian aae 346 aae + language Archi aqc 347 aqc + language Ardhamāgadhī Prākrit pka 348 pka + language Are mwc 349 mwc + language Areba aea 350 aea + language Arem aem 351 aem + language Arequipa-La Unión Quechua qxu 352 qxu + language Argentine Sign Language aed 353 aed + language Argobba agj 354 agj + language Arguni agf 355 agf + language Arhuaco arh 356 arh + language Arhâ aqr 357 aqr + language Arhö aok 358 aok + language Ari aac 359 aac + language Aribwatsa laz 360 laz + language Aribwaung ylu 361 ylu + language Arifama-Miniafia aai 362 aai + language Arigidi aqg 363 aqg + language Arikapú ark 364 ark + language Arikara ari 365 ari + language Arikem ait 366 ait + language Arin xrn 367 xrn + language Aringa luc 368 luc + language Armazic xrm 369 xrm + language Armenian hye 370 hye hy arm + language Armenian Sign Language aen 371 aen + language Arop-Lokep apr 372 apr + language Arop-Sissano aps 373 aps + language Arosi aia 374 aia + language Arpitan frp 375 frp + language Arritinngithigh rrt 376 rrt + language Arta atz 377 atz + language Aruamu msy 378 msy + language Aruek aur 379 aur + language Aruop lsr 380 lsr + language Arutani atx 381 atx + language Aruá (Amazonas State) aru 382 aru + language Aruá (Rodonia State) arx 383 arx + language Arvanitika Albanian aat 384 aat + language As asz 385 asz + language Asaro'o mtv 386 mtv + language Ashe ahs 387 ahs + language Ashkun ask 388 ask + language Asho Chin csh 389 csh + language Ashtiani atn 390 atn + language Asháninka cni 391 cni + language Ashéninka Pajonal cjo 392 cjo + language Ashéninka Perené prq 393 prq + language Asilulu asl 394 asl + language Askopan eiv 395 eiv + language Asoa asv 396 asv + language Assamese asm 397 asm as + language Assangori sjg 398 sjg + language Assiniboine asb 399 asb + language Assyrian Neo-Aramaic aii 400 aii + language Asturian ast 401 ast + language Asu (Nigeria) aum 402 aum + language Asu (Tanzania) asa 403 asa + language Asue Awyu psa 404 psa + language Asumboa aua 405 aua + language Asunción Mixtepec Zapotec zoo 406 zoo + language Asuri asr 407 asr + language Ata atm 408 atm + language Ata Manobo atd 409 atd + language Atakapa aqp 410 aqp + language Atampaya amz 411 amz + language Atatláhuca Mixtec mib 412 mib + language Atauran adb 413 adb + language Atayal tay 414 tay + language Atemble ate 415 ate + language Athpariya aph 416 aph + language Ati atk 417 atk + language Atikamekw atj 418 atj + language Atohwaim aqm 419 aqm + language Atong (Cameroon) ato 420 ato + language Atong (India) aot 421 aot + language Atorada aox 422 aox + language Atsahuaca atc 423 atc + language Atsam cch 424 cch + language Atsugewi atw 425 atw + language Attapady Kurumba pkr 426 pkr + language Attié ati 427 ati + language Atzingo Matlatzinca ocu 428 ocu + language Au avt 429 avt + language Aulua aul 430 aul + language Aurá aux 431 aux + language Aushi auh 432 auh + language Aushiri avs 433 avs + language Auslan asf 434 asf + language Austral aut 435 aut + language Australian Aborigines Sign Language asw 436 asw + language Austrian Sign Language asq 437 asq + language Auwe smf 438 smf + language Auye auu 439 auu + language Auyokawa auo 440 auo + language Avaric ava 441 ava av + language Avatime avn 442 avn + language Avau avb 443 avb + language Avestan ave 444 ave ae + language Avikam avi 445 avi + language Avokaya avu 446 avu + language Avá-Canoeiro avv 447 avv + language Awa (China) vwa 448 vwa + language Awa (Papua New Guinea) awb 449 awb + language Awa-Cuaiquer kwi 450 kwi + language Awabakal awk 451 awk + language Awad Bing bcu 452 bcu + language Awadhi awa 453 awa + language Awak awo 454 awo + language Awar aya 455 aya + language Awara awx 456 awx + language Awbono awh 457 awh + language Aweer bob 458 bob + language Awera awr 459 awr + language Awetí awe 460 awe + language Awing azo 461 azo + language Awiyaana auy 462 auy + language Awjilah auj 463 auj + language Awngi awn 464 awn + language Awngthim gwm 465 gwm + language Awtuw kmn 466 kmn + language Awu yiu 467 yiu + language Awun aww 468 aww + language Awutu afu 469 afu + language Awyi auw 470 auw + language Axamb ahb 471 ahb + language Axi Yi yix 472 yix + language Ayabadhu ayd 473 ayd + language Ayacucho Quechua quy 474 quy + language Ayautla Mazatec vmy 475 vmy + language Ayere aye 476 aye + language Ayerrerenge axe 477 axe + language Ayi (Papua New Guinea) ayq 478 ayq + language Ayiwo nfl 479 nfl + language Ayizi yyz 480 yyz + language Ayizo Gbe ayb 481 ayb + language Aymara aym 482 aym ay + language Ayoquesco Zapotec zaf 483 zaf + language Ayoreo ayo 484 ayo + language Ayu ayu 485 ayu + language Ayutla Mixtec miy 486 miy + language Azerbaijani aze 487 aze az + language Azha aza 488 aza + language Azhe yiz 489 yiz + language Azoyú Me'phaa tpc 490 tpc + language Baan bvj 491 bvj + language Baangi bqx 492 bqx + language Baatonum bba 493 bba + language Baba bbw 494 bbw + language Baba Malay mbf 495 mbf + language Babango bbm 496 bbm + language Babanki bbk 497 bbk + language Babatana baa 498 baa + language Babine bcr 499 bcr + language Babuza bzg 500 bzg + language Bacama bcy 501 bcy + language Bacanese Malay btj 502 btj + language Bactrian xbc 503 xbc + language Bada (Indonesia) bhz 504 bhz + language Bada (Nigeria) bau 505 bau + language Badaga bfq 506 bfq + language Bade bde 507 bde + language Badeshi bdz 508 bdz + language Badimaya bia 509 bia + language Badjiri jbi 510 jbi + language Badui bac 511 bac + language Badyara pbp 512 pbp + language Baeggu bvd 513 bvd + language Baelelea bvc 514 bvc + language Baetora btr 515 btr + language Bafanji bfj 516 bfj + language Bafaw-Balong bwt 517 bwt + language Bafia ksf 518 ksf + language Bafut bfd 519 bfd + language Baga Kaloum bqf 520 bqf + language Baga Koga bgo 521 bgo + language Baga Manduri bmd 522 bmd + language Baga Pokur bcg 523 bcg + language Baga Sitemu bsp 524 bsp + language Baga Sobané bsv 525 bsv + language Bagheli bfy 526 bfy + language Bagirmi bmi 527 bmi + language Bagirmi Fulfulde fui 528 fui + language Bago-Kusuntu bqg 529 bqg + language Bagri bgq 530 bgq + language Bagupi bpi 531 bpi + language Bagusa bqb 532 bqb + language Bagvalal kva 533 kva + language Baha Buyang yha 534 yha + language Baham bdw 535 bdw + language Bahamas Creole English bah 536 bah + language Baharna Arabic abv 537 abv + language Bahau bhv 538 bhv + language Bahinemo bjh 539 bjh + language Bahing bhj 540 bhj + language Bahnar bdq 541 bdq + language Bahonsuai bsu 542 bsu + language Bai (South Sudan) bdj 543 bdj + language Baibai bbf 544 bbf + language Baikeno bkx 545 bkx + language Baima bqh 546 bqh + language Baimak bmx 547 bmx + language Bainouk-Gunyaamolo bcz 548 bcz + language Bainouk-Gunyuño bab 549 bab + language Bainouk-Samik bcb 550 bcb + language Baiso bsw 551 bsw + language Baissa Fali fah 552 fah + language Bajan bjs 553 bjs + language Bajelani bjm 554 bjm + language Bajjika vjk 555 vjk + language Baka (Cameroon) bkc 556 bkc + language Baka (South Sudan) bdh 557 bdh + language Bakairí bkq 558 bkq + language Bakaka bqz 559 bqz + language Bakhtiari bqi 560 bqi + language Baki bki 561 bki + language Bakoko bkh 562 bkh + language Bakole kme 563 kme + language Bakpinka bbs 564 bbs + language Bakumpai bkr 565 bkr + language Bakwé bjw 566 bjw + language Balaesang bls 567 bls + language Balaibalan zba 568 zba + language Balangao blw 569 blw + language Balangingi sse 570 sse + language Balanta-Ganja bjt 571 bjt + language Balanta-Kentohe ble 572 ble + language Balantak blz 573 blz + language Baldemu bdn 574 bdn + language Bali (Democratic Republic of Congo) bcp 575 bcp + language Bali (Nigeria) bcn 576 bcn + language Balinese ban 577 ban + language Balinese Malay mhp 578 mhp + language Balkan Gagauz Turkish bgx 579 bgx + language Balkan Romani rmn 580 rmn + language Balo bqo 581 bqo + language Baloi biz 582 biz + language Balti bft 583 bft + language Baltic Romani rml 584 rml + language Baluan-Pam blq 585 blq + language Baluchi bal 586 bal + language Bamako Sign Language bog 587 bog + language Bamali bbq 588 bbq + language Bambalang bmo 589 bmo + language Bambam ptu 590 ptu + language Bambara bam 591 bam bm + language Bambassi myf 592 myf + language Bambili-Bambui baw 593 baw + language Bamenyam bce 594 bce + language Bamu bcf 595 bcf + language Bamukumbit bqt 596 bqt + language Bamun bax 597 bax + language Bamunka bvm 598 bvm + language Bamwe bmg 599 bmg + language Ban Khor Sign Language bfk 600 bfk + language Bana bcw 601 bcw + language Banao Itneg bjx 602 bjx + language Banaro byz 603 byz + language Banda (Indonesia) bnd 604 bnd + language Banda Malay bpq 605 bpq + language Banda-Bambari liy 606 liy + language Banda-Banda bpd 607 bpd + language Banda-Mbrès bqk 608 bqk + language Banda-Ndélé bfl 609 bfl + language Banda-Yangere yaj 610 yaj + language Bandi bza 611 bza + language Bandial bqj 612 bqj + language Bandjalang bdy 613 bdy + language Bangala bxg 614 bxg + language Bangandu bgf 615 bgf + language Bangba bbe 616 bbe + language Banggai bgz 617 bgz + language Banggarla bjb 618 bjb + language Bangi bni 619 bni + language Bangime dba 620 dba + language Bangka mfb 621 mfb + language Bangolan bgj 622 bgj + language Bangubangu bnx 623 bnx + language Bangwinji bsj 624 bsj + language Baniva bvv 625 bvv + language Baniwa bwi 626 bwi + language Banjar bjn 627 bjn + language Bankagooma bxw 628 bxw + language Bankal jjr 629 jjr + language Bankan Tey Dogon dbw 630 dbw + language Bankon abb 631 abb + language Bannoni bcm 632 bcm + language Bantawa bap 633 bap + language Bantayanon bfx 634 bfx + language Bantik bnq 635 bnq + language Bantoanon bno 636 bno + language Banyjima pnw 637 pnw + language Baoulé bci 638 bci + language Bara Malagasy bhr 639 bhr + language Baraamu brd 640 brd + language Barababaraba rbp 641 rbp + language Barai bbb 642 bbb + language Barakai baj 643 baj + language Baram Kayan kys 644 kys + language Barama bbg 645 bbg + language Barambu brm 646 brm + language Baramu bmz 647 bmz + language Barapasi brp 648 brp + language Baras brs 649 brs + language Barasana-Eduria bsn 650 bsn + language Barbaram vmb 651 vmb + language Barbareño boi 652 boi + language Barclayville Grebo gry 653 gry + language Bardi bcj 654 bcj + language Barein bva 655 bva + language Bargam mlp 656 mlp + language Bari bfa 657 bfa mot Barí + language Bariai bch 658 bch + language Bariji bjc 659 bjc + language Barikanchi bxo 660 bxo + language Barikewa jbk 661 jbk + language Barok bjk 662 bjk + language Barombi bbi 663 bbi + language Barro Negro Tunebo tbn 664 tbn + language Barrow Point bpt 665 bpt + language Baruga bjz 666 bjz + language Baruya byr 667 byr + language Barwe bwg 668 bwg + language Barzani Jewish Neo-Aramaic bjf 669 bjf + language Baré bae 670 bae + language Basa (Cameroon) bas 672 bas + language Basa (Nigeria) bzw 673 bzw + language Basa-Gumna bsl 674 bsl + language Basa-Gurmana buj 675 buj + language Basap bdb 676 bdb + language Basay byq 677 byq + language Bashkardi bsg 678 bsg + language Bashkir bak 679 bak ba + language Basketo bst 680 bst + language Basque eus 681 eus eu baq + language Bassa bsq 682 bsq + language Bassa-Kontagora bsr 683 bsr + language Bassari bsc 684 bsc + language Bassossi bsi 685 bsi + language Bata bta 686 bta + language Batad Ifugao ifb 687 ifb + language Batak bya 688 bya + language Batak Alas-Kluet btz 689 btz + language Batak Angkola akb 690 akb + language Batak Dairi btd 691 btd + language Batak Karo btx 692 btx + language Batak Mandailing btm 693 btm + language Batak Simalungun bts 694 bts + language Batak Toba bbc 695 bbc + language Batanga bnm 696 bnm + language Batek btq 697 btq + language Bateri btv 698 btv + language Bathari bhm 699 bhm + language Bati (Cameroon) btc 700 btc + language Bati (Indonesia) bvt 701 bvt + language Batjala xby 702 xby + language Bats bbl 703 bbl + language Batu btu 704 btu + language Batui zbt 705 zbt + language Batuley bay 706 bay + language Bau bbd 707 bbd + language Bau Bidayuh sne 708 sne + language Bauchi bsf 709 bsf + language Bauni bpe 710 bpe + language Baure brg 711 brg + language Bauria bge 712 bge + language Bauwaki bwk 713 bwk + language Bauzi bvz 714 bvz + language Bavarian bar 715 bar + language Bawm Chin bgr 716 bgr + language Bay Miwok mkq 717 mkq + language Bayali bjy 718 bjy + language Baybayanon bvy 719 bvy + language Baygo byg 720 byg + language Bayono byl 721 byl + language Bayot bda 722 bda + language Bayungu bxj 723 bxj + language Bazigar bfr 724 bfr + language Beami beo 725 beo + language Beaver bea 726 bea + language Beba bfp 727 bfp + language Bebele beb 728 beb + language Bebeli bek 729 bek + language Bebil bxp 730 bxp + language Bedjond bjv 731 bjv + language Bedoanas bed 732 bed + language Beeke bkf 733 bkf + language Beele bxq 734 bxq + language Beembe beq 735 beq + language Beezen bnz 736 bnz + language Befang bby 737 bby + language Beginci ebc 738 ebc + language Beja bej 739 bej + language Bekati' bei 740 bei + language Bekwarra bkv 741 bkv + language Bekwel bkw 742 bkw + language Belait beg 743 beg + language Belanda Bor bxb 744 bxb + language Belanda Viri bvi 745 bvi + language Belarusian bel 746 bel be + language Belhariya byw 747 byw + language Beli (Papua New Guinea) bey 748 bey + language Beli (South Sudan) blm 749 blm + language Belize Kriol English bzj 750 bzj + language Bella Coola blc 751 blc + language Bellari brw 752 brw + language Belning glb 753 glb + language Bemba (Zambia) bem 754 bem + language Bembe bmb 755 bmb + language Ben Tey Dogon dbt 756 dbt + language Bena (Nigeria) yun 757 yun + language Bena (Tanzania) bez 758 bez + language Benabena bef 759 bef + language Benamanga egm 760 egm + language Bench bcq 761 bcq + language Bende bdp 762 bdp + language Bendi bct 763 bct + language Beng nhb 764 nhb + language Benga bng 765 bng + language Bengali, Bangla ben 766 ben Bengali Bangla bn + language Benggoi bgy 767 bgy + language Bengkala Sign Language bqy 768 bqy + language Bentong bnu 769 bnu + language Benyadu' byd 770 byd + language Beothuk bue 771 bue + language Bepour bie 772 bie + language Berakou bxv 773 bxv + language Berau Malay bve 774 bve + language Berbice Creole Dutch brc 775 brc + language Berik bkl 776 bkl + language Berinomo bit 777 bit + language Berom bom 778 bom + language Berta wti 779 wti + language Berti byt 780 byt + language Besisi mhe 781 mhe + language Besme bes 782 bes + language Besoa bep 783 bep + language Betaf bfe 784 bfe + language Betawi bew 785 bew + language Bete byf 786 byf + language Bete-Bendi btt 787 btt + language Beti (Côte d'Ivoire) eot 788 eot + language Betta Kurumba xub 789 xub + language Bezhta kap 790 kap + language Bhadrawahi bhd 791 bhd + language Bhalay bhx 792 bhx + language Bharia bha 793 bha + language Bhatri bgw 794 bgw + language Bhattiyali bht 795 bht + language Bhaya bhe 796 bhe + language Bhele bhy 797 bhy + language Bhilali bhi 798 bhi + language Bhili bhb 799 bhb + language Bhojpuri bho 800 bho + language Bhoti Kinnauri nes 801 nes + language Bhujel byh 802 byh + language Bhunjia bhu 803 bhu + language Biafada bif 804 bif + language Biage bdf 805 bdf + language Biak bhw 806 bhw + language Biali beh 807 beh + language Bian Marind bpv 808 bpv + language Biangai big 809 big + language Biao byk 810 byk + language Biao Mon bmt 811 bmt + language Biao-Jiao Mien bje 812 bje + language Biatah Bidayuh bth 813 bth + language Bibbulman xbp 814 xbp + language Bidhawal ihw 815 ihw + language Bidiyo bid 816 bid + language Bidjara bym 817 bym + language Bidyogo bjg 818 bjg + language Biem bmc 819 bmc + language Bierebo bnk 820 bnk + language Bieria brj 821 brj + language Biete biu 822 biu + language Big Nambas nmb 823 nmb + language Biga bhc 824 bhc + language Bigambal xbe 825 xbe + language Bih ibh 826 ibh + language Bihari bih 827 bih bh + language Bijim jbm 828 jbm + language Bijori bix 829 bix + language Bikol bik 830 bik + language Bikya byb 831 byb + language Bila bip 832 bip + language Bilakura bql 833 bql + language Bilaspuri kfs 834 kfs + language Bilba bpz 835 bpz + language Bilbil brz 836 brz + language Bile bil 837 bil + language Bilin byn 838 byn + language Bilma Kanuri bms 839 bms + language Biloxi bll 840 bll + language Bilua blb 841 blb + language Bilur bxf 842 bxf + language Bima bhp 843 bhp + language Bimin bhl 844 bhl + language Bimoba bim 845 bim + language Bina (Nigeria) byj 846 byj + language Bina (Papua New Guinea) bmn 847 bmn + language Binahari bxz 848 bxz + language Binandere bhg 849 bhg + language Bindal xbd 850 xbd + language Bine bon 851 bon + language Bini bin 852 bin + language Binji bpj 853 bpj + language Binongan Itneg itb 854 itb + language Bintauna bne 855 bne + language Bintulu bny 856 bny + language Binukid bkd 857 bkd + language Binumarien bjr 858 bjr + language Bipi biq 859 biq + language Bira brf 860 brf + language Birale bxe 861 bxe + language Birao brr 862 brr + language Birgit btf 863 btf + language Birhor biy 864 biy + language Biri bzr 865 bzr + language Biritai bqq 866 bqq + language Birked brk 867 brk + language Birri bvq 868 bvq + language Birrpayi xbj 869 xbj + language Birwa brl 870 brl + language Biseni ije 871 ije + language Bishnupriya bpy 872 bpy + language Bishuo bwh 873 bwh + language Bisis bnw 874 bnw + language Bislama bis 875 bis bi + language Bisorio bir 876 bir + language Bissa bib 877 bib + language Bisu bzi 878 bzi + language Bit bgk 879 bgk + language Bitare brt 880 brt + language Bitur mcc 881 mcc + language Biwat bwm 882 bwm + language Biyo byo 883 byo + language Biyom bpm 884 bpm + language Blablanga blp 885 blp + language Blafe bfh 886 bfh + language Blagar beu 887 beu + language Blang blr 888 blr + language Blissymbols zbl 889 zbl + language Bo (Laos) bgl 890 bgl + language Bo (Papua New Guinea) bpw 891 bpw + language Bo-Rukul mae 892 mae + language Bo-Ung mux 893 mux + language Boano (Maluku) bzn 894 bzn + language Boano (Sulawesi) bzl 895 bzl + language Bobongko bgb 896 bgb + language Bobot bty 897 bty + language Bodo (Central African Republic) boy 898 boy + language Bodo (India) brx 899 brx + language Bodo Gadaba gbj 900 gbj + language Bodo Parja bdv 901 bdv + language Bofi bff 902 bff + language Boga bvw 903 bvw + language Bogaya boq 904 boq + language Boghom bux 905 bux + language Boguru bqu 906 bqu + language Bohtan Neo-Aramaic bhn 907 bhn + language Boikin bzf 908 bzf + language Bokha ybk 909 ybk + language Boko (Benin) bqc 910 bqc + language Boko (Democratic Republic of Congo) bkp 911 bkp + language Bokobaru bus 912 bus + language Bokoto bdt 913 bdt + language Bokyi bky 914 bky + language Bola bnp 915 bnp + language Bolango bld 916 bld + language Bole bol 917 bol + language Bolgarian xbo 918 xbo + language Bolgo bvo 919 bvo + language Bolia bli 920 bli + language Bolinao smk 921 smk + language Bolivian Sign Language bvl 922 bvl + language Boloki bkt 923 bkt + language Bolon bof 924 bof + language Bolondo bzm 925 bzm + language Bolongan blj 926 blj + language Bolyu ply 927 ply + language Bom-Kim bmf 928 bmf + language Boma boh 929 boh + language Bomboli bml 930 bml + language Bomboma bws 931 bws + language Bomitaba zmx 932 zmx + language Bomu bmq 933 bmq + language Bomwali bmw 934 bmw + language Bon Gula glc 935 glc + language Bonan peh 936 peh + language Bondei bou 937 bou + language Bondo bfw 938 bfw + language Bondoukou Kulango kzc 939 kzc + language Bondum Dom Dogon dbu 940 dbu + language Bonerate bna 941 bna + language Bonerif bnv 942 bnv + language Bonggi bdg 943 bdg + language Bonggo bpg 944 bpg + language Bongili bui 945 bui + language Bongo bot 946 bot + language Bongu bpu 947 bpu + language Bonjo bok 948 bok + language Bonkeng bvg 949 bvg + language Bonkiman bop 950 bop + language Bontok bnc 951 bnc + language Bookan bnb 952 bnb + language Boon bnl 953 bnl + language Boor bvf 954 bvf + language Bora boa 955 boa + language Borana-Arsi-Guji Oromo gax 956 gax + language Border Kuna kvn 957 kvn + language Borei gai 958 gai + language Borgu Fulfulde fue 959 fue + language Boro (Ethiopia) bwo 960 bwo + language Boro (Ghana) xxb 961 xxb + language Borong ksr 962 ksr + language Boruca brn 963 brn + language Borôro bor 964 bor + language Boselewa bwf 965 bwf + language Bosngun bqs 966 bqs + language Bosnian bos 967 bos bs + language Bote-Majhi bmj 968 bmj + language Botlikh bph 969 bph + language Botolan Sambal sbl 970 sbl + language Bouna Kulango nku 971 nku + language Bouni suo 972 suo + language Bouyei pcc 973 pcc + language Bozaba bzo 974 bzo + language Bragat aof 975 aof + language Brahui brh 976 brh + language Braj bra 977 bra + language Brao brb 978 brb + language Brazilian Sign Language bzs 979 bzs + language Brem buq 980 buq + language Breri brq 981 brq + language Breton bre 982 bre br + language Bribri bzd 983 bzd + language Bribri Sign Language rib 984 rib + language Brithenig bzt 985 bzt + language British Sign Language bfi 986 bfi + language Brokkat bro 987 bro + language Brokpake sgt 988 sgt + language Brokskat bkk 989 bkk + language Brooke's Point Palawano plw 990 plw + language Broome Pearling Lugger Pidgin bpl 991 bpl + language Brunca Sign Language rnb 992 rnb + language Brunei kxd 993 kxd + language Brunei Bisaya bsb 994 bsb + language Bruny Island Tasmanian xpz 995 xpz + language Bu (Bauchi State) zbu 996 zbu + language Bu (Kaduna State) jid 997 jid + language Bu-Nao Bunu bwx 998 bwx + language Bua bub 999 bub + language Bualkhaw Chin cbl 1000 cbl + language Buamu box 1001 box + language Bube bvb 1002 bvb + language Bubi buw 1003 buw + language Bubia bbx 1004 bbx + language Budeh Stieng stt 1005 stt + language Budibud btp 1006 btp + language Budong-Budong bdx 1007 bdx + language Budu buu 1008 buu + language Budukh bdk 1009 bdk + language Buduma bdm 1010 bdm + language Budza bja 1011 bja + language Bugan bbh 1012 bbh + language Bugawac buk 1013 buk + language Bughotu bgt 1014 bgt + language Buginese bug 1015 bug + language Buglere sab 1016 sab + language Bugun bgg 1017 bgg + language Buhi'non Bikol ubl 1018 ubl + language Buhid bku 1019 bku + language Buhutu bxh 1020 bxh + language Bukar-Sadung Bidayuh sdo 1021 sdo + language Bukat bvk 1022 bvk + language Bukharic bhh 1023 bhh + language Bukit Malay bvu 1024 bvu + language Bukitan bkn 1025 bkn + language Bukiyip ape 1026 ape + language Buksa tkb 1027 tkb + language Bukusu bxk 1028 bxk + language Bukwen buz 1029 buz + language Bulgarian bul 1030 bul bg + language Bulgarian Sign Language bqn 1031 bqn + language Bulgebi bmp 1032 bmp + language Buli uly 1033 uly + language Buli (Ghana) bwu 1034 bwu + language Buli (Indonesia) bzq 1035 bzq + language Bullom So buy 1036 buy + language Bulo Stieng sti 1037 sti + language Bulu (Cameroon) bum 1038 bum + language Bulu (Papua New Guinea) bjl 1039 bjl + language Bum bmv 1040 bmv + language Bumaji byp 1041 byp + language Bumang bvp 1042 bvp + language Bumbita Arapesh aon 1043 aon + language Bumthangkha kjz 1044 kjz + language Bun buv 1045 buv + language Buna bvn 1046 bvn + language Bunak bfn 1047 bfn + language Bunama bdd 1048 bdd + language Bundeli bns 1049 bns + language Bung bqd 1050 bqd + language Bungain but 1051 but + language Bunganditj xbg 1052 xbg + language Bungku bkz 1053 bkz + language Bungu wun 1054 wun + language Bunoge Dogon dgb 1055 dgb + language Bunuba bck 1056 bck + language Bunun bnn 1057 bnn + language Buol blf 1058 blf + language Bura-Pabir bwr 1059 bwr + language Burak bys 1060 bys + language Buraka bkg 1061 bkg + language Burarra bvr 1062 bvr + language Burate bti 1063 bti + language Burduna bxn 1064 bxn + language Bure bvh 1065 bvh + language Buriat bua 1066 bua + language Burji bji 1067 bji + language Burmbar vrt 1068 vrt + language Burmese mya 1069 mya my bur + language Burmeso bzu 1070 bzu + language Buru (Indonesia) mhs 1071 mhs + language Buru (Nigeria) bqw 1072 bqw + language Burui bry 1073 bry + language Burumakok aip 1074 aip + language Burun bdi 1075 bdi + language Burundian Sign Language lsb 1076 lsb + language Burunge bds 1077 bds + language Burushaski bsk 1078 bsk + language Burusu bqr 1079 bqr + language Buruwai asi 1080 asi + language Busa bqp 1081 bqp + language Busam bxs 1082 bxs + language Busami bsm 1083 bsm + language Busang Kayan bfg 1084 bfg + language Bushi buc 1085 buc + language Bushoong buf 1086 buf + language Buso bso 1087 bso + language Busoa bup 1088 bup + language Bussa dox 1089 dox + language Busuu bju 1090 bju + language Butbut Kalinga kyb 1091 kyb + language Butmas-Tur bnr 1092 bnr + language Butuanon btw 1093 btw + language Buwal bhs 1094 bhs + language Buyu byi 1095 byi + language Buyuan Jinuo jiy 1096 jiy + language Bwa bww 1097 bww + language Bwaidoka bwd 1098 bwd + language Bwanabwana tte 1099 tte + language Bwatoo bwa 1100 bwa + language Bwe Karen bwe 1101 bwe + language Bwela bwl 1102 bwl + language Bwile bwc 1103 bwc + language Bwisi bwz 1104 bwz + language Byangsi bee 1105 bee + language Byep mkk 1106 mkk + language Bädi Kanum khd 1107 khd + language C'Lela dri 1108 dri + language Caac msq 1109 msq + language Cabiyarí cbb 1110 cbb + language Cabécar cjp 1111 cjp + language Cacaloxtepec Mixtec miu 1112 miu + language Cacaopera ccr 1113 ccr + language Cacgia Roglai roc 1114 roc + language Cacua cbv 1115 cbv + language Caddo cad 1116 cad + language Cafundo Creole ccd 1117 ccd + language Cahuarano cah 1118 cah + language Cahuilla chl 1119 chl + language Cajamarca Quechua qvc 1120 qvc + language Cajatambo North Lima Quechua qvl 1121 qvl + language Cajonos Zapotec zad 1122 zad + language Cajun French frc 1123 frc + language Caka ckx 1124 ckx + language Cakchiquel-Quiché Mixed Language ckz 1125 ckz + language Cakfem-Mushere cky 1126 cky + language Calamian Tagbanwa tbk 1127 tbk + language Calderón Highland Quichua qud 1128 qud + language Callawalla caw 1129 caw + language Caluyanun clu 1130 clu + language Caló rmq 1131 rmq + language Cambodian Sign Language csx 1132 csx + language Cameroon Mambila mcu 1133 mcu + language Cameroon Pidgin wes 1134 wes + language Camling rab 1135 rab + language Campalagian cml 1136 cml + language Campidanese Sardinian sro 1137 sro + language Camsá kbh 1138 kbh + language Camtho cmt 1139 cmt + language Camunic xcc 1140 xcc + language Candoshi-Shapra cbu 1141 cbu + language Canela ram 1142 ram + language Canichana caz 1143 caz + language Cao Lan mlc 1144 mlc + language Cao Miao cov 1145 cov + language Capanahua kaq 1146 kaq + language Capiznon cps 1147 cps + language Cappadocian Greek cpg 1148 cpg + language Caquinte cot 1149 cot + language Car Nicobarese caq 1150 caq + language Cara cfd 1151 cfd + language Carabayo cby 1152 cby + language Caramanta crf 1153 crf + language Carapana cbc 1154 cbc + language Carian xcr 1155 xcr + language Caribbean Hindustani hns 1156 hns + language Caribbean Javanese jvn 1157 jvn + language Carijona cbd 1158 cbd + language Carolina Algonquian crr 1159 crr + language Carolinian cal 1160 cal + language Carpathian Romani rmc 1161 rmc + language Carrier crx 1162 crx + language Cashibo-Cacataibo cbr 1163 cbr + language Cashinahua cbs 1164 cbs + language Casiguran Dumagat Agta dgc 1165 dgc + language Casuarina Coast Asmat asc 1166 asc + language Catalan Sign Language csc 1167 csc + language Catalan, Valencian cat 1168 cat Catalan Valencian ca + language Catawba chc 1169 chc + language Cavineña cav 1170 cav + language Cayubaba cyb 1171 cyb + language Cayuga cay 1172 cay + language Cayuse xcy 1173 xcy + language Cañar Highland Quichua qxr 1174 qxr + language Ca̱hungwa̱rya̱ nat 1175 nat + language Cebaara Senoufo sef 1176 sef + language Cebuano ceb 1177 ceb + language Celtiberian xce 1178 xce + language Cemuhî cam 1179 cam + language Cen cen 1180 cen + language Central Asmat cns 1181 cns + language Central Atlas Tamazight tzm 1182 tzm + language Central Awyu awu 1183 awu + language Central Aymara ayr 1184 ayr + language Central Bai bca 1185 bca + language Central Berawan zbc 1186 zbc + language Central Bikol bcl 1187 bcl + language Central Bontok lbk 1188 lbk + language Central Cagayan Agta agt 1189 agt + language Central Grebo grv 1190 grv + language Central Hongshuihe Zhuang zch 1191 zch + language Central Huasteca Nahuatl nch 1192 nch + language Central Huishui Hmong hmc 1193 hmc + language Central Kanuri knc 1194 knc + language Central Kurdish ckb 1195 ckb + language Central Maewo mwo 1196 mwo + language Central Malay pse 1197 pse + language Central Masela mxz 1198 mxz + language Central Mashan Hmong hmm 1199 hmm + language Central Mazahua maz 1200 maz + language Central Melanau mel 1201 mel + language Central Mnong cmo 1202 cmo + language Central Nahuatl nhn 1203 nhn + language Central Nicobarese ncb 1204 ncb + language Central Ojibwa ojc 1205 ojc + language Central Okinawan ryu 1206 ryu + language Central Palawano plc 1207 plc + language Central Pame pbs 1208 pbs + language Central Pashto pst 1209 pst + language Central Pomo poo 1210 poo + language Central Puebla Nahuatl ncx 1211 ncx + language Central Sama sml 1212 sml + language Central Siberian Yupik ess 1213 ess + language Central Sierra Miwok csm 1214 csm + language Central Subanen syb 1215 syb + language Central Tagbanwa tgt 1216 tgt + language Central Tarahumara tar 1217 tar + language Central Tunebo tuf 1218 tuf + language Central Yupik esu 1219 esu + language Central-Eastern Niger Fulfulde fuq 1220 fuq + language Centúúm cet 1221 cet + language Cerma cme 1222 cme + language Cha'ari cxh 1223 cxh + language Chabu sbf 1224 sbf + language Chachapoyas Quechua quk 1225 quk + language Chachi cbi 1226 cbi + language Chadian Arabic shu 1227 shu + language Chadian Sign Language cds 1228 cds + language Chadong cdy 1229 cdy + language Chagatai chg 1230 chg + language Chaima ciy 1231 ciy + language Chak ckh 1232 ckh + language Chakali cli 1233 cli + language Chakavian ckm 1234 ckm + language Chakma ccp 1235 ccp + language Chala cll 1236 cll + language Chaldean Neo-Aramaic cld 1237 cld + language Chalikha tgf 1238 tgf + language Chamacoco ceg 1239 ceg + language Chamalal cji 1240 cji + language Chambeali cdh 1241 cdh + language Chambri can 1242 can + language Chamicuro ccc 1243 ccc + language Chamorro cha 1244 cha ch + language Chang Naga nbc 1245 nbc + language Changriwa cga 1246 cga + language Changthang cna 1247 cna + language Chantyal chx 1248 chx + language Chané caj 1249 caj + language Chara cra 1250 cra + language Chaudangsi cdn 1251 cdn + language Chaura crv 1252 crv + language Chavacano cbk 1253 cbk + language Chayahuita cbt 1254 cbt + language Chayuco Mixtec mih 1255 mih + language Chazumba Mixtec xtb 1256 xtb + language Che ruk 1257 ruk + language Chechen che 1258 che ce + language Cheke Holo mrn 1259 mrn + language Chemakum xch 1260 xch + language Chenapian cjn 1261 cjn + language Chenchu cde 1262 cde + language Chenoua cnu 1263 cnu + language Chepang cdm 1264 cdm + language Chepya ycp 1265 ycp + language Cherepon cpn 1266 cpn + language Cherokee chr 1267 chr + language Chesu ych 1268 ych + language Chetco ctc 1269 ctc + language Chewong cwg 1270 cwg + language Cheyenne chy 1271 chy + language Chhattisgarhi hne 1272 hne + language Chhintange ctn 1273 ctn + language Chhulung cur 1274 cur + language Chiangmai Sign Language csd 1275 csd + language Chiapanec cip 1276 cip + language Chibcha chb 1277 chb + language Chicahuaxtla Triqui trs 1278 trs + language Chichewa, Chewa, Nyanja nya 1279 nya Chichewa Chewa ny Nyanja + language Chichicapan Zapotec zpv 1280 zpv + language Chichimeca-Jonaz pei 1281 pei + language Chickasaw cic 1282 cic + language Chicomuceltec cob 1283 cob + language Chiga cgg 1284 cgg + language Chigmecatitlán Mixtec mii 1285 mii + language Chilcotin clc 1286 clc + language Chilean Sign Language csg 1287 csg + language Chilisso clh 1288 clh + language Chiltepec Chinantec csa 1289 csa + language Chimalapa Zoque zoh 1290 zoh + language Chimariko cid 1291 cid + language Chimborazo Highland Quichua qug 1292 qug + language Chimila cbg 1293 cbg + language China Buriat bxu 1294 bxu + language Chinali cih 1295 cih + language Chinbon Chin cnb 1296 cnb + language Chincha Quechua qxc 1297 qxc + language Chinese zho 1298 zho chi zh + language Chinese Pidgin English cpi 1299 cpi + language Chinese Sign Language csl 1300 csl + language Chinook chh 1301 chh + language Chinook jargon chn 1302 chn + language Chipaya cap 1303 cap + language Chipewyan chp 1304 chp + language Chippewa ciw 1305 ciw + language Chiquihuitlán Mazatec maq 1306 maq + language Chiquitano cax 1307 cax + language Chiquián Ancash Quechua qxa 1308 qxa + language Chiripá nhd 1309 nhd + language Chiru cdf 1310 cdf + language Chitimacha ctm 1311 ctm + language Chitkuli Kinnauri cik 1312 cik + language Chittagonian ctg 1313 ctg + language Chitwania Tharu the 1314 the + language Choapan Zapotec zpc 1315 zpc + language Chocangacakha cgk 1316 cgk + language Chochotec coz 1317 coz + language Choctaw cho 1318 cho + language Chodri cdi 1319 cdi + language Chokri Naga nri 1320 nri + language Chokwe cjk 1321 cjk + language Chol ctu 1322 ctu + language Cholón cht 1323 cht + language Chong cog 1324 cog + language Choni cda 1325 cda + language Chonyi-Dzihana-Kauma coh 1326 coh + language Chopi cce 1327 cce + language Chorasmian xco 1328 xco + language Chortí caa 1329 caa + language Chothe Naga nct 1330 nct + language Chrau crw 1331 crw + language Chru cje 1332 cje + language Chuanqiandian Cluster Miao cqd 1333 cqd + language Chuave cjv 1334 cjv + language Chug cvg 1335 cvg + language Chuj cac 1336 cac + language Chuka cuh 1337 cuh + language Chukot ckt 1338 ckt + language Chukwa cuw 1339 cuw + language Chulym clw 1340 clw + language Chumburung ncu 1341 ncu + language Chung cnq 1342 cnq + language Churahi cdj 1343 cdj + language Chut scb 1344 scb + language Chuukese chk 1345 chk + language Chuvantsy xcv 1346 xcv + language Chuvash chv 1347 chv cv + language Chuwabu chw 1348 chw + language Chácobo cao 1349 cao + language Ci Gbe cib 1350 cib + language Cia-Cia cia 1351 cia + language Cibak ckl 1352 ckl + language Cicipu awc 1353 awc + language Cimbrian cim 1354 cim + language Cinda-Regi-Tiyal cdr 1355 cdr + language Cineni cie 1356 cie + language Cinta Larga cin 1357 cin + language Cisalpine Gaulish xcg 1358 xcg + language Cishingini asg 1359 asg + language Citak txt 1360 txt + language Ciwogai tgd 1361 tgd + language Clallam clm 1362 clm + language Classical Armenian xcl 1363 xcl + language Classical Mandaic myz 1364 myz + language Classical Mongolian cmg 1365 cmg + language Classical Nahuatl nci 1366 nci + language Classical Newari nwc 1367 nwc + language Classical Quechua qwc 1368 qwc + language Classical Sanskrit cls 1369 cls + language Classical Syriac syc 1370 syc + language Classical Tibetan xct 1371 xct + language Coahuilteco xcw 1372 xcw + language Coast Miwok csi 1373 csi + language Coastal Konjo kjc 1374 kjc + language Coatecas Altas Zapotec zca 1375 zca + language Coatepec Nahuatl naz 1376 naz + language Coatlán Mixe mco 1377 mco + language Coatlán Zapotec zps 1378 zps + language Coatzospan Mixtec miz 1379 miz + language Cocama-Cocamilla cod 1380 cod + language Cochimi coj 1381 coj + language Cocopa coc 1382 coc + language Cocos Islands Malay coa 1383 coa + language Coeur d'Alene crd 1384 crd + language Cofán con 1385 con + language Cogui kog 1386 kog + language Col liw 1387 liw + language Colombian Sign Language csn 1388 csn + language Colonia Tovar German gct 1389 gct + language Colorado cof 1390 cof + language Columbia-Wenatchi col 1391 col + language Comaltepec Chinantec cco 1392 cco + language Comanche com 1393 com + language Comecrudo xcm 1394 xcm + language Como Karim cfg 1395 cfg + language Comox coo 1396 coo + language Con cno 1397 cno + language Congo Swahili swc 1398 swc + language Coos csz 1399 csz + language Copainalá Zoque zoc 1400 zoc + language Copala Triqui trc 1401 trc + language Coptic cop 1402 cop + language Coquille coq 1403 coq + language Cori cry 1404 cry + language Cornish cor 1405 cor kw + language Corongo Ancash Quechua qwa 1406 qwa + language Corsican cos 1407 cos co + language Costa Rican Sign Language csr 1408 csr + language Cotabato Manobo mta 1409 mta + language Cotoname xcn 1410 xcn + language Cowlitz cow 1411 cow + language Coyotepec Popoloca pbf 1412 pbf + language Coyutla Totonac toc 1413 toc + language Cree cre 1414 cre cr + language Creek mus 1415 mus + language Crimean Tatar crh 1416 crh + language Croatia Sign Language csq 1417 csq + language Croatian hrv 1418 hrv Logudorese Sardinian src hr Croatian + language Cross River Mbembe mfn 1419 mfn + language Crow cro 1420 cro + language Cruzeño crz 1421 crz + language Cua cua 1422 cua + language Cuba Sign Language csf 1423 csf + language Cubeo cub 1424 cub + language Cuiba cui 1425 cui + language Cuitlatec cuy 1426 cuy + language Culina cul 1427 cul + language Cumanagoto cuo 1428 cuo + language Cumbric xcb 1429 xcb + language Cun cuq 1430 cuq + language Cuneiform Luwian xlu 1431 xlu + language Cupeño cup 1432 cup + language Curonian xcu 1433 xcu + language Curripaco kpc 1434 kpc + language Cusco Quechua quz 1435 quz + language Cutchi-Swahili ccl 1436 ccl + language Cuvok cuv 1437 cuv + language Cuyamecalco Mixtec xtu 1438 xtu + language Cuyonon cyo 1439 cyo + language Cwi Bwamu bwy 1440 bwy + language Cypriot Arabic acy 1441 acy + language Czech ces 1442 ces cs cze + language Czech Sign Language cse 1443 cse + language Côông cnc 1444 cnc + language Da'a Kaili kzf 1445 kzf + language Daai Chin dao 1446 dao + language Daakaka bpa 1447 bpa + language Daantanai' lni 1448 lni + language Daasanach dsh 1449 dsh + language Daatsʼíin dtn 1450 dtn + language Daba dbq 1451 dbq + language Dabarre dbr 1452 dbr + language Dabe dbe 1453 dbe + language Dacian xdc 1454 xdc + language Dadi Dadi dda 1455 dda + language Dadibi mps 1456 mps + language Dadiya dbd 1457 dbd + language Daga dgz 1458 dgz + language Dagaari Dioula dgd 1459 dgd + language Dagba dgk 1460 dgk + language Dagbani dag 1461 dag + language Dagik dec 1462 dec + language Dagoman dgn 1463 dgn + language Dahalik dlk 1464 dlk + language Dahalo dal 1465 dal + language Daho-Doo das 1466 das + language Dai dij 1467 dij + language Dai Zhuang zhd 1468 zhd + language Dair drb 1469 drb + language Dakka dkk 1470 dkk + language Dakota dak 1471 dak + language Dakpakha dka 1472 dka + language Dalabon ngk 1473 ngk + language Dalmatian dlm 1474 dlm + language Daloa Bété bev 1475 bev + language Dama dmm 1476 dmm + language Damakawa dam 1477 dam + language Damal uhn 1478 uhn + language Dambi dac 1479 dac + language Dameli dml 1480 dml + language Dampelas dms 1481 dms + language Dan dnj 1482 dnj + language Danaru dnr 1483 dnr + language Danau dnu 1484 dnu + language Dandami Maria daq 1485 daq + language Dangaléat daa 1486 daa + language Dangaura Tharu thl 1487 thl + language Danish dan 1488 dan da + language Danish Sign Language dsl 1489 dsl + language Dano aso 1490 aso + language Danu dnv 1491 dnv + language Dao daz 1492 daz + language Daonda dnd 1493 dnd + language Dar Daju Daju djc 1494 djc + language Dar Fur Daju daj 1495 daj + language Dar Sila Daju dau 1496 dau + language Darai dry 1497 dry + language Dargwa dar 1498 dar + language Dari prs 1499 prs + language Darkinyung xda 1500 xda + language Darlong dln 1501 dln + language Darmiya drd 1502 drd + language Daro-Matu Melanau dro 1503 dro + language Dass dot 1504 dot + language Datooga tcc 1505 tcc + language Daungwurrung dgw 1506 dgw + language Daur dta 1507 dta + language Davawenyo daw 1508 daw + language Dawawa dww 1509 dww + language Dawera-Daweloor ddw 1510 ddw + language Dawik Kui dwk 1511 dwk + language Dawro dwr 1512 dwr + language Day dai 1513 dai + language Dayi dax 1514 dax + language Daza dzd 1515 dzd + language Dazaga dzg 1516 dzg + language Deccan dcc 1517 dcc + language Dedua ded 1518 ded + language Defaka afn 1519 afn + language Defi Gbe gbh 1520 gbh + language Deg mzw 1521 mzw + language Degema deg 1522 deg + language Degenan dge 1523 dge + language Degexit'an ing 1524 ing + language Dehu dhv 1525 dhv + language Dehwari deh 1526 deh + language Dek dek 1527 dek + language Dela-Oenale row 1528 row + language Delaware del 1529 del + language Delo ntr 1530 ntr + language Dem dem 1531 dem + language Dema dmx 1532 dmx + language Demisa dei 1533 dei + language Demta dmy 1534 dmy + language Dendi (Benin) ddn 1535 ddn + language Dendi (Central African Republic) deq 1536 deq + language Dengese dez 1537 dez + language Dengka dnk 1538 dnk + language Deno dbb 1539 dbb + language Denya anv 1540 anv + language Dení dny 1541 dny + language Deori der 1542 der + language Dera (Indonesia) kbv 1543 kbv + language Dera (Nigeria) kna 1544 kna + language Desano des 1545 des + language Desiya dso 1546 dso + language Dewas Rai dwz 1547 dwz + language Dewoin dee 1548 dee + language Dezfuli def 1549 def + language Dghwede dgh 1550 dgh + language Dhaiso dhs 1551 dhs + language Dhalandji dhl 1552 dhl + language Dhangu-Djangu dhg 1553 dhg + language Dhanki dhn 1554 dhn + language Dhanwar (Nepal) dhw 1555 dhw + language Dhao nfa 1556 nfa + language Dharawal tbh 1557 tbh + language Dhargari dhr 1558 dhr + language Dharuk xdk 1559 xdk + language Dharumbal xgm 1560 xgm + language Dhatki mki 1561 mki + language Dhimal dhi 1562 dhi + language Dhodia dho 1563 dho + language Dhofari Arabic adf 1564 adf + language Dhudhuroa ddr 1565 ddr + language Dhundari dhd 1566 dhd + language Dhungaloo dhx 1567 dhx + language Dhurga dhu 1568 dhu + language Dhuwal dwu 1569 dwu + language Dhuwaya dwy 1570 dwy + language Dia dia 1571 dia + language Dibabawon Manobo mbd 1572 mbd + language Dibiyaso dby 1573 dby + language Dibo dio 1574 dio + language Dibole bvx 1575 bvx + language Dicamay Agta duy 1576 duy + language Didinga did 1577 did + language Dido ddo 1578 ddo + language Dieri dif 1579 dif + language Digaro-Mishmi mhu 1580 mhu + language Digo dig 1581 dig + language Dii dur 1582 dur + language Dijim-Bwilim cfa 1583 cfa + language Dilling dil 1584 dil + language Dima jma 1585 jma + language Dimasa dis 1586 dis + language Dimbong dii 1587 dii + language Dime dim 1588 dim + language Dimli (individual language) diq 1589 diq + language Ding diz 1590 diz + language Dinka din 1591 din + language Dir-Nyamzak-Mbarimi nzr 1592 nzr + language Dirasha gdl 1593 gdl + language Diri dwa 1594 dwa + language Diriku diu 1595 diu + language Dirim dir 1596 dir + language Disa dsi 1597 dsi + language Ditammari tbz 1598 tbz + language Ditidaht dtd 1599 dtd + language Diuwe diy 1600 diy + language Diuxi-Tilantongo Mixtec xtd 1601 xtd + language Dixon Reef dix 1602 dix + language Dizin mdx 1603 mdx + language Djabugay dyy 1604 dyy + language Djabwurrung tjw 1605 tjw + language Djadjawurrung dja 1606 dja + language Djambarrpuyngu djr 1607 djr + language Djamindjung djd 1608 djd + language Djangun djf 1609 djf + language Djawi djw 1610 djw + language Djeebbana djj 1611 djj + language Djimini Senoufo dyi 1612 dyi + language Djinang dji 1613 dji + language Djinba djb 1614 djb + language Djiwarli dze 1615 dze + language Dobel kvo 1616 kvo + language Dobu dob 1617 dob + language Doe doe 1618 doe + language Doga dgg 1619 dgg + language Doghoro dgx 1620 dgx + language Dogoso dgs 1621 dgs + language Dogosé dos 1622 dos + language Dogri (individual language) dgo 1623 dgo + language Dogri (macrolanguage) doi 1624 doi + language Dogul Dom Dogon dbg 1625 dbg + language Doka dbi 1626 dbi + language Doko-Uyanga uya 1627 uya + language Dokshi dsk 1628 dsk + language Dolgan dlg 1629 dlg + language Dolpo dre 1630 dre + language Dom doa 1631 doa + language Domaaki dmk 1632 dmk + language Domari rmt 1633 rmt + language Dombe dov 1634 dov + language Dominican Sign Language doq 1635 doq + language Dompo doy 1636 doy + language Domu dof 1637 dof + language Domung dev 1638 dev + language Dondo dok 1639 dok + language Dong doh 1640 doh + language Dongo doo 1641 doo + language Dongotono ddd 1642 ddd + language Dongshanba Lalo yik 1643 yik + language Dongxiang sce 1644 sce + language Donno So Dogon dds 1645 dds + language Doondo dde 1646 dde + language Dori'o dor 1647 dor + language Doromu-Koki kqc 1648 kqc + language Dorze doz 1649 doz + language Doso dol 1650 dol + language Dotyali dty 1651 dty + language Doutai tds 1652 tds + language Doyayo dow 1653 dow + language Drents drt 1654 drt + language Drung duu 1655 duu + language Duala dua 1656 dua + language Duano dup 1657 dup + language Duau dva 1658 dva + language Dubli dub 1659 dub + language Dubu dmu 1660 dmu + language Dugun ndu 1661 ndu + language Duguri dbm 1662 dbm + language Dugwor dme 1663 dme + language Duhwa kbz 1664 kbz + language Duke nke 1665 nke + language Dulbu dbo 1666 dbo + language Duli-Gey duz 1667 duz + language Duma dma 1668 dma + language Dumbea duf 1669 duf + language Dumi dus 1670 dus + language Dumpas dmv 1671 dmv + language Dumun dui 1672 dui + language Duna duc 1673 duc + language Dungan dng 1674 dng + language Dungmali raa 1675 raa + language Dungra Bhil duh 1676 duh + language Dungu dbv 1677 dbv + language Dupaninan Agta duo 1678 duo + language Dura drq 1679 drq + language Duri mvp 1680 mvp + language Duriankere dbn 1681 dbn + language Durop krp 1682 krp + language Duruma dug 1683 dug + language Duruwa pci 1684 pci + language Dusner dsn 1685 dsn + language Dusun Deyah dun 1686 dun + language Dusun Malang duq 1687 duq + language Dusun Witu duw 1688 duw + language Dutch nld 1689 nld dut nl + language Dutch Sign Language dse 1690 dse + language Dutton World Speedwords dws 1691 dws + language Duungooma dux 1692 dux + language Duupa dae 1693 dae + language Duvle duv 1694 duv + language Duwai dbp 1695 dbp + language Duwet gve 1696 gve + language Dũya ldb 1697 ldb + language Dwang nnu 1698 nnu + language Dyaberdyaber dyb 1699 dyb + language Dyan dya 1700 dya + language Dyangadi dyn 1701 dyn + language Dyarim dyr 1702 dyr + language Dyirbal dbl 1703 dbl + language Dyugun dyd 1704 dyd + language Dyula dyu 1705 dyu + language Dza jen 1706 jen + language Dzalakha dzl 1707 dzl + language Dzando dzn 1708 dzn + language Dzao Min bpn 1709 bpn + language Dzongkha dzo 1710 dzo dz + language Dzùùngoo dnn 1711 dnn + language Dâw kwa 1712 kwa + language E eee 1713 eee + language E'ma Buyang yzg 1714 yzg + language E'ñapa Woromaipu pbh 1715 pbh + language Early Tripuri xtr 1716 xtr + language East Ambae omb 1717 omb + language East Berawan zbe 1718 zbe + language East Damar dmr 1719 dmr + language East Futuna fud 1720 fud + language East Kewa kjs 1721 kjs + language East Limba lma 1722 lma + language East Makian mky 1723 mky + language East Masela vme 1724 vme + language East Nyala nle 1725 nle + language East Tarangan tre 1726 tre + language East Yugur yuy 1727 yuy + language Eastern Abnaki aaq 1728 aaq + language Eastern Acipa acp 1729 acp + language Eastern Apurímac Quechua qve 1730 qve + language Eastern Arrernte aer 1731 aer + language Eastern Balochi bgp 1732 bgp + language Eastern Bolivian Guaraní gui 1733 gui + language Eastern Bontok ebk 1734 ebk + language Eastern Bru bru 1735 bru + language Eastern Canadian Inuktitut ike 1736 ike + language Eastern Cham cjm 1737 cjm + language Eastern Durango Nahuatl azd 1738 azd + language Eastern Egyptian Bedawi Arabic avl 1739 avl + language Eastern Frisian frs 1740 frs + language Eastern Gorkha Tamang tge 1741 tge + language Eastern Highland Chatino cly 1742 cly + language Eastern Highland Otomi otm 1743 otm + language Eastern Hongshuihe Zhuang zeh 1744 zeh + language Eastern Huasteca Nahuatl nhe 1745 nhe + language Eastern Huishui Hmong hme 1746 hme + language Eastern Karaboro xrb 1747 xrb + language Eastern Katu ktv 1748 ktv + language Eastern Kayah eky 1749 eky + language Eastern Keres kee 1750 kee + language Eastern Khumi Chin cek 1751 cek + language Eastern Krahn kqo 1752 kqo + language Eastern Lalu yit 1753 yit + language Eastern Lawa lwl 1754 lwl + language Eastern Magar mgp 1755 mgp + language Eastern Maninkakan emk 1756 emk + language Eastern Mari mhr 1757 mhr + language Eastern Maroon Creole djk 1758 djk + language Eastern Meohang emg 1759 emg + language Eastern Minyag emq 1760 emq + language Eastern Mnong mng 1761 mng + language Eastern Muria emu 1762 emu + language Eastern Ngad'a nea 1763 nea + language Eastern Nisu nos 1764 nos + language Eastern Ojibwa ojg 1765 ojg + language Eastern Oromo hae 1766 hae + language Eastern Parbate Kham kif 1767 kif + language Eastern Penan pez 1768 pez + language Eastern Pomo peb 1769 peb + language Eastern Qiandong Miao hmq 1770 hmq + language Eastern Subanen sfe 1771 sfe + language Eastern Tamang taj 1772 taj + language Eastern Tawbuid bnj 1773 bnj + language Eastern Xiangxi Miao muq 1774 muq + language Eastern Xwla Gbe gbx 1775 gbx + language Eastern Yiddish ydd 1776 ydd + language Ebira igb 1777 igb + language Eblan xeb 1778 xeb + language Ebrié ebr 1779 ebr + language Ebughu ebg 1780 ebg + language Ecuadorian Sign Language ecs 1781 ecs + language Ede Cabe cbj 1782 cbj + language Ede Ica ica 1783 ica + language Ede Idaca idd 1784 idd + language Ede Ije ijj 1785 ijj + language Edera Awyu awy 1786 awy + language Edolo etr 1787 etr + language Edomite xdm 1788 xdm + language Edopi dbf 1789 dbf + language Efai efa 1790 efa + language Efe efe 1791 efe + language Efik efi 1792 efi + language Efutop ofu 1793 ofu + language Ega ega 1794 ega + language Eggon ego 1795 ego + language Egypt Sign Language esl 1796 esl + language Egyptian (Ancient) egy 1797 egy + language Egyptian Arabic arz 1798 arz + language Ehueun ehu 1799 ehu + language Eipomek eip 1800 eip + language Eitiep eit 1801 eit + language Ejagham etu 1802 etu + language Ejamat eja 1803 eja + language Ekai Chin cey 1804 cey + language Ekajuk eka 1805 eka + language Ekari ekg 1806 ekg + language Eki eki 1807 eki + language Ekit eke 1808 eke + language Ekpeye ekp 1809 ekp + language El Alto Zapotec zpp 1810 zpp + language El Hugeirat elh 1811 elh + language El Molo elo 1812 elo + language El Nayar Cora crn 1813 crn + language Elamite elx 1814 elx + language Eleme elm 1815 elm + language Elepi ele 1816 ele + language Elfdalian ovd 1817 ovd + language Elip ekm 1818 ekm + language Elkei elk 1819 elk + language Elotepec Zapotec zte 1820 zte + language Eloyi afo 1821 afo + language Elseng mrf 1822 mrf + language Elu elu 1823 elu + language Elymian xly 1824 xly + language Emae mmw 1825 mmw + language Emai-Iuleha-Ora ema 1826 ema + language Eman emn 1827 emn + language Embaloh emb 1828 emb + language Emberá-Baudó bdc 1829 bdc + language Emberá-Catío cto 1830 cto + language Emberá-Chamí cmi 1831 cmi + language Emberá-Tadó tdc 1832 tdc + language Embu ebu 1833 ebu + language Emerillon eme 1834 eme + language Emilian egl 1835 egl + language Emplawas emw 1836 emw + language Emumu enr 1837 enr + language En enc 1838 enc + language Enawené-Nawé unk 1839 unk + language Ende end 1840 end + language Enga enq 1841 enq + language Engdewu ngr 1842 ngr + language Engenni enn 1843 enn + language Enggano eno 1844 eno + language English eng 1845 eng en + language Enlhet enl 1846 enl + language Enrekang ptt 1847 ptt + language Enu enu 1848 enu + language Enwan (Akwa Ibom State) enw 1849 enw + language Enwan (Edo State) env 1850 env + language Enxet enx 1851 enx + language Enya gey 1852 gey + language Epena sja 1853 sja + language Epi-Olmec xep 1854 xep + language Epie epi 1855 epi + language Epigraphic Mayan emy 1856 emy + language Eravallan era 1857 era + language Erave kjy 1858 kjy + language Ere twp 1859 twp + language Eritai ert 1860 ert + language Erokwanas erw 1861 erw + language Erre err 1862 err + language Erromintxela emx 1863 emx + language Ersu ers 1864 ers + language Eruwa erh 1865 erh + language Erzya myv 1866 myv + language Esan ish 1867 ish + language Ese mcq 1868 mcq + language Ese Ejja ese 1869 ese + language Eshtehardi esh 1870 esh + language Esimbi ags 1871 ags + language Eskayan esy 1872 esy + language Esperanto epo 1873 epo eo + language Esselen esq 1874 esq + language Estado de México Otomi ots 1875 ots + language Estonian est 1876 est et + language Estonian Sign Language eso 1877 eso + language Esuma esm 1878 esm + language Etchemin etc 1879 etc + language Etebi etb 1880 etb + language Eten etx 1881 etx + language Eteocretan ecr 1882 ecr + language Eteocypriot ecy 1883 ecy + language Ethiopian Sign Language eth 1884 eth + language Etkywan ich 1885 ich + language Eton (Cameroon) eto 1886 eto + language Eton (Vanuatu) etn 1887 etn + language Etruscan ett 1888 ett + language Etulo utr 1889 utr + language Eudeve eud 1890 eud + language Evant bzz 1891 bzz + language Even eve 1892 eve + language Evenki evn 1893 evn + language Eviya gev 1894 gev + language Ewage-Notu nou 1895 nou + language Ewe ewe 1896 ewe ee + language Ewondo ewo 1897 ewo + language Extremaduran ext 1898 ext + language Eyak eya 1899 eya + language Ezaa eza 1900 eza + language Fa d'Ambu fab 1901 fab + language Fagani faf 1902 faf + language Faifi fif 1903 fif + language Faire Atta azt 1904 azt + language Faita faj 1905 faj + language Faiwol fai 1906 fai + language Fala fax 1907 fax + language Falam Chin cfm 1908 cfm + language Fali fli 1909 fli + language Faliscan xfa 1910 xfa + language Fam fam 1911 fam + language Fanagalo fng 1912 fng + language Fanamaket bjp 1913 bjp + language Fanbak fnb 1914 fnb + language Fang (Cameroon) fak 1915 fak + language Fang (Equatorial Guinea) fan 1916 fan + language Fania fni 1917 fni + language Fanti fat 1918 fat + language Far Western Muria fmu 1919 fmu + language Farefare gur 1920 gur + language Faroese fao 1921 fao fo + language Fas fqs 1922 fqs + language Fasu faa 1923 faa + language Fataleka far 1924 far + language Fataluku ddg 1925 ddg + language Fayu fau 1926 fau + language Fe'fe' fmp 1927 fmp + language Fembe agl 1928 agl + language Fernando Po Creole English fpe 1929 fpe + language Feroge fer 1930 fer + language Fiji Hindi hif 1931 hif + language Fijian fij 1932 fij fj + language Filipino fil 1933 fil + language Filomena Mata-Coahuitlán Totonac tlp 1934 tlp + language Finland-Swedish Sign Language fss 1935 fss + language Finnish fin 1936 fin fi + language Finnish Sign Language fse 1937 fse + language Finongan fag 1938 fag + language Fipa fip 1939 fip + language Firan fir 1940 fir + language Fiwaga fiw 1941 fiw + language Flaaitaal fly 1942 fly + language Flinders Island fln 1943 fln + language Foau flh 1944 flh + language Foi foi 1945 foi + language Foia Foia ffi 1946 ffi + language Folopa ppo 1947 ppo + language Foma fom 1948 fom + language Fon fon 1949 fon + language Fongoro fgr 1950 fgr + language Foodo fod 1951 fod + language Forak frq 1952 frq + language Fordata frd 1953 frd + language Fore for 1954 for + language Forest Enets enf 1955 enf + language Fortsenal frt 1956 frt + language Francisco León Zoque zos 1957 zos + language Frankish frk 1958 frk + language French fra 1959 fra fre fr + language French Sign Language fsl 1960 fsl + language Friulian fur 1961 fur + language Fula, Fulah ful 1962 ful ff Fula Fulah + language Fuliiru flr 1963 flr + language Fulniô fun 1964 fun + language Fum fum 1965 fum + language Fungwa ula 1966 ula + language Fur fvr 1967 fvr + language Furu fuu 1968 fuu + language Futuna-Aniwa fut 1969 fut + language Fuyug fuy 1970 fuy + language Fwe fwe 1971 fwe + language Fwâi fwa 1972 fwa + language Fyam pym 1973 pym + language Fyer fie 1974 fie + language Ga gaa 1975 gaa + language Ga'anda gqa 1976 gqa + language Ga'dang gdg 1977 gdg + language Gaa ttb 1978 ttb + language Gaam tbi 1979 tbi + language Gabi-Gabi gbw 1980 gbw + language Gabri gab 1981 gab + language Gabrielino-Fernandeño xgf 1982 xgf + language Gadang gdk 1983 gdk + language Gaddang gad 1984 gad + language Gaddi gbk 1985 gbk + language Gade ged 1986 ged + language Gade Lohar gda 1987 gda + language Gadjerawang gdh 1988 gdh + language Gadsup gaj 1989 gaj + language Gaelic, Scottish Gaelic gla 1990 gla Gaelic Scottish Gaelic gd + language Gafat gft 1991 gft + language Gagadu gbu 1992 gbu + language Gagauz gag 1993 gag + language Gagnoa Bété btg 1994 btg + language Gagu ggu 1995 ggu + language Gahri bfu 1996 bfu + language Gaikundi gbf 1997 gbf + language Gail gic 1998 gic + language Gaina gcn 1999 gcn + language Gal gap 2000 gap + language Galambu glo 2001 glo + language Galatian xga 2002 xga + language Galela gbi 2003 gbi + language Galeya gar 2004 gar + language Galibi Carib car 2005 car + language Galice gce 2006 gce + language Galician glg 2007 glg gl + language Galindan xgl 2008 xgl + language Gallurese Sardinian sdn 2009 sdn + language Galo adl 2010 adl + language Galolen gal 2011 gal + language Gamale Kham kgj 2012 kgj + language Gambera gma 2013 gma + language Gambian Wolof wof 2014 wof + language Gamilaraay kld 2015 kld + language Gamit gbl 2016 gbl + language Gamkonora gak 2017 gak + language Gamo gmv 2018 gmv + language Gamo-Ningi bte 2019 bte + language Gan Chinese gan 2020 gan + language Gana gnq 2021 gnq + language Ganang gne 2022 gne + language Ganda lug 2023 lug lg + language Gane gzn 2024 gzn + language Ganggalida gcd 2025 gcd + language Ganglau ggl 2026 ggl + language Gangte gnb 2027 gnb + language Gangulu gnl 2028 gnl + language Gants gao 2029 gao + language Ganza gza 2030 gza + language Ganzi gnz 2031 gnz + language Gao gga 2032 gga + language Gapapaiwa pwg 2033 pwg + language Garhwali gbm 2034 gbm + language Garifuna cab 2035 cab + language Garig-Ilgar ilg 2036 ilg + language Garingbal xgi 2037 xgi + language Garlali gll 2038 gll + language Garo grt 2039 grt + language Garre gex 2040 gex + language Garrwa wrk 2041 wrk + language Garus gyb 2042 gyb + language Garza xgr 2043 xgr + language Gata' gaq 2044 gaq + language Gavak dmc 2045 dmc + language Gavar gou 2046 gou + language Gavião Do Jiparaná gvo 2047 gvo + language Gawar-Bati gwt 2048 gwt + language Gawri gwc 2049 gwc + language Gawwada gwd 2050 gwd + language Gayil gyl 2051 gyl + language Gayo gay 2052 gay + language Gazi gzi 2053 gzi + language Gaɓogbo gie 2054 gie + language Gbagyi gbr 2055 gbr + language Gbanu gbv 2056 gbv + language Gbanziri gbg 2057 gbg + language Gbari gby 2058 gby + language Gbaya (Central African Republic) gba 2059 gba + language Gbaya (Sudan) krs 2060 krs + language Gbaya-Bossangoa gbp 2061 gbp + language Gbaya-Bozoum gbq 2062 gbq + language Gbaya-Mbodomo gmm 2063 gmm + language Gbayi gyg 2064 gyg + language Gbesi Gbe gbs 2065 gbs + language Gbii ggb 2066 ggb + language Gbin xgb 2067 xgb + language Gbiri-Niragu grh 2068 grh + language Gboloo Grebo gec 2069 gec + language Ge hmj 2070 hmj + language Geba Karen kvq 2071 kvq + language Gebe gei 2072 gei + language Gedaged gdd 2073 gdd + language Gedeo drs 2074 drs + language Geez gez 2075 gez + language Geji gyz 2076 gyz + language Geko Karen ghk 2077 ghk + language Gela nlg 2078 nlg + language Geme geq 2079 geq + language Gen gej 2080 gej + language Gende gaf 2081 gaf + language Gengle geg 2082 geg + language Georgian kat 2083 kat geo ka + language Gepo ygp 2084 ygp + language Gera gew 2085 gew + language Gerai gef 2086 gef + language German deu 2087 deu de ger + language German Sign Language gsg 2088 gsg + language Geruma gea 2089 gea + language Geser-Gorom ges 2090 ges + language Ghadamès gha 2091 gha + language Ghanaian Pidgin English gpe 2092 gpe + language Ghanaian Sign Language gse 2093 gse + language Ghandruk Sign Language gds 2094 gds + language Ghanongga ghn 2095 ghn + language Ghari gri 2096 gri + language Ghayavi bmk 2097 bmk + language Gheg Albanian aln 2098 aln + language Ghera ghr 2099 ghr + language Ghodoberi gdo 2100 gdo + language Ghomara gho 2101 gho + language Ghomálá' bbj 2102 bbj + language Ghotuo aaa 2103 aaa + language Ghulfan ghl 2104 ghl + language Giangan bgi 2105 bgi + language Gibanawa gib 2106 gib + language Gidar gid 2107 gid + language Giiwo kks 2108 kks + language Gikyode acd 2109 acd + language Gilaki glk 2110 glk + language Gilbertese gil 2111 gil + language Gilima gix 2112 gix + language Gilyak niv 2113 niv + language Gimi (Eastern Highlands) gim 2114 gim + language Gimi (West New Britain) gip 2115 gip + language Gimme kmp 2116 kmp + language Gimnime gmn 2117 gmn + language Ginuman gnm 2118 gnm + language Ginyanga ayg 2119 ayg + language Girawa bbr 2120 bbr + language Girirra gii 2121 gii + language Giryama nyf 2122 nyf + language Githabul gih 2123 gih + language Gitonga toh 2124 toh + language Gitua ggt 2125 ggt + language Gitxsan git 2126 git + language Giyug giy 2127 giy + language Gizrra tof 2128 tof + language Glaro-Twabo glr 2129 glr + language Glavda glw 2130 glw + language Glio-Oubi oub 2131 oub + language Gnau gnu 2132 gnu + language Goan Konkani gom 2133 gom + language Goaria gig 2134 gig + language Gobasi goi 2135 goi + language Gobu gox 2136 gox + language Godié god 2137 god + language Godwari gdx 2138 gdx + language Goemai ank 2139 ank + language Gofa gof 2140 gof + language Gogo gog 2141 gog + language Gogodala ggw 2142 ggw + language Gokana gkn 2143 gkn + language Gola gol 2144 gol + language Golin gvf 2145 gvf + language Golpa lja 2146 lja + language Gondi gon 2147 gon + language Gone Dau goo 2148 goo + language Gongduk goe 2149 goe + language Gonja gjn 2150 gjn + language Goo gov 2151 gov + language Gooniyandi gni 2152 gni + language Gor gqr 2153 gqr + language Gorakor goc 2154 goc + language Gorap goq 2155 goq + language Goreng xgg 2156 xgg + language Gorontalo gor 2157 gor + language Gorovu grq 2158 grq + language Gorowa gow 2159 gow + language Gothic got 2160 got + language Goundo goy 2161 goy + language Gourmanchéma gux 2162 gux + language Gowlan goj 2163 goj + language Gowli gok 2164 gok + language Gowro gwf 2165 gwf + language Gozarkhani goz 2166 goz + language Grangali nli 2167 nli + language Grass Koiari kbk 2168 kbk + language Grebo grb 2169 grb + language Greek (modern) ell 2170 ell Modern Greek (1453-) el gre Greek + language Greek Sign Language gss 2171 gss + language Green Gelao giq 2172 giq + language Greenlandic, Kalaallisut kal 2173 kal kl Greenlandic Kalaallisut + language Grenadian Creole English gcl 2174 gcl + language Gresi grs 2175 grs + language Groma gro 2176 gro + language Gronings gos 2177 gos + language Gros Ventre ats 2178 ats + language Gua gwx 2179 gwx + language Guadeloupean Creole French gcf 2180 gcf + language Guahibo guh 2181 guh + language Guajajára gub 2182 gub + language Guajá gvj 2183 gvj + language Guambiano gum 2184 gum + language Guana (Brazil) gqn 2185 gqn + language Guana (Paraguay) gva 2186 gva + language Guanano gvc 2187 gvc + language Guanche gnc 2188 gnc + language Guanyinqiao jiq 2189 jiq + language Guaraní grn 2190 grn Guarani gn + language Guarayu gyr 2191 gyr + language Guarequena gae 2192 gae + language Guatemalan Sign Language gsm 2193 gsm + language Guató gta 2194 gta + language Guayabero guo 2195 guo + language Gudang xgd 2196 xgd + language Gudanji nji 2197 nji + language Gude gde 2198 gde + language Gudu gdu 2199 gdu + language Guduf-Gava gdf 2200 gdf + language Guerrero Amuzgo amu 2201 amu + language Guerrero Nahuatl ngu 2202 ngu + language Guevea De Humboldt Zapotec zpg 2203 zpg + language Gugadj ggd 2204 ggd + language Gugu Badhun gdc 2205 gdc + language Gugu Warra wrw 2206 wrw + language Gugubera kkp 2207 kkp + language Guhu-Samane ghs 2208 ghs + language Guianese Creole French gcr 2209 gcr + language Guibei Zhuang zgb 2210 zgb + language Guiberoua Béte bet 2211 bet + language Guibian Zhuang zgn 2212 zgn + language Guinea Kpelle gkp 2213 gkp + language Guinea-Bissau Sign Language lgs 2214 lgs + language Guinean Sign Language gus 2215 gus + language Guiqiong gqi 2216 gqi + language Gujarati guj 2217 guj gu + language Gujari gju 2218 gju + language Gula (Central African Republic) kcm 2219 kcm + language Gula (Chad) glu 2220 glu + language Gula Iro glj 2221 glj + language Gula'alaa gmb 2222 gmb + language Gulay gvl 2223 gvl + language Gule gly 2224 gly + language Gulf Arabic afb 2225 afb + language Gumalu gmu 2226 gmu + language Gumatj gnn 2227 gnn + language Gumawana gvs 2228 gvs + language Gumuz guk 2229 guk + language Gun guw 2230 guw + language Gundi gdi 2231 gdi + language Gunditjmara gjm 2232 gjm + language Gundungurra xrd 2233 xrd + language Gungabula gyf 2234 gyf + language Gungu rub 2235 rub + language Guntai gnt 2236 gnt + language Gunwinggu gup 2237 gup + language Gunya gyy 2238 gyy + language Gupa-Abawa gpa 2239 gpa + language Gupapuyngu guf 2240 guf + language Guramalum grz 2241 grz + language Gurani hac 2242 hac + language Gurdjar gdj 2243 gdj + language Gureng Gureng gnr 2244 gnr + language Gurgula ggg 2245 ggg + language Guriaso grx 2246 grx + language Gurindji gue 2247 gue + language Gurindji Kriol gjr 2248 gjr + language Gurmana gvm 2249 gvm + language Guro goa 2250 goa + language Gurr-goni gge 2251 gge + language Gurung gvr 2252 gvr + language Guruntum-Mbaaru grd 2253 grd + language Gusii guz 2254 guz + language Gusilay gsl 2255 gsl + language Guugu Yimidhirr kky 2256 kky + language Guwa xgw 2257 xgw + language Guwamu gwu 2258 gwu + language Guya gka 2259 gka + language Guyanese Creole English gyn 2260 gyn + language Guyani gvy 2261 gvy + language Gvoko ngs 2262 ngs + language Gwa gwb 2263 gwb + language Gwahatike dah 2264 dah + language Gwak jgk 2265 jgk + language Gwamhi-Wuri bga 2266 bga + language Gwandara gwn 2267 gwn + language Gweda grw 2268 grw + language Gweno gwe 2269 gwe + language Gwere gwr 2270 gwr + language Gwich'in gwi 2271 gwi + language Gyalsumdo gyo 2272 gyo + language Gyele gyi 2273 gyi + language Gyem gye 2274 gye + language Güilá Zapotec ztu 2275 ztu + language Gāndhārī pgd 2276 pgd + language Ha haq 2277 haq + language Habu hbu 2278 hbu + language Hadiyya hdy 2279 hdy + language Hadothi hoj 2280 hoj + language Hadrami xhd 2281 xhd + language Hadrami Arabic ayh 2282 ayh + language Hadza hts 2283 hts + language Haeke aek 2284 aek + language Hahon hah 2285 hah + language Haida hai 2286 hai + language Haigwai hgw 2287 hgw + language Haiphong Sign Language haf 2288 haf + language Haisla has 2289 has + language Haitian Creole, Haitian hat 2290 hat Haitian Creole Haitian ht + language Haitian Vodoun Culture Language hvc 2291 hvc + language Haiǁom hgm 2292 hgm + language Haji hji 2293 hji + language Hajong haj 2294 haj + language Hakha Chin cnh 2295 cnh + language Hakka Chinese hak 2296 hak + language Hakö hao 2297 hao + language Halang hal 2298 hal + language Halang Doan hld 2299 hld + language Halbi hlb 2300 hlb + language Halh Mongolian khk 2301 khk + language Halia hla 2302 hla + language Halkomelem hur 2303 hur + language Hamap hmu 2304 hmu + language Hamba hba 2305 hba + language Hamer-Banna amf 2306 amf + language Hamtai hmt 2307 hmt + language Han haa 2308 haa + language Hanga hag 2309 hag + language Hanga Hundi wos 2310 wos + language Hangaza han 2311 han + language Hani hni 2312 hni + language Hano lml 2313 lml + language Hanoi Sign Language hab 2314 hab + language Hanunoo hnn 2315 hnn + language Harami xha 2316 xha + language Harari har 2317 har + language Harijan Kinnauri kjo 2318 kjo + language Haroi hro 2319 hro + language Harsusi hss 2320 hss + language Haruai tmd 2321 tmd + language Haruku hrk 2322 hrk + language Haryanvi bgc 2323 bgc + language Harzani hrz 2324 hrz + language Hasha ybj 2325 ybj + language Hassaniyya mey 2326 mey + language Hatam had 2327 had + language Hattic xht 2328 xht + language Hausa hau 2329 hau ha + language Hausa Sign Language hsl 2330 hsl + language Havasupai-Walapai-Yavapai yuf 2331 yuf + language Haveke hvk 2332 hvk + language Havu hav 2333 hav + language Hawai'i Creole English hwc 2334 hwc + language Hawai'i Sign Language (HSL) hps 2335 hps + language Hawaiian haw 2336 haw + language Haya hay 2337 hay + language Hazaragi haz 2338 haz + language Hdi xed 2339 xed + language Hebrew (modern) heb 2340 heb Hebrew he + language Hehe heh 2341 heh + language Heiban hbn 2342 hbn + language Heiltsuk hei 2343 hei + language Helong heg 2344 heg + language Hema nix 2345 nix + language Hemba hem 2346 hem + language Herdé hed 2347 hed + language Herero her 2348 her hz + language Hermit llf 2349 llf + language Hernican xhr 2350 xhr + language Hewa ham 2351 ham + language Heyo auk 2352 auk + language Hiberno-Scottish Gaelic ghc 2353 ghc + language Hibito hib 2354 hib + language Hidatsa hid 2355 hid + language Hieroglyphic Luwian hlu 2356 hlu + language Higaonon mba 2357 mba + language Highland Konjo kjk 2358 kjk + language Highland Oaxaca Chontal chd 2359 chd + language Highland Popoluca poi 2360 poi + language Highland Puebla Nahuatl azz 2361 azz + language Highland Totonac tos 2362 tos + language Hijazi Arabic acw 2363 acw + language Hijuk hij 2364 hij + language Hiligaynon hil 2365 hil + language Himarimã hir 2366 hir + language Hindi hin 2367 hin hi + language Hinduri hii 2368 hii + language Hinukh gin 2369 gin + language Hiri Motu hmo 2370 hmo ho + language Hittite hit 2371 hit + language Hitu htu 2372 htu + language Hiw hiw 2373 hiw + language Hixkaryána hix 2374 hix + language Hlai lic 2375 lic + language Hlepho Phowa yhl 2376 yhl + language Hlersu hle 2377 hle + language Hmar hmr 2378 hmr + language Hmong hmn 2379 hmn + language Hmong Daw mww 2380 mww + language Hmong Don hmf 2381 hmf + language Hmong Dô hmv 2382 hmv + language Hmong Njua hnj 2383 hnj + language Hmong Shua hmz 2384 hmz + language Hmwaveke mrk 2385 mrk + language Ho hoc 2386 hoc + language Ho Chi Minh City Sign Language hos 2387 hos + language Ho-Chunk win 2388 win + language Hoava hoa 2389 hoa + language Hobyót hoh 2390 hoh + language Hoia Hoia hhi 2391 hhi + language Holikachuk hoi 2392 hoi + language Holiya hoy 2393 hoy + language Holma hod 2394 hod + language Holoholo hoo 2395 hoo + language Holu hol 2396 hol + language Homa hom 2397 hom + language Honduras Sign Language hds 2398 hds + language Hong Kong Sign Language hks 2399 hks + language Honi how 2400 how + language Hopi hop 2401 hop + language Horned Miao hrm 2402 hrm + language Horo hor 2403 hor + language Horom hoe 2404 hoe + language Horpa ero 2405 ero + language Hote hot 2406 hot + language Hoti hti 2407 hti + language Hovongan hov 2408 hov + language Hoyahoya hhy 2409 hhy + language Hozo hoz 2410 hoz + language Hpon hpo 2411 hpo + language Hrangkhol hra 2412 hra + language Hre hre 2413 hre + language Hruso hru 2414 hru + language Hu huo 2415 huo + language Huachipaeri hug 2416 hug + language Huallaga Huánuco Quechua qub 2417 qub + language Huamalíes-Dos de Mayo Huánuco Quechua qvh 2418 qvh + language Huambisa hub 2419 hub + language Huarijio var 2420 var + language Huastec hus 2421 hus + language Huaulu hud 2422 hud + language Huautla Mazatec mau 2423 mau + language Huaxcaleca Nahuatl nhq 2424 nhq + language Huaylas Ancash Quechua qwh 2425 qwh + language Huaylla Wanca Quechua qvw 2426 qvw + language Huba hbb 2427 hbb + language Huehuetla Tepehua tee 2428 tee + language Huichol hch 2429 hch + language Huilliche huh 2430 huh + language Huitepec Mixtec mxs 2431 mxs + language Huizhou Chinese czh 2432 czh + language Hukumina huw 2433 huw + language Hula hul 2434 hul + language Hulaulá huy 2435 huy + language Huli hui 2436 hui + language Hulung huk 2437 huk + language Humburi Senni Songhay hmb 2438 hmb + language Humene huf 2439 huf + language Humla hut 2440 hut + language Hunde hke 2441 hke + language Hung hnu 2442 hnu + language Hungana hum 2443 hum + language Hungarian hun 2444 hun hu + language Hungarian Sign Language hsh 2445 hsh + language Hungu hng 2446 hng + language Hunjara-Kaina Ke hkk 2447 hkk + language Hunnic xhc 2448 xhc + language Hunsrik hrx 2449 hrx + language Hunzib huz 2450 huz + language Hupa hup 2451 hup + language Hupdë jup 2452 jup + language Hupla hap 2453 hap + language Hurrian xhu 2454 xhu + language Hutterite German geh 2455 geh + language Hwana hwo 2456 hwo + language Hya hya 2457 hya + language Hyam jab 2458 jab + language Hyolmo scp 2459 scp + language Hértevin hrt 2460 hrt + language Hõne juh 2461 juh + language I-Wak iwk 2462 iwk + language Iaai iai 2463 iai + language Iamalele yml 2464 yml + language Iatmul ian 2465 ian + language Iau tmu 2466 tmu + language Ibali Teke tek 2467 tek + language Ibaloi ibl 2468 ibl + language Iban iba 2469 iba + language Ibanag ibg 2470 ibg + language Ibani iby 2471 iby + language Ibatan ivb 2472 ivb + language Iberian xib 2473 xib + language Ibibio ibb 2474 ibb + language Ibino ibn 2475 ibn + language Ibu ibu 2476 ibu + language Ibuoro ibr 2477 ibr + language Icelandic isl 2478 isl ice is + language Icelandic Sign Language icl 2479 icl + language Iceve-Maci bec 2480 bec + language Ida'an dbj 2481 dbj + language Idakho-Isukha-Tiriki ida 2482 ida + language Idaté idt 2483 idt + language Idere ide 2484 ide + language Idesa ids 2485 ids + language Idi idi 2486 idi + language Ido ido 2487 ido io + language Idoma idu 2488 idu + language Idon idc 2489 idc + language Idu-Mishmi clk 2490 clk + language Iduna viv 2491 viv + language Ifo iff 2492 iff + language Ifè ife 2493 ife + language Igala igl 2494 igl + language Igana igg 2495 igg + language Igbo ibo 2496 ibo ig + language Igede ige 2497 ige + language Ignaciano ign 2498 ign + language Igo ahl 2499 ahl + language Iguta nar 2500 nar + language Igwe igw 2501 igw + language Iha ihp 2502 ihp + language Iha Based Pidgin ihb 2503 ihb + language Ihievbe ihi 2504 ihi + language Ik ikx 2505 ikx + language Ika ikk 2506 ikk + language Ikaranggal ikr 2507 ikr + language Ikhin-Arokho ikh 2508 ikh + language Ikizu ikz 2509 ikz + language Iko iki 2510 iki + language Ikobi meb 2511 meb + language Ikoma-Nata-Isenye ntk 2512 ntk + language Ikpeng txi 2513 txi + language Ikpeshi ikp 2514 ikp + language Ikposo kpo 2515 kpo + language Iku-Gora-Ankwa ikv 2516 ikv + language Ikulu ikl 2517 ikl + language Ikwere ikw 2518 ikw + language Ikwo iqw 2519 iqw + language Ila ilb 2520 ilb + language Ile Ape ila 2521 ila + language Ili Turki ili 2522 ili + language Ili'uun ilu 2523 ilu + language Ilianen Manobo mbi 2524 mbi + language Illyrian xil 2525 xil + language Iloko ilo 2526 ilo + language Ilongot ilk 2527 ilk + language Ilue ilv 2528 ilv + language Ilwana mlk 2529 mlk + language Imbabura Highland Quichua qvi 2530 qvi + language Imbongu imo 2531 imo + language Imonda imn 2532 imn + language Imotong imt 2533 imt + language Imroing imr 2534 imr + language Inabaknon abx 2535 abx + language Inapang mzu 2536 mzu + language Inari Sami smn 2537 smn + language Indian Sign Language ins 2538 ins + language Indo-Portuguese idb 2539 idb + language Indonesian ind 2540 ind id + language Indonesian Bajau bdl 2541 bdl + language Indonesian Sign Language inl 2542 inl + language Indri idr 2543 idr + language Indus Kohistani mvy 2544 mvy + language Indus Valley Language xiv 2545 xiv + language Inebu One oin 2546 oin + language Ineseño inz 2547 inz + language Inga inb 2548 inb + language Ingrian izh 2549 izh + language Ingush inh 2550 inh + language Inlaod Itneg iti 2551 iti + language Innu moe 2552 moe + language Inoke-Yate ino 2553 ino + language Inonhan loc 2554 loc + language Inor ior 2555 ior + language Inpui Naga nkf 2556 nkf + language Interglossa igs 2557 igs + language Interlingua ina 2558 ina Interlingua ia Interlingua (International Auxiliary Language Association) + language Interlingue ile 2559 ile ie + language International Sign ils 2560 ils + language Interslavic isv 2561 isv + language Intha int 2562 int + language Inuinnaqtun ikt 2563 ikt + language Inuit Sign Language iks 2564 iks + language Inuktitut iku 2565 iku iu + language Inupiaq ipk 2566 ipk ik + language Iowa-Oto iow 2567 iow + language Ipalapa Amuzgo azm 2568 azm + language Ipiko ipo 2569 ipo + language Ipili ipi 2570 ipi + language Ipulo ass 2571 ass + language Iquito iqu 2572 iqu + language Ir irr 2573 irr + language Iranian Persian pes 2574 pes + language Iranian Sign Language psc 2575 psc + language Iranun (Malaysia) ilm 2576 ilm + language Iranun (Philippines) ilp 2577 ilp + language Iraqw irk 2578 irk + language Irarutu irh 2579 irh + language Iraya iry 2580 iry + language Iresim ire 2581 ire + language Irish gle 2582 gle ga + language Irish Sign Language isg 2583 isg + language Irula iru 2584 iru + language Irántxe irn 2585 irn + language Isabi isa 2586 isa + language Isanzu isn 2587 isn + language Isarog Agta agk 2588 agk + language Isconahua isc 2589 isc + language Isebe igo 2590 igo + language Isekiri its 2591 its + language Ishkashimi isk 2592 isk + language Isinai inn 2593 inn + language Isirawa srl 2594 srl + language Island Carib crb 2595 crb + language Islander Creole English icr 2596 icr + language Isnag isd 2597 isd + language Isoko iso 2598 iso + language Israeli Sign Language isr 2599 isr + language Isthmus Mixe mir 2600 mir + language Isthmus Zapotec zai 2601 zai + language Isthmus-Cosoleacaque Nahuatl nhk 2602 nhk + language Isthmus-Mecayapan Nahuatl nhx 2603 nhx + language Isthmus-Pajapan Nahuatl nhp 2604 nhp + language Istriot ist 2605 ist + language Istro Romanian ruo 2606 ruo + language Isu (Fako Division) szv 2607 szv + language Isu (Menchum Division) isu 2608 isu + language Italian ita 2609 ita it + language Italian Sign Language ise 2610 ise + language Itawit itv 2611 itv + language Itelmen itl 2612 itl + language Itene ite 2613 ite + language Iteri itr 2614 itr + language Itik itx 2615 itx + language Ito itw 2616 itw + language Itonama ito 2617 ito + language Itu Mbon Uzo itm 2618 itm + language Itundujia Mixtec mce 2619 mce + language Itzá itz 2620 itz + language Iu Mien ium 2621 ium + language Ivatan ivv 2622 ivv + language Ivbie North-Okpela-Arhe atg 2623 atg + language Iwaidja ibd 2624 ibd + language Iwal kbm 2625 kbm + language Iwam iwm 2626 iwm + language Iwur iwo 2627 iwo + language Ixcatec ixc 2628 ixc + language Ixcatlán Mazatec mzi 2629 mzi + language Ixil ixl 2630 ixl + language Ixtayutla Mixtec vmj 2631 vmj + language Ixtenco Otomi otz 2632 otz + language Iyayu iya 2633 iya + language Iyive uiv 2634 uiv + language Iyo nca 2635 nca + language Iyo'wujwa Chorote crq 2636 crq + language Iyojwa'ja Chorote crt 2637 crt + language Izere izr 2638 izr + language Izii izz 2639 izz + language Izon ijc 2640 ijc + language Izora cbo 2641 cbo + language Iñapari inp 2642 inp + language Jabutí jbt 2643 jbt + language Jad jda 2644 jda + language Jadgali jdg 2645 jdg + language Jah Hut jah 2646 jah + language Jahanka jad 2647 jad + language Jair Awyu awv 2648 awv + language Jaitmatang xjt 2649 xjt + language Jakati jat 2650 jat + language Jakattoe jrt 2651 jrt + language Jakun jak 2652 jak + language Jalapa De Díaz Mazatec maj 2653 maj + language Jalkunan bxl 2654 bxl + language Jamaican Country Sign Language jcs 2655 jcs + language Jamaican Creole English jam 2656 jam + language Jamaican Sign Language jls 2657 jls + language Jamamadí jaa 2658 jaa + language Jambi Malay jax 2659 jax + language Jamiltepec Mixtec mxt 2660 mxt + language Jamsay Dogon djm 2661 djm + language Jandai jan 2662 jan + language Jandavra jnd 2663 jnd + language Jangkang djo 2664 djo + language Jangshung jna 2665 jna + language Janji jni 2666 jni + language Japanese jpn 2667 jpn ja + language Japanese Sign Language jsl 2668 jsl + language Japrería jru 2669 jru + language Jaqaru jqr 2670 jqr + language Jara jaf 2671 jaf + language Jarai jra 2672 jra + language Jarawa (India) anq 2673 anq + language Jaru ddj 2674 ddj + language Jauja Wanca Quechua qxw 2675 qxw + language Jaunsari jns 2676 jns + language Javanese jav 2677 jav jv + language Javindo jvd 2678 jvd + language Jawe jaz 2679 jaz + language Jawoyn djn 2680 djn + language Jaya jyy 2681 jyy + language Jebero jeb 2682 jeb + language Jeh jeh 2683 jeh + language Jehai jhi 2684 jhi + language Jejara Naga pzn 2685 pzn + language Jejueo jje 2686 jje + language Jemez tow 2687 tow + language Jenaama Bozo bze 2688 bze + language Jennu Kurumba xuj 2689 xuj + language Jere jer 2690 jer + language Jeri Kuo jek 2691 jek + language Jerung jee 2692 jee + language Jewish Babylonian Aramaic (ca. 200-1200 CE) tmr 2693 tmr + language Jewish Palestinian Aramaic jpa 2694 jpa + language Jhankot Sign Language jhs 2695 jhs + language Jiamao jio 2696 jio + language Jiarong jya 2697 jya + language Jiba juo 2698 juo + language Jibu jib 2699 jib + language Jicarilla Apache apj 2700 apj + language Jiiddu jii 2701 jii + language Jilbe jie 2702 jie + language Jilim jil 2703 jil + language Jimi (Cameroon) jim 2704 jim + language Jimi (Nigeria) jmi 2705 jmi + language Jina jia 2706 jia + language Jingulu jig 2707 jig + language Jinyu Chinese cjy 2708 cjy + language Jiongnai Bunu pnu 2709 pnu + language Jirel jul 2710 jul + language Jiru jrr 2711 jrr + language Jita jit 2712 jit + language Jju kaj 2713 kaj + language Joba job 2714 job + language Jofotek-Bromnya jbr 2715 jbr + language Jogi jog 2716 jog + language Jola-Fonyi dyo 2717 dyo + language Jola-Kasa csk 2718 csk + language Jonkor Bourmataguil jeu 2719 jeu + language Jordanian Sign Language jos 2720 jos + language Jorá jor 2721 jor + language Jowulu jow 2722 jow + language Ju juu 2723 juu + language Juang jun 2724 jun + language Judeo-Arabic jrb 2725 jrb + language Judeo-Berber jbe 2726 jbe + language Judeo-Georgian jge 2727 jge + language Judeo-Iraqi Arabic yhd 2728 yhd + language Judeo-Italian itk 2729 itk + language Judeo-Moroccan Arabic aju 2730 aju + language Judeo-Persian jpr 2731 jpr + language Judeo-Tat jdt 2732 jdt + language Judeo-Tripolitanian Arabic yud 2733 yud + language Judeo-Yemeni Arabic jye 2734 jye + language Jukun Takum jbu 2735 jbu + language Jumjum jum 2736 jum + language Jumla Sign Language jus 2737 jus + language Jumli jml 2738 jml + language Jungle Inga inj 2739 inj + language Juquila Mixe mxq 2740 mxq + language Jur Modo bex 2741 bex + language Juray juy 2742 juy + language Jurchen juc 2743 juc + language Jurúna jur 2744 jur + language Jutish jut 2745 jut + language Juwal mwb 2746 mwb + language Juxtlahuaca Mixtec vmc 2747 vmc + language Juǀ'hoan ktz 2748 ktz + language Jwira-Pepesa jwi 2749 jwi + language Jèrriais nrf 2750 nrf + language Júma jua 2751 jua + language K'iche' quc 2752 quc + language Kaamba xku 2753 xku + language Kaan ldl 2754 ldl + language Kaang Chin ckn 2755 ckn + language Kaansa gna 2756 gna + language Kaba ksp 2757 ksp + language Kabalai kvf 2758 kvf + language Kabardian kbd 2759 kbd + language Kabatei xkp 2760 xkp + language Kabiyè kbp 2761 kbp + language Kabola klz 2762 klz + language Kabore One onk 2763 onk + language Kabras lkb 2764 lkb + language Kaburi uka 2765 uka + language Kabutra kbu 2766 kbu + language Kabuverdianu kea 2767 kea + language Kabwa cwa 2768 cwa + language Kabwari kcw 2769 kcw + language Kabyle kab 2770 kab + language Kachama-Ganjule kcx 2771 kcx + language Kachari xac 2772 xac + language Kachhi kfr 2773 kfr + language Kachi Koli gjk 2774 gjk + language Kachin kac 2775 kac + language Kachok xkk 2776 xkk + language Kacipo-Bale Suri koe 2777 koe + language Kadai kzd 2778 kzd + language Kadar kej 2779 kej + language Kadaru kdu 2780 kdu + language Kadazan Dusun dtp 2781 dtp + language Kadiwéu kbc 2782 kbc + language Kadu zkd 2783 zkd + language Kadung dkg 2784 dkg + language Kaduo ktp 2785 ktp + language Kaeku kkq 2786 kkq + language Kaera jka 2787 jka + language Kafa kbr 2788 kbr + language Kafoa kpu 2789 kpu + language Kagan Kalagan kll 2790 kll + language Kagate syw 2791 syw + language Kagayanen cgc 2792 cgc + language Kagoma kdm 2793 kdm + language Kagoro xkg 2794 xkg + language Kagulu kki 2795 kki + language Kahe hka 2796 hka + language Kahua agw 2797 agw + language Kaian kct 2798 kct + language Kaibobo kzb 2799 kzb + language Kaidipang kzp 2800 kzp + language Kaiep kbw 2801 kbw + language Kaikadi kep 2802 kep + language Kaikavian Literary Language kjv 2803 kjv + language Kaike kzq 2804 kzq + language Kaimbulawa zka 2805 zka + language Kaimbé xai 2806 xai + language Kaingang kgp 2807 kgp + language Kairak ckr 2808 ckr + language Kairiru kxa 2809 kxa + language Kairui-Midiki krd 2810 krd + language Kais kzm 2811 kzm + language Kaitag xdq 2812 xdq + language Kaivi kce 2813 kce + language Kaiwá kgk 2814 kgk + language Kaiy tcq 2815 tcq + language Kajakse ckq 2816 ckq + language Kajali xkj 2817 xkj + language Kajaman kag 2818 kag + language Kakabai kqf 2819 kqf + language Kakabe kke 2820 kke + language Kakanda kka 2821 kka + language Kaki Ae tbd 2822 tbd + language Kako kkj 2823 kkj + language Kakwa keo 2824 keo + language Kala Lagaw Ya mwp 2825 mwp + language Kalaamaya lkm 2826 lkm + language Kalabakan kve 2827 kve + language Kalabari ijn 2828 ijn + language Kalabra kzz 2829 kzz + language Kalagan kqe 2830 kqe + language Kalaktang Monpa kkf 2831 kkf + language Kalam kmh 2832 kmh + language Kalamsé knz 2833 knz + language Kalanadi wkl 2834 wkl + language Kalanga kck 2835 kck + language Kalanguya kak 2836 kak + language Kalao kly 2837 kly + language Kalapuya kyl 2838 kyl + language Kalarko kba 2839 kba + language Kalasha kls 2840 kls + language Kalenjin kln 2841 kln + language Kalispel-Pend d'Oreille fla 2842 fla + language Kalkoti xka 2843 xka + language Kalkutung ktg 2844 ktg + language Kalmyk xal 2845 xal + language Kalo Finnish Romani rmf 2846 rmf + language Kalou ywa 2847 ywa + language Kaluli bco 2848 bco + language Kalumpang kli 2849 kli + language Kam kdx 2850 kdx + language Kamakan vkm 2851 vkm + language Kamang woi 2852 woi + language Kamano kbq 2853 kbq + language Kamantan kci 2854 kci + language Kamar keq 2855 keq + language Kamara jmr 2856 jmr + language Kamarian kzx 2857 kzx + language Kamaru kgx 2858 kgx + language Kamas xas 2859 xas + language Kamasa klp 2860 klp + language Kamasau kms 2861 kms + language Kamayo kyk 2862 kyk + language Kamayurá kay 2863 kay + language Kamba (Kenya) kam 2864 kam + language Kambaata ktb 2865 ktb + language Kambaira kyy 2866 kyy + language Kambera xbr 2867 xbr + language Kamberau irx 2868 irx + language Kambiwá xbw 2869 xbw + language Kami (Nigeria) kmi 2870 kmi + language Kami (Tanzania) kcu 2871 kcu + language Kamo kcq 2872 kcq + language Kamoro kgq 2873 kgq + language Kamu xmu 2874 xmu + language Kamula xla 2875 xla + language Kamviri xvi 2876 xvi + language Kamwe hig 2877 hig + language Kanakanabu xnb 2878 xnb + language Kanamarí knm 2879 knm + language Kanan zkn 2880 zkn + language Kanashi xns 2881 xns + language Kanasi soq 2882 soq + language Kanauji bjj 2883 bjj + language Kandas kqw 2884 kqw + language Kandawo gam 2885 gam + language Kande kbs 2886 kbs + language Kanembu kbl 2887 kbl + language Kang kyp 2888 kyp + language Kanga kcp 2889 kcp + language Kangean kkv 2890 kkv + language Kanggape igm 2891 igm + language Kangjia kxs 2892 kxs + language Kango (Bas-Uélé District) kty 2893 kty + language Kango (Tshopo District) kzy 2894 kzy + language Kangri xnr 2895 xnr + language Kaniet ktk 2896 ktk + language Kanikkaran kev 2897 kev + language Kaningdon-Nindem kdp 2898 kdp + language Kaningi kzo 2899 kzo + language Kaningra knr 2900 knr + language Kaninuwa wat 2901 wat + language Kanite kmu 2902 kmu + language Kanjari kft 2903 kft + language Kanju kbe 2904 kbe + language Kankanaey kne 2905 kne + language Kannada kan 2906 kan kn + language Kannada Kurumba kfi 2907 kfi + language Kanowit-Tanjong Melanau kxn 2908 kxn + language Kanoé kxo 2909 kxo + language Kansa ksk 2910 ksk + language Kantosi xkt 2911 xkt + language Kanu khx 2912 khx + language Kanufi kni 2913 kni + language Kanuri kau 2914 kau kr + language Kanyok kny 2915 kny + language Kao kax 2916 kax + language Kaonde kqn 2917 kqn + language Kap ykm 2918 ykm + language Kapin tbx 2919 tbx + language Kapinawá xpn 2920 xpn + language Kapingamarangi kpg 2921 kpg + language Kapori khp 2922 khp + language Kapriman dju 2923 dju + language Kaptiau kbi 2924 kbi + language Kapya klo 2925 klo + language Kaqchikel cak 2926 cak + language Kara (Central African Republic) kah 2927 kah + language Kara (Korea) zra 2928 zra + language Kara (Papua New Guinea) leu 2929 leu + language Kara (Tanzania) reg 2930 reg + language Kara-Kalpak kaa 2931 kaa + language Karachay-Balkar krc 2932 krc + language Karagas kim 2933 kim + language Karaim kdr 2934 kdr + language Karajarri gbd 2935 gbd + language Karajá kpj 2936 kpj + language Karakhanid xqa 2937 xqa + language Karami xar 2938 xar + language Karamojong kdj 2939 kdj + language Karang kzr 2940 kzr + language Karanga kth 2941 kth + language Karankawa zkk 2942 zkk + language Karao kyj 2943 kyj + language Karas kgv 2944 kgv + language Karata kpt 2945 kpt + language Karawa xrw 2946 xrw + language Karbi mjw 2947 mjw + language Kare (Central African Republic) kbn 2948 kbn + language Kare (Papua New Guinea) kmf 2949 kmf + language Karekare kai 2950 kai + language Karelian krl 2951 krl + language Karenggapa eaa 2952 eaa + language Karey kyd 2953 kyd + language Kari kbj 2954 kbj + language Karingani kgn 2955 kgn + language Karipuna kuq 2956 kuq + language Karipúna Creole French kmv 2957 kmv + language Karirí-Xocó kzw 2958 kzw + language Karitiâna ktn 2959 ktn + language Kariya kil 2960 kil + language Kariyarra vka 2961 vka + language Karkar-Yuri yuj 2962 yuj + language Karkin krb 2963 krb + language Karko kko 2964 kko + language Karnai bbv 2965 bbv + language Karo (Brazil) arr 2966 arr + language Karo (Ethiopia) kxh 2967 kxh + language Karok kyh 2968 kyh + language Karon krx 2969 krx + language Karon Dori kgw 2970 kgw + language Karore xkx 2971 xkx + language Karuwali rxw 2972 rxw + language Kasanga ccj 2973 ccj + language Kasem xsm 2974 xsm + language Kashaya kju 2975 kju + language Kashmiri kas 2976 kas ks + language Kashubian csb 2977 csb + language Kasiguranin ksn 2978 ksn + language Kaska kkz 2979 kkz + language Kaskean zsk 2980 zsk + language Kasua khs 2981 khs + language Katabaga ktq 2982 ktq + language Katawixi xat 2983 xat + language Katbol tmb 2984 tmb + language Katcha-Kadugli-Miri xtc 2985 xtc + language Kathoriya Tharu tkt 2986 tkt + language Kathu ykt 2987 ykt + language Kati bsh 2988 bsh + language Katkari kfu 2989 kfu + language Katla kcr 2990 kcr + language Kato ktw 2991 ktw + language Katso kaf 2992 kaf + language Katua kta 2993 kta + language Katukína kav 2994 kav + language Kaulong pss 2995 pss + language Kaur vkk 2996 vkk + language Kaure bpp 2997 bpp + language Kaurna zku 2998 zku + language Kauwera xau 2999 xau + language Kavalan ckv 3000 ckv + language Kavet krv 3001 krv + language Kawacha kcb 3002 kcb + language Kawaiisu xaw 3003 xaw + language Kawe kgb 3004 kgb + language Kawi kaw 3005 kaw + language Kaxararí ktx 3006 ktx + language Kaxuiâna kbb 3007 kbb + language Kayabí kyz 3008 kyz + language Kayagar kyt 3009 kyt + language Kayan pdu 3010 pdu + language Kayan Mahakam xay 3011 xay + language Kayan River Kayan xkn 3012 xkn + language Kayapó txu 3013 txu + language Kayardild gyd 3014 gyd + language Kayaw kvl 3015 kvl + language Kayeli kzl 3016 kzl + language Kayong kxy 3017 kxy + language Kayort kyv 3018 kyv + language Kaytetye gbb 3019 gbb + language Kayupulau kzu 3020 kzu + language Kazakh kaz 3021 kaz kk + language Kazukuru kzk 3022 kzk + language Ke'o xxk 3023 xxk + language Keak keh 3024 keh + language Keapara khz 3025 khz + language Kedah Malay meo 3026 meo + language Kedang ksx 3027 ksx + language Keder kdy 3028 kdy + language Keerray-Woorroong wkr 3029 wkr + language Kehu khh 3030 khh + language Kei kei 3031 kei + language Keiga kec 3032 kec + language Kein bmh 3033 bmh + language Keiyo eyo 3034 eyo + language Kekchí kek 3035 kek + language Kela (Democratic Republic of Congo) kel 3036 kel + language Kela (Papua New Guinea) kcl 3037 kcl + language Kelabit kzi 3038 kzi + language Kele (Democratic Republic of Congo) khy 3039 khy + language Kele (Papua New Guinea) sbc 3040 sbc + language Keley-I Kallahan ify 3041 ify + language Keliko kbo 3042 kbo + language Kelo xel 3043 xel + language Kelon kyo 3044 kyo + language Kemak kem 3045 kem + language Kembayan xem 3046 xem + language Kemberano bzp 3047 bzp + language Kembra xkw 3048 xkw + language Kemedzung dmo 3049 dmo + language Kemi Sami sjk 3050 sjk + language Kemiehua kfj 3051 kfj + language Kemtuik kmt 3052 kmt + language Kenaboi xbn 3053 xbn + language Kenati gat 3054 gat + language Kendayan knx 3055 knx + language Kendeje klf 3056 klf + language Kendem kvm 3057 kvm + language Kenga kyq 3058 kyq + language Keningau Murut kxi 3059 kxi + language Keninjal knl 3060 knl + language Kensiu kns 3061 kns + language Kenswei Nsei ndb 3062 ndb + language Kenyan Sign Language xki 3063 xki + language Kenyang ken 3064 ken + language Kenyi lke 3065 lke + language Kenzi xnz 3066 xnz + language Keoru-Ahia xeu 3067 xeu + language Kepkiriwát kpn 3068 kpn + language Kepo' kuk 3069 kuk + language Kera ker 3070 ker + language Kerak hhr 3071 hhr + language Kereho xke 3072 xke + language Kerek krk 3073 krk + language Kerewe ked 3074 ked + language Kerewo kxz 3075 kxz + language Kerinci kvr 3076 kvr + language Kesawai xes 3077 xes + language Ket ket 3078 ket + language Ketangalan kae 3079 kae + language Kete kcv 3080 kcv + language Ketengban xte 3081 xte + language Ketum ktt 3082 ktt + language Keyagana kyg 3083 kyg + language Kgalagadi xkv 3084 xkv + language Khah hkh 3085 hkh + language Khakas kjh 3086 kjh + language Khalaj klj 3087 klj + language Khaling klr 3088 klr + language Khamba kbg 3089 kbg + language Khamnigan Mongol ykh 3090 ykh + language Khams Tibetan khg 3091 khg + language Khamti kht 3092 kht + language Khamyang ksu 3093 ksu + language Khana ogo 3094 ogo + language Khandesi khn 3095 khn + language Khanty kca 3096 kca + language Khao xao 3097 xao + language Kharam Naga kfw 3098 kfw + language Kharia khr 3099 khr + language Kharia Thar ksy 3100 ksy + language Khasi kha 3101 kha + language Khayo lko 3102 lko + language Khazar zkz 3103 zkz + language Khe kqg 3104 kqg + language Khehek tlx 3105 tlx + language Khengkha xkf 3106 xkf + language Khetrani xhe 3107 xhe + language Khezha Naga nkh 3108 nkh + language Khiamniungan Naga kix 3109 kix + language Khinalugh kjj 3110 kjj + language Khirwar kwx 3111 kwx + language Khisa kqm 3112 kqm + language Khlula ykl 3113 ykl + language Khmer khm 3114 khm km + language Khmu kjg 3115 kjg + language Kho'ini xkc 3116 xkc + language Khoekhoe naq 3117 naq + language Khoibu Naga nkb 3118 nkb + language Kholok ktc 3119 ktc + language Khorasani Turkish kmz 3120 kmz + language Khorezmian zkh 3121 zkh + language Khotanese kho 3122 kho + language Khowar khw 3123 khw + language Khua xhv 3124 xhv + language Khuen khf 3125 khf + language Khumi Chin cnk 3126 cnk + language Khunsari kfm 3127 kfm + language Khvarshi khv 3128 khv + language Kháng kjm 3129 kjm + language Khün kkh 3130 kkh + language Kibala blv 3131 blv + language Kibet kie 3132 kie + language Kibiri prm 3133 prm + language Kickapoo kic 3134 kic + language Kija gia 3135 gia + language Kikai kzg 3136 kzg + language Kikuyu, Gikuyu kik 3137 kik Kikuyu Gikuyu ki + language Kildin Sami sjd 3138 sjd + language Kilivila kij 3139 kij + language Kiliwa klb 3140 klb + language Kilmeri kih 3141 kih + language Kim kia 3142 kia + language Kim Mun mji 3143 mji + language Kimaama kig 3144 kig + language Kimaragang kqr 3145 kqr + language Kimbu kiv 3146 kiv + language Kimbundu kmb 3147 kmb + language Kimki sbt 3148 sbt + language Kimré kqp 3149 kqp + language Kinabalian cbw 3150 cbw + language Kinalakna kco 3151 kco + language Kinamiging Manobo mkx 3152 mkx + language Kinaray-A krj 3153 krj + language Kinga zga 3154 zga + language Kinnauri kfk 3155 kfk + language Kintaq knq 3156 knq + language Kinuku kkd 3157 kkd + language Kinyarwanda kin 3158 kin rw + language Kioko ues 3159 ues + language Kiong kkm 3160 kkm + language Kiorr xko 3161 xko + language Kiowa kio 3162 kio + language Kiowa Apache apk 3163 apk + language Kipsigis sgc 3164 sgc + language Kiput kyi 3165 kyi + language Kir-Balar kkr 3166 kkr + language Kire geb 3167 geb + language Kirghiz kir 3168 kir Kyrgyz ky + language Kirike okr 3169 okr + language Kirikiri kiy 3170 kiy + language Kirmanjki (individual language) kiu 3171 kiu + language Kirundi run 3172 run Rundi rn + language Kirya-Konzəl fkk 3173 fkk + language Kis kis 3174 kis + language Kisa lks 3175 lks + language Kisan xis 3176 xis + language Kisankasa kqh 3177 kqh + language Kisar kje 3178 kje + language Kisi kiz 3179 kiz + language Kistane gru 3180 gru + language Kita Maninkakan mwk 3181 mwk + language Kitan zkt 3182 zkt + language Kitsai kii 3183 kii + language Kituba (Congo) mkw 3184 mkw + language Kituba (Democratic Republic of Congo) ktu 3185 ktu + language Kiunum wei 3186 wei + language Kizamani izm 3187 izm + language Kla-Dan lda 3188 lda + language Klamath-Modoc kla 3189 kla + language Klao klu 3190 klu + language Klias River Kadazan kqt 3191 kqt + language Klingon tlh 3192 tlh + language Knaanic czk 3193 czk + language Ko fuj 3194 fuj + language Koalib kib 3195 kib + language Koasati cku 3196 cku + language Koba kpd 3197 kpd + language Kobiana kcj 3198 kcj + language Kobo okc 3199 okc + language Kobol kgu 3200 kgu + language Kobon kpw 3201 kpw + language Koch kdq 3202 kdq + language Kochila Tharu thq 3203 thq + language Koda cdz 3204 cdz + language Kodaku ksz 3205 ksz + language Kodava kfa 3206 kfa + language Kodeoha vko 3207 vko + language Kodi kod 3208 kod + language Kodia kwp 3209 kwp + language Koenoem kcs 3210 kcs + language Kofa kso 3211 kso + language Kofei kpi 3212 kpi + language Kofyar kwl 3213 kwl + language Koguryo zkg 3214 zkg + language Kohin kkx 3215 kkx + language Kohistani Shina plk 3216 plk + language Koho kpm 3217 kpm + language Kohumono bcs 3218 bcs + language Koi kkt 3219 kkt + language Koireng nkd 3220 nkd + language Koitabu kqi 3221 kqi + language Koiwat kxt 3222 kxt + language Kok Borok trp 3223 trp + language Kok-Nar gko 3224 gko + language Kokata ktd 3225 ktd + language Koke kou 3226 kou + language Koki Naga nxk 3227 nxk + language Koko Babangk okg 3228 okg + language Kokoda xod 3229 xod + language Kokola kzn 3230 kzn + language Kokota kkk 3231 kkk + language Kol (Bangladesh) ekl 3232 ekl + language Kol (Cameroon) biw 3233 biw + language Kol (Papua New Guinea) kol 3234 kol + language Kola kvv 3235 kvv + language Kolbila klc 3236 klc + language Kolibugan Subanon skn 3237 skn + language Koluwawa klx 3238 klx + language Kom (Cameroon) bkm 3239 bkm + language Kom (India) kmm 3240 kmm + language Koma kmy 3241 kmy + language Komba kpf 3242 kpf + language Kombai tyn 3243 tyn + language Kombio xbi 3244 xbi + language Komering kge 3245 kge + language Komi kom 3246 kom kv + language Komi-Permyak koi 3247 koi + language Komi-Zyrian kpv 3248 kpv + language Kominimung xoi 3249 xoi + language Komo (Democratic Republic of Congo) kmw 3250 kmw + language Komo (Sudan) xom 3251 xom + language Komodo kvh 3252 kvh + language Kompane kvp 3253 kvp + language Komyandaret kzv 3254 kzv + language Kon Keu kkn 3255 kkn + language Konai kxw 3256 kxw + language Konda knd 3257 knd + language Konda-Dora kfc 3258 kfc + language Koneraw kdw 3259 kdw + language Kongo kon 3260 kon kg + language Konkani (individual language) knn 3261 knn + language Konkani (macrolanguage) kok 3262 kok + language Konkomba xon 3263 xon + language Konni kma 3264 kma + language Kono (Guinea) knu 3265 knu + language Kono (Nigeria) klk 3266 klk + language Kono (Sierra Leone) kno 3267 kno + language Konomala koa 3268 koa + language Konongo kcz 3269 kcz + language Konso kxc 3270 kxc + language Konyak Naga nbe 3271 nbe + language Konyanka Maninka mku 3272 mku + language Konzo koo 3273 koo + language Koongo kng 3274 kng + language Koonzime ozm 3275 ozm + language Koorete kqy 3276 kqy + language Kopar xop 3277 xop + language Kopkaka opk 3278 opk + language Korafe-Yegha kpr 3279 kpr + language Korak koz 3280 koz + language Korana kqz 3281 kqz + language Korandje kcy 3282 kcy + language Korean kor 3283 kor ko + language Korean Sign Language kvk 3284 kvk + language Koreguaje coe 3285 coe + language Koresh-e Rostam okh 3286 okh + language Korku kfq 3287 kfq + language Korlai Creole Portuguese vkp 3288 vkp + language Koro (Côte d'Ivoire) kfo 3289 kfo + language Koro (India) jkr 3290 jkr + language Koro (Papua New Guinea) kxr 3291 kxr + language Koro (Vanuatu) krf 3292 krf + language Koro Nulu vkn 3293 vkn + language Koro Wachi bqv 3294 bqv + language Koro Zuba vkz 3295 vkz + language Koromfé kfz 3296 kfz + language Koromira kqj 3297 kqj + language Koronadal Blaan bpr 3298 bpr + language Koroni xkq 3299 xkq + language Koropó xxr 3300 xxr + language Koroshi ktl 3301 ktl + language Korowai khe 3302 khe + language Korra Koraga kfd 3303 kfd + language Korubo xor 3304 xor + language Korupun-Sela kpq 3305 kpq + language Korwa kfp 3306 kfp + language Koryak kpy 3307 kpy + language Kosadle kiq 3308 kiq + language Kosarek Yale kkl 3309 kkl + language Kosena kze 3310 kze + language Koshin kid 3311 kid + language Kosraean kos 3312 kos + language Kota (Gabon) koq 3313 koq + language Kota (India) kfe 3314 kfe + language Kota Bangun Kutai Malay mqg 3315 mqg + language Kota Marudu Talantang grm 3316 grm + language Kotafon Gbe kqk 3317 kqk + language Kotava avk 3318 avk + language Koti eko 3319 eko + language Kott zko 3320 zko + language Kou snz 3321 snz + language Kouya kyf 3322 kyf + language Kovai kqb 3323 kqb + language Kove kvc 3324 kvc + language Kowaki xow 3325 xow + language Kowiai kwh 3326 kwh + language Koy Sanjaq Surat kqd 3327 kqd + language Koya kff 3328 kff + language Koyaga kga 3329 kga + language Koyo koh 3330 koh + language Koyra Chiini Songhay khq 3331 khq + language Koyraboro Senni Songhai ses 3332 ses + language Koyukon koy 3333 koy + language Kpagua kuw 3334 kuw + language Kpala kpl 3335 kpl + language Kpan kpk 3336 kpk + language Kpasam pbn 3337 pbn + language Kpati koc 3338 koc + language Kpatili kym 3339 kym + language Kpeego cpo 3340 cpo + language Kpelle kpe 3341 kpe + language Kpessi kef 3342 kef + language Kplang kph 3343 kph + language Krache kye 3344 kye + language Krahô xra 3345 xra + language Kraol rka 3346 rka + language Krenak kqq 3347 kqq + language Krevinian zkv 3348 zkv + language Kreye xre 3349 xre + language Kriang ngt 3350 ngt + language Krikati-Timbira xri 3351 xri + language Krio kri 3352 kri + language Kriol rop 3353 rop + language Krisa ksi 3354 ksi + language Krobu kxb 3355 kxb + language Krongo kgo 3356 kgo + language Krung krr 3357 krr + language Krymchak jct 3358 jct + language Kryts kry 3359 kry + language Kua tyu 3360 tyu + language Kua-nsi ykn 3361 ykn + language Kuamasi yku 3362 yku + language Kuan uan 3363 uan + language Kuanhua xnh 3364 xnh + language Kuanua ksd 3365 ksd + language Kuanyama, Kwanyama kua 3366 kua Kuanyama kj Kwanyama + language Kubachi ugh 3367 ugh + language Kube kgf 3368 kgf + language Kubi kof 3369 kof + language Kubo jko 3370 jko + language Kubu kvb 3371 kvb + language Kucong lkc 3372 lkc + language Kudiya kfg 3373 kfg + language Kudmali kyw 3374 kyw + language Kudu-Camo kov 3375 kov + language Kufr Qassem Sign Language (KQSL) sqx 3376 sqx + language Kugama kow 3377 kow + language Kugbo kes 3378 kes + language Kugu-Muminh xmh 3379 xmh + language Kui (India) uki 3380 uki + language Kui (Indonesia) kvd 3381 kvd + language Kuijau dkr 3382 dkr + language Kuikúro-Kalapálo kui 3383 kui + language Kujarge vkj 3384 vkj + language Kuk kfn 3385 kfn + language Kukatja kux 3386 kux + language Kuke ght 3387 ght + language Kukele kez 3388 kez + language Kukna kex 3389 kex + language Kuku ukv 3390 ukv + language Kuku-Mangk xmq 3391 xmq + language Kuku-Mu'inh xmp 3392 xmp + language Kuku-Ugbanh ugb 3393 ugb + language Kuku-Uwanh uwa 3394 uwa + language Kuku-Yalanji gvn 3395 gvn + language Kula tpg 3396 tpg + language Kulere kul 3397 kul + language Kulfa kxj 3398 kxj + language Kulina Pano xpk 3399 xpk + language Kulisusu vkl 3400 vkl + language Kullu Pahari kfx 3401 kfx + language Kulon uon 3402 uon + language Kulung (Nepal) kle 3403 kle + language Kulung (Nigeria) bbu 3404 bbu + language Kumalu ksl 3405 ksl + language Kumam kdi 3406 kdi + language Kuman (Papua New Guinea) kue 3407 kue + language Kuman (Russia) qwm 3408 qwm + language Kumaoni kfy 3409 kfy + language Kumarbhag Paharia kmj 3410 kmj + language Kumba ksm 3411 ksm + language Kumbainggar kgs 3412 kgs + language Kumbaran wkb 3413 wkb + language Kumbewaha xks 3414 xks + language Kumhali kra 3415 kra + language Kumiai dih 3416 dih + language Kumukio kuo 3417 kuo + language Kumyk kum 3418 kum + language Kumzari zum 3419 zum + language Kunama kun 3420 kun + language Kunbarlang wlg 3421 wlg + language Kunda kdn 3422 kdn + language Kundal Shahi shd 3423 shd + language Kunduvadi wku 3424 wku + language Kung kfl 3425 kfl + language Kung-Ekoka knw 3426 knw + language Kungarakany ggk 3427 ggk + language Kungardutyi gdt 3428 gdt + language Kunggari kgl 3429 kgl + language Kungkari lku 3430 lku + language Kuni kse 3431 kse + language Kuni-Boazi kvg 3432 kvg + language Kunigami xug 3433 xug + language Kunimaipa kup 3434 kup + language Kunja pep 3435 pep + language Kunjen kjn 3436 kjn + language Kunyi njx 3437 njx + language Kunza kuz 3438 kuz + language Kuo xuo 3439 xuo + language Kuot kto 3440 kto + language Kupa kug 3441 kug + language Kupang Malay mkn 3442 mkn + language Kupia key 3443 key + language Kupsabiny kpz 3444 kpz + language Kur kuv 3445 kuv + language Kura Ede Nago nqk 3446 nqk + language Kurama krh 3447 krh + language Kuranko knk 3448 knk + language Kurdish kur 3449 kur ku + language Kuri nbn 3450 nbn + language Kuria kuj 3451 kuj + language Kurichiya kfh 3452 kfh + language Kurmukar kfv 3453 kfv + language Kurnai unn 3454 unn + language Kurrama vku 3455 vku + language Kurti ktm 3456 ktm + language Kurtokha xkz 3457 xkz + language Kurudu kjr 3458 kjr + language Kurukh kru 3459 kru + language Kuruáya kyr 3460 kyr + language Kusaal kus 3461 kus + language Kusaghe ksg 3462 ksg + language Kushi kuh 3463 kuh + language Kusu ksv 3464 ksv + language Kusunda kgg 3465 kgg + language Kutenai kut 3466 kut + language Kutep kub 3467 kub + language Kuthant xut 3468 xut + language Kutong skm 3469 skm + language Kutto kpa 3470 kpa + language Kutu kdc 3471 kdc + language Kuturmi khj 3472 khj + language Kuuk Thaayorre thd 3473 thd + language Kuuk-Yak uky 3474 uky + language Kuuku-Ya'u kuy 3475 kuy + language Kuvale olu 3476 olu + language Kuvi kxv 3477 kxv + language Kuwaa blh 3478 blh + language Kuwaataay cwt 3479 cwt + language Kuwema woa 3480 woa + language Kuy kdt 3481 kdt + language Kven Finnish fkv 3482 fkv + language Kw'adza wka 3483 wka + language Kwa kwb 3484 kwb + language Kwa' bko 3485 bko + language Kwaami ksq 3486 ksq + language Kwadi kwz 3487 kwz + language Kwaio kwd 3488 kwd + language Kwaja kdz 3489 kdz + language Kwakiutl kwk 3490 kwk + language Kwakum kwu 3491 kwu + language Kwalhioqua-Tlatskanai qwt 3492 qwt + language Kwama kmq 3493 kmq + language Kwambi kwm 3494 kwm + language Kwamera tnk 3495 tnk + language Kwami ktf 3496 ktf + language Kwamtim One okk 3497 okk + language Kwandu xdo 3498 xdo + language Kwang kvi 3499 kvi + language Kwanga kwj 3500 kwj + language Kwangali kwn 3501 kwn + language Kwanja knp 3502 knp + language Kwara'ae kwf 3503 kwf + language Kwasio nmg 3504 nmg + language Kwaya kya 3505 kya + language Kwaza xwa 3506 xwa + language Kwegu xwg 3507 xwg + language Kwer kwr 3508 kwr + language Kwerba kwe 3509 kwe + language Kwerba Mamberamo xwr 3510 xwr + language Kwere cwe 3511 cwe + language Kwerisa kkb 3512 kkb + language Kwese kws 3513 kws + language Kwesten kwt 3514 kwt + language Kwini gww 3515 gww + language Kwinsu kuc 3516 kuc + language Kwinti kww 3517 kww + language Kwoma kmo 3518 kmo + language Kwomtari kwo 3519 kwo + language Kxoe xuu 3520 xuu + language Kyak bka 3521 bka + language Kyaka kyc 3522 kyc + language Kyan-Karyaw Naga nqq 3523 nqq + language Kyanga tye 3524 tye + language Kyenele kql 3525 kql + language Kyerung kgy 3526 kgy + language Kâte kmg 3527 kmg + language Kélé keb 3528 keb + language Kölsch ksh 3529 ksh + language Kɛlɛngaxo Bozo bzx 3530 bzx + language La'bi lbi 3531 lbi + language Laal gdm 3532 gdm + language Laari ldi 3533 ldi + language Laarim loh 3534 loh + language Laba lau 3535 lau + language Label lbb 3536 lbb + language Labir jku 3537 jku + language Labo mwi 3538 mwi + language Labo Phowa ypb 3539 ypb + language Labu lbu 3540 lbu + language Labuk-Kinabatangan Kadazan dtb 3541 dtb + language Lacandon lac 3542 lac + language Lachi lbt 3543 lbt + language Lachiguiri Zapotec zpa 3544 zpa + language Lachixío Zapotec zpl 3545 zpl + language Ladakhi lbj 3546 lbj + language Ladin lld 3547 lld + language Ladino lad 3548 lad + language Ladji Ladji llj 3549 llj + language Laeko-Libuat lkl 3550 lkl + language Lafofa laf 3551 laf + language Laghu lgb 3552 lgb + language Laghuu lgh 3553 lgh + language Lagwan kot 3554 kot + language Laha (Indonesia) lhh 3555 lhh + language Laha (Viet Nam) lha 3556 lha + language Lahanan lhn 3557 lhn + language Lahnda lah 3558 lah + language Lahta Karen kvt 3559 kvt + language Lahu lhu 3560 lhu + language Lahu Shi lhi 3561 lhi + language Lahul Lohar lhl 3562 lhl + language Laimbue lmx 3563 lmx + language Laitu Chin clj 3564 clj + language Laiyolo lji 3565 lji + language Lak lbe 3566 lbe + language Laka (Chad) lap 3567 lap + language Lakalei lka 3568 lka + language Lake Miwok lmw 3569 lmw + language Lakha lkh 3570 lkh + language Laki lki 3571 lki + language Lakkia lbc 3572 lbc + language Lakon lkn 3573 lkn + language Lakondê lkd 3574 lkd + language Lakota lkt 3575 lkt + language Lakota Dida dic 3576 dic + language Lakurumau lxm 3577 lxm + language Lala nrz 3578 nrz + language Lala-Bisa leb 3579 leb + language Lala-Roba lla 3580 lla + language Lalana Chinantec cnl 3581 cnl + language Lalia lal 3582 lal + language Lama (Togo) las 3583 las + language Lama Bai lay 3584 lay + language Lamaholot slp 3585 slp + language Lamalama lby 3586 lby + language Lamalera lmr 3587 lmr + language Lamang hia 3588 hia + language Lamatuka lmq 3589 lmq + language Lamba lam 3590 lam + language Lambadi lmn 3591 lmn + language Lambayeque Quechua quf 3592 quf + language Lambichhong lmh 3593 lmh + language Lamboya lmy 3594 lmy + language Lambya lai 3595 lai + language Lame bma 3596 bma + language Lamenu lmu 3597 lmu + language Lamja-Dengsa-Tola ldh 3598 ldh + language Lamkang lmk 3599 lmk + language Lamma lev 3600 lev + language Lamnso' lns 3601 lns + language Lamogai lmg 3602 lmg + language Lampung Api ljp 3603 ljp + language Lampung Nyo abl 3604 abl + language Lamu llh 3605 llh + language Lanas Lobu ruu 3606 ruu + language Landoma ldm 3607 ldm + language Lang'e yne 3608 yne + language Langam lnm 3609 lnm + language Langbashe lna 3610 lna + language Langnian Buyang yln 3611 yln + language Lango (South Sudan) lgo 3612 lgo + language Lango (Uganda) laj 3613 laj + language Langobardic lng 3614 lng + language Langue des signes de Belgique Francophone sfb 3615 sfb + language Lanima lnw 3616 lnw + language Lanoh lnh 3617 lnh + language Lao lao 3618 lao lo + language Lao Naga nlq 3619 nlq + language Laomian lwm 3620 lwm + language Laopang lbg 3621 lbg + language Laos Sign Language lso 3622 lso + language Lapaguía-Guivini Zapotec ztl 3623 ztl + language Laragia lrg 3624 lrg + language Larantuka Malay lrt 3625 lrt + language Lardil lbz 3626 lbz + language Larevat lrv 3627 lrv + language Large Flowery Miao hmd 3628 hmd + language Lari lrl 3629 lrl + language Larike-Wakasihu alo 3630 alo + language Laro lro 3631 lro + language Larteh lar 3632 lar + language Laru lan 3633 lan + language Las Delicias Zapotec zcd 3634 zcd + language Lasalimu llm 3635 llm + language Lasgerdi lsa 3636 lsa + language Lashi lsi 3637 lsi + language Lasi lss 3638 lss + language Late Middle Chinese ltc 3639 ltc + language Latgalian ltg 3640 ltg + language Latin lat 3641 lat la + language Latu ltu 3642 ltu + language Latundê ltn 3643 ltn + language Latvian lav 3644 lav lv + language Latvian Sign Language lsl 3645 lsl + language Lau llu 3646 llu + language Laua luf 3647 luf + language Lauan llx 3648 llx + language Lauje law 3649 law + language Laura lur 3650 lur + language Laurentian lre 3651 lre + language Lautu Chin clt 3652 clt + language Lavatbura-Lamusong lbv 3653 lbv + language Laven lbo 3654 lbo + language Lavi lvi 3655 lvi + language Lavukaleve lvk 3656 lvk + language Lawangan lbx 3657 lbx + language Lawu lwu 3658 lwu + language Lawunuia tgi 3659 tgi + language Layakha lya 3660 lya + language Laz lzz 3661 lzz + language Lealao Chinantec cle 3662 cle + language Leco lec 3663 lec + language Ledo Kaili lew 3664 lew + language Leelau ldk 3665 ldk + language Lefa lfa 3666 lfa + language Lega-Mwenga lgm 3667 lgm + language Lega-Shabunda lea 3668 lea + language Legbo agb 3669 agb + language Legenyem lcc 3670 lcc + language Lehali tql 3671 tql + language Lehalurup urr 3672 urr + language Lehar cae 3673 cae + language Leinong Naga lzn 3674 lzn + language Leipon lek 3675 lek + language Lelak llk 3676 llk + language Lele (Chad) lln 3677 lln + language Lele (Democratic Republic of Congo) lel 3678 lel + language Lele (Guinea) llc 3679 llc + language Lele (Papua New Guinea) lle 3680 lle + language Lelemi lef 3681 lef + language Lelepa lpa 3682 lpa + language Lembena leq 3683 leq + language Lemerig lrz 3684 lrz + language Lemio lei 3685 lei + language Lemnian xle 3686 xle + language Lemolang ley 3687 ley + language Lemoro ldj 3688 ldj + language Lenakel tnl 3689 tnl + language Lenca len 3690 len + language Lendu led 3691 led + language Lengilu lgi 3692 lgi + language Lengo lgr 3693 lgr + language Lengola lej 3694 lej + language Leningitij lnj 3695 lnj + language Lenje leh 3696 leh + language Lenkau ler 3697 ler + language Lenyima ldg 3698 ldg + language Lepcha lep 3699 lep + language Lepki lpe 3700 lpe + language Lepontic xlp 3701 xlp + language Lere gnh 3702 gnh + language Lese les 3703 les + language Lesing-Gelimi let 3704 let + language Letemboi nms 3705 nms + language Leti (Cameroon) leo 3706 leo + language Leti (Indonesia) lti 3707 lti + language Levantine Arabic apc 3708 apc + language Levuka lvu 3709 lvu + language Lewo lww 3710 lww + language Lewo Eleng lwe 3711 lwe + language Lewotobi lwt 3712 lwt + language Leyigha ayi 3713 ayi + language Lezghian lez 3714 lez + language Lhokpu lhp 3715 lhp + language Lhomi lhm 3716 lhm + language Li'o ljl 3717 ljl + language Liabuku lix 3718 lix + language Liana-Seti ste 3719 ste + language Liangmai Naga njn 3720 njn + language Lianshan Zhuang zln 3721 zln + language Liberia Kpelle xpe 3722 xpe + language Liberian English lir 3723 lir + language Libido liq 3724 liq + language Libinza liz 3725 liz + language Libon Bikol lbl 3726 lbl + language Liburnian xli 3727 xli + language Libyan Arabic ayl 3728 ayl + language Libyan Sign Language lbs 3729 lbs + language Lidzonka add 3730 add + language Ligbi lig 3731 lig + language Ligenza lgz 3732 lgz + language Ligurian lij 3733 lij + language Ligurian (Ancient) xlg 3734 xlg + language Lihir lih 3735 lih + language Lijili mgi 3736 mgi + language Lika lik 3737 lik + language Liki lio 3738 lio + language Likila lie 3739 lie + language Likuba kxx 3740 kxx + language Likum lib 3741 lib + language Likwala kwc 3742 kwc + language Lilau lll 3743 lll + language Lillooet lil 3744 lil + language Limassa bme 3745 bme + language Limbu lif 3746 lif + language Limbum lmp 3747 lmp + language Limburgan, Limburger, Limburgish lim 3748 lim Limburgan Limburger Limburgish li + language Limi ylm 3749 ylm + language Limilngan lmc 3750 lmc + language Limos Kalinga kmk 3751 kmk + language Linear A lab 3752 lab + language Lingala lin 3753 lin ln + language Lingao onb 3754 onb + language Lingarak lgk 3755 lgk + language Lingua Franca pml 3756 pml + language Lingua Franca Nova lfn 3757 lfn + language Lipan Apache apl 3758 apl + language Lipo lpo 3759 lpo + language Lisabata-Nuniali lcs 3760 lcs + language Lisela lcl 3761 lcl + language Lish lsh 3762 lsh + language Lishana Deni lsd 3763 lsd + language Lishanid Noshan aij 3764 aij + language Lishán Didán trg 3765 trg + language Lisu lis 3766 lis + language Literary Chinese lzh 3767 lzh + language Lithuanian lit 3768 lit lt + language Lithuanian Sign Language lls 3769 lls + language Litzlitz lzl 3770 lzl + language Liujiang Zhuang zlj 3771 zlj + language Liuqian Zhuang zlq 3772 zlq + language Liv liv 3773 liv + language Livvi olo 3774 olo + language Lo-Toga lht 3775 lht + language Loarki lrk 3776 lrk + language Lobala loq 3777 loq + language Lobi lob 3778 lob + language Lodhi lbm 3779 lbm + language Logba lgq 3780 lgq + language Logir lqr 3781 lqr + language Logo log 3782 log + language Logol lof 3783 lof + language Logooli rag 3784 rag + language Logorik liu 3785 liu + language Lohorung lbr 3786 lbr + language Loja Highland Quichua qvj 3787 qvj + language Lojban jbo 3788 jbo + language Lokaa yaz 3789 yaz + language Loke loy 3790 loy + language Loko lok 3791 lok + language Lokoya lky 3792 lky + language Lola lcd 3793 lcd + language Lolak llq 3794 llq + language Lole llg 3795 llg + language Lolo llb 3796 llb + language Loloda loa 3797 loa + language Lolopo ycl 3798 ycl + language Loma (Côte d'Ivoire) loi 3799 loi + language Loma (Liberia) lom 3800 lom + language Lomaiviti lmv 3801 lmv + language Lomavren rmi 3802 rmi + language Lombard lmo 3803 lmo + language Lombi lmi 3804 lmi + language Lombo loo 3805 loo + language Lomwe ngl 3806 ngl + language Loncong lce 3807 lce + language Long Phuri Naga lpn 3808 lpn + language Long Wat ttw 3809 ttw + language Longgu lgu 3810 lgu + language Longto wok 3811 wok + language Longuda lnu 3812 lnu + language Loniu los 3813 los + language Lonwolwol crc 3814 crc + language Lonzo lnz 3815 lnz + language Loo ldo 3816 ldo + language Lopa lop 3817 lop + language Lopi lov 3818 lov + language Lopit lpx 3819 lpx + language Lorang lrn 3820 lrn + language Lorediakarkar lnn 3821 lnn + language Loreto-Ucayali Spanish spq 3822 spq + language Lote uvl 3823 uvl + language Lotha Naga njh 3824 njh + language Lotud dtr 3825 dtr + language Lou loj 3826 loj + language Louisiana Creole lou 3827 lou + language Loun lox 3828 lox + language Loup A xlo 3829 xlo + language Loup B xlb 3830 xlb + language Low German nds 3831 nds + language Lower Burdekin xbb 3832 xbb + language Lower Chehalis cea 3833 cea + language Lower Grand Valley Dani dni 3834 dni + language Lower Nossob nsb 3835 nsb + language Lower Silesian sli 3836 sli + language Lower Sorbian dsb 3837 dsb + language Lower Southern Aranda axl 3838 axl + language Lower Ta'oih tto 3839 tto + language Lower Tanana taa 3840 taa + language Lowland Oaxaca Chontal clo 3841 clo + language Lowland Tarahumara tac 3842 tac + language Loxicha Zapotec ztp 3843 ztp + language Lozi loz 3844 loz + language Luang lex 3845 lex + language Luba-Katanga lub 3846 lub lu + language Luba-Lulua lua 3847 lua + language Lubila kcc 3848 kcc + language Lubu lcf 3849 lcf + language Lubuagan Kalinga knb 3850 knb + language Luchazi lch 3851 lch + language Lucumi luq 3852 luq + language Ludian lud 3853 lud + language Lufu ldq 3854 ldq + language Lugbara lgg 3855 lgg + language Luguru ruf 3856 ruf + language Luhu lcq 3857 lcq + language Luimbi lum 3858 lum + language Luiseno lui 3859 lui + language Lukpa dop 3860 dop + language Lule ule 3861 ule + language Lule Sami smj 3862 smj + language Lumba-Yakkha luu 3863 luu + language Lumbu lup 3864 lup + language Lumun lmd 3865 lmd + language Luna luj 3866 luj + language Lunanakha luk 3867 luk + language Lunda lun 3868 lun + language Lundayeh lnd 3869 lnd + language Lungalunga vmg 3870 vmg + language Lungga lga 3871 lga + language Luo (Cameroon) luw 3872 luw + language Luo (Kenya and Tanzania) luo 3873 luo + language Luopohe Hmong hml 3874 hml + language Luri ldd 3875 ldd + language Lusengo lse 3876 lse + language Lushai lus 3877 lus + language Lushootseed lut 3878 lut + language Lusi khl 3879 khl + language Lusitanian xls 3880 xls + language Lutos ndy 3881 ndy + language Luvale lue 3882 lue + language Luwati luv 3883 luv + language Luwo lwo 3884 lwo + language Luxembourgish, Letzeburgesch ltz 3885 ltz Luxembourgish lb Letzeburgesch + language Luyana lyn 3886 lyn + language Luyia luy 3887 luy + language Lwalu lwa 3888 lwa + language Lwel lvl 3889 lvl + language Lycian xlc 3890 xlc + language Lydian xld 3891 xld + language Lyngngam lyg 3892 lyg + language Lyélé lee 3893 lee + language Láadan ldn 3894 ldn + language Láá Láá Bwamu bwj 3895 bwj + language Lü khb 3896 khb + language Ma (Democratic Republic of Congo) msj 3897 msj + language Ma (Papua New Guinea) mjn 3898 mjn + language Ma Manda skc 3899 skc + language Ma'anyan mhy 3900 mhy + language Ma'di mhi 3901 mhi + language Ma'ya slz 3902 slz + language Maa cma 3903 cma + language Maaka mew 3904 mew + language Maasina Fulfulde ffm 3905 ffm + language Maay ymm 3906 ymm + language Maba (Chad) mde 3907 mde + language Maba (Indonesia) mqa 3908 mqa + language Mabaale mmz 3909 mmz + language Mabaan mfz 3910 mfz + language Mabaka Valley Kalinga kkg 3911 kkg + language Mabire muj 3912 muj + language Maca mca 3913 mca + language Macaguaje mcl 3914 mcl + language Macaguán mbn 3915 mbn + language Macanese mzs 3916 mzs + language Macedo-Romanian rup 3917 rup + language Macedonian mkd 3918 mkd mac mk + language Machame jmc 3919 jmc + language Machiguenga mcb 3920 mcb + language Machinere mpd 3921 mpd + language Machinga mvw 3922 mvw + language Maco wpc 3923 wpc + language Macuna myy 3924 myy + language Macushi mbc 3925 mbc + language Mada (Cameroon) mxu 3926 mxu + language Mada (Nigeria) mda 3927 mda + language Madagascar Sign Language mzc 3928 mzc + language Madak mmx 3929 mmx + language Madhi Madhi dmd 3930 dmd + language Madi grg 3931 grg + language Madurese mad 3932 mad + language Mae mme 3933 mme + language Maek hmk 3934 hmk + language Maeng Itneg itt 3935 itt + language Mafa maf 3936 maf + language Mafea mkv 3937 mkv + language Mag-Indi Ayta blx 3938 blx + language Mag-antsi Ayta sgb 3939 sgb + language Magahi mag 3940 mag + language Magbukun Ayta ayt 3941 ayt + language Magdalena Peñasco Mixtec xtm 3942 xtm + language Magoma gmx 3943 gmx + language Magori zgr 3944 zgr + language Maguindanaon mdh 3945 mdh + language Magɨ (Madang Province) gkd 3946 gkd + language Magɨyi gmg 3947 gmg + language Mahali mjx 3948 mjx + language Mahasu Pahari bfz 3949 bfz + language Mahican mjy 3950 mjy + language Mahongwe mhb 3951 mhb + language Mahou mxx 3952 mxx + language Mai Brat ayz 3953 ayz + language Maia sks 3954 sks + language Maiadomu mzz 3955 mzz + language Maiani tnh 3956 tnh + language Maii mmm 3957 mmm + language Mailu mgu 3958 mgu + language Maindo cwb 3959 cwb + language Mainfränkisch vmf 3960 vmf + language Mainstream Kenyah xkl 3961 xkl + language Mairasi zrs 3962 zrs + language Maisin mbq 3963 mbq + language Maithili mai 3964 mai + language Maiwa (Indonesia) wmm 3965 wmm + language Maiwa (Papua New Guinea) mti 3966 mti + language Maiwala mum 3967 mum + language Majang mpe 3968 mpe + language Majera xmj 3969 xmj + language Majhi mjz 3970 mjz + language Majhwar mmj 3971 mmj + language Majukayang Kalinga kmd 3972 kmd + language Mak (China) mkg 3973 mkg + language Mak (Nigeria) pbl 3974 pbl + language Makaa mcp 3975 mcp + language Makah myh 3976 myh + language Makalero mjb 3977 mjb + language Makasae mkz 3978 mkz + language Makasar mak 3979 mak + language Makassar Malay mfp 3980 mfp + language Makayam aup 3981 aup + language Makhuwa vmw 3982 vmw + language Makhuwa-Marrevone xmc 3983 xmc + language Makhuwa-Meetto mgh 3984 mgh + language Makhuwa-Moniga mhm 3985 mhm + language Makhuwa-Saka xsq 3986 xsq + language Makhuwa-Shirima vmk 3987 vmk + language Maklew mgf 3988 mgf + language Makolkol zmh 3989 zmh + language Makonde kde 3990 kde + language Maku'a lva 3991 lva + language Makuri Naga jmn 3992 jmn + language Makuráp mpu 3993 mpu + language Makwe ymk 3994 ymk + language Makyan Naga umn 3995 umn + language Mal mlf 3996 mlf + language Mal Paharia mkb 3997 mkb + language Mala (Nigeria) ruy 3998 ruy + language Mala (Papua New Guinea) ped 3999 ped + language Mala Malasar ima 4000 ima + language Malaccan Creole Malay ccm 4001 ccm + language Malaccan Creole Portuguese mcm 4002 mcm + language Malagasy mlg 4003 mlg mg + language Malak Malak mpb 4004 mpb + language Malalamai mmt 4005 mmt + language Malango mln 4006 mln + language Malankuravan mjo 4007 mjo + language Malapandaram mjp 4008 mjp + language Malaryan mjq 4009 mjq + language Malas mkr 4010 mkr + language Malasar ymr 4011 ymr + language Malavedan mjr 4012 mjr + language Malawi Lomwe lon 4013 lon + language Malawi Sena swk 4014 swk + language Malawian Sign Language lws 4015 lws + language Malay msa 4016 msa may ms Malay Malay (macrolanguage) + language Malay (individual language) zlm 4017 zlm + language Malayalam mal 4018 mal ml + language Malayic Dayak xdy 4019 xdy + language Malaynon mlz 4020 mlz + language Malayo mbp 4021 mbp + language Malaysian Sign Language xml 4022 xml + language Malba Birifor bfo 4023 bfo + language Maldivian, Dhivehi, Divehi div 4024 div dv Maldivian Dhivehi Divehi + language Male (Ethiopia) mdy 4025 mdy + language Male (Papua New Guinea) mdc 4026 mdc + language Malecite-Passamaquoddy pqm 4027 pqm + language Maleng pkt 4028 pkt + language Maleu-Kilenge mgl 4029 mgl + language Malfaxal mlx 4030 mlx + language Malgana vml 4031 vml + language Malgbe mxf 4032 mxf + language Mali gcc 4033 gcc + language Malila mgq 4034 mgq + language Malimba mzd 4035 mzd + language Malimpung mli 4036 mli + language Malinaltepec Me'phaa tcf 4037 tcf + language Malo mla 4038 mla + language Malol mbk 4039 mbk + language Maltese mlt 4040 mlt mt + language Maltese Sign Language mdl 4041 mdl + language Malua Bay mll 4042 mll + language Malvi mup 4043 mup + language Malyangapa yga 4044 yga + language Maléku Jaíka gut 4045 gut + language Mam mam 4046 mam + language Mama mma 4047 mma + language Mamaa mhf 4048 mhf + language Mamaindé wmd 4049 wmd + language Mamanwa mmn 4050 mmn + language Mamara Senoufo myk 4051 myk + language Mamasa mqj 4052 mqj + language Mambae mgm 4053 mgm + language Mambai mcs 4054 mcs + language Mamboru mvd 4055 mvd + language Mambwe-Lungu mgr 4056 mgr + language Mampruli maw 4057 maw + language Mamuju mqx 4058 mqx + language Mamulique emm 4059 emm + language Mamusi kdf 4060 kdf + language Mamvu mdi 4061 mdi + language Man Met mml 4062 mml + language Manado Malay xmm 4063 xmm + language Manam mva 4064 mva + language Manambu mle 4065 mle + language Manangba nmm 4066 nmm + language Manangkari znk 4067 znk + language Manchu mnc 4068 mnc + language Manda (Australia) zma 4069 zma + language Manda (India) mha 4070 mha + language Manda (Tanzania) mgs 4071 mgs + language Mandahuaca mht 4072 mht + language Mandaic mid 4073 mid + language Mandan mhq 4074 mhq + language Mandandanyi zmk 4075 zmk + language Mandar mdr 4076 mdr + language Mandara tbf 4077 tbf + language Mandari mqu 4078 mqu + language Mandarin Chinese cmn 4079 cmn + language Mandaya mry 4080 mry + language Mandeali mjl 4081 mjl + language Mander mqr 4082 mqr + language Mandingo man 4083 man + language Mandinka mnk 4084 mnk + language Mandjak mfv 4085 mfv + language Mandobo Atas aax 4086 aax + language Mandobo Bawah bwp 4087 bwp + language Manem jet 4088 jet + language Mang zng 4089 zng + language Manga Kanuri kby 4090 kby + language Mangala mem 4091 mem + language Mangareva mrv 4092 mrv + language Mangarrayi mpc 4093 mpc + language Mangas zns 4094 zns + language Mangayat myj 4095 myj + language Mangbetu mdj 4096 mdj + language Mangbutu mdk 4097 mdk + language Mangerr zme 4098 zme + language Mangga Buang mmo 4099 mmo + language Manggarai mqy 4100 mqy + language Mango mge 4101 mge + language Mangole mqc 4102 mqc + language Mangseng mbh 4103 mbh + language Mangue mom 4104 mom + language Manichaean Middle Persian xmn 4105 xmn + language Manide abd 4106 abd + language Manikion mnx 4107 mnx + language Manipa mqp 4108 mqp + language Manipuri mni 4109 mni + language Mankanya knf 4110 knf + language Mankiyali nlm 4111 nlm + language Manna-Dora mju 4112 mju + language Mannan mjv 4113 mjv + language Mano mev 4114 mev + language Manombai woo 4115 woo + language Mansaka msk 4116 msk + language Mansi mns 4117 mns + language Mansoanka msw 4118 msw + language Manta myg 4119 myg + language Mantsi nty 4120 nty + language Manumanaw Karen kxf 4121 kxf + language Manx glv 4122 glv gv + language Manya mzj 4123 mzj + language Manyawa mny 4124 mny + language Manyika mxc 4125 mxc + language Manza mzv 4126 mzv + language Mao Naga nbi 4127 nbi + language Maonan mmd 4128 mmd + language Maore Comorian swb 4129 swb + language Mape mlh 4130 mlh + language Mapena mnm 4131 mnm + language Mapia mpy 4132 mpy + language Mapidian mpw 4133 mpw + language Mapos Buang bzh 4134 bzh + language Mapoyo mcg 4135 mcg + language Mapudungun arn 4136 arn + language Mapun sjm 4137 sjm + language Maquiritari mch 4138 mch + language Mara Chin mrh 4139 mrh + language Marachi lri 4140 lri + language Maraghei vmh 4141 vmh + language Maragus mrs 4142 mrs + language Maram Naga nma 4143 nma + language Marama lrm 4144 lrm + language Maranao mrw 4145 mrw + language Maranunggu zmr 4146 zmr + language Mararit mgb 4147 mgb + language Marathi (Marāṭhī) mar 4148 mar mr Marathi Marāṭhī + language Marau mvr 4149 mvr + language Marba mpg 4150 mpg + language Mardin Sign Language dsz 4151 dsz + language Maremgi mrx 4152 mrx + language Marenje vmr 4153 vmr + language Marfa mvu 4154 mvu + language Margany zmc 4155 zmc + language Marghi Central mrt 4156 mrt + language Marghi South mfm 4157 mfm + language Margos-Yarowilca-Lauricocha Quechua qvm 4158 qvm + language Margu mhg 4159 mhg + language Mari (East Sepik Province) mbx 4160 mbx + language Mari (Madang Province) hob 4161 hob + language Mari (Russia) chm 4162 chm + language Maria (India) mrr 4163 mrr + language Maria (Papua New Guinea) mds 4164 mds + language Maricopa mrc 4165 mrc + language Maridan zmd 4166 zmd + language Maridjabin zmj 4167 zmj + language Marik dad 4168 dad + language Marimanindji zmm 4169 zmm + language Marind mrz 4170 mrz + language Maring mbw 4171 mbw + language Maring Naga nng 4172 nng + language Maringarr zmt 4173 zmt + language Marino mrb 4174 mrb + language Mariri mqi 4175 mqi + language Maritime Sign Language nsr 4176 nsr + language Maritsauá msp 4177 msp + language Mariyedi zmy 4178 zmy + language Marka rkm 4179 rkm + language Markweeta enb 4180 enb + language Marma rmz 4181 rmz + language Marovo mvo 4182 mvo + language Marra mec 4183 mec + language Marriammu xru 4184 xru + language Marrithiyel mfr 4185 mfr + language Marrucinian umc 4186 umc + language Marshallese mah 4187 mah mh + language Marsian ims 4188 ims + language Martha's Vineyard Sign Language mre 4189 mre + language Marti Ke zmg 4190 zmg + language Martu Wangka mpj 4191 mpj + language Martuyhunira vma 4192 vma + language Maru mhx 4193 mhx + language Marwari mwr 4194 mwr + language Marwari (India) rwr 4195 rwr + language Marwari (Pakistan) mve 4196 mve + language Marúbo mzr 4197 mzr + language Masaaba myx 4198 myx + language Masadiit Itneg tis 4199 tis + language Masai mas 4200 mas + language Masalit mls 4201 mls + language Masana mcn 4202 mcn + language Masbatenyo msb 4203 msb + language Mashco Piro cuj 4204 cuj + language Mashi (Nigeria) jms 4205 jms + language Mashi (Zambia) mho 4206 mho + language Masikoro Malagasy msh 4207 msh + language Masimasi ism 4208 ism + language Masiwang bnf 4209 bnf + language Maskelynes klv 4210 klv + language Maslam msv 4211 msv + language Masmaje mes 4212 mes + language Massalat mdg 4213 mdg + language Massep mvs 4214 mvs + language Matagalpa mtn 4215 mtn + language Matal mfh 4216 mfh + language Matambwe wtb 4217 wtb + language Matbat xmt 4218 xmt + language Matengo mgv 4219 mgv + language Matepi mqe 4220 mqe + language Matigsalug Manobo mbt 4221 mbt + language Matipuhy mzo 4222 mzo + language Matngala zml 4223 zml + language Mato met 4224 met + language Mato Grosso Arára axg 4225 axg + language Mator mtm 4226 mtm + language Matsés mcf 4227 mcf + language Mattole mvb 4228 mvb + language Matu Chin hlt 4229 hlt + language Matukar mjk 4230 mjk + language Matumbi mgw 4231 mgw + language Matya Samo stj 4232 stj + language Matís mpq 4233 mpq + language Maung mph 4234 mph + language Mauritian Sign Language lsy 4235 lsy + language Mauwake mhl 4236 mhl + language Mawa (Chad) mcw 4237 mcw + language Mawa (Nigeria) wma 4238 wma + language Mawak mjj 4239 mjj + language Mawan mcz 4240 mcz + language Mawayana mzx 4241 mzx + language Mawchi mke 4242 mke + language Mawes mgk 4243 mgk + language Maxakalí mbl 4244 mbl + language Maxi Gbe mxl 4245 mxl + language Maya Samo sym 4246 sym + language Mayaguduna xmy 4247 xmy + language Mayangna yan 4248 yan + language Mayawali yxa 4249 yxa + language Mayeka myc 4250 myc + language Mayi-Kulan xyk 4251 xyk + language Mayi-Thakurti xyt 4252 xyt + language Mayi-Yapi xyj 4253 xyj + language Mayo mfy 4254 mfy + language Mayogo mdm 4255 mdm + language Mayoyao Ifugao ifu 4256 ifu + language Mazagway dkx 4257 dkx + language Mazaltepec Zapotec zpy 4258 zpy + language Mazanderani mzn 4259 mzn + language Mazatlán Mazatec vmz 4260 vmz + language Mazatlán Mixe mzl 4261 mzl + language Mba mfc 4262 mfc + language Mbala mdp 4263 mdp + language Mbalanhu lnb 4264 lnb + language Mbandja zmz 4265 zmz + language Mbangala mxg 4266 mxg + language Mbangi mgn 4267 mgn + language Mbangwe zmn 4268 zmn + language Mbara (Australia) mvl 4269 mvl + language Mbara (Chad) mpk 4270 mpk + language Mbariman-Gudhinma zmv 4271 zmv + language Mbati mdn 4272 mdn + language Mbato gwa 4273 gwa + language Mbay myb 4274 myb + language Mbe mfo 4275 mfo + language Mbe' mtk 4276 mtk + language Mbelime mql 4277 mql + language Mbere mdt 4278 mdt + language Mbesa zms 4279 zms + language Mbessa emz 4280 emz + language Mbo (Cameroon) mbo 4281 mbo + language Mbo (Democratic Republic of Congo) zmw 4282 zmw + language Mboi moi 4283 moi + language Mboko mdu 4284 mdu + language Mbole mdq 4285 mdq + language Mbonga xmb 4286 xmb + language Mbongno bgu 4287 bgu + language Mbosi mdw 4288 mdw + language Mbowe mxo 4289 mxo + language Mbre mka 4290 mka + language Mbudum xmd 4291 xmd + language Mbugu mhd 4292 mhd + language Mbugwe mgz 4293 mgz + language Mbuk bpc 4294 bpc + language Mbuko mqb 4295 mqb + language Mbukushu mhw 4296 mhw + language Mbula mna 4297 mna + language Mbula-Bwazza mbu 4298 mbu + language Mbule mlb 4299 mlb + language Mbulungish mbv 4300 mbv + language Mbum mdd 4301 mdd + language Mbunda mck 4302 mck + language Mbunga mgy 4303 mgy + language Mburku bbt 4304 bbt + language Mbwela mfu 4305 mfu + language Mbyá Guaraní gun 4306 gun + language Me'en mym 4307 mym + language Medebur mjm 4308 mjm + language Medefaidrin dmf 4309 dmf + language Media Lengua mue 4310 mue + language Median xme 4311 xme + language Mednyj Aleut mud 4312 mud + language Medumba byv 4313 byv + language Mefele mfj 4314 mfj + language Megam mef 4315 mef + language Megleno Romanian ruq 4316 ruq + language Mehek nux 4317 nux + language Mehináku mmh 4318 mmh + language Mehri gdq 4319 gdq + language Mekeo mek 4320 mek + language Mekmek mvk 4321 mvk + language Mekwei msf 4322 msf + language Mel-Khaonh hkn 4323 hkn + language Mele-Fila mxe 4324 mxe + language Melo mfx 4325 mfx + language Melpa med 4326 med + language Memoni mby 4327 mby + language Mendalam Kayan xkd 4328 xkd + language Mendankwe-Nkwen mfd 4329 mfd + language Mende (Papua New Guinea) sim 4330 sim + language Mende (Sierra Leone) men 4331 men + language Mengaka xmg 4332 xmg + language Mengen mee 4333 mee + language Mengisa mct 4334 mct + language Menka mea 4335 mea + language Menominee mez 4336 mez + language Mentawai mwv 4337 mwv + language Menya mcr 4338 mcr + language Meoswar mvx 4339 mvx + language Mer mnu 4340 mnu + language Meramera mxm 4341 mxm + language Merei lmb 4342 lmb + language Merey meq 4343 meq + language Meriam Mir ulk 4344 ulk + language Merlav mrm 4345 mrm + language Meroitic xmr 4346 xmr + language Meru mer 4347 mer + language Merwari wry 4348 wry + language Mesaka iyo 4349 iyo + language Mescalero-Chiricahua Apache apm 4350 apm + language Mese mci 4351 mci + language Meskwaki sac 4352 sac + language Mesme zim 4353 zim + language Mesmes mys 4354 mys + language Mesopotamian Arabic acm 4355 acm + language Mesqan mvz 4356 mvz + language Messapic cms 4357 cms + language Meta' mgo 4358 mgo + language Metlatónoc Mixtec mxv 4359 mxv + language Mewari mtr 4360 mtr + language Mewati wtm 4361 wtm + language Mexican Sign Language mfs 4362 mfs + language Meyah mej 4363 mej + language Mezontla Popoloca pbe 4364 pbe + language Mezquital Otomi ote 4365 ote + language Mfinu zmf 4366 zmf + language Mfumte nfu 4367 nfu + language Mgbolizhia gmz 4368 gmz + language Mi'kmaq mic 4369 mic + language Miahuatlán Zapotec zam 4370 zam + language Miami mia 4371 mia + language Mian mpt 4372 mpt + language Miani pla 4373 pla + language Michif crg 4374 crg + language Michigamea cmm 4375 cmm + language Michoacán Mazahua mmc 4376 mmc + language Michoacán Nahuatl ncl 4377 ncl + language Mid Grand Valley Dani dnt 4378 dnt + language Mid-Southern Banda bjo 4379 bjo + language Middle Armenian axm 4380 axm + language Middle Breton xbm 4381 xbm + language Middle Cornish cnx 4382 cnx + language Middle Dutch (ca. 1050-1350) dum 4383 dum + language Middle English (1100-1500) enm 4384 enm + language Middle French (ca. 1400-1600) frm 4385 frm + language Middle High German (ca. 1050-1500) gmh 4386 gmh + language Middle Hittite htx 4387 htx + language Middle Irish (900-1200) mga 4388 mga + language Middle Khmer (1400 to 1850 CE) xhm 4389 xhm + language Middle Korean (10th-16th cent.) okm 4390 okm + language Middle Low German gml 4391 gml + language Middle Mongolian xng 4392 xng + language Middle Newar nwx 4393 nwx + language Middle Watut mpl 4394 mpl + language Middle Welsh wlm 4395 wlm + language Midob mei 4396 mei + language Migaama mmy 4397 mmy + language Migabac mpp 4398 mpp + language Migum klm 4399 klm + language Miju-Mishmi mxj 4400 mxj + language Mikasuki mik 4401 mik + language Mili ymh 4402 ymh + language Miltu mlj 4403 mlj + language Miluk iml 4404 iml + language Milyan imy 4405 imy + language Min Bei Chinese mnp 4406 mnp + language Min Dong Chinese cdo 4407 cdo + language Min Nan Chinese nan 4408 nan + language Min Zhong Chinese czo 4409 czo + language Mina (Cameroon) hna 4410 hna + language Minaean inm 4411 inm + language Minang xrg 4412 xrg + language Minangkabau min 4413 min + language Minanibai mcv 4414 mcv + language Minaveha mvn 4415 mvn + language Minderico drc 4416 drc + language Mindiri mpn 4417 mpn + language Mingang Doso mko 4418 mko + language Mingrelian xmf 4419 xmf + language Minica Huitoto hto 4420 hto + language Minidien wii 4421 wii + language Minjungbal xjb 4422 xjb + language Minkin xxm 4423 xxm + language Minoan omn 4424 omn + language Minokok mqq 4425 mqq + language Minriq mnq 4426 mnq + language Mintil mzt 4427 mzt + language Minz Zhuang zgm 4428 zgm + language Miqie yiq 4429 yiq + language Mirandese mwl 4430 mwl + language Miraya Bikol rbl 4431 rbl + language Mirgan zrg 4432 zrg + language Miriti mmv 4433 mmv + language Miriwoong mep 4434 mep + language Miriwoong Sign Language rsm 4435 rsm + language Mirning gmr 4436 gmr + language Miship mjs 4437 mjs + language Misima-Panaeati mpx 4438 mpx + language Mising mrg 4439 mrg + language Mitla Zapotec zaw 4440 zaw + language Mitlatongo Mixtec vmm 4441 vmm + language Mittu mwu 4442 mwu + language Mituku zmq 4443 zmq + language Miu mpo 4444 mpo + language Miwa vmi 4445 vmi + language Mixed Great Andamanese gac 4446 gac + language Mixtepec Mixtec mix 4447 mix + language Mixtepec Zapotec zpm 4448 zpm + language Miya mkf 4449 mkf + language Miyako mvi 4450 mvi + language Miyakubo Sign Language ehs 4451 ehs + language Miyobe soy 4452 soy + language Mlabri mra 4453 mra + language Mlahsö lhs 4454 lhs + language Mlap kja 4455 kja + language Mlomp mlo 4456 mlo + language Mmaala mmu 4457 mmu + language Mmen bfm 4458 bfm + language Mo'da gbn 4459 gbn + language Moabite obm 4460 obm + language Moba mfq 4461 mfq + language Mobilian mod 4462 mod + language Mobumrin Aizi ahm 4463 ahm + language Mobwa Karen jkm 4464 jkm + language Mochi old 4465 old + language Mochica omc 4466 omc + language Mocho mhc 4467 mhc + language Mocoví moc 4468 moc + language Modang mxd 4469 mxd + language Modole mqo 4470 mqo + language Moere mvq 4471 mvq + language Mofu-Gudur mif 4472 mif + language Mogholi mhj 4473 mhj + language Mogofin mfg 4474 mfg + language Mogum mou 4475 mou + language Mohave mov 4476 mov + language Mohawk moh 4477 moh + language Mohegan-Pequot xpq 4478 xpq + language Moi (Congo) mow 4479 mow + language Moi (Indonesia) mxn 4480 mxn + language Moikodi mkp 4481 mkp + language Moingi mwz 4482 mwz + language Moji ymi 4483 ymi + language Mok mqt 4484 mqt + language Mokati wnb 4485 wnb + language Moken mwt 4486 mwt + language Mokerang mft 4487 mft + language Mokilese mkj 4488 mkj + language Moklen mkm 4489 mkm + language Mokole mkl 4490 mkl + language Mokpwe bri 4491 bri + language Moksela vms 4492 vms + language Moksha mdf 4493 mdf + language Molale mbe 4494 mbe + language Molbog pwm 4495 pwm + language Moldova Sign Language vsi 4496 vsi + language Molengue bxc 4497 bxc + language Molima mox 4498 mox + language Molmo One aun 4499 aun + language Molo zmo 4500 zmo + language Molof msl 4501 msl + language Moloko mlw 4502 mlw + language Mom Jango ver 4503 ver + language Moma myl 4504 myl + language Momare msz 4505 msz + language Mombo Dogon dmb 4506 dmb + language Mombum mso 4507 mso + language Momina mmb 4508 mmb + language Momuna mqf 4509 mqf + language Mon mnw 4510 mnw + language Monastic Sign Language mzg 4511 mzg + language Mondropolon npn 4512 npn + language Mondé mnd 4513 mnd + language Mongo lol 4514 lol + language Mongol mgt 4515 mgt + language Mongolia Buriat bxm 4516 bxm + language Mongolian mon 4517 mon mn + language Mongolian Sign Language msr 4518 msr + language Mongondow mog 4519 mog + language Moni mnz 4520 mnz + language Mono (Cameroon) mru 4521 mru + language Mono (Democratic Republic of Congo) mnh 4522 mnh + language Mono (Solomon Islands) mte 4523 mte + language Mono (USA) mnr 4524 mnr + language Monom moo 4525 moo + language Monsang Naga nmh 4526 nmh + language Montenegrin cnr 4527 cnr + language Montol mtl 4528 mtl + language Monumbo mxk 4529 mxk + language Monzombo moj 4530 moj + language Moo gwg 4531 gwg + language Moose Cree crm 4532 crm + language Mopán Maya mop 4533 mop + language Mor (Bomberai Peninsula) moq 4534 moq + language Mor (Mor Islands) mhz 4535 mhz + language Moraid msg 4536 msg + language Morawa mze 4537 mze + language Morelos Nahuatl nhm 4538 nhm + language Morerebi xmo 4539 xmo + language Moresada msx 4540 msx + language Mori Atas mzq 4541 mzq + language Mori Bawah xmz 4542 xmz + language Morigi mdb 4543 mdb + language Moriori rrm 4544 rrm + language Morisyen mfe 4545 mfe + language Moro mor 4546 mor + language Moroccan Arabic ary 4547 ary + language Moroccan Sign Language xms 4548 xms + language Morokodo mgc 4549 mgc + language Morom bdo 4550 bdo + language Moronene mqn 4551 mqn + language Morori mok 4552 mok + language Morouas mrp 4553 mrp + language Morrobalama umg 4554 umg + language Mortlockese mrl 4555 mrl + language Moru mgd 4556 mgd + language Mosimo mqv 4557 mqv + language Moskona mtj 4558 mtj + language Mossi mos 4559 mos + language Mota mtt 4560 mtt + language Motlav mlv 4561 mlv + language Motu meu 4562 meu + language Mouk-Aria mwh 4563 mwh + language Moundadan Chetty cty 4564 cty + language Mountain Koiali kpx 4565 kpx + language Mouwase jmw 4566 jmw + language Movima mzp 4567 mzp + language Moyadan Itneg ity 4568 ity + language Moyon Naga nmo 4569 nmo + language Mozambican Sign Language mzy 4570 mzy + language Mozarabic mxi 4571 mxi + language Mpade mpi 4572 mpi + language Mpalitjanh xpj 4573 xpj + language Mpi mpz 4574 mpz + language Mpiemo mcx 4575 mcx + language Mpinda pnd 4576 pnd + language Mpoto mpa 4577 mpa + language Mpotovoro mvt 4578 mvt + language Mpumpong mgg 4579 mgg + language Mpuono zmp 4580 zmp + language Mpur akc 4581 akc + language Mro-Khimi Chin cmr 4582 cmr + language Mru mro 4583 mro + language Mser kqx 4584 kqx + language Mt. Iraya Agta atl 4585 atl + language Mt. Iriga Agta agz 4586 agz + language Muak Sa-aak ukk 4587 ukk + language Mualang mtd 4588 mtd + language Mubami tsx 4589 tsx + language Mubi mub 4590 mub + language Muda ymd 4591 ymd + language Mudburra dmw 4592 dmw + language Mudhili Gadaba gau 4593 gau + language Mudu Koraga vmd 4594 vmd + language Muduga udg 4595 udg + language Mufian aoj 4596 aoj + language Mugom muk 4597 muk + language Muinane bmr 4598 bmr + language Mukha-Dora mmk 4599 mmk + language Mukulu moz 4600 moz + language Mulaha mfw 4601 mfw + language Mulam mlm 4602 mlm + language Mulao giu 4603 giu + language Mulgi mvh 4604 mvh + language Mullu Kurumba kpb 4605 kpb + language Multiple languages mul 4606 mul + language Muluridyi vmu 4607 vmu + language Mum kqa 4608 kqa + language Mumuye mzm 4609 mzm + language Muna mnb 4610 mnb + language Munda unx 4611 unx + language Mundabli boe 4612 boe + language Mundang mua 4613 mua + language Mundani mnf 4614 mnf + language Mundari unr 4615 unr + language Mundat mmf 4616 mmf + language Mundurukú myu 4617 myu + language Mungaka mhk 4618 mhk + language Munggui mth 4619 mth + language Mungkip mpv 4620 mpv + language Muniche myr 4621 myr + language Munit mtc 4622 mtc + language Munji mnj 4623 mnj + language Munsee umu 4624 umu + language Muong mtq 4625 mtq + language Mur Pano tkv 4626 tkv + language Muratayak asx 4627 asx + language Murik (Malaysia) mxr 4628 mxr + language Murik (Papua New Guinea) mtf 4629 mtf + language Murkim rmh 4630 rmh + language Murle mur 4631 mur + language Murrinh-Patha mwf 4632 mwf + language Mursi muz 4633 muz + language Murui Huitoto huu 4634 huu + language Murupi mqw 4635 mqw + language Muruwari zmu 4636 zmu + language Musak mmq 4637 mmq + language Musar mmi 4638 mmi + language Musasa smm 4639 smm + language Musey mse 4640 mse + language Musgu mug 4641 mug + language Mushungulu xma 4642 xma + language Musi mui 4643 mui + language Muskum mje 4644 mje + language Muslim Tat ttt 4645 ttt + language Musom msu 4646 msu + language Mussau-Emira emi 4647 emi + language Muthuvan muv 4648 muv + language Mutu tuc 4649 tuc + language Muyang muy 4650 muy + language Muyuw myw 4651 myw + language Muzi ymz 4652 ymz + language Mvanip mcj 4653 mcj + language Mvuba mxh 4654 mxh + language Mwaghavul sur 4655 sur + language Mwali Comorian wlc 4656 wlc + language Mwan moa 4657 moa + language Mwani wmw 4658 wmw + language Mwatebu mwa 4659 mwa + language Mwera (Chimwera) mwe 4660 mwe + language Mwera (Nyasa) mjh 4661 mjh + language Mwimbi-Muthambi mws 4662 mws + language Myanmar Sign Language ysm 4663 ysm + language Mycenaean Greek gmy 4664 gmy + language Myene mye 4665 mye + language Mysian yms 4666 yms + language Mzieme Naga nme 4667 nme + language Mághdì gmd 4668 gmd + language Máku xak 4669 xak + language Ménik tnr 4670 tnr + language Mískito miq 4671 miq + language Mócheno mhn 4672 mhn + language Mün Chin mwq 4673 mwq + language Mündü muh 4674 muh + language Māhārāṣṭri Prākrit pmh 4675 pmh + language Māori mri 4676 mri Māori Maori mao mi + language N'Ko nqo 4677 nqo + language Na nbt 4678 nbt + language Na-kara nck 4679 nck + language Naaba nao 4680 nao + language Naami bzv 4681 bzv + language Naasioi nas 4682 nas + language Naba mne 4683 mne + language Nabak naf 4684 naf + language Nabi mty 4685 mty + language Nachering ncd 4686 ncd + language Nadruvian ndf 4687 ndf + language Nadëb mbj 4688 mbj + language Nafaanra nfr 4689 nfr + language Nafi srf 4690 srf + language Nafri nxx 4691 nxx + language Nafusi jbn 4692 jbn + language Naga Pidgin nag 4693 nag + language Nagarchal nbg 4694 nbg + language Nage nxe 4695 nxe + language Nagumi ngv 4696 ngv + language Nahali nlx 4697 nlx + language Nahari nhh 4698 nhh + language Nai bio 4699 bio + language Najdi Arabic ars 4700 ars + language Naka'ela nae 4701 nae + language Nakai nkj 4702 nkj + language Nakame nib 4703 nib + language Nakanai nak 4704 nak + language Nake nbk 4705 nbk + language Naki mff 4706 mff + language Nakwi nax 4707 nax + language Nalca nlc 4708 nlc + language Nali nss 4709 nss + language Nalik nal 4710 nal + language Nalu naj 4711 naj + language Naluo Yi ylo 4712 ylo + language Nalögo nlz 4713 nlz + language Nama (Papua New Guinea) nmx 4714 nmx + language Namakura nmk 4715 nmk + language Namat nkm 4716 nkm + language Nambo ncm 4717 ncm + language Nambya nmq 4718 nmq + language Namia nnm 4719 nnm + language Namiae nvm 4720 nvm + language Namibian Sign Language nbs 4721 nbs + language Namla naa 4722 naa + language Namo mxw 4723 mxw + language Namonuito nmt 4724 nmt + language Namosi-Naitasiri-Serua bwb 4725 bwb + language Namuyi nmy 4726 nmy + language Nanai gld 4727 gld + language Nancere nnc 4728 nnc + language Nande nnb 4729 nnb + language Nandi niq 4730 niq + language Nanerigé Sénoufo sen 4731 sen + language Nanga Dama Dogon nzz 4732 nzz + language Nankina nnk 4733 nnk + language Nanti cox 4734 cox + language Nanticoke nnt 4735 nnt + language Nanubae afk 4736 afk + language Napo Lowland Quechua qvo 4737 qvo + language Napu npy 4738 npy + language Nar Phu npa 4739 npa + language Nara nrb 4740 nrb + language Narak nac 4741 nac + language Narango nrg 4742 nrg + language Nari Nari rnr 4743 rnr + language Naro nhr 4744 nhr + language Narom nrm 4745 nrm + language Narragansett xnt 4746 xnt + language Narua nru 4747 nru + language Narungga nnr 4748 nnr + language Nasal nsy 4749 nsy + language Nasarian nvh 4750 nvh + language Naskapi nsk 4751 nsk + language Natanzi ntz 4752 ntz + language Natchez ncz 4753 ncz + language Nateni ntm 4754 ntm + language Nathembo nte 4755 nte + language Natioro nti 4756 nti + language Natügu ntu 4757 ntu + language Nauete nxa 4758 nxa + language Naukan Yupik ynk 4759 ynk + language Nauna ncn 4760 ncn + language Nauo nwo 4761 nwo + language Nauru nau 4762 nau na + language Navajo, Navaho nav 4763 nav nv Navajo Navaho + language Navut nsw 4764 nsw + language Nawaru nwr 4765 nwr + language Nawathinehena nwa 4766 nwa + language Nawdm nmz 4767 nmz + language Nawuri naw 4768 naw + language Naxi nxq 4769 nxq + language Nayi noz 4770 noz + language Nayini nyq 4771 nyq + language Ncane ncr 4772 ncr + language Nchumbulu nlu 4773 nlu + language Nda'nda' nnz 4774 nnz + language Ndai gke 4775 gke + language Ndaka ndk 4776 ndk + language Ndali ndh 4777 ndh + language Ndam ndm 4778 ndm + language Ndamba ndj 4779 ndj + language Ndambomo nxo 4780 nxo + language Ndasa nda 4781 nda + language Ndau ndc 4782 ndc + language Nde-Gbite ned 4783 ned + language Nde-Nsele-Nta ndd 4784 ndd + language Ndemli nml 4785 nml + language Ndendeule dne 4786 dne + language Ndengereko ndg 4787 ndg + language Nding eli 4788 eli + language Ndo ndp 4789 ndp + language Ndobo ndw 4790 ndw + language Ndoe nbb 4791 nbb + language Ndogo ndz 4792 ndz + language Ndolo ndl 4793 ndl + language Ndom nqm 4794 nqm + language Ndombe ndq 4795 ndq + language Ndonde Hamba njd 4796 njd + language Ndonga ndo 4797 ndo ng + language Ndoola ndr 4798 ndr + language Ndra'ngith dgt 4799 dgt + language Ndrulo dno 4800 dno + language Nduga ndx 4801 ndx + language Ndumu nmd 4802 nmd + language Ndunda nuh 4803 nuh + language Ndunga ndt 4804 ndt + language Ndut ndv 4805 ndv + language Ndwewe nww 4806 nww + language Ndyuka-Trio Pidgin njt 4807 njt + language Ndzwani Comorian wni 4808 wni + language Neapolitan nap 4809 nap + language Nedebang nec 4810 nec + language Nefamese nef 4811 nef + language Negerhollands dcr 4812 dcr + language Negeri Sembilan Malay zmi 4813 zmi + language Negidal neg 4814 neg + language Nehan nsn 4815 nsn + language Nek nif 4816 nif + language Nekgini nkg 4817 nkg + language Neko nej 4818 nej + language Neku nek 4819 nek + language Nema gsn 4820 gsn + language Neme nex 4821 nex + language Nemi nem 4822 nem + language Nen nqn 4823 nqn + language Nend anh 4824 anh + language Nenets yrk 4825 yrk + language Nengone nen 4826 nen + language Neo neu 4827 neu + language Neo-Hittite nei 4828 nei + language Nepalese Sign Language nsp 4829 nsp + language Nepali nep 4830 nep Nepali (macrolanguage) ne + language Nepali (individual language) npi 4831 npi + language Nete net 4832 net + language New Caledonian Javanese jas 4833 jas + language New Zealand Sign Language nzs 4834 nzs + language Newari new 4835 new + language Neyo ney 4836 ney + language Nez Perce nez 4837 nez + language Ngaanyatjarra ntj 4838 ntj + language Ngad'a nxg 4839 nxg + language Ngadjunmaya nju 4840 nju + language Ngadjuri jui 4841 jui + language Ngaing nnf 4842 nnf + language Ngaju nij 4843 nij + language Ngala nud 4844 nud + language Ngalakgan nig 4845 nig + language Ngalum szb 4846 szb + language Ngam nmc 4847 nmc + language Ngamambo nbv 4848 nbv + language Ngambay sba 4849 sba + language Ngamini nmv 4850 nmv + language Ngamo nbh 4851 nbh + language Ngan'gityemerri nam 4852 nam + language Nganakarti xnk 4853 xnk + language Nganasan nio 4854 nio + language Ngandi nid 4855 nid + language Ngando (Central African Republic) ngd 4856 ngd + language Ngando (Democratic Republic of Congo) nxd 4857 nxd + language Ngandyera nne 4858 nne + language Ngangam gng 4859 gng + language Ngantangarra ntg 4860 ntg + language Nganyaywana nyx 4861 nyx + language Ngardi rxd 4862 rxd + language Ngarigu xni 4863 xni + language Ngarinyin ung 4864 ung + language Ngarinyman nbj 4865 nbj + language Ngarla nrk 4866 nrk + language Ngarluma nrl 4867 nrl + language Ngarrindjeri nay 4868 nay + language Ngas anc 4869 anc + language Ngasa nsg 4870 nsg + language Ngatik Men's Creole ngm 4871 ngm + language Ngawn Chin cnw 4872 cnw + language Ngawun nxn 4873 nxn + language Ngayawung nwg 4874 nwg + language Ngazidja Comorian zdj 4875 zdj + language Ngbaka nga 4876 nga + language Ngbaka Ma'bo nbm 4877 nbm + language Ngbaka Manza ngg 4878 ngg + language Ngbee jgb 4879 jgb + language Ngbinda nbd 4880 nbd + language Ngbundu nuu 4881 nuu + language Ngelima agh 4882 agh + language Ngemba nge 4883 nge + language Ngen gnj 4884 gnj + language Ngendelengo nql 4885 nql + language Ngete nnn 4886 nnn + language Nggem nbq 4887 nbq + language Nggwahyi ngx 4888 ngx + language Ngie ngj 4889 ngj + language Ngiemboon nnh 4890 nnh + language Ngile jle 4891 jle + language Ngindo nnq 4892 nnq + language Ngiti niy 4893 niy + language Ngizim ngi 4894 ngi + language Ngkâlmpw Kanum kcd 4895 kcd + language Ngom nra 4896 nra + language Ngomba jgo 4897 jgo + language Ngombale nla 4898 nla + language Ngombe (Central African Republic) nmj 4899 nmj + language Ngombe (Democratic Republic of Congo) ngc 4900 ngc + language Ngongo noq 4901 noq + language Ngoni (Mozambique) xnq 4902 xnq + language Ngoni (Tanzania) xnj 4903 xnj + language Ngoshie nsh 4904 nsh + language Ngul nlo 4905 nlo + language Ngulu ngp 4906 ngp + language Nguluwan nuw 4907 nuw + language Ngumbarl xnm 4908 xnm + language Ngumbi nui 4909 nui + language Ngunawal xul 4910 xul + language Ngundi ndn 4911 ndn + language Ngundu nue 4912 nue + language Ngungwel ngz 4913 ngz + language Ngurimi ngq 4914 ngq + language Ngurmbur nrx 4915 nrx + language Nguôn nuo 4916 nuo + language Ngwaba ngw 4917 ngw + language Ngwe nwe 4918 nwe + language Ngwo ngn 4919 ngn + language Ngäbere gym 4920 gym + language Nhanda nha 4921 nha + language Nhengatu yrl 4922 yrl + language Nhirrpi hrp 4923 hrp + language Nhuwala nhf 4924 nhf + language Nias nia 4925 nia + language Nicaragua Creole English bzk 4926 bzk + language Nicaraguan Sign Language ncs 4927 ncs + language Niellim nie 4928 nie + language Nigeria Mambila mzk 4929 mzk + language Nigerian Fulfulde fuv 4930 fuv + language Nigerian Pidgin pcm 4931 pcm + language Nigerian Sign Language nsi 4932 nsi + language Nihali nll 4933 nll + language Nii nii 4934 nii + language Niksek gbe 4935 gbe + language Nila nil 4936 nil + language Nilamba nim 4937 nim + language Nimadi noe 4938 noe + language Nimanbur nmp 4939 nmp + language Nimbari nmr 4940 nmr + language Nimboran nir 4941 nir + language Nimi nis 4942 nis + language Nimo niw 4943 niw + language Nimoa nmw 4944 nmw + language Ninam shb 4945 shb + language Nindi nxi 4946 nxi + language Ningera nby 4947 nby + language Ninggerum nxr 4948 nxr + language Ningil niz 4949 niz + language Ninia Yali nlk 4950 nlk + language Ninzo nin 4951 nin + language Nipsan nps 4952 nps + language Nisa njs 4953 njs + language Nisenan nsz 4954 nsz + language Nisga'a ncg 4955 ncg + language Nisi (China) yso 4956 yso + language Niuafo'ou num 4957 num + language Niuatoputapu nkp 4958 nkp + language Niuean niu 4959 niu + language Nivaclé cag 4960 cag + language Niwer Mil hrc 4961 hrc + language Njalgulgule njl 4962 njl + language Njebi nzb 4963 nzb + language Njen njj 4964 njj + language Njerep njr 4965 njr + language Njyem njy 4966 njy + language Nkami nkq 4967 nkq + language Nkangala nkn 4968 nkn + language Nkari nkz 4969 nkz + language Nkem-Nkum isi 4970 isi + language Nkhumbi khu 4971 khu + language Nkongho nkc 4972 nkc + language Nkonya nko 4973 nko + language Nkoroo nkx 4974 nkx + language Nkoya nka 4975 nka + language Nkukoli nbo 4976 nbo + language Nkutu nkw 4977 nkw + language Nnam nbp 4978 nbp + language No linguistic content zxx 4979 zxx + language Nobiin fia 4980 fia + language Nobonob gaw 4981 gaw + language Nocte Naga njb 4982 njb + language Nogai nog 4983 nog + language Noipx npx 4984 npx + language Noiri noi 4985 noi + language Nokuku nkk 4986 nkk + language Nomaande lem 4987 lem + language Nomane nof 4988 nof + language Nomatsiguenga not 4989 not + language Nomlaki nol 4990 nol + language Nomu noh 4991 noh + language Nong Zhuang zhn 4992 zhn + language Nonuya noj 4993 noj + language Nooksack nok 4994 nok + language Noon snf 4995 snf + language Noone nhu 4996 nhu + language Nopala Chatino cya 4997 cya + language Noric nrc 4998 nrc + language Norn nrn 4999 nrn + language Norra nrr 5000 nrr + language North Alaskan Inupiatun esi 5001 esi + language North Ambrym mmg 5002 mmg + language North Asmat nks 5003 nks + language North Awyu yir 5004 yir + language North Azerbaijani azj 5005 azj + language North Babar bcd 5006 bcd + language North Bolivian Quechua qul 5007 qul + language North Central Mixe neq 5008 neq + language North Efate llp 5009 llp + language North Fali fll 5010 fll + language North Giziga gis 5011 gis + language North Junín Quechua qvn 5012 qvn + language North Marquesan mrq 5013 mrq + language North Mesopotamian Arabic ayp 5014 ayp + language North Midlands Tasmanian xph 5015 xph + language North Mofu mfk 5016 mfk + language North Moluccan Malay max 5017 max + language North Muyu kti 5018 kti + language North Nuaulu nni 5019 nni + language North Picene nrp 5020 nrp + language North Slavey scs 5021 scs + language North Tairora tbg 5022 tbg + language North Tanna tnn 5023 tnn + language North Wahgi whg 5024 whg + language North Watut una 5025 una + language Northeast Kiwai kiw 5026 kiw + language Northeast Maidu nmu 5027 nmu + language Northeast Pashai aee 5028 aee + language Northeastern Dinka dip 5029 dip + language Northeastern Pomo pef 5030 pef + language Northeastern Tasmanian xpb 5031 xpb + language Northeastern Thai tts 5032 tts + language Northern Alta aqn 5033 aqn + language Northern Altai atv 5034 atv + language Northern Amami-Oshima ryn 5035 ryn + language Northern Betsimisaraka Malagasy bmm 5036 bmm + language Northern Binukidnon kyn 5037 kyn + language Northern Bobo Madaré bbo 5038 bbo + language Northern Bontok rbk 5039 rbk + language Northern Catanduanes Bikol cts 5040 cts + language Northern Conchucos Ancash Quechua qxn 5041 qxn + language Northern Dagara dgi 5042 dgi + language Northern Dong doc 5043 doc + language Northern East Cree crl 5044 crl + language Northern Emberá emp 5045 emp + language Northern Frisian frr 5046 frr + language Northern Ghale ghh 5047 ghh + language Northern Gondi gno 5048 gno + language Northern Grebo gbo 5049 gbo + language Northern Guiyang Hmong huj 5050 huj + language Northern Haida hdn 5051 hdn + language Northern Hindko hno 5052 hno + language Northern Huishui Hmong hmi 5053 hmi + language Northern Kalapuya nrt 5054 nrt + language Northern Kankanay xnn 5055 xnn + language Northern Katang ncq 5056 ncq + language Northern Khmer kxm 5057 kxm + language Northern Kissi kqs 5058 kqs + language Northern Kurdish kmr 5059 kmr + language Northern Luri lrc 5060 lrc + language Northern Mashan Hmong hmp 5061 hmp + language Northern Muji ymx 5062 ymx + language Northern Nago xkb 5063 xkb + language Northern Ndebele nde 5064 nde nd North Ndebele + language Northern Ngbandi ngb 5065 ngb + language Northern Nisu yiv 5066 yiv + language Northern Nuni nuv 5067 nuv + language Northern Oaxaca Nahuatl nhy 5068 nhy + language Northern Ohlone cst 5069 cst + language Northern One onr 5070 onr + language Northern Paiute pao 5071 pao + language Northern Pame pmq 5072 pmq + language Northern Pashto pbu 5073 pbu + language Northern Pastaza Quichua qvz 5074 qvz + language Northern Ping Chinese cnp 5075 cnp + language Northern Pomo pej 5076 pej + language Northern Puebla Nahuatl ncj 5077 ncj + language Northern Pumi pmi 5078 pmi + language Northern Qiandong Miao hea 5079 hea + language Northern Qiang cng 5080 cng + language Northern Rengma Naga nnl 5081 nnl + language Northern Roglai rog 5082 rog + language Northern Sami sme 5083 sme se + language Northern Sierra Miwok nsq 5084 nsq + language Northern Sorsoganon bks 5085 bks + language Northern Subanen stb 5086 stb + language Northern Tarahumara thh 5087 thh + language Northern Tasmanian xpv 5088 xpv + language Northern Tepehuan ntp 5089 ntp + language Northern Thai nod 5090 nod + language Northern Tidung ntd 5091 ntd + language Northern Tiwa twf 5092 twf + language Northern Tlaxiaco Mixtec xtn 5093 xtn + language Northern Toussian tsp 5094 tsp + language Northern Tujia tji 5095 tji + language Northern Tutchone ttm 5096 ttm + language Northern Uzbek uzn 5097 uzn + language Northern Yukaghir ykg 5098 ykg + language Northwest Alaska Inupiatun esk 5099 esk + language Northwest Gbaya gya 5100 gya + language Northwest Maidu mjd 5101 mjd + language Northwest Oaxaca Mixtec mxa 5102 mxa + language Northwest Pashai glh 5103 glh + language Northwestern Dinka diw 5104 diw + language Northwestern Fars faz 5105 faz + language Northwestern Kolami kfb 5106 kfb + language Northwestern Nisu nsf 5107 nsf + language Northwestern Ojibwa ojb 5108 ojb + language Northwestern Tasmanian xpw 5109 xpw + language Norwegian nor 5110 nor no + language Norwegian Bokmål nob 5111 nob nb + language Norwegian Nynorsk nno 5112 nno nn + language Norwegian Sign Language nsl 5113 nsl + language Notre bly 5114 bly + language Notsi ncf 5115 ncf + language Nottoway ntw 5116 ntw + language Nottoway-Meherrin nwy 5117 nwy + language Novial nov 5118 nov + language Noy noy 5119 noy + language Nsenga nse 5120 nse + language Nshi nsc 5121 nsc + language Nsongo nsx 5122 nsx + language Ntcham bud 5123 bud + language Nteng nqt 5124 nqt + language Ntomba nto 5125 nto + language Nubaca baf 5126 baf + language Nubi kcn 5127 kcn + language Nubri kte 5128 kte + language Nuer nus 5129 nus + language Nugunu (Australia) nnv 5130 nnv + language Nugunu (Cameroon) yas 5131 yas + language Nuk noc 5132 noc + language Nukak Makú mbr 5133 mbr + language Nukna klt 5134 klt + language Nukuini nuc 5135 nuc + language Nukumanu nuq 5136 nuq + language Nukunul xnu 5137 xnu + language Nukuoro nkr 5138 nkr + language Nukuria nur 5139 nur + language Numana nbr 5140 nbr + language Numanggang nop 5141 nop + language Numbami sij 5142 sij + language Nume tgs 5143 tgs + language Numidian nxm 5144 nxm + language Numèè kdk 5145 kdk + language Nung (Viet Nam) nut 5146 nut + language Nungali nug 5147 nug + language Nunggubuyu nuy 5148 nuy + language Nungu rin 5149 rin + language Nuosu iii 5150 iii ii Sichuan Yi + language Nupbikha npb 5151 npb + language Nupe-Nupe-Tako nup 5152 nup + language Nusa Laut nul 5153 nul + language Nusu nuf 5154 nuf + language Nuu-chah-nulth nuk 5155 nuk + language Nyabwa nwb 5156 nwb + language Nyaheun nev 5157 nev + language Nyahkur cbn 5158 cbn + language Nyakyusa-Ngonde nyy 5159 nyy + language Nyali nlj 5160 nlj + language Nyam nmi 5161 nmi + language Nyamal nly 5162 nly + language Nyambo now 5163 now + language Nyamusa-Molo nwm 5164 nwm + language Nyamwanga mwn 5165 mwn + language Nyamwezi nym 5166 nym + language Nyaneka nyk 5167 nyk + language Nyang'i nyp 5168 nyp + language Nyanga nyj 5169 nyj + language Nyanga-li nyc 5170 nyc + language Nyangatom nnj 5171 nnj + language Nyangbo nyb 5172 nyb + language Nyangga nny 5173 nny + language Nyangumarta nna 5174 nna + language Nyankole nyn 5175 nyn + language Nyankpa yes 5176 yes + language Nyarafolo Senoufo sev 5177 sev + language Nyaturu rim 5178 rim + language Nyaw nyw 5179 nyw + language Nyawaygi nyt 5180 nyt + language Nyemba nba 5181 nba + language Nyengo nye 5182 nye + language Nyenkha neh 5183 neh + language Nyeu nyl 5184 nyl + language Nyiha (Malawi) nyr 5185 nyr + language Nyiha (Tanzania) nih 5186 nih + language Nyika (Malawi and Zambia) nkv 5187 nkv + language Nyika (Tanzania) nkt 5188 nkt + language Nyikina nyh 5189 nyh + language Nyindrou lid 5190 lid + language Nyindu nyg 5191 nyg + language Nyishi njz 5192 njz + language Nyiyaparli xny 5193 xny + language Nyokon nvo 5194 nvo + language Nyole nuj 5195 nuj + language Nyong muo 5196 muo + language Nyore nyd 5197 nyd + language Nyoro nyo 5198 nyo + language Nyulnyul nyv 5199 nyv + language Nyungar nys 5200 nys + language Nyungwe nyu 5201 nyu + language Nyâlayu yly 5202 yly + language Nzadi nzd 5203 nzd + language Nzakambay nzy 5204 nzy + language Nzakara nzk 5205 nzk + language Nzanyi nja 5206 nja + language Nzima nzi 5207 nzi + language Ná-Meo neo 5208 neo + language Nêlêmwa-Nixumwak nee 5209 nee + language Nüpode Huitoto hux 5210 hux + language Nǁng ngh 5211 ngh + language O'chi'chi' xoc 5212 xoc + language O'du tyh 5213 tyh + language Obanliku bzy 5214 bzy + language Obispeño obi 5215 obi + language Oblo obl 5216 obl + language Obo Manobo obo 5217 obo + language Obokuitai afz 5218 afz + language Obolo ann 5219 ann + language Obulom obu 5220 obu + language Ocaina oca 5221 oca + language Occitan oci 5222 oci Occitan (post 1500) oc + language Ocotepec Mixtec mie 5223 mie + language Ocotlán Zapotec zac 5224 zac + language Od odk 5225 odk + language Odia ory 5226 ory + language Odiai bhf 5227 bhf + language Odoodee kkc 5228 kkc + language Odual odu 5229 odu + language Odut oda 5230 oda + language Ofayé opy 5231 opy + language Official Aramaic (700-300 BCE) arc 5232 arc + language Ofo ofo 5233 ofo + language Ogbah ogc 5234 ogc + language Ogbia ogb 5235 ogb + language Ogbogolo ogg 5236 ogg + language Ogbronuagum ogu 5237 ogu + language Ogea eri 5238 eri + language Oirata oia 5239 oia + language Ojibwe, Ojibwa oji 5240 oji Ojibwe oj Ojibwa + language Ojitlán Chinantec chj 5241 chj + language Okanagan oka 5242 oka + language Oki-No-Erabu okn 5243 okn + language Okiek oki 5244 oki + language Oko-Eni-Osayen oks 5245 oks + language Oko-Juwoi okj 5246 okj + language Okobo okb 5247 okb + language Okodia okd 5248 okd + language Okolie oie 5249 oie + language Okolod kqv 5250 kqv + language Okpamheri opa 5251 opa + language Okpe (Northwestern Edo) okx 5252 okx + language Okpe (Southwestern Edo) oke 5253 oke + language Oksapmin opm 5254 opm + language Oku oku 5255 oku + language Old Aramaic (up to 700 BCE) oar 5256 oar + language Old Avar oav 5257 oav + language Old Breton obt 5258 obt + language Old Burmese obr 5259 obr + language Old Cham ocm 5260 ocm + language Old Chinese och 5261 och + language Old Church Slavonic chu 5262 chu cu Church Slavonic Church Slavic Old Church Slavonic Old Bulgarian + language Old Cornish oco 5263 oco + language Old Dutch odt 5264 odt + language Old English (ca. 450-1100) ang 5265 ang + language Old French (842-ca. 1400) fro 5266 fro + language Old Frisian ofs 5267 ofs + language Old Georgian oge 5268 oge + language Old High German (ca. 750-1050) goh 5269 goh + language Old Hittite oht 5270 oht + language Old Hungarian ohu 5271 ohu + language Old Irish (to 900) sga 5272 sga + language Old Japanese ojp 5273 ojp + language Old Kentish Sign Language okl 5274 okl + language Old Khmer okz 5275 okz + language Old Korean (3rd-9th cent.) oko 5276 oko + language Old Lithuanian olt 5277 olt + language Old Malay omy 5278 omy + language Old Manipuri omp 5279 omp + language Old Marathi omr 5280 omr + language Old Mon omx 5281 omx + language Old Norse non 5282 non + language Old Nubian onw 5283 onw + language Old Ossetic oos 5284 oos + language Old Persian (ca. 600-400 B.C.) peo 5285 peo + language Old Provençal (to 1500) pro 5286 pro + language Old Russian orv 5287 orv + language Old Saxon osx 5288 osx + language Old Spanish osp 5289 osp + language Old Sundanese osn 5290 osn + language Old Tamil oty 5291 oty + language Old Tibetan otb 5292 otb + language Old Turkish otk 5293 otk + language Old Uighur oui 5294 oui + language Old Welsh owl 5295 owl + language Olekha ole 5296 ole + language Olkol olk 5297 olk + language Olo ong 5298 ong + language Oloma olm 5299 olm + language Olrat olr 5300 olr + language Olu'bo lul 5301 lul + language Olulumo-Ikom iko 5302 iko + language Oluta Popoluca plo 5303 plo + language Omagua omg 5304 omg + language Omaha-Ponca oma 5305 oma + language Omani Arabic acx 5306 acx + language Ombamba mbm 5307 mbm + language Ombo oml 5308 oml + language Ometepec Nahuatl nht 5309 nht + language Omi omi 5310 omi + language Omok omk 5311 omk + language Omotik omt 5312 omt + language Omurano omu 5313 omu + language Ona ona 5314 ona + language Oneida one 5315 one + language Ong oog 5316 oog + language Onin oni 5317 oni + language Onin Based Pidgin onx 5318 onx + language Onjob onj 5319 onj + language Ono ons 5320 ons + language Onobasulu onn 5321 onn + language Onondaga ono 5322 ono + language Ontenu ont 5323 ont + language Ontong Java ojv 5324 ojv + language Oorlams oor 5325 oor + language Opao opo 5326 opo + language Opata opt 5327 opt + language Orang Kanaq orn 5328 orn + language Orang Seletar ors 5329 ors + language Oraon Sadri sdr 5330 sdr + language Orejón ore 5331 ore + language Oring org 5332 org + language Oriya ori 5333 ori or Oriya (macrolanguage) + language Orizaba Nahuatl nlv 5334 nlv + language Orma orc 5335 orc + language Ormu orz 5336 orz + language Ormuri oru 5337 oru + language Oro orx 5338 orx + language Oro Win orw 5339 orw + language Oroch oac 5340 oac + language Oroha ora 5341 ora + language Orok oaa 5342 oaa + language Orokaiva okv 5343 okv + language Oroko bdu 5344 bdu + language Orokolo oro 5345 oro + language Oromo orm 5346 orm om + language Oroqen orh 5347 orh + language Orowe bpk 5348 bpk + language Oruma orr 5349 orr + language Orya ury 5350 ury + language Osage osa 5351 osa + language Osatu ost 5352 ost + language Oscan osc 5353 osc + language Osing osi 5354 osi + language Ososo oso 5355 oso + language Ossetian, Ossetic oss 5356 oss os Ossetian Ossetic + language Ot Danum otd 5357 otd + language Otank uta 5358 uta + language Oti oti 5359 oti + language Otoro otr 5360 otr + language Ottawa otw 5361 otw + language Ottoman Turkish (1500-1928) ota 5362 ota + language Otuho lot 5363 lot + language Otuke otu 5364 otu + language Ouma oum 5365 oum + language Oune oue 5366 oue + language Owa stn 5367 stn + language Owenia wsr 5368 wsr + language Owiniga owi 5369 owi + language Oy oyb 5370 oyb + language Oya'oya oyy 5371 oyy + language Oyda oyd 5372 oyd + language Oyster Bay Tasmanian xpd 5373 xpd + language Ozolotepec Zapotec zao 5374 zao + language Ozumacín Chinantec chz 5375 chz + language Pa Di pdi 5376 pdi + language Pa'a pqa 5377 pqa + language Pa'o Karen blk 5378 blk + language Pa-Hng pha 5379 pha + language Paakantyi drl 5380 drl + language Paama pma 5381 pma + language Paasaal sig 5382 sig + language Pacahuara pcp 5383 pcp + language Pacaraos Quechua qvp 5384 qvp + language Pacific Gulf Yupik ems 5385 ems + language Pacoh pac 5386 pac + language Padoe pdo 5387 pdo + language Paekche pkc 5388 pkc + language Paelignian pgn 5389 pgn + language Pagi pgi 5390 pgi + language Pagibete pae 5391 pae + language Pagu pgu 5392 pgu + language Pahanan Agta apf 5393 apf + language Pahari phj 5394 phj + language Pahari-Potwari phr 5395 phr + language Pahi lgt 5396 lgt + language Pahlavani phv 5397 phv + language Pahlavi pal 5398 pal + language Pai Tavytera pta 5399 pta + language Paicî pri 5400 pri + language Paipai ppi 5401 ppi + language Paite Chin pck 5402 pck + language Paiwan pwn 5403 pwn + language Pak-Tong pkg 5404 pkg + language Pakanha pkn 5405 pkn + language Pakaásnovos pav 5406 pav + language Pakistan Sign Language pks 5407 pks + language Paku pku 5408 pku + language Paku Karen jkp 5409 jkp + language Pal abw 5410 abw + language Palaic plq 5411 plq + language Palaka Senoufo plr 5412 plr + language Palantla Chinantec cpa 5413 cpa + language Palauan pau 5414 pau + language Paleni pnl 5415 pnl + language Palenquero pln 5416 pln + language Palikúr plu 5417 plu + language Paliyan pcf 5418 pcf + language Pallanganmiddang pmd 5419 pmd + language Paloor fap 5420 fap + language Palu'e ple 5421 ple + language Paluan plz 5422 plz + language Palya Bareli bpx 5423 bpx + language Pam pmn 5424 pmn + language Pambia pmb 5425 pmb + language Pamona pmf 5426 pmf + language Pamosu hih 5427 hih + language Pampanga pam 5428 pam + language Pamplona Atta att 5429 att + language Pana (Burkina Faso) pnq 5430 pnq + language Pana (Central African Republic) pnz 5431 pnz + language Panamanian Sign Language lsp 5432 lsp + language Panamint par 5433 par + language Panao Huánuco Quechua qxh 5434 qxh + language Panará kre 5435 kre + language Panasuan psn 5436 psn + language Panawa pwb 5437 pwb + language Pancana pnp 5438 pnp + language Panchpargania tdb 5439 tdb + language Pande bkj 5440 bkj + language Pangasinan pag 5441 pag + language Pangseng pgs 5442 pgs + language Pangu png 5443 png + language Pangutaran Sama slm 5444 slm + language Pangwa pbr 5445 pbr + language Pangwali pgg 5446 pgg + language Panim pnr 5447 pnr + language Paniya pcg 5448 pcg + language Panjabi, Punjabi pan 5449 pan pa Panjabi Punjabi + language Pankararé pax 5450 pax + language Pankararú paz 5451 paz + language Pankhu pkh 5452 pkh + language Pannei pnc 5453 pnc + language Pano mqz 5454 mqz + language Panoan Katukína knt 5455 knt + language Panobo pno 5456 pno + language Panyi Bai bfc 5457 bfc + language Papantla Totonac top 5458 top + language Papapana ppn 5459 ppn + language Papar dpp 5460 dpp + language Papasena pas 5461 pas + language Papel pbo 5462 pbo + language Papi ppe 5463 ppe + language Papiamento pap 5464 pap + language Papora ppu 5465 ppu + language Papua New Guinean Sign Language pgz 5466 pgz + language Papuan Malay pmy 5467 pmy + language Papuma ppm 5468 ppm + language Parachi prc 5469 prc + language Paraguayan Guaraní gug 5470 gug + language Paraguayan Sign Language pys 5471 pys + language Parakanã pak 5472 pak + language Paranan prf 5473 prf + language Paranawát paf 5474 paf + language Paraujano pbg 5475 pbg + language Parauk prk 5476 prk + language Parawen prw 5477 prw + language Pardhan pch 5478 pch + language Pardhi pcl 5479 pcl + language Pare ppt 5480 ppt + language Parecís pab 5481 pab + language Parenga pcj 5482 pcj + language Parkari Koli kvx 5483 kvx + language Parkwa pbi 5484 pbi + language Parsi-Dari prd 5485 prd + language Parthian xpr 5486 xpr + language Parya paq 5487 paq + language Pará Arára aap 5488 aap + language Pará Gavião gvp 5489 gvp + language Pashto, Pushto pus 5490 pus Pashto ps Pushto + language Pasi psq 5491 psq + language Pass Valley Yali yac 5492 yac + language Patamona pbc 5493 pbc + language Patani ptn 5494 ptn + language Pataxó Hã-Ha-Hãe pth 5495 pth + language Patep ptp 5496 ptp + language Pathiya pty 5497 pty + language Patpatar gfk 5498 gfk + language Pattani lae 5499 lae + language Pattani Malay mfa 5500 mfa + language Pattapu ptq 5501 ptq + language Patwin pwi 5502 pwi + language Paulohi plh 5503 plh + language Paumarí pad 5504 pad + language Paunaka pnk 5505 pnk + language Pauri Bareli bfb 5506 bfb + language Pauserna psm 5507 psm + language Pawaia pwa 5508 pwa + language Pawnee paw 5509 paw + language Paynamar pmr 5510 pmr + language Pazeh pzh 5511 pzh + language Pe pai 5512 pai + language Pear pcb 5513 pcb + language Pech pay 5514 pay + language Pecheneg xpc 5515 xpc + language Pedi nso 5516 nso + language Pei ppq 5517 ppq + language Pekal pel 5518 pel + language Pela bxd 5519 bxd + language Pele-Ata ata 5520 ata + language Pelende ppp 5521 ppp + language Pemon aoc 5522 aoc + language Penang Sign Language psg 5523 psg + language Penchal pek 5524 pek + language Pendau ums 5525 ums + language Pengo peg 5526 peg + language Pennsylvania German pdc 5527 pdc + language Penrhyn pnh 5528 pnh + language Pentlatch ptw 5529 ptw + language Perai wet 5530 wet + language Peranakan Indonesian pea 5531 pea + language Pere pfe 5532 pfe + language Peripheral Mongolian mvf 5533 mvf + language Pero pip 5534 pip + language Persian (Farsi) fas 5535 fas Persian fa Farsi per + language Peruvian Sign Language prl 5536 prl + language Pesse pze 5537 pze + language Petapa Zapotec zpe 5538 zpe + language Petats pex 5539 pex + language Petjo pey 5540 pey + language Peñoles Mixtec mil 5541 mil + language Pfaelzisch pfl 5542 pfl + language Phai prt 5543 prt + language Phake phk 5544 phk + language Phala ypa 5545 ypa + language Phalura phl 5546 phl + language Phana' phq 5547 phq + language Phangduwali phw 5548 phw + language Phende pem 5549 pem + language Philippine Sign Language psp 5550 psp + language Phimbi phm 5551 phm + language Phoenician phn 5552 phn + language Phola ypg 5553 ypg + language Pholo yip 5554 yip + language Phom Naga nph 5555 nph + language Phong-Kniang pnx 5556 pnx + language Phrae Pwo Karen kjt 5557 kjt + language Phrygian xpg 5558 xpg + language Phu Thai pht 5559 pht + language Phuan phu 5560 phu + language Phudagi phd 5561 phd + language Phuie pug 5562 pug + language Phukha phh 5563 phh + language Phuma ypm 5564 ypm + language Phunoi pho 5565 pho + language Phuong phg 5566 phg + language Phupa ypp 5567 ypp + language Phupha yph 5568 yph + language Phuza ypz 5569 ypz + language Piamatsina ptr 5570 ptr + language Piame pin 5571 pin + language Piapoco pio 5572 pio + language Piaroa pid 5573 pid + language Picard pcd 5574 pcd + language Pichis Ashéninka cpu 5575 cpu + language Pictish xpi 5576 xpi + language Pidgin Delaware dep 5577 dep + language Piemontese pms 5578 pms + language Pijao pij 5579 pij + language Pije piz 5580 piz + language Pijin pis 5581 pis + language Pilagá plg 5582 plg + language Pileni piv 5583 piv + language Pima Bajo pia 5584 pia + language Pimbwe piw 5585 piw + language Pinai-Hagahai pnn 5586 pnn + language Pindiini pti 5587 pti + language Pingelapese pif 5588 pif + language Pinigura pnv 5589 pnv + language Pinjarup pnj 5590 pnj + language Pinji pic 5591 pic + language Pinotepa Nacional Mixtec mio 5592 mio + language Pintupi-Luritja piu 5593 piu + language Pinyin pny 5594 pny + language Pipil ppl 5595 ppl + language Pirahã myp 5596 myp + language Piratapuyo pir 5597 pir + language Pirlatapa bxi 5598 bxi + language Piro pie 5599 pie + language Pirriya xpa 5600 xpa + language Pisabo pig 5601 pig + language Pisaflores Tepehua tpp 5602 tpp + language Piscataway psy 5603 psy + language Pisidian xps 5604 xps + language Pitcairn-Norfolk pih 5605 pih + language Pite Sami sje 5606 sje + language Piti pcn 5607 pcn + language Pitjantjatjara pjt 5608 pjt + language Pitta Pitta pit 5609 pit + language Piu pix 5610 pix + language Piya-Kwonci piy 5611 piy + language Plains Cree crk 5612 crk + language Plains Indian Sign Language psd 5613 psd + language Plains Miwok pmw 5614 pmw + language Plapo Krumen ktj 5615 ktj + language Plateau Malagasy plt 5616 plt + language Plautdietsch pdt 5617 pdt + language Playero gob 5618 gob + language Pnar pbv 5619 pbv + language Pochuri Naga npo 5620 npo + language Pochutec xpo 5621 xpo + language Podena pdn 5622 pdn + language Pogolo poy 5623 poy + language Pohnpeian pon 5624 pon + language Pokangá pok 5625 pok + language Poke pof 5626 pof + language Pokomo pkb 5627 pkb + language Polabian pox 5628 pox + language Polari pld 5629 pld + language Polish pol 5630 pol pl + language Polish Sign Language pso 5631 pso + language Polonombauk plb 5632 plb + language Pom pmo 5633 pmo + language Pomo pmm 5634 pmm + language Ponam ncc 5635 ncc + language Ponosakan pns 5636 pns + language Pontic pnt 5637 pnt + language Ponyo-Gongwang Naga npg 5638 npg + language Popti' jac 5639 jac + language Poqomam poc 5640 poc + language Poqomchi' poh 5641 poh + language Porohanon prh 5642 prh + language Port Sandwich psw 5643 psw + language Port Sorell Tasmanian xpl 5644 xpl + language Port Vato ptv 5645 ptv + language Portuguese por 5646 por pt + language Portuguese Sign Language psr 5647 psr + language Potawatomi pot 5648 pot + language Potiguára pog 5649 pog + language Pottangi Ollar Gadaba gdb 5650 gdb + language Poumei Naga pmx 5651 pmx + language Pouye bye 5652 bye + language Powari pwr 5653 pwr + language Powhatan pim 5654 pim + language Poyanáwa pyn 5655 pyn + language Prasuni prn 5656 prn + language Primitive Irish pgl 5657 pgl + language Principense pre 5658 pre + language Providencia Sign Language prz 5659 prz + language Prussian prg 5660 prg + language Psikye kvj 5661 kvj + language Pu-Xian Chinese cpx 5662 cpx + language Puare pux 5663 pux + language Pudtol Atta atp 5664 atp + language Puebla Mazatec pbm 5665 pbm + language Puelche pue 5666 pue + language Puerto Rican Sign Language psl 5667 psl + language Puimei Naga npu 5668 npu + language Puinave pui 5669 pui + language Pukapuka pkp 5670 pkp + language Pulaar fuc 5671 fuc + language Pulabu pup 5672 pup + language Pular fuf 5673 fuf + language Puluwatese puw 5674 puw + language Puma pum 5675 pum + language Pumpokol xpm 5676 xpm + language Pumé yae 5677 yae + language Punan Aput pud 5678 pud + language Punan Bah-Biau pna 5679 pna + language Punan Batu 1 pnm 5680 pnm + language Punan Merah puf 5681 puf + language Punan Merap puc 5682 puc + language Punan Tubu puj 5683 puj + language Punic xpu 5684 xpu + language Puno Quechua qxp 5685 qxp + language Punthamara xpt 5686 xpt + language Punu puu 5687 puu + language Puoc puo 5688 puo + language Puquina puq 5689 puq + language Puragi pru 5690 pru + language Purari iar 5691 iar + language Purepecha tsz 5692 tsz + language Puri prr 5693 prr + language Purik prx 5694 prx + language Purisimeño puy 5695 puy + language Puroik suv 5696 suv + language Puruborá pur 5697 pur + language Purum pub 5698 pub + language Putai mfl 5699 mfl + language Putoh put 5700 put + language Putukwam afe 5701 afe + language Puyo xpy 5702 xpy + language Puyo-Paekche xpp 5703 xpp + language Puyuma pyu 5704 pyu + language Pwaamei pme 5705 pme + language Pwapwâ pop 5706 pop + language Pwo Eastern Karen kjp 5707 kjp + language Pwo Northern Karen pww 5708 pww + language Pwo Western Karen pwo 5709 pwo + language Pyapun pcw 5710 pcw + language Pye Krumen pye 5711 pye + language Pyen pyy 5712 pyy + language Pyu (Myanmar) pyx 5713 pyx + language Pyu (Papua New Guinea) pby 5714 pby + language Páez pbb 5715 pbb + language Pááfang pfa 5716 pfa + language Päri lkr 5717 lkr + language Pémono pev 5718 pev + language Pévé lme 5719 lme + language Pökoot pko 5720 pko + language Pāli pli 5721 pli pi Pali + language Q'anjob'al kjb 5722 kjb + language Qabiao laq 5723 laq + language Qaqet byx 5724 byx + language Qashqa'i qxq 5725 qxq + language Qatabanian xqt 5726 xqt + language Qau gqu 5727 gqu + language Qawasqar alc 5728 alc + language Qila Muji ymq 5729 ymq + language Qimant ahg 5730 ahg + language Qiubei Zhuang zqe 5731 zqe + language Quapaw qua 5732 qua + language Quebec Sign Language fcs 5733 fcs + language Quechan yum 5734 yum + language Quechua que 5735 que qu + language Quenya qya 5736 qya + language Querétaro Otomi otq 5737 otq + language Quetzaltepec Mixe pxm 5738 pxm + language Queyu qvy 5739 qvy + language Quiavicuzas Zapotec zpj 5740 zpj + language Quileute qui 5741 qui + language Quinault qun 5742 qun + language Quinqui quq 5743 quq + language Quioquitani-Quierí Zapotec ztq 5744 ztq + language Quiotepec Chinantec chq 5745 chq + language Quiripi qyp 5746 qyp + language Rabha rah 5747 rah + language Rade rad 5748 rad + language Raetic xrr 5749 xrr + language Rahambuu raz 5750 raz + language Rajah Kabunsuwan Manobo mqk 5751 mqk + language Rajasthani raj 5752 raj + language Rajbanshi rjs 5753 rjs + language Raji rji 5754 rji + language Rajong rjg 5755 rjg + language Rajput Garasia gra 5756 gra + language Rakahanga-Manihiki rkh 5757 rkh + language Rakhine rki 5758 rki + language Ralte ral 5759 ral + language Rama rma 5760 rma + language Ramoaaina rai 5761 rai + language Ramopa kjx 5762 kjx + language Rampi lje 5763 lje + language Rana Tharu thr 5764 thr + language Rang rax 5765 rax + language Rangi lag 5766 lag + language Rangkas rgk 5767 rgk + language Ranglong rnl 5768 rnl + language Rangpuri rkt 5769 rkt + language Rao rao 5770 rao + language Rapa ray 5771 ray + language Rapanui rap 5772 rap + language Rapoisi kyx 5773 kyx + language Rapting rpt 5774 rpt + language Rara Bakati' lra 5775 lra + language Rarotongan rar 5776 rar + language Rasawa rac 5777 rac + language Ratagnon btn 5778 btn + language Ratahan rth 5779 rth + language Rathawi rtw 5780 rtw + language Rathwi Bareli bgd 5781 bgd + language Raute rau 5782 rau + language Ravula yea 5783 yea + language Rawa rwo 5784 rwo + language Rawang raw 5785 raw + language Rawat jnl 5786 jnl + language Rawngtu Chin weu 5787 weu + language Rawo rwa 5788 rwa + language Rayón Zoque zor 5789 zor + language Razajerdi rat 5790 rat + language Red Gelao gir 5791 gir + language Reel atu 5792 atu + language Rejang rej 5793 rej + language Rejang Kayan ree 5794 ree + language Reli rei 5795 rei + language Rema bow 5796 bow + language Rembarrnga rmb 5797 rmb + language Rembong reb 5798 reb + language Remo rem 5799 rem + language Remontado Dumagat agv 5800 agv + language Rempi rmp 5801 rmp + language Remun lkj 5802 lkj + language Rendille rel 5803 rel + language Rengao ren 5804 ren + language Rennell-Bellona mnv 5805 mnv + language Repanbitip rpn 5806 rpn + language Rer Bare rer 5807 rer + language Rerau rea 5808 rea + language Rerep pgk 5809 pgk + language Reshe res 5810 res + language Resígaro rgr 5811 rgr + language Retta ret 5812 ret + language Reyesano rey 5813 rey + language Riang (India) ria 5814 ria + language Riang Lai yin 5815 yin + language Riang Lang ril 5816 ril + language Riantana ran 5817 ran + language Ribun rir 5818 rir + language Rigwe iri 5819 iri + language Rikbaktsa rkb 5820 rkb + language Rinconada Bikol bto 5821 bto + language Rincón Zapotec zar 5822 zar + language Ringgou rgu 5823 rgu + language Ririo rri 5824 rri + language Rishiwa rsw 5825 rsw + language Ritharrngu rit 5826 rit + language Riung riu 5827 riu + language Riverain Sango snj 5828 snj + language Rmeet lbn 5829 lbn + language Rogo rod 5830 rod + language Rohingya rhg 5831 rhg + language Roma rmm 5832 rmm + language Romagnol rgn 5833 rgn + language Romam rmx 5834 rmx + language Romanian ron 5835 ron rum ro + language Romanian Sign Language rms 5836 rms + language Romano-Greek rge 5837 rge + language Romano-Serbian rsb 5838 rsb + language Romanova rmv 5839 rmv + language Romansh roh 5840 roh rm + language Romany rom 5841 rom + language Romblomanon rol 5842 rol + language Rombo rof 5843 rof + language Romkun rmk 5844 rmk + language Ron cla 5845 cla + language Ronga rng 5846 rng + language Rongga ror 5847 ror + language Rongmei Naga nbu 5848 nbu + language Rongpo rnp 5849 rnp + language Ronji roe 5850 roe + language Roon rnn 5851 rnn + language Roria rga 5852 rga + language Rotokas roo 5853 roo + language Rotuman rtm 5854 rtm + language Roviana rug 5855 rug + language Ruching Palaung pce 5856 pce + language Rudbari rdb 5857 rdb + language Rufiji rui 5858 rui + language Ruga ruh 5859 ruh + language Rukai dru 5860 dru + language Ruma ruz 5861 ruz + language Rumai Palaung rbb 5862 rbb + language Rumu klq 5863 klq + language Runga rou 5864 rou + language Rungtu Chin rtc 5865 rtc + language Rungus drg 5866 drg + language Rungwa rnw 5867 rnw + language Russia Buriat bxr 5868 bxr + language Russian rus 5869 rus ru + language Russian Sign Language rsl 5870 rsl + language Rusyn rue 5871 rue + language Ruthenian rsk 5872 rsk + language Rutul rut 5873 rut + language Ruuli ruc 5874 ruc + language Ruund rnd 5875 rnd + language Ruwila rwl 5876 rwl + language Rwa rwk 5877 rwk + language Rwandan Sign Language rsn 5878 rsn + language Réunion Creole French rcf 5879 rcf + language Rāziḥī rzh 5880 rzh + language S'gaw Karen ksw 5881 ksw + language Sa sax 5882 sax + language Sa'a apb 5883 apb + language Sa'ban snv 5884 snv + language Sa'och scq 5885 scq + language Saafi-Saafi sav 5886 sav + language Saam raq 5887 raq + language Saamia lsm 5888 lsm + language Saaroa sxr 5889 sxr + language Saba saa 5890 saa + language Sabaean xsa 5891 xsa + language Sabah Bisaya bsy 5892 bsy + language Sabah Malay msi 5893 msi + language Sabanê sae 5894 sae + language Sabaot spy 5895 spy + language Sabine sbv 5896 sbv + language Sabu hvn 5897 hvn + language Sabüm sbo 5898 sbo + language Sacapulteco quv 5899 quv + language Sadri sck 5900 sck + language Saek skb 5901 skb + language Saep spd 5902 spd + language Safaliba saf 5903 saf + language Safeyoka apz 5904 apz + language Safwa sbk 5905 sbk + language Sagala sbm 5906 sbm + language Sagalla tga 5907 tga + language Saho ssy 5908 ssy + language Sahu saj 5909 saj + language Saidi Arabic aec 5910 aec + language Saint Lucian Creole French acf 5911 acf + language Saisiyat xsy 5912 xsy + language Sajalong sjl 5913 sjl + language Sajau Basap sjb 5914 sjb + language Sakachep sch 5915 sch + language Sakalava Malagasy skg 5916 skg + language Sakao sku 5917 sku + language Sakata skt 5918 skt + language Sake sak 5919 sak + language Sakirabiá skf 5920 skf + language Sakizaya szy 5921 szy + language Sala shq 5922 shq + language Salampasu slx 5923 slx + language Salar slr 5924 slr + language Salas sgu 5925 sgu + language Salasaca Highland Quichua qxl 5926 qxl + language Salawati xmx 5927 xmx + language Saleman sau 5928 sau + language Saliba sbe 5929 sbe slc Sáliba + language Salinan sln 5930 sln + language Sallands sdz 5931 sdz + language Salt-Yui sll 5932 sll + language Saluan loe 5933 loe + language Salumá slj 5934 slj + language Salvadoran Sign Language esn 5935 esn + language Sam snx 5936 snx + language Samaritan smp 5937 smp + language Samaritan Aramaic sam 5938 sam + language Samarokena tmj 5939 tmj + language Samatao ysd 5940 ysd + language Samay syx 5941 syx + language Samba smx 5942 smx + language Samba Daka ccg 5943 ccg + language Samba Leko ndi 5944 ndi + language Sambal xsb 5945 xsb + language Sambalpuri spv 5946 spv + language Sambe xab 5947 xab + language Samberigi ssx 5948 ssx + language Samburu saq 5949 saq + language Samei smh 5950 smh + language Samo smq 5951 smq + language Samoan smo 5952 smo sm + language Samogitian sgs 5953 sgs + language Samosa swm 5954 swm + language Sampang rav 5955 rav + language Samre sxm 5956 sxm + language Samtao stu 5957 stu + language Samvedi smv 5958 smv + language San Agustín Mixtepec Zapotec ztm 5959 ztm + language San Baltazar Loxicha Zapotec zpx 5960 zpx + language San Blas Kuna cuk 5961 cuk + language San Dionisio Del Mar Huave hve 5962 hve + language San Felipe Otlaltepec Popoloca pow 5963 pow + language San Francisco Del Mar Huave hue 5964 hue + language San Francisco Matlatzinca mat 5965 mat + language San Jerónimo Tecóatl Mazatec maa 5966 maa + language San Juan Atzingo Popoloca poe 5967 poe + language San Juan Colorado Mixtec mjc 5968 mjc + language San Juan Teita Mixtec xtj 5969 xtj + language San Luís Temalacayuca Popoloca pps 5970 pps + language San Marcos Tlacoyalco Popoloca pls 5971 pls + language San Martín Itunyoso Triqui trq 5972 trq + language San Martín Quechua qvs 5973 qvs + language San Mateo Del Mar Huave huv 5974 huv + language San Miguel Creole French scf 5975 scf + language San Miguel El Grande Mixtec mig 5976 mig + language San Miguel Piedras Mixtec xtp 5977 xtp + language San Pedro Amuzgos Amuzgo azg 5978 azg + language San Pedro Quiatoni Zapotec zpf 5979 zpf + language San Salvador Kongo kwy 5980 kwy + language San Vicente Coatlán Zapotec zpt 5981 zpt + language Sanaani Arabic ayn 5982 ayn + language Sanapaná spn 5983 spn + language Sandawe sad 5984 sad + language Sanga (Democratic Republic of Congo) sng 5985 sng + language Sanga (Nigeria) xsn 5986 xsn + language Sanggau scg 5987 scg + language Sangil snl 5988 snl + language Sangir sxn 5989 sxn + language Sangisari sgr 5990 sgr + language Sangkong sgk 5991 sgk + language Sanglechi sgy 5992 sgy + language Sango sag 5993 sag sg + language Sangtam Naga nsa 5994 nsa + language Sangu (Gabon) snq 5995 snq + language Sangu (Tanzania) sbp 5996 sbp + language Sani ysn 5997 ysn + language Sanie ysy 5998 ysy + language Saniyo-Hiyewe sny 5999 sny + language Sankaran Maninka msc 6000 msc + language Sansi ssi 6001 ssi + language Sanskrit (Saṁskṛta) san 6002 san Saṁskṛta sa Sanskrit + language Santa Ana de Tusi Pasco Quechua qxt 6003 qxt + language Santa Catarina Albarradas Zapotec ztn 6004 ztn + language Santa Inés Ahuatempan Popoloca pca 6005 pca + language Santa Inés Yatzechi Zapotec zpn 6006 zpn + language Santa Lucía Monteverde Mixtec mdv 6007 mdv + language Santa María Del Mar Huave hvv 6008 hvv + language Santa María La Alta Nahuatl nhz 6009 nhz + language Santa María Quiegolani Zapotec zpi 6010 zpi + language Santa María Zacatepec Mixtec mza 6011 mza + language Santa Teresa Cora cok 6012 cok + language Santali sat 6013 sat + language Santiago Xanica Zapotec zpr 6014 zpr + language Santiago del Estero Quichua qus 6015 qus + language Santo Domingo Albarradas Zapotec zas 6016 zas + language Sanumá xsu 6017 xsu + language Saparua spr 6018 spr + language Sapo krn 6019 krn + language Saponi spi 6020 spi + language Saposa sps 6021 sps + language Sapuan spu 6022 spu + language Sapé spc 6023 spc + language Sar mwm 6024 mwm + language Sara sre 6025 sre + language Sara Kaba sbz 6026 sbz + language Sara Kaba Deme kwg 6027 kwg + language Sara Kaba Náà kwv 6028 kwv + language Saraiki skr 6029 skr + language Saramaccan srm 6030 srm + language Sarangani Blaan bps 6031 bps + language Sarangani Manobo mbs 6032 mbs + language Sarasira zsa 6033 zsa + language Saraveca sar 6034 sar + language Sardinian srd 6035 srd sc + language Sari asj 6036 asj + language Sarikoli srh 6037 srh + language Sarli sdf 6038 sdf + language Sarsi srs 6039 srs + language Sartang onp 6040 onp + language Sarua swy 6041 swy + language Sarudu sdu 6042 sdu + language Saruga sra 6043 sra + language Sasak sas 6044 sas + language Sasaru sxs 6045 sxs + language Sassarese Sardinian sdc 6046 sdc + language Satawalese stw 6047 stw + language Saterfriesisch stq 6048 stq + language Sateré-Mawé mav 6049 mav + language Saudi Arabian Sign Language sdl 6050 sdl + language Sauraseni Prākrit psu 6051 psu + language Saurashtra saz 6052 saz + language Sauri srt 6053 srt + language Sauria Paharia mjt 6054 mjt + language Sause sao 6055 sao + language Sausi ssj 6056 ssj + language Savi sdg 6057 sdg + language Savosavo svs 6058 svs + language Sawai szw 6059 szw + language Saweru swr 6060 swr + language Sawi saw 6061 saw + language Sawila swt 6062 swt + language Sawknah swn 6063 swn + language Saxwe Gbe sxw 6064 sxw + language Saya say 6065 say + language Sayula Popoluca pos 6066 pos + language Scots sco 6067 sco + language Scythian xsc 6068 xsc + language Sea Island Creole English gul 6069 gul + language Seba kdg 6070 kdg + language Sebat Bet Gurage sgw 6071 sgw + language Seberuang sbx 6072 sbx + language Sebop sib 6073 sib + language Sechelt sec 6074 sec + language Secoya sey 6075 sey + language Sedang sed 6076 sed + language Sediq trv 6077 trv + language Sedoa tvw 6078 tvw + language Seeku sos 6079 sos + language Segai sge 6080 sge + language Segeju seg 6081 seg + language Seget sbg 6082 sbg + language Sehwi sfw 6083 sfw + language Seimat ssg 6084 ssg + language Seit-Kaitetu hik 6085 hik + language Sekani sek 6086 sek + language Sekapan skp 6087 skp + language Sekar skz 6088 skz + language Seke (Nepal) skj 6089 skj + language Seke (Vanuatu) ske 6090 ske + language Sekele vaj 6091 vaj + language Seki syi 6092 syi + language Seko Padang skx 6093 skx + language Seko Tengah sko 6094 sko + language Sekpele lip 6095 lip + language Selangor Sign Language kgi 6096 kgi + language Selaru slu 6097 slu + language Selayar sly 6098 sly + language Selee snw 6099 snw + language Selepet spl 6100 spl + language Selian sxl 6101 sxl + language Selkup sel 6102 sel + language Selungai Murut slg 6103 slg + language Seluwasan sws 6104 sws + language Semai sea 6105 sea + language Semandang sdq 6106 sdq + language Semaq Beri szc 6107 szc + language Sembakung Murut sbr 6108 sbr + language Semelai sza 6109 sza + language Semimi etz 6110 etz + language Semnam ssm 6111 ssm + language Semnani smy 6112 smy + language Sempan xse 6113 xse + language Sena seh 6114 seh + language Senara Sénoufo seq 6115 seq + language Senaya syn 6116 syn + language Sene sej 6117 sej + language Seneca see 6118 see + language Sened sds 6119 sds + language Sengele szg 6120 szg + language Senggi snu 6121 snu + language Sengo spk 6122 spk + language Sengseng ssz 6123 ssz + language Senhaja De Srair sjs 6124 sjs + language Sensi sni 6125 sni + language Sentani set 6126 set + language Senthang Chin sez 6127 sez + language Sentinel std 6128 std + language Sepa (Indonesia) spb 6129 spb + language Sepa (Papua New Guinea) spe 6130 spe + language Sepik Iwam iws 6131 iws + language Sera sry 6132 sry + language Serbian srp 6133 srp scc sr + language Serbo-Croatian hbs 6134 hbs + language Sere swf 6135 swf + language Serer srr 6136 srr + language Seri sei 6137 sei + language Serili sve 6138 sve + language Seroa kqu 6139 kqu + language Serrano ser 6140 ser + language Serua srw 6141 srw + language Serudung Murut srk 6142 srk + language Serui-Laut seu 6143 seu + language Seselwa Creole French crs 6144 crs + language Seta stf 6145 stf + language Setaman stm 6146 stm + language Seti sbi 6147 sbi + language Settla sta 6148 sta + language Severn Ojibwa ojs 6149 ojs + language Sewa Bay sew 6150 sew + language Seychelles Sign Language lsw 6151 lsw + language Seze sze 6152 sze + language Sha scw 6153 scw + language Shabak sdb 6154 sdb + language Shahmirzadi srz 6155 srz + language Shahrudi shm 6156 shm + language Shall-Zwall sha 6157 sha + language Shama-Sambuga sqa 6158 sqa + language Shamang xsh 6159 xsh + language Shambala ksb 6160 ksb + language Shan shn 6161 shn + language Shanenawa swo 6162 swo + language Shanga sho 6163 sho + language Sharanahua mcd 6164 mcd + language Shark Bay ssv 6165 ssv + language Sharwa swq 6166 swq + language Shasta sht 6167 sht + language Shatt shj 6168 shj + language Shau sqh 6169 sqh + language Shawnee sjw 6170 sjw + language She shx 6171 shx + language Shehri shv 6172 shv + language Shekhawati swv 6173 swv + language Shekkacho moy 6174 moy + language Sheko she 6175 she + language Shelta sth 6176 sth + language Shempire Senoufo seb 6177 seb + language Shendu shl 6178 shl + language Sheni scv 6179 scv + language Sherbro bun 6180 bun + language Sherdukpen sdp 6181 sdp + language Sherpa xsr 6182 xsr + language Sheshi Kham kip 6183 kip + language Shi shr 6184 shr + language Shihhi Arabic ssh 6185 ssh + language Shiki gua 6186 gua + language Shilluk shk 6187 shk + language Shina scl 6188 scl + language Shipibo-Conibo shp 6189 shp + language Sholaga sle 6190 sle + language Shom Peng sii 6191 sii + language Shona sna 6192 sna sn + language Shoo-Minda-Nye bcv 6193 bcv + language Shor cjs 6194 cjs + language Shoshoni shh 6195 shh + language Shua shg 6196 shg + language Shuadit sdt 6197 sdt + language Shuar jiv 6198 jiv + language Shubi suj 6199 suj + language Shughni sgh 6200 sgh + language Shuhi sxg 6201 sxg + language Shumashti sts 6202 sts + language Shumcho scu 6203 scu + language Shuswap shs 6204 shs + language Shwai shw 6205 shw + language Shwe Palaung pll 6206 pll + language Sialum slw 6207 slw + language Siamou sif 6208 sif + language Sian spg 6209 spg + language Siane snp 6210 snp + language Siang sya 6211 sya + language Siar-Lak sjr 6212 sjr + language Siawi mmp 6213 mmp + language Sibe nco 6214 nco + language Siberian Tatar sty 6215 sty + language Sibu Melanau sdx 6216 sdx + language Sicanian sxc 6217 sxc + language Sicel scx 6218 scx + language Sicilian scn 6219 scn + language Siculo Arabic sqr 6220 sqr + language Sidamo sid 6221 sid + language Sidetic xsd 6222 xsd + language Sie erg 6223 erg + language Sierra Leone Sign Language sgx 6224 sgx + language Sierra Negra Nahuatl nsu 6225 nsu + language Sierra de Juárez Zapotec zaa 6226 zaa + language Sighu sxe 6227 sxe + language Sihan snr 6228 snr + language Sihuas Ancash Quechua qws 6229 qws + language Sika ski 6230 ski + language Sikaiana sky 6231 sky + language Sikaritai tty 6232 tty + language Sikiana sik 6233 sik + language Sikkimese sip 6234 sip + language Siksika bla 6235 bla + language Sikule skh 6236 skh + language Sila slt 6237 slt + language Silacayoapan Mixtec mks 6238 mks + language Sileibi sbq 6239 sbq + language Silesian szl 6240 szl + language Silimo wul 6241 wul + language Siliput mkc 6242 mkc + language Silopi xsp 6243 xsp + language Silt'e stv 6244 stv + language Simaa sie 6245 sie + language Simba sbw 6246 sbw + language Simbali smg 6247 smg + language Simbari smb 6248 smb + language Simbo sbb 6249 sbb + language Simeku smz 6250 smz + language Simeulue smr 6251 smr + language Simte smt 6252 smt + language Sinagen siu 6253 siu + language Sinasina sst 6254 sst + language Sinaugoro snc 6255 snc + language Sindarin sjn 6256 sjn + language Sindhi snd 6257 snd sd + language Sindhi Bhil sbn 6258 sbn + language Sindihui Mixtec xts 6259 xts + language Singa sgm 6260 sgm + language Singapore Sign Language sls 6261 sls + language Singpho sgp 6262 sgp + language Sinhala, Sinhalese sin 6263 sin si Sinhala Sinhalese + language Sinicahua Mixtec xti 6264 xti + language Sininkere skq 6265 skq + language Sinte Romani rmo 6266 rmo + language Sinyar sys 6267 sys + language Sio xsi 6268 xsi + language Siona snn 6269 snn + language Sipacapense qum 6270 qum + language Sira swj 6271 swj + language Siraya fos 6272 fos + language Sirenik Yupik ysr 6273 ysr + language Siri sir 6274 sir + language Siriano sri 6275 sri + language Sirionó srq 6276 srq + language Sirmauri srx 6277 srx + language Siroi ssd 6278 ssd + language Sissala sld 6279 sld + language Sissano sso 6280 sso + language Siuslaw sis 6281 sis + language Sivandi siy 6282 siy + language Sivia Sign Language lsv 6283 lsv + language Siwai siw 6284 siw + language Siwi siz 6285 siz + language Siwu akp 6286 akp + language Siyin Chin csy 6287 csy + language Skagit ska 6288 ska + language Skalvian svx 6289 svx + language Skepi Creole Dutch skw 6290 skw + language Skolt Sami sms 6291 sms + language Skou skv 6292 skv + language Slave (Athapascan) den 6293 den + language Slavomolisano svm 6294 svm + language Slovak slk 6295 slk slo sk + language Slovakian Sign Language svk 6296 svk + language Slovenian slv 6297 slv sl Slovenian Slovene + language Small Flowery Miao sfm 6298 sfm + language Smärky Kanum kxq 6299 kxq + language Snohomish sno 6300 sno + language So (Democratic Republic of Congo) soc 6301 soc + language So'a ssq 6302 ssq + language Sobei sob 6303 sob + language Sochiapam Chinantec cso 6304 cso + language Soga xog 6305 xog + language Sogdian sog 6306 sog + language Soi soj 6307 soj + language Sokoro sok 6308 sok + language Solano xso 6309 xso + language Soli sby 6310 sby + language Solomon Islands Sign Language szs 6311 szs + language Solong aaw 6312 aaw + language Solos sol 6313 sol + language Som smc 6314 smc + language Somali som 6315 som so + language Somba-Siawari bmu 6316 bmu + language Somrai sor 6317 sor + language Somray smu 6318 smu + language Somyev kgt 6319 kgt + language Sonaga ysg 6320 ysg + language Sonde shc 6321 shc + language Songe sop 6322 sop + language Songlai Chin csj 6323 csj + language Songo soo 6324 soo + language Songomeno soe 6325 soe + language Songoora sod 6326 sod + language Sonha soi 6327 soi + language Sonia siq 6328 siq + language Soninke snk 6329 snk + language Sonsorol sov 6330 sov + language Soo teu 6331 teu + language Sop urw 6332 urw + language Soqotri sqt 6333 sqt + language Sora srb 6334 srb + language Sori-Harengan sbh 6335 sbh + language Sorkhei sqo 6336 sqo + language Sorothaptic sxo 6337 sxo + language Sorsogon Ayta ays 6338 ays + language Sos Kundi sdk 6339 sdk + language Sota Kanum krz 6340 krz + language Sou sqq 6341 sqq + language Sou Nama tlt 6342 tlt + language Sou Upaa wha 6343 wha + language South African Sign Language sfs 6344 sfs + language South Awyu aws 6345 aws + language South Azerbaijani azb 6346 azb + language South Bolivian Quechua quh 6347 quh + language South Central Banda lnl 6348 lnl + language South Central Dinka dib 6349 dib + language South Efate erk 6350 erk + language South Fali fal 6351 fal + language South Giziga giz 6352 giz + language South Lembata lmf 6353 lmf + language South Marquesan mqm 6354 mqm + language South Muyu kts 6355 kts + language South Nuaulu nxl 6356 nxl + language South Picene spx 6357 spx + language South Slavey xsl 6358 xsl + language South Tairora omw 6359 omw + language South Ucayali Ashéninka cpy 6360 cpy + language South Watut mcy 6361 mcy + language South West Bay sns 6362 sns + language Southeast Ambrym tvk 6363 tvk + language Southeast Babar vbb 6364 vbb + language Southeast Ijo ijs 6365 ijs + language Southeast Pashai psi 6366 psi + language Southeast Tasmanian xpf 6367 xpf + language Southeastern Dinka dks 6368 dks + language Southeastern Ixtlán Zapotec zpd 6369 zpd + language Southeastern Kolami nit 6370 nit + language Southeastern Nochixtlán Mixtec mxy 6371 mxy + language Southeastern Pomo pom 6372 pom + language Southeastern Puebla Nahuatl npl 6373 npl + language Southeastern Tarahumara tcu 6374 tcu + language Southeastern Tepehuan stp 6375 stp + language Southern Alta agy 6376 agy + language Southern Altai alt 6377 alt + language Southern Amami-Oshima ams 6378 ams + language Southern Aymara ayc 6379 ayc + language Southern Bai bfs 6380 bfs + language Southern Balochi bcc 6381 bcc + language Southern Betsimisaraka Malagasy bzc 6382 bzc + language Southern Binukidnon mtw 6383 mtw + language Southern Birifor biv 6384 biv + language Southern Bobo Madaré bwq 6385 bwq + language Southern Bontok obk 6386 obk + language Southern Carrier caf 6387 caf + language Southern Catanduanes Bikol bln 6388 bln + language Southern Conchucos Ancash Quechua qxo 6389 qxo + language Southern Dagaare dga 6390 dga + language Southern Dong kmc 6391 kmc + language Southern East Cree crj 6392 crj + language Southern Ghale ghe 6393 ghe + language Southern Grebo grj 6394 grj + language Southern Guiyang Hmong hmy 6395 hmy + language Southern Haida hax 6396 hax + language Southern Hindko hnd 6397 hnd + language Southern Kalapuya sxk 6398 sxk + language Southern Kalinga ksc 6399 ksc + language Southern Katang sct 6400 sct + language Southern Kisi kss 6401 kss + language Southern Kiwai kjd 6402 kjd + language Southern Kurdish sdh 6403 sdh + language Southern Lolopo ysp 6404 ysp + language Southern Luri luz 6405 luz + language Southern Ma'di snm 6406 snm + language Southern Mashan Hmong hma 6407 hma + language Southern Mnong mnn 6408 mnn + language Southern Muji ymc 6409 ymc + language Southern Nago nqg 6410 nqg + language Southern Nambikuára nab 6411 nab + language Southern Ndebele nbl 6412 nbl South Ndebele nr + language Southern Ngbandi nbw 6413 nbw + language Southern Nicobarese nik 6414 nik + language Southern Nisu nsd 6415 nsd + language Southern Nuni nnw 6416 nnw + language Southern Ohlone css 6417 css + language Southern One osu 6418 osu + language Southern Pame pmz 6419 pmz + language Southern Pashto pbt 6420 pbt + language Southern Pastaza Quechua qup 6421 qup + language Southern Ping Chinese csp 6422 csp + language Southern Pomo peq 6423 peq + language Southern Puebla Mixtec mit 6424 mit + language Southern Puget Sound Salish slh 6425 slh + language Southern Pumi pmj 6426 pmj + language Southern Qiandong Miao hms 6427 hms + language Southern Qiang qxs 6428 qxs + language Southern Rengma Naga nre 6429 nre + language Southern Rincon Zapotec zsr 6430 zsr + language Southern Roglai rgs 6431 rgs + language Southern Sama ssb 6432 ssb + language Southern Sami sma 6433 sma + language Southern Samo sbd 6434 sbd + language Southern Sierra Miwok skd 6435 skd + language Southern Sorsoganon srv 6436 srv + language Southern Sotho sot 6437 sot st + language Southern Subanen laa 6438 laa + language Southern Thai sou 6439 sou + language Southern Tidung itd 6440 itd + language Southern Tiwa tix 6441 tix + language Southern Toussian wib 6442 wib + language Southern Tujia tjs 6443 tjs + language Southern Tutchone tce 6444 tce + language Southern Uzbek uzs 6445 uzs + language Southern Yamphu lrr 6446 lrr + language Southern Yukaghir yux 6447 yux + language Southwest Gbaya gso 6448 gso + language Southwest Palawano plv 6449 plv + language Southwest Pashai psh 6450 psh + language Southwest Tanna nwi 6451 nwi + language Southwestern Bontok vbk 6452 vbk + language Southwestern Dinka dik 6453 dik + language Southwestern Fars fay 6454 fay + language Southwestern Guiyang Hmong hmg 6455 hmg + language Southwestern Huishui Hmong hmh 6456 hmh + language Southwestern Nisu nsv 6457 nsv + language Southwestern Tarahumara twr 6458 twr + language Southwestern Tasmanian xpx 6459 xpx + language Southwestern Tepehuan tla 6460 tla + language Southwestern Tlaxiaco Mixtec meh 6461 meh + language Sowa sww 6462 sww + language Sowanda sow 6463 sow + language Soyaltepec Mazatec vmp 6464 vmp + language Soyaltepec Mixtec vmq 6465 vmq + language Spanish Sign Language ssp 6466 ssp + language Spanish, Castilian spa 6467 spa Castilian Spanish es + language Spiti Bhoti spt 6468 spt + language Spokane spo 6469 spo + language Squamish squ 6470 squ + language Sranan Tongo srn 6471 srn + language Sri Lankan Creole Malay sci 6472 sci + language Sri Lankan Sign Language sqs 6473 sqs + language Standard Arabic arb 6474 arb + language Standard Estonian ekk 6475 ekk + language Standard Latvian lvs 6476 lvs + language Standard Malay zsm 6477 zsm + language Standard Moroccan Tamazight zgh 6478 zgh + language Stellingwerfs stl 6479 stl + language Stod Bhoti sbu 6480 sbu + language Stoney sto 6481 sto + language Straits Salish str 6482 str + language Suabo szp 6483 szp + language Suarmin seo 6484 seo + language Suau swp 6485 swp + language Suba sxb 6486 sxb + language Suba-Simbiti ssc 6487 ssc + language Subi xsj 6488 xsj + language Subiya sbs 6489 sbs + language Subtiaba sut 6490 sut + language Sudanese Arabic apd 6491 apd + language Sudanese Creole Arabic pga 6492 pga + language Sudest tgo 6493 tgo + language Sudovian xsv 6494 xsv + language Suena sue 6495 sue + language Suga sgi 6496 sgi + language Suganga sug 6497 sug + language Sugut Dusun kzs 6498 kzs + language Sui swi 6499 swi + language Suki sui 6500 sui + language Suku sub 6501 sub + language Sukuma suk 6502 suk + language Sukur syk 6503 syk + language Sukurum zsu 6504 zsu + language Sula szn 6505 szn + language Sulka sua 6506 sua + language Sulod srg 6507 srg + language Suma sqm 6508 sqm + language Sumariup siv 6509 siv + language Sumau six 6510 six + language Sumbawa smw 6511 smw + language Sumbwa suw 6512 suw + language Sumerian sux 6513 sux + language Sumi Naga nsm 6514 nsm + language Sumtu Chin csv 6515 csv + language Sunam ssk 6516 ssk + language Sundanese sun 6517 sun su + language Sunwar suz 6518 suz + language Suoy syo 6519 syo + language Supyire Senoufo spp 6520 spp + language Sur tdl 6521 tdl + language Surbakhal sbj 6522 sbj + language Surgujia sgj 6523 sgj + language Surigaonon sgd 6524 sgd + language Surjapuri sjp 6525 sjp + language Sursurunga sgz 6526 sgz + language Suruahá swx 6527 swx + language Surubu sde 6528 sde + language Suruí sru 6529 sru + language Suruí Do Pará mdz 6530 mdz + language Susquehannock sqn 6531 sqn + language Susu sus 6532 sus + language Susuami ssu 6533 ssu + language Suundi sdj 6534 sdj + language Suwawa swu 6535 swu + language Suyá suy 6536 suy + language Svan sva 6537 sva + language Swabian swg 6538 swg + language Swahili swa 6539 swa sw Swahili (macrolanguage) + language Swahili (individual language) swh 6540 swh + language Swampy Cree csw 6541 csw + language Swati ssw 6542 ssw ss + language Swedish swe 6543 swe sv + language Swedish Sign Language swl 6544 swl + language Swiss German gsw 6545 gsw + language Swiss-French Sign Language ssr 6546 ssr + language Swiss-German Sign Language sgg 6547 sgg + language Swiss-Italian Sign Language slf 6548 slf + language Swo sox 6549 sox + language Syenara Senoufo shz 6550 shz + language Sylheti syl 6551 syl + language Syriac syr 6552 syr + language São Paulo Kaingáng zkp 6554 zkp + language Sãotomense cri 6555 cri + language Sìcìté Sénoufo sep 6556 sep + language Sô sss 6557 sss + language T'apo lgn 6558 lgn + language T'en tct 6559 tct + language Ta'izzi-Adeni Arabic acq 6560 acq + language Taabwa tap 6561 tap + language Tabaa Zapotec zat 6562 zat + language Tabaru tby 6563 tby + language Tabasco Chontal chf 6564 chf + language Tabasco Nahuatl nhc 6565 nhc + language Tabasco Zoque zoq 6566 zoq + language Tabassaran tab 6567 tab + language Tabla tnm 6568 tnm + language Tabo knv 6569 knv + language Tabriak tzx 6570 tzx + language Tacahua Mixtec xtt 6571 xtt + language Tacana tna 6572 tna + language Tachawit shy 6573 shy + language Tachelhit shi 6574 shi + language Tachoni lts 6575 lts + language Tadaksahak dsq 6576 dsq + language Tado klw 6577 klw + language Tadyawan tdy 6578 tdy + language Tae' rob 6579 rob + language Tafi tcd 6580 tcd + language Tagabawa bgs 6581 bgs + language Tagakaulo klg 6582 klg + language Tagal Murut mvv 6583 mvv + language Tagalaka tgz 6584 tgz + language Tagalog tgl 6585 tgl tl + language Tagargrent oua 6586 oua + language Tagbanwa tbw 6587 tbw + language Tagbu tbm 6588 tbm + language Tagdal tda 6589 tda + language Tagin tgj 6590 tgj + language Tagish tgx 6591 tgx + language Tagoi tag 6592 tag + language Tagwana Senoufo tgw 6593 tgw + language Tahaggart Tamahaq thv 6594 thv + language Tahitian tah 6595 tah ty + language Tahltan tht 6596 tht + language Tai taw 6597 taw + language Tai Daeng tyr 6598 tyr + language Tai Dam blt 6599 blt + language Tai Do tyj 6600 tyj + language Tai Dón twh 6601 twh + language Tai Hongjin tiz 6602 tiz + language Tai Laing tjl 6603 tjl + language Tai Loi tlq 6604 tlq + language Tai Long thi 6605 thi + language Tai Nüa tdd 6606 tdd + language Tai Pao tpo 6607 tpo + language Tai Thanh tmm 6608 tmm + language Tai Ya cuu 6609 cuu + language Taiap gpn 6610 gpn + language Taikat aos 6611 aos + language Tainae ago 6612 ago + language Taino tnq 6613 tnq + language Tairaha bxa 6614 bxa + language Tairuma uar 6615 uar + language Taita dav 6616 dav + language Taivoan tvx 6617 tvx + language Taiwan Sign Language tss 6618 tss + language Taje pee 6619 pee + language Tajik tgk 6620 tgk tg + language Tajiki Arabic abh 6621 abh + language Tajio tdj 6622 tdj + language Tajuasohn tja 6623 tja + language Takelma tkm 6624 tkm + language Takestani tks 6625 tks + language Takia tbc 6626 tbc + language Takua tkz 6627 tkz + language Takuu nho 6628 nho + language Takwane tke 6629 tke + language Tal tal 6630 tal + language Tala tak 6631 tak + language Talaud tld 6632 tld + language Taliabu tlv 6633 tlv + language Talieng tdf 6634 tdf + language Talinga-Bwisi tlj 6635 tlj + language Talise tlr 6636 tlr + language Talodi tlo 6637 tlo + language Taloki tlk 6638 tlk + language Talondo' tln 6639 tln + language Talossan tzl 6640 tzl + language Talu yta 6641 yta + language Talysh tly 6642 tly + language Tama (Chad) tma 6643 tma + language Tama (Colombia) ten 6644 ten + language Tamagario tcg 6645 tcg + language Taman (Indonesia) tmn 6646 tmn + language Taman (Myanmar) tcl 6647 tcl + language Tamanaku tmz 6648 tmz + language Tamashek tmh 6649 tmh + language Tamasheq taq 6650 taq + language Tamazola Mixtec vmx 6651 vmx + language Tambas tdk 6652 tdk + language Tambora xxt 6653 xxt + language Tambotalo tls 6654 tls + language Tami tmy 6655 tmy + language Tamil tam 6656 tam ta + language Tamki tax 6657 tax + language Tamnim Citak tml 6658 tml + language Tampias Lobu low 6659 low + language Tampuan tpu 6660 tpu + language Tampulma tpm 6661 tpm + language Tanacross tcb 6662 tcb + language Tanahmerah tcm 6663 tcm + language Tanaina tfn 6664 tfn + language Tanapag tpv 6665 tpv + language Tandaganon tgn 6666 tgn + language Tandia tni 6667 tni + language Tandroy-Mahafaly Malagasy tdx 6668 tdx + language Tanema tnx 6669 tnx + language Tangale tan 6670 tan + language Tangchangya tnv 6671 tnv + language Tanggu tgu 6672 tgu + language Tangkhul Naga (India) nmf 6673 nmf + language Tangkhul Naga (Myanmar) ntx 6674 ntx + language Tangko tkx 6675 tkx + language Tanglang ytl 6676 ytl + language Tangoa tgp 6677 tgp + language Tanguat tbs 6678 tbs + language Tangut txg 6679 txg + language Tanimbili tbe 6680 tbe + language Tanimuca-Retuarã tnc 6681 tnc + language Tanjijili uji 6682 uji + language Tanosy Malagasy txy 6683 txy + language Tanudan Kalinga kml 6684 kml + language Tanzanian Sign Language tza 6685 tza + language Tapei afp 6686 afp + language Tapieté tpj 6687 tpj + language Tapirapé taf 6688 taf + language Tarao Naga tro 6689 tro + language Tareng tgr 6690 tgr + language Tariana tae 6691 tae + language Tarifit rif 6692 rif + language Tarjumo txj 6693 txj + language Tarok yer 6694 yer + language Tarpia tpf 6695 tpf + language Tartessian txr 6696 txr + language Taruma tdm 6697 tdm + language Tasawaq twq 6698 twq + language Tase Naga nst 6699 nst + language Tasmate tmt 6700 tmt + language Tataltepec Chatino cta 6701 cta + language Tatana txx 6702 txx + language Tatar tat 6703 tat tt + language Tatuyo tav 6704 tav + language Tauade ttd 6705 ttd + language Taulil tuh 6706 tuh + language Taungyo tco 6707 tco + language Taupota tpa 6708 tpa + language Tause tad 6709 tad + language Taushiro trr 6710 trr + language Tausug tsg 6711 tsg + language Tauya tya 6712 tya + language Taveta tvs 6713 tvs + language Tavoyan tvn 6714 tvn + language Tavringer Romani rmu 6715 rmu + language Tawala tbo 6716 tbo + language Tawallammat Tamajaq ttq 6717 ttq + language Tawandê xtw 6718 xtw + language Tawang Monpa twm 6719 twm + language Tawara twl 6720 twl + language Taworta tbp 6721 tbp + language Tawoyan twy 6722 twy + language Tawr Chin tcp 6723 tcp + language Tay Boi tas 6724 tas + language Tay Khang tnu 6725 tnu + language Tayart Tamajeq thz 6726 thz + language Tayo cks 6727 cks + language Taznatit grr 6728 grr + language Tboli tbl 6729 tbl + language Tchitchege tck 6730 tck + language Tchumbuli bqa 6731 bqa + language Te'un tve 6732 tve + language Teanu tkw 6733 tkw + language Tebul Sign Language tsy 6734 tsy + language Tebul Ure Dogon dtu 6735 dtu + language Tecpatlán Totonac tcw 6736 tcw + language Tedaga tuq 6737 tuq + language Tedim Chin ctd 6738 ctd + language Tee tkq 6739 tkq + language Tefaro tfo 6740 tfo + language Tegali ras 6741 ras + language Tehit kps 6742 kps + language Tehuelche teh 6743 teh + language Tejalapan Zapotec ztt 6744 ztt + language Teke-Ebo ebo 6745 ebo + language Teke-Fuumu ifm 6746 ifm + language Teke-Kukuya kkw 6747 kkw + language Teke-Laali lli 6748 lli + language Teke-Nzikou nzu 6749 nzu + language Teke-Tege teg 6750 teg + language Teke-Tsaayi tyi 6751 tyi + language Teke-Tyee tyx 6752 tyx + language Tektiteko ttc 6753 ttc + language Tela-Masbuar tvm 6754 tvm + language Telefol tlf 6755 tlf + language Telugu tel 6756 tel te + language Tem kdh 6757 kdh + language Temacine Tamazight tjo 6758 tjo + language Temascaltepec Nahuatl nhv 6759 nhv + language Tembo (Kitembo) tbt 6760 tbt + language Tembo (Motembo) tmv 6761 tmv + language Tembé tqb 6762 tqb + language Teme tdo 6763 tdo + language Temein teq 6764 teq + language Temi soz 6765 soz + language Temiar tea 6766 tea + language Temoaya Otomi ott 6767 ott + language Temoq tmo 6768 tmo + language Temuan tmw 6769 tmw + language Ten'edn tnz 6770 tnz + language Tena Lowland Quichua quw 6771 quw + language Tenango Otomi otn 6772 otn + language Tene Kan Dogon dtk 6773 dtk + language Tenggarong Kutai Malay vkt 6774 vkt + language Tengger tes 6775 tes + language Tenharim pah 6776 pah + language Tenino tqn 6777 tqn + language Tenis tns 6778 tns + language Tennet tex 6779 tex + language Teop tio 6780 tio + language Teor tev 6781 tev + language Tepecano tep 6782 tep + language Tepetotutla Chinantec cnt 6783 cnt + language Tepeuxila Cuicatec cux 6784 cux + language Tepinapa Chinantec cte 6785 cte + language Tepo Krumen ted 6786 ted + language Ter Sami sjt 6787 sjt + language Tera ttr 6788 ttr + language Terebu trb 6789 trb + language Terei buo 6790 buo + language Tereno ter 6791 ter + language Teressa tef 6792 tef + language Tereweng twg 6793 twg + language Teribe tfr 6794 tfr + language Terik tec 6795 tec + language Termanu twu 6796 twu + language Ternate tft 6797 tft + language Ternateño tmg 6798 tmg + language Tesaka Malagasy tkg 6799 tkg + language Tese keg 6800 keg + language Teshenawa twc 6801 twc + language Teso teo 6802 teo + language Tetela tll 6803 tll + language Tetelcingo Nahuatl nhg 6804 nhg + language Tetete teb 6805 teb + language Tetserret tez 6806 tez + language Tetum tet 6807 tet + language Tetun Dili tdt 6808 tdt + language Teutila Cuicatec cut 6809 cut + language Tewa (Indonesia) twe 6810 twe + language Tewa (USA) tew 6811 tew + language Tewe twx 6812 twx + language Texcatepec Otomi otx 6813 otx + language Texistepec Popoluca poq 6814 poq + language Texmelucan Zapotec zpz 6815 zpz + language Tezoatlán Mixtec mxb 6816 mxb + language Tha thy 6817 thy + language Thachanadan thn 6818 thn + language Thado Chin tcz 6819 tcz + language Thai tha 6820 tha th + language Thai Sign Language tsq 6821 tsq + language Thai Song soa 6822 soa + language Thaiphum Chin cth 6823 cth + language Thakali ths 6824 ths + language Thangal Naga nki 6825 nki + language Thangmi thf 6826 thf + language Thao ssf 6827 ssf + language Tharaka thk 6828 thk + language Thawa xtv 6829 xtv + language Thaypan typ 6830 typ + language Thiin iin 6831 iin + language Tho tou 6832 tou + language Thompson thp 6833 thp + language Thopho ytp 6834 ytp + language Thracian txh 6835 txh + language Thu Lao tyl 6836 tyl + language Thulung tdh 6837 tdh + language Thur lth 6838 lth + language Thuri thu 6839 thu + language Tiagbamrin Aizi ahi 6840 ahi + language Tiale mnl 6841 mnl + language Tiang tbj 6842 tbj + language Tibea ngy 6843 ngy + language Tibetan Sign Language lsn 6844 lsn + language Tibetan, Tibetan Standard, Central bod 6845 bod Tibetan Tibetan Central Tibetan Standard tib bo + language Tichurong tcn 6846 tcn + language Ticuna tca 6847 tca + language Tidaá Mixtec mtx 6848 mtx + language Tidikelt Tamazight tia 6849 tia + language Tidore tvo 6850 tvo + language Tiemacèwè Bozo boo 6851 boo + language Tiene tii 6852 tii + language Tifal tif 6853 tif + language Tigak tgc 6854 tgc + language Tigon Mbembe nza 6855 nza + language Tigre tig 6856 tig + language Tigrinya tir 6857 tir ti + language Tii txq 6858 txq + language Tijaltepec Mixtec xtl 6859 xtl + language Tikar tik 6860 tik + language Tikopia tkp 6861 tkp + language Tilapa Otomi otl 6862 otl + language Tillamook til 6863 til + language Tilquiapan Zapotec zts 6864 zts + language Tilung tij 6865 tij + language Tima tms 6866 tms + language Timbe tim 6867 tim + language Timne tem 6868 tem + language Timor Pidgin tvy 6869 tvy + language Timucua tjm 6870 tjm + language Timugon Murut tih 6871 tih + language Tinani lbf 6872 lbf + language Tindi tin 6873 tin + language Tingui-Boto tgv 6874 tgv + language Tinigua tit 6875 tit + language Tinputz tpz 6876 tpz + language Tippera tpe 6877 tpe + language Tira tic 6878 tic + language Tirahi tra 6879 tra + language Tiranige Diga Dogon tde 6880 tde + language Tiri cir 6881 cir + language Tirmaga-Chai Suri suq 6882 suq + language Tiruray tiy 6883 tiy + language Tita tdq 6884 tdq + language Titan ttv 6885 ttv + language Tiv tiv 6886 tiv + language Tiwa lax 6887 lax + language Tiwi tiw 6888 tiw + language Tiyaa tyy 6889 tyy + language Tiéfo tiq 6890 tiq + language Tiéyaxo Bozo boz 6891 boz + language Tjungundji tjj 6892 tjj + language Tjupany tjp 6893 tjp + language Tjurruru tju 6894 tju + language Tlachichilco Tepehua tpt 6895 tpt + language Tlacoapa Me'phaa tpl 6896 tpl + language Tlacoatzintepec Chinantec ctl 6897 ctl + language Tlacolulita Zapotec zpk 6898 zpk + language Tlahuitoltepec Mixe mxp 6899 mxp + language Tlamacazapa Nahuatl nuz 6900 nuz + language Tlazoyaltepec Mixtec mqh 6901 mqh + language Tlicho dgr 6902 dgr + language Tlingit tli 6903 tli + language To toz 6904 toz + language To'abaita mlu 6905 mlu + language Toaripi tqo 6906 tqo + language Toba tob 6907 tob + language Toba-Maskoy tmf 6908 tmf + language Tobagonian Creole English tgh 6909 tgh + language Tobanga tng 6910 tng + language Tobati tti 6911 tti + language Tobelo tlb 6912 tlb + language Tobian tox 6913 tox + language Tobilung tgb 6914 tgb + language Tobo tbv 6915 tbv + language Tocantins Asurini asu 6916 asu + language Tocho taz 6917 taz + language Toda tcx 6918 tcx + language Todrah tdr 6919 tdr + language Tofanma tlg 6920 tlg + language Tofin Gbe tfi 6921 tfi + language Togbo-Vara Banda tor 6922 tor + language Togoyo tgy 6923 tgy + language Tohono O'odham ood 6924 ood + language Tojolabal toj 6925 toj + language Tok Pisin tpi 6926 tpi + language Tokano zuh 6927 zuh + language Tokelau tkl 6928 tkl + language Tokharian A xto 6929 xto + language Tokharian B txb 6930 txb + language Toki Pona tok 6931 tok + language Toku-No-Shima tkn 6932 tkn + language Tol jic 6933 jic + language Tolaki lbw 6934 lbw + language Tolomako tlm 6935 tlm + language Tolowa tol 6936 tol + language Toma tod 6937 tod + language Tomadino tdi 6938 tdi + language Tombelala ttp 6939 ttp + language Tombonuo txa 6940 txa + language Tombulu tom 6941 tom + language Tomini txm 6942 txm + language Tommo So Dogon dto 6943 dto + language Tomo Kan Dogon dtm 6944 dtm + language Tomoip tqp 6945 tqp + language Tondano tdn 6946 tdn + language Tondi Songway Kiini tst 6947 tst + language Tonga (Nyasa) tog 6948 tog + language Tonga (Tonga Islands) ton 6949 ton to Tonga + language Tonga (Zambia) toi 6950 toi + language Tongwe tny 6951 tny + language Tonjon tjn 6952 tjn + language Tonkawa tqw 6953 tqw + language Tonsawang tnw 6954 tnw + language Tonsea txs 6955 txs + language Tontemboan tnt 6956 tnt + language Tooro ttj 6957 ttj + language Topoiyo toy 6958 toy + language Toposa toq 6959 toq + language Toraja-Sa'dan sda 6960 sda + language Toram trj 6961 trj + language Torau ttu 6962 ttu + language Tornedalen Finnish fit 6963 fit + language Toro tdv 6964 tdv + language Toro So Dogon dts 6965 dts + language Toro Tegu Dogon dtt 6966 dtt + language Toromono tno 6967 tno + language Torona tqr 6968 tqr + language Torres Strait Creole tcs 6969 tcs + language Torricelli tei 6970 tei + language Torwali trw 6971 trw + language Torá trz 6972 trz + language Tosk Albanian als 6973 als + language Totela ttl 6974 ttl + language Toto txo 6975 txo + language Totoli txe 6976 txe + language Totomachapan Zapotec zph 6977 zph + language Totontepec Mixe mto 6978 mto + language Totoro ttk 6979 ttk + language Touo tqu 6980 tqu + language Toura (Côte d'Ivoire) neb 6981 neb + language Toura (Papua New Guinea) don 6982 don + language Towei ttn 6983 ttn + language Transalpine Gaulish xtg 6984 xtg + language Traveller Danish rmd 6985 rmd + language Traveller Norwegian rmg 6986 rmg + language Traveller Scottish trl 6987 trl + language Tregami trm 6988 trm + language Tremembé tme 6989 tme + language Trieng stg 6990 stg + language Trimuris tip 6991 tip + language Tring tgq 6992 tgq + language Tringgus-Sembaan Bidayuh trx 6993 trx + language Trinidad and Tobago Sign Language lst 6994 lst + language Trinidadian Creole English trf 6995 trf + language Trinitario trn 6996 trn + language Trió tri 6997 tri + language Truká tka 6998 tka + language Trumai tpy 6999 tpy + language Ts'ün-Lao tsl 7000 tsl + language Tsaangi tsa 7001 tsa + language Tsakhur tkr 7002 tkr + language Tsakonian tsd 7003 tsd + language Tsakwambo kvz 7004 kvz + language Tsamai tsb 7005 tsb + language Tsat huq 7006 huq + language Tseku tsk 7007 tsk + language Tsetsaut txc 7008 txc + language Tshangla tsj 7009 tsj + language Tsikimba kdl 7010 kdl + language Tsimané cas 7011 cas + language Tsimihety Malagasy xmw 7012 xmw + language Tsimshian tsi 7013 tsi + language Tsishingini tsw 7014 tsw + language Tso ldp 7015 ldp + language Tsoa hio 7016 hio + language Tsogo tsv 7017 tsv + language Tsonga tso 7018 tso ts + language Tsotso lto 7019 lto + language Tsou tsu 7020 tsu + language Tsucuba cbq 7021 cbq + language Tsum ttz 7022 ttz + language Tsuvadi tvd 7023 tvd + language Tsuvan tsh 7024 tsh + language Tswa tsc 7025 tsc + language Tswana tsn 7026 tsn tn + language Tswapong two 7027 two + language Tu mjg 7028 mjg + language Tuamotuan pmt 7029 pmt + language Tubar tbu 7030 tbu + language Tucano tuo 7031 tuo + language Tugen tuy 7032 tuy + language Tugun tzn 7033 tzn + language Tugutil tuj 7034 tuj + language Tukang Besi North khc 7035 khc + language Tukang Besi South bhq 7036 bhq + language Tuki bag 7037 bag + language Tukpa tpq 7038 tpq + language Tukudede tkd 7039 tkd + language Tukumanféd tkf 7040 tkf + language Tula tul 7041 tul + language Tulai tvi 7042 tvi + language Tulehu tlu 7043 tlu + language Tulishi tey 7044 tey + language Tulu tcy 7045 tcy + language Tulu-Bohuai rak 7046 rak + language Tuma-Irumu iou 7047 iou + language Tumak tmc 7048 tmc + language Tumari Kanuri krt 7049 krt + language Tumbuka tum 7050 tum + language Tumi kku 7051 kku + language Tumleo tmq 7052 tmq + language Tumshuqese xtq 7053 xtq + language Tumtum tbr 7054 tbr + language Tumulung Sisaala sil 7055 sil + language Tumzabt mzb 7056 mzb + language Tundra Enets enh 7057 enh + language Tunen tvu 7058 tvu + language Tungag lcm 7059 lcm + language Tunggare trt 7060 trt + language Tunia tug 7061 tug + language Tunica tun 7062 tun + language Tunisian Arabic aeb 7063 aeb + language Tunisian Sign Language tse 7064 tse + language Tunjung tjg 7065 tjg + language Tunni tqq 7066 tqq + language Tunzu dza 7067 dza + language Tuotomb ttf 7068 ttf + language Tuparí tpr 7069 tpr + language Tupinambá tpn 7070 tpn + language Tupinikin tpk 7071 tpk + language Tupuri tui 7072 tui + language Turaka trh 7073 trh + language Turi trd 7074 trd + language Turiwára twt 7075 twt + language Turka tuz 7076 tuz + language Turkana tuv 7077 tuv + language Turkish tur 7078 tur tr + language Turkish Sign Language tsm 7079 tsm + language Turkmen tuk 7080 tuk tk + language Turks And Caicos Creole English tch 7081 tch + language Turoyo tru 7082 tru + language Turumsa tqm 7083 tqm + language Turung try 7084 try + language Tuscarora tus 7085 tus + language Tutelo tta 7086 tta + language Tutong ttg 7087 ttg + language Tutsa Naga tvt 7088 tvt + language Tutuba tmi 7089 tmi + language Tututepec Mixtec mtu 7090 mtu + language Tututni tuu 7091 tuu + language Tuvalu tvl 7092 tvl + language Tuvinian tyv 7093 tyv + language Tuwali Ifugao ifk 7094 ifk + language Tuwari tww 7095 tww + language Tuwuli bov 7096 bov + language Tuxináwa tux 7097 tux + language Tuxá tud 7098 tud + language Tuyuca tue 7099 tue + language Twana twa 7100 twa + language Twendi twn 7101 twn + language Twents twd 7102 twd + language Twi twi 7103 twi tw + language Tyap kcg 7104 kcg + language Tz'utujil tzj 7105 tzj + language Tzeltal tzh 7106 tzh + language Tzotzil tzo 7107 tzo + language Tày tyz 7108 tyz + language Tày Sa Pa tys 7109 tys + language Tày Tac tyt 7110 tyt + language Téén lor 7111 lor + language Tübatulabal tub 7112 tub + language U uuu 7113 uuu + language Uab Meto aoz 7114 aoz + language Uamué uam 7115 uam + language Uare ksj 7116 ksj + language Ubaghara byc 7117 byc + language Ubang uba 7118 uba + language Ubi ubi 7119 ubi + language Ubir ubr 7120 ubr + language Ubykh uby 7121 uby + language Ucayali-Yurúa Ashéninka cpb 7122 cpb + language Uda uda 7123 uda + language Udi udi 7124 udi + language Udihe ude 7125 ude + language Udmurt udm 7126 udm + language Uduk udu 7127 udu + language Ufim ufi 7128 ufi + language Ugandan Sign Language ugn 7129 ugn + language Ugaritic uga 7130 uga + language Ughele uge 7131 uge + language Ugong ugo 7132 ugo + language Uhami uha 7133 uha + language Uighur, Uyghur uig 7134 uig Uighur ug Uyghur + language Uisai uis 7135 uis + language Ujir udj 7136 udj + language Ukaan kcf 7137 kcf + language Ukhwejo ukh 7138 ukh + language Ukit umi 7139 umi + language Ukpe-Bayobiri ukp 7140 ukp + language Ukpet-Ehom akd 7141 akd + language Ukrainian ukr 7142 ukr uk + language Ukrainian Sign Language ukl 7143 ukl + language Ukue uku 7144 uku + language Ukuriguma ukg 7145 ukg + language Ukwa ukq 7146 ukq + language Ukwuani-Aboh-Ndoni ukw 7147 ukw + language Ulau-Suain svb 7148 svb + language Ulch ulc 7149 ulc + language Ulithian uli 7150 uli + language Ullatan ull 7151 ull + language Ulukwumi ulb 7152 ulb + language Ulumanda' ulm 7153 ulm + language Ulwa ulw 7154 ulw + language Uma ppk 7155 ppk + language Uma' Lasan xky 7156 xky + language Uma' Lung ulu 7157 ulu + language Umanakaina gdn 7158 gdn + language Umatilla uma 7159 uma + language Umbindhamu umd 7160 umd + language Umbrian xum 7161 xum + language Umbu-Ungu ubu 7162 ubu + language Umbugarla umr 7163 umr + language Umbundu umb 7164 umb + language Ume Sami sju 7165 sju + language Umeda upi 7166 upi + language Umiida xud 7167 xud + language Umiray Dumaget Agta due 7168 due + language Umon umm 7169 umm + language Umotína umo 7170 umo + language Umpila ump 7171 ump + language Una mtg 7172 mtg + language Unami unm 7173 unm + language Uncoded languages mis 7174 mis + language Unde Kaili unz 7175 unz + language Undetermined und 7176 und + language Uneapa bbn 7177 bbn + language Uneme une 7178 une + language Unggaranggu xun 7179 xun + language Unggumi xgu 7180 xgu + language Uni uni 7181 uni + language Unserdeutsch uln 7182 uln + language Unua onu 7183 onu + language Unubahe unu 7184 unu + language Upper Chehalis cjh 7185 cjh + language Upper Grand Valley Dani dna 7186 dna + language Upper Guinea Crioulo pov 7187 pov + language Upper Kinabatangan dmg 7188 dmg + language Upper Kuskokwim kuu 7189 kuu + language Upper Necaxa Totonac tku 7190 tku + language Upper Saxon sxu 7191 sxu + language Upper Sorbian hsb 7192 hsb + language Upper Ta'oih tth 7193 tth + language Upper Tanana tau 7194 tau + language Upper Taromi tov 7195 tov + language Upper Umpqua xup 7196 xup + language Ura (Papua New Guinea) uro 7197 uro + language Ura (Vanuatu) uur 7198 uur + language Uradhi urf 7199 urf + language Urak Lawoi' urk 7200 urk + language Urali url 7201 url + language Urapmin urm 7202 urm + language Urarina ura 7203 ura + language Urartian xur 7204 xur + language Urat urt 7205 urt + language Urdu urd 7206 urd ur + language Urhobo urh 7207 urh + language Uri uvh 7208 uvh + language Urigina urg 7209 urg + language Urim uri 7210 uri + language Urimo urx 7211 urx + language Uripiv-Wala-Rano-Atchin upv 7212 upv + language Urningangg urc 7213 urc + language Uru ure 7214 ure + language Uru-Eu-Wau-Wau urz 7215 urz + language Uru-Pa-In urp 7216 urp + language Uruangnirin urn 7217 urn + language Uruava urv 7218 urv + language Urubú-Kaapor urb 7219 urb + language Urubú-Kaapor Sign Language uks 7220 uks + language Uruguayan Sign Language ugy 7221 ugy + language Urum uum 7222 uum + language Urumi uru 7223 uru + language Usaghade usk 7224 usk + language Usan wnu 7225 wnu + language Usarufa usa 7226 usa + language Ushojo ush 7227 ush + language Usila Chinantec cuc 7228 cuc + language Usku ulf 7229 ulf + language Uspanteco usp 7230 usp + language Usui usi 7231 usi + language Utarmbung omo 7232 omo + language Ute-Southern Paiute ute 7233 ute + language Utu utu 7234 utu + language Uvbie evh 7235 evh + language Uya usu 7236 usu + language Uyajitaya duk 7237 duk + language Uzbek uzb 7238 uzb uz + language Uzbeki Arabic auz 7239 auz + language Uzekwe eze 7240 eze + language Vaagri Booli vaa 7241 vaa + language Vafsi vaf 7242 vaf + language Vaghri vgr 7243 vgr + language Vaghua tva 7244 tva + language Vagla vag 7245 vag + language Vai vai 7246 vai + language Vaiphei vap 7247 vap + language Vale vae 7248 vae + language Valencian Sign Language vsv 7249 vsv + language Valle Nacional Chinantec cvn 7250 cvn + language Valley Maidu vmv 7251 vmv + language Valman van 7252 van + language Valpei vlp 7253 vlp + language Vamale mkt 7254 mkt + language Vame mlr 7255 mlr + language Vandalic xvn 7256 xvn + language Vangunu mpr 7257 mpr + language Vanimo vam 7258 vam + language Vano vnk 7259 vnk + language Vanuma vau 7260 vau + language Vao vao 7261 vao + language Varhadi-Nagpuri vah 7262 vah + language Varisi vrs 7263 vrs + language Varli vav 7264 vav + language Vasavi vas 7265 vas + language Veddah ved 7266 ved + language Vedic Sanskrit vsn 7267 vsn + language Vehes val 7268 val + language Veluws vel 7269 vel + language Vemgo-Mabas vem 7270 vem + language Venda ven 7271 ven ve + language Venetian vec 7272 vec + language Venetic xve 7273 xve + language Venezuelan Sign Language vsl 7274 vsl + language Vengo bav 7275 bav + language Ventureño veo 7276 veo + language Veps vep 7277 vep + language Vera'a vra 7278 vra + language Vestinian xvs 7279 xvs + language Vidunda vid 7280 vid + language Viemo vig 7281 vig + language Vietnamese vie 7282 vie vi + language Vilela vil 7283 vil + language Vili vif 7284 vif + language Villa Viciosa Agta dyg 7285 dyg + language Vincentian Creole English svc 7286 svc + language Vinmavis vnm 7287 vnm + language Vinza vin 7288 vin + language Virgin Islands Creole English vic 7289 vic + language Vishavan vis 7290 vis + language Viti vit 7291 vit + language Vitou vto 7292 vto + language Vitu wiv 7293 wiv + language Vlaams vls 7294 vls + language Vlaamse Gebarentaal vgt 7295 vgt + language Vlax Romani rmy 7296 rmy + language Volapük vol 7297 vol vo + language Volscian xvo 7298 xvo + language Vono kch 7299 kch + language Voro vor 7300 vor vro Võro + language Votic vot 7301 vot + language Vumbu vum 7302 vum + language Vunapu vnp 7303 vnp + language Vunjo vun 7304 vun + language Vurës msn 7305 msn + language Vute vut 7306 vut + language Vwanji wbi 7307 wbi + language Wa wbm 7309 wbm + language Wa'ema wag 7310 wag + language Waama wwa 7311 wwa + language Waamwang wmn 7312 wmn + language Waata ssn 7313 ssn + language Wab wab 7314 wab + language Wabo wbb 7315 wbb + language Waboda kmx 7316 kmx + language Waci Gbe wci 7317 wci + language Wadaginam wdg 7318 wdg + language Waddar wbq 7319 wbq + language Wadi Wadi xwd 7320 xwd + language Wadikali wdk 7321 wdk + language Wadiyara Koli kxp 7322 kxp + language Wadjabangayi wdy 7323 wdy + language Wadjiginy wdj 7324 wdj + language Wadjigu wdu 7325 wdu + language Wae Rana wrx 7326 wrx + language Waffa waj 7327 waj + language Wagawaga wgb 7328 wgb + language Wagaya wga 7329 wga + language Wagdi wbr 7330 wbr + language Wagi fad 7331 fad + language Wagiman waq 7332 waq + language Wahau Kayan whu 7333 whu + language Wahau Kenyah whk 7334 whk + language Wahgi wgi 7335 wgi + language Waigali wbk 7336 wbk + language Waigeo wgo 7337 wgo + language Wailaki wlk 7338 wlk + language Wailapa wlr 7339 wlr + language Waima rro 7340 rro + language Waima'a wmh 7341 wmh + language Waimaha bao 7342 bao + language Waimiri-Atroari atr 7343 atr + language Waioli wli 7344 wli + language Waiwai waw 7345 waw + language Waja wja 7346 wja + language Wajarri wbv 7347 wbv + language Wajuk xwj 7348 xwj + language Waka wav 7349 wav + language Wakabunga wwb 7350 wwb + language Wakawaka wkw 7351 wkw + language Wakde wkd 7352 wkd + language Wakhi wbl 7353 wbl + language Wakoná waf 7354 waf + language Wala lgl 7355 lgl + language Walak wlw 7356 wlw + language Walangama nlw 7357 nlw + language Wali (Ghana) wlx 7358 wlx + language Wali (Sudan) wll 7359 wll + language Waling wly 7360 wly + language Walio wla 7361 wla + language Walla Walla waa 7362 waa + language Wallisian wls 7363 wls + language Walloon wln 7364 wln wa + language Walmajarri wmt 7365 wmt + language Walser wae 7366 wae + language Walungge ola 7367 ola + language Waluwarra wrb 7368 wrb + language Wamas wmc 7369 wmc + language Wambaya wmb 7370 wmb + language Wambon wms 7371 wms + language Wambule wme 7372 wme + language Wamesa wad 7373 wad + language Wamey cou 7374 cou + language Wamin wmi 7375 wmi + language Wampanoag wam 7376 wam + language Wampar lbq 7377 lbq + language Wampur waz 7378 waz + language Wan wan 7379 wan + language Wanap wnp 7380 wnp + language Wancho Naga nnp 7381 nnp + language Wanda wbh 7382 wbh + language Wandala mfi 7383 mfi + language Wandarang wnd 7384 wnd + language Wandji wdd 7385 wdd + language Waneci wne 7386 wne + language Wanga lwg 7387 lwg + language Wangaaybuwan-Ngiyambaa wyb 7388 wyb + language Wanggamala wnm 7389 wnm + language Wanggom wng 7390 wng + language Wangkangurru wgg 7391 wgg + language Wangkayutyuru wky 7392 wky + language Wangkumara xwk 7393 xwk + language Wannu jub 7394 jub + language Wano wno 7395 wno + language Wantoat wnc 7396 wnc + language Wanukaka wnk 7397 wnk + language Wanyi wny 7398 wny + language Wané hwa 7399 hwa + language Waorani auc 7400 auc + language Wapan juk 7401 juk + language Wapishana wap 7402 wap + language Wappo wao 7403 wao + language War-Jaintia aml 7404 aml + language Wara wbf 7405 wbf tci Wára + language Warao wba 7406 wba + language Waray (Australia) wrz 7407 wrz + language Waray (Philippines) war 7408 war + language Wardaman wrr 7409 wrr + language Wardandi wxw 7410 wxw + language Warembori wsa 7411 wsa + language Wares wai 7412 wai + language Waris wrs 7413 wrs + language Waritai wbe 7414 wbe + language Wariyangga wri 7415 wri + language Warji wji 7416 wji + language Warkay-Bipim bgv 7417 bgv + language Warlmanpa wrl 7418 wrl + language Warlpiri wbp 7419 wbp + language Warnang wrn 7420 wrn + language Warnman wbt 7421 wbt + language Waropen wrp 7422 wrp + language Warrgamay wgy 7423 wgy + language Warrwa wwr 7424 wwr + language Waru wru 7425 wru + language Warumungu wrm 7426 wrm + language Waruna wrv 7427 wrv + language Warungu wrg 7428 wrg + language Warwar Feni hrw 7429 hrw + language Wasa wss 7430 wss + language Wasco-Wishram wac 7431 wac + language Wasembo gsp 7432 gsp + language Washo was 7433 was + language Waskia wsk 7434 wsk + language Wasu wsu 7435 wsu + language Watakataui wtk 7436 wtk + language Watam wax 7437 wax + language Wathawurrung wth 7438 wth + language Watiwa wtf 7439 wtf + language Watubela wah 7440 wah + language Waube kop 7441 kop + language Waurá wau 7442 wau + language Wauyai wuy 7443 wuy + language Wawa www 7444 www + language Wawonii wow 7445 wow + language Waxianghua wxa 7446 wxa + language Wayampi oym 7447 oym + language Wayana way 7448 way + language Wayanad Chetti ctt 7449 ctt + language Wayoró wyr 7450 wyr + language Wayu vay 7451 vay + language Wayuu guc 7452 guc + language Wedau wed 7453 wed + language Weh weh 7454 weh + language Wejewa wew 7455 wew + language Welaun wlh 7456 wlh + language Weliki klh 7457 klh + language Welsh cym 7458 cym cy wel + language Welsh Romani rmw 7459 rmw + language Wemale weo 7460 weo + language Wemba Wemba xww 7461 xww + language Weme Gbe wem 7462 wem + language Wendat wdt 7463 wdt + language Wergaia weg 7464 weg + language Weri wer 7465 wer + language Wersing kvw 7466 kvw + language West Albay Bikol fbl 7467 fbl + language West Ambae nnd 7468 nnd + language West Bengal Sign Language wbs 7469 wbs + language West Berawan zbw 7470 zbw + language West Central Banda bbp 7471 bbp + language West Central Oromo gaz 7472 gaz + language West Coast Bajau bdr 7473 bdr + language West Damar drn 7474 drn + language West Goodenough ddi 7475 ddi + language West Kewa kew 7476 kew + language West Lembata lmj 7477 lmj + language West Makian mqs 7478 mqs + language West Masela mss 7479 mss + language West Tarangan txn 7480 txn + language West Uvean uve 7481 uve + language West Yugur ybe 7482 ybe + language West-Central Limba lia 7483 lia + language Western Abnaki abe 7484 abe + language Western Apache apw 7485 apw + language Western Armenian hyw 7486 hyw + language Western Arrarnta are 7487 are + language Western Balochi bgn 7488 bgn + language Western Bolivian Guaraní gnw 7489 gnw + language Western Bru brv 7490 brv + language Western Bukidnon Manobo mbb 7491 mbb + language Western Cham cja 7492 cja + language Western Dani dnw 7493 dnw + language Western Durango Nahuatl azn 7494 azn + language Western Fijian wyy 7495 wyy + language Western Frisian fry 7496 fry fy + language Western Highland Chatino ctp 7497 ctp + language Western Highland Purepecha pua 7498 pua + language Western Huasteca Nahuatl nhw 7499 nhw + language Western Juxtlahuaca Mixtec jmx 7500 jmx + language Western Kanjobal knj 7501 knj + language Western Karaboro kza 7502 kza + language Western Katu kuf 7503 kuf + language Western Kayah kyu 7504 kyu + language Western Keres kjq 7505 kjq + language Western Krahn krw 7506 krw + language Western Lalu ywl 7507 ywl + language Western Lawa lcp 7508 lcp + language Western Magar mrd 7509 mrd + language Western Maninkakan mlq 7510 mlq + language Western Mari mrj 7511 mrj + language Western Mashan Hmong hmw 7512 hmw + language Western Meohang raf 7513 raf + language Western Minyag wmg 7514 wmg + language Western Muria mut 7515 mut + language Western Neo-Aramaic amw 7516 amw + language Western Niger Fulfulde fuh 7517 fuh + language Western Ojibwa ojw 7518 ojw + language Western Panjabi pnb 7519 pnb + language Western Parbate Kham kjl 7520 kjl + language Western Penan pne 7521 pne + language Western Sisaala ssl 7522 ssl + language Western Subanon suc 7523 suc + language Western Tamang tdg 7524 tdg + language Western Tawbuid twb 7525 twb + language Western Tlacolula Valley Zapotec zab 7526 zab + language Western Totonac tqt 7527 tqt + language Western Tunebo tnb 7528 tnb + language Western Xiangxi Miao mmr 7529 mmr + language Western Xwla Gbe xwl 7530 xwl + language Western Yiddish yih 7531 yih + language Westphalien wep 7532 wep + language Wetamut wwo 7533 wwo + language Wewaw wea 7534 wea + language Weyto woy 7535 woy + language White Gelao giw 7536 giw + language White Lachi lwh 7537 lwh + language Whitesands tnp 7538 tnp + language Wiarumus tua 7539 tua + language Wichita wic 7540 wic + language Wichí Lhamtés Güisnay mzh 7541 mzh + language Wichí Lhamtés Nocten mtp 7542 mtp + language Wichí Lhamtés Vejoz wlv 7543 wlv + language Wik Ngathan wig 7544 wig + language Wik-Epa wie 7545 wie + language Wik-Iiyanh wij 7546 wij + language Wik-Keyangan wif 7547 wif + language Wik-Me'anha wih 7548 wih + language Wik-Mungkan wim 7549 wim + language Wikalkan wik 7550 wik + language Wikngenchera wua 7551 wua + language Wilawila wil 7552 wil + language Wintu wnw 7553 wnw + language Winyé kst 7554 kst + language Wipi gdr 7555 gdr + language Wiradjuri wrh 7556 wrh + language Wiraféd wir 7557 wir + language Wirangu wgu 7558 wgu + language Wiru wiu 7559 wiu + language Wiyot wiy 7560 wiy + language Woccon xwc 7561 xwc + language Wogamusin wog 7562 wog + language Wogeo woc 7563 woc + language Woi wbw 7564 wbw + language Woiwurrung wyi 7565 wyi + language Wojenaka jod 7566 jod + language Wolane wle 7567 wle + language Wolani wod 7568 wod + language Wolaytta wal 7569 wal + language Woleaian woe 7570 woe + language Wolio wlo 7571 wlo + language Wolof wol 7572 wol wo + language Wom (Nigeria) wom 7573 wom + language Wom (Papua New Guinea) wmo 7574 wmo + language Womo wmx 7575 wmx + language Wongo won 7576 won + language Woods Cree cwd 7577 cwd + language Woria wor 7578 wor + language Worimi kda 7579 kda + language Worodougou jud 7580 jud + language Worrorra wro 7581 wro + language Wotapuri-Katarqalai wsv 7582 wsv + language Wotjobaluk xwt 7583 xwt + language Wotu wtw 7584 wtw + language Woun Meu noa 7585 noa + language Written Oirat xwo 7586 xwo + language Wu Chinese wuu 7587 wuu + language Wuding-Luquan Yi ywq 7588 ywq + language Wudu wud 7589 wud + language Wuliwuli wlu 7590 wlu + language Wulna wux 7591 wux + language Wumboko bqm 7592 bqm + language Wumbvu wum 7593 wum + language Wumeng Nasu ywu 7594 ywu + language Wunai Bunu bwn 7595 bwn + language Wunambal wub 7596 wub + language Wunumara wnn 7597 wnn + language Wurrugu wur 7598 wur + language Wusa Nasu yig 7599 yig + language Wushi bse 7600 bse + language Wusi wsi 7601 wsi + language Wutung wut 7602 wut + language Wutunhua wuh 7603 wuh + language Wuvulu-Aua wuv 7604 wuv + language Wuzlam udl 7605 udl + language Wyandot wyn 7606 wyn + language Wymysorys wym 7607 wym + language Wãpha juw 7609 juw + language Wè Northern wob 7610 wob + language Wè Southern gxx 7611 gxx + language Wè Western wec 7612 wec + language Xaasongaxango kao 7613 kao + language Xadani Zapotec zax 7614 zax + language Xakriabá xkr 7615 xkr + language Xamtanga xan 7616 xan + language Xanaguía Zapotec ztg 7617 ztg + language Xavánte xav 7618 xav + language Xerénte xer 7619 xer + language Xetá xet 7620 xet + language Xhosa xho 7621 xho xh + language Xiang Chinese hsn 7622 hsn + language Xibe sjo 7623 sjo + language Xicotepec De Juárez Totonac too 7624 too + language Xinca xin 7625 xin + language Xingú Asuriní asn 7626 asn + language Xipaya xiy 7627 xiy + language Xiri xii 7628 xii + language Xiriâna xir 7629 xir + language Xishanba Lalo ywt 7630 ywt + language Xokleng xok 7631 xok + language Xukurú xoo 7632 xoo + language Xwela Gbe xwe 7633 xwe + language Xârâcùù ane 7634 ane + language Xârâgurè axx 7635 axx + language Yaaku muu 7636 muu + language Yabarana yar 7637 yar + language Yabaâna ybn 7638 ybn + language Yabem jae 7639 jae + language Yaben ybm 7640 ybm + language Yabong ybo 7641 ybo + language Yabula Yabula yxy 7642 yxy + language Yace ekr 7643 ekr + language Yaeyama rys 7644 rys + language Yafi wfg 7645 wfg + language Yagara yxg 7646 yxg + language Yagaria ygr 7647 ygr + language Yagnobi yai 7648 yai + language Yagomi ygm 7649 ygm + language Yagua yad 7650 yad + language Yagwoia ygw 7651 ygw + language Yahadian ner 7652 ner + language Yahang rhp 7653 rhp + language Yahuna ynu 7654 ynu + language Yaka (Central African Republic) axk 7655 axk + language Yaka (Congo) iyx 7656 iyx + language Yaka (Democratic Republic of Congo) yaf 7657 yaf + language Yakaikeke ykk 7658 ykk + language Yakama yak 7659 yak + language Yakan yka 7660 yka + language Yakha ybh 7661 ybh + language Yakoma yky 7662 yky + language Yakut sah 7663 sah + language Yala yba 7664 yba + language Yalahatan jal 7665 jal + language Yalakalore xyl 7666 xyl + language Yalarnnga ylr 7667 ylr + language Yale nce 7668 nce + language Yaleba ylb 7669 ylb + language Yalunka yal 7670 yal + language Yalálag Zapotec zpu 7671 zpu + language Yamap ymp 7672 ymp + language Yamba yam 7673 yam + language Yambes ymb 7674 ymb + language Yambeta yat 7675 yat + language Yamdena jmd 7676 jmd + language Yameo yme 7677 yme + language Yami tao 7678 tao + language Yaminahua yaa 7679 yaa + language Yamna ymn 7680 ymn + language Yamongeri ymg 7681 ymg + language Yamphu ybi 7682 ybi + language Yan-nhangu jay 7683 jay + language Yan-nhaŋu Sign Language yhs 7684 yhs + language Yana ynn 7685 ynn + language Yanahuanca Pasco Quechua qur 7686 qur + language Yanda yda 7687 yda + language Yanda Dom Dogon dym 7688 dym + language Yandjibara xyb 7689 xyb + language Yandruwandha ynd 7690 ynd + language Yanesha' ame 7691 ame + language Yang Zhuang zyg 7692 zyg + language Yangben yav 7693 yav + language Yangkam bsx 7694 bsx + language Yangman jng 7695 jng + language Yango yng 7696 yng + language Yangulam ynl 7697 ynl + language Yangum Dey yde 7698 yde + language Yangum Gel ygl 7699 ygl + language Yangum Mon ymo 7700 ymo + language Yankunytjatjara kdd 7701 kdd + language Yanomamö guu 7702 guu + language Yanomámi wca 7703 wca + language Yansi yns 7704 yns + language Yanyuwa jao 7705 jao + language Yao yao 7706 yao + language Yaosakor Asmat asy 7707 asy + language Yaouré yre 7708 yre + language Yapese yap 7709 yap + language Yapunda yev 7710 yev + language Yaqay jaq 7711 jaq + language Yaqui yaq 7712 yaq + language Yarawata yrw 7713 yrw + language Yardliyawarra yxl 7714 yxl + language Yareba yrb 7715 yrb + language Yareni Zapotec zae 7716 zae + language Yarluyandi yry 7717 yry + language Yaroamë yro 7718 yro + language Yarsun yrs 7719 yrs + language Yasa yko 7720 yko + language Yassic ysc 7721 ysc + language Yatay yty 7722 yty + language Yatee Zapotec zty 7723 zty + language Yatzachi Zapotec zav 7724 zav + language Yau (Morobe Province) yuw 7725 yuw + language Yau (Sandaun Province) yyu 7726 yyu + language Yaul yla 7727 yla + language Yauma yax 7728 yax + language Yaur jau 7729 jau + language Yautepec Zapotec zpb 7730 zpb + language Yauyos Quechua qux 7731 qux + language Yavitero yvt 7732 yvt + language Yawa yva 7733 yva + language Yawalapití yaw 7734 yaw + language Yawanawa ywn 7735 ywn + language Yawarawarga yww 7736 yww + language Yaweyuha yby 7737 yby + language Yawijibaya jbw 7738 jbw + language Yawiyo ybx 7739 ybx + language Yawuru ywr 7740 ywr + language Yaygir xya 7741 xya + language Yazgulyam yah 7742 yah + language Yecuatla Totonac tlc 7743 tlc + language Yei jei 7744 jei + language Yekhee ets 7745 ets + language Yekora ykr 7746 ykr + language Yela yel 7747 yel + language Yele yle 7748 yle + language Yelmek jel 7749 jel + language Yelogu ylg 7750 ylg + language Yemba ybb 7751 ybb + language Yemsa jnj 7752 jnj + language Yendang ynq 7753 ynq + language Yeni yei 7754 yei + language Yeniche yec 7755 yec + language Yerakai yra 7756 yra + language Yeretuar gop 7757 gop + language Yerong yrn 7758 yrn + language Yerukula yeu 7759 yeu + language Yessan-Mayo yss 7760 yss + language Yetfa yet 7761 yet + language Yevanic yej 7762 yej + language Yeyi yey 7763 yey + language Yiddish yid 7764 yid yi + language Yidgha ydg 7765 ydg + language Yidiny yii 7766 yii + language Yil yll 7767 yll + language Yilan Creole ycr 7768 ycr + language Yimas yee 7769 yee + language Yimchungru Naga yim 7770 yim + language Yinbaw Karen kvu 7771 kvu + language Yindjibarndi yij 7772 yij + language Yindjilandji yil 7773 yil + language Yine pib 7774 pib + language Yinggarda yia 7775 yia + language Yinhawangka ywg 7776 ywg + language Yiningayi ygi 7777 ygi + language Yintale Karen kvy 7778 kvy + language Yinwum yxm 7779 yxm + language Yir Yoront yyr 7780 yyr + language Yirandali ljw 7781 ljw + language Yirrk-Mel yrm 7782 yrm + language Yis yis 7783 yis + language Yitha Yitha xth 7784 xth + language Yoba yob 7785 yob + language Yocoboué Dida gud 7786 gud + language Yogad yog 7787 yog + language Yoidik ydk 7788 ydk + language Yoke yki 7789 yki + language Yokuts yok 7790 yok + language Yola yol 7791 yol + language Yoloxochitl Mixtec xty 7792 xty + language Yolŋu Sign Language ygs 7793 ygs + language Yom pil 7794 pil + language Yombe yom 7795 yom + language Yonaguni yoi 7796 yoi + language Yong yno 7797 yno + language Yongbei Zhuang zyb 7798 zyb + language Yongkom yon 7799 yon + language Yongnan Zhuang zyn 7800 zyn + language Yopno yut 7801 yut + language Yora mts 7802 mts + language Yoron yox 7803 yox + language Yorta Yorta xyy 7804 xyy + language Yoruba yor 7805 yor yo + language Yosondúa Mixtec mpm 7806 mpm + language Yotti yot 7807 yot + language Youjiang Zhuang zyj 7808 zyj + language Youle Jinuo jiu 7809 jiu + language Younuo Bunu buh 7810 buh + language Yout Wam ytw 7811 ytw + language Yoy yoy 7812 yoy + language Yuanga nua 7813 nua + language Yucatec Maya Sign Language msd 7814 msd + language Yucateco yua 7815 yua + language Yuchi yuc 7816 yuc + language Yucuañe Mixtec mvg 7817 mvg + language Yucuna ycn 7818 ycn + language Yue Chinese yue 7819 yue + language Yug yug 7820 yug + language Yugambal yub 7821 yub + language Yugoslavian Sign Language ysl 7822 ysl + language Yugul ygu 7823 ygu + language Yuhup yab 7824 yab + language Yuki yuk 7825 yuk + language Yukpa yup 7826 yup + language Yukuben ybl 7827 ybl + language Yulu yul 7828 yul + language Yuqui yuq 7829 yuq + language Yuracare yuz 7830 yuz + language Yurats rts 7831 rts + language Yurok yur 7832 yur + language Yuru ljx 7833 ljx + language Yurutí yui 7834 yui + language Yutanduchi Mixtec mab 7835 mab + language Yuwana yau 7836 yau + language Yuyu yxu 7837 yxu + language Ywom gek 7838 gek + language Yámana yag 7839 yag + language Zaachila Zapotec ztx 7840 ztx + language Zabana kji 7841 kji + language Zacatepec Chatino ctz 7842 ctz + language Zacatlán-Ahuacatlán-Tepetzintla Nahuatl nhi 7843 nhi + language Zaghawa zag 7844 zag + language Zaiwa atb 7845 atb + language Zakhring zkr 7846 zkr + language Zambian Sign Language zsl 7847 zsl + language Zan Gula zna 7848 zna + language Zanaki zak 7849 zak + language Zande (individual language) zne 7850 zne + language Zangskari zau 7851 zau + language Zangwal zah 7852 zah + language Zaniza Zapotec zpw 7853 zpw + language Zapotec zap 7854 zap + language Zaramo zaj 7855 zaj + language Zari zaz 7856 zaz + language Zarma dje 7857 dje + language Zarphatic zrp 7858 zrp + language Zauzou zal 7859 zal + language Zay zwa 7860 zwa + language Zayein Karen kxk 7861 kxk + language Zayse-Zergulla zay 7862 zay + language Zaza zza 7863 zza + language Zazao jaj 7864 jaj + language Zeem zem 7865 zem + language Zeeuws zea 7866 zea + language Zemba dhm 7867 dhm + language Zeme Naga nzm 7868 nzm + language Zemgalian xzm 7869 xzm + language Zenag zeg 7870 zeg + language Zenaga zen 7871 zen + language Zenzontepec Chatino czn 7872 czn + language Zerenkel zrn 7873 zrn + language Zhaba zhb 7874 zhb + language Zhang-Zhung xzh 7875 xzh + language Zhire zhi 7876 zhi + language Zhoa zhw 7877 zhw + language Zhuang, Chuang zha 7878 zha za Zhuang Chuang + language Zia zia 7879 zia + language Zialo zil 7880 zil + language Zigula ziw 7881 ziw + language Zimakani zik 7882 zik + language Zimba zmb 7883 zmb + language Zimbabwe Sign Language zib 7884 zib + language Zinza zin 7885 zin + language Zire sih 7886 sih + language Zizilivakan ziz 7887 ziz + language Zo'é pto 7888 pto + language Zokhuo yzk 7889 yzk + language Zoogocho Zapotec zpq 7890 zpq + language Zoroastrian Dari gbz 7891 gbz + language Zotung Chin czt 7892 czt + language Zou zom 7893 zom + language Zul zlu 7894 zlu + language Zula zla 7895 zla + language Zulgo-Gemzek gnd 7896 gnd + language Zulu zul 7897 zul zu + language Zumaya zuy 7898 zuy + language Zumbun jmb 7899 jmb + language Zuni zun 7900 zun + language Zuojiang Zhuang zzj 7901 zzj + language Zyphe Chin zyp 7902 zyp + language Záparo zro 7903 zro + language sTodsde jih 7904 jih + language us-Saare uss 7905 uss + language ut-Hun uth 7906 uth + language ut-Ma'in gel 7907 gel + language Àhàn ahn 7908 ahn + language Áncá acb 7909 acb + language Ömie aom 7910 aom + language Önge oon 7911 oon + language ǀGwi gwj 7912 gwj + language ǀXam xam 7913 xam + language ǁAni hnh 7914 hnh + language ǁGana gnk 7915 gnk + language ǁXegwi xeg 7916 xeg + language ǂHua huc 7917 huc + language ǂUngkue gku 7918 gku + language ǃXóõ nmn 7919 nmn + language Not applicable 7920 + publicationRelationType IsCitedBy RT1 1 + publicationRelationType Cites RT2 2 + publicationRelationType IsSupplementTo RT3 3 + publicationRelationType IsSupplementedBy RT4 4 + publicationRelationType IsReferencedBy RT5 5 + publicationRelationType References RT6 6 \ No newline at end of file diff --git a/scripts/api/data/metadatablocks/computational_workflow.tsv b/scripts/api/data/metadatablocks/computational_workflow.tsv index 51b69cfdb80..3cd0c26a464 100644 --- a/scripts/api/data/metadatablocks/computational_workflow.tsv +++ b/scripts/api/data/metadatablocks/computational_workflow.tsv @@ -2,7 +2,7 @@ computationalworkflow Computational Workflow Metadata #datasetField name title description watermark fieldType displayOrder displayFormat advancedSearchField allowControlledVocabulary allowmultiples facetable displayoncreate required parent metadatablock_id termURI workflowType Computational Workflow Type The kind of Computational Workflow, which is designed to compose and execute a series of computational or data manipulation steps in a scientific application text 0 TRUE TRUE TRUE TRUE TRUE FALSE computationalworkflow - workflowCodeRepository External Code Repository URL A link to the repository where the un-compiled, human readable code and related code is located (e.g. GitHub, GitLab, SVN) https://... url 1 FALSE FALSE TRUE FALSE TRUE FALSE computationalworkflow + workflowCodeRepository External Code Repository URL A link to the repository where the un-compiled, human readable code and related code is located (e.g. GitHub, GitLab, SVN) https://... url 1 #VALUE FALSE FALSE TRUE FALSE TRUE FALSE computationalworkflow workflowDocumentation Documentation A link (URL) to the documentation or text describing the Computational Workflow and its use textbox 2 FALSE FALSE TRUE FALSE TRUE FALSE computationalworkflow #controlledVocabulary DatasetField Value identifier displayOrder workflowType Common Workflow Language (CWL) workflowtype_cwl 1 diff --git a/scripts/api/data/metadatablocks/geospatial.tsv b/scripts/api/data/metadatablocks/geospatial.tsv index ce481c1bf84..11408317410 100644 --- a/scripts/api/data/metadatablocks/geospatial.tsv +++ b/scripts/api/data/metadatablocks/geospatial.tsv @@ -8,10 +8,10 @@ otherGeographicCoverage Other Other information on the geographic coverage of the data. text 4 #VALUE, FALSE FALSE FALSE TRUE FALSE FALSE geographicCoverage geospatial geographicUnit Geographic Unit Lowest level of geographic aggregation covered by the Dataset, e.g., village, county, region. text 5 TRUE FALSE TRUE TRUE FALSE FALSE geospatial geographicBoundingBox Geographic Bounding Box The fundamental geometric description for any Dataset that models geography is the geographic bounding box. It describes the minimum box, defined by west and east longitudes and north and south latitudes, which includes the largest geographic extent of the Dataset's geographic coverage. This element is used in the first pass of a coordinate-based search. Inclusion of this element in the codebook is recommended, but is required if the bound polygon box is included. none 6 FALSE FALSE TRUE FALSE FALSE FALSE geospatial - westLongitude Westernmost (Left) Longitude Westernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -180,0 <= West Bounding Longitude Value <= 180,0. text 7 FALSE FALSE FALSE FALSE FALSE FALSE geographicBoundingBox geospatial - eastLongitude Easternmost (Right) Longitude Easternmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -180,0 <= East Bounding Longitude Value <= 180,0. text 8 FALSE FALSE FALSE FALSE FALSE FALSE geographicBoundingBox geospatial - northLongitude Northernmost (Top) Latitude Northernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -90,0 <= North Bounding Latitude Value <= 90,0. text 9 FALSE FALSE FALSE FALSE FALSE FALSE geographicBoundingBox geospatial - southLongitude Southernmost (Bottom) Latitude Southernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -90,0 <= South Bounding Latitude Value <= 90,0. text 10 FALSE FALSE FALSE FALSE FALSE FALSE geographicBoundingBox geospatial + westLongitude Westernmost (Left) Longitude Westernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -180.0 <= West Bounding Longitude Value <= 180.0. text 7 FALSE FALSE FALSE FALSE FALSE FALSE geographicBoundingBox geospatial + eastLongitude Easternmost (Right) Longitude Easternmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -180.0 <= East Bounding Longitude Value <= 180.0. text 8 FALSE FALSE FALSE FALSE FALSE FALSE geographicBoundingBox geospatial + northLatitude Northernmost (Top) Latitude Northernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -90.0 <= North Bounding Latitude Value <= 90.0. text 9 FALSE FALSE FALSE FALSE FALSE FALSE geographicBoundingBox geospatial + southLatitude Southernmost (Bottom) Latitude Southernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -90.0 <= South Bounding Latitude Value <= 90.0. text 10 FALSE FALSE FALSE FALSE FALSE FALSE geographicBoundingBox geospatial #controlledVocabulary DatasetField Value identifier displayOrder country Afghanistan 0 country Albania 1 diff --git a/scripts/api/data/storageSites/add-storage-site.json b/scripts/api/data/storageSites/add-storage-site.json deleted file mode 100644 index d13ec2f165d..00000000000 --- a/scripts/api/data/storageSites/add-storage-site.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "hostname": "dataverse.librascholar.edu", - "name": "LibraScholar, USA", - "primaryStorage": true, - "transferProtocols": "rsync,posix,globus" -} diff --git a/scripts/api/setup-all.sh b/scripts/api/setup-all.sh index e247caa72b5..b7f962209e4 100755 --- a/scripts/api/setup-all.sh +++ b/scripts/api/setup-all.sh @@ -57,10 +57,6 @@ echo "- Allow internal signup" 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" @@ -69,7 +65,7 @@ echo echo "Setting up the admin user (and as superuser)" 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" +curl -X PUT "${DATAVERSE_URL}/api/admin/superuser/dataverseAdmin" -d "true" echo echo "Setting up the root dataverse" diff --git a/scripts/api/setup-optional-harvard.sh b/scripts/api/setup-optional-harvard.sh index fcbcc08a8e6..1311464e8ff 100755 --- a/scripts/api/setup-optional-harvard.sh +++ b/scripts/api/setup-optional-harvard.sh @@ -3,6 +3,7 @@ SERVER=http://localhost:8080/api echo "Setting up Harvard-specific settings" # :Authority and :Shoulder are commented out so this script can be used on test servers +# Should now use the new multipid JVM options instead of these settings #curl -X PUT -d 10.7910 "$SERVER/admin/settings/:Authority" #curl -X PUT -d "DVN/" "$SERVER/admin/settings/:Shoulder" echo "- Application Status header" diff --git a/scripts/deploy/phoenix.dataverse.org/post b/scripts/deploy/phoenix.dataverse.org/post index e4c8817844b..9d37c183a1a 100755 --- a/scripts/deploy/phoenix.dataverse.org/post +++ b/scripts/deploy/phoenix.dataverse.org/post @@ -4,7 +4,6 @@ cd scripts/api cd ../.. psql -U dvnapp dvndb -f scripts/database/reference_data.sql psql -U dvnapp dvndb -f doc/sphinx-guides/source/_static/util/createsequence.sql -curl http://localhost:8080/api/admin/settings/:DoiProvider -X PUT -d FAKE scripts/search/tests/publish-dataverse-root git checkout scripts/api/data/dv-root.json scripts/search/tests/grant-authusers-add-on-root diff --git a/scripts/dev/dev-rebuild.sh b/scripts/dev/dev-rebuild.sh index 9eae195b135..898212b4664 100755 --- a/scripts/dev/dev-rebuild.sh +++ b/scripts/dev/dev-rebuild.sh @@ -56,9 +56,6 @@ cd ../.. echo "Creating SQL sequence..." psql -h localhost -U $DB_USER $DB_NAME -f doc/sphinx-guides/source/_static/util/createsequence.sql -echo "Setting DOI provider to \"FAKE\"..." -curl http://localhost:8080/api/admin/settings/:DoiProvider -X PUT -d FAKE - echo "Allowing GUI edits to be visible without redeploy..." $PAYARA_DIR/glassfish/bin/asadmin create-system-properties "dataverse.jsf.refresh-period=1" diff --git a/scripts/dev/docker-final-setup.sh b/scripts/dev/docker-final-setup.sh index d2453619ec2..e20ce7ad6b6 100755 --- a/scripts/dev/docker-final-setup.sh +++ b/scripts/dev/docker-final-setup.sh @@ -10,9 +10,6 @@ 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 diff --git a/scripts/installer/Makefile b/scripts/installer/Makefile index 399bc65168a..8ea95534986 100644 --- a/scripts/installer/Makefile +++ b/scripts/installer/Makefile @@ -55,13 +55,13 @@ ${JHOVE_SCHEMA}: ../../conf/jhove/jhoveConfig.xsd ${INSTALLER_ZIP_DIR} @echo copying jhove schema file /bin/cp ../../conf/jhove/jhoveConfig.xsd ${INSTALLER_ZIP_DIR} -${SOLR_SCHEMA}: ../../conf/solr/9.3.0/schema.xml ../../conf/solr/9.3.0/update-fields.sh ${INSTALLER_ZIP_DIR} +${SOLR_SCHEMA}: ../../conf/solr/schema.xml ../../conf/solr/update-fields.sh ${INSTALLER_ZIP_DIR} @echo copying Solr schema file - /bin/cp ../../conf/solr/9.3.0/schema.xml ../../conf/solr/9.3.0/update-fields.sh ${INSTALLER_ZIP_DIR} + /bin/cp ../../conf/solr/schema.xml ../../conf/solr/update-fields.sh ${INSTALLER_ZIP_DIR} -${SOLR_CONFIG}: ../../conf/solr/9.3.0/solrconfig.xml ${INSTALLER_ZIP_DIR} +${SOLR_CONFIG}: ../../conf/solr/solrconfig.xml ${INSTALLER_ZIP_DIR} @echo copying Solr config file - /bin/cp ../../conf/solr/9.3.0/solrconfig.xml ${INSTALLER_ZIP_DIR} + /bin/cp ../../conf/solr/solrconfig.xml ${INSTALLER_ZIP_DIR} ${PYTHON_FILES}: README_python.txt install.py installConfig.py installAppServer.py installUtils.py requirements.txt default.config interactive.config ${INSTALLER_ZIP_DIR} @echo copying Python installer files diff --git a/scripts/installer/as-setup.sh b/scripts/installer/as-setup.sh index fc5b378cff5..e87122ba77c 100755 --- a/scripts/installer/as-setup.sh +++ b/scripts/installer/as-setup.sh @@ -102,23 +102,22 @@ function preliminary_setup() # password reset token timeout in minutes ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddataverse.auth.password-reset-timeout-in-minutes=60" - # DataCite DOI Settings + # Fake DOI Settings # (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 "\-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" - + ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddataverse.pid.providers=fake" + ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddataverse.pid.fake.type=FAKE" + ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddataverse.pid.fake.label=Fake DOI Provider" + ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddataverse.pid.fake.authority=10.5072" + ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddataverse.pid.fake.shoulder=FK2/" + ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddataverse.pid.default-provider=fake" # jvm-options use colons as separators, escape as literal - DOI_DATACITERESTAPIURL_ESC=`echo $DOI_DATACITERESTAPIURL | sed -e 's/:/\\\:/'` - ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddataverse.pid.datacite.rest-api-url=$DOI_DATACITERESTAPIURL_ESC" + #DOI_DATACITERESTAPIURL_ESC=`echo $DOI_DATACITERESTAPIURL | sed -e 's/:/\\\:/'` + #./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddataverse.pid.testDC.datacite.rest-api-url=$DOI_DATACITERESTAPIURL_ESC" ./asadmin $ASADMIN_OPTS create-jvm-options "-Ddataverse.timerServer=true" - # Workaround for FISH-7722: Failed to deploy war with @Stateless https://github.com/payara/Payara/issues/6337 - ./asadmin $ASADMIN_OPTS create-jvm-options --add-opens=java.base/java.io=ALL-UNNAMED - # enable comet support ./asadmin $ASADMIN_OPTS set server-config.network-config.protocols.protocol.http-listener-1.http.comet-support-enabled="true" @@ -146,12 +145,10 @@ function final_setup(){ # delete any existing mail/notifyMailSession; configure port, if provided: ./asadmin delete-javamail-resource mail/notifyMailSession - - if [ $SMTP_SERVER_PORT"x" != "x" ] - then - ./asadmin $ASADMIN_OPTS create-javamail-resource --mailhost "$SMTP_SERVER" --mailuser "dataversenotify" --fromaddress "do-not-reply@${HOST_ADDRESS}" --property mail.smtp.port="${SMTP_SERVER_PORT}" mail/notifyMailSession - else - ./asadmin $ASADMIN_OPTS create-javamail-resource --mailhost "$SMTP_SERVER" --mailuser "dataversenotify" --fromaddress "do-not-reply@${HOST_ADDRESS}" mail/notifyMailSession + ./asadmin $ASADMIN_OPTS create-system-properties "dataverse.mail.system-email='${ADMIN_EMAIL}'" + ./asadmin $ASADMIN_OPTS create-system-properties "dataverse.mail.mta.host='${SMTP_SERVER}'" + if [ "x${SMTP_SERVER_PORT}" != "x" ]; then + ./asadmin $ASADMIN_OPTS create-system-properties "dataverse.mail.mta.port='${SMTP_SERVER_PORT}'" fi } @@ -279,6 +276,12 @@ if [ ! -d "$DOMAIN_DIR" ] exit 2 fi +if [ -z "$ADMIN_EMAIL" ] + then + echo "You must specify the system admin email address (ADMIN_EMAIL)." + exit 1 +fi + echo "Setting up your app. server (Payara) to support Dataverse" echo "Payara directory: "$GLASSFISH_ROOT echo "Domain directory: "$DOMAIN_DIR diff --git a/scripts/installer/install.py b/scripts/installer/install.py index 18995695638..005fbad46e0 100644 --- a/scripts/installer/install.py +++ b/scripts/installer/install.py @@ -568,14 +568,6 @@ except: sys.exit("Failure to execute setup-all.sh! aborting.") -# 7b. configure admin email in the application settings -print("configuring system email address...") -returnCode = subprocess.call(["curl", "-X", "PUT", "-d", adminEmail, apiUrl+"/admin/settings/:SystemEmail"]) -if returnCode != 0: - print("\nWARNING: failed to configure the admin email in the Dataverse settings!") -else: - print("\ndone.") - # 8c. configure remote Solr location, if specified if solrLocation != "LOCAL": print("configuring remote Solr location... ("+solrLocation+")") @@ -591,15 +583,14 @@ print("\n\nYou should now have a running Dataverse instance at") print(" http://" + hostName + ":8080\n\n") -# DataCite instructions: +# PID instructions: -print("\nYour Dataverse has been configured to use DataCite, to register DOI global identifiers in the ") +print("\nYour Dataverse has been configured to use a Fake DOI Provider, registering (non-resolvable) DOI global identifiers in the ") print("test name space \"10.5072\" with the \"shoulder\" \"FK2\"") -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-Ddataverse.pid.datacite.username=...") -print("\t-Ddataverse.pid.datacite.password=...") +print("You can reconfigure to use additional/alternative providers.") +print("If you intend to use DOIs, you should contact DataCite (support\@datacite.org) or GDCC (see https://www.gdcc.io/about.html) and request a test account.") +print("Once you receive the account information (name, password, authority, shoulder), add them to your configuration ") +print("as described in the Dataverse Guides (see https://guides.dataverse.org/en/latest/installation/config.html#persistent-identifiers-and-publishing-datasets),") 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/installer/installAppServer.py b/scripts/installer/installAppServer.py index 698f5ba9a58..7636490c583 100644 --- a/scripts/installer/installAppServer.py +++ b/scripts/installer/installAppServer.py @@ -6,8 +6,9 @@ def runAsadminScript(config): # commands to set up all the app. server (payara6) components for the application. # All the parameters must be passed to that script as environmental # variables: - os.environ['GLASSFISH_DOMAIN'] = "domain1"; - os.environ['ASADMIN_OPTS'] = ""; + os.environ['GLASSFISH_DOMAIN'] = "domain1" + os.environ['ASADMIN_OPTS'] = "" + os.environ['ADMIN_EMAIL'] = config.get('system','ADMIN_EMAIL') os.environ['HOST_ADDRESS'] = config.get('glassfish','HOST_DNS_ADDRESS') os.environ['GLASSFISH_ROOT'] = config.get('glassfish','GLASSFISH_DIRECTORY') diff --git a/scripts/intellij/cpwebapp.sh b/scripts/intellij/cpwebapp.sh deleted file mode 100755 index 6ecad367048..00000000000 --- a/scripts/intellij/cpwebapp.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -# -# cpwebapp -# -# Usage: -# -# Add a File watcher by importing watchers.xml into IntelliJ IDEA, and let it do the copying whenever you save a -# file under webapp. -# -# https://www.jetbrains.com/help/idea/settings-tools-file-watchers.html -# -# Alternatively, you can add an External tool and trigger via menu or shortcut to do the copying manually: -# -# https://www.jetbrains.com/help/idea/configuring-third-party-tools.html -# - -PROJECT_DIR=$1 -FILE_TO_COPY=$2 -RELATIVE_PATH="${FILE_TO_COPY#$PROJECT_DIR/}" - -# Check if RELATIVE_PATH starts with 'src/main/webapp', otherwise ignore -if [[ $RELATIVE_PATH == src/main/webapp* ]]; then - # Get current version. Any other way to do this? A simple VERSION file would help. - VERSION=`perl -ne 'print $1 if /(.*?)<\/revision>/' ./modules/dataverse-parent/pom.xml` - RELATIVE_PATH_WITHOUT_WEBAPP="${RELATIVE_PATH#src/main/webapp/}" - TARGET_DIR=./docker-dev-volumes/glassfish/applications/dataverse-$VERSION - TARGET_PATH="${TARGET_DIR}/${RELATIVE_PATH_WITHOUT_WEBAPP}" - - mkdir -p "$(dirname "$TARGET_PATH")" - cp "$FILE_TO_COPY" "$TARGET_PATH" - - echo "File $FILE_TO_COPY copied to $TARGET_PATH" -fi diff --git a/scripts/issues/8578/citation_languages_10762.tsv b/scripts/issues/8578/citation_languages_10762.tsv new file mode 100644 index 00000000000..bd68edaee72 --- /dev/null +++ b/scripts/issues/8578/citation_languages_10762.tsv @@ -0,0 +1,7921 @@ + language 'Are'are alu 0 alu + language 'Auhelawa kud 1 kud + language A'ou aou 2 aou + language A-Pucikwar apq 3 apq + language Aari aiw 4 aiw + language Aasáx aas 5 aas + language Abadi kbt 6 kbt + language Abaga abg 7 abg + language Abai Sungai abf 8 abf + language Abanyom abm 9 abm + language Abar mij 10 mij + language Abau aau 11 aau + language Abaza abq 12 abq + language Abellen Ayta abp 13 abp + language Abidji abi 14 abi + language Abinomn bsa 15 bsa + language Abipon axb 16 axb + language Abishira ash 17 ash + language Abkhaz, Abkhazian abk 18 abk ab Abkhaz Abkhazian + language Abom aob 19 aob + language Abon abo 20 abo + language Abron abr 21 abr + language Abu ado 22 ado + language Abu' Arapesh aah 23 aah + language Abua abn 24 abn + language Abui abz 25 abz + language Abun kgr 26 kgr + language Abure abu 27 abu + language Abureni mgj 28 mgj + language Abé aba 29 aba + language Acatepec Me'phaa tpx 30 tpx + language Achagua aca 31 aca + language Achang acn 32 acn + language Ache yif 33 yif + language Acheron acz 34 acz + language Achi acr 35 acr + language Achinese ace 36 ace + language Achterhoeks act 37 act + language Achuar-Shiwiar acu 38 acu + language Achumawi acv 39 acv + language Aché guq 40 guq + language Acoli ach 41 ach + language Acroá acs 42 acs + language Adai xad 43 xad + language Adamawa Fulfulde fub 44 fub + language Adamorobe Sign Language ads 45 ads + language Adang adn 46 adn + language Adangbe adq 47 adq + language Adangme ada 48 ada + language Adara kad 49 kad + language Adasen tiu 50 tiu + language Adele ade 51 ade + language Adhola adh 52 adh + language Adi adi 53 adi + language Adilabad Gondi wsg 54 wsg + language Adioukrou adj 55 adj + language Adithinngithigh dth 56 dth + language Adivasi Oriya ort 57 ort + language Adiwasi Garasia gas 58 gas + language Adnyamathanha adt 59 adt + language Adonara adr 60 adr + language Aduge adu 61 adu + language Adyghe ady 62 ady + language Adzera adz 63 adz + language Aeka aez 64 aez + language Aekyom awi 65 awi + language Aequian xae 66 xae + language Aer aeq 67 aeq + language Afade aal 68 aal + language Afar aar 69 aar aa + language Afghan Sign Language afg 70 afg + language Afitti aft 71 aft + language Afrihili afh 72 afh + language Afrikaans afr 73 afr af + language Afro-Seminole Creole afs 74 afs + language Agarabi agd 75 agd + language Agariya agi 76 agi + language Agatu agc 77 agc + language Agavotaguerra avo 78 avo + language Aghem agq 79 agq + language Aghu ahh 80 ahh + language Aghu-Tharnggala gtu 81 gtu + language Aghul agx 82 agx + language Aghwan xag 83 xag + language Agi aif 84 aif + language Agob kit 85 kit + language Agoi ibm 86 ibm + language Aguacateco agu 87 agu + language Aguano aga 88 aga + language Aguaruna agr 89 agr + language Aguna aug 90 aug + language Agusan Manobo msm 91 msm + language Agutaynen agn 92 agn + language Agwagwune yay 93 yay + language Ahanta aha 94 aha + language Aheri Gondi esg 95 esg + language Aheu thm 96 thm + language Ahirani ahr 97 ahr + language Ahom aho 98 aho + language Ahtena aht 99 aht + language Ahwai nfd 100 nfd + language Ai-Cham aih 101 aih + language Aighon aix 102 aix + language Aikanã tba 103 tba + language Aiklep mwg 104 mwg + language Aimaq aiq 105 aiq + language Aimele ail 106 ail + language Aimol aim 107 aim + language Ainbai aic 108 aic + language Ainu (China) aib 109 aib + language Ainu (Japan) ain 110 ain + language Aiome aki 111 aki + language Airoran air 112 air + language Aiton aio 113 aio + language Aja (Benin) ajg 114 ajg + language Aja (South Sudan) aja 115 aja + language Ajawa ajw 116 ajw + language Ajië aji 117 aji + language Ajumbu muc 118 muc + language Ajyíninka Apurucayali cpc 119 cpc + language Ak akq 120 akq + language Aka soh 121 soh + language Aka-Bea abj 122 abj + language Aka-Bo akm 123 akm + language Aka-Cari aci 124 aci + language Aka-Jeru akj 125 akj + language Aka-Kede akx 126 akx + language Aka-Kol aky 127 aky + language Aka-Kora ack 128 ack + language Akan aka 129 aka ak + language Akar-Bale acl 130 acl + language Akaselem aks 131 aks + language Akawaio ake 132 ake + language Ake aik 133 aik + language Akebu keu 134 keu + language Akei tsr 135 tsr + language Akeu aeu 136 aeu + language Akha ahk 137 ahk + language Akhvakh akv 138 akv + language Akkadian akk 139 akk + language Akkala Sami sia 140 sia + language Aklanon akl 141 akl + language Akolet akt 142 akt + language Akoose bss 143 bss + language Akoye miw 144 miw + language Akpa akf 145 akf + language Akpes ibe 146 ibe + language Akrukay afi 147 afi + language Akukem spm 148 spm + language Akuku ayk 149 ayk + language Akum aku 150 aku + language Akuntsu aqz 151 aqz + language Akurio ako 152 ako + language Akwa akw 153 akw + language Akyaung Ari Naga nqy 154 nqy + language Al-Sayyid Bedouin Sign Language syy 155 syy + language Alaba-K’abeena alw 156 alw + language Alabama akz 157 akz + language Alabat Island Agta dul 158 dul + language Alacatlatzala Mixtec mim 159 mim + language Alago ala 160 ala + language Alagwa wbj 161 wbj + language Alak alk 162 alk + language Alamblak amp 163 amp + language Alangan alj 164 alj + language Alanic xln 165 xln + language Alapmunte apv 166 apv + language Alawa alh 167 alh + language Albanian sqi 168 sqi alb sq + language Albanian Sign Language sqk 169 sqk + language Albarradas Sign Language lsc 170 lsc + language Alcozauca Mixtec xta 171 xta + language Alege alf 172 alf + language Alekano gah 173 gah + language Aleut ale 174 ale + language Algerian Arabic arq 175 arq + language Algerian Jewish Sign Language ajs 176 ajs + language Algerian Saharan Arabic aao 177 aao + language Algerian Sign Language asp 178 asp + language Algonquin alq 179 alq + language Ali aiy 180 aiy + language Alladian ald 181 ald + language Allar all 182 all + language Alngith aid 183 aid + language Alo Phola ypo 184 ypo + language Alor aol 185 aol + language Aloápam Zapotec zaq 186 zaq + language Alsea aes 187 aes + language Alu Kurumba xua 188 xua + language Alugu aub 189 aub + language Alumu-Tesu aab 190 aab + language Alune alp 191 alp + language Aluo yna 192 yna + language Alur alz 193 alz + language Alutor alr 194 alr + language Alviri-Vidari avd 195 avd + language Alyawarr aly 196 aly + language Ama (Papua New Guinea) amm 197 amm + language Ama (Sudan) nyi 198 nyi + language Amahai amq 199 amq + language Amahuaca amc 200 amc + language Amaimon ali 201 ali + language Amal aad 202 aad + language Amami Koniya Sign Language jks 203 jks + language Amanab amn 204 amn + language Amanayé ama 205 ama + language Amara aie 206 aie + language Amarakaeri amr 207 amr + language Amarasi aaz 208 aaz + language Amatlán Zapotec zpo 209 zpo + language Amba (Solomon Islands) utp 210 utp + language Amba (Uganda) rwm 211 rwm + language Ambai amk 212 amk + language Ambakich aew 213 aew + language Ambala Ayta abc 214 abc + language Ambelau amv 215 amv + language Ambele ael 216 ael + language Amblong alm 217 alm + language Ambo amb 218 amb + language Ambo-Pasco Quechua qva 219 qva + language Ambonese Malay abs 220 abs + language Ambrak aag 221 aag + language Ambul apo 222 apo + language Ambulas abt 223 abt + language Amdang amj 224 amj + language Amdo Tibetan adx 225 adx + language Amele aey 226 aey + language American Sign Language ase 227 ase + language Amganad Ifugao ifa 228 ifa + language Amharic amh 229 amh am + language Ami amy 230 amy + language Amis ami 231 ami + language Amo amo 232 amo + language Amol alx 233 alx + language Amoltepec Mixtec mbz 234 mbz + language Ampanang apg 235 apg + language Ampari Dogon aqd 236 aqd + language Amri Karbi ajz 237 ajz + language Amto amt 238 amt + language Amundava adw 239 adw + language Amurdak amg 240 amg + language Ana Tinga Dogon dti 241 dti + language Anaang anw 242 anw + language Anakalangu akg 243 akg + language Anal anm 244 anm + language Anam pda 245 pda + language Anambé aan 246 aan + language Anamgura imi 247 imi + language Anasi bpo 248 bpo + language Ancient Greek (to 1453) grc 249 grc + language Ancient Hebrew hbo 250 hbo + language Ancient Macedonian xmk 251 xmk + language Ancient North Arabian xna 252 xna + language Ancient Zapotec xzp 253 xzp + language Andaandi dgl 254 dgl + language Andai afd 255 afd + language Andajin ajn 256 ajn + language Andalusian Arabic xaa 257 xaa + language Andaman Creole Hindi hca 258 hca + language Andaqui ana 259 ana + language Andarum aod 260 aod + language Andegerebinha adg 261 adg + language Andh anr 262 anr + language Andi ani 263 ani + language Andio bzb 264 bzb + language Andoa anb 265 anb + language Andoque ano 266 ano + language Andra-Hus anx 267 anx + language Aneityum aty 268 aty + language Anem anz 269 anz + language Aneme Wake aby 270 aby + language Anfillo myo 271 myo + language Angaataha agm 272 agm + language Angaité aqt 273 aqt + language Angal age 274 age + language Angal Enen aoe 275 aoe + language Angal Heneng akh 276 akh + language Angami Naga njm 277 njm + language Angguruk Yali yli 278 yli + language Angika anp 279 anp + language Angkamuthi avm 280 avm + language Anglo-Norman xno 281 xno + language Angloromani rme 282 rme + language Angolar aoa 283 aoa + language Angor agg 284 agg + language Angoram aog 285 aog + language Angosturas Tunebo tnd 286 tnd + language Anguthimri awg 287 awg + language Ani Phowa ypn 288 ypn + language Anii blo 289 blo + language Animere anf 290 anf + language Anindilyakwa aoi 291 aoi + language Aninka aqk 292 aqk + language Anjam boj 293 boj + language Ankave aak 294 aak + language Anmatyerre amx 295 amx + language Anong nun 296 nun + language Anor anj 297 anj + language Anserma ans 298 ans + language Ansus and 299 and + language Antakarinya ant 300 ant + language Antankarana Malagasy xmv 301 xmv + language Antigua and Barbuda Creole English aig 302 aig + language Anu-Hkongso Chin anl 303 anl + language Anuak anu 304 anu + language Anufo cko 305 cko + language Anuki aui 306 aui + language Anus auq 307 auq + language Anuta aud 308 aud + language Anyin any 309 any + language Anyin Morofo mtb 310 mtb + language Ao Naga njo 311 njo + language Aoheng pni 312 pni + language Aore aor 313 aor + language Ap Ma kbx 314 kbx + language Apalachee xap 315 xap + language Apalaí apy 316 apy + language Apali ena 317 ena + language Apasco-Apoala Mixtec mip 318 mip + language Apatani apt 319 apt + language Apiaká api 320 api + language Apinayé apn 321 apn + language Apma app 322 app + language Aproumu Aizi ahp 323 ahp + language Apurinã apu 324 apu + language Aputai apx 325 apx + language Aquitanian xaq 326 xaq + language Arabana ard 327 ard + language Arabela arl 328 arl + language Arabic ara 329 ara ar + language Aragonese arg 330 arg an + language Araki akr 331 akr + language Arakwal rkw 332 rkw + language Aralle-Tabulahan atq 333 atq + language Arammba stk 334 stk + language Aranadan aaf 335 aaf + language Aranama-Tamique xrt 336 xrt + language Arandai jbj 337 jbj + language Araona aro 338 aro + language Arapaho arp 339 arp + language Arapaso arj 340 arj + language Ararandewára xaj 341 xaj + language Arawak arw 342 arw + language Araweté awt 343 awt + language Arawum awm 344 awm + language Arbore arv 345 arv + language Arbëreshë Albanian aae 346 aae + language Archi aqc 347 aqc + language Ardhamāgadhī Prākrit pka 348 pka + language Are mwc 349 mwc + language Areba aea 350 aea + language Arem aem 351 aem + language Arequipa-La Unión Quechua qxu 352 qxu + language Argentine Sign Language aed 353 aed + language Argobba agj 354 agj + language Arguni agf 355 agf + language Arhuaco arh 356 arh + language Arhâ aqr 357 aqr + language Arhö aok 358 aok + language Ari aac 359 aac + language Aribwatsa laz 360 laz + language Aribwaung ylu 361 ylu + language Arifama-Miniafia aai 362 aai + language Arigidi aqg 363 aqg + language Arikapú ark 364 ark + language Arikara ari 365 ari + language Arikem ait 366 ait + language Arin xrn 367 xrn + language Aringa luc 368 luc + language Armazic xrm 369 xrm + language Armenian hye 370 hye hy arm + language Armenian Sign Language aen 371 aen + language Arop-Lokep apr 372 apr + language Arop-Sissano aps 373 aps + language Arosi aia 374 aia + language Arpitan frp 375 frp + language Arritinngithigh rrt 376 rrt + language Arta atz 377 atz + language Aruamu msy 378 msy + language Aruek aur 379 aur + language Aruop lsr 380 lsr + language Arutani atx 381 atx + language Aruá (Amazonas State) aru 382 aru + language Aruá (Rodonia State) arx 383 arx + language Arvanitika Albanian aat 384 aat + language As asz 385 asz + language Asaro'o mtv 386 mtv + language Ashe ahs 387 ahs + language Ashkun ask 388 ask + language Asho Chin csh 389 csh + language Ashtiani atn 390 atn + language Asháninka cni 391 cni + language Ashéninka Pajonal cjo 392 cjo + language Ashéninka Perené prq 393 prq + language Asilulu asl 394 asl + language Askopan eiv 395 eiv + language Asoa asv 396 asv + language Assamese asm 397 asm as + language Assangori sjg 398 sjg + language Assiniboine asb 399 asb + language Assyrian Neo-Aramaic aii 400 aii + language Asturian ast 401 ast + language Asu (Nigeria) aum 402 aum + language Asu (Tanzania) asa 403 asa + language Asue Awyu psa 404 psa + language Asumboa aua 405 aua + language Asunción Mixtepec Zapotec zoo 406 zoo + language Asuri asr 407 asr + language Ata atm 408 atm + language Ata Manobo atd 409 atd + language Atakapa aqp 410 aqp + language Atampaya amz 411 amz + language Atatláhuca Mixtec mib 412 mib + language Atauran adb 413 adb + language Atayal tay 414 tay + language Atemble ate 415 ate + language Athpariya aph 416 aph + language Ati atk 417 atk + language Atikamekw atj 418 atj + language Atohwaim aqm 419 aqm + language Atong (Cameroon) ato 420 ato + language Atong (India) aot 421 aot + language Atorada aox 422 aox + language Atsahuaca atc 423 atc + language Atsam cch 424 cch + language Atsugewi atw 425 atw + language Attapady Kurumba pkr 426 pkr + language Attié ati 427 ati + language Atzingo Matlatzinca ocu 428 ocu + language Au avt 429 avt + language Aulua aul 430 aul + language Aurá aux 431 aux + language Aushi auh 432 auh + language Aushiri avs 433 avs + language Auslan asf 434 asf + language Austral aut 435 aut + language Australian Aborigines Sign Language asw 436 asw + language Austrian Sign Language asq 437 asq + language Auwe smf 438 smf + language Auye auu 439 auu + language Auyokawa auo 440 auo + language Avaric ava 441 ava av + language Avatime avn 442 avn + language Avau avb 443 avb + language Avestan ave 444 ave ae + language Avikam avi 445 avi + language Avokaya avu 446 avu + language Avá-Canoeiro avv 447 avv + language Awa (China) vwa 448 vwa + language Awa (Papua New Guinea) awb 449 awb + language Awa-Cuaiquer kwi 450 kwi + language Awabakal awk 451 awk + language Awad Bing bcu 452 bcu + language Awadhi awa 453 awa + language Awak awo 454 awo + language Awar aya 455 aya + language Awara awx 456 awx + language Awbono awh 457 awh + language Aweer bob 458 bob + language Awera awr 459 awr + language Awetí awe 460 awe + language Awing azo 461 azo + language Awiyaana auy 462 auy + language Awjilah auj 463 auj + language Awngi awn 464 awn + language Awngthim gwm 465 gwm + language Awtuw kmn 466 kmn + language Awu yiu 467 yiu + language Awun aww 468 aww + language Awutu afu 469 afu + language Awyi auw 470 auw + language Axamb ahb 471 ahb + language Axi Yi yix 472 yix + language Ayabadhu ayd 473 ayd + language Ayacucho Quechua quy 474 quy + language Ayautla Mazatec vmy 475 vmy + language Ayere aye 476 aye + language Ayerrerenge axe 477 axe + language Ayi (Papua New Guinea) ayq 478 ayq + language Ayiwo nfl 479 nfl + language Ayizi yyz 480 yyz + language Ayizo Gbe ayb 481 ayb + language Aymara aym 482 aym ay + language Ayoquesco Zapotec zaf 483 zaf + language Ayoreo ayo 484 ayo + language Ayu ayu 485 ayu + language Ayutla Mixtec miy 486 miy + language Azerbaijani aze 487 aze az + language Azha aza 488 aza + language Azhe yiz 489 yiz + language Azoyú Me'phaa tpc 490 tpc + language Baan bvj 491 bvj + language Baangi bqx 492 bqx + language Baatonum bba 493 bba + language Baba bbw 494 bbw + language Baba Malay mbf 495 mbf + language Babango bbm 496 bbm + language Babanki bbk 497 bbk + language Babatana baa 498 baa + language Babine bcr 499 bcr + language Babuza bzg 500 bzg + language Bacama bcy 501 bcy + language Bacanese Malay btj 502 btj + language Bactrian xbc 503 xbc + language Bada (Indonesia) bhz 504 bhz + language Bada (Nigeria) bau 505 bau + language Badaga bfq 506 bfq + language Bade bde 507 bde + language Badeshi bdz 508 bdz + language Badimaya bia 509 bia + language Badjiri jbi 510 jbi + language Badui bac 511 bac + language Badyara pbp 512 pbp + language Baeggu bvd 513 bvd + language Baelelea bvc 514 bvc + language Baetora btr 515 btr + language Bafanji bfj 516 bfj + language Bafaw-Balong bwt 517 bwt + language Bafia ksf 518 ksf + language Bafut bfd 519 bfd + language Baga Kaloum bqf 520 bqf + language Baga Koga bgo 521 bgo + language Baga Manduri bmd 522 bmd + language Baga Pokur bcg 523 bcg + language Baga Sitemu bsp 524 bsp + language Baga Sobané bsv 525 bsv + language Bagheli bfy 526 bfy + language Bagirmi bmi 527 bmi + language Bagirmi Fulfulde fui 528 fui + language Bago-Kusuntu bqg 529 bqg + language Bagri bgq 530 bgq + language Bagupi bpi 531 bpi + language Bagusa bqb 532 bqb + language Bagvalal kva 533 kva + language Baha Buyang yha 534 yha + language Baham bdw 535 bdw + language Bahamas Creole English bah 536 bah + language Baharna Arabic abv 537 abv + language Bahau bhv 538 bhv + language Bahinemo bjh 539 bjh + language Bahing bhj 540 bhj + language Bahnar bdq 541 bdq + language Bahonsuai bsu 542 bsu + language Bai (South Sudan) bdj 543 bdj + language Baibai bbf 544 bbf + language Baikeno bkx 545 bkx + language Baima bqh 546 bqh + language Baimak bmx 547 bmx + language Bainouk-Gunyaamolo bcz 548 bcz + language Bainouk-Gunyuño bab 549 bab + language Bainouk-Samik bcb 550 bcb + language Baiso bsw 551 bsw + language Baissa Fali fah 552 fah + language Bajan bjs 553 bjs + language Bajelani bjm 554 bjm + language Bajjika vjk 555 vjk + language Baka (Cameroon) bkc 556 bkc + language Baka (South Sudan) bdh 557 bdh + language Bakairí bkq 558 bkq + language Bakaka bqz 559 bqz + language Bakhtiari bqi 560 bqi + language Baki bki 561 bki + language Bakoko bkh 562 bkh + language Bakole kme 563 kme + language Bakpinka bbs 564 bbs + language Bakumpai bkr 565 bkr + language Bakwé bjw 566 bjw + language Balaesang bls 567 bls + language Balaibalan zba 568 zba + language Balangao blw 569 blw + language Balangingi sse 570 sse + language Balanta-Ganja bjt 571 bjt + language Balanta-Kentohe ble 572 ble + language Balantak blz 573 blz + language Baldemu bdn 574 bdn + language Bali (Democratic Republic of Congo) bcp 575 bcp + language Bali (Nigeria) bcn 576 bcn + language Balinese ban 577 ban + language Balinese Malay mhp 578 mhp + language Balkan Gagauz Turkish bgx 579 bgx + language Balkan Romani rmn 580 rmn + language Balo bqo 581 bqo + language Baloi biz 582 biz + language Balti bft 583 bft + language Baltic Romani rml 584 rml + language Baluan-Pam blq 585 blq + language Baluchi bal 586 bal + language Bamako Sign Language bog 587 bog + language Bamali bbq 588 bbq + language Bambalang bmo 589 bmo + language Bambam ptu 590 ptu + language Bambara bam 591 bam bm + language Bambassi myf 592 myf + language Bambili-Bambui baw 593 baw + language Bamenyam bce 594 bce + language Bamu bcf 595 bcf + language Bamukumbit bqt 596 bqt + language Bamun bax 597 bax + language Bamunka bvm 598 bvm + language Bamwe bmg 599 bmg + language Ban Khor Sign Language bfk 600 bfk + language Bana bcw 601 bcw + language Banao Itneg bjx 602 bjx + language Banaro byz 603 byz + language Banda (Indonesia) bnd 604 bnd + language Banda Malay bpq 605 bpq + language Banda-Bambari liy 606 liy + language Banda-Banda bpd 607 bpd + language Banda-Mbrès bqk 608 bqk + language Banda-Ndélé bfl 609 bfl + language Banda-Yangere yaj 610 yaj + language Bandi bza 611 bza + language Bandial bqj 612 bqj + language Bandjalang bdy 613 bdy + language Bangala bxg 614 bxg + language Bangandu bgf 615 bgf + language Bangba bbe 616 bbe + language Banggai bgz 617 bgz + language Banggarla bjb 618 bjb + language Bangi bni 619 bni + language Bangime dba 620 dba + language Bangka mfb 621 mfb + language Bangolan bgj 622 bgj + language Bangubangu bnx 623 bnx + language Bangwinji bsj 624 bsj + language Baniva bvv 625 bvv + language Baniwa bwi 626 bwi + language Banjar bjn 627 bjn + language Bankagooma bxw 628 bxw + language Bankal jjr 629 jjr + language Bankan Tey Dogon dbw 630 dbw + language Bankon abb 631 abb + language Bannoni bcm 632 bcm + language Bantawa bap 633 bap + language Bantayanon bfx 634 bfx + language Bantik bnq 635 bnq + language Bantoanon bno 636 bno + language Banyjima pnw 637 pnw + language Baoulé bci 638 bci + language Bara Malagasy bhr 639 bhr + language Baraamu brd 640 brd + language Barababaraba rbp 641 rbp + language Barai bbb 642 bbb + language Barakai baj 643 baj + language Baram Kayan kys 644 kys + language Barama bbg 645 bbg + language Barambu brm 646 brm + language Baramu bmz 647 bmz + language Barapasi brp 648 brp + language Baras brs 649 brs + language Barasana-Eduria bsn 650 bsn + language Barbaram vmb 651 vmb + language Barbareño boi 652 boi + language Barclayville Grebo gry 653 gry + language Bardi bcj 654 bcj + language Barein bva 655 bva + language Bargam mlp 656 mlp + language Bari bfa 657 bfa + language Bariai bch 658 bch + language Bariji bjc 659 bjc + language Barikanchi bxo 660 bxo + language Barikewa jbk 661 jbk + language Barok bjk 662 bjk + language Barombi bbi 663 bbi + language Barro Negro Tunebo tbn 664 tbn + language Barrow Point bpt 665 bpt + language Baruga bjz 666 bjz + language Baruya byr 667 byr + language Barwe bwg 668 bwg + language Barzani Jewish Neo-Aramaic bjf 669 bjf + language Baré bae 670 bae + language Barí mot 671 mot + language Basa (Cameroon) bas 672 bas + language Basa (Nigeria) bzw 673 bzw + language Basa-Gumna bsl 674 bsl + language Basa-Gurmana buj 675 buj + language Basap bdb 676 bdb + language Basay byq 677 byq + language Bashkardi bsg 678 bsg + language Bashkir bak 679 bak ba + language Basketo bst 680 bst + language Basque eus 681 eus eu baq + language Bassa bsq 682 bsq + language Bassa-Kontagora bsr 683 bsr + language Bassari bsc 684 bsc + language Bassossi bsi 685 bsi + language Bata bta 686 bta + language Batad Ifugao ifb 687 ifb + language Batak bya 688 bya + language Batak Alas-Kluet btz 689 btz + language Batak Angkola akb 690 akb + language Batak Dairi btd 691 btd + language Batak Karo btx 692 btx + language Batak Mandailing btm 693 btm + language Batak Simalungun bts 694 bts + language Batak Toba bbc 695 bbc + language Batanga bnm 696 bnm + language Batek btq 697 btq + language Bateri btv 698 btv + language Bathari bhm 699 bhm + language Bati (Cameroon) btc 700 btc + language Bati (Indonesia) bvt 701 bvt + language Batjala xby 702 xby + language Bats bbl 703 bbl + language Batu btu 704 btu + language Batui zbt 705 zbt + language Batuley bay 706 bay + language Bau bbd 707 bbd + language Bau Bidayuh sne 708 sne + language Bauchi bsf 709 bsf + language Bauni bpe 710 bpe + language Baure brg 711 brg + language Bauria bge 712 bge + language Bauwaki bwk 713 bwk + language Bauzi bvz 714 bvz + language Bavarian bar 715 bar + language Bawm Chin bgr 716 bgr + language Bay Miwok mkq 717 mkq + language Bayali bjy 718 bjy + language Baybayanon bvy 719 bvy + language Baygo byg 720 byg + language Bayono byl 721 byl + language Bayot bda 722 bda + language Bayungu bxj 723 bxj + language Bazigar bfr 724 bfr + language Beami beo 725 beo + language Beaver bea 726 bea + language Beba bfp 727 bfp + language Bebele beb 728 beb + language Bebeli bek 729 bek + language Bebil bxp 730 bxp + language Bedjond bjv 731 bjv + language Bedoanas bed 732 bed + language Beeke bkf 733 bkf + language Beele bxq 734 bxq + language Beembe beq 735 beq + language Beezen bnz 736 bnz + language Befang bby 737 bby + language Beginci ebc 738 ebc + language Beja bej 739 bej + language Bekati' bei 740 bei + language Bekwarra bkv 741 bkv + language Bekwel bkw 742 bkw + language Belait beg 743 beg + language Belanda Bor bxb 744 bxb + language Belanda Viri bvi 745 bvi + language Belarusian bel 746 bel be + language Belhariya byw 747 byw + language Beli (Papua New Guinea) bey 748 bey + language Beli (South Sudan) blm 749 blm + language Belize Kriol English bzj 750 bzj + language Bella Coola blc 751 blc + language Bellari brw 752 brw + language Belning glb 753 glb + language Bemba (Zambia) bem 754 bem + language Bembe bmb 755 bmb + language Ben Tey Dogon dbt 756 dbt + language Bena (Nigeria) yun 757 yun + language Bena (Tanzania) bez 758 bez + language Benabena bef 759 bef + language Benamanga egm 760 egm + language Bench bcq 761 bcq + language Bende bdp 762 bdp + language Bendi bct 763 bct + language Beng nhb 764 nhb + language Benga bng 765 bng + language Bengali, Bangla ben 766 ben Bengali Bangla bn + language Benggoi bgy 767 bgy + language Bengkala Sign Language bqy 768 bqy + language Bentong bnu 769 bnu + language Benyadu' byd 770 byd + language Beothuk bue 771 bue + language Bepour bie 772 bie + language Berakou bxv 773 bxv + language Berau Malay bve 774 bve + language Berbice Creole Dutch brc 775 brc + language Berik bkl 776 bkl + language Berinomo bit 777 bit + language Berom bom 778 bom + language Berta wti 779 wti + language Berti byt 780 byt + language Besisi mhe 781 mhe + language Besme bes 782 bes + language Besoa bep 783 bep + language Betaf bfe 784 bfe + language Betawi bew 785 bew + language Bete byf 786 byf + language Bete-Bendi btt 787 btt + language Beti (Côte d'Ivoire) eot 788 eot + language Betta Kurumba xub 789 xub + language Bezhta kap 790 kap + language Bhadrawahi bhd 791 bhd + language Bhalay bhx 792 bhx + language Bharia bha 793 bha + language Bhatri bgw 794 bgw + language Bhattiyali bht 795 bht + language Bhaya bhe 796 bhe + language Bhele bhy 797 bhy + language Bhilali bhi 798 bhi + language Bhili bhb 799 bhb + language Bhojpuri bho 800 bho + language Bhoti Kinnauri nes 801 nes + language Bhujel byh 802 byh + language Bhunjia bhu 803 bhu + language Biafada bif 804 bif + language Biage bdf 805 bdf + language Biak bhw 806 bhw + language Biali beh 807 beh + language Bian Marind bpv 808 bpv + language Biangai big 809 big + language Biao byk 810 byk + language Biao Mon bmt 811 bmt + language Biao-Jiao Mien bje 812 bje + language Biatah Bidayuh bth 813 bth + language Bibbulman xbp 814 xbp + language Bidhawal ihw 815 ihw + language Bidiyo bid 816 bid + language Bidjara bym 817 bym + language Bidyogo bjg 818 bjg + language Biem bmc 819 bmc + language Bierebo bnk 820 bnk + language Bieria brj 821 brj + language Biete biu 822 biu + language Big Nambas nmb 823 nmb + language Biga bhc 824 bhc + language Bigambal xbe 825 xbe + language Bih ibh 826 ibh + language Bihari bih 827 bih bh + language Bijim jbm 828 jbm + language Bijori bix 829 bix + language Bikol bik 830 bik + language Bikya byb 831 byb + language Bila bip 832 bip + language Bilakura bql 833 bql + language Bilaspuri kfs 834 kfs + language Bilba bpz 835 bpz + language Bilbil brz 836 brz + language Bile bil 837 bil + language Bilin byn 838 byn + language Bilma Kanuri bms 839 bms + language Biloxi bll 840 bll + language Bilua blb 841 blb + language Bilur bxf 842 bxf + language Bima bhp 843 bhp + language Bimin bhl 844 bhl + language Bimoba bim 845 bim + language Bina (Nigeria) byj 846 byj + language Bina (Papua New Guinea) bmn 847 bmn + language Binahari bxz 848 bxz + language Binandere bhg 849 bhg + language Bindal xbd 850 xbd + language Bine bon 851 bon + language Bini bin 852 bin + language Binji bpj 853 bpj + language Binongan Itneg itb 854 itb + language Bintauna bne 855 bne + language Bintulu bny 856 bny + language Binukid bkd 857 bkd + language Binumarien bjr 858 bjr + language Bipi biq 859 biq + language Bira brf 860 brf + language Birale bxe 861 bxe + language Birao brr 862 brr + language Birgit btf 863 btf + language Birhor biy 864 biy + language Biri bzr 865 bzr + language Biritai bqq 866 bqq + language Birked brk 867 brk + language Birri bvq 868 bvq + language Birrpayi xbj 869 xbj + language Birwa brl 870 brl + language Biseni ije 871 ije + language Bishnupriya bpy 872 bpy + language Bishuo bwh 873 bwh + language Bisis bnw 874 bnw + language Bislama bis 875 bis bi + language Bisorio bir 876 bir + language Bissa bib 877 bib + language Bisu bzi 878 bzi + language Bit bgk 879 bgk + language Bitare brt 880 brt + language Bitur mcc 881 mcc + language Biwat bwm 882 bwm + language Biyo byo 883 byo + language Biyom bpm 884 bpm + language Blablanga blp 885 blp + language Blafe bfh 886 bfh + language Blagar beu 887 beu + language Blang blr 888 blr + language Blissymbols zbl 889 zbl + language Bo (Laos) bgl 890 bgl + language Bo (Papua New Guinea) bpw 891 bpw + language Bo-Rukul mae 892 mae + language Bo-Ung mux 893 mux + language Boano (Maluku) bzn 894 bzn + language Boano (Sulawesi) bzl 895 bzl + language Bobongko bgb 896 bgb + language Bobot bty 897 bty + language Bodo (Central African Republic) boy 898 boy + language Bodo (India) brx 899 brx + language Bodo Gadaba gbj 900 gbj + language Bodo Parja bdv 901 bdv + language Bofi bff 902 bff + language Boga bvw 903 bvw + language Bogaya boq 904 boq + language Boghom bux 905 bux + language Boguru bqu 906 bqu + language Bohtan Neo-Aramaic bhn 907 bhn + language Boikin bzf 908 bzf + language Bokha ybk 909 ybk + language Boko (Benin) bqc 910 bqc + language Boko (Democratic Republic of Congo) bkp 911 bkp + language Bokobaru bus 912 bus + language Bokoto bdt 913 bdt + language Bokyi bky 914 bky + language Bola bnp 915 bnp + language Bolango bld 916 bld + language Bole bol 917 bol + language Bolgarian xbo 918 xbo + language Bolgo bvo 919 bvo + language Bolia bli 920 bli + language Bolinao smk 921 smk + language Bolivian Sign Language bvl 922 bvl + language Boloki bkt 923 bkt + language Bolon bof 924 bof + language Bolondo bzm 925 bzm + language Bolongan blj 926 blj + language Bolyu ply 927 ply + language Bom-Kim bmf 928 bmf + language Boma boh 929 boh + language Bomboli bml 930 bml + language Bomboma bws 931 bws + language Bomitaba zmx 932 zmx + language Bomu bmq 933 bmq + language Bomwali bmw 934 bmw + language Bon Gula glc 935 glc + language Bonan peh 936 peh + language Bondei bou 937 bou + language Bondo bfw 938 bfw + language Bondoukou Kulango kzc 939 kzc + language Bondum Dom Dogon dbu 940 dbu + language Bonerate bna 941 bna + language Bonerif bnv 942 bnv + language Bonggi bdg 943 bdg + language Bonggo bpg 944 bpg + language Bongili bui 945 bui + language Bongo bot 946 bot + language Bongu bpu 947 bpu + language Bonjo bok 948 bok + language Bonkeng bvg 949 bvg + language Bonkiman bop 950 bop + language Bontok bnc 951 bnc + language Bookan bnb 952 bnb + language Boon bnl 953 bnl + language Boor bvf 954 bvf + language Bora boa 955 boa + language Borana-Arsi-Guji Oromo gax 956 gax + language Border Kuna kvn 957 kvn + language Borei gai 958 gai + language Borgu Fulfulde fue 959 fue + language Boro (Ethiopia) bwo 960 bwo + language Boro (Ghana) xxb 961 xxb + language Borong ksr 962 ksr + language Boruca brn 963 brn + language Borôro bor 964 bor + language Boselewa bwf 965 bwf + language Bosngun bqs 966 bqs + language Bosnian bos 967 bos bs + language Bote-Majhi bmj 968 bmj + language Botlikh bph 969 bph + language Botolan Sambal sbl 970 sbl + language Bouna Kulango nku 971 nku + language Bouni suo 972 suo + language Bouyei pcc 973 pcc + language Bozaba bzo 974 bzo + language Bragat aof 975 aof + language Brahui brh 976 brh + language Braj bra 977 bra + language Brao brb 978 brb + language Brazilian Sign Language bzs 979 bzs + language Brem buq 980 buq + language Breri brq 981 brq + language Breton bre 982 bre br + language Bribri bzd 983 bzd + language Bribri Sign Language rib 984 rib + language Brithenig bzt 985 bzt + language British Sign Language bfi 986 bfi + language Brokkat bro 987 bro + language Brokpake sgt 988 sgt + language Brokskat bkk 989 bkk + language Brooke's Point Palawano plw 990 plw + language Broome Pearling Lugger Pidgin bpl 991 bpl + language Brunca Sign Language rnb 992 rnb + language Brunei kxd 993 kxd + language Brunei Bisaya bsb 994 bsb + language Bruny Island Tasmanian xpz 995 xpz + language Bu (Bauchi State) zbu 996 zbu + language Bu (Kaduna State) jid 997 jid + language Bu-Nao Bunu bwx 998 bwx + language Bua bub 999 bub + language Bualkhaw Chin cbl 1000 cbl + language Buamu box 1001 box + language Bube bvb 1002 bvb + language Bubi buw 1003 buw + language Bubia bbx 1004 bbx + language Budeh Stieng stt 1005 stt + language Budibud btp 1006 btp + language Budong-Budong bdx 1007 bdx + language Budu buu 1008 buu + language Budukh bdk 1009 bdk + language Buduma bdm 1010 bdm + language Budza bja 1011 bja + language Bugan bbh 1012 bbh + language Bugawac buk 1013 buk + language Bughotu bgt 1014 bgt + language Buginese bug 1015 bug + language Buglere sab 1016 sab + language Bugun bgg 1017 bgg + language Buhi'non Bikol ubl 1018 ubl + language Buhid bku 1019 bku + language Buhutu bxh 1020 bxh + language Bukar-Sadung Bidayuh sdo 1021 sdo + language Bukat bvk 1022 bvk + language Bukharic bhh 1023 bhh + language Bukit Malay bvu 1024 bvu + language Bukitan bkn 1025 bkn + language Bukiyip ape 1026 ape + language Buksa tkb 1027 tkb + language Bukusu bxk 1028 bxk + language Bukwen buz 1029 buz + language Bulgarian bul 1030 bul bg + language Bulgarian Sign Language bqn 1031 bqn + language Bulgebi bmp 1032 bmp + language Buli uly 1033 uly + language Buli (Ghana) bwu 1034 bwu + language Buli (Indonesia) bzq 1035 bzq + language Bullom So buy 1036 buy + language Bulo Stieng sti 1037 sti + language Bulu (Cameroon) bum 1038 bum + language Bulu (Papua New Guinea) bjl 1039 bjl + language Bum bmv 1040 bmv + language Bumaji byp 1041 byp + language Bumang bvp 1042 bvp + language Bumbita Arapesh aon 1043 aon + language Bumthangkha kjz 1044 kjz + language Bun buv 1045 buv + language Buna bvn 1046 bvn + language Bunak bfn 1047 bfn + language Bunama bdd 1048 bdd + language Bundeli bns 1049 bns + language Bung bqd 1050 bqd + language Bungain but 1051 but + language Bunganditj xbg 1052 xbg + language Bungku bkz 1053 bkz + language Bungu wun 1054 wun + language Bunoge Dogon dgb 1055 dgb + language Bunuba bck 1056 bck + language Bunun bnn 1057 bnn + language Buol blf 1058 blf + language Bura-Pabir bwr 1059 bwr + language Burak bys 1060 bys + language Buraka bkg 1061 bkg + language Burarra bvr 1062 bvr + language Burate bti 1063 bti + language Burduna bxn 1064 bxn + language Bure bvh 1065 bvh + language Buriat bua 1066 bua + language Burji bji 1067 bji + language Burmbar vrt 1068 vrt + language Burmese mya 1069 mya my bur + language Burmeso bzu 1070 bzu + language Buru (Indonesia) mhs 1071 mhs + language Buru (Nigeria) bqw 1072 bqw + language Burui bry 1073 bry + language Burumakok aip 1074 aip + language Burun bdi 1075 bdi + language Burundian Sign Language lsb 1076 lsb + language Burunge bds 1077 bds + language Burushaski bsk 1078 bsk + language Burusu bqr 1079 bqr + language Buruwai asi 1080 asi + language Busa bqp 1081 bqp + language Busam bxs 1082 bxs + language Busami bsm 1083 bsm + language Busang Kayan bfg 1084 bfg + language Bushi buc 1085 buc + language Bushoong buf 1086 buf + language Buso bso 1087 bso + language Busoa bup 1088 bup + language Bussa dox 1089 dox + language Busuu bju 1090 bju + language Butbut Kalinga kyb 1091 kyb + language Butmas-Tur bnr 1092 bnr + language Butuanon btw 1093 btw + language Buwal bhs 1094 bhs + language Buyu byi 1095 byi + language Buyuan Jinuo jiy 1096 jiy + language Bwa bww 1097 bww + language Bwaidoka bwd 1098 bwd + language Bwanabwana tte 1099 tte + language Bwatoo bwa 1100 bwa + language Bwe Karen bwe 1101 bwe + language Bwela bwl 1102 bwl + language Bwile bwc 1103 bwc + language Bwisi bwz 1104 bwz + language Byangsi bee 1105 bee + language Byep mkk 1106 mkk + language Bädi Kanum khd 1107 khd + language C'Lela dri 1108 dri + language Caac msq 1109 msq + language Cabiyarí cbb 1110 cbb + language Cabécar cjp 1111 cjp + language Cacaloxtepec Mixtec miu 1112 miu + language Cacaopera ccr 1113 ccr + language Cacgia Roglai roc 1114 roc + language Cacua cbv 1115 cbv + language Caddo cad 1116 cad + language Cafundo Creole ccd 1117 ccd + language Cahuarano cah 1118 cah + language Cahuilla chl 1119 chl + language Cajamarca Quechua qvc 1120 qvc + language Cajatambo North Lima Quechua qvl 1121 qvl + language Cajonos Zapotec zad 1122 zad + language Cajun French frc 1123 frc + language Caka ckx 1124 ckx + language Cakchiquel-Quiché Mixed Language ckz 1125 ckz + language Cakfem-Mushere cky 1126 cky + language Calamian Tagbanwa tbk 1127 tbk + language Calderón Highland Quichua qud 1128 qud + language Callawalla caw 1129 caw + language Caluyanun clu 1130 clu + language Caló rmq 1131 rmq + language Cambodian Sign Language csx 1132 csx + language Cameroon Mambila mcu 1133 mcu + language Cameroon Pidgin wes 1134 wes + language Camling rab 1135 rab + language Campalagian cml 1136 cml + language Campidanese Sardinian sro 1137 sro + language Camsá kbh 1138 kbh + language Camtho cmt 1139 cmt + language Camunic xcc 1140 xcc + language Candoshi-Shapra cbu 1141 cbu + language Canela ram 1142 ram + language Canichana caz 1143 caz + language Cao Lan mlc 1144 mlc + language Cao Miao cov 1145 cov + language Capanahua kaq 1146 kaq + language Capiznon cps 1147 cps + language Cappadocian Greek cpg 1148 cpg + language Caquinte cot 1149 cot + language Car Nicobarese caq 1150 caq + language Cara cfd 1151 cfd + language Carabayo cby 1152 cby + language Caramanta crf 1153 crf + language Carapana cbc 1154 cbc + language Carian xcr 1155 xcr + language Caribbean Hindustani hns 1156 hns + language Caribbean Javanese jvn 1157 jvn + language Carijona cbd 1158 cbd + language Carolina Algonquian crr 1159 crr + language Carolinian cal 1160 cal + language Carpathian Romani rmc 1161 rmc + language Carrier crx 1162 crx + language Cashibo-Cacataibo cbr 1163 cbr + language Cashinahua cbs 1164 cbs + language Casiguran Dumagat Agta dgc 1165 dgc + language Castilian, Spanish spa 1166 spa Castilian Spanish es + language Casuarina Coast Asmat asc 1167 asc + language Catalan Sign Language csc 1168 csc + language Catalan, Valencian cat 1169 cat Catalan Valencian ca + language Catawba chc 1170 chc + language Cavineña cav 1171 cav + language Cayubaba cyb 1172 cyb + language Cayuga cay 1173 cay + language Cayuse xcy 1174 xcy + language Cañar Highland Quichua qxr 1175 qxr + language Ca̱hungwa̱rya̱ nat 1176 nat + language Cebaara Senoufo sef 1177 sef + language Cebuano ceb 1178 ceb + language Celtiberian xce 1179 xce + language Cemuhî cam 1180 cam + language Cen cen 1181 cen + language Central Asmat cns 1182 cns + language Central Atlas Tamazight tzm 1183 tzm + language Central Awyu awu 1184 awu + language Central Aymara ayr 1185 ayr + language Central Bai bca 1186 bca + language Central Berawan zbc 1187 zbc + language Central Bikol bcl 1188 bcl + language Central Bontok lbk 1189 lbk + language Central Cagayan Agta agt 1190 agt + language Central Grebo grv 1191 grv + language Central Hongshuihe Zhuang zch 1192 zch + language Central Huasteca Nahuatl nch 1193 nch + language Central Huishui Hmong hmc 1194 hmc + language Central Kanuri knc 1195 knc + language Central Kurdish ckb 1196 ckb + language Central Maewo mwo 1197 mwo + language Central Malay pse 1198 pse + language Central Masela mxz 1199 mxz + language Central Mashan Hmong hmm 1200 hmm + language Central Mazahua maz 1201 maz + language Central Melanau mel 1202 mel + language Central Mnong cmo 1203 cmo + language Central Nahuatl nhn 1204 nhn + language Central Nicobarese ncb 1205 ncb + language Central Ojibwa ojc 1206 ojc + language Central Okinawan ryu 1207 ryu + language Central Palawano plc 1208 plc + language Central Pame pbs 1209 pbs + language Central Pashto pst 1210 pst + language Central Pomo poo 1211 poo + language Central Puebla Nahuatl ncx 1212 ncx + language Central Sama sml 1213 sml + language Central Siberian Yupik ess 1214 ess + language Central Sierra Miwok csm 1215 csm + language Central Subanen syb 1216 syb + language Central Tagbanwa tgt 1217 tgt + language Central Tarahumara tar 1218 tar + language Central Tunebo tuf 1219 tuf + language Central Yupik esu 1220 esu + language Central-Eastern Niger Fulfulde fuq 1221 fuq + language Centúúm cet 1222 cet + language Cerma cme 1223 cme + language Cha'ari cxh 1224 cxh + language Chabu sbf 1225 sbf + language Chachapoyas Quechua quk 1226 quk + language Chachi cbi 1227 cbi + language Chadian Arabic shu 1228 shu + language Chadian Sign Language cds 1229 cds + language Chadong cdy 1230 cdy + language Chagatai chg 1231 chg + language Chaima ciy 1232 ciy + language Chak ckh 1233 ckh + language Chakali cli 1234 cli + language Chakavian ckm 1235 ckm + language Chakma ccp 1236 ccp + language Chala cll 1237 cll + language Chaldean Neo-Aramaic cld 1238 cld + language Chalikha tgf 1239 tgf + language Chamacoco ceg 1240 ceg + language Chamalal cji 1241 cji + language Chambeali cdh 1242 cdh + language Chambri can 1243 can + language Chamicuro ccc 1244 ccc + language Chamorro cha 1245 cha ch + language Chang Naga nbc 1246 nbc + language Changriwa cga 1247 cga + language Changthang cna 1248 cna + language Chantyal chx 1249 chx + language Chané caj 1250 caj + language Chara cra 1251 cra + language Chaudangsi cdn 1252 cdn + language Chaura crv 1253 crv + language Chavacano cbk 1254 cbk + language Chayahuita cbt 1255 cbt + language Chayuco Mixtec mih 1256 mih + language Chazumba Mixtec xtb 1257 xtb + language Che ruk 1258 ruk + language Chechen che 1259 che ce + language Cheke Holo mrn 1260 mrn + language Chemakum xch 1261 xch + language Chenapian cjn 1262 cjn + language Chenchu cde 1263 cde + language Chenoua cnu 1264 cnu + language Chepang cdm 1265 cdm + language Chepya ycp 1266 ycp + language Cherepon cpn 1267 cpn + language Cherokee chr 1268 chr + language Chesu ych 1269 ych + language Chetco ctc 1270 ctc + language Chewong cwg 1271 cwg + language Cheyenne chy 1272 chy + language Chhattisgarhi hne 1273 hne + language Chhintange ctn 1274 ctn + language Chhulung cur 1275 cur + language Chiangmai Sign Language csd 1276 csd + language Chiapanec cip 1277 cip + language Chibcha chb 1278 chb + language Chicahuaxtla Triqui trs 1279 trs + language Chichewa, Chewa, Nyanja nya 1280 nya Chichewa Chewa ny Nyanja + language Chichicapan Zapotec zpv 1281 zpv + language Chichimeca-Jonaz pei 1282 pei + language Chickasaw cic 1283 cic + language Chicomuceltec cob 1284 cob + language Chiga cgg 1285 cgg + language Chigmecatitlán Mixtec mii 1286 mii + language Chilcotin clc 1287 clc + language Chilean Sign Language csg 1288 csg + language Chilisso clh 1289 clh + language Chiltepec Chinantec csa 1290 csa + language Chimalapa Zoque zoh 1291 zoh + language Chimariko cid 1292 cid + language Chimborazo Highland Quichua qug 1293 qug + language Chimila cbg 1294 cbg + language China Buriat bxu 1295 bxu + language Chinali cih 1296 cih + language Chinbon Chin cnb 1297 cnb + language Chincha Quechua qxc 1298 qxc + language Chinese zho 1299 zho chi zh + language Chinese Pidgin English cpi 1300 cpi + language Chinese Sign Language csl 1301 csl + language Chinook chh 1302 chh + language Chinook jargon chn 1303 chn + language Chipaya cap 1304 cap + language Chipewyan chp 1305 chp + language Chippewa ciw 1306 ciw + language Chiquihuitlán Mazatec maq 1307 maq + language Chiquitano cax 1308 cax + language Chiquián Ancash Quechua qxa 1309 qxa + language Chiripá nhd 1310 nhd + language Chiru cdf 1311 cdf + language Chitimacha ctm 1312 ctm + language Chitkuli Kinnauri cik 1313 cik + language Chittagonian ctg 1314 ctg + language Chitwania Tharu the 1315 the + language Choapan Zapotec zpc 1316 zpc + language Chocangacakha cgk 1317 cgk + language Chochotec coz 1318 coz + language Choctaw cho 1319 cho + language Chodri cdi 1320 cdi + language Chokri Naga nri 1321 nri + language Chokwe cjk 1322 cjk + language Chol ctu 1323 ctu + language Cholón cht 1324 cht + language Chong cog 1325 cog + language Choni cda 1326 cda + language Chonyi-Dzihana-Kauma coh 1327 coh + language Chopi cce 1328 cce + language Chorasmian xco 1329 xco + language Chortí caa 1330 caa + language Chothe Naga nct 1331 nct + language Chrau crw 1332 crw + language Chru cje 1333 cje + language Chuanqiandian Cluster Miao cqd 1334 cqd + language Chuave cjv 1335 cjv + language Chug cvg 1336 cvg + language Chuj cac 1337 cac + language Chuka cuh 1338 cuh + language Chukot ckt 1339 ckt + language Chukwa cuw 1340 cuw + language Chulym clw 1341 clw + language Chumburung ncu 1342 ncu + language Chung cnq 1343 cnq + language Churahi cdj 1344 cdj + language Church Slavonic, Church Slavic, Old Church Slavonic, Old Bulgarian chu 1345 chu cu Church Slavonic Church Slavic Old Church Slavonic Old Bulgarian + language Chut scb 1346 scb + language Chuukese chk 1347 chk + language Chuvantsy xcv 1348 xcv + language Chuvash chv 1349 chv cv + language Chuwabu chw 1350 chw + language Chácobo cao 1351 cao + language Ci Gbe cib 1352 cib + language Cia-Cia cia 1353 cia + language Cibak ckl 1354 ckl + language Cicipu awc 1355 awc + language Cimbrian cim 1356 cim + language Cinda-Regi-Tiyal cdr 1357 cdr + language Cineni cie 1358 cie + language Cinta Larga cin 1359 cin + language Cisalpine Gaulish xcg 1360 xcg + language Cishingini asg 1361 asg + language Citak txt 1362 txt + language Ciwogai tgd 1363 tgd + language Clallam clm 1364 clm + language Classical Armenian xcl 1365 xcl + language Classical Mandaic myz 1366 myz + language Classical Mongolian cmg 1367 cmg + language Classical Nahuatl nci 1368 nci + language Classical Newari nwc 1369 nwc + language Classical Quechua qwc 1370 qwc + language Classical Sanskrit cls 1371 cls + language Classical Syriac syc 1372 syc + language Classical Tibetan xct 1373 xct + language Coahuilteco xcw 1374 xcw + language Coast Miwok csi 1375 csi + language Coastal Konjo kjc 1376 kjc + language Coatecas Altas Zapotec zca 1377 zca + language Coatepec Nahuatl naz 1378 naz + language Coatlán Mixe mco 1379 mco + language Coatlán Zapotec zps 1380 zps + language Coatzospan Mixtec miz 1381 miz + language Cocama-Cocamilla cod 1382 cod + language Cochimi coj 1383 coj + language Cocopa coc 1384 coc + language Cocos Islands Malay coa 1385 coa + language Coeur d'Alene crd 1386 crd + language Cofán con 1387 con + language Cogui kog 1388 kog + language Col liw 1389 liw + language Colombian Sign Language csn 1390 csn + language Colonia Tovar German gct 1391 gct + language Colorado cof 1392 cof + language Columbia-Wenatchi col 1393 col + language Comaltepec Chinantec cco 1394 cco + language Comanche com 1395 com + language Comecrudo xcm 1396 xcm + language Como Karim cfg 1397 cfg + language Comox coo 1398 coo + language Con cno 1399 cno + language Congo Swahili swc 1400 swc + language Coos csz 1401 csz + language Copainalá Zoque zoc 1402 zoc + language Copala Triqui trc 1403 trc + language Coptic cop 1404 cop + language Coquille coq 1405 coq + language Cori cry 1406 cry + language Cornish cor 1407 cor kw + language Corongo Ancash Quechua qwa 1408 qwa + language Corsican cos 1409 cos co + language Costa Rican Sign Language csr 1410 csr + language Cotabato Manobo mta 1411 mta + language Cotoname xcn 1412 xcn + language Cowlitz cow 1413 cow + language Coyotepec Popoloca pbf 1414 pbf + language Coyutla Totonac toc 1415 toc + language Cree cre 1416 cre cr + language Creek mus 1417 mus + language Crimean Tatar crh 1418 crh + language Croatia Sign Language csq 1419 csq + language Cross River Mbembe mfn 1420 mfn + language Crow cro 1421 cro + language Cruzeño crz 1422 crz + language Cua cua 1423 cua + language Cuba Sign Language csf 1424 csf + language Cubeo cub 1425 cub + language Cuiba cui 1426 cui + language Cuitlatec cuy 1427 cuy + language Culina cul 1428 cul + language Cumanagoto cuo 1429 cuo + language Cumbric xcb 1430 xcb + language Cun cuq 1431 cuq + language Cuneiform Luwian xlu 1432 xlu + language Cupeño cup 1433 cup + language Curonian xcu 1434 xcu + language Curripaco kpc 1435 kpc + language Cusco Quechua quz 1436 quz + language Cutchi-Swahili ccl 1437 ccl + language Cuvok cuv 1438 cuv + language Cuyamecalco Mixtec xtu 1439 xtu + language Cuyonon cyo 1440 cyo + language Cwi Bwamu bwy 1441 bwy + language Cypriot Arabic acy 1442 acy + language Czech ces 1443 ces cs cze + language Czech Sign Language cse 1444 cse + language Côông cnc 1445 cnc + language Da'a Kaili kzf 1446 kzf + language Daai Chin dao 1447 dao + language Daakaka bpa 1448 bpa + language Daantanai' lni 1449 lni + language Daasanach dsh 1450 dsh + language Daatsʼíin dtn 1451 dtn + language Daba dbq 1452 dbq + language Dabarre dbr 1453 dbr + language Dabe dbe 1454 dbe + language Dacian xdc 1455 xdc + language Dadi Dadi dda 1456 dda + language Dadibi mps 1457 mps + language Dadiya dbd 1458 dbd + language Daga dgz 1459 dgz + language Dagaari Dioula dgd 1460 dgd + language Dagba dgk 1461 dgk + language Dagbani dag 1462 dag + language Dagik dec 1463 dec + language Dagoman dgn 1464 dgn + language Dahalik dlk 1465 dlk + language Dahalo dal 1466 dal + language Daho-Doo das 1467 das + language Dai dij 1468 dij + language Dai Zhuang zhd 1469 zhd + language Dair drb 1470 drb + language Dakka dkk 1471 dkk + language Dakota dak 1472 dak + language Dakpakha dka 1473 dka + language Dalabon ngk 1474 ngk + language Dalmatian dlm 1475 dlm + language Daloa Bété bev 1476 bev + language Dama dmm 1477 dmm + language Damakawa dam 1478 dam + language Damal uhn 1479 uhn + language Dambi dac 1480 dac + language Dameli dml 1481 dml + language Dampelas dms 1482 dms + language Dan dnj 1483 dnj + language Danaru dnr 1484 dnr + language Danau dnu 1485 dnu + language Dandami Maria daq 1486 daq + language Dangaléat daa 1487 daa + language Dangaura Tharu thl 1488 thl + language Danish dan 1489 dan da + language Danish Sign Language dsl 1490 dsl + language Dano aso 1491 aso + language Danu dnv 1492 dnv + language Dao daz 1493 daz + language Daonda dnd 1494 dnd + language Dar Daju Daju djc 1495 djc + language Dar Fur Daju daj 1496 daj + language Dar Sila Daju dau 1497 dau + language Darai dry 1498 dry + language Dargwa dar 1499 dar + language Dari prs 1500 prs + language Darkinyung xda 1501 xda + language Darlong dln 1502 dln + language Darmiya drd 1503 drd + language Daro-Matu Melanau dro 1504 dro + language Dass dot 1505 dot + language Datooga tcc 1506 tcc + language Daungwurrung dgw 1507 dgw + language Daur dta 1508 dta + language Davawenyo daw 1509 daw + language Dawawa dww 1510 dww + language Dawera-Daweloor ddw 1511 ddw + language Dawik Kui dwk 1512 dwk + language Dawro dwr 1513 dwr + language Day dai 1514 dai + language Dayi dax 1515 dax + language Daza dzd 1516 dzd + language Dazaga dzg 1517 dzg + language Deccan dcc 1518 dcc + language Dedua ded 1519 ded + language Defaka afn 1520 afn + language Defi Gbe gbh 1521 gbh + language Deg mzw 1522 mzw + language Degema deg 1523 deg + language Degenan dge 1524 dge + language Degexit'an ing 1525 ing + language Dehu dhv 1526 dhv + language Dehwari deh 1527 deh + language Dek dek 1528 dek + language Dela-Oenale row 1529 row + language Delaware del 1530 del + language Delo ntr 1531 ntr + language Dem dem 1532 dem + language Dema dmx 1533 dmx + language Demisa dei 1534 dei + language Demta dmy 1535 dmy + language Dendi (Benin) ddn 1536 ddn + language Dendi (Central African Republic) deq 1537 deq + language Dengese dez 1538 dez + language Dengka dnk 1539 dnk + language Deno dbb 1540 dbb + language Denya anv 1541 anv + language Dení dny 1542 dny + language Deori der 1543 der + language Dera (Indonesia) kbv 1544 kbv + language Dera (Nigeria) kna 1545 kna + language Desano des 1546 des + language Desiya dso 1547 dso + language Dewas Rai dwz 1548 dwz + language Dewoin dee 1549 dee + language Dezfuli def 1550 def + language Dghwede dgh 1551 dgh + language Dhaiso dhs 1552 dhs + language Dhalandji dhl 1553 dhl + language Dhangu-Djangu dhg 1554 dhg + language Dhanki dhn 1555 dhn + language Dhanwar (Nepal) dhw 1556 dhw + language Dhao nfa 1557 nfa + language Dharawal tbh 1558 tbh + language Dhargari dhr 1559 dhr + language Dharuk xdk 1560 xdk + language Dharumbal xgm 1561 xgm + language Dhatki mki 1562 mki + language Dhimal dhi 1563 dhi + language Dhodia dho 1564 dho + language Dhofari Arabic adf 1565 adf + language Dhudhuroa ddr 1566 ddr + language Dhundari dhd 1567 dhd + language Dhungaloo dhx 1568 dhx + language Dhurga dhu 1569 dhu + language Dhuwal dwu 1570 dwu + language Dhuwaya dwy 1571 dwy + language Dia dia 1572 dia + language Dibabawon Manobo mbd 1573 mbd + language Dibiyaso dby 1574 dby + language Dibo dio 1575 dio + language Dibole bvx 1576 bvx + language Dicamay Agta duy 1577 duy + language Didinga did 1578 did + language Dido ddo 1579 ddo + language Dieri dif 1580 dif + language Digaro-Mishmi mhu 1581 mhu + language Digo dig 1582 dig + language Dii dur 1583 dur + language Dijim-Bwilim cfa 1584 cfa + language Dilling dil 1585 dil + language Dima jma 1586 jma + language Dimasa dis 1587 dis + language Dimbong dii 1588 dii + language Dime dim 1589 dim + language Dimli (individual language) diq 1590 diq + language Ding diz 1591 diz + language Dinka din 1592 din + language Dir-Nyamzak-Mbarimi nzr 1593 nzr + language Dirasha gdl 1594 gdl + language Diri dwa 1595 dwa + language Diriku diu 1596 diu + language Dirim dir 1597 dir + language Disa dsi 1598 dsi + language Ditammari tbz 1599 tbz + language Ditidaht dtd 1600 dtd + language Diuwe diy 1601 diy + language Diuxi-Tilantongo Mixtec xtd 1602 xtd + language Dixon Reef dix 1603 dix + language Dizin mdx 1604 mdx + language Djabugay dyy 1605 dyy + language Djabwurrung tjw 1606 tjw + language Djadjawurrung dja 1607 dja + language Djambarrpuyngu djr 1608 djr + language Djamindjung djd 1609 djd + language Djangun djf 1610 djf + language Djawi djw 1611 djw + language Djeebbana djj 1612 djj + language Djimini Senoufo dyi 1613 dyi + language Djinang dji 1614 dji + language Djinba djb 1615 djb + language Djiwarli dze 1616 dze + language Dobel kvo 1617 kvo + language Dobu dob 1618 dob + language Doe doe 1619 doe + language Doga dgg 1620 dgg + language Doghoro dgx 1621 dgx + language Dogoso dgs 1622 dgs + language Dogosé dos 1623 dos + language Dogri (individual language) dgo 1624 dgo + language Dogri (macrolanguage) doi 1625 doi + language Dogul Dom Dogon dbg 1626 dbg + language Doka dbi 1627 dbi + language Doko-Uyanga uya 1628 uya + language Dokshi dsk 1629 dsk + language Dolgan dlg 1630 dlg + language Dolpo dre 1631 dre + language Dom doa 1632 doa + language Domaaki dmk 1633 dmk + language Domari rmt 1634 rmt + language Dombe dov 1635 dov + language Dominican Sign Language doq 1636 doq + language Dompo doy 1637 doy + language Domu dof 1638 dof + language Domung dev 1639 dev + language Dondo dok 1640 dok + language Dong doh 1641 doh + language Dongo doo 1642 doo + language Dongotono ddd 1643 ddd + language Dongshanba Lalo yik 1644 yik + language Dongxiang sce 1645 sce + language Donno So Dogon dds 1646 dds + language Doondo dde 1647 dde + language Dori'o dor 1648 dor + language Doromu-Koki kqc 1649 kqc + language Dorze doz 1650 doz + language Doso dol 1651 dol + language Dotyali dty 1652 dty + language Doutai tds 1653 tds + language Doyayo dow 1654 dow + language Drents drt 1655 drt + language Drung duu 1656 duu + language Duala dua 1657 dua + language Duano dup 1658 dup + language Duau dva 1659 dva + language Dubli dub 1660 dub + language Dubu dmu 1661 dmu + language Dugun ndu 1662 ndu + language Duguri dbm 1663 dbm + language Dugwor dme 1664 dme + language Duhwa kbz 1665 kbz + language Duke nke 1666 nke + language Dulbu dbo 1667 dbo + language Duli-Gey duz 1668 duz + language Duma dma 1669 dma + language Dumbea duf 1670 duf + language Dumi dus 1671 dus + language Dumpas dmv 1672 dmv + language Dumun dui 1673 dui + language Duna duc 1674 duc + language Dungan dng 1675 dng + language Dungmali raa 1676 raa + language Dungra Bhil duh 1677 duh + language Dungu dbv 1678 dbv + language Dupaninan Agta duo 1679 duo + language Dura drq 1680 drq + language Duri mvp 1681 mvp + language Duriankere dbn 1682 dbn + language Durop krp 1683 krp + language Duruma dug 1684 dug + language Duruwa pci 1685 pci + language Dusner dsn 1686 dsn + language Dusun Deyah dun 1687 dun + language Dusun Malang duq 1688 duq + language Dusun Witu duw 1689 duw + language Dutch nld 1690 nld dut nl + language Dutch Sign Language dse 1691 dse + language Dutton World Speedwords dws 1692 dws + language Duungooma dux 1693 dux + language Duupa dae 1694 dae + language Duvle duv 1695 duv + language Duwai dbp 1696 dbp + language Duwet gve 1697 gve + language Dũya ldb 1698 ldb + language Dwang nnu 1699 nnu + language Dyaberdyaber dyb 1700 dyb + language Dyan dya 1701 dya + language Dyangadi dyn 1702 dyn + language Dyarim dyr 1703 dyr + language Dyirbal dbl 1704 dbl + language Dyugun dyd 1705 dyd + language Dyula dyu 1706 dyu + language Dza jen 1707 jen + language Dzalakha dzl 1708 dzl + language Dzando dzn 1709 dzn + language Dzao Min bpn 1710 bpn + language Dzongkha dzo 1711 dzo dz + language Dzùùngoo dnn 1712 dnn + language Dâw kwa 1713 kwa + language E eee 1714 eee + language E'ma Buyang yzg 1715 yzg + language E'ñapa Woromaipu pbh 1716 pbh + language Early Tripuri xtr 1717 xtr + language East Ambae omb 1718 omb + language East Berawan zbe 1719 zbe + language East Damar dmr 1720 dmr + language East Futuna fud 1721 fud + language East Kewa kjs 1722 kjs + language East Limba lma 1723 lma + language East Makian mky 1724 mky + language East Masela vme 1725 vme + language East Nyala nle 1726 nle + language East Tarangan tre 1727 tre + language East Yugur yuy 1728 yuy + language Eastern Abnaki aaq 1729 aaq + language Eastern Acipa acp 1730 acp + language Eastern Apurímac Quechua qve 1731 qve + language Eastern Arrernte aer 1732 aer + language Eastern Balochi bgp 1733 bgp + language Eastern Bolivian Guaraní gui 1734 gui + language Eastern Bontok ebk 1735 ebk + language Eastern Bru bru 1736 bru + language Eastern Canadian Inuktitut ike 1737 ike + language Eastern Cham cjm 1738 cjm + language Eastern Durango Nahuatl azd 1739 azd + language Eastern Egyptian Bedawi Arabic avl 1740 avl + language Eastern Frisian frs 1741 frs + language Eastern Gorkha Tamang tge 1742 tge + language Eastern Highland Chatino cly 1743 cly + language Eastern Highland Otomi otm 1744 otm + language Eastern Hongshuihe Zhuang zeh 1745 zeh + language Eastern Huasteca Nahuatl nhe 1746 nhe + language Eastern Huishui Hmong hme 1747 hme + language Eastern Karaboro xrb 1748 xrb + language Eastern Katu ktv 1749 ktv + language Eastern Kayah eky 1750 eky + language Eastern Keres kee 1751 kee + language Eastern Khumi Chin cek 1752 cek + language Eastern Krahn kqo 1753 kqo + language Eastern Lalu yit 1754 yit + language Eastern Lawa lwl 1755 lwl + language Eastern Magar mgp 1756 mgp + language Eastern Maninkakan emk 1757 emk + language Eastern Mari mhr 1758 mhr + language Eastern Maroon Creole djk 1759 djk + language Eastern Meohang emg 1760 emg + language Eastern Minyag emq 1761 emq + language Eastern Mnong mng 1762 mng + language Eastern Muria emu 1763 emu + language Eastern Ngad'a nea 1764 nea + language Eastern Nisu nos 1765 nos + language Eastern Ojibwa ojg 1766 ojg + language Eastern Oromo hae 1767 hae + language Eastern Parbate Kham kif 1768 kif + language Eastern Penan pez 1769 pez + language Eastern Pomo peb 1770 peb + language Eastern Qiandong Miao hmq 1771 hmq + language Eastern Subanen sfe 1772 sfe + language Eastern Tamang taj 1773 taj + language Eastern Tawbuid bnj 1774 bnj + language Eastern Xiangxi Miao muq 1775 muq + language Eastern Xwla Gbe gbx 1776 gbx + language Eastern Yiddish ydd 1777 ydd + language Ebira igb 1778 igb + language Eblan xeb 1779 xeb + language Ebrié ebr 1780 ebr + language Ebughu ebg 1781 ebg + language Ecuadorian Sign Language ecs 1782 ecs + language Ede Cabe cbj 1783 cbj + language Ede Ica ica 1784 ica + language Ede Idaca idd 1785 idd + language Ede Ije ijj 1786 ijj + language Edera Awyu awy 1787 awy + language Edolo etr 1788 etr + language Edomite xdm 1789 xdm + language Edopi dbf 1790 dbf + language Efai efa 1791 efa + language Efe efe 1792 efe + language Efik efi 1793 efi + language Efutop ofu 1794 ofu + language Ega ega 1795 ega + language Eggon ego 1796 ego + language Egypt Sign Language esl 1797 esl + language Egyptian (Ancient) egy 1798 egy + language Egyptian Arabic arz 1799 arz + language Ehueun ehu 1800 ehu + language Eipomek eip 1801 eip + language Eitiep eit 1802 eit + language Ejagham etu 1803 etu + language Ejamat eja 1804 eja + language Ekai Chin cey 1805 cey + language Ekajuk eka 1806 eka + language Ekari ekg 1807 ekg + language Eki eki 1808 eki + language Ekit eke 1809 eke + language Ekpeye ekp 1810 ekp + language El Alto Zapotec zpp 1811 zpp + language El Hugeirat elh 1812 elh + language El Molo elo 1813 elo + language El Nayar Cora crn 1814 crn + language Elamite elx 1815 elx + language Eleme elm 1816 elm + language Elepi ele 1817 ele + language Elfdalian ovd 1818 ovd + language Elip ekm 1819 ekm + language Elkei elk 1820 elk + language Elotepec Zapotec zte 1821 zte + language Eloyi afo 1822 afo + language Elseng mrf 1823 mrf + language Elu elu 1824 elu + language Elymian xly 1825 xly + language Emae mmw 1826 mmw + language Emai-Iuleha-Ora ema 1827 ema + language Eman emn 1828 emn + language Embaloh emb 1829 emb + language Emberá-Baudó bdc 1830 bdc + language Emberá-Catío cto 1831 cto + language Emberá-Chamí cmi 1832 cmi + language Emberá-Tadó tdc 1833 tdc + language Embu ebu 1834 ebu + language Emerillon eme 1835 eme + language Emilian egl 1836 egl + language Emplawas emw 1837 emw + language Emumu enr 1838 enr + language En enc 1839 enc + language Enawené-Nawé unk 1840 unk + language Ende end 1841 end + language Enga enq 1842 enq + language Engdewu ngr 1843 ngr + language Engenni enn 1844 enn + language Enggano eno 1845 eno + language English eng 1846 eng en + language Enlhet enl 1847 enl + language Enrekang ptt 1848 ptt + language Enu enu 1849 enu + language Enwan (Akwa Ibom State) enw 1850 enw + language Enwan (Edo State) env 1851 env + language Enxet enx 1852 enx + language Enya gey 1853 gey + language Epena sja 1854 sja + language Epi-Olmec xep 1855 xep + language Epie epi 1856 epi + language Epigraphic Mayan emy 1857 emy + language Eravallan era 1858 era + language Erave kjy 1859 kjy + language Ere twp 1860 twp + language Eritai ert 1861 ert + language Erokwanas erw 1862 erw + language Erre err 1863 err + language Erromintxela emx 1864 emx + language Ersu ers 1865 ers + language Eruwa erh 1866 erh + language Erzya myv 1867 myv + language Esan ish 1868 ish + language Ese mcq 1869 mcq + language Ese Ejja ese 1870 ese + language Eshtehardi esh 1871 esh + language Esimbi ags 1872 ags + language Eskayan esy 1873 esy + language Esperanto epo 1874 epo eo + language Esselen esq 1875 esq + language Estado de México Otomi ots 1876 ots + language Estonian est 1877 est et + language Estonian Sign Language eso 1878 eso + language Esuma esm 1879 esm + language Etchemin etc 1880 etc + language Etebi etb 1881 etb + language Eten etx 1882 etx + language Eteocretan ecr 1883 ecr + language Eteocypriot ecy 1884 ecy + language Ethiopian Sign Language eth 1885 eth + language Etkywan ich 1886 ich + language Eton (Cameroon) eto 1887 eto + language Eton (Vanuatu) etn 1888 etn + language Etruscan ett 1889 ett + language Etulo utr 1890 utr + language Eudeve eud 1891 eud + language Evant bzz 1892 bzz + language Even eve 1893 eve + language Evenki evn 1894 evn + language Eviya gev 1895 gev + language Ewage-Notu nou 1896 nou + language Ewe ewe 1897 ewe ee + language Ewondo ewo 1898 ewo + language Extremaduran ext 1899 ext + language Eyak eya 1900 eya + language Ezaa eza 1901 eza + language Fa d'Ambu fab 1902 fab + language Fagani faf 1903 faf + language Faifi fif 1904 fif + language Faire Atta azt 1905 azt + language Faita faj 1906 faj + language Faiwol fai 1907 fai + language Fala fax 1908 fax + language Falam Chin cfm 1909 cfm + language Fali fli 1910 fli + language Faliscan xfa 1911 xfa + language Fam fam 1912 fam + language Fanagalo fng 1913 fng + language Fanamaket bjp 1914 bjp + language Fanbak fnb 1915 fnb + language Fang (Cameroon) fak 1916 fak + language Fang (Equatorial Guinea) fan 1917 fan + language Fania fni 1918 fni + language Fanti fat 1919 fat + language Far Western Muria fmu 1920 fmu + language Farefare gur 1921 gur + language Faroese fao 1922 fao fo + language Fas fqs 1923 fqs + language Fasu faa 1924 faa + language Fataleka far 1925 far + language Fataluku ddg 1926 ddg + language Fayu fau 1927 fau + language Fe'fe' fmp 1928 fmp + language Fembe agl 1929 agl + language Fernando Po Creole English fpe 1930 fpe + language Feroge fer 1931 fer + language Fiji Hindi hif 1932 hif + language Fijian fij 1933 fij fj + language Filipino fil 1934 fil + language Filomena Mata-Coahuitlán Totonac tlp 1935 tlp + language Finland-Swedish Sign Language fss 1936 fss + language Finnish fin 1937 fin fi + language Finnish Sign Language fse 1938 fse + language Finongan fag 1939 fag + language Fipa fip 1940 fip + language Firan fir 1941 fir + language Fiwaga fiw 1942 fiw + language Flaaitaal fly 1943 fly + language Flinders Island fln 1944 fln + language Foau flh 1945 flh + language Foi foi 1946 foi + language Foia Foia ffi 1947 ffi + language Folopa ppo 1948 ppo + language Foma fom 1949 fom + language Fon fon 1950 fon + language Fongoro fgr 1951 fgr + language Foodo fod 1952 fod + language Forak frq 1953 frq + language Fordata frd 1954 frd + language Fore for 1955 for + language Forest Enets enf 1956 enf + language Fortsenal frt 1957 frt + language Francisco León Zoque zos 1958 zos + language Frankish frk 1959 frk + language French fra 1960 fra fre fr + language French Sign Language fsl 1961 fsl + language Friulian fur 1962 fur + language Fula, Fulah ful 1963 ful ff Fula Fulah + language Fuliiru flr 1964 flr + language Fulniô fun 1965 fun + language Fum fum 1966 fum + language Fungwa ula 1967 ula + language Fur fvr 1968 fvr + language Furu fuu 1969 fuu + language Futuna-Aniwa fut 1970 fut + language Fuyug fuy 1971 fuy + language Fwe fwe 1972 fwe + language Fwâi fwa 1973 fwa + language Fyam pym 1974 pym + language Fyer fie 1975 fie + language Ga gaa 1976 gaa + language Ga'anda gqa 1977 gqa + language Ga'dang gdg 1978 gdg + language Gaa ttb 1979 ttb + language Gaam tbi 1980 tbi + language Gabi-Gabi gbw 1981 gbw + language Gabri gab 1982 gab + language Gabrielino-Fernandeño xgf 1983 xgf + language Gadang gdk 1984 gdk + language Gaddang gad 1985 gad + language Gaddi gbk 1986 gbk + language Gade ged 1987 ged + language Gade Lohar gda 1988 gda + language Gadjerawang gdh 1989 gdh + language Gadsup gaj 1990 gaj + language Gaelic, Scottish Gaelic gla 1991 gla Gaelic Scottish Gaelic gd + language Gafat gft 1992 gft + language Gagadu gbu 1993 gbu + language Gagauz gag 1994 gag + language Gagnoa Bété btg 1995 btg + language Gagu ggu 1996 ggu + language Gahri bfu 1997 bfu + language Gaikundi gbf 1998 gbf + language Gail gic 1999 gic + language Gaina gcn 2000 gcn + language Gal gap 2001 gap + language Galambu glo 2002 glo + language Galatian xga 2003 xga + language Galela gbi 2004 gbi + language Galeya gar 2005 gar + language Galibi Carib car 2006 car + language Galice gce 2007 gce + language Galician glg 2008 glg gl + language Galindan xgl 2009 xgl + language Gallurese Sardinian sdn 2010 sdn + language Galo adl 2011 adl + language Galolen gal 2012 gal + language Gamale Kham kgj 2013 kgj + language Gambera gma 2014 gma + language Gambian Wolof wof 2015 wof + language Gamilaraay kld 2016 kld + language Gamit gbl 2017 gbl + language Gamkonora gak 2018 gak + language Gamo gmv 2019 gmv + language Gamo-Ningi bte 2020 bte + language Gan Chinese gan 2021 gan + language Gana gnq 2022 gnq + language Ganang gne 2023 gne + language Ganda lug 2024 lug lg + language Gane gzn 2025 gzn + language Ganggalida gcd 2026 gcd + language Ganglau ggl 2027 ggl + language Gangte gnb 2028 gnb + language Gangulu gnl 2029 gnl + language Gants gao 2030 gao + language Ganza gza 2031 gza + language Ganzi gnz 2032 gnz + language Gao gga 2033 gga + language Gapapaiwa pwg 2034 pwg + language Garhwali gbm 2035 gbm + language Garifuna cab 2036 cab + language Garig-Ilgar ilg 2037 ilg + language Garingbal xgi 2038 xgi + language Garlali gll 2039 gll + language Garo grt 2040 grt + language Garre gex 2041 gex + language Garrwa wrk 2042 wrk + language Garus gyb 2043 gyb + language Garza xgr 2044 xgr + language Gata' gaq 2045 gaq + language Gavak dmc 2046 dmc + language Gavar gou 2047 gou + language Gavião Do Jiparaná gvo 2048 gvo + language Gawar-Bati gwt 2049 gwt + language Gawri gwc 2050 gwc + language Gawwada gwd 2051 gwd + language Gayil gyl 2052 gyl + language Gayo gay 2053 gay + language Gazi gzi 2054 gzi + language Gaɓogbo gie 2055 gie + language Gbagyi gbr 2056 gbr + language Gbanu gbv 2057 gbv + language Gbanziri gbg 2058 gbg + language Gbari gby 2059 gby + language Gbaya (Central African Republic) gba 2060 gba + language Gbaya (Sudan) krs 2061 krs + language Gbaya-Bossangoa gbp 2062 gbp + language Gbaya-Bozoum gbq 2063 gbq + language Gbaya-Mbodomo gmm 2064 gmm + language Gbayi gyg 2065 gyg + language Gbesi Gbe gbs 2066 gbs + language Gbii ggb 2067 ggb + language Gbin xgb 2068 xgb + language Gbiri-Niragu grh 2069 grh + language Gboloo Grebo gec 2070 gec + language Ge hmj 2071 hmj + language Geba Karen kvq 2072 kvq + language Gebe gei 2073 gei + language Gedaged gdd 2074 gdd + language Gedeo drs 2075 drs + language Geez gez 2076 gez + language Geji gyz 2077 gyz + language Geko Karen ghk 2078 ghk + language Gela nlg 2079 nlg + language Geme geq 2080 geq + language Gen gej 2081 gej + language Gende gaf 2082 gaf + language Gengle geg 2083 geg + language Georgian kat 2084 kat geo ka + language Gepo ygp 2085 ygp + language Gera gew 2086 gew + language Gerai gef 2087 gef + language German deu 2088 deu de ger + language German Sign Language gsg 2089 gsg + language Geruma gea 2090 gea + language Geser-Gorom ges 2091 ges + language Ghadamès gha 2092 gha + language Ghanaian Pidgin English gpe 2093 gpe + language Ghanaian Sign Language gse 2094 gse + language Ghandruk Sign Language gds 2095 gds + language Ghanongga ghn 2096 ghn + language Ghari gri 2097 gri + language Ghayavi bmk 2098 bmk + language Gheg Albanian aln 2099 aln + language Ghera ghr 2100 ghr + language Ghodoberi gdo 2101 gdo + language Ghomara gho 2102 gho + language Ghomálá' bbj 2103 bbj + language Ghotuo aaa 2104 aaa + language Ghulfan ghl 2105 ghl + language Giangan bgi 2106 bgi + language Gibanawa gib 2107 gib + language Gidar gid 2108 gid + language Giiwo kks 2109 kks + language Gikyode acd 2110 acd + language Gilaki glk 2111 glk + language Gilbertese gil 2112 gil + language Gilima gix 2113 gix + language Gilyak niv 2114 niv + language Gimi (Eastern Highlands) gim 2115 gim + language Gimi (West New Britain) gip 2116 gip + language Gimme kmp 2117 kmp + language Gimnime gmn 2118 gmn + language Ginuman gnm 2119 gnm + language Ginyanga ayg 2120 ayg + language Girawa bbr 2121 bbr + language Girirra gii 2122 gii + language Giryama nyf 2123 nyf + language Githabul gih 2124 gih + language Gitonga toh 2125 toh + language Gitua ggt 2126 ggt + language Gitxsan git 2127 git + language Giyug giy 2128 giy + language Gizrra tof 2129 tof + language Glaro-Twabo glr 2130 glr + language Glavda glw 2131 glw + language Glio-Oubi oub 2132 oub + language Gnau gnu 2133 gnu + language Goan Konkani gom 2134 gom + language Goaria gig 2135 gig + language Gobasi goi 2136 goi + language Gobu gox 2137 gox + language Godié god 2138 god + language Godwari gdx 2139 gdx + language Goemai ank 2140 ank + language Gofa gof 2141 gof + language Gogo gog 2142 gog + language Gogodala ggw 2143 ggw + language Gokana gkn 2144 gkn + language Gola gol 2145 gol + language Golin gvf 2146 gvf + language Golpa lja 2147 lja + language Gondi gon 2148 gon + language Gone Dau goo 2149 goo + language Gongduk goe 2150 goe + language Gonja gjn 2151 gjn + language Goo gov 2152 gov + language Gooniyandi gni 2153 gni + language Gor gqr 2154 gqr + language Gorakor goc 2155 goc + language Gorap goq 2156 goq + language Goreng xgg 2157 xgg + language Gorontalo gor 2158 gor + language Gorovu grq 2159 grq + language Gorowa gow 2160 gow + language Gothic got 2161 got + language Goundo goy 2162 goy + language Gourmanchéma gux 2163 gux + language Gowlan goj 2164 goj + language Gowli gok 2165 gok + language Gowro gwf 2166 gwf + language Gozarkhani goz 2167 goz + language Grangali nli 2168 nli + language Grass Koiari kbk 2169 kbk + language Grebo grb 2170 grb + language Greek Sign Language gss 2171 gss + language Green Gelao giq 2172 giq + language Greenlandic, Kalaallisut kal 2173 kal kl Greenlandic Kalaallisut + language Grenadian Creole English gcl 2174 gcl + language Gresi grs 2175 grs + language Groma gro 2176 gro + language Gronings gos 2177 gos + language Gros Ventre ats 2178 ats + language Gua gwx 2179 gwx + language Guadeloupean Creole French gcf 2180 gcf + language Guahibo guh 2181 guh + language Guajajára gub 2182 gub + language Guajá gvj 2183 gvj + language Guambiano gum 2184 gum + language Guana (Brazil) gqn 2185 gqn + language Guana (Paraguay) gva 2186 gva + language Guanano gvc 2187 gvc + language Guanche gnc 2188 gnc + language Guanyinqiao jiq 2189 jiq + language Guarani, Guaraní grn 2190 grn Guarani gn Guaraní + language Guarayu gyr 2191 gyr + language Guarequena gae 2192 gae + language Guatemalan Sign Language gsm 2193 gsm + language Guató gta 2194 gta + language Guayabero guo 2195 guo + language Gudang xgd 2196 xgd + language Gudanji nji 2197 nji + language Gude gde 2198 gde + language Gudu gdu 2199 gdu + language Guduf-Gava gdf 2200 gdf + language Guerrero Amuzgo amu 2201 amu + language Guerrero Nahuatl ngu 2202 ngu + language Guevea De Humboldt Zapotec zpg 2203 zpg + language Gugadj ggd 2204 ggd + language Gugu Badhun gdc 2205 gdc + language Gugu Warra wrw 2206 wrw + language Gugubera kkp 2207 kkp + language Guhu-Samane ghs 2208 ghs + language Guianese Creole French gcr 2209 gcr + language Guibei Zhuang zgb 2210 zgb + language Guiberoua Béte bet 2211 bet + language Guibian Zhuang zgn 2212 zgn + language Guinea Kpelle gkp 2213 gkp + language Guinea-Bissau Sign Language lgs 2214 lgs + language Guinean Sign Language gus 2215 gus + language Guiqiong gqi 2216 gqi + language Gujarati guj 2217 guj gu + language Gujari gju 2218 gju + language Gula (Central African Republic) kcm 2219 kcm + language Gula (Chad) glu 2220 glu + language Gula Iro glj 2221 glj + language Gula'alaa gmb 2222 gmb + language Gulay gvl 2223 gvl + language Gule gly 2224 gly + language Gulf Arabic afb 2225 afb + language Gumalu gmu 2226 gmu + language Gumatj gnn 2227 gnn + language Gumawana gvs 2228 gvs + language Gumuz guk 2229 guk + language Gun guw 2230 guw + language Gundi gdi 2231 gdi + language Gunditjmara gjm 2232 gjm + language Gundungurra xrd 2233 xrd + language Gungabula gyf 2234 gyf + language Gungu rub 2235 rub + language Guntai gnt 2236 gnt + language Gunwinggu gup 2237 gup + language Gunya gyy 2238 gyy + language Gupa-Abawa gpa 2239 gpa + language Gupapuyngu guf 2240 guf + language Guramalum grz 2241 grz + language Gurani hac 2242 hac + language Gurdjar gdj 2243 gdj + language Gureng Gureng gnr 2244 gnr + language Gurgula ggg 2245 ggg + language Guriaso grx 2246 grx + language Gurindji gue 2247 gue + language Gurindji Kriol gjr 2248 gjr + language Gurmana gvm 2249 gvm + language Guro goa 2250 goa + language Gurr-goni gge 2251 gge + language Gurung gvr 2252 gvr + language Guruntum-Mbaaru grd 2253 grd + language Gusii guz 2254 guz + language Gusilay gsl 2255 gsl + language Guugu Yimidhirr kky 2256 kky + language Guwa xgw 2257 xgw + language Guwamu gwu 2258 gwu + language Guya gka 2259 gka + language Guyanese Creole English gyn 2260 gyn + language Guyani gvy 2261 gvy + language Gvoko ngs 2262 ngs + language Gwa gwb 2263 gwb + language Gwahatike dah 2264 dah + language Gwak jgk 2265 jgk + language Gwamhi-Wuri bga 2266 bga + language Gwandara gwn 2267 gwn + language Gweda grw 2268 grw + language Gweno gwe 2269 gwe + language Gwere gwr 2270 gwr + language Gwichʼin gwi 2271 gwi + language Gyalsumdo gyo 2272 gyo + language Gyele gyi 2273 gyi + language Gyem gye 2274 gye + language Güilá Zapotec ztu 2275 ztu + language Gāndhārī pgd 2276 pgd + language Ha haq 2277 haq + language Habu hbu 2278 hbu + language Hadiyya hdy 2279 hdy + language Hadothi hoj 2280 hoj + language Hadrami xhd 2281 xhd + language Hadrami Arabic ayh 2282 ayh + language Hadza hts 2283 hts + language Haeke aek 2284 aek + language Hahon hah 2285 hah + language Haida hai 2286 hai + language Haigwai hgw 2287 hgw + language Haiphong Sign Language haf 2288 haf + language Haisla has 2289 has + language Haitian Creole, Haitian hat 2290 hat Haitian Creole Haitian ht + language Haitian Vodoun Culture Language hvc 2291 hvc + language Haiǁom hgm 2292 hgm + language Haji hji 2293 hji + language Hajong haj 2294 haj + language Hakha Chin cnh 2295 cnh + language Hakka Chinese hak 2296 hak + language Hakö hao 2297 hao + language Halang hal 2298 hal + language Halang Doan hld 2299 hld + language Halbi hlb 2300 hlb + language Halh Mongolian khk 2301 khk + language Halia hla 2302 hla + language Halkomelem hur 2303 hur + language Hamap hmu 2304 hmu + language Hamba hba 2305 hba + language Hamer-Banna amf 2306 amf + language Hamtai hmt 2307 hmt + language Han haa 2308 haa + language Hanga hag 2309 hag + language Hanga Hundi wos 2310 wos + language Hangaza han 2311 han + language Hani hni 2312 hni + language Hano lml 2313 lml + language Hanoi Sign Language hab 2314 hab + language Hanunoo hnn 2315 hnn + language Harami xha 2316 xha + language Harari har 2317 har + language Harijan Kinnauri kjo 2318 kjo + language Haroi hro 2319 hro + language Harsusi hss 2320 hss + language Haruai tmd 2321 tmd + language Haruku hrk 2322 hrk + language Haryanvi bgc 2323 bgc + language Harzani hrz 2324 hrz + language Hasha ybj 2325 ybj + language Hassaniyya mey 2326 mey + language Hatam had 2327 had + language Hattic xht 2328 xht + language Hausa hau 2329 hau ha + language Hausa Sign Language hsl 2330 hsl + language Havasupai-Walapai-Yavapai yuf 2331 yuf + language Haveke hvk 2332 hvk + language Havu hav 2333 hav + language Hawai'i Creole English hwc 2334 hwc + language Hawai'i Sign Language (HSL) hps 2335 hps + language Hawaiian haw 2336 haw + language Haya hay 2337 hay + language Hazaragi haz 2338 haz + language Hdi xed 2339 xed + language Hebrew (modern), Hebrew heb 2340 heb Hebrew (modern) Hebrew he + language Hehe heh 2341 heh + language Heiban hbn 2342 hbn + language Heiltsuk hei 2343 hei + language Helong heg 2344 heg + language Hema nix 2345 nix + language Hemba hem 2346 hem + language Herdé hed 2347 hed + language Herero her 2348 her hz + language Hermit llf 2349 llf + language Hernican xhr 2350 xhr + language Hewa ham 2351 ham + language Heyo auk 2352 auk + language Hiberno-Scottish Gaelic ghc 2353 ghc + language Hibito hib 2354 hib + language Hidatsa hid 2355 hid + language Hieroglyphic Luwian hlu 2356 hlu + language Higaonon mba 2357 mba + language Highland Konjo kjk 2358 kjk + language Highland Oaxaca Chontal chd 2359 chd + language Highland Popoluca poi 2360 poi + language Highland Puebla Nahuatl azz 2361 azz + language Highland Totonac tos 2362 tos + language Hijazi Arabic acw 2363 acw + language Hijuk hij 2364 hij + language Hiligaynon hil 2365 hil + language Himarimã hir 2366 hir + language Hindi hin 2367 hin hi + language Hinduri hii 2368 hii + language Hinukh gin 2369 gin + language Hiri Motu hmo 2370 hmo ho + language Hittite hit 2371 hit + language Hitu htu 2372 htu + language Hiw hiw 2373 hiw + language Hixkaryána hix 2374 hix + language Hlai lic 2375 lic + language Hlepho Phowa yhl 2376 yhl + language Hlersu hle 2377 hle + language Hmar hmr 2378 hmr + language Hmong hmn 2379 hmn + language Hmong Daw mww 2380 mww + language Hmong Don hmf 2381 hmf + language Hmong Dô hmv 2382 hmv + language Hmong Njua hnj 2383 hnj + language Hmong Shua hmz 2384 hmz + language Hmwaveke mrk 2385 mrk + language Ho hoc 2386 hoc + language Ho Chi Minh City Sign Language hos 2387 hos + language Ho-Chunk win 2388 win + language Hoava hoa 2389 hoa + language Hobyót hoh 2390 hoh + language Hoia Hoia hhi 2391 hhi + language Holikachuk hoi 2392 hoi + language Holiya hoy 2393 hoy + language Holma hod 2394 hod + language Holoholo hoo 2395 hoo + language Holu hol 2396 hol + language Homa hom 2397 hom + language Honduras Sign Language hds 2398 hds + language Hong Kong Sign Language hks 2399 hks + language Honi how 2400 how + language Hopi hop 2401 hop + language Horned Miao hrm 2402 hrm + language Horo hor 2403 hor + language Horom hoe 2404 hoe + language Horpa ero 2405 ero + language Hote hot 2406 hot + language Hoti hti 2407 hti + language Hovongan hov 2408 hov + language Hoyahoya hhy 2409 hhy + language Hozo hoz 2410 hoz + language Hpon hpo 2411 hpo + language Hrangkhol hra 2412 hra + language Hre hre 2413 hre + language Hruso hru 2414 hru + language Hu huo 2415 huo + language Huachipaeri hug 2416 hug + language Huallaga Huánuco Quechua qub 2417 qub + language Huamalíes-Dos de Mayo Huánuco Quechua qvh 2418 qvh + language Huambisa hub 2419 hub + language Huarijio var 2420 var + language Huastec hus 2421 hus + language Huaulu hud 2422 hud + language Huautla Mazatec mau 2423 mau + language Huaxcaleca Nahuatl nhq 2424 nhq + language Huaylas Ancash Quechua qwh 2425 qwh + language Huaylla Wanca Quechua qvw 2426 qvw + language Huba hbb 2427 hbb + language Huehuetla Tepehua tee 2428 tee + language Huichol hch 2429 hch + language Huilliche huh 2430 huh + language Huitepec Mixtec mxs 2431 mxs + language Huizhou Chinese czh 2432 czh + language Hukumina huw 2433 huw + language Hula hul 2434 hul + language Hulaulá huy 2435 huy + language Huli hui 2436 hui + language Hulung huk 2437 huk + language Humburi Senni Songhay hmb 2438 hmb + language Humene huf 2439 huf + language Humla hut 2440 hut + language Hunde hke 2441 hke + language Hung hnu 2442 hnu + language Hungana hum 2443 hum + language Hungarian hun 2444 hun hu + language Hungarian Sign Language hsh 2445 hsh + language Hungu hng 2446 hng + language Hunjara-Kaina Ke hkk 2447 hkk + language Hunnic xhc 2448 xhc + language Hunsrik hrx 2449 hrx + language Hunzib huz 2450 huz + language Hupa hup 2451 hup + language Hupdë jup 2452 jup + language Hupla hap 2453 hap + language Hurrian xhu 2454 xhu + language Hutterite German geh 2455 geh + language Hwana hwo 2456 hwo + language Hya hya 2457 hya + language Hyam jab 2458 jab + language Hyolmo scp 2459 scp + language Hértevin hrt 2460 hrt + language Hõne juh 2461 juh + language I-Wak iwk 2462 iwk + language Iaai iai 2463 iai + language Iamalele yml 2464 yml + language Iatmul ian 2465 ian + language Iau tmu 2466 tmu + language Ibali Teke tek 2467 tek + language Ibaloi ibl 2468 ibl + language Iban iba 2469 iba + language Ibanag ibg 2470 ibg + language Ibani iby 2471 iby + language Ibatan ivb 2472 ivb + language Iberian xib 2473 xib + language Ibibio ibb 2474 ibb + language Ibino ibn 2475 ibn + language Ibu ibu 2476 ibu + language Ibuoro ibr 2477 ibr + language Icelandic isl 2478 isl ice is + language Icelandic Sign Language icl 2479 icl + language Iceve-Maci bec 2480 bec + language Ida'an dbj 2481 dbj + language Idakho-Isukha-Tiriki ida 2482 ida + language Idaté idt 2483 idt + language Idere ide 2484 ide + language Idesa ids 2485 ids + language Idi idi 2486 idi + language Ido ido 2487 ido io + language Idoma idu 2488 idu + language Idon idc 2489 idc + language Idu-Mishmi clk 2490 clk + language Iduna viv 2491 viv + language Ifo iff 2492 iff + language Ifè ife 2493 ife + language Igala igl 2494 igl + language Igana igg 2495 igg + language Igbo ibo 2496 ibo ig + language Igede ige 2497 ige + language Ignaciano ign 2498 ign + language Igo ahl 2499 ahl + language Iguta nar 2500 nar + language Igwe igw 2501 igw + language Iha ihp 2502 ihp + language Iha Based Pidgin ihb 2503 ihb + language Ihievbe ihi 2504 ihi + language Ik ikx 2505 ikx + language Ika ikk 2506 ikk + language Ikaranggal ikr 2507 ikr + language Ikhin-Arokho ikh 2508 ikh + language Ikizu ikz 2509 ikz + language Iko iki 2510 iki + language Ikobi meb 2511 meb + language Ikoma-Nata-Isenye ntk 2512 ntk + language Ikpeng txi 2513 txi + language Ikpeshi ikp 2514 ikp + language Ikposo kpo 2515 kpo + language Iku-Gora-Ankwa ikv 2516 ikv + language Ikulu ikl 2517 ikl + language Ikwere ikw 2518 ikw + language Ikwo iqw 2519 iqw + language Ila ilb 2520 ilb + language Ile Ape ila 2521 ila + language Ili Turki ili 2522 ili + language Ili'uun ilu 2523 ilu + language Ilianen Manobo mbi 2524 mbi + language Illyrian xil 2525 xil + language Iloko ilo 2526 ilo + language Ilongot ilk 2527 ilk + language Ilue ilv 2528 ilv + language Ilwana mlk 2529 mlk + language Imbabura Highland Quichua qvi 2530 qvi + language Imbongu imo 2531 imo + language Imonda imn 2532 imn + language Imotong imt 2533 imt + language Imroing imr 2534 imr + language Inabaknon abx 2535 abx + language Inapang mzu 2536 mzu + language Inari Sami smn 2537 smn + language Indian Sign Language ins 2538 ins + language Indo-Portuguese idb 2539 idb + language Indonesian ind 2540 ind id + language Indonesian Bajau bdl 2541 bdl + language Indonesian Sign Language inl 2542 inl + language Indri idr 2543 idr + language Indus Kohistani mvy 2544 mvy + language Indus Valley Language xiv 2545 xiv + language Inebu One oin 2546 oin + language Ineseño inz 2547 inz + language Inga inb 2548 inb + language Ingrian izh 2549 izh + language Ingush inh 2550 inh + language Inlaod Itneg iti 2551 iti + language Innu moe 2552 moe + language Inoke-Yate ino 2553 ino + language Inonhan loc 2554 loc + language Inor ior 2555 ior + language Inpui Naga nkf 2556 nkf + language Interglossa igs 2557 igs + language Interlingua, Interlingua (International Auxiliary Language Association) ina 2558 ina Interlingua ia Interlingua (International Auxiliary Language Association) + language Interlingue ile 2559 ile ie + language International Sign ils 2560 ils + language Interslavic isv 2561 isv + language Intha int 2562 int + language Inuinnaqtun ikt 2563 ikt + language Inuit Sign Language iks 2564 iks + language Inuktitut iku 2565 iku iu + language Inupiaq ipk 2566 ipk ik + language Iowa-Oto iow 2567 iow + language Ipalapa Amuzgo azm 2568 azm + language Ipiko ipo 2569 ipo + language Ipili ipi 2570 ipi + language Ipulo ass 2571 ass + language Iquito iqu 2572 iqu + language Ir irr 2573 irr + language Iranian Persian pes 2574 pes + language Iranian Sign Language psc 2575 psc + language Iranun (Malaysia) ilm 2576 ilm + language Iranun (Philippines) ilp 2577 ilp + language Iraqw irk 2578 irk + language Irarutu irh 2579 irh + language Iraya iry 2580 iry + language Iresim ire 2581 ire + language Irish gle 2582 gle ga + language Irish Sign Language isg 2583 isg + language Irula iru 2584 iru + language Irántxe irn 2585 irn + language Isabi isa 2586 isa + language Isanzu isn 2587 isn + language Isarog Agta agk 2588 agk + language Isconahua isc 2589 isc + language Isebe igo 2590 igo + language Isekiri its 2591 its + language Ishkashimi isk 2592 isk + language Isinai inn 2593 inn + language Isirawa srl 2594 srl + language Island Carib crb 2595 crb + language Islander Creole English icr 2596 icr + language Isnag isd 2597 isd + language Isoko iso 2598 iso + language Israeli Sign Language isr 2599 isr + language Isthmus Mixe mir 2600 mir + language Isthmus Zapotec zai 2601 zai + language Isthmus-Cosoleacaque Nahuatl nhk 2602 nhk + language Isthmus-Mecayapan Nahuatl nhx 2603 nhx + language Isthmus-Pajapan Nahuatl nhp 2604 nhp + language Istriot ist 2605 ist + language Istro Romanian ruo 2606 ruo + language Isu (Fako Division) szv 2607 szv + language Isu (Menchum Division) isu 2608 isu + language Italian ita 2609 ita it + language Italian Sign Language ise 2610 ise + language Itawit itv 2611 itv + language Itelmen itl 2612 itl + language Itene ite 2613 ite + language Iteri itr 2614 itr + language Itik itx 2615 itx + language Ito itw 2616 itw + language Itonama ito 2617 ito + language Itu Mbon Uzo itm 2618 itm + language Itundujia Mixtec mce 2619 mce + language Itzá itz 2620 itz + language Iu Mien ium 2621 ium + language Ivatan ivv 2622 ivv + language Ivbie North-Okpela-Arhe atg 2623 atg + language Iwaidja ibd 2624 ibd + language Iwal kbm 2625 kbm + language Iwam iwm 2626 iwm + language Iwur iwo 2627 iwo + language Ixcatec ixc 2628 ixc + language Ixcatlán Mazatec mzi 2629 mzi + language Ixil ixl 2630 ixl + language Ixtayutla Mixtec vmj 2631 vmj + language Ixtenco Otomi otz 2632 otz + language Iyayu iya 2633 iya + language Iyive uiv 2634 uiv + language Iyo nca 2635 nca + language Iyo'wujwa Chorote crq 2636 crq + language Iyojwa'ja Chorote crt 2637 crt + language Izere izr 2638 izr + language Izii izz 2639 izz + language Izon ijc 2640 ijc + language Izora cbo 2641 cbo + language Iñapari inp 2642 inp + language Jabutí jbt 2643 jbt + language Jad jda 2644 jda + language Jadgali jdg 2645 jdg + language Jah Hut jah 2646 jah + language Jahanka jad 2647 jad + language Jair Awyu awv 2648 awv + language Jaitmatang xjt 2649 xjt + language Jakati jat 2650 jat + language Jakattoe jrt 2651 jrt + language Jakun jak 2652 jak + language Jalapa De Díaz Mazatec maj 2653 maj + language Jalkunan bxl 2654 bxl + language Jamaican Country Sign Language jcs 2655 jcs + language Jamaican Creole English jam 2656 jam + language Jamaican Sign Language jls 2657 jls + language Jamamadí jaa 2658 jaa + language Jambi Malay jax 2659 jax + language Jamiltepec Mixtec mxt 2660 mxt + language Jamsay Dogon djm 2661 djm + language Jandai jan 2662 jan + language Jandavra jnd 2663 jnd + language Jangkang djo 2664 djo + language Jangshung jna 2665 jna + language Janji jni 2666 jni + language Japanese jpn 2667 jpn ja + language Japanese Sign Language jsl 2668 jsl + language Japrería jru 2669 jru + language Jaqaru jqr 2670 jqr + language Jara jaf 2671 jaf + language Jarai jra 2672 jra + language Jarawa (India) anq 2673 anq + language Jaru ddj 2674 ddj + language Jauja Wanca Quechua qxw 2675 qxw + language Jaunsari jns 2676 jns + language Javanese jav 2677 jav jv + language Javindo jvd 2678 jvd + language Jawe jaz 2679 jaz + language Jawoyn djn 2680 djn + language Jaya jyy 2681 jyy + language Jebero jeb 2682 jeb + language Jeh jeh 2683 jeh + language Jehai jhi 2684 jhi + language Jejara Naga pzn 2685 pzn + language Jejueo jje 2686 jje + language Jemez tow 2687 tow + language Jenaama Bozo bze 2688 bze + language Jennu Kurumba xuj 2689 xuj + language Jere jer 2690 jer + language Jeri Kuo jek 2691 jek + language Jerung jee 2692 jee + language Jewish Babylonian Aramaic (ca. 200-1200 CE) tmr 2693 tmr + language Jewish Palestinian Aramaic jpa 2694 jpa + language Jhankot Sign Language jhs 2695 jhs + language Jiamao jio 2696 jio + language Jiarong jya 2697 jya + language Jiba juo 2698 juo + language Jibu jib 2699 jib + language Jicarilla Apache apj 2700 apj + language Jiiddu jii 2701 jii + language Jilbe jie 2702 jie + language Jilim jil 2703 jil + language Jimi (Cameroon) jim 2704 jim + language Jimi (Nigeria) jmi 2705 jmi + language Jina jia 2706 jia + language Jingulu jig 2707 jig + language Jinyu Chinese cjy 2708 cjy + language Jiongnai Bunu pnu 2709 pnu + language Jirel jul 2710 jul + language Jiru jrr 2711 jrr + language Jita jit 2712 jit + language Jju kaj 2713 kaj + language Joba job 2714 job + language Jofotek-Bromnya jbr 2715 jbr + language Jogi jog 2716 jog + language Jola-Fonyi dyo 2717 dyo + language Jola-Kasa csk 2718 csk + language Jonkor Bourmataguil jeu 2719 jeu + language Jordanian Sign Language jos 2720 jos + language Jorá jor 2721 jor + language Jowulu jow 2722 jow + language Ju juu 2723 juu + language Juang jun 2724 jun + language Judeo-Arabic jrb 2725 jrb + language Judeo-Berber jbe 2726 jbe + language Judeo-Georgian jge 2727 jge + language Judeo-Iraqi Arabic yhd 2728 yhd + language Judeo-Italian itk 2729 itk + language Judeo-Moroccan Arabic aju 2730 aju + language Judeo-Persian jpr 2731 jpr + language Judeo-Tat jdt 2732 jdt + language Judeo-Tripolitanian Arabic yud 2733 yud + language Judeo-Yemeni Arabic jye 2734 jye + language Jukun Takum jbu 2735 jbu + language Jumjum jum 2736 jum + language Jumla Sign Language jus 2737 jus + language Jumli jml 2738 jml + language Jungle Inga inj 2739 inj + language Juquila Mixe mxq 2740 mxq + language Jur Modo bex 2741 bex + language Juray juy 2742 juy + language Jurchen juc 2743 juc + language Jurúna jur 2744 jur + language Jutish jut 2745 jut + language Juwal mwb 2746 mwb + language Juxtlahuaca Mixtec vmc 2747 vmc + language Juǀʼhoan ktz 2748 ktz + language Jwira-Pepesa jwi 2749 jwi + language Jèrriais nrf 2750 nrf + language Júma jua 2751 jua + language K'iche' quc 2752 quc + language Kaamba xku 2753 xku + language Kaan ldl 2754 ldl + language Kaang Chin ckn 2755 ckn + language Kaansa gna 2756 gna + language Kaba ksp 2757 ksp + language Kabalai kvf 2758 kvf + language Kabardian kbd 2759 kbd + language Kabatei xkp 2760 xkp + language Kabiyè kbp 2761 kbp + language Kabola klz 2762 klz + language Kabore One onk 2763 onk + language Kabras lkb 2764 lkb + language Kaburi uka 2765 uka + language Kabutra kbu 2766 kbu + language Kabuverdianu kea 2767 kea + language Kabwa cwa 2768 cwa + language Kabwari kcw 2769 kcw + language Kabyle kab 2770 kab + language Kachama-Ganjule kcx 2771 kcx + language Kachari xac 2772 xac + language Kachhi kfr 2773 kfr + language Kachi Koli gjk 2774 gjk + language Kachin kac 2775 kac + language Kachok xkk 2776 xkk + language Kacipo-Bale Suri koe 2777 koe + language Kadai kzd 2778 kzd + language Kadar kej 2779 kej + language Kadaru kdu 2780 kdu + language Kadazan Dusun dtp 2781 dtp + language Kadiwéu kbc 2782 kbc + language Kadu zkd 2783 zkd + language Kadung dkg 2784 dkg + language Kaduo ktp 2785 ktp + language Kaeku kkq 2786 kkq + language Kaera jka 2787 jka + language Kafa kbr 2788 kbr + language Kafoa kpu 2789 kpu + language Kagan Kalagan kll 2790 kll + language Kagate syw 2791 syw + language Kagayanen cgc 2792 cgc + language Kagoma kdm 2793 kdm + language Kagoro xkg 2794 xkg + language Kagulu kki 2795 kki + language Kahe hka 2796 hka + language Kahua agw 2797 agw + language Kaian kct 2798 kct + language Kaibobo kzb 2799 kzb + language Kaidipang kzp 2800 kzp + language Kaiep kbw 2801 kbw + language Kaikadi kep 2802 kep + language Kaikavian Literary Language kjv 2803 kjv + language Kaike kzq 2804 kzq + language Kaimbulawa zka 2805 zka + language Kaimbé xai 2806 xai + language Kaingang kgp 2807 kgp + language Kairak ckr 2808 ckr + language Kairiru kxa 2809 kxa + language Kairui-Midiki krd 2810 krd + language Kais kzm 2811 kzm + language Kaitag xdq 2812 xdq + language Kaivi kce 2813 kce + language Kaiwá kgk 2814 kgk + language Kaiy tcq 2815 tcq + language Kajakse ckq 2816 ckq + language Kajali xkj 2817 xkj + language Kajaman kag 2818 kag + language Kakabai kqf 2819 kqf + language Kakabe kke 2820 kke + language Kakanda kka 2821 kka + language Kaki Ae tbd 2822 tbd + language Kako kkj 2823 kkj + language Kakwa keo 2824 keo + language Kala Lagaw Ya mwp 2825 mwp + language Kalaamaya lkm 2826 lkm + language Kalabakan kve 2827 kve + language Kalabari ijn 2828 ijn + language Kalabra kzz 2829 kzz + language Kalagan kqe 2830 kqe + language Kalaktang Monpa kkf 2831 kkf + language Kalam kmh 2832 kmh + language Kalamsé knz 2833 knz + language Kalanadi wkl 2834 wkl + language Kalanga kck 2835 kck + language Kalanguya kak 2836 kak + language Kalao kly 2837 kly + language Kalapuya kyl 2838 kyl + language Kalarko kba 2839 kba + language Kalasha kls 2840 kls + language Kalenjin kln 2841 kln + language Kalispel-Pend d'Oreille fla 2842 fla + language Kalkoti xka 2843 xka + language Kalkutung ktg 2844 ktg + language Kalmyk xal 2845 xal + language Kalo Finnish Romani rmf 2846 rmf + language Kalou ywa 2847 ywa + language Kaluli bco 2848 bco + language Kalumpang kli 2849 kli + language Kam kdx 2850 kdx + language Kamakan vkm 2851 vkm + language Kamang woi 2852 woi + language Kamano kbq 2853 kbq + language Kamantan kci 2854 kci + language Kamar keq 2855 keq + language Kamara jmr 2856 jmr + language Kamarian kzx 2857 kzx + language Kamaru kgx 2858 kgx + language Kamas xas 2859 xas + language Kamasa klp 2860 klp + language Kamasau kms 2861 kms + language Kamayo kyk 2862 kyk + language Kamayurá kay 2863 kay + language Kamba (Kenya) kam 2864 kam + language Kambaata ktb 2865 ktb + language Kambaira kyy 2866 kyy + language Kambera xbr 2867 xbr + language Kamberau irx 2868 irx + language Kambiwá xbw 2869 xbw + language Kami (Nigeria) kmi 2870 kmi + language Kami (Tanzania) kcu 2871 kcu + language Kamo kcq 2872 kcq + language Kamoro kgq 2873 kgq + language Kamu xmu 2874 xmu + language Kamula xla 2875 xla + language Kamviri xvi 2876 xvi + language Kamwe hig 2877 hig + language Kanakanabu xnb 2878 xnb + language Kanamarí knm 2879 knm + language Kanan zkn 2880 zkn + language Kanashi xns 2881 xns + language Kanasi soq 2882 soq + language Kanauji bjj 2883 bjj + language Kandas kqw 2884 kqw + language Kandawo gam 2885 gam + language Kande kbs 2886 kbs + language Kanembu kbl 2887 kbl + language Kang kyp 2888 kyp + language Kanga kcp 2889 kcp + language Kangean kkv 2890 kkv + language Kanggape igm 2891 igm + language Kangjia kxs 2892 kxs + language Kango (Bas-Uélé District) kty 2893 kty + language Kango (Tshopo District) kzy 2894 kzy + language Kangri xnr 2895 xnr + language Kaniet ktk 2896 ktk + language Kanikkaran kev 2897 kev + language Kaningdon-Nindem kdp 2898 kdp + language Kaningi kzo 2899 kzo + language Kaningra knr 2900 knr + language Kaninuwa wat 2901 wat + language Kanite kmu 2902 kmu + language Kanjari kft 2903 kft + language Kanju kbe 2904 kbe + language Kankanaey kne 2905 kne + language Kannada kan 2906 kan kn + language Kannada Kurumba kfi 2907 kfi + language Kanowit-Tanjong Melanau kxn 2908 kxn + language Kanoé kxo 2909 kxo + language Kansa ksk 2910 ksk + language Kantosi xkt 2911 xkt + language Kanu khx 2912 khx + language Kanufi kni 2913 kni + language Kanuri kau 2914 kau kr + language Kanyok kny 2915 kny + language Kao kax 2916 kax + language Kaonde kqn 2917 kqn + language Kap ykm 2918 ykm + language Kapin tbx 2919 tbx + language Kapinawá xpn 2920 xpn + language Kapingamarangi kpg 2921 kpg + language Kapori khp 2922 khp + language Kapriman dju 2923 dju + language Kaptiau kbi 2924 kbi + language Kapya klo 2925 klo + language Kaqchikel cak 2926 cak + language Kara (Central African Republic) kah 2927 kah + language Kara (Korea) zra 2928 zra + language Kara (Papua New Guinea) leu 2929 leu + language Kara (Tanzania) reg 2930 reg + language Kara-Kalpak kaa 2931 kaa + language Karachay-Balkar krc 2932 krc + language Karagas kim 2933 kim + language Karaim kdr 2934 kdr + language Karajarri gbd 2935 gbd + language Karajá kpj 2936 kpj + language Karakhanid xqa 2937 xqa + language Karami xar 2938 xar + language Karamojong kdj 2939 kdj + language Karang kzr 2940 kzr + language Karanga kth 2941 kth + language Karankawa zkk 2942 zkk + language Karao kyj 2943 kyj + language Karas kgv 2944 kgv + language Karata kpt 2945 kpt + language Karawa xrw 2946 xrw + language Karbi mjw 2947 mjw + language Kare (Central African Republic) kbn 2948 kbn + language Kare (Papua New Guinea) kmf 2949 kmf + language Karekare kai 2950 kai + language Karelian krl 2951 krl + language Karenggapa eaa 2952 eaa + language Karey kyd 2953 kyd + language Kari kbj 2954 kbj + language Karingani kgn 2955 kgn + language Karipuna kuq 2956 kuq + language Karipúna Creole French kmv 2957 kmv + language Karirí-Xocó kzw 2958 kzw + language Karitiâna ktn 2959 ktn + language Kariya kil 2960 kil + language Kariyarra vka 2961 vka + language Karkar-Yuri yuj 2962 yuj + language Karkin krb 2963 krb + language Karko kko 2964 kko + language Karnai bbv 2965 bbv + language Karo (Brazil) arr 2966 arr + language Karo (Ethiopia) kxh 2967 kxh + language Karok kyh 2968 kyh + language Karon krx 2969 krx + language Karon Dori kgw 2970 kgw + language Karore xkx 2971 xkx + language Karuwali rxw 2972 rxw + language Kasanga ccj 2973 ccj + language Kasem xsm 2974 xsm + language Kashaya kju 2975 kju + language Kashmiri kas 2976 kas ks + language Kashubian csb 2977 csb + language Kasiguranin ksn 2978 ksn + language Kaska kkz 2979 kkz + language Kaskean zsk 2980 zsk + language Kasua khs 2981 khs + language Katabaga ktq 2982 ktq + language Katawixi xat 2983 xat + language Katbol tmb 2984 tmb + language Katcha-Kadugli-Miri xtc 2985 xtc + language Kathoriya Tharu tkt 2986 tkt + language Kathu ykt 2987 ykt + language Kati bsh 2988 bsh + language Katkari kfu 2989 kfu + language Katla kcr 2990 kcr + language Kato ktw 2991 ktw + language Katso kaf 2992 kaf + language Katua kta 2993 kta + language Katukína kav 2994 kav + language Kaulong pss 2995 pss + language Kaur vkk 2996 vkk + language Kaure bpp 2997 bpp + language Kaurna zku 2998 zku + language Kauwera xau 2999 xau + language Kavalan ckv 3000 ckv + language Kavet krv 3001 krv + language Kawacha kcb 3002 kcb + language Kawaiisu xaw 3003 xaw + language Kawe kgb 3004 kgb + language Kawi kaw 3005 kaw + language Kaxararí ktx 3006 ktx + language Kaxuiâna kbb 3007 kbb + language Kayabí kyz 3008 kyz + language Kayagar kyt 3009 kyt + language Kayan pdu 3010 pdu + language Kayan Mahakam xay 3011 xay + language Kayan River Kayan xkn 3012 xkn + language Kayapó txu 3013 txu + language Kayardild gyd 3014 gyd + language Kayaw kvl 3015 kvl + language Kayeli kzl 3016 kzl + language Kayong kxy 3017 kxy + language Kayort kyv 3018 kyv + language Kaytetye gbb 3019 gbb + language Kayupulau kzu 3020 kzu + language Kazakh kaz 3021 kaz kk + language Kazukuru kzk 3022 kzk + language Ke'o xxk 3023 xxk + language Keak keh 3024 keh + language Keapara khz 3025 khz + language Kedah Malay meo 3026 meo + language Kedang ksx 3027 ksx + language Keder kdy 3028 kdy + language Keerray-Woorroong wkr 3029 wkr + language Kehu khh 3030 khh + language Kei kei 3031 kei + language Keiga kec 3032 kec + language Kein bmh 3033 bmh + language Keiyo eyo 3034 eyo + language Kekchí kek 3035 kek + language Kela (Democratic Republic of Congo) kel 3036 kel + language Kela (Papua New Guinea) kcl 3037 kcl + language Kelabit kzi 3038 kzi + language Kele (Democratic Republic of Congo) khy 3039 khy + language Kele (Papua New Guinea) sbc 3040 sbc + language Keley-I Kallahan ify 3041 ify + language Keliko kbo 3042 kbo + language Kelo xel 3043 xel + language Kelon kyo 3044 kyo + language Kemak kem 3045 kem + language Kembayan xem 3046 xem + language Kemberano bzp 3047 bzp + language Kembra xkw 3048 xkw + language Kemedzung dmo 3049 dmo + language Kemi Sami sjk 3050 sjk + language Kemiehua kfj 3051 kfj + language Kemtuik kmt 3052 kmt + language Kenaboi xbn 3053 xbn + language Kenati gat 3054 gat + language Kendayan knx 3055 knx + language Kendeje klf 3056 klf + language Kendem kvm 3057 kvm + language Kenga kyq 3058 kyq + language Keningau Murut kxi 3059 kxi + language Keninjal knl 3060 knl + language Kensiu kns 3061 kns + language Kenswei Nsei ndb 3062 ndb + language Kenyan Sign Language xki 3063 xki + language Kenyang ken 3064 ken + language Kenyi lke 3065 lke + language Kenzi xnz 3066 xnz + language Keoru-Ahia xeu 3067 xeu + language Kepkiriwát kpn 3068 kpn + language Kepo' kuk 3069 kuk + language Kera ker 3070 ker + language Kerak hhr 3071 hhr + language Kereho xke 3072 xke + language Kerek krk 3073 krk + language Kerewe ked 3074 ked + language Kerewo kxz 3075 kxz + language Kerinci kvr 3076 kvr + language Kesawai xes 3077 xes + language Ket ket 3078 ket + language Ketangalan kae 3079 kae + language Kete kcv 3080 kcv + language Ketengban xte 3081 xte + language Ketum ktt 3082 ktt + language Keyagana kyg 3083 kyg + language Kgalagadi xkv 3084 xkv + language Khah hkh 3085 hkh + language Khakas kjh 3086 kjh + language Khalaj klj 3087 klj + language Khaling klr 3088 klr + language Khamba kbg 3089 kbg + language Khamnigan Mongol ykh 3090 ykh + language Khams Tibetan khg 3091 khg + language Khamti kht 3092 kht + language Khamyang ksu 3093 ksu + language Khana ogo 3094 ogo + language Khandesi khn 3095 khn + language Khanty kca 3096 kca + language Khao xao 3097 xao + language Kharam Naga kfw 3098 kfw + language Kharia khr 3099 khr + language Kharia Thar ksy 3100 ksy + language Khasi kha 3101 kha + language Khayo lko 3102 lko + language Khazar zkz 3103 zkz + language Khe kqg 3104 kqg + language Khehek tlx 3105 tlx + language Khengkha xkf 3106 xkf + language Khetrani xhe 3107 xhe + language Khezha Naga nkh 3108 nkh + language Khiamniungan Naga kix 3109 kix + language Khinalugh kjj 3110 kjj + language Khirwar kwx 3111 kwx + language Khisa kqm 3112 kqm + language Khlula ykl 3113 ykl + language Khmer khm 3114 khm km + language Khmu kjg 3115 kjg + language Kho'ini xkc 3116 xkc + language Khoekhoe naq 3117 naq + language Khoibu Naga nkb 3118 nkb + language Kholok ktc 3119 ktc + language Khorasani Turkish kmz 3120 kmz + language Khorezmian zkh 3121 zkh + language Khotanese kho 3122 kho + language Khowar khw 3123 khw + language Khua xhv 3124 xhv + language Khuen khf 3125 khf + language Khumi Chin cnk 3126 cnk + language Khunsari kfm 3127 kfm + language Khvarshi khv 3128 khv + language Kháng kjm 3129 kjm + language Khün kkh 3130 kkh + language Kibala blv 3131 blv + language Kibet kie 3132 kie + language Kibiri prm 3133 prm + language Kickapoo kic 3134 kic + language Kija gia 3135 gia + language Kikai kzg 3136 kzg + language Kikuyu, Gikuyu kik 3137 kik Kikuyu Gikuyu ki + language Kildin Sami sjd 3138 sjd + language Kilivila kij 3139 kij + language Kiliwa klb 3140 klb + language Kilmeri kih 3141 kih + language Kim kia 3142 kia + language Kim Mun mji 3143 mji + language Kimaama kig 3144 kig + language Kimaragang kqr 3145 kqr + language Kimbu kiv 3146 kiv + language Kimbundu kmb 3147 kmb + language Kimki sbt 3148 sbt + language Kimré kqp 3149 kqp + language Kinabalian cbw 3150 cbw + language Kinalakna kco 3151 kco + language Kinamiging Manobo mkx 3152 mkx + language Kinaray-A krj 3153 krj + language Kinga zga 3154 zga + language Kinnauri kfk 3155 kfk + language Kintaq knq 3156 knq + language Kinuku kkd 3157 kkd + language Kinyarwanda kin 3158 kin rw + language Kioko ues 3159 ues + language Kiong kkm 3160 kkm + language Kiorr xko 3161 xko + language Kiowa kio 3162 kio + language Kiowa Apache apk 3163 apk + language Kipsigis sgc 3164 sgc + language Kiput kyi 3165 kyi + language Kir-Balar kkr 3166 kkr + language Kire geb 3167 geb + language Kirghiz, Kyrgyz kir 3168 kir Kirghiz Kyrgyz ky + language Kirike okr 3169 okr + language Kirikiri kiy 3170 kiy + language Kirmanjki (individual language) kiu 3171 kiu + language Kirya-Konzəl fkk 3172 fkk + language Kis kis 3173 kis + language Kisa lks 3174 lks + language Kisan xis 3175 xis + language Kisankasa kqh 3176 kqh + language Kisar kje 3177 kje + language Kisi kiz 3178 kiz + language Kistane gru 3179 gru + language Kita Maninkakan mwk 3180 mwk + language Kitan zkt 3181 zkt + language Kitsai kii 3182 kii + language Kituba (Congo) mkw 3183 mkw + language Kituba (Democratic Republic of Congo) ktu 3184 ktu + language Kiunum wei 3185 wei + language Kizamani izm 3186 izm + language Kla-Dan lda 3187 lda + language Klamath-Modoc kla 3188 kla + language Klao klu 3189 klu + language Klias River Kadazan kqt 3190 kqt + language Klingon tlh 3191 tlh + language Knaanic czk 3192 czk + language Ko fuj 3193 fuj + language Koalib kib 3194 kib + language Koasati cku 3195 cku + language Koba kpd 3196 kpd + language Kobiana kcj 3197 kcj + language Kobo okc 3198 okc + language Kobol kgu 3199 kgu + language Kobon kpw 3200 kpw + language Koch kdq 3201 kdq + language Kochila Tharu thq 3202 thq + language Koda cdz 3203 cdz + language Kodaku ksz 3204 ksz + language Kodava kfa 3205 kfa + language Kodeoha vko 3206 vko + language Kodi kod 3207 kod + language Kodia kwp 3208 kwp + language Koenoem kcs 3209 kcs + language Kofa kso 3210 kso + language Kofei kpi 3211 kpi + language Kofyar kwl 3212 kwl + language Koguryo zkg 3213 zkg + language Kohin kkx 3214 kkx + language Kohistani Shina plk 3215 plk + language Koho kpm 3216 kpm + language Kohumono bcs 3217 bcs + language Koi kkt 3218 kkt + language Koireng nkd 3219 nkd + language Koitabu kqi 3220 kqi + language Koiwat kxt 3221 kxt + language Kok Borok trp 3222 trp + language Kok-Nar gko 3223 gko + language Kokata ktd 3224 ktd + language Koke kou 3225 kou + language Koki Naga nxk 3226 nxk + language Koko Babangk okg 3227 okg + language Kokoda xod 3228 xod + language Kokola kzn 3229 kzn + language Kokota kkk 3230 kkk + language Kol (Bangladesh) ekl 3231 ekl + language Kol (Cameroon) biw 3232 biw + language Kol (Papua New Guinea) kol 3233 kol + language Kola kvv 3234 kvv + language Kolbila klc 3235 klc + language Kolibugan Subanon skn 3236 skn + language Koluwawa klx 3237 klx + language Kom (Cameroon) bkm 3238 bkm + language Kom (India) kmm 3239 kmm + language Koma kmy 3240 kmy + language Komba kpf 3241 kpf + language Kombai tyn 3242 tyn + language Kombio xbi 3243 xbi + language Komering kge 3244 kge + language Komi kom 3245 kom kv + language Komi-Permyak koi 3246 koi + language Komi-Zyrian kpv 3247 kpv + language Kominimung xoi 3248 xoi + language Komo (Democratic Republic of Congo) kmw 3249 kmw + language Komo (Sudan) xom 3250 xom + language Komodo kvh 3251 kvh + language Kompane kvp 3252 kvp + language Komyandaret kzv 3253 kzv + language Kon Keu kkn 3254 kkn + language Konai kxw 3255 kxw + language Konda knd 3256 knd + language Konda-Dora kfc 3257 kfc + language Koneraw kdw 3258 kdw + language Kongo kon 3259 kon kg + language Konkani (individual language) knn 3260 knn + language Konkani (macrolanguage) kok 3261 kok + language Konkomba xon 3262 xon + language Konni kma 3263 kma + language Kono (Guinea) knu 3264 knu + language Kono (Nigeria) klk 3265 klk + language Kono (Sierra Leone) kno 3266 kno + language Konomala koa 3267 koa + language Konongo kcz 3268 kcz + language Konso kxc 3269 kxc + language Konyak Naga nbe 3270 nbe + language Konyanka Maninka mku 3271 mku + language Konzo koo 3272 koo + language Koongo kng 3273 kng + language Koonzime ozm 3274 ozm + language Koorete kqy 3275 kqy + language Kopar xop 3276 xop + language Kopkaka opk 3277 opk + language Korafe-Yegha kpr 3278 kpr + language Korak koz 3279 koz + language Korana kqz 3280 kqz + language Korandje kcy 3281 kcy + language Korean kor 3282 kor ko + language Korean Sign Language kvk 3283 kvk + language Koreguaje coe 3284 coe + language Koresh-e Rostam okh 3285 okh + language Korku kfq 3286 kfq + language Korlai Creole Portuguese vkp 3287 vkp + language Koro (Côte d'Ivoire) kfo 3288 kfo + language Koro (India) jkr 3289 jkr + language Koro (Papua New Guinea) kxr 3290 kxr + language Koro (Vanuatu) krf 3291 krf + language Koro Nulu vkn 3292 vkn + language Koro Wachi bqv 3293 bqv + language Koro Zuba vkz 3294 vkz + language Koromfé kfz 3295 kfz + language Koromira kqj 3296 kqj + language Koronadal Blaan bpr 3297 bpr + language Koroni xkq 3298 xkq + language Koropó xxr 3299 xxr + language Koroshi ktl 3300 ktl + language Korowai khe 3301 khe + language Korra Koraga kfd 3302 kfd + language Korubo xor 3303 xor + language Korupun-Sela kpq 3304 kpq + language Korwa kfp 3305 kfp + language Koryak kpy 3306 kpy + language Kosadle kiq 3307 kiq + language Kosarek Yale kkl 3308 kkl + language Kosena kze 3309 kze + language Koshin kid 3310 kid + language Kosraean kos 3311 kos + language Kota (Gabon) koq 3312 koq + language Kota (India) kfe 3313 kfe + language Kota Bangun Kutai Malay mqg 3314 mqg + language Kota Marudu Talantang grm 3315 grm + language Kotafon Gbe kqk 3316 kqk + language Kotava avk 3317 avk + language Koti eko 3318 eko + language Kott zko 3319 zko + language Kou snz 3320 snz + language Kouya kyf 3321 kyf + language Kovai kqb 3322 kqb + language Kove kvc 3323 kvc + language Kowaki xow 3324 xow + language Kowiai kwh 3325 kwh + language Koy Sanjaq Surat kqd 3326 kqd + language Koya kff 3327 kff + language Koyaga kga 3328 kga + language Koyo koh 3329 koh + language Koyra Chiini Songhay khq 3330 khq + language Koyraboro Senni Songhai ses 3331 ses + language Koyukon koy 3332 koy + language Kpagua kuw 3333 kuw + language Kpala kpl 3334 kpl + language Kpan kpk 3335 kpk + language Kpasam pbn 3336 pbn + language Kpati koc 3337 koc + language Kpatili kym 3338 kym + language Kpeego cpo 3339 cpo + language Kpelle kpe 3340 kpe + language Kpessi kef 3341 kef + language Kplang kph 3342 kph + language Krache kye 3343 kye + language Krahô xra 3344 xra + language Kraol rka 3345 rka + language Krenak kqq 3346 kqq + language Krevinian zkv 3347 zkv + language Kreye xre 3348 xre + language Kriang ngt 3349 ngt + language Krikati-Timbira xri 3350 xri + language Krio kri 3351 kri + language Kriol rop 3352 rop + language Krisa ksi 3353 ksi + language Krobu kxb 3354 kxb + language Krongo kgo 3355 kgo + language Krung krr 3356 krr + language Krymchak jct 3357 jct + language Kryts kry 3358 kry + language Kua tyu 3359 tyu + language Kua-nsi ykn 3360 ykn + language Kuamasi yku 3361 yku + language Kuan uan 3362 uan + language Kuanhua xnh 3363 xnh + language Kuanua ksd 3364 ksd + language Kuanyama, Kwanyama kua 3365 kua Kuanyama kj Kwanyama + language Kubachi ugh 3366 ugh + language Kube kgf 3367 kgf + language Kubi kof 3368 kof + language Kubo jko 3369 jko + language Kubu kvb 3370 kvb + language Kucong lkc 3371 lkc + language Kudiya kfg 3372 kfg + language Kudmali kyw 3373 kyw + language Kudu-Camo kov 3374 kov + language Kufr Qassem Sign Language (KQSL) sqx 3375 sqx + language Kugama kow 3376 kow + language Kugbo kes 3377 kes + language Kugu-Muminh xmh 3378 xmh + language Kui (India) uki 3379 uki + language Kui (Indonesia) kvd 3380 kvd + language Kuijau dkr 3381 dkr + language Kuikúro-Kalapálo kui 3382 kui + language Kujarge vkj 3383 vkj + language Kuk kfn 3384 kfn + language Kukatja kux 3385 kux + language Kuke ght 3386 ght + language Kukele kez 3387 kez + language Kukna kex 3388 kex + language Kuku ukv 3389 ukv + language Kuku-Mangk xmq 3390 xmq + language Kuku-Mu'inh xmp 3391 xmp + language Kuku-Ugbanh ugb 3392 ugb + language Kuku-Uwanh uwa 3393 uwa + language Kuku-Yalanji gvn 3394 gvn + language Kula tpg 3395 tpg + language Kulere kul 3396 kul + language Kulfa kxj 3397 kxj + language Kulina Pano xpk 3398 xpk + language Kulisusu vkl 3399 vkl + language Kullu Pahari kfx 3400 kfx + language Kulon uon 3401 uon + language Kulung (Nepal) kle 3402 kle + language Kulung (Nigeria) bbu 3403 bbu + language Kumalu ksl 3404 ksl + language Kumam kdi 3405 kdi + language Kuman (Papua New Guinea) kue 3406 kue + language Kuman (Russia) qwm 3407 qwm + language Kumaoni kfy 3408 kfy + language Kumarbhag Paharia kmj 3409 kmj + language Kumba ksm 3410 ksm + language Kumbainggar kgs 3411 kgs + language Kumbaran wkb 3412 wkb + language Kumbewaha xks 3413 xks + language Kumhali kra 3414 kra + language Kumiai dih 3415 dih + language Kumukio kuo 3416 kuo + language Kumyk kum 3417 kum + language Kumzari zum 3418 zum + language Kunama kun 3419 kun + language Kunbarlang wlg 3420 wlg + language Kunda kdn 3421 kdn + language Kundal Shahi shd 3422 shd + language Kunduvadi wku 3423 wku + language Kung kfl 3424 kfl + language Kung-Ekoka knw 3425 knw + language Kungarakany ggk 3426 ggk + language Kungardutyi gdt 3427 gdt + language Kunggari kgl 3428 kgl + language Kungkari lku 3429 lku + language Kuni kse 3430 kse + language Kuni-Boazi kvg 3431 kvg + language Kunigami xug 3432 xug + language Kunimaipa kup 3433 kup + language Kunja pep 3434 pep + language Kunjen kjn 3435 kjn + language Kunyi njx 3436 njx + language Kunza kuz 3437 kuz + language Kuo xuo 3438 xuo + language Kuot kto 3439 kto + language Kupa kug 3440 kug + language Kupang Malay mkn 3441 mkn + language Kupia key 3442 key + language Kupsabiny kpz 3443 kpz + language Kur kuv 3444 kuv + language Kura Ede Nago nqk 3445 nqk + language Kurama krh 3446 krh + language Kuranko knk 3447 knk + language Kurdish kur 3448 kur ku + language Kuri nbn 3449 nbn + language Kuria kuj 3450 kuj + language Kurichiya kfh 3451 kfh + language Kurmukar kfv 3452 kfv + language Kurnai unn 3453 unn + language Kurrama vku 3454 vku + language Kurti ktm 3455 ktm + language Kurtokha xkz 3456 xkz + language Kurudu kjr 3457 kjr + language Kurukh kru 3458 kru + language Kuruáya kyr 3459 kyr + language Kusaal kus 3460 kus + language Kusaghe ksg 3461 ksg + language Kushi kuh 3462 kuh + language Kusu ksv 3463 ksv + language Kusunda kgg 3464 kgg + language Kutenai kut 3465 kut + language Kutep kub 3466 kub + language Kuthant xut 3467 xut + language Kutong skm 3468 skm + language Kutto kpa 3469 kpa + language Kutu kdc 3470 kdc + language Kuturmi khj 3471 khj + language Kuuk Thaayorre thd 3472 thd + language Kuuk-Yak uky 3473 uky + language Kuuku-Ya'u kuy 3474 kuy + language Kuvale olu 3475 olu + language Kuvi kxv 3476 kxv + language Kuwaa blh 3477 blh + language Kuwaataay cwt 3478 cwt + language Kuwema woa 3479 woa + language Kuy kdt 3480 kdt + language Kven Finnish fkv 3481 fkv + language Kw'adza wka 3482 wka + language Kwa kwb 3483 kwb + language Kwa' bko 3484 bko + language Kwaami ksq 3485 ksq + language Kwadi kwz 3486 kwz + language Kwaio kwd 3487 kwd + language Kwaja kdz 3488 kdz + language Kwakiutl kwk 3489 kwk + language Kwakum kwu 3490 kwu + language Kwalhioqua-Tlatskanai qwt 3491 qwt + language Kwama kmq 3492 kmq + language Kwambi kwm 3493 kwm + language Kwamera tnk 3494 tnk + language Kwami ktf 3495 ktf + language Kwamtim One okk 3496 okk + language Kwandu xdo 3497 xdo + language Kwang kvi 3498 kvi + language Kwanga kwj 3499 kwj + language Kwangali kwn 3500 kwn + language Kwanja knp 3501 knp + language Kwara'ae kwf 3502 kwf + language Kwasio nmg 3503 nmg + language Kwaya kya 3504 kya + language Kwaza xwa 3505 xwa + language Kwegu xwg 3506 xwg + language Kwer kwr 3507 kwr + language Kwerba kwe 3508 kwe + language Kwerba Mamberamo xwr 3509 xwr + language Kwere cwe 3510 cwe + language Kwerisa kkb 3511 kkb + language Kwese kws 3512 kws + language Kwesten kwt 3513 kwt + language Kwini gww 3514 gww + language Kwinsu kuc 3515 kuc + language Kwinti kww 3516 kww + language Kwoma kmo 3517 kmo + language Kwomtari kwo 3518 kwo + language Kxoe xuu 3519 xuu + language Kyak bka 3520 bka + language Kyaka kyc 3521 kyc + language Kyan-Karyaw Naga nqq 3522 nqq + language Kyanga tye 3523 tye + language Kyenele kql 3524 kql + language Kyerung kgy 3525 kgy + language Kâte kmg 3526 kmg + language Kélé keb 3527 keb + language Kölsch ksh 3528 ksh + language Kɛlɛngaxo Bozo bzx 3529 bzx + language La'bi lbi 3530 lbi + language Laal gdm 3531 gdm + language Laari ldi 3532 ldi + language Laarim loh 3533 loh + language Laba lau 3534 lau + language Label lbb 3535 lbb + language Labir jku 3536 jku + language Labo mwi 3537 mwi + language Labo Phowa ypb 3538 ypb + language Labu lbu 3539 lbu + language Labuk-Kinabatangan Kadazan dtb 3540 dtb + language Lacandon lac 3541 lac + language Lachi lbt 3542 lbt + language Lachiguiri Zapotec zpa 3543 zpa + language Lachixío Zapotec zpl 3544 zpl + language Ladakhi lbj 3545 lbj + language Ladin lld 3546 lld + language Ladino lad 3547 lad + language Ladji Ladji llj 3548 llj + language Laeko-Libuat lkl 3549 lkl + language Lafofa laf 3550 laf + language Laghu lgb 3551 lgb + language Laghuu lgh 3552 lgh + language Lagwan kot 3553 kot + language Laha (Indonesia) lhh 3554 lhh + language Laha (Viet Nam) lha 3555 lha + language Lahanan lhn 3556 lhn + language Lahnda lah 3557 lah + language Lahta Karen kvt 3558 kvt + language Lahu lhu 3559 lhu + language Lahu Shi lhi 3560 lhi + language Lahul Lohar lhl 3561 lhl + language Laimbue lmx 3562 lmx + language Laitu Chin clj 3563 clj + language Laiyolo lji 3564 lji + language Lak lbe 3565 lbe + language Laka (Chad) lap 3566 lap + language Lakalei lka 3567 lka + language Lake Miwok lmw 3568 lmw + language Lakha lkh 3569 lkh + language Laki lki 3570 lki + language Lakkia lbc 3571 lbc + language Lakon lkn 3572 lkn + language Lakondê lkd 3573 lkd + language Lakota lkt 3574 lkt + language Lakota Dida dic 3575 dic + language Lakurumau lxm 3576 lxm + language Lala nrz 3577 nrz + language Lala-Bisa leb 3578 leb + language Lala-Roba lla 3579 lla + language Lalana Chinantec cnl 3580 cnl + language Lalia lal 3581 lal + language Lama (Togo) las 3582 las + language Lama Bai lay 3583 lay + language Lamaholot slp 3584 slp + language Lamalama lby 3585 lby + language Lamalera lmr 3586 lmr + language Lamang hia 3587 hia + language Lamatuka lmq 3588 lmq + language Lamba lam 3589 lam + language Lambadi lmn 3590 lmn + language Lambayeque Quechua quf 3591 quf + language Lambichhong lmh 3592 lmh + language Lamboya lmy 3593 lmy + language Lambya lai 3594 lai + language Lame bma 3595 bma + language Lamenu lmu 3596 lmu + language Lamja-Dengsa-Tola ldh 3597 ldh + language Lamkang lmk 3598 lmk + language Lamma lev 3599 lev + language Lamnso' lns 3600 lns + language Lamogai lmg 3601 lmg + language Lampung Api ljp 3602 ljp + language Lampung Nyo abl 3603 abl + language Lamu llh 3604 llh + language Lanas Lobu ruu 3605 ruu + language Landoma ldm 3606 ldm + language Lang'e yne 3607 yne + language Langam lnm 3608 lnm + language Langbashe lna 3609 lna + language Langnian Buyang yln 3610 yln + language Lango (South Sudan) lgo 3611 lgo + language Lango (Uganda) laj 3612 laj + language Langobardic lng 3613 lng + language Langue des signes de Belgique Francophone sfb 3614 sfb + language Lanima lnw 3615 lnw + language Lanoh lnh 3616 lnh + language Lao lao 3617 lao lo + language Lao Naga nlq 3618 nlq + language Laomian lwm 3619 lwm + language Laopang lbg 3620 lbg + language Laos Sign Language lso 3621 lso + language Lapaguía-Guivini Zapotec ztl 3622 ztl + language Laragia lrg 3623 lrg + language Larantuka Malay lrt 3624 lrt + language Lardil lbz 3625 lbz + language Larevat lrv 3626 lrv + language Large Flowery Miao hmd 3627 hmd + language Lari lrl 3628 lrl + language Larike-Wakasihu alo 3629 alo + language Laro lro 3630 lro + language Larteh lar 3631 lar + language Laru lan 3632 lan + language Las Delicias Zapotec zcd 3633 zcd + language Lasalimu llm 3634 llm + language Lasgerdi lsa 3635 lsa + language Lashi lsi 3636 lsi + language Lasi lss 3637 lss + language Late Middle Chinese ltc 3638 ltc + language Latgalian ltg 3639 ltg + language Latin lat 3640 lat la + language Latu ltu 3641 ltu + language Latundê ltn 3642 ltn + language Latvian lav 3643 lav lv + language Latvian Sign Language lsl 3644 lsl + language Lau llu 3645 llu + language Laua luf 3646 luf + language Lauan llx 3647 llx + language Lauje law 3648 law + language Laura lur 3649 lur + language Laurentian lre 3650 lre + language Lautu Chin clt 3651 clt + language Lavatbura-Lamusong lbv 3652 lbv + language Laven lbo 3653 lbo + language Lavi lvi 3654 lvi + language Lavukaleve lvk 3655 lvk + language Lawangan lbx 3656 lbx + language Lawu lwu 3657 lwu + language Lawunuia tgi 3658 tgi + language Layakha lya 3659 lya + language Laz lzz 3660 lzz + language Lealao Chinantec cle 3661 cle + language Leco lec 3662 lec + language Ledo Kaili lew 3663 lew + language Leelau ldk 3664 ldk + language Lefa lfa 3665 lfa + language Lega-Mwenga lgm 3666 lgm + language Lega-Shabunda lea 3667 lea + language Legbo agb 3668 agb + language Legenyem lcc 3669 lcc + language Lehali tql 3670 tql + language Lehalurup urr 3671 urr + language Lehar cae 3672 cae + language Leinong Naga lzn 3673 lzn + language Leipon lek 3674 lek + language Lelak llk 3675 llk + language Lele (Chad) lln 3676 lln + language Lele (Democratic Republic of Congo) lel 3677 lel + language Lele (Guinea) llc 3678 llc + language Lele (Papua New Guinea) lle 3679 lle + language Lelemi lef 3680 lef + language Lelepa lpa 3681 lpa + language Lembena leq 3682 leq + language Lemerig lrz 3683 lrz + language Lemio lei 3684 lei + language Lemnian xle 3685 xle + language Lemolang ley 3686 ley + language Lemoro ldj 3687 ldj + language Lenakel tnl 3688 tnl + language Lenca len 3689 len + language Lendu led 3690 led + language Lengilu lgi 3691 lgi + language Lengo lgr 3692 lgr + language Lengola lej 3693 lej + language Leningitij lnj 3694 lnj + language Lenje leh 3695 leh + language Lenkau ler 3696 ler + language Lenyima ldg 3697 ldg + language Lepcha lep 3698 lep + language Lepki lpe 3699 lpe + language Lepontic xlp 3700 xlp + language Lere gnh 3701 gnh + language Lese les 3702 les + language Lesing-Gelimi let 3703 let + language Letemboi nms 3704 nms + language Leti (Cameroon) leo 3705 leo + language Leti (Indonesia) lti 3706 lti + language Levantine Arabic apc 3707 apc + language Levuka lvu 3708 lvu + language Lewo lww 3709 lww + language Lewo Eleng lwe 3710 lwe + language Lewotobi lwt 3711 lwt + language Leyigha ayi 3712 ayi + language Lezghian lez 3713 lez + language Lhokpu lhp 3714 lhp + language Lhomi lhm 3715 lhm + language Li'o ljl 3716 ljl + language Liabuku lix 3717 lix + language Liana-Seti ste 3718 ste + language Liangmai Naga njn 3719 njn + language Lianshan Zhuang zln 3720 zln + language Liberia Kpelle xpe 3721 xpe + language Liberian English lir 3722 lir + language Libido liq 3723 liq + language Libinza liz 3724 liz + language Libon Bikol lbl 3725 lbl + language Liburnian xli 3726 xli + language Libyan Arabic ayl 3727 ayl + language Libyan Sign Language lbs 3728 lbs + language Lidzonka add 3729 add + language Ligbi lig 3730 lig + language Ligenza lgz 3731 lgz + language Ligurian lij 3732 lij + language Ligurian (Ancient) xlg 3733 xlg + language Lihir lih 3734 lih + language Lijili mgi 3735 mgi + language Lika lik 3736 lik + language Liki lio 3737 lio + language Likila lie 3738 lie + language Likuba kxx 3739 kxx + language Likum lib 3740 lib + language Likwala kwc 3741 kwc + language Lilau lll 3742 lll + language Lillooet lil 3743 lil + language Limassa bme 3744 bme + language Limbu lif 3745 lif + language Limbum lmp 3746 lmp + language Limburgan, Limburger, Limburgish lim 3747 lim Limburgan Limburger Limburgish li + language Limi ylm 3748 ylm + language Limilngan lmc 3749 lmc + language Limos Kalinga kmk 3750 kmk + language Linear A lab 3751 lab + language Lingala lin 3752 lin ln + language Lingao onb 3753 onb + language Lingarak lgk 3754 lgk + language Lingua Franca pml 3755 pml + language Lingua Franca Nova lfn 3756 lfn + language Lipan Apache apl 3757 apl + language Lipo lpo 3758 lpo + language Lisabata-Nuniali lcs 3759 lcs + language Lisela lcl 3760 lcl + language Lish lsh 3761 lsh + language Lishana Deni lsd 3762 lsd + language Lishanid Noshan aij 3763 aij + language Lishán Didán trg 3764 trg + language Lisu lis 3765 lis + language Literary Chinese lzh 3766 lzh + language Lithuanian lit 3767 lit lt + language Lithuanian Sign Language lls 3768 lls + language Litzlitz lzl 3769 lzl + language Liujiang Zhuang zlj 3770 zlj + language Liuqian Zhuang zlq 3771 zlq + language Liv liv 3772 liv + language Livvi olo 3773 olo + language Lo-Toga lht 3774 lht + language Loarki lrk 3775 lrk + language Lobala loq 3776 loq + language Lobi lob 3777 lob + language Lodhi lbm 3778 lbm + language Logba lgq 3779 lgq + language Logir lqr 3780 lqr + language Logo log 3781 log + language Logol lof 3782 lof + language Logooli rag 3783 rag + language Logorik liu 3784 liu + language Logudorese Sardinian, Croatian hrv 3785 hrv Logudorese Sardinian src hr Croatian + language Lohorung lbr 3786 lbr + language Loja Highland Quichua qvj 3787 qvj + language Lojban jbo 3788 jbo + language Lokaa yaz 3789 yaz + language Loke loy 3790 loy + language Loko lok 3791 lok + language Lokoya lky 3792 lky + language Lola lcd 3793 lcd + language Lolak llq 3794 llq + language Lole llg 3795 llg + language Lolo llb 3796 llb + language Loloda loa 3797 loa + language Lolopo ycl 3798 ycl + language Loma (Côte d'Ivoire) loi 3799 loi + language Loma (Liberia) lom 3800 lom + language Lomaiviti lmv 3801 lmv + language Lomavren rmi 3802 rmi + language Lombard lmo 3803 lmo + language Lombi lmi 3804 lmi + language Lombo loo 3805 loo + language Lomwe ngl 3806 ngl + language Loncong lce 3807 lce + language Long Phuri Naga lpn 3808 lpn + language Long Wat ttw 3809 ttw + language Longgu lgu 3810 lgu + language Longto wok 3811 wok + language Longuda lnu 3812 lnu + language Loniu los 3813 los + language Lonwolwol crc 3814 crc + language Lonzo lnz 3815 lnz + language Loo ldo 3816 ldo + language Lopa lop 3817 lop + language Lopi lov 3818 lov + language Lopit lpx 3819 lpx + language Lorang lrn 3820 lrn + language Lorediakarkar lnn 3821 lnn + language Loreto-Ucayali Spanish spq 3822 spq + language Lote uvl 3823 uvl + language Lotha Naga njh 3824 njh + language Lotud dtr 3825 dtr + language Lou loj 3826 loj + language Louisiana Creole lou 3827 lou + language Loun lox 3828 lox + language Loup A xlo 3829 xlo + language Loup B xlb 3830 xlb + language Low German nds 3831 nds + language Lower Burdekin xbb 3832 xbb + language Lower Chehalis cea 3833 cea + language Lower Grand Valley Dani dni 3834 dni + language Lower Nossob nsb 3835 nsb + language Lower Silesian sli 3836 sli + language Lower Sorbian dsb 3837 dsb + language Lower Southern Aranda axl 3838 axl + language Lower Ta'oih tto 3839 tto + language Lower Tanana taa 3840 taa + language Lowland Oaxaca Chontal clo 3841 clo + language Lowland Tarahumara tac 3842 tac + language Loxicha Zapotec ztp 3843 ztp + language Lozi loz 3844 loz + language Luang lex 3845 lex + language Luba-Katanga lub 3846 lub lu + language Luba-Lulua lua 3847 lua + language Lubila kcc 3848 kcc + language Lubu lcf 3849 lcf + language Lubuagan Kalinga knb 3850 knb + language Luchazi lch 3851 lch + language Lucumi luq 3852 luq + language Ludian lud 3853 lud + language Lufu ldq 3854 ldq + language Lugbara lgg 3855 lgg + language Luguru ruf 3856 ruf + language Luhu lcq 3857 lcq + language Luimbi lum 3858 lum + language Luiseno lui 3859 lui + language Lukpa dop 3860 dop + language Lule ule 3861 ule + language Lule Sami smj 3862 smj + language Lumba-Yakkha luu 3863 luu + language Lumbu lup 3864 lup + language Lumun lmd 3865 lmd + language Luna luj 3866 luj + language Lunanakha luk 3867 luk + language Lunda lun 3868 lun + language Lundayeh lnd 3869 lnd + language Lungalunga vmg 3870 vmg + language Lungga lga 3871 lga + language Luo (Cameroon) luw 3872 luw + language Luo (Kenya and Tanzania) luo 3873 luo + language Luopohe Hmong hml 3874 hml + language Luri ldd 3875 ldd + language Lusengo lse 3876 lse + language Lushai lus 3877 lus + language Lushootseed lut 3878 lut + language Lusi khl 3879 khl + language Lusitanian xls 3880 xls + language Lutos ndy 3881 ndy + language Luvale lue 3882 lue + language Luwati luv 3883 luv + language Luwo lwo 3884 lwo + language Luxembourgish, Letzeburgesch ltz 3885 ltz Luxembourgish lb Letzeburgesch + language Luyana lyn 3886 lyn + language Luyia luy 3887 luy + language Lwalu lwa 3888 lwa + language Lwel lvl 3889 lvl + language Lycian xlc 3890 xlc + language Lydian xld 3891 xld + language Lyngngam lyg 3892 lyg + language Lyélé lee 3893 lee + language Láadan ldn 3894 ldn + language Láá Láá Bwamu bwj 3895 bwj + language Lü khb 3896 khb + language Ma (Democratic Republic of Congo) msj 3897 msj + language Ma (Papua New Guinea) mjn 3898 mjn + language Ma Manda skc 3899 skc + language Ma'anyan mhy 3900 mhy + language Ma'di mhi 3901 mhi + language Ma'ya slz 3902 slz + language Maa cma 3903 cma + language Maaka mew 3904 mew + language Maasina Fulfulde ffm 3905 ffm + language Maay ymm 3906 ymm + language Maba (Chad) mde 3907 mde + language Maba (Indonesia) mqa 3908 mqa + language Mabaale mmz 3909 mmz + language Mabaan mfz 3910 mfz + language Mabaka Valley Kalinga kkg 3911 kkg + language Mabire muj 3912 muj + language Maca mca 3913 mca + language Macaguaje mcl 3914 mcl + language Macaguán mbn 3915 mbn + language Macanese mzs 3916 mzs + language Macedo-Romanian rup 3917 rup + language Macedonian mkd 3918 mkd mac mk + language Machame jmc 3919 jmc + language Machiguenga mcb 3920 mcb + language Machinere mpd 3921 mpd + language Machinga mvw 3922 mvw + language Maco wpc 3923 wpc + language Macuna myy 3924 myy + language Macushi mbc 3925 mbc + language Mada (Cameroon) mxu 3926 mxu + language Mada (Nigeria) mda 3927 mda + language Madagascar Sign Language mzc 3928 mzc + language Madak mmx 3929 mmx + language Madhi Madhi dmd 3930 dmd + language Madi grg 3931 grg + language Madurese mad 3932 mad + language Mae mme 3933 mme + language Maek hmk 3934 hmk + language Maeng Itneg itt 3935 itt + language Mafa maf 3936 maf + language Mafea mkv 3937 mkv + language Mag-Indi Ayta blx 3938 blx + language Mag-antsi Ayta sgb 3939 sgb + language Magahi mag 3940 mag + language Magbukun Ayta ayt 3941 ayt + language Magdalena Peñasco Mixtec xtm 3942 xtm + language Magoma gmx 3943 gmx + language Magori zgr 3944 zgr + language Maguindanaon mdh 3945 mdh + language Magɨ (Madang Province) gkd 3946 gkd + language Magɨyi gmg 3947 gmg + language Mahali mjx 3948 mjx + language Mahasu Pahari bfz 3949 bfz + language Mahican mjy 3950 mjy + language Mahongwe mhb 3951 mhb + language Mahou mxx 3952 mxx + language Mai Brat ayz 3953 ayz + language Maia sks 3954 sks + language Maiadomu mzz 3955 mzz + language Maiani tnh 3956 tnh + language Maii mmm 3957 mmm + language Mailu mgu 3958 mgu + language Maindo cwb 3959 cwb + language Mainfränkisch vmf 3960 vmf + language Mainstream Kenyah xkl 3961 xkl + language Mairasi zrs 3962 zrs + language Maisin mbq 3963 mbq + language Maithili mai 3964 mai + language Maiwa (Indonesia) wmm 3965 wmm + language Maiwa (Papua New Guinea) mti 3966 mti + language Maiwala mum 3967 mum + language Majang mpe 3968 mpe + language Majera xmj 3969 xmj + language Majhi mjz 3970 mjz + language Majhwar mmj 3971 mmj + language Majukayang Kalinga kmd 3972 kmd + language Mak (China) mkg 3973 mkg + language Mak (Nigeria) pbl 3974 pbl + language Makaa mcp 3975 mcp + language Makah myh 3976 myh + language Makalero mjb 3977 mjb + language Makasae mkz 3978 mkz + language Makasar mak 3979 mak + language Makassar Malay mfp 3980 mfp + language Makayam aup 3981 aup + language Makhuwa vmw 3982 vmw + language Makhuwa-Marrevone xmc 3983 xmc + language Makhuwa-Meetto mgh 3984 mgh + language Makhuwa-Moniga mhm 3985 mhm + language Makhuwa-Saka xsq 3986 xsq + language Makhuwa-Shirima vmk 3987 vmk + language Maklew mgf 3988 mgf + language Makolkol zmh 3989 zmh + language Makonde kde 3990 kde + language Maku'a lva 3991 lva + language Makuri Naga jmn 3992 jmn + language Makuráp mpu 3993 mpu + language Makwe ymk 3994 ymk + language Makyan Naga umn 3995 umn + language Mal mlf 3996 mlf + language Mal Paharia mkb 3997 mkb + language Mala (Nigeria) ruy 3998 ruy + language Mala (Papua New Guinea) ped 3999 ped + language Mala Malasar ima 4000 ima + language Malaccan Creole Malay ccm 4001 ccm + language Malaccan Creole Portuguese mcm 4002 mcm + language Malagasy mlg 4003 mlg mg + language Malak Malak mpb 4004 mpb + language Malalamai mmt 4005 mmt + language Malango mln 4006 mln + language Malankuravan mjo 4007 mjo + language Malapandaram mjp 4008 mjp + language Malaryan mjq 4009 mjq + language Malas mkr 4010 mkr + language Malasar ymr 4011 ymr + language Malavedan mjr 4012 mjr + language Malawi Lomwe lon 4013 lon + language Malawi Sena swk 4014 swk + language Malawian Sign Language lws 4015 lws + language Malay (individual language) zlm 4016 zlm + language Malay, Malay (macrolanguage) msa 4017 msa may ms Malay Malay (macrolanguage) + language Malayalam mal 4018 mal ml + language Malayic Dayak xdy 4019 xdy + language Malaynon mlz 4020 mlz + language Malayo mbp 4021 mbp + language Malaysian Sign Language xml 4022 xml + language Malba Birifor bfo 4023 bfo + language Maldivian, Dhivehi, Divehi div 4024 div dv Maldivian Dhivehi Divehi + language Male (Ethiopia) mdy 4025 mdy + language Male (Papua New Guinea) mdc 4026 mdc + language Malecite-Passamaquoddy pqm 4027 pqm + language Maleng pkt 4028 pkt + language Maleu-Kilenge mgl 4029 mgl + language Malfaxal mlx 4030 mlx + language Malgana vml 4031 vml + language Malgbe mxf 4032 mxf + language Mali gcc 4033 gcc + language Malila mgq 4034 mgq + language Malimba mzd 4035 mzd + language Malimpung mli 4036 mli + language Malinaltepec Me'phaa tcf 4037 tcf + language Malo mla 4038 mla + language Malol mbk 4039 mbk + language Maltese mlt 4040 mlt mt + language Maltese Sign Language mdl 4041 mdl + language Malua Bay mll 4042 mll + language Malvi mup 4043 mup + language Malyangapa yga 4044 yga + language Maléku Jaíka gut 4045 gut + language Mam mam 4046 mam + language Mama mma 4047 mma + language Mamaa mhf 4048 mhf + language Mamaindé wmd 4049 wmd + language Mamanwa mmn 4050 mmn + language Mamara Senoufo myk 4051 myk + language Mamasa mqj 4052 mqj + language Mambae mgm 4053 mgm + language Mambai mcs 4054 mcs + language Mamboru mvd 4055 mvd + language Mambwe-Lungu mgr 4056 mgr + language Mampruli maw 4057 maw + language Mamuju mqx 4058 mqx + language Mamulique emm 4059 emm + language Mamusi kdf 4060 kdf + language Mamvu mdi 4061 mdi + language Man Met mml 4062 mml + language Manado Malay xmm 4063 xmm + language Manam mva 4064 mva + language Manambu mle 4065 mle + language Manangba nmm 4066 nmm + language Manangkari znk 4067 znk + language Manchu mnc 4068 mnc + language Manda (Australia) zma 4069 zma + language Manda (India) mha 4070 mha + language Manda (Tanzania) mgs 4071 mgs + language Mandahuaca mht 4072 mht + language Mandaic mid 4073 mid + language Mandan mhq 4074 mhq + language Mandandanyi zmk 4075 zmk + language Mandar mdr 4076 mdr + language Mandara tbf 4077 tbf + language Mandari mqu 4078 mqu + language Mandarin Chinese cmn 4079 cmn + language Mandaya mry 4080 mry + language Mandeali mjl 4081 mjl + language Mander mqr 4082 mqr + language Mandingo man 4083 man + language Mandinka mnk 4084 mnk + language Mandjak mfv 4085 mfv + language Mandobo Atas aax 4086 aax + language Mandobo Bawah bwp 4087 bwp + language Manem jet 4088 jet + language Mang zng 4089 zng + language Manga Kanuri kby 4090 kby + language Mangala mem 4091 mem + language Mangareva mrv 4092 mrv + language Mangarrayi mpc 4093 mpc + language Mangas zns 4094 zns + language Mangayat myj 4095 myj + language Mangbetu mdj 4096 mdj + language Mangbutu mdk 4097 mdk + language Mangerr zme 4098 zme + language Mangga Buang mmo 4099 mmo + language Manggarai mqy 4100 mqy + language Mango mge 4101 mge + language Mangole mqc 4102 mqc + language Mangseng mbh 4103 mbh + language Mangue mom 4104 mom + language Manichaean Middle Persian xmn 4105 xmn + language Manide abd 4106 abd + language Manikion mnx 4107 mnx + language Manipa mqp 4108 mqp + language Manipuri mni 4109 mni + language Mankanya knf 4110 knf + language Mankiyali nlm 4111 nlm + language Manna-Dora mju 4112 mju + language Mannan mjv 4113 mjv + language Mano mev 4114 mev + language Manombai woo 4115 woo + language Mansaka msk 4116 msk + language Mansi mns 4117 mns + language Mansoanka msw 4118 msw + language Manta myg 4119 myg + language Mantsi nty 4120 nty + language Manumanaw Karen kxf 4121 kxf + language Manx glv 4122 glv gv + language Manya mzj 4123 mzj + language Manyawa mny 4124 mny + language Manyika mxc 4125 mxc + language Manza mzv 4126 mzv + language Mao Naga nbi 4127 nbi + language Maonan mmd 4128 mmd + language Maore Comorian swb 4129 swb + language Mape mlh 4130 mlh + language Mapena mnm 4131 mnm + language Mapia mpy 4132 mpy + language Mapidian mpw 4133 mpw + language Mapos Buang bzh 4134 bzh + language Mapoyo mcg 4135 mcg + language Mapudungun arn 4136 arn + language Mapun sjm 4137 sjm + language Maquiritari mch 4138 mch + language Mara Chin mrh 4139 mrh + language Marachi lri 4140 lri + language Maraghei vmh 4141 vmh + language Maragus mrs 4142 mrs + language Maram Naga nma 4143 nma + language Marama lrm 4144 lrm + language Maranao mrw 4145 mrw + language Maranunggu zmr 4146 zmr + language Mararit mgb 4147 mgb + language Marathi, Marathi (Marāṭhī) mar 4148 mar mr Marathi Marathi (Marāṭhī) + language Marau mvr 4149 mvr + language Marba mpg 4150 mpg + language Mardin Sign Language dsz 4151 dsz + language Maremgi mrx 4152 mrx + language Marenje vmr 4153 vmr + language Marfa mvu 4154 mvu + language Margany zmc 4155 zmc + language Marghi Central mrt 4156 mrt + language Marghi South mfm 4157 mfm + language Margos-Yarowilca-Lauricocha Quechua qvm 4158 qvm + language Margu mhg 4159 mhg + language Mari (East Sepik Province) mbx 4160 mbx + language Mari (Madang Province) hob 4161 hob + language Mari (Russia) chm 4162 chm + language Maria (India) mrr 4163 mrr + language Maria (Papua New Guinea) mds 4164 mds + language Maricopa mrc 4165 mrc + language Maridan zmd 4166 zmd + language Maridjabin zmj 4167 zmj + language Marik dad 4168 dad + language Marimanindji zmm 4169 zmm + language Marind mrz 4170 mrz + language Maring mbw 4171 mbw + language Maring Naga nng 4172 nng + language Maringarr zmt 4173 zmt + language Marino mrb 4174 mrb + language Mariri mqi 4175 mqi + language Maritime Sign Language nsr 4176 nsr + language Maritsauá msp 4177 msp + language Mariyedi zmy 4178 zmy + language Marka rkm 4179 rkm + language Markweeta enb 4180 enb + language Marma rmz 4181 rmz + language Marovo mvo 4182 mvo + language Marra mec 4183 mec + language Marriammu xru 4184 xru + language Marrithiyel mfr 4185 mfr + language Marrucinian umc 4186 umc + language Marshallese mah 4187 mah mh + language Marsian ims 4188 ims + language Martha's Vineyard Sign Language mre 4189 mre + language Marti Ke zmg 4190 zmg + language Martu Wangka mpj 4191 mpj + language Martuyhunira vma 4192 vma + language Maru mhx 4193 mhx + language Marwari mwr 4194 mwr + language Marwari (India) rwr 4195 rwr + language Marwari (Pakistan) mve 4196 mve + language Marúbo mzr 4197 mzr + language Masaaba myx 4198 myx + language Masadiit Itneg tis 4199 tis + language Masai mas 4200 mas + language Masalit mls 4201 mls + language Masana mcn 4202 mcn + language Masbatenyo msb 4203 msb + language Mashco Piro cuj 4204 cuj + language Mashi (Nigeria) jms 4205 jms + language Mashi (Zambia) mho 4206 mho + language Masikoro Malagasy msh 4207 msh + language Masimasi ism 4208 ism + language Masiwang bnf 4209 bnf + language Maskelynes klv 4210 klv + language Maslam msv 4211 msv + language Masmaje mes 4212 mes + language Massalat mdg 4213 mdg + language Massep mvs 4214 mvs + language Matagalpa mtn 4215 mtn + language Matal mfh 4216 mfh + language Matambwe wtb 4217 wtb + language Matbat xmt 4218 xmt + language Matengo mgv 4219 mgv + language Matepi mqe 4220 mqe + language Matigsalug Manobo mbt 4221 mbt + language Matipuhy mzo 4222 mzo + language Matngala zml 4223 zml + language Mato met 4224 met + language Mato Grosso Arára axg 4225 axg + language Mator mtm 4226 mtm + language Matsés mcf 4227 mcf + language Mattole mvb 4228 mvb + language Matu Chin hlt 4229 hlt + language Matukar mjk 4230 mjk + language Matumbi mgw 4231 mgw + language Matya Samo stj 4232 stj + language Matís mpq 4233 mpq + language Maung mph 4234 mph + language Mauritian Sign Language lsy 4235 lsy + language Mauwake mhl 4236 mhl + language Mawa (Chad) mcw 4237 mcw + language Mawa (Nigeria) wma 4238 wma + language Mawak mjj 4239 mjj + language Mawan mcz 4240 mcz + language Mawayana mzx 4241 mzx + language Mawchi mke 4242 mke + language Mawes mgk 4243 mgk + language Maxakalí mbl 4244 mbl + language Maxi Gbe mxl 4245 mxl + language Maya Samo sym 4246 sym + language Mayaguduna xmy 4247 xmy + language Mayangna yan 4248 yan + language Mayawali yxa 4249 yxa + language Mayeka myc 4250 myc + language Mayi-Kulan xyk 4251 xyk + language Mayi-Thakurti xyt 4252 xyt + language Mayi-Yapi xyj 4253 xyj + language Mayo mfy 4254 mfy + language Mayogo mdm 4255 mdm + language Mayoyao Ifugao ifu 4256 ifu + language Mazagway dkx 4257 dkx + language Mazaltepec Zapotec zpy 4258 zpy + language Mazanderani mzn 4259 mzn + language Mazatlán Mazatec vmz 4260 vmz + language Mazatlán Mixe mzl 4261 mzl + language Mba mfc 4262 mfc + language Mbala mdp 4263 mdp + language Mbalanhu lnb 4264 lnb + language Mbandja zmz 4265 zmz + language Mbangala mxg 4266 mxg + language Mbangi mgn 4267 mgn + language Mbangwe zmn 4268 zmn + language Mbara (Australia) mvl 4269 mvl + language Mbara (Chad) mpk 4270 mpk + language Mbariman-Gudhinma zmv 4271 zmv + language Mbati mdn 4272 mdn + language Mbato gwa 4273 gwa + language Mbay myb 4274 myb + language Mbe mfo 4275 mfo + language Mbe' mtk 4276 mtk + language Mbelime mql 4277 mql + language Mbere mdt 4278 mdt + language Mbesa zms 4279 zms + language Mbessa emz 4280 emz + language Mbo (Cameroon) mbo 4281 mbo + language Mbo (Democratic Republic of Congo) zmw 4282 zmw + language Mboi moi 4283 moi + language Mboko mdu 4284 mdu + language Mbole mdq 4285 mdq + language Mbonga xmb 4286 xmb + language Mbongno bgu 4287 bgu + language Mbosi mdw 4288 mdw + language Mbowe mxo 4289 mxo + language Mbre mka 4290 mka + language Mbudum xmd 4291 xmd + language Mbugu mhd 4292 mhd + language Mbugwe mgz 4293 mgz + language Mbuk bpc 4294 bpc + language Mbuko mqb 4295 mqb + language Mbukushu mhw 4296 mhw + language Mbula mna 4297 mna + language Mbula-Bwazza mbu 4298 mbu + language Mbule mlb 4299 mlb + language Mbulungish mbv 4300 mbv + language Mbum mdd 4301 mdd + language Mbunda mck 4302 mck + language Mbunga mgy 4303 mgy + language Mburku bbt 4304 bbt + language Mbwela mfu 4305 mfu + language Mbyá Guaraní gun 4306 gun + language Me'en mym 4307 mym + language Medebur mjm 4308 mjm + language Medefaidrin dmf 4309 dmf + language Media Lengua mue 4310 mue + language Median xme 4311 xme + language Mednyj Aleut mud 4312 mud + language Medumba byv 4313 byv + language Mefele mfj 4314 mfj + language Megam mef 4315 mef + language Megleno Romanian ruq 4316 ruq + language Mehek nux 4317 nux + language Mehináku mmh 4318 mmh + language Mehri gdq 4319 gdq + language Mekeo mek 4320 mek + language Mekmek mvk 4321 mvk + language Mekwei msf 4322 msf + language Mel-Khaonh hkn 4323 hkn + language Mele-Fila mxe 4324 mxe + language Melo mfx 4325 mfx + language Melpa med 4326 med + language Memoni mby 4327 mby + language Mendalam Kayan xkd 4328 xkd + language Mendankwe-Nkwen mfd 4329 mfd + language Mende (Papua New Guinea) sim 4330 sim + language Mende (Sierra Leone) men 4331 men + language Mengaka xmg 4332 xmg + language Mengen mee 4333 mee + language Mengisa mct 4334 mct + language Menka mea 4335 mea + language Menominee mez 4336 mez + language Mentawai mwv 4337 mwv + language Menya mcr 4338 mcr + language Meoswar mvx 4339 mvx + language Mer mnu 4340 mnu + language Meramera mxm 4341 mxm + language Merei lmb 4342 lmb + language Merey meq 4343 meq + language Meriam Mir ulk 4344 ulk + language Merlav mrm 4345 mrm + language Meroitic xmr 4346 xmr + language Meru mer 4347 mer + language Merwari wry 4348 wry + language Mesaka iyo 4349 iyo + language Mescalero-Chiricahua Apache apm 4350 apm + language Mese mci 4351 mci + language Meskwaki sac 4352 sac + language Mesme zim 4353 zim + language Mesmes mys 4354 mys + language Mesopotamian Arabic acm 4355 acm + language Mesqan mvz 4356 mvz + language Messapic cms 4357 cms + language Meta' mgo 4358 mgo + language Metlatónoc Mixtec mxv 4359 mxv + language Mewari mtr 4360 mtr + language Mewati wtm 4361 wtm + language Mexican Sign Language mfs 4362 mfs + language Meyah mej 4363 mej + language Mezontla Popoloca pbe 4364 pbe + language Mezquital Otomi ote 4365 ote + language Mfinu zmf 4366 zmf + language Mfumte nfu 4367 nfu + language Mgbolizhia gmz 4368 gmz + language Mi'kmaq mic 4369 mic + language Miahuatlán Zapotec zam 4370 zam + language Miami mia 4371 mia + language Mian mpt 4372 mpt + language Miani pla 4373 pla + language Michif crg 4374 crg + language Michigamea cmm 4375 cmm + language Michoacán Mazahua mmc 4376 mmc + language Michoacán Nahuatl ncl 4377 ncl + language Mid Grand Valley Dani dnt 4378 dnt + language Mid-Southern Banda bjo 4379 bjo + language Middle Armenian axm 4380 axm + language Middle Breton xbm 4381 xbm + language Middle Cornish cnx 4382 cnx + language Middle Dutch (ca. 1050-1350) dum 4383 dum + language Middle English (1100-1500) enm 4384 enm + language Middle French (ca. 1400-1600) frm 4385 frm + language Middle High German (ca. 1050-1500) gmh 4386 gmh + language Middle Hittite htx 4387 htx + language Middle Irish (900-1200) mga 4388 mga + language Middle Khmer (1400 to 1850 CE) xhm 4389 xhm + language Middle Korean (10th-16th cent.) okm 4390 okm + language Middle Low German gml 4391 gml + language Middle Mongolian xng 4392 xng + language Middle Newar nwx 4393 nwx + language Middle Watut mpl 4394 mpl + language Middle Welsh wlm 4395 wlm + language Midob mei 4396 mei + language Migaama mmy 4397 mmy + language Migabac mpp 4398 mpp + language Migum klm 4399 klm + language Miju-Mishmi mxj 4400 mxj + language Mikasuki mik 4401 mik + language Mili ymh 4402 ymh + language Miltu mlj 4403 mlj + language Miluk iml 4404 iml + language Milyan imy 4405 imy + language Min Bei Chinese mnp 4406 mnp + language Min Dong Chinese cdo 4407 cdo + language Min Nan Chinese nan 4408 nan + language Min Zhong Chinese czo 4409 czo + language Mina (Cameroon) hna 4410 hna + language Minaean inm 4411 inm + language Minang xrg 4412 xrg + language Minangkabau min 4413 min + language Minanibai mcv 4414 mcv + language Minaveha mvn 4415 mvn + language Minderico drc 4416 drc + language Mindiri mpn 4417 mpn + language Mingang Doso mko 4418 mko + language Mingrelian xmf 4419 xmf + language Minica Huitoto hto 4420 hto + language Minidien wii 4421 wii + language Minjungbal xjb 4422 xjb + language Minkin xxm 4423 xxm + language Minoan omn 4424 omn + language Minokok mqq 4425 mqq + language Minriq mnq 4426 mnq + language Mintil mzt 4427 mzt + language Minz Zhuang zgm 4428 zgm + language Miqie yiq 4429 yiq + language Mirandese mwl 4430 mwl + language Miraya Bikol rbl 4431 rbl + language Mirgan zrg 4432 zrg + language Miriti mmv 4433 mmv + language Miriwoong mep 4434 mep + language Miriwoong Sign Language rsm 4435 rsm + language Mirning gmr 4436 gmr + language Miship mjs 4437 mjs + language Misima-Panaeati mpx 4438 mpx + language Mising mrg 4439 mrg + language Mitla Zapotec zaw 4440 zaw + language Mitlatongo Mixtec vmm 4441 vmm + language Mittu mwu 4442 mwu + language Mituku zmq 4443 zmq + language Miu mpo 4444 mpo + language Miwa vmi 4445 vmi + language Mixed Great Andamanese gac 4446 gac + language Mixtepec Mixtec mix 4447 mix + language Mixtepec Zapotec zpm 4448 zpm + language Miya mkf 4449 mkf + language Miyako mvi 4450 mvi + language Miyakubo Sign Language ehs 4451 ehs + language Miyobe soy 4452 soy + language Mlabri mra 4453 mra + language Mlahsö lhs 4454 lhs + language Mlap kja 4455 kja + language Mlomp mlo 4456 mlo + language Mmaala mmu 4457 mmu + language Mmen bfm 4458 bfm + language Mo'da gbn 4459 gbn + language Moabite obm 4460 obm + language Moba mfq 4461 mfq + language Mobilian mod 4462 mod + language Mobumrin Aizi ahm 4463 ahm + language Mobwa Karen jkm 4464 jkm + language Mochi old 4465 old + language Mochica omc 4466 omc + language Mocho mhc 4467 mhc + language Mocoví moc 4468 moc + language Modang mxd 4469 mxd + language Modern Greek (1453-), Greek (modern) ell 4470 ell Modern Greek (1453-) el gre Greek Greek (modern) + language Modole mqo 4471 mqo + language Moere mvq 4472 mvq + language Mofu-Gudur mif 4473 mif + language Mogholi mhj 4474 mhj + language Mogofin mfg 4475 mfg + language Mogum mou 4476 mou + language Mohave mov 4477 mov + language Mohawk moh 4478 moh + language Mohegan-Pequot xpq 4479 xpq + language Moi (Congo) mow 4480 mow + language Moi (Indonesia) mxn 4481 mxn + language Moikodi mkp 4482 mkp + language Moingi mwz 4483 mwz + language Moji ymi 4484 ymi + language Mok mqt 4485 mqt + language Mokati wnb 4486 wnb + language Moken mwt 4487 mwt + language Mokerang mft 4488 mft + language Mokilese mkj 4489 mkj + language Moklen mkm 4490 mkm + language Mokole mkl 4491 mkl + language Mokpwe bri 4492 bri + language Moksela vms 4493 vms + language Moksha mdf 4494 mdf + language Molale mbe 4495 mbe + language Molbog pwm 4496 pwm + language Moldova Sign Language vsi 4497 vsi + language Molengue bxc 4498 bxc + language Molima mox 4499 mox + language Molmo One aun 4500 aun + language Molo zmo 4501 zmo + language Molof msl 4502 msl + language Moloko mlw 4503 mlw + language Mom Jango ver 4504 ver + language Moma myl 4505 myl + language Momare msz 4506 msz + language Mombo Dogon dmb 4507 dmb + language Mombum mso 4508 mso + language Momina mmb 4509 mmb + language Momuna mqf 4510 mqf + language Mon mnw 4511 mnw + language Monastic Sign Language mzg 4512 mzg + language Mondropolon npn 4513 npn + language Mondé mnd 4514 mnd + language Mongo lol 4515 lol + language Mongol mgt 4516 mgt + language Mongolia Buriat bxm 4517 bxm + language Mongolian mon 4518 mon mn + language Mongolian Sign Language msr 4519 msr + language Mongondow mog 4520 mog + language Moni mnz 4521 mnz + language Mono (Cameroon) mru 4522 mru + language Mono (Democratic Republic of Congo) mnh 4523 mnh + language Mono (Solomon Islands) mte 4524 mte + language Mono (USA) mnr 4525 mnr + language Monom moo 4526 moo + language Monsang Naga nmh 4527 nmh + language Montenegrin cnr 4528 cnr + language Montol mtl 4529 mtl + language Monumbo mxk 4530 mxk + language Monzombo moj 4531 moj + language Moo gwg 4532 gwg + language Moose Cree crm 4533 crm + language Mopán Maya mop 4534 mop + language Mor (Bomberai Peninsula) moq 4535 moq + language Mor (Mor Islands) mhz 4536 mhz + language Moraid msg 4537 msg + language Morawa mze 4538 mze + language Morelos Nahuatl nhm 4539 nhm + language Morerebi xmo 4540 xmo + language Moresada msx 4541 msx + language Mori Atas mzq 4542 mzq + language Mori Bawah xmz 4543 xmz + language Morigi mdb 4544 mdb + language Moriori rrm 4545 rrm + language Morisyen mfe 4546 mfe + language Moro mor 4547 mor + language Moroccan Arabic ary 4548 ary + language Moroccan Sign Language xms 4549 xms + language Morokodo mgc 4550 mgc + language Morom bdo 4551 bdo + language Moronene mqn 4552 mqn + language Morori mok 4553 mok + language Morouas mrp 4554 mrp + language Morrobalama umg 4555 umg + language Mortlockese mrl 4556 mrl + language Moru mgd 4557 mgd + language Mosimo mqv 4558 mqv + language Moskona mtj 4559 mtj + language Mossi mos 4560 mos + language Mota mtt 4561 mtt + language Motlav mlv 4562 mlv + language Motu meu 4563 meu + language Mouk-Aria mwh 4564 mwh + language Moundadan Chetty cty 4565 cty + language Mountain Koiali kpx 4566 kpx + language Mouwase jmw 4567 jmw + language Movima mzp 4568 mzp + language Moyadan Itneg ity 4569 ity + language Moyon Naga nmo 4570 nmo + language Mozambican Sign Language mzy 4571 mzy + language Mozarabic mxi 4572 mxi + language Mpade mpi 4573 mpi + language Mpalitjanh xpj 4574 xpj + language Mpi mpz 4575 mpz + language Mpiemo mcx 4576 mcx + language Mpinda pnd 4577 pnd + language Mpoto mpa 4578 mpa + language Mpotovoro mvt 4579 mvt + language Mpumpong mgg 4580 mgg + language Mpuono zmp 4581 zmp + language Mpur akc 4582 akc + language Mro-Khimi Chin cmr 4583 cmr + language Mru mro 4584 mro + language Mser kqx 4585 kqx + language Mt. Iraya Agta atl 4586 atl + language Mt. Iriga Agta agz 4587 agz + language Muak Sa-aak ukk 4588 ukk + language Mualang mtd 4589 mtd + language Mubami tsx 4590 tsx + language Mubi mub 4591 mub + language Muda ymd 4592 ymd + language Mudburra dmw 4593 dmw + language Mudhili Gadaba gau 4594 gau + language Mudu Koraga vmd 4595 vmd + language Muduga udg 4596 udg + language Mufian aoj 4597 aoj + language Mugom muk 4598 muk + language Muinane bmr 4599 bmr + language Mukha-Dora mmk 4600 mmk + language Mukulu moz 4601 moz + language Mulaha mfw 4602 mfw + language Mulam mlm 4603 mlm + language Mulao giu 4604 giu + language Mulgi mvh 4605 mvh + language Mullu Kurumba kpb 4606 kpb + language Multiple languages mul 4607 mul + language Muluridyi vmu 4608 vmu + language Mum kqa 4609 kqa + language Mumuye mzm 4610 mzm + language Muna mnb 4611 mnb + language Munda unx 4612 unx + language Mundabli boe 4613 boe + language Mundang mua 4614 mua + language Mundani mnf 4615 mnf + language Mundari unr 4616 unr + language Mundat mmf 4617 mmf + language Mundurukú myu 4618 myu + language Mungaka mhk 4619 mhk + language Munggui mth 4620 mth + language Mungkip mpv 4621 mpv + language Muniche myr 4622 myr + language Munit mtc 4623 mtc + language Munji mnj 4624 mnj + language Munsee umu 4625 umu + language Muong mtq 4626 mtq + language Mur Pano tkv 4627 tkv + language Muratayak asx 4628 asx + language Murik (Malaysia) mxr 4629 mxr + language Murik (Papua New Guinea) mtf 4630 mtf + language Murkim rmh 4631 rmh + language Murle mur 4632 mur + language Murrinh-Patha mwf 4633 mwf + language Mursi muz 4634 muz + language Murui Huitoto huu 4635 huu + language Murupi mqw 4636 mqw + language Muruwari zmu 4637 zmu + language Musak mmq 4638 mmq + language Musar mmi 4639 mmi + language Musasa smm 4640 smm + language Musey mse 4641 mse + language Musgu mug 4642 mug + language Mushungulu xma 4643 xma + language Musi mui 4644 mui + language Muskum mje 4645 mje + language Muslim Tat ttt 4646 ttt + language Musom msu 4647 msu + language Mussau-Emira emi 4648 emi + language Muthuvan muv 4649 muv + language Mutu tuc 4650 tuc + language Muyang muy 4651 muy + language Muyuw myw 4652 myw + language Muzi ymz 4653 ymz + language Mvanip mcj 4654 mcj + language Mvuba mxh 4655 mxh + language Mwaghavul sur 4656 sur + language Mwali Comorian wlc 4657 wlc + language Mwan moa 4658 moa + language Mwani wmw 4659 wmw + language Mwatebu mwa 4660 mwa + language Mwera (Chimwera) mwe 4661 mwe + language Mwera (Nyasa) mjh 4662 mjh + language Mwimbi-Muthambi mws 4663 mws + language Myanmar Sign Language ysm 4664 ysm + language Mycenaean Greek gmy 4665 gmy + language Myene mye 4666 mye + language Mysian yms 4667 yms + language Mzieme Naga nme 4668 nme + language Mághdì gmd 4669 gmd + language Máku xak 4670 xak + language Ménik tnr 4671 tnr + language Mískito miq 4672 miq + language Mócheno mhn 4673 mhn + language Mün Chin mwq 4674 mwq + language Mündü muh 4675 muh + language Māhārāṣṭri Prākrit pmh 4676 pmh + language Māori, Maori mri 4677 mri Māori Maori mao mi + language N'Ko nqo 4678 nqo + language Na nbt 4679 nbt + language Na-kara nck 4680 nck + language Naaba nao 4681 nao + language Naami bzv 4682 bzv + language Naasioi nas 4683 nas + language Naba mne 4684 mne + language Nabak naf 4685 naf + language Nabi mty 4686 mty + language Nachering ncd 4687 ncd + language Nadruvian ndf 4688 ndf + language Nadëb mbj 4689 mbj + language Nafaanra nfr 4690 nfr + language Nafi srf 4691 srf + language Nafri nxx 4692 nxx + language Nafusi jbn 4693 jbn + language Naga Pidgin nag 4694 nag + language Nagarchal nbg 4695 nbg + language Nage nxe 4696 nxe + language Nagumi ngv 4697 ngv + language Nahali nlx 4698 nlx + language Nahari nhh 4699 nhh + language Nai bio 4700 bio + language Najdi Arabic ars 4701 ars + language Naka'ela nae 4702 nae + language Nakai nkj 4703 nkj + language Nakame nib 4704 nib + language Nakanai nak 4705 nak + language Nake nbk 4706 nbk + language Naki mff 4707 mff + language Nakwi nax 4708 nax + language Nalca nlc 4709 nlc + language Nali nss 4710 nss + language Nalik nal 4711 nal + language Nalu naj 4712 naj + language Naluo Yi ylo 4713 ylo + language Nalögo nlz 4714 nlz + language Nama (Papua New Guinea) nmx 4715 nmx + language Namakura nmk 4716 nmk + language Namat nkm 4717 nkm + language Nambo ncm 4718 ncm + language Nambya nmq 4719 nmq + language Namia nnm 4720 nnm + language Namiae nvm 4721 nvm + language Namibian Sign Language nbs 4722 nbs + language Namla naa 4723 naa + language Namo mxw 4724 mxw + language Namonuito nmt 4725 nmt + language Namosi-Naitasiri-Serua bwb 4726 bwb + language Namuyi nmy 4727 nmy + language Nanai gld 4728 gld + language Nancere nnc 4729 nnc + language Nande nnb 4730 nnb + language Nandi niq 4731 niq + language Nanerigé Sénoufo sen 4732 sen + language Nanga Dama Dogon nzz 4733 nzz + language Nankina nnk 4734 nnk + language Nanti cox 4735 cox + language Nanticoke nnt 4736 nnt + language Nanubae afk 4737 afk + language Napo Lowland Quechua qvo 4738 qvo + language Napu npy 4739 npy + language Nar Phu npa 4740 npa + language Nara nrb 4741 nrb + language Narak nac 4742 nac + language Narango nrg 4743 nrg + language Nari Nari rnr 4744 rnr + language Naro nhr 4745 nhr + language Narom nrm 4746 nrm + language Narragansett xnt 4747 xnt + language Narua nru 4748 nru + language Narungga nnr 4749 nnr + language Nasal nsy 4750 nsy + language Nasarian nvh 4751 nvh + language Naskapi nsk 4752 nsk + language Natanzi ntz 4753 ntz + language Natchez ncz 4754 ncz + language Nateni ntm 4755 ntm + language Nathembo nte 4756 nte + language Natioro nti 4757 nti + language Natügu ntu 4758 ntu + language Nauete nxa 4759 nxa + language Naukan Yupik ynk 4760 ynk + language Nauna ncn 4761 ncn + language Nauo nwo 4762 nwo + language Nauru nau 4763 nau na + language Navajo, Navaho nav 4764 nav nv Navajo Navaho + language Navut nsw 4765 nsw + language Nawaru nwr 4766 nwr + language Nawathinehena nwa 4767 nwa + language Nawdm nmz 4768 nmz + language Nawuri naw 4769 naw + language Naxi nxq 4770 nxq + language Nayi noz 4771 noz + language Nayini nyq 4772 nyq + language Ncane ncr 4773 ncr + language Nchumbulu nlu 4774 nlu + language Nda'nda' nnz 4775 nnz + language Ndai gke 4776 gke + language Ndaka ndk 4777 ndk + language Ndali ndh 4778 ndh + language Ndam ndm 4779 ndm + language Ndamba ndj 4780 ndj + language Ndambomo nxo 4781 nxo + language Ndasa nda 4782 nda + language Ndau ndc 4783 ndc + language Nde-Gbite ned 4784 ned + language Nde-Nsele-Nta ndd 4785 ndd + language Ndemli nml 4786 nml + language Ndendeule dne 4787 dne + language Ndengereko ndg 4788 ndg + language Nding eli 4789 eli + language Ndo ndp 4790 ndp + language Ndobo ndw 4791 ndw + language Ndoe nbb 4792 nbb + language Ndogo ndz 4793 ndz + language Ndolo ndl 4794 ndl + language Ndom nqm 4795 nqm + language Ndombe ndq 4796 ndq + language Ndonde Hamba njd 4797 njd + language Ndonga ndo 4798 ndo ng + language Ndoola ndr 4799 ndr + language Ndra'ngith dgt 4800 dgt + language Ndrulo dno 4801 dno + language Nduga ndx 4802 ndx + language Ndumu nmd 4803 nmd + language Ndunda nuh 4804 nuh + language Ndunga ndt 4805 ndt + language Ndut ndv 4806 ndv + language Ndwewe nww 4807 nww + language Ndyuka-Trio Pidgin njt 4808 njt + language Ndzwani Comorian wni 4809 wni + language Neapolitan nap 4810 nap + language Nedebang nec 4811 nec + language Nefamese nef 4812 nef + language Negerhollands dcr 4813 dcr + language Negeri Sembilan Malay zmi 4814 zmi + language Negidal neg 4815 neg + language Nehan nsn 4816 nsn + language Nek nif 4817 nif + language Nekgini nkg 4818 nkg + language Neko nej 4819 nej + language Neku nek 4820 nek + language Nema gsn 4821 gsn + language Neme nex 4822 nex + language Nemi nem 4823 nem + language Nen nqn 4824 nqn + language Nend anh 4825 anh + language Nenets yrk 4826 yrk + language Nengone nen 4827 nen + language Neo neu 4828 neu + language Neo-Hittite nei 4829 nei + language Nepalese Sign Language nsp 4830 nsp + language Nepali (individual language) npi 4831 npi + language Nepali (macrolanguage), Nepali nep 4832 nep Nepali (macrolanguage) ne Nepali + language Nete net 4833 net + language New Caledonian Javanese jas 4834 jas + language New Zealand Sign Language nzs 4835 nzs + language Newari new 4836 new + language Neyo ney 4837 ney + language Nez Perce nez 4838 nez + language Ngaanyatjarra ntj 4839 ntj + language Ngad'a nxg 4840 nxg + language Ngadjunmaya nju 4841 nju + language Ngadjuri jui 4842 jui + language Ngaing nnf 4843 nnf + language Ngaju nij 4844 nij + language Ngala nud 4845 nud + language Ngalakgan nig 4846 nig + language Ngalum szb 4847 szb + language Ngam nmc 4848 nmc + language Ngamambo nbv 4849 nbv + language Ngambay sba 4850 sba + language Ngamini nmv 4851 nmv + language Ngamo nbh 4852 nbh + language Ngan'gityemerri nam 4853 nam + language Nganakarti xnk 4854 xnk + language Nganasan nio 4855 nio + language Ngandi nid 4856 nid + language Ngando (Central African Republic) ngd 4857 ngd + language Ngando (Democratic Republic of Congo) nxd 4858 nxd + language Ngandyera nne 4859 nne + language Ngangam gng 4860 gng + language Ngantangarra ntg 4861 ntg + language Nganyaywana nyx 4862 nyx + language Ngardi rxd 4863 rxd + language Ngarigu xni 4864 xni + language Ngarinyin ung 4865 ung + language Ngarinyman nbj 4866 nbj + language Ngarla nrk 4867 nrk + language Ngarluma nrl 4868 nrl + language Ngarrindjeri nay 4869 nay + language Ngas anc 4870 anc + language Ngasa nsg 4871 nsg + language Ngatik Men's Creole ngm 4872 ngm + language Ngawn Chin cnw 4873 cnw + language Ngawun nxn 4874 nxn + language Ngayawung nwg 4875 nwg + language Ngazidja Comorian zdj 4876 zdj + language Ngbaka nga 4877 nga + language Ngbaka Ma'bo nbm 4878 nbm + language Ngbaka Manza ngg 4879 ngg + language Ngbee jgb 4880 jgb + language Ngbinda nbd 4881 nbd + language Ngbundu nuu 4882 nuu + language Ngelima agh 4883 agh + language Ngemba nge 4884 nge + language Ngen gnj 4885 gnj + language Ngendelengo nql 4886 nql + language Ngete nnn 4887 nnn + language Nggem nbq 4888 nbq + language Nggwahyi ngx 4889 ngx + language Ngie ngj 4890 ngj + language Ngiemboon nnh 4891 nnh + language Ngile jle 4892 jle + language Ngindo nnq 4893 nnq + language Ngiti niy 4894 niy + language Ngizim ngi 4895 ngi + language Ngkâlmpw Kanum kcd 4896 kcd + language Ngom nra 4897 nra + language Ngomba jgo 4898 jgo + language Ngombale nla 4899 nla + language Ngombe (Central African Republic) nmj 4900 nmj + language Ngombe (Democratic Republic of Congo) ngc 4901 ngc + language Ngongo noq 4902 noq + language Ngoni (Mozambique) xnq 4903 xnq + language Ngoni (Tanzania) xnj 4904 xnj + language Ngoshie nsh 4905 nsh + language Ngul nlo 4906 nlo + language Ngulu ngp 4907 ngp + language Nguluwan nuw 4908 nuw + language Ngumbarl xnm 4909 xnm + language Ngumbi nui 4910 nui + language Ngunawal xul 4911 xul + language Ngundi ndn 4912 ndn + language Ngundu nue 4913 nue + language Ngungwel ngz 4914 ngz + language Ngurimi ngq 4915 ngq + language Ngurmbur nrx 4916 nrx + language Nguôn nuo 4917 nuo + language Ngwaba ngw 4918 ngw + language Ngwe nwe 4919 nwe + language Ngwo ngn 4920 ngn + language Ngäbere gym 4921 gym + language Nhanda nha 4922 nha + language Nhengatu yrl 4923 yrl + language Nhirrpi hrp 4924 hrp + language Nhuwala nhf 4925 nhf + language Nias nia 4926 nia + language Nicaragua Creole English bzk 4927 bzk + language Nicaraguan Sign Language ncs 4928 ncs + language Niellim nie 4929 nie + language Nigeria Mambila mzk 4930 mzk + language Nigerian Fulfulde fuv 4931 fuv + language Nigerian Pidgin pcm 4932 pcm + language Nigerian Sign Language nsi 4933 nsi + language Nihali nll 4934 nll + language Nii nii 4935 nii + language Niksek gbe 4936 gbe + language Nila nil 4937 nil + language Nilamba nim 4938 nim + language Nimadi noe 4939 noe + language Nimanbur nmp 4940 nmp + language Nimbari nmr 4941 nmr + language Nimboran nir 4942 nir + language Nimi nis 4943 nis + language Nimo niw 4944 niw + language Nimoa nmw 4945 nmw + language Ninam shb 4946 shb + language Nindi nxi 4947 nxi + language Ningera nby 4948 nby + language Ninggerum nxr 4949 nxr + language Ningil niz 4950 niz + language Ninia Yali nlk 4951 nlk + language Ninzo nin 4952 nin + language Nipsan nps 4953 nps + language Nisa njs 4954 njs + language Nisenan nsz 4955 nsz + language Nisga'a ncg 4956 ncg + language Nisi (China) yso 4957 yso + language Niuafo'ou num 4958 num + language Niuatoputapu nkp 4959 nkp + language Niuean niu 4960 niu + language Nivaclé cag 4961 cag + language Niwer Mil hrc 4962 hrc + language Njalgulgule njl 4963 njl + language Njebi nzb 4964 nzb + language Njen njj 4965 njj + language Njerep njr 4966 njr + language Njyem njy 4967 njy + language Nkami nkq 4968 nkq + language Nkangala nkn 4969 nkn + language Nkari nkz 4970 nkz + language Nkem-Nkum isi 4971 isi + language Nkhumbi khu 4972 khu + language Nkongho nkc 4973 nkc + language Nkonya nko 4974 nko + language Nkoroo nkx 4975 nkx + language Nkoya nka 4976 nka + language Nkukoli nbo 4977 nbo + language Nkutu nkw 4978 nkw + language Nnam nbp 4979 nbp + language No linguistic content zxx 4980 zxx + language Nobiin fia 4981 fia + language Nobonob gaw 4982 gaw + language Nocte Naga njb 4983 njb + language Nogai nog 4984 nog + language Noipx npx 4985 npx + language Noiri noi 4986 noi + language Nokuku nkk 4987 nkk + language Nomaande lem 4988 lem + language Nomane nof 4989 nof + language Nomatsiguenga not 4990 not + language Nomlaki nol 4991 nol + language Nomu noh 4992 noh + language Nong Zhuang zhn 4993 zhn + language Nonuya noj 4994 noj + language Nooksack nok 4995 nok + language Noon snf 4996 snf + language Noone nhu 4997 nhu + language Nopala Chatino cya 4998 cya + language Noric nrc 4999 nrc + language Norn nrn 5000 nrn + language Norra nrr 5001 nrr + language North Alaskan Inupiatun esi 5002 esi + language North Ambrym mmg 5003 mmg + language North Asmat nks 5004 nks + language North Awyu yir 5005 yir + language North Azerbaijani azj 5006 azj + language North Babar bcd 5007 bcd + language North Bolivian Quechua qul 5008 qul + language North Central Mixe neq 5009 neq + language North Efate llp 5010 llp + language North Fali fll 5011 fll + language North Giziga gis 5012 gis + language North Junín Quechua qvn 5013 qvn + language North Marquesan mrq 5014 mrq + language North Mesopotamian Arabic ayp 5015 ayp + language North Midlands Tasmanian xph 5016 xph + language North Mofu mfk 5017 mfk + language North Moluccan Malay max 5018 max + language North Muyu kti 5019 kti + language North Nuaulu nni 5020 nni + language North Picene nrp 5021 nrp + language North Slavey scs 5022 scs + language North Tairora tbg 5023 tbg + language North Tanna tnn 5024 tnn + language North Wahgi whg 5025 whg + language North Watut una 5026 una + language Northeast Kiwai kiw 5027 kiw + language Northeast Maidu nmu 5028 nmu + language Northeast Pashai aee 5029 aee + language Northeastern Dinka dip 5030 dip + language Northeastern Pomo pef 5031 pef + language Northeastern Tasmanian xpb 5032 xpb + language Northeastern Thai tts 5033 tts + language Northern Alta aqn 5034 aqn + language Northern Altai atv 5035 atv + language Northern Amami-Oshima ryn 5036 ryn + language Northern Betsimisaraka Malagasy bmm 5037 bmm + language Northern Binukidnon kyn 5038 kyn + language Northern Bobo Madaré bbo 5039 bbo + language Northern Bontok rbk 5040 rbk + language Northern Catanduanes Bikol cts 5041 cts + language Northern Conchucos Ancash Quechua qxn 5042 qxn + language Northern Dagara dgi 5043 dgi + language Northern Dong doc 5044 doc + language Northern East Cree crl 5045 crl + language Northern Emberá emp 5046 emp + language Northern Frisian frr 5047 frr + language Northern Ghale ghh 5048 ghh + language Northern Gondi gno 5049 gno + language Northern Grebo gbo 5050 gbo + language Northern Guiyang Hmong huj 5051 huj + language Northern Haida hdn 5052 hdn + language Northern Hindko hno 5053 hno + language Northern Huishui Hmong hmi 5054 hmi + language Northern Kalapuya nrt 5055 nrt + language Northern Kankanay xnn 5056 xnn + language Northern Katang ncq 5057 ncq + language Northern Khmer kxm 5058 kxm + language Northern Kissi kqs 5059 kqs + language Northern Kurdish kmr 5060 kmr + language Northern Luri lrc 5061 lrc + language Northern Mashan Hmong hmp 5062 hmp + language Northern Muji ymx 5063 ymx + language Northern Nago xkb 5064 xkb + language Northern Ndebele, North Ndebele nde 5065 nde Northern Ndebele nd North Ndebele + language Northern Ngbandi ngb 5066 ngb + language Northern Nisu yiv 5067 yiv + language Northern Nuni nuv 5068 nuv + language Northern Oaxaca Nahuatl nhy 5069 nhy + language Northern Ohlone cst 5070 cst + language Northern One onr 5071 onr + language Northern Paiute pao 5072 pao + language Northern Pame pmq 5073 pmq + language Northern Pashto pbu 5074 pbu + language Northern Pastaza Quichua qvz 5075 qvz + language Northern Ping Chinese cnp 5076 cnp + language Northern Pomo pej 5077 pej + language Northern Puebla Nahuatl ncj 5078 ncj + language Northern Pumi pmi 5079 pmi + language Northern Qiandong Miao hea 5080 hea + language Northern Qiang cng 5081 cng + language Northern Rengma Naga nnl 5082 nnl + language Northern Roglai rog 5083 rog + language Northern Sami sme 5084 sme se + language Northern Sierra Miwok nsq 5085 nsq + language Northern Sorsoganon bks 5086 bks + language Northern Subanen stb 5087 stb + language Northern Tarahumara thh 5088 thh + language Northern Tasmanian xpv 5089 xpv + language Northern Tepehuan ntp 5090 ntp + language Northern Thai nod 5091 nod + language Northern Tidung ntd 5092 ntd + language Northern Tiwa twf 5093 twf + language Northern Tlaxiaco Mixtec xtn 5094 xtn + language Northern Toussian tsp 5095 tsp + language Northern Tujia tji 5096 tji + language Northern Tutchone ttm 5097 ttm + language Northern Uzbek uzn 5098 uzn + language Northern Yukaghir ykg 5099 ykg + language Northwest Alaska Inupiatun esk 5100 esk + language Northwest Gbaya gya 5101 gya + language Northwest Maidu mjd 5102 mjd + language Northwest Oaxaca Mixtec mxa 5103 mxa + language Northwest Pashai glh 5104 glh + language Northwestern Dinka diw 5105 diw + language Northwestern Fars faz 5106 faz + language Northwestern Kolami kfb 5107 kfb + language Northwestern Nisu nsf 5108 nsf + language Northwestern Ojibwa ojb 5109 ojb + language Northwestern Tasmanian xpw 5110 xpw + language Norwegian nor 5111 nor no + language Norwegian Bokmål nob 5112 nob nb + language Norwegian Nynorsk nno 5113 nno nn + language Norwegian Sign Language nsl 5114 nsl + language Notre bly 5115 bly + language Notsi ncf 5116 ncf + language Nottoway ntw 5117 ntw + language Nottoway-Meherrin nwy 5118 nwy + language Novial nov 5119 nov + language Noy noy 5120 noy + language Nsenga nse 5121 nse + language Nshi nsc 5122 nsc + language Nsongo nsx 5123 nsx + language Ntcham bud 5124 bud + language Nteng nqt 5125 nqt + language Ntomba nto 5126 nto + language Nubaca baf 5127 baf + language Nubi kcn 5128 kcn + language Nubri kte 5129 kte + language Nuer nus 5130 nus + language Nugunu (Australia) nnv 5131 nnv + language Nugunu (Cameroon) yas 5132 yas + language Nuk noc 5133 noc + language Nukak Makú mbr 5134 mbr + language Nukna klt 5135 klt + language Nukuini nuc 5136 nuc + language Nukumanu nuq 5137 nuq + language Nukunul xnu 5138 xnu + language Nukuoro nkr 5139 nkr + language Nukuria nur 5140 nur + language Numana nbr 5141 nbr + language Numanggang nop 5142 nop + language Numbami sij 5143 sij + language Nume tgs 5144 tgs + language Numidian nxm 5145 nxm + language Numèè kdk 5146 kdk + language Nung (Viet Nam) nut 5147 nut + language Nungali nug 5148 nug + language Nunggubuyu nuy 5149 nuy + language Nungu rin 5150 rin + language Nupbikha npb 5151 npb + language Nupe-Nupe-Tako nup 5152 nup + language Nusa Laut nul 5153 nul + language Nusu nuf 5154 nuf + language Nuu-chah-nulth nuk 5155 nuk + language Nyabwa nwb 5156 nwb + language Nyaheun nev 5157 nev + language Nyahkur cbn 5158 cbn + language Nyakyusa-Ngonde nyy 5159 nyy + language Nyali nlj 5160 nlj + language Nyam nmi 5161 nmi + language Nyamal nly 5162 nly + language Nyambo now 5163 now + language Nyamusa-Molo nwm 5164 nwm + language Nyamwanga mwn 5165 mwn + language Nyamwezi nym 5166 nym + language Nyaneka nyk 5167 nyk + language Nyang'i nyp 5168 nyp + language Nyanga nyj 5169 nyj + language Nyanga-li nyc 5170 nyc + language Nyangatom nnj 5171 nnj + language Nyangbo nyb 5172 nyb + language Nyangga nny 5173 nny + language Nyangumarta nna 5174 nna + language Nyankole nyn 5175 nyn + language Nyankpa yes 5176 yes + language Nyarafolo Senoufo sev 5177 sev + language Nyaturu rim 5178 rim + language Nyaw nyw 5179 nyw + language Nyawaygi nyt 5180 nyt + language Nyemba nba 5181 nba + language Nyengo nye 5182 nye + language Nyenkha neh 5183 neh + language Nyeu nyl 5184 nyl + language Nyiha (Malawi) nyr 5185 nyr + language Nyiha (Tanzania) nih 5186 nih + language Nyika (Malawi and Zambia) nkv 5187 nkv + language Nyika (Tanzania) nkt 5188 nkt + language Nyikina nyh 5189 nyh + language Nyindrou lid 5190 lid + language Nyindu nyg 5191 nyg + language Nyishi njz 5192 njz + language Nyiyaparli xny 5193 xny + language Nyokon nvo 5194 nvo + language Nyole nuj 5195 nuj + language Nyong muo 5196 muo + language Nyore nyd 5197 nyd + language Nyoro nyo 5198 nyo + language Nyulnyul nyv 5199 nyv + language Nyungar nys 5200 nys + language Nyungwe nyu 5201 nyu + language Nyâlayu yly 5202 yly + language Nzadi nzd 5203 nzd + language Nzakambay nzy 5204 nzy + language Nzakara nzk 5205 nzk + language Nzanyi nja 5206 nja + language Nzima nzi 5207 nzi + language Ná-Meo neo 5208 neo + language Nêlêmwa-Nixumwak nee 5209 nee + language Nüpode Huitoto hux 5210 hux + language Nǁng ngh 5211 ngh + language O'chi'chi' xoc 5212 xoc + language O'du tyh 5213 tyh + language Obanliku bzy 5214 bzy + language Obispeño obi 5215 obi + language Oblo obl 5216 obl + language Obo Manobo obo 5217 obo + language Obokuitai afz 5218 afz + language Obolo ann 5219 ann + language Obulom obu 5220 obu + language Ocaina oca 5221 oca + language Occitan (post 1500), Occitan oci 5222 oci Occitan (post 1500) oc Occitan + language Ocotepec Mixtec mie 5223 mie + language Ocotlán Zapotec zac 5224 zac + language Od odk 5225 odk + language Odia ory 5226 ory + language Odiai bhf 5227 bhf + language Odoodee kkc 5228 kkc + language Odual odu 5229 odu + language Odut oda 5230 oda + language Ofayé opy 5231 opy + language Official Aramaic (700-300 BCE) arc 5232 arc + language Ofo ofo 5233 ofo + language Ogbah ogc 5234 ogc + language Ogbia ogb 5235 ogb + language Ogbogolo ogg 5236 ogg + language Ogbronuagum ogu 5237 ogu + language Ogea eri 5238 eri + language Oirata oia 5239 oia + language Ojibwe, Ojibwa oji 5240 oji Ojibwe oj Ojibwa + language Ojitlán Chinantec chj 5241 chj + language Okanagan oka 5242 oka + language Oki-No-Erabu okn 5243 okn + language Okiek oki 5244 oki + language Oko-Eni-Osayen oks 5245 oks + language Oko-Juwoi okj 5246 okj + language Okobo okb 5247 okb + language Okodia okd 5248 okd + language Okolie oie 5249 oie + language Okolod kqv 5250 kqv + language Okpamheri opa 5251 opa + language Okpe (Northwestern Edo) okx 5252 okx + language Okpe (Southwestern Edo) oke 5253 oke + language Oksapmin opm 5254 opm + language Oku oku 5255 oku + language Old Aramaic (up to 700 BCE) oar 5256 oar + language Old Avar oav 5257 oav + language Old Breton obt 5258 obt + language Old Burmese obr 5259 obr + language Old Cham ocm 5260 ocm + language Old Chinese och 5261 och + language Old Cornish oco 5262 oco + language Old Dutch odt 5263 odt + language Old English (ca. 450-1100) ang 5264 ang + language Old French (842-ca. 1400) fro 5265 fro + language Old Frisian ofs 5266 ofs + language Old Georgian oge 5267 oge + language Old High German (ca. 750-1050) goh 5268 goh + language Old Hittite oht 5269 oht + language Old Hungarian ohu 5270 ohu + language Old Irish (to 900) sga 5271 sga + language Old Japanese ojp 5272 ojp + language Old Kentish Sign Language okl 5273 okl + language Old Khmer okz 5274 okz + language Old Korean (3rd-9th cent.) oko 5275 oko + language Old Lithuanian olt 5276 olt + language Old Malay omy 5277 omy + language Old Manipuri omp 5278 omp + language Old Marathi omr 5279 omr + language Old Mon omx 5280 omx + language Old Norse non 5281 non + language Old Nubian onw 5282 onw + language Old Ossetic oos 5283 oos + language Old Persian (ca. 600-400 B.C.) peo 5284 peo + language Old Provençal (to 1500) pro 5285 pro + language Old Russian orv 5286 orv + language Old Saxon osx 5287 osx + language Old Spanish osp 5288 osp + language Old Sundanese osn 5289 osn + language Old Tamil oty 5290 oty + language Old Tibetan otb 5291 otb + language Old Turkish otk 5292 otk + language Old Uighur oui 5293 oui + language Old Welsh owl 5294 owl + language Olekha ole 5295 ole + language Olkol olk 5296 olk + language Olo ong 5297 ong + language Oloma olm 5298 olm + language Olrat olr 5299 olr + language Olu'bo lul 5300 lul + language Olulumo-Ikom iko 5301 iko + language Oluta Popoluca plo 5302 plo + language Omagua omg 5303 omg + language Omaha-Ponca oma 5304 oma + language Omani Arabic acx 5305 acx + language Ombamba mbm 5306 mbm + language Ombo oml 5307 oml + language Ometepec Nahuatl nht 5308 nht + language Omi omi 5309 omi + language Omok omk 5310 omk + language Omotik omt 5311 omt + language Omurano omu 5312 omu + language Ona ona 5313 ona + language Oneida one 5314 one + language Ong oog 5315 oog + language Onin oni 5316 oni + language Onin Based Pidgin onx 5317 onx + language Onjob onj 5318 onj + language Ono ons 5319 ons + language Onobasulu onn 5320 onn + language Onondaga ono 5321 ono + language Ontenu ont 5322 ont + language Ontong Java ojv 5323 ojv + language Oorlams oor 5324 oor + language Opao opo 5325 opo + language Opata opt 5326 opt + language Orang Kanaq orn 5327 orn + language Orang Seletar ors 5328 ors + language Oraon Sadri sdr 5329 sdr + language Orejón ore 5330 ore + language Oring org 5331 org + language Oriya, Oriya (macrolanguage) ori 5332 ori Oriya or Oriya (macrolanguage) + language Orizaba Nahuatl nlv 5333 nlv + language Orma orc 5334 orc + language Ormu orz 5335 orz + language Ormuri oru 5336 oru + language Oro orx 5337 orx + language Oro Win orw 5338 orw + language Oroch oac 5339 oac + language Oroha ora 5340 ora + language Orok oaa 5341 oaa + language Orokaiva okv 5342 okv + language Oroko bdu 5343 bdu + language Orokolo oro 5344 oro + language Oromo orm 5345 orm om + language Oroqen orh 5346 orh + language Orowe bpk 5347 bpk + language Oruma orr 5348 orr + language Orya ury 5349 ury + language Osage osa 5350 osa + language Osatu ost 5351 ost + language Oscan osc 5352 osc + language Osing osi 5353 osi + language Ososo oso 5354 oso + language Ossetian, Ossetic oss 5355 oss os Ossetian Ossetic + language Ot Danum otd 5356 otd + language Otank uta 5357 uta + language Oti oti 5358 oti + language Otoro otr 5359 otr + language Ottawa otw 5360 otw + language Ottoman Turkish (1500-1928) ota 5361 ota + language Otuho lot 5362 lot + language Otuke otu 5363 otu + language Ouma oum 5364 oum + language Oune oue 5365 oue + language Owa stn 5366 stn + language Owenia wsr 5367 wsr + language Owiniga owi 5368 owi + language Oy oyb 5369 oyb + language Oya'oya oyy 5370 oyy + language Oyda oyd 5371 oyd + language Oyster Bay Tasmanian xpd 5372 xpd + language Ozolotepec Zapotec zao 5373 zao + language Ozumacín Chinantec chz 5374 chz + language Pa Di pdi 5375 pdi + language Pa'a pqa 5376 pqa + language Pa'o Karen blk 5377 blk + language Pa-Hng pha 5378 pha + language Paakantyi drl 5379 drl + language Paama pma 5380 pma + language Paasaal sig 5381 sig + language Pacahuara pcp 5382 pcp + language Pacaraos Quechua qvp 5383 qvp + language Pacific Gulf Yupik ems 5384 ems + language Pacoh pac 5385 pac + language Padoe pdo 5386 pdo + language Paekche pkc 5387 pkc + language Paelignian pgn 5388 pgn + language Pagi pgi 5389 pgi + language Pagibete pae 5390 pae + language Pagu pgu 5391 pgu + language Pahanan Agta apf 5392 apf + language Pahari phj 5393 phj + language Pahari-Potwari phr 5394 phr + language Pahi lgt 5395 lgt + language Pahlavani phv 5396 phv + language Pahlavi pal 5397 pal + language Pai Tavytera pta 5398 pta + language Paicî pri 5399 pri + language Paipai ppi 5400 ppi + language Paite Chin pck 5401 pck + language Paiwan pwn 5402 pwn + language Pak-Tong pkg 5403 pkg + language Pakanha pkn 5404 pkn + language Pakaásnovos pav 5405 pav + language Pakistan Sign Language pks 5406 pks + language Paku pku 5407 pku + language Paku Karen jkp 5408 jkp + language Pal abw 5409 abw + language Palaic plq 5410 plq + language Palaka Senoufo plr 5411 plr + language Palantla Chinantec cpa 5412 cpa + language Palauan pau 5413 pau + language Paleni pnl 5414 pnl + language Palenquero pln 5415 pln + language Palikúr plu 5416 plu + language Paliyan pcf 5417 pcf + language Pallanganmiddang pmd 5418 pmd + language Paloor fap 5419 fap + language Palu'e ple 5420 ple + language Paluan plz 5421 plz + language Palya Bareli bpx 5422 bpx + language Pam pmn 5423 pmn + language Pambia pmb 5424 pmb + language Pamona pmf 5425 pmf + language Pamosu hih 5426 hih + language Pampanga pam 5427 pam + language Pamplona Atta att 5428 att + language Pana (Burkina Faso) pnq 5429 pnq + language Pana (Central African Republic) pnz 5430 pnz + language Panamanian Sign Language lsp 5431 lsp + language Panamint par 5432 par + language Panao Huánuco Quechua qxh 5433 qxh + language Panará kre 5434 kre + language Panasuan psn 5435 psn + language Panawa pwb 5436 pwb + language Pancana pnp 5437 pnp + language Panchpargania tdb 5438 tdb + language Pande bkj 5439 bkj + language Pangasinan pag 5440 pag + language Pangseng pgs 5441 pgs + language Pangu png 5442 png + language Pangutaran Sama slm 5443 slm + language Pangwa pbr 5444 pbr + language Pangwali pgg 5445 pgg + language Panim pnr 5446 pnr + language Paniya pcg 5447 pcg + language Panjabi, Punjabi pan 5448 pan pa Panjabi Punjabi + language Pankararé pax 5449 pax + language Pankararú paz 5450 paz + language Pankhu pkh 5451 pkh + language Pannei pnc 5452 pnc + language Pano mqz 5453 mqz + language Panoan Katukína knt 5454 knt + language Panobo pno 5455 pno + language Panyi Bai bfc 5456 bfc + language Papantla Totonac top 5457 top + language Papapana ppn 5458 ppn + language Papar dpp 5459 dpp + language Papasena pas 5460 pas + language Papel pbo 5461 pbo + language Papi ppe 5462 ppe + language Papiamento pap 5463 pap + language Papora ppu 5464 ppu + language Papua New Guinean Sign Language pgz 5465 pgz + language Papuan Malay pmy 5466 pmy + language Papuma ppm 5467 ppm + language Parachi prc 5468 prc + language Paraguayan Guaraní gug 5469 gug + language Paraguayan Sign Language pys 5470 pys + language Parakanã pak 5471 pak + language Paranan prf 5472 prf + language Paranawát paf 5473 paf + language Paraujano pbg 5474 pbg + language Parauk prk 5475 prk + language Parawen prw 5476 prw + language Pardhan pch 5477 pch + language Pardhi pcl 5478 pcl + language Pare ppt 5479 ppt + language Parecís pab 5480 pab + language Parenga pcj 5481 pcj + language Parkari Koli kvx 5482 kvx + language Parkwa pbi 5483 pbi + language Parsi-Dari prd 5484 prd + language Parthian xpr 5485 xpr + language Parya paq 5486 paq + language Pará Arára aap 5487 aap + language Pará Gavião gvp 5488 gvp + language Pashto, Pushto pus 5489 pus Pashto ps Pushto + language Pasi psq 5490 psq + language Pass Valley Yali yac 5491 yac + language Patamona pbc 5492 pbc + language Patani ptn 5493 ptn + language Pataxó Hã-Ha-Hãe pth 5494 pth + language Patep ptp 5495 ptp + language Pathiya pty 5496 pty + language Patpatar gfk 5497 gfk + language Pattani lae 5498 lae + language Pattani Malay mfa 5499 mfa + language Pattapu ptq 5500 ptq + language Patwin pwi 5501 pwi + language Paulohi plh 5502 plh + language Paumarí pad 5503 pad + language Paunaka pnk 5504 pnk + language Pauri Bareli bfb 5505 bfb + language Pauserna psm 5506 psm + language Pawaia pwa 5507 pwa + language Pawnee paw 5508 paw + language Paynamar pmr 5509 pmr + language Pazeh pzh 5510 pzh + language Pe pai 5511 pai + language Pear pcb 5512 pcb + language Pech pay 5513 pay + language Pecheneg xpc 5514 xpc + language Pedi nso 5515 nso + language Pei ppq 5516 ppq + language Pekal pel 5517 pel + language Pela bxd 5518 bxd + language Pele-Ata ata 5519 ata + language Pelende ppp 5520 ppp + language Pemon aoc 5521 aoc + language Penang Sign Language psg 5522 psg + language Penchal pek 5523 pek + language Pendau ums 5524 ums + language Pengo peg 5525 peg + language Pennsylvania German pdc 5526 pdc + language Penrhyn pnh 5527 pnh + language Pentlatch ptw 5528 ptw + language Perai wet 5529 wet + language Peranakan Indonesian pea 5530 pea + language Pere pfe 5531 pfe + language Peripheral Mongolian mvf 5532 mvf + language Pero pip 5533 pip + language Persian, Persian (Farsi) fas 5534 fas Persian fa Persian (Farsi) per + language Peruvian Sign Language prl 5535 prl + language Pesse pze 5536 pze + language Petapa Zapotec zpe 5537 zpe + language Petats pex 5538 pex + language Petjo pey 5539 pey + language Peñoles Mixtec mil 5540 mil + language Pfaelzisch pfl 5541 pfl + language Phai prt 5542 prt + language Phake phk 5543 phk + language Phala ypa 5544 ypa + language Phalura phl 5545 phl + language Phana' phq 5546 phq + language Phangduwali phw 5547 phw + language Phende pem 5548 pem + language Philippine Sign Language psp 5549 psp + language Phimbi phm 5550 phm + language Phoenician phn 5551 phn + language Phola ypg 5552 ypg + language Pholo yip 5553 yip + language Phom Naga nph 5554 nph + language Phong-Kniang pnx 5555 pnx + language Phrae Pwo Karen kjt 5556 kjt + language Phrygian xpg 5557 xpg + language Phu Thai pht 5558 pht + language Phuan phu 5559 phu + language Phudagi phd 5560 phd + language Phuie pug 5561 pug + language Phukha phh 5562 phh + language Phuma ypm 5563 ypm + language Phunoi pho 5564 pho + language Phuong phg 5565 phg + language Phupa ypp 5566 ypp + language Phupha yph 5567 yph + language Phuza ypz 5568 ypz + language Piamatsina ptr 5569 ptr + language Piame pin 5570 pin + language Piapoco pio 5571 pio + language Piaroa pid 5572 pid + language Picard pcd 5573 pcd + language Pichis Ashéninka cpu 5574 cpu + language Pictish xpi 5575 xpi + language Pidgin Delaware dep 5576 dep + language Piemontese pms 5577 pms + language Pijao pij 5578 pij + language Pije piz 5579 piz + language Pijin pis 5580 pis + language Pilagá plg 5581 plg + language Pileni piv 5582 piv + language Pima Bajo pia 5583 pia + language Pimbwe piw 5584 piw + language Pinai-Hagahai pnn 5585 pnn + language Pindiini pti 5586 pti + language Pingelapese pif 5587 pif + language Pinigura pnv 5588 pnv + language Pinjarup pnj 5589 pnj + language Pinji pic 5590 pic + language Pinotepa Nacional Mixtec mio 5591 mio + language Pintupi-Luritja piu 5592 piu + language Pinyin pny 5593 pny + language Pipil ppl 5594 ppl + language Pirahã myp 5595 myp + language Piratapuyo pir 5596 pir + language Pirlatapa bxi 5597 bxi + language Piro pie 5598 pie + language Pirriya xpa 5599 xpa + language Pisabo pig 5600 pig + language Pisaflores Tepehua tpp 5601 tpp + language Piscataway psy 5602 psy + language Pisidian xps 5603 xps + language Pitcairn-Norfolk pih 5604 pih + language Pite Sami sje 5605 sje + language Piti pcn 5606 pcn + language Pitjantjatjara pjt 5607 pjt + language Pitta Pitta pit 5608 pit + language Piu pix 5609 pix + language Piya-Kwonci piy 5610 piy + language Plains Cree crk 5611 crk + language Plains Indian Sign Language psd 5612 psd + language Plains Miwok pmw 5613 pmw + language Plapo Krumen ktj 5614 ktj + language Plateau Malagasy plt 5615 plt + language Plautdietsch pdt 5616 pdt + language Playero gob 5617 gob + language Pnar pbv 5618 pbv + language Pochuri Naga npo 5619 npo + language Pochutec xpo 5620 xpo + language Podena pdn 5621 pdn + language Pogolo poy 5622 poy + language Pohnpeian pon 5623 pon + language Pokangá pok 5624 pok + language Poke pof 5625 pof + language Pokomo pkb 5626 pkb + language Polabian pox 5627 pox + language Polari pld 5628 pld + language Polish pol 5629 pol pl + language Polish Sign Language pso 5630 pso + language Polonombauk plb 5631 plb + language Pom pmo 5632 pmo + language Pomo pmm 5633 pmm + language Ponam ncc 5634 ncc + language Ponosakan pns 5635 pns + language Pontic pnt 5636 pnt + language Ponyo-Gongwang Naga npg 5637 npg + language Popti' jac 5638 jac + language Poqomam poc 5639 poc + language Poqomchi' poh 5640 poh + language Porohanon prh 5641 prh + language Port Sandwich psw 5642 psw + language Port Sorell Tasmanian xpl 5643 xpl + language Port Vato ptv 5644 ptv + language Portuguese por 5645 por pt + language Portuguese Sign Language psr 5646 psr + language Potawatomi pot 5647 pot + language Potiguára pog 5648 pog + language Pottangi Ollar Gadaba gdb 5649 gdb + language Poumei Naga pmx 5650 pmx + language Pouye bye 5651 bye + language Powari pwr 5652 pwr + language Powhatan pim 5653 pim + language Poyanáwa pyn 5654 pyn + language Prasuni prn 5655 prn + language Primitive Irish pgl 5656 pgl + language Principense pre 5657 pre + language Providencia Sign Language prz 5658 prz + language Prussian prg 5659 prg + language Psikye kvj 5660 kvj + language Pu-Xian Chinese cpx 5661 cpx + language Puare pux 5662 pux + language Pudtol Atta atp 5663 atp + language Puebla Mazatec pbm 5664 pbm + language Puelche pue 5665 pue + language Puerto Rican Sign Language psl 5666 psl + language Puimei Naga npu 5667 npu + language Puinave pui 5668 pui + language Pukapuka pkp 5669 pkp + language Pulaar fuc 5670 fuc + language Pulabu pup 5671 pup + language Pular fuf 5672 fuf + language Puluwatese puw 5673 puw + language Puma pum 5674 pum + language Pumpokol xpm 5675 xpm + language Pumé yae 5676 yae + language Punan Aput pud 5677 pud + language Punan Bah-Biau pna 5678 pna + language Punan Batu 1 pnm 5679 pnm + language Punan Merah puf 5680 puf + language Punan Merap puc 5681 puc + language Punan Tubu puj 5682 puj + language Punic xpu 5683 xpu + language Puno Quechua qxp 5684 qxp + language Punthamara xpt 5685 xpt + language Punu puu 5686 puu + language Puoc puo 5687 puo + language Puquina puq 5688 puq + language Puragi pru 5689 pru + language Purari iar 5690 iar + language Purepecha tsz 5691 tsz + language Puri prr 5692 prr + language Purik prx 5693 prx + language Purisimeño puy 5694 puy + language Puroik suv 5695 suv + language Puruborá pur 5696 pur + language Purum pub 5697 pub + language Putai mfl 5698 mfl + language Putoh put 5699 put + language Putukwam afe 5700 afe + language Puyo xpy 5701 xpy + language Puyo-Paekche xpp 5702 xpp + language Puyuma pyu 5703 pyu + language Pwaamei pme 5704 pme + language Pwapwâ pop 5705 pop + language Pwo Eastern Karen kjp 5706 kjp + language Pwo Northern Karen pww 5707 pww + language Pwo Western Karen pwo 5708 pwo + language Pyapun pcw 5709 pcw + language Pye Krumen pye 5710 pye + language Pyen pyy 5711 pyy + language Pyu (Myanmar) pyx 5712 pyx + language Pyu (Papua New Guinea) pby 5713 pby + language Páez pbb 5714 pbb + language Pááfang pfa 5715 pfa + language Päri lkr 5716 lkr + language Pémono pev 5717 pev + language Pévé lme 5718 lme + language Pökoot pko 5719 pko + language Pāli, Pali pli 5720 pli Pāli pi Pali + language Q'anjob'al kjb 5721 kjb + language Qabiao laq 5722 laq + language Qaqet byx 5723 byx + language Qashqa'i qxq 5724 qxq + language Qatabanian xqt 5725 xqt + language Qau gqu 5726 gqu + language Qawasqar alc 5727 alc + language Qila Muji ymq 5728 ymq + language Qimant ahg 5729 ahg + language Qiubei Zhuang zqe 5730 zqe + language Quapaw qua 5731 qua + language Quebec Sign Language fcs 5732 fcs + language Quechan yum 5733 yum + language Quechua que 5734 que qu + language Quenya qya 5735 qya + language Querétaro Otomi otq 5736 otq + language Quetzaltepec Mixe pxm 5737 pxm + language Queyu qvy 5738 qvy + language Quiavicuzas Zapotec zpj 5739 zpj + language Quileute qui 5740 qui + language Quinault qun 5741 qun + language Quinqui quq 5742 quq + language Quioquitani-Quierí Zapotec ztq 5743 ztq + language Quiotepec Chinantec chq 5744 chq + language Quiripi qyp 5745 qyp + language Rabha rah 5746 rah + language Rade rad 5747 rad + language Raetic xrr 5748 xrr + language Rahambuu raz 5749 raz + language Rajah Kabunsuwan Manobo mqk 5750 mqk + language Rajasthani raj 5751 raj + language Rajbanshi rjs 5752 rjs + language Raji rji 5753 rji + language Rajong rjg 5754 rjg + language Rajput Garasia gra 5755 gra + language Rakahanga-Manihiki rkh 5756 rkh + language Rakhine rki 5757 rki + language Ralte ral 5758 ral + language Rama rma 5759 rma + language Ramoaaina rai 5760 rai + language Ramopa kjx 5761 kjx + language Rampi lje 5762 lje + language Rana Tharu thr 5763 thr + language Rang rax 5764 rax + language Rangi lag 5765 lag + language Rangkas rgk 5766 rgk + language Ranglong rnl 5767 rnl + language Rangpuri rkt 5768 rkt + language Rao rao 5769 rao + language Rapa ray 5770 ray + language Rapanui rap 5771 rap + language Rapoisi kyx 5772 kyx + language Rapting rpt 5773 rpt + language Rara Bakati' lra 5774 lra + language Rarotongan rar 5775 rar + language Rasawa rac 5776 rac + language Ratagnon btn 5777 btn + language Ratahan rth 5778 rth + language Rathawi rtw 5779 rtw + language Rathwi Bareli bgd 5780 bgd + language Raute rau 5781 rau + language Ravula yea 5782 yea + language Rawa rwo 5783 rwo + language Rawang raw 5784 raw + language Rawat jnl 5785 jnl + language Rawngtu Chin weu 5786 weu + language Rawo rwa 5787 rwa + language Rayón Zoque zor 5788 zor + language Razajerdi rat 5789 rat + language Red Gelao gir 5790 gir + language Reel atu 5791 atu + language Rejang rej 5792 rej + language Rejang Kayan ree 5793 ree + language Reli rei 5794 rei + language Rema bow 5795 bow + language Rembarrnga rmb 5796 rmb + language Rembong reb 5797 reb + language Remo rem 5798 rem + language Remontado Dumagat agv 5799 agv + language Rempi rmp 5800 rmp + language Remun lkj 5801 lkj + language Rendille rel 5802 rel + language Rengao ren 5803 ren + language Rennell-Bellona mnv 5804 mnv + language Repanbitip rpn 5805 rpn + language Rer Bare rer 5806 rer + language Rerau rea 5807 rea + language Rerep pgk 5808 pgk + language Reshe res 5809 res + language Resígaro rgr 5810 rgr + language Retta ret 5811 ret + language Reyesano rey 5812 rey + language Riang (India) ria 5813 ria + language Riang Lai yin 5814 yin + language Riang Lang ril 5815 ril + language Riantana ran 5816 ran + language Ribun rir 5817 rir + language Rigwe iri 5818 iri + language Rikbaktsa rkb 5819 rkb + language Rinconada Bikol bto 5820 bto + language Rincón Zapotec zar 5821 zar + language Ringgou rgu 5822 rgu + language Ririo rri 5823 rri + language Rishiwa rsw 5824 rsw + language Ritharrngu rit 5825 rit + language Riung riu 5826 riu + language Riverain Sango snj 5827 snj + language Rmeet lbn 5828 lbn + language Rogo rod 5829 rod + language Rohingya rhg 5830 rhg + language Roma rmm 5831 rmm + language Romagnol rgn 5832 rgn + language Romam rmx 5833 rmx + language Romanian ron 5834 ron rum ro + language Romanian Sign Language rms 5835 rms + language Romano-Greek rge 5836 rge + language Romano-Serbian rsb 5837 rsb + language Romanova rmv 5838 rmv + language Romansh roh 5839 roh rm + language Romany rom 5840 rom + language Romblomanon rol 5841 rol + language Rombo rof 5842 rof + language Romkun rmk 5843 rmk + language Ron cla 5844 cla + language Ronga rng 5845 rng + language Rongga ror 5846 ror + language Rongmei Naga nbu 5847 nbu + language Rongpo rnp 5848 rnp + language Ronji roe 5849 roe + language Roon rnn 5850 rnn + language Roria rga 5851 rga + language Rotokas roo 5852 roo + language Rotuman rtm 5853 rtm + language Roviana rug 5854 rug + language Ruching Palaung pce 5855 pce + language Rudbari rdb 5856 rdb + language Rufiji rui 5857 rui + language Ruga ruh 5858 ruh + language Rukai dru 5859 dru + language Ruma ruz 5860 ruz + language Rumai Palaung rbb 5861 rbb + language Rumu klq 5862 klq + language Rundi, Kirundi run 5863 run Rundi rn Kirundi + language Runga rou 5864 rou + language Rungtu Chin rtc 5865 rtc + language Rungus drg 5866 drg + language Rungwa rnw 5867 rnw + language Russia Buriat bxr 5868 bxr + language Russian rus 5869 rus ru + language Russian Sign Language rsl 5870 rsl + language Rusyn rue 5871 rue + language Ruthenian rsk 5872 rsk + language Rutul rut 5873 rut + language Ruuli ruc 5874 ruc + language Ruund rnd 5875 rnd + language Ruwila rwl 5876 rwl + language Rwa rwk 5877 rwk + language Rwandan Sign Language rsn 5878 rsn + language Réunion Creole French rcf 5879 rcf + language Rāziḥī rzh 5880 rzh + language S'gaw Karen ksw 5881 ksw + language Sa sax 5882 sax + language Sa'a apb 5883 apb + language Sa'ban snv 5884 snv + language Sa'och scq 5885 scq + language Saafi-Saafi sav 5886 sav + language Saam raq 5887 raq + language Saamia lsm 5888 lsm + language Saaroa sxr 5889 sxr + language Saba saa 5890 saa + language Sabaean xsa 5891 xsa + language Sabah Bisaya bsy 5892 bsy + language Sabah Malay msi 5893 msi + language Sabanê sae 5894 sae + language Sabaot spy 5895 spy + language Sabine sbv 5896 sbv + language Sabu hvn 5897 hvn + language Sabüm sbo 5898 sbo + language Sacapulteco quv 5899 quv + language Sadri sck 5900 sck + language Saek skb 5901 skb + language Saep spd 5902 spd + language Safaliba saf 5903 saf + language Safeyoka apz 5904 apz + language Safwa sbk 5905 sbk + language Sagala sbm 5906 sbm + language Sagalla tga 5907 tga + language Saho ssy 5908 ssy + language Sahu saj 5909 saj + language Saidi Arabic aec 5910 aec + language Saint Lucian Creole French acf 5911 acf + language Saisiyat xsy 5912 xsy + language Sajalong sjl 5913 sjl + language Sajau Basap sjb 5914 sjb + language Sakachep sch 5915 sch + language Sakalava Malagasy skg 5916 skg + language Sakao sku 5917 sku + language Sakata skt 5918 skt + language Sake sak 5919 sak + language Sakirabiá skf 5920 skf + language Sakizaya szy 5921 szy + language Sala shq 5922 shq + language Salampasu slx 5923 slx + language Salar slr 5924 slr + language Salas sgu 5925 sgu + language Salasaca Highland Quichua qxl 5926 qxl + language Salawati xmx 5927 xmx + language Saleman sau 5928 sau + language Saliba sbe 5929 sbe + language Salinan sln 5930 sln + language Sallands sdz 5931 sdz + language Salt-Yui sll 5932 sll + language Saluan loe 5933 loe + language Salumá slj 5934 slj + language Salvadoran Sign Language esn 5935 esn + language Sam snx 5936 snx + language Samaritan smp 5937 smp + language Samaritan Aramaic sam 5938 sam + language Samarokena tmj 5939 tmj + language Samatao ysd 5940 ysd + language Samay syx 5941 syx + language Samba smx 5942 smx + language Samba Daka ccg 5943 ccg + language Samba Leko ndi 5944 ndi + language Sambal xsb 5945 xsb + language Sambalpuri spv 5946 spv + language Sambe xab 5947 xab + language Samberigi ssx 5948 ssx + language Samburu saq 5949 saq + language Samei smh 5950 smh + language Samo smq 5951 smq + language Samoan smo 5952 smo sm + language Samogitian sgs 5953 sgs + language Samosa swm 5954 swm + language Sampang rav 5955 rav + language Samre sxm 5956 sxm + language Samtao stu 5957 stu + language Samvedi smv 5958 smv + language San Agustín Mixtepec Zapotec ztm 5959 ztm + language San Baltazar Loxicha Zapotec zpx 5960 zpx + language San Blas Kuna cuk 5961 cuk + language San Dionisio Del Mar Huave hve 5962 hve + language San Felipe Otlaltepec Popoloca pow 5963 pow + language San Francisco Del Mar Huave hue 5964 hue + language San Francisco Matlatzinca mat 5965 mat + language San Jerónimo Tecóatl Mazatec maa 5966 maa + language San Juan Atzingo Popoloca poe 5967 poe + language San Juan Colorado Mixtec mjc 5968 mjc + language San Juan Teita Mixtec xtj 5969 xtj + language San Luís Temalacayuca Popoloca pps 5970 pps + language San Marcos Tlacoyalco Popoloca pls 5971 pls + language San Martín Itunyoso Triqui trq 5972 trq + language San Martín Quechua qvs 5973 qvs + language San Mateo Del Mar Huave huv 5974 huv + language San Miguel Creole French scf 5975 scf + language San Miguel El Grande Mixtec mig 5976 mig + language San Miguel Piedras Mixtec xtp 5977 xtp + language San Pedro Amuzgos Amuzgo azg 5978 azg + language San Pedro Quiatoni Zapotec zpf 5979 zpf + language San Salvador Kongo kwy 5980 kwy + language San Vicente Coatlán Zapotec zpt 5981 zpt + language Sanaani Arabic ayn 5982 ayn + language Sanapaná spn 5983 spn + language Sandawe sad 5984 sad + language Sanga (Democratic Republic of Congo) sng 5985 sng + language Sanga (Nigeria) xsn 5986 xsn + language Sanggau scg 5987 scg + language Sangil snl 5988 snl + language Sangir sxn 5989 sxn + language Sangisari sgr 5990 sgr + language Sangkong sgk 5991 sgk + language Sanglechi sgy 5992 sgy + language Sango sag 5993 sag sg + language Sangtam Naga nsa 5994 nsa + language Sangu (Gabon) snq 5995 snq + language Sangu (Tanzania) sbp 5996 sbp + language Sani ysn 5997 ysn + language Sanie ysy 5998 ysy + language Saniyo-Hiyewe sny 5999 sny + language Sankaran Maninka msc 6000 msc + language Sansi ssi 6001 ssi + language Sanskrit (Saṁskṛta), Sanskrit san 6002 san Sanskrit (Saṁskṛta) sa Sanskrit + language Santa Ana de Tusi Pasco Quechua qxt 6003 qxt + language Santa Catarina Albarradas Zapotec ztn 6004 ztn + language Santa Inés Ahuatempan Popoloca pca 6005 pca + language Santa Inés Yatzechi Zapotec zpn 6006 zpn + language Santa Lucía Monteverde Mixtec mdv 6007 mdv + language Santa María Del Mar Huave hvv 6008 hvv + language Santa María La Alta Nahuatl nhz 6009 nhz + language Santa María Quiegolani Zapotec zpi 6010 zpi + language Santa María Zacatepec Mixtec mza 6011 mza + language Santa Teresa Cora cok 6012 cok + language Santali sat 6013 sat + language Santiago Xanica Zapotec zpr 6014 zpr + language Santiago del Estero Quichua qus 6015 qus + language Santo Domingo Albarradas Zapotec zas 6016 zas + language Sanumá xsu 6017 xsu + language Saparua spr 6018 spr + language Sapo krn 6019 krn + language Saponi spi 6020 spi + language Saposa sps 6021 sps + language Sapuan spu 6022 spu + language Sapé spc 6023 spc + language Sar mwm 6024 mwm + language Sara sre 6025 sre + language Sara Kaba sbz 6026 sbz + language Sara Kaba Deme kwg 6027 kwg + language Sara Kaba Náà kwv 6028 kwv + language Saraiki skr 6029 skr + language Saramaccan srm 6030 srm + language Sarangani Blaan bps 6031 bps + language Sarangani Manobo mbs 6032 mbs + language Sarasira zsa 6033 zsa + language Saraveca sar 6034 sar + language Sardinian srd 6035 srd sc + language Sari asj 6036 asj + language Sarikoli srh 6037 srh + language Sarli sdf 6038 sdf + language Sarsi srs 6039 srs + language Sartang onp 6040 onp + language Sarua swy 6041 swy + language Sarudu sdu 6042 sdu + language Saruga sra 6043 sra + language Sasak sas 6044 sas + language Sasaru sxs 6045 sxs + language Sassarese Sardinian sdc 6046 sdc + language Satawalese stw 6047 stw + language Saterfriesisch stq 6048 stq + language Sateré-Mawé mav 6049 mav + language Saudi Arabian Sign Language sdl 6050 sdl + language Sauraseni Prākrit psu 6051 psu + language Saurashtra saz 6052 saz + language Sauri srt 6053 srt + language Sauria Paharia mjt 6054 mjt + language Sause sao 6055 sao + language Sausi ssj 6056 ssj + language Savi sdg 6057 sdg + language Savosavo svs 6058 svs + language Sawai szw 6059 szw + language Saweru swr 6060 swr + language Sawi saw 6061 saw + language Sawila swt 6062 swt + language Sawknah swn 6063 swn + language Saxwe Gbe sxw 6064 sxw + language Saya say 6065 say + language Sayula Popoluca pos 6066 pos + language Scots sco 6067 sco + language Scythian xsc 6068 xsc + language Sea Island Creole English gul 6069 gul + language Seba kdg 6070 kdg + language Sebat Bet Gurage sgw 6071 sgw + language Seberuang sbx 6072 sbx + language Sebop sib 6073 sib + language Sechelt sec 6074 sec + language Secoya sey 6075 sey + language Sedang sed 6076 sed + language Sediq trv 6077 trv + language Sedoa tvw 6078 tvw + language Seeku sos 6079 sos + language Segai sge 6080 sge + language Segeju seg 6081 seg + language Seget sbg 6082 sbg + language Sehwi sfw 6083 sfw + language Seimat ssg 6084 ssg + language Seit-Kaitetu hik 6085 hik + language Sekani sek 6086 sek + language Sekapan skp 6087 skp + language Sekar skz 6088 skz + language Seke (Nepal) skj 6089 skj + language Seke (Vanuatu) ske 6090 ske + language Sekele vaj 6091 vaj + language Seki syi 6092 syi + language Seko Padang skx 6093 skx + language Seko Tengah sko 6094 sko + language Sekpele lip 6095 lip + language Selangor Sign Language kgi 6096 kgi + language Selaru slu 6097 slu + language Selayar sly 6098 sly + language Selee snw 6099 snw + language Selepet spl 6100 spl + language Selian sxl 6101 sxl + language Selkup sel 6102 sel + language Selungai Murut slg 6103 slg + language Seluwasan sws 6104 sws + language Semai sea 6105 sea + language Semandang sdq 6106 sdq + language Semaq Beri szc 6107 szc + language Sembakung Murut sbr 6108 sbr + language Semelai sza 6109 sza + language Semimi etz 6110 etz + language Semnam ssm 6111 ssm + language Semnani smy 6112 smy + language Sempan xse 6113 xse + language Sena seh 6114 seh + language Senara Sénoufo seq 6115 seq + language Senaya syn 6116 syn + language Sene sej 6117 sej + language Seneca see 6118 see + language Sened sds 6119 sds + language Sengele szg 6120 szg + language Senggi snu 6121 snu + language Sengo spk 6122 spk + language Sengseng ssz 6123 ssz + language Senhaja De Srair sjs 6124 sjs + language Sensi sni 6125 sni + language Sentani set 6126 set + language Senthang Chin sez 6127 sez + language Sentinel std 6128 std + language Sepa (Indonesia) spb 6129 spb + language Sepa (Papua New Guinea) spe 6130 spe + language Sepik Iwam iws 6131 iws + language Sera sry 6132 sry + language Serbian srp 6133 srp scc sr + language Serbo-Croatian hbs 6134 hbs + language Sere swf 6135 swf + language Serer srr 6136 srr + language Seri sei 6137 sei + language Serili sve 6138 sve + language Seroa kqu 6139 kqu + language Serrano ser 6140 ser + language Serua srw 6141 srw + language Serudung Murut srk 6142 srk + language Serui-Laut seu 6143 seu + language Seselwa Creole French crs 6144 crs + language Seta stf 6145 stf + language Setaman stm 6146 stm + language Seti sbi 6147 sbi + language Settla sta 6148 sta + language Severn Ojibwa ojs 6149 ojs + language Sewa Bay sew 6150 sew + language Seychelles Sign Language lsw 6151 lsw + language Seze sze 6152 sze + language Sha scw 6153 scw + language Shabak sdb 6154 sdb + language Shahmirzadi srz 6155 srz + language Shahrudi shm 6156 shm + language Shall-Zwall sha 6157 sha + language Shama-Sambuga sqa 6158 sqa + language Shamang xsh 6159 xsh + language Shambala ksb 6160 ksb + language Shan shn 6161 shn + language Shanenawa swo 6162 swo + language Shanga sho 6163 sho + language Sharanahua mcd 6164 mcd + language Shark Bay ssv 6165 ssv + language Sharwa swq 6166 swq + language Shasta sht 6167 sht + language Shatt shj 6168 shj + language Shau sqh 6169 sqh + language Shawnee sjw 6170 sjw + language She shx 6171 shx + language Shehri shv 6172 shv + language Shekhawati swv 6173 swv + language Shekkacho moy 6174 moy + language Sheko she 6175 she + language Shelta sth 6176 sth + language Shempire Senoufo seb 6177 seb + language Shendu shl 6178 shl + language Sheni scv 6179 scv + language Sherbro bun 6180 bun + language Sherdukpen sdp 6181 sdp + language Sherpa xsr 6182 xsr + language Sheshi Kham kip 6183 kip + language Shi shr 6184 shr + language Shihhi Arabic ssh 6185 ssh + language Shiki gua 6186 gua + language Shilluk shk 6187 shk + language Shina scl 6188 scl + language Shipibo-Conibo shp 6189 shp + language Sholaga sle 6190 sle + language Shom Peng sii 6191 sii + language Shona sna 6192 sna sn + language Shoo-Minda-Nye bcv 6193 bcv + language Shor cjs 6194 cjs + language Shoshoni shh 6195 shh + language Shua shg 6196 shg + language Shuadit sdt 6197 sdt + language Shuar jiv 6198 jiv + language Shubi suj 6199 suj + language Shughni sgh 6200 sgh + language Shuhi sxg 6201 sxg + language Shumashti sts 6202 sts + language Shumcho scu 6203 scu + language Shuswap shs 6204 shs + language Shwai shw 6205 shw + language Shwe Palaung pll 6206 pll + language Sialum slw 6207 slw + language Siamou sif 6208 sif + language Sian spg 6209 spg + language Siane snp 6210 snp + language Siang sya 6211 sya + language Siar-Lak sjr 6212 sjr + language Siawi mmp 6213 mmp + language Sibe nco 6214 nco + language Siberian Tatar sty 6215 sty + language Sibu Melanau sdx 6216 sdx + language Sicanian sxc 6217 sxc + language Sicel scx 6218 scx + language Sichuan Yi, Nuosu iii 6219 iii ii Sichuan Yi Nuosu + language Sicilian scn 6220 scn + language Siculo Arabic sqr 6221 sqr + language Sidamo sid 6222 sid + language Sidetic xsd 6223 xsd + language Sie erg 6224 erg + language Sierra Leone Sign Language sgx 6225 sgx + language Sierra Negra Nahuatl nsu 6226 nsu + language Sierra de Juárez Zapotec zaa 6227 zaa + language Sighu sxe 6228 sxe + language Sihan snr 6229 snr + language Sihuas Ancash Quechua qws 6230 qws + language Sika ski 6231 ski + language Sikaiana sky 6232 sky + language Sikaritai tty 6233 tty + language Sikiana sik 6234 sik + language Sikkimese sip 6235 sip + language Siksika bla 6236 bla + language Sikule skh 6237 skh + language Sila slt 6238 slt + language Silacayoapan Mixtec mks 6239 mks + language Sileibi sbq 6240 sbq + language Silesian szl 6241 szl + language Silimo wul 6242 wul + language Siliput mkc 6243 mkc + language Silopi xsp 6244 xsp + language Silt'e stv 6245 stv + language Simaa sie 6246 sie + language Simba sbw 6247 sbw + language Simbali smg 6248 smg + language Simbari smb 6249 smb + language Simbo sbb 6250 sbb + language Simeku smz 6251 smz + language Simeulue smr 6252 smr + language Simte smt 6253 smt + language Sinagen siu 6254 siu + language Sinasina sst 6255 sst + language Sinaugoro snc 6256 snc + language Sindarin sjn 6257 sjn + language Sindhi snd 6258 snd sd + language Sindhi Bhil sbn 6259 sbn + language Sindihui Mixtec xts 6260 xts + language Singa sgm 6261 sgm + language Singapore Sign Language sls 6262 sls + language Singpho sgp 6263 sgp + language Sinhala, Sinhalese sin 6264 sin si Sinhala Sinhalese + language Sinicahua Mixtec xti 6265 xti + language Sininkere skq 6266 skq + language Sinte Romani rmo 6267 rmo + language Sinyar sys 6268 sys + language Sio xsi 6269 xsi + language Siona snn 6270 snn + language Sipacapense qum 6271 qum + language Sira swj 6272 swj + language Siraya fos 6273 fos + language Sirenik Yupik ysr 6274 ysr + language Siri sir 6275 sir + language Siriano sri 6276 sri + language Sirionó srq 6277 srq + language Sirmauri srx 6278 srx + language Siroi ssd 6279 ssd + language Sissala sld 6280 sld + language Sissano sso 6281 sso + language Siuslaw sis 6282 sis + language Sivandi siy 6283 siy + language Sivia Sign Language lsv 6284 lsv + language Siwai siw 6285 siw + language Siwi siz 6286 siz + language Siwu akp 6287 akp + language Siyin Chin csy 6288 csy + language Skagit ska 6289 ska + language Skalvian svx 6290 svx + language Skepi Creole Dutch skw 6291 skw + language Skolt Sami sms 6292 sms + language Skou skv 6293 skv + language Slave (Athapascan) den 6294 den + language Slavomolisano svm 6295 svm + language Slovak slk 6296 slk slo sk + language Slovakian Sign Language svk 6297 svk + language Slovenian, Slovene slv 6298 slv sl Slovenian Slovene + language Small Flowery Miao sfm 6299 sfm + language Smärky Kanum kxq 6300 kxq + language Snohomish sno 6301 sno + language So (Democratic Republic of Congo) soc 6302 soc + language So'a ssq 6303 ssq + language Sobei sob 6304 sob + language Sochiapam Chinantec cso 6305 cso + language Soga xog 6306 xog + language Sogdian sog 6307 sog + language Soi soj 6308 soj + language Sokoro sok 6309 sok + language Solano xso 6310 xso + language Soli sby 6311 sby + language Solomon Islands Sign Language szs 6312 szs + language Solong aaw 6313 aaw + language Solos sol 6314 sol + language Som smc 6315 smc + language Somali som 6316 som so + language Somba-Siawari bmu 6317 bmu + language Somrai sor 6318 sor + language Somray smu 6319 smu + language Somyev kgt 6320 kgt + language Sonaga ysg 6321 ysg + language Sonde shc 6322 shc + language Songe sop 6323 sop + language Songlai Chin csj 6324 csj + language Songo soo 6325 soo + language Songomeno soe 6326 soe + language Songoora sod 6327 sod + language Sonha soi 6328 soi + language Sonia siq 6329 siq + language Soninke snk 6330 snk + language Sonsorol sov 6331 sov + language Soo teu 6332 teu + language Sop urw 6333 urw + language Soqotri sqt 6334 sqt + language Sora srb 6335 srb + language Sori-Harengan sbh 6336 sbh + language Sorkhei sqo 6337 sqo + language Sorothaptic sxo 6338 sxo + language Sorsogon Ayta ays 6339 ays + language Sos Kundi sdk 6340 sdk + language Sota Kanum krz 6341 krz + language Sou sqq 6342 sqq + language Sou Nama tlt 6343 tlt + language Sou Upaa wha 6344 wha + language South African Sign Language sfs 6345 sfs + language South Awyu aws 6346 aws + language South Azerbaijani azb 6347 azb + language South Bolivian Quechua quh 6348 quh + language South Central Banda lnl 6349 lnl + language South Central Dinka dib 6350 dib + language South Efate erk 6351 erk + language South Fali fal 6352 fal + language South Giziga giz 6353 giz + language South Lembata lmf 6354 lmf + language South Marquesan mqm 6355 mqm + language South Muyu kts 6356 kts + language South Ndebele, Southern Ndebele nbl 6357 nbl South Ndebele nr Southern Ndebele + language South Nuaulu nxl 6358 nxl + language South Picene spx 6359 spx + language South Slavey xsl 6360 xsl + language South Tairora omw 6361 omw + language South Ucayali Ashéninka cpy 6362 cpy + language South Watut mcy 6363 mcy + language South West Bay sns 6364 sns + language Southeast Ambrym tvk 6365 tvk + language Southeast Babar vbb 6366 vbb + language Southeast Ijo ijs 6367 ijs + language Southeast Pashai psi 6368 psi + language Southeast Tasmanian xpf 6369 xpf + language Southeastern Dinka dks 6370 dks + language Southeastern Ixtlán Zapotec zpd 6371 zpd + language Southeastern Kolami nit 6372 nit + language Southeastern Nochixtlán Mixtec mxy 6373 mxy + language Southeastern Pomo pom 6374 pom + language Southeastern Puebla Nahuatl npl 6375 npl + language Southeastern Tarahumara tcu 6376 tcu + language Southeastern Tepehuan stp 6377 stp + language Southern Alta agy 6378 agy + language Southern Altai alt 6379 alt + language Southern Amami-Oshima ams 6380 ams + language Southern Aymara ayc 6381 ayc + language Southern Bai bfs 6382 bfs + language Southern Balochi bcc 6383 bcc + language Southern Betsimisaraka Malagasy bzc 6384 bzc + language Southern Binukidnon mtw 6385 mtw + language Southern Birifor biv 6386 biv + language Southern Bobo Madaré bwq 6387 bwq + language Southern Bontok obk 6388 obk + language Southern Carrier caf 6389 caf + language Southern Catanduanes Bikol bln 6390 bln + language Southern Conchucos Ancash Quechua qxo 6391 qxo + language Southern Dagaare dga 6392 dga + language Southern Dong kmc 6393 kmc + language Southern East Cree crj 6394 crj + language Southern Ghale ghe 6395 ghe + language Southern Grebo grj 6396 grj + language Southern Guiyang Hmong hmy 6397 hmy + language Southern Haida hax 6398 hax + language Southern Hindko hnd 6399 hnd + language Southern Kalapuya sxk 6400 sxk + language Southern Kalinga ksc 6401 ksc + language Southern Katang sct 6402 sct + language Southern Kisi kss 6403 kss + language Southern Kiwai kjd 6404 kjd + language Southern Kurdish sdh 6405 sdh + language Southern Lolopo ysp 6406 ysp + language Southern Luri luz 6407 luz + language Southern Ma'di snm 6408 snm + language Southern Mashan Hmong hma 6409 hma + language Southern Mnong mnn 6410 mnn + language Southern Muji ymc 6411 ymc + language Southern Nago nqg 6412 nqg + language Southern Nambikuára nab 6413 nab + language Southern Ngbandi nbw 6414 nbw + language Southern Nicobarese nik 6415 nik + language Southern Nisu nsd 6416 nsd + language Southern Nuni nnw 6417 nnw + language Southern Ohlone css 6418 css + language Southern One osu 6419 osu + language Southern Pame pmz 6420 pmz + language Southern Pashto pbt 6421 pbt + language Southern Pastaza Quechua qup 6422 qup + language Southern Ping Chinese csp 6423 csp + language Southern Pomo peq 6424 peq + language Southern Puebla Mixtec mit 6425 mit + language Southern Puget Sound Salish slh 6426 slh + language Southern Pumi pmj 6427 pmj + language Southern Qiandong Miao hms 6428 hms + language Southern Qiang qxs 6429 qxs + language Southern Rengma Naga nre 6430 nre + language Southern Rincon Zapotec zsr 6431 zsr + language Southern Roglai rgs 6432 rgs + language Southern Sama ssb 6433 ssb + language Southern Sami sma 6434 sma + language Southern Samo sbd 6435 sbd + language Southern Sierra Miwok skd 6436 skd + language Southern Sorsoganon srv 6437 srv + language Southern Sotho sot 6438 sot st + language Southern Subanen laa 6439 laa + language Southern Thai sou 6440 sou + language Southern Tidung itd 6441 itd + language Southern Tiwa tix 6442 tix + language Southern Toussian wib 6443 wib + language Southern Tujia tjs 6444 tjs + language Southern Tutchone tce 6445 tce + language Southern Uzbek uzs 6446 uzs + language Southern Yamphu lrr 6447 lrr + language Southern Yukaghir yux 6448 yux + language Southwest Gbaya gso 6449 gso + language Southwest Palawano plv 6450 plv + language Southwest Pashai psh 6451 psh + language Southwest Tanna nwi 6452 nwi + language Southwestern Bontok vbk 6453 vbk + language Southwestern Dinka dik 6454 dik + language Southwestern Fars fay 6455 fay + language Southwestern Guiyang Hmong hmg 6456 hmg + language Southwestern Huishui Hmong hmh 6457 hmh + language Southwestern Nisu nsv 6458 nsv + language Southwestern Tarahumara twr 6459 twr + language Southwestern Tasmanian xpx 6460 xpx + language Southwestern Tepehuan tla 6461 tla + language Southwestern Tlaxiaco Mixtec meh 6462 meh + language Sowa sww 6463 sww + language Sowanda sow 6464 sow + language Soyaltepec Mazatec vmp 6465 vmp + language Soyaltepec Mixtec vmq 6466 vmq + language Spanish Sign Language ssp 6467 ssp + language Spiti Bhoti spt 6468 spt + language Spokane spo 6469 spo + language Squamish squ 6470 squ + language Sranan Tongo srn 6471 srn + language Sri Lankan Creole Malay sci 6472 sci + language Sri Lankan Sign Language sqs 6473 sqs + language Standard Arabic arb 6474 arb + language Standard Estonian ekk 6475 ekk + language Standard Latvian lvs 6476 lvs + language Standard Malay zsm 6477 zsm + language Standard Moroccan Tamazight zgh 6478 zgh + language Stellingwerfs stl 6479 stl + language Stod Bhoti sbu 6480 sbu + language Stoney sto 6481 sto + language Straits Salish str 6482 str + language Suabo szp 6483 szp + language Suarmin seo 6484 seo + language Suau swp 6485 swp + language Suba sxb 6486 sxb + language Suba-Simbiti ssc 6487 ssc + language Subi xsj 6488 xsj + language Subiya sbs 6489 sbs + language Subtiaba sut 6490 sut + language Sudanese Arabic apd 6491 apd + language Sudanese Creole Arabic pga 6492 pga + language Sudest tgo 6493 tgo + language Sudovian xsv 6494 xsv + language Suena sue 6495 sue + language Suga sgi 6496 sgi + language Suganga sug 6497 sug + language Sugut Dusun kzs 6498 kzs + language Sui swi 6499 swi + language Suki sui 6500 sui + language Suku sub 6501 sub + language Sukuma suk 6502 suk + language Sukur syk 6503 syk + language Sukurum zsu 6504 zsu + language Sula szn 6505 szn + language Sulka sua 6506 sua + language Sulod srg 6507 srg + language Suma sqm 6508 sqm + language Sumariup siv 6509 siv + language Sumau six 6510 six + language Sumbawa smw 6511 smw + language Sumbwa suw 6512 suw + language Sumerian sux 6513 sux + language Sumi Naga nsm 6514 nsm + language Sumtu Chin csv 6515 csv + language Sunam ssk 6516 ssk + language Sundanese sun 6517 sun su + language Sunwar suz 6518 suz + language Suoy syo 6519 syo + language Supyire Senoufo spp 6520 spp + language Sur tdl 6521 tdl + language Surbakhal sbj 6522 sbj + language Surgujia sgj 6523 sgj + language Surigaonon sgd 6524 sgd + language Surjapuri sjp 6525 sjp + language Sursurunga sgz 6526 sgz + language Suruahá swx 6527 swx + language Surubu sde 6528 sde + language Suruí sru 6529 sru + language Suruí Do Pará mdz 6530 mdz + language Susquehannock sqn 6531 sqn + language Susu sus 6532 sus + language Susuami ssu 6533 ssu + language Suundi sdj 6534 sdj + language Suwawa swu 6535 swu + language Suyá suy 6536 suy + language Svan sva 6537 sva + language Swabian swg 6538 swg + language Swahili (individual language) swh 6539 swh + language Swahili, Swahili (macrolanguage) swa 6540 swa sw Swahili Swahili (macrolanguage) + language Swampy Cree csw 6541 csw + language Swati ssw 6542 ssw ss + language Swedish swe 6543 swe sv + language Swedish Sign Language swl 6544 swl + language Swiss German gsw 6545 gsw + language Swiss-French Sign Language ssr 6546 ssr + language Swiss-German Sign Language sgg 6547 sgg + language Swiss-Italian Sign Language slf 6548 slf + language Swo sox 6549 sox + language Syenara Senoufo shz 6550 shz + language Sylheti syl 6551 syl + language Syriac syr 6552 syr + language Sáliba slc 6553 slc + language São Paulo Kaingáng zkp 6554 zkp + language Sãotomense cri 6555 cri + language Sìcìté Sénoufo sep 6556 sep + language Sô sss 6557 sss + language T'apo lgn 6558 lgn + language T'en tct 6559 tct + language Ta'izzi-Adeni Arabic acq 6560 acq + language Taabwa tap 6561 tap + language Tabaa Zapotec zat 6562 zat + language Tabaru tby 6563 tby + language Tabasco Chontal chf 6564 chf + language Tabasco Nahuatl nhc 6565 nhc + language Tabasco Zoque zoq 6566 zoq + language Tabassaran tab 6567 tab + language Tabla tnm 6568 tnm + language Tabo knv 6569 knv + language Tabriak tzx 6570 tzx + language Tacahua Mixtec xtt 6571 xtt + language Tacana tna 6572 tna + language Tachawit shy 6573 shy + language Tachelhit shi 6574 shi + language Tachoni lts 6575 lts + language Tadaksahak dsq 6576 dsq + language Tado klw 6577 klw + language Tadyawan tdy 6578 tdy + language Tae' rob 6579 rob + language Tafi tcd 6580 tcd + language Tagabawa bgs 6581 bgs + language Tagakaulo klg 6582 klg + language Tagal Murut mvv 6583 mvv + language Tagalaka tgz 6584 tgz + language Tagalog tgl 6585 tgl tl + language Tagargrent oua 6586 oua + language Tagbanwa tbw 6587 tbw + language Tagbu tbm 6588 tbm + language Tagdal tda 6589 tda + language Tagin tgj 6590 tgj + language Tagish tgx 6591 tgx + language Tagoi tag 6592 tag + language Tagwana Senoufo tgw 6593 tgw + language Tahaggart Tamahaq thv 6594 thv + language Tahitian tah 6595 tah ty + language Tahltan tht 6596 tht + language Tai taw 6597 taw + language Tai Daeng tyr 6598 tyr + language Tai Dam blt 6599 blt + language Tai Do tyj 6600 tyj + language Tai Dón twh 6601 twh + language Tai Hongjin tiz 6602 tiz + language Tai Laing tjl 6603 tjl + language Tai Loi tlq 6604 tlq + language Tai Long thi 6605 thi + language Tai Nüa tdd 6606 tdd + language Tai Pao tpo 6607 tpo + language Tai Thanh tmm 6608 tmm + language Tai Ya cuu 6609 cuu + language Taiap gpn 6610 gpn + language Taikat aos 6611 aos + language Tainae ago 6612 ago + language Taino tnq 6613 tnq + language Tairaha bxa 6614 bxa + language Tairuma uar 6615 uar + language Taita dav 6616 dav + language Taivoan tvx 6617 tvx + language Taiwan Sign Language tss 6618 tss + language Taje pee 6619 pee + language Tajik tgk 6620 tgk tg + language Tajiki Arabic abh 6621 abh + language Tajio tdj 6622 tdj + language Tajuasohn tja 6623 tja + language Takelma tkm 6624 tkm + language Takestani tks 6625 tks + language Takia tbc 6626 tbc + language Takua tkz 6627 tkz + language Takuu nho 6628 nho + language Takwane tke 6629 tke + language Tal tal 6630 tal + language Tala tak 6631 tak + language Talaud tld 6632 tld + language Taliabu tlv 6633 tlv + language Talieng tdf 6634 tdf + language Talinga-Bwisi tlj 6635 tlj + language Talise tlr 6636 tlr + language Talodi tlo 6637 tlo + language Taloki tlk 6638 tlk + language Talondo' tln 6639 tln + language Talossan tzl 6640 tzl + language Talu yta 6641 yta + language Talysh tly 6642 tly + language Tama (Chad) tma 6643 tma + language Tama (Colombia) ten 6644 ten + language Tamagario tcg 6645 tcg + language Taman (Indonesia) tmn 6646 tmn + language Taman (Myanmar) tcl 6647 tcl + language Tamanaku tmz 6648 tmz + language Tamashek tmh 6649 tmh + language Tamasheq taq 6650 taq + language Tamazola Mixtec vmx 6651 vmx + language Tambas tdk 6652 tdk + language Tambora xxt 6653 xxt + language Tambotalo tls 6654 tls + language Tami tmy 6655 tmy + language Tamil tam 6656 tam ta + language Tamki tax 6657 tax + language Tamnim Citak tml 6658 tml + language Tampias Lobu low 6659 low + language Tampuan tpu 6660 tpu + language Tampulma tpm 6661 tpm + language Tanacross tcb 6662 tcb + language Tanahmerah tcm 6663 tcm + language Tanaina tfn 6664 tfn + language Tanapag tpv 6665 tpv + language Tandaganon tgn 6666 tgn + language Tandia tni 6667 tni + language Tandroy-Mahafaly Malagasy tdx 6668 tdx + language Tanema tnx 6669 tnx + language Tangale tan 6670 tan + language Tangchangya tnv 6671 tnv + language Tanggu tgu 6672 tgu + language Tangkhul Naga (India) nmf 6673 nmf + language Tangkhul Naga (Myanmar) ntx 6674 ntx + language Tangko tkx 6675 tkx + language Tanglang ytl 6676 ytl + language Tangoa tgp 6677 tgp + language Tanguat tbs 6678 tbs + language Tangut txg 6679 txg + language Tanimbili tbe 6680 tbe + language Tanimuca-Retuarã tnc 6681 tnc + language Tanjijili uji 6682 uji + language Tanosy Malagasy txy 6683 txy + language Tanudan Kalinga kml 6684 kml + language Tanzanian Sign Language tza 6685 tza + language Tapei afp 6686 afp + language Tapieté tpj 6687 tpj + language Tapirapé taf 6688 taf + language Tarao Naga tro 6689 tro + language Tareng tgr 6690 tgr + language Tariana tae 6691 tae + language Tarifit rif 6692 rif + language Tarjumo txj 6693 txj + language Tarok yer 6694 yer + language Tarpia tpf 6695 tpf + language Tartessian txr 6696 txr + language Taruma tdm 6697 tdm + language Tasawaq twq 6698 twq + language Tase Naga nst 6699 nst + language Tasmate tmt 6700 tmt + language Tataltepec Chatino cta 6701 cta + language Tatana txx 6702 txx + language Tatar tat 6703 tat tt + language Tatuyo tav 6704 tav + language Tauade ttd 6705 ttd + language Taulil tuh 6706 tuh + language Taungyo tco 6707 tco + language Taupota tpa 6708 tpa + language Tause tad 6709 tad + language Taushiro trr 6710 trr + language Tausug tsg 6711 tsg + language Tauya tya 6712 tya + language Taveta tvs 6713 tvs + language Tavoyan tvn 6714 tvn + language Tavringer Romani rmu 6715 rmu + language Tawala tbo 6716 tbo + language Tawallammat Tamajaq ttq 6717 ttq + language Tawandê xtw 6718 xtw + language Tawang Monpa twm 6719 twm + language Tawara twl 6720 twl + language Taworta tbp 6721 tbp + language Tawoyan twy 6722 twy + language Tawr Chin tcp 6723 tcp + language Tay Boi tas 6724 tas + language Tay Khang tnu 6725 tnu + language Tayart Tamajeq thz 6726 thz + language Tayo cks 6727 cks + language Taznatit grr 6728 grr + language Tboli tbl 6729 tbl + language Tchitchege tck 6730 tck + language Tchumbuli bqa 6731 bqa + language Te'un tve 6732 tve + language Teanu tkw 6733 tkw + language Tebul Sign Language tsy 6734 tsy + language Tebul Ure Dogon dtu 6735 dtu + language Tecpatlán Totonac tcw 6736 tcw + language Tedaga tuq 6737 tuq + language Tedim Chin ctd 6738 ctd + language Tee tkq 6739 tkq + language Tefaro tfo 6740 tfo + language Tegali ras 6741 ras + language Tehit kps 6742 kps + language Tehuelche teh 6743 teh + language Tejalapan Zapotec ztt 6744 ztt + language Teke-Ebo ebo 6745 ebo + language Teke-Fuumu ifm 6746 ifm + language Teke-Kukuya kkw 6747 kkw + language Teke-Laali lli 6748 lli + language Teke-Nzikou nzu 6749 nzu + language Teke-Tege teg 6750 teg + language Teke-Tsaayi tyi 6751 tyi + language Teke-Tyee tyx 6752 tyx + language Tektiteko ttc 6753 ttc + language Tela-Masbuar tvm 6754 tvm + language Telefol tlf 6755 tlf + language Telugu tel 6756 tel te + language Tem kdh 6757 kdh + language Temacine Tamazight tjo 6758 tjo + language Temascaltepec Nahuatl nhv 6759 nhv + language Tembo (Kitembo) tbt 6760 tbt + language Tembo (Motembo) tmv 6761 tmv + language Tembé tqb 6762 tqb + language Teme tdo 6763 tdo + language Temein teq 6764 teq + language Temi soz 6765 soz + language Temiar tea 6766 tea + language Temoaya Otomi ott 6767 ott + language Temoq tmo 6768 tmo + language Temuan tmw 6769 tmw + language Ten'edn tnz 6770 tnz + language Tena Lowland Quichua quw 6771 quw + language Tenango Otomi otn 6772 otn + language Tene Kan Dogon dtk 6773 dtk + language Tenggarong Kutai Malay vkt 6774 vkt + language Tengger tes 6775 tes + language Tenharim pah 6776 pah + language Tenino tqn 6777 tqn + language Tenis tns 6778 tns + language Tennet tex 6779 tex + language Teop tio 6780 tio + language Teor tev 6781 tev + language Tepecano tep 6782 tep + language Tepetotutla Chinantec cnt 6783 cnt + language Tepeuxila Cuicatec cux 6784 cux + language Tepinapa Chinantec cte 6785 cte + language Tepo Krumen ted 6786 ted + language Ter Sami sjt 6787 sjt + language Tera ttr 6788 ttr + language Terebu trb 6789 trb + language Terei buo 6790 buo + language Tereno ter 6791 ter + language Teressa tef 6792 tef + language Tereweng twg 6793 twg + language Teribe tfr 6794 tfr + language Terik tec 6795 tec + language Termanu twu 6796 twu + language Ternate tft 6797 tft + language Ternateño tmg 6798 tmg + language Tesaka Malagasy tkg 6799 tkg + language Tese keg 6800 keg + language Teshenawa twc 6801 twc + language Teso teo 6802 teo + language Tetela tll 6803 tll + language Tetelcingo Nahuatl nhg 6804 nhg + language Tetete teb 6805 teb + language Tetserret tez 6806 tez + language Tetum tet 6807 tet + language Tetun Dili tdt 6808 tdt + language Teutila Cuicatec cut 6809 cut + language Tewa (Indonesia) twe 6810 twe + language Tewa (USA) tew 6811 tew + language Tewe twx 6812 twx + language Texcatepec Otomi otx 6813 otx + language Texistepec Popoluca poq 6814 poq + language Texmelucan Zapotec zpz 6815 zpz + language Tezoatlán Mixtec mxb 6816 mxb + language Tha thy 6817 thy + language Thachanadan thn 6818 thn + language Thado Chin tcz 6819 tcz + language Thai tha 6820 tha th + language Thai Sign Language tsq 6821 tsq + language Thai Song soa 6822 soa + language Thaiphum Chin cth 6823 cth + language Thakali ths 6824 ths + language Thangal Naga nki 6825 nki + language Thangmi thf 6826 thf + language Thao ssf 6827 ssf + language Tharaka thk 6828 thk + language Thawa xtv 6829 xtv + language Thaypan typ 6830 typ + language Thiin iin 6831 iin + language Tho tou 6832 tou + language Thompson thp 6833 thp + language Thopho ytp 6834 ytp + language Thracian txh 6835 txh + language Thu Lao tyl 6836 tyl + language Thulung tdh 6837 tdh + language Thur lth 6838 lth + language Thuri thu 6839 thu + language Tiagbamrin Aizi ahi 6840 ahi + language Tiale mnl 6841 mnl + language Tiang tbj 6842 tbj + language Tibea ngy 6843 ngy + language Tibetan Sign Language lsn 6844 lsn + language Tibetan, Tibetan Standard, Central bod 6845 bod Tibetan Tibetan Central Tibetan Standard Central tib bo + language Tichurong tcn 6846 tcn + language Ticuna tca 6847 tca + language Tidaá Mixtec mtx 6848 mtx + language Tidikelt Tamazight tia 6849 tia + language Tidore tvo 6850 tvo + language Tiemacèwè Bozo boo 6851 boo + language Tiene tii 6852 tii + language Tifal tif 6853 tif + language Tigak tgc 6854 tgc + language Tigon Mbembe nza 6855 nza + language Tigre tig 6856 tig + language Tigrinya tir 6857 tir ti + language Tii txq 6858 txq + language Tijaltepec Mixtec xtl 6859 xtl + language Tikar tik 6860 tik + language Tikopia tkp 6861 tkp + language Tilapa Otomi otl 6862 otl + language Tillamook til 6863 til + language Tilquiapan Zapotec zts 6864 zts + language Tilung tij 6865 tij + language Tima tms 6866 tms + language Timbe tim 6867 tim + language Timne tem 6868 tem + language Timor Pidgin tvy 6869 tvy + language Timucua tjm 6870 tjm + language Timugon Murut tih 6871 tih + language Tinani lbf 6872 lbf + language Tindi tin 6873 tin + language Tingui-Boto tgv 6874 tgv + language Tinigua tit 6875 tit + language Tinputz tpz 6876 tpz + language Tippera tpe 6877 tpe + language Tira tic 6878 tic + language Tirahi tra 6879 tra + language Tiranige Diga Dogon tde 6880 tde + language Tiri cir 6881 cir + language Tirmaga-Chai Suri suq 6882 suq + language Tiruray tiy 6883 tiy + language Tita tdq 6884 tdq + language Titan ttv 6885 ttv + language Tiv tiv 6886 tiv + language Tiwa lax 6887 lax + language Tiwi tiw 6888 tiw + language Tiyaa tyy 6889 tyy + language Tiéfo tiq 6890 tiq + language Tiéyaxo Bozo boz 6891 boz + language Tjungundji tjj 6892 tjj + language Tjupany tjp 6893 tjp + language Tjurruru tju 6894 tju + language Tlachichilco Tepehua tpt 6895 tpt + language Tlacoapa Me'phaa tpl 6896 tpl + language Tlacoatzintepec Chinantec ctl 6897 ctl + language Tlacolulita Zapotec zpk 6898 zpk + language Tlahuitoltepec Mixe mxp 6899 mxp + language Tlamacazapa Nahuatl nuz 6900 nuz + language Tlazoyaltepec Mixtec mqh 6901 mqh + language Tlicho dgr 6902 dgr + language Tlingit tli 6903 tli + language To toz 6904 toz + language To'abaita mlu 6905 mlu + language Toaripi tqo 6906 tqo + language Toba tob 6907 tob + language Toba-Maskoy tmf 6908 tmf + language Tobagonian Creole English tgh 6909 tgh + language Tobanga tng 6910 tng + language Tobati tti 6911 tti + language Tobelo tlb 6912 tlb + language Tobian tox 6913 tox + language Tobilung tgb 6914 tgb + language Tobo tbv 6915 tbv + language Tocantins Asurini asu 6916 asu + language Tocho taz 6917 taz + language Toda tcx 6918 tcx + language Todrah tdr 6919 tdr + language Tofanma tlg 6920 tlg + language Tofin Gbe tfi 6921 tfi + language Togbo-Vara Banda tor 6922 tor + language Togoyo tgy 6923 tgy + language Tohono O'odham ood 6924 ood + language Tojolabal toj 6925 toj + language Tok Pisin tpi 6926 tpi + language Tokano zuh 6927 zuh + language Tokelau tkl 6928 tkl + language Tokharian A xto 6929 xto + language Tokharian B txb 6930 txb + language Toki Pona tok 6931 tok + language Toku-No-Shima tkn 6932 tkn + language Tol jic 6933 jic + language Tolaki lbw 6934 lbw + language Tolomako tlm 6935 tlm + language Tolowa tol 6936 tol + language Toma tod 6937 tod + language Tomadino tdi 6938 tdi + language Tombelala ttp 6939 ttp + language Tombonuo txa 6940 txa + language Tombulu tom 6941 tom + language Tomini txm 6942 txm + language Tommo So Dogon dto 6943 dto + language Tomo Kan Dogon dtm 6944 dtm + language Tomoip tqp 6945 tqp + language Tondano tdn 6946 tdn + language Tondi Songway Kiini tst 6947 tst + language Tonga (Nyasa) tog 6948 tog + language Tonga (Tonga Islands) ton 6949 ton to Tonga + language Tonga (Zambia) toi 6950 toi + language Tongwe tny 6951 tny + language Tonjon tjn 6952 tjn + language Tonkawa tqw 6953 tqw + language Tonsawang tnw 6954 tnw + language Tonsea txs 6955 txs + language Tontemboan tnt 6956 tnt + language Tooro ttj 6957 ttj + language Topoiyo toy 6958 toy + language Toposa toq 6959 toq + language Toraja-Sa'dan sda 6960 sda + language Toram trj 6961 trj + language Torau ttu 6962 ttu + language Tornedalen Finnish fit 6963 fit + language Toro tdv 6964 tdv + language Toro So Dogon dts 6965 dts + language Toro Tegu Dogon dtt 6966 dtt + language Toromono tno 6967 tno + language Torona tqr 6968 tqr + language Torres Strait Creole tcs 6969 tcs + language Torricelli tei 6970 tei + language Torwali trw 6971 trw + language Torá trz 6972 trz + language Tosk Albanian als 6973 als + language Totela ttl 6974 ttl + language Toto txo 6975 txo + language Totoli txe 6976 txe + language Totomachapan Zapotec zph 6977 zph + language Totontepec Mixe mto 6978 mto + language Totoro ttk 6979 ttk + language Touo tqu 6980 tqu + language Toura (Côte d'Ivoire) neb 6981 neb + language Toura (Papua New Guinea) don 6982 don + language Towei ttn 6983 ttn + language Transalpine Gaulish xtg 6984 xtg + language Traveller Danish rmd 6985 rmd + language Traveller Norwegian rmg 6986 rmg + language Traveller Scottish trl 6987 trl + language Tregami trm 6988 trm + language Tremembé tme 6989 tme + language Trieng stg 6990 stg + language Trimuris tip 6991 tip + language Tring tgq 6992 tgq + language Tringgus-Sembaan Bidayuh trx 6993 trx + language Trinidad and Tobago Sign Language lst 6994 lst + language Trinidadian Creole English trf 6995 trf + language Trinitario trn 6996 trn + language Trió tri 6997 tri + language Truká tka 6998 tka + language Trumai tpy 6999 tpy + language Ts'ün-Lao tsl 7000 tsl + language Tsaangi tsa 7001 tsa + language Tsakhur tkr 7002 tkr + language Tsakonian tsd 7003 tsd + language Tsakwambo kvz 7004 kvz + language Tsamai tsb 7005 tsb + language Tsat huq 7006 huq + language Tseku tsk 7007 tsk + language Tsetsaut txc 7008 txc + language Tshangla tsj 7009 tsj + language Tsikimba kdl 7010 kdl + language Tsimané cas 7011 cas + language Tsimihety Malagasy xmw 7012 xmw + language Tsimshian tsi 7013 tsi + language Tsishingini tsw 7014 tsw + language Tso ldp 7015 ldp + language Tsoa hio 7016 hio + language Tsogo tsv 7017 tsv + language Tsonga tso 7018 tso ts + language Tsotso lto 7019 lto + language Tsou tsu 7020 tsu + language Tsucuba cbq 7021 cbq + language Tsum ttz 7022 ttz + language Tsuvadi tvd 7023 tvd + language Tsuvan tsh 7024 tsh + language Tswa tsc 7025 tsc + language Tswana tsn 7026 tsn tn + language Tswapong two 7027 two + language Tu mjg 7028 mjg + language Tuamotuan pmt 7029 pmt + language Tubar tbu 7030 tbu + language Tucano tuo 7031 tuo + language Tugen tuy 7032 tuy + language Tugun tzn 7033 tzn + language Tugutil tuj 7034 tuj + language Tukang Besi North khc 7035 khc + language Tukang Besi South bhq 7036 bhq + language Tuki bag 7037 bag + language Tukpa tpq 7038 tpq + language Tukudede tkd 7039 tkd + language Tukumanféd tkf 7040 tkf + language Tula tul 7041 tul + language Tulai tvi 7042 tvi + language Tulehu tlu 7043 tlu + language Tulishi tey 7044 tey + language Tulu tcy 7045 tcy + language Tulu-Bohuai rak 7046 rak + language Tuma-Irumu iou 7047 iou + language Tumak tmc 7048 tmc + language Tumari Kanuri krt 7049 krt + language Tumbuka tum 7050 tum + language Tumi kku 7051 kku + language Tumleo tmq 7052 tmq + language Tumshuqese xtq 7053 xtq + language Tumtum tbr 7054 tbr + language Tumulung Sisaala sil 7055 sil + language Tumzabt mzb 7056 mzb + language Tundra Enets enh 7057 enh + language Tunen tvu 7058 tvu + language Tungag lcm 7059 lcm + language Tunggare trt 7060 trt + language Tunia tug 7061 tug + language Tunica tun 7062 tun + language Tunisian Arabic aeb 7063 aeb + language Tunisian Sign Language tse 7064 tse + language Tunjung tjg 7065 tjg + language Tunni tqq 7066 tqq + language Tunzu dza 7067 dza + language Tuotomb ttf 7068 ttf + language Tuparí tpr 7069 tpr + language Tupinambá tpn 7070 tpn + language Tupinikin tpk 7071 tpk + language Tupuri tui 7072 tui + language Turaka trh 7073 trh + language Turi trd 7074 trd + language Turiwára twt 7075 twt + language Turka tuz 7076 tuz + language Turkana tuv 7077 tuv + language Turkish tur 7078 tur tr + language Turkish Sign Language tsm 7079 tsm + language Turkmen tuk 7080 tuk tk + language Turks And Caicos Creole English tch 7081 tch + language Turoyo tru 7082 tru + language Turumsa tqm 7083 tqm + language Turung try 7084 try + language Tuscarora tus 7085 tus + language Tutelo tta 7086 tta + language Tutong ttg 7087 ttg + language Tutsa Naga tvt 7088 tvt + language Tutuba tmi 7089 tmi + language Tututepec Mixtec mtu 7090 mtu + language Tututni tuu 7091 tuu + language Tuvalu tvl 7092 tvl + language Tuvinian tyv 7093 tyv + language Tuwali Ifugao ifk 7094 ifk + language Tuwari tww 7095 tww + language Tuwuli bov 7096 bov + language Tuxináwa tux 7097 tux + language Tuxá tud 7098 tud + language Tuyuca tue 7099 tue + language Twana twa 7100 twa + language Twendi twn 7101 twn + language Twents twd 7102 twd + language Twi twi 7103 twi tw + language Tyap kcg 7104 kcg + language Tz'utujil tzj 7105 tzj + language Tzeltal tzh 7106 tzh + language Tzotzil tzo 7107 tzo + language Tày tyz 7108 tyz + language Tày Sa Pa tys 7109 tys + language Tày Tac tyt 7110 tyt + language Téén lor 7111 lor + language Tübatulabal tub 7112 tub + language U uuu 7113 uuu + language Uab Meto aoz 7114 aoz + language Uamué uam 7115 uam + language Uare ksj 7116 ksj + language Ubaghara byc 7117 byc + language Ubang uba 7118 uba + language Ubi ubi 7119 ubi + language Ubir ubr 7120 ubr + language Ubykh uby 7121 uby + language Ucayali-Yurúa Ashéninka cpb 7122 cpb + language Uda uda 7123 uda + language Udi udi 7124 udi + language Udihe ude 7125 ude + language Udmurt udm 7126 udm + language Uduk udu 7127 udu + language Ufim ufi 7128 ufi + language Ugandan Sign Language ugn 7129 ugn + language Ugaritic uga 7130 uga + language Ughele uge 7131 uge + language Ugong ugo 7132 ugo + language Uhami uha 7133 uha + language Uighur, Uyghur uig 7134 uig Uighur ug Uyghur + language Uisai uis 7135 uis + language Ujir udj 7136 udj + language Ukaan kcf 7137 kcf + language Ukhwejo ukh 7138 ukh + language Ukit umi 7139 umi + language Ukpe-Bayobiri ukp 7140 ukp + language Ukpet-Ehom akd 7141 akd + language Ukrainian ukr 7142 ukr uk + language Ukrainian Sign Language ukl 7143 ukl + language Ukue uku 7144 uku + language Ukuriguma ukg 7145 ukg + language Ukwa ukq 7146 ukq + language Ukwuani-Aboh-Ndoni ukw 7147 ukw + language Ulau-Suain svb 7148 svb + language Ulch ulc 7149 ulc + language Ulithian uli 7150 uli + language Ullatan ull 7151 ull + language Ulukwumi ulb 7152 ulb + language Ulumanda' ulm 7153 ulm + language Ulwa ulw 7154 ulw + language Uma ppk 7155 ppk + language Uma' Lasan xky 7156 xky + language Uma' Lung ulu 7157 ulu + language Umanakaina gdn 7158 gdn + language Umatilla uma 7159 uma + language Umbindhamu umd 7160 umd + language Umbrian xum 7161 xum + language Umbu-Ungu ubu 7162 ubu + language Umbugarla umr 7163 umr + language Umbundu umb 7164 umb + language Ume Sami sju 7165 sju + language Umeda upi 7166 upi + language Umiida xud 7167 xud + language Umiray Dumaget Agta due 7168 due + language Umon umm 7169 umm + language Umotína umo 7170 umo + language Umpila ump 7171 ump + language Una mtg 7172 mtg + language Unami unm 7173 unm + language Uncoded languages mis 7174 mis + language Unde Kaili unz 7175 unz + language Undetermined und 7176 und + language Uneapa bbn 7177 bbn + language Uneme une 7178 une + language Unggaranggu xun 7179 xun + language Unggumi xgu 7180 xgu + language Uni uni 7181 uni + language Unserdeutsch uln 7182 uln + language Unua onu 7183 onu + language Unubahe unu 7184 unu + language Upper Chehalis cjh 7185 cjh + language Upper Grand Valley Dani dna 7186 dna + language Upper Guinea Crioulo pov 7187 pov + language Upper Kinabatangan dmg 7188 dmg + language Upper Kuskokwim kuu 7189 kuu + language Upper Necaxa Totonac tku 7190 tku + language Upper Saxon sxu 7191 sxu + language Upper Sorbian hsb 7192 hsb + language Upper Ta'oih tth 7193 tth + language Upper Tanana tau 7194 tau + language Upper Taromi tov 7195 tov + language Upper Umpqua xup 7196 xup + language Ura (Papua New Guinea) uro 7197 uro + language Ura (Vanuatu) uur 7198 uur + language Uradhi urf 7199 urf + language Urak Lawoi' urk 7200 urk + language Urali url 7201 url + language Urapmin urm 7202 urm + language Urarina ura 7203 ura + language Urartian xur 7204 xur + language Urat urt 7205 urt + language Urdu urd 7206 urd ur + language Urhobo urh 7207 urh + language Uri uvh 7208 uvh + language Urigina urg 7209 urg + language Urim uri 7210 uri + language Urimo urx 7211 urx + language Uripiv-Wala-Rano-Atchin upv 7212 upv + language Urningangg urc 7213 urc + language Uru ure 7214 ure + language Uru-Eu-Wau-Wau urz 7215 urz + language Uru-Pa-In urp 7216 urp + language Uruangnirin urn 7217 urn + language Uruava urv 7218 urv + language Urubú-Kaapor urb 7219 urb + language Urubú-Kaapor Sign Language uks 7220 uks + language Uruguayan Sign Language ugy 7221 ugy + language Urum uum 7222 uum + language Urumi uru 7223 uru + language Usaghade usk 7224 usk + language Usan wnu 7225 wnu + language Usarufa usa 7226 usa + language Ushojo ush 7227 ush + language Usila Chinantec cuc 7228 cuc + language Usku ulf 7229 ulf + language Uspanteco usp 7230 usp + language Usui usi 7231 usi + language Utarmbung omo 7232 omo + language Ute-Southern Paiute ute 7233 ute + language Utu utu 7234 utu + language Uvbie evh 7235 evh + language Uya usu 7236 usu + language Uyajitaya duk 7237 duk + language Uzbek uzb 7238 uzb uz + language Uzbeki Arabic auz 7239 auz + language Uzekwe eze 7240 eze + language Vaagri Booli vaa 7241 vaa + language Vafsi vaf 7242 vaf + language Vaghri vgr 7243 vgr + language Vaghua tva 7244 tva + language Vagla vag 7245 vag + language Vai vai 7246 vai + language Vaiphei vap 7247 vap + language Vale vae 7248 vae + language Valencian Sign Language vsv 7249 vsv + language Valle Nacional Chinantec cvn 7250 cvn + language Valley Maidu vmv 7251 vmv + language Valman van 7252 van + language Valpei vlp 7253 vlp + language Vamale mkt 7254 mkt + language Vame mlr 7255 mlr + language Vandalic xvn 7256 xvn + language Vangunu mpr 7257 mpr + language Vanimo vam 7258 vam + language Vano vnk 7259 vnk + language Vanuma vau 7260 vau + language Vao vao 7261 vao + language Varhadi-Nagpuri vah 7262 vah + language Varisi vrs 7263 vrs + language Varli vav 7264 vav + language Vasavi vas 7265 vas + language Veddah ved 7266 ved + language Vedic Sanskrit vsn 7267 vsn + language Vehes val 7268 val + language Veluws vel 7269 vel + language Vemgo-Mabas vem 7270 vem + language Venda ven 7271 ven ve + language Venetian vec 7272 vec + language Venetic xve 7273 xve + language Venezuelan Sign Language vsl 7274 vsl + language Vengo bav 7275 bav + language Ventureño veo 7276 veo + language Veps vep 7277 vep + language Vera'a vra 7278 vra + language Vestinian xvs 7279 xvs + language Vidunda vid 7280 vid + language Viemo vig 7281 vig + language Vietnamese vie 7282 vie vi + language Vilela vil 7283 vil + language Vili vif 7284 vif + language Villa Viciosa Agta dyg 7285 dyg + language Vincentian Creole English svc 7286 svc + language Vinmavis vnm 7287 vnm + language Vinza vin 7288 vin + language Virgin Islands Creole English vic 7289 vic + language Vishavan vis 7290 vis + language Viti vit 7291 vit + language Vitou vto 7292 vto + language Vitu wiv 7293 wiv + language Vlaams vls 7294 vls + language Vlaamse Gebarentaal vgt 7295 vgt + language Vlax Romani rmy 7296 rmy + language Volapük vol 7297 vol vo + language Volscian xvo 7298 xvo + language Vono kch 7299 kch + language Voro vor 7300 vor + language Votic vot 7301 vot + language Vumbu vum 7302 vum + language Vunapu vnp 7303 vnp + language Vunjo vun 7304 vun + language Vurës msn 7305 msn + language Vute vut 7306 vut + language Vwanji wbi 7307 wbi + language Võro vro 7308 vro + language Wa wbm 7309 wbm + language Wa'ema wag 7310 wag + language Waama wwa 7311 wwa + language Waamwang wmn 7312 wmn + language Waata ssn 7313 ssn + language Wab wab 7314 wab + language Wabo wbb 7315 wbb + language Waboda kmx 7316 kmx + language Waci Gbe wci 7317 wci + language Wadaginam wdg 7318 wdg + language Waddar wbq 7319 wbq + language Wadi Wadi xwd 7320 xwd + language Wadikali wdk 7321 wdk + language Wadiyara Koli kxp 7322 kxp + language Wadjabangayi wdy 7323 wdy + language Wadjiginy wdj 7324 wdj + language Wadjigu wdu 7325 wdu + language Wae Rana wrx 7326 wrx + language Waffa waj 7327 waj + language Wagawaga wgb 7328 wgb + language Wagaya wga 7329 wga + language Wagdi wbr 7330 wbr + language Wagi fad 7331 fad + language Wagiman waq 7332 waq + language Wahau Kayan whu 7333 whu + language Wahau Kenyah whk 7334 whk + language Wahgi wgi 7335 wgi + language Waigali wbk 7336 wbk + language Waigeo wgo 7337 wgo + language Wailaki wlk 7338 wlk + language Wailapa wlr 7339 wlr + language Waima rro 7340 rro + language Waima'a wmh 7341 wmh + language Waimaha bao 7342 bao + language Waimiri-Atroari atr 7343 atr + language Waioli wli 7344 wli + language Waiwai waw 7345 waw + language Waja wja 7346 wja + language Wajarri wbv 7347 wbv + language Wajuk xwj 7348 xwj + language Waka wav 7349 wav + language Wakabunga wwb 7350 wwb + language Wakawaka wkw 7351 wkw + language Wakde wkd 7352 wkd + language Wakhi wbl 7353 wbl + language Wakoná waf 7354 waf + language Wala lgl 7355 lgl + language Walak wlw 7356 wlw + language Walangama nlw 7357 nlw + language Wali (Ghana) wlx 7358 wlx + language Wali (Sudan) wll 7359 wll + language Waling wly 7360 wly + language Walio wla 7361 wla + language Walla Walla waa 7362 waa + language Wallisian wls 7363 wls + language Walloon wln 7364 wln wa + language Walmajarri wmt 7365 wmt + language Walser wae 7366 wae + language Walungge ola 7367 ola + language Waluwarra wrb 7368 wrb + language Wamas wmc 7369 wmc + language Wambaya wmb 7370 wmb + language Wambon wms 7371 wms + language Wambule wme 7372 wme + language Wamesa wad 7373 wad + language Wamey cou 7374 cou + language Wamin wmi 7375 wmi + language Wampanoag wam 7376 wam + language Wampar lbq 7377 lbq + language Wampur waz 7378 waz + language Wan wan 7379 wan + language Wanap wnp 7380 wnp + language Wancho Naga nnp 7381 nnp + language Wanda wbh 7382 wbh + language Wandala mfi 7383 mfi + language Wandarang wnd 7384 wnd + language Wandji wdd 7385 wdd + language Waneci wne 7386 wne + language Wanga lwg 7387 lwg + language Wangaaybuwan-Ngiyambaa wyb 7388 wyb + language Wanggamala wnm 7389 wnm + language Wanggom wng 7390 wng + language Wangkangurru wgg 7391 wgg + language Wangkayutyuru wky 7392 wky + language Wangkumara xwk 7393 xwk + language Wannu jub 7394 jub + language Wano wno 7395 wno + language Wantoat wnc 7396 wnc + language Wanukaka wnk 7397 wnk + language Wanyi wny 7398 wny + language Wané hwa 7399 hwa + language Waorani auc 7400 auc + language Wapan juk 7401 juk + language Wapishana wap 7402 wap + language Wappo wao 7403 wao + language War-Jaintia aml 7404 aml + language Wara wbf 7405 wbf + language Warao wba 7406 wba + language Waray (Australia) wrz 7407 wrz + language Waray (Philippines) war 7408 war + language Wardaman wrr 7409 wrr + language Wardandi wxw 7410 wxw + language Warembori wsa 7411 wsa + language Wares wai 7412 wai + language Waris wrs 7413 wrs + language Waritai wbe 7414 wbe + language Wariyangga wri 7415 wri + language Warji wji 7416 wji + language Warkay-Bipim bgv 7417 bgv + language Warlmanpa wrl 7418 wrl + language Warlpiri wbp 7419 wbp + language Warnang wrn 7420 wrn + language Warnman wbt 7421 wbt + language Waropen wrp 7422 wrp + language Warrgamay wgy 7423 wgy + language Warrwa wwr 7424 wwr + language Waru wru 7425 wru + language Warumungu wrm 7426 wrm + language Waruna wrv 7427 wrv + language Warungu wrg 7428 wrg + language Warwar Feni hrw 7429 hrw + language Wasa wss 7430 wss + language Wasco-Wishram wac 7431 wac + language Wasembo gsp 7432 gsp + language Washo was 7433 was + language Waskia wsk 7434 wsk + language Wasu wsu 7435 wsu + language Watakataui wtk 7436 wtk + language Watam wax 7437 wax + language Wathawurrung wth 7438 wth + language Watiwa wtf 7439 wtf + language Watubela wah 7440 wah + language Waube kop 7441 kop + language Waurá wau 7442 wau + language Wauyai wuy 7443 wuy + language Wawa www 7444 www + language Wawonii wow 7445 wow + language Waxianghua wxa 7446 wxa + language Wayampi oym 7447 oym + language Wayana way 7448 way + language Wayanad Chetti ctt 7449 ctt + language Wayoró wyr 7450 wyr + language Wayu vay 7451 vay + language Wayuu guc 7452 guc + language Wedau wed 7453 wed + language Weh weh 7454 weh + language Wejewa wew 7455 wew + language Welaun wlh 7456 wlh + language Weliki klh 7457 klh + language Welsh cym 7458 cym cy wel + language Welsh Romani rmw 7459 rmw + language Wemale weo 7460 weo + language Wemba Wemba xww 7461 xww + language Weme Gbe wem 7462 wem + language Wendat wdt 7463 wdt + language Wergaia weg 7464 weg + language Weri wer 7465 wer + language Wersing kvw 7466 kvw + language West Albay Bikol fbl 7467 fbl + language West Ambae nnd 7468 nnd + language West Bengal Sign Language wbs 7469 wbs + language West Berawan zbw 7470 zbw + language West Central Banda bbp 7471 bbp + language West Central Oromo gaz 7472 gaz + language West Coast Bajau bdr 7473 bdr + language West Damar drn 7474 drn + language West Goodenough ddi 7475 ddi + language West Kewa kew 7476 kew + language West Lembata lmj 7477 lmj + language West Makian mqs 7478 mqs + language West Masela mss 7479 mss + language West Tarangan txn 7480 txn + language West Uvean uve 7481 uve + language West Yugur ybe 7482 ybe + language West-Central Limba lia 7483 lia + language Western Abnaki abe 7484 abe + language Western Apache apw 7485 apw + language Western Armenian hyw 7486 hyw + language Western Arrarnta are 7487 are + language Western Balochi bgn 7488 bgn + language Western Bolivian Guaraní gnw 7489 gnw + language Western Bru brv 7490 brv + language Western Bukidnon Manobo mbb 7491 mbb + language Western Cham cja 7492 cja + language Western Dani dnw 7493 dnw + language Western Durango Nahuatl azn 7494 azn + language Western Fijian wyy 7495 wyy + language Western Frisian fry 7496 fry fy + language Western Highland Chatino ctp 7497 ctp + language Western Highland Purepecha pua 7498 pua + language Western Huasteca Nahuatl nhw 7499 nhw + language Western Juxtlahuaca Mixtec jmx 7500 jmx + language Western Kanjobal knj 7501 knj + language Western Karaboro kza 7502 kza + language Western Katu kuf 7503 kuf + language Western Kayah kyu 7504 kyu + language Western Keres kjq 7505 kjq + language Western Krahn krw 7506 krw + language Western Lalu ywl 7507 ywl + language Western Lawa lcp 7508 lcp + language Western Magar mrd 7509 mrd + language Western Maninkakan mlq 7510 mlq + language Western Mari mrj 7511 mrj + language Western Mashan Hmong hmw 7512 hmw + language Western Meohang raf 7513 raf + language Western Minyag wmg 7514 wmg + language Western Muria mut 7515 mut + language Western Neo-Aramaic amw 7516 amw + language Western Niger Fulfulde fuh 7517 fuh + language Western Ojibwa ojw 7518 ojw + language Western Panjabi pnb 7519 pnb + language Western Parbate Kham kjl 7520 kjl + language Western Penan pne 7521 pne + language Western Sisaala ssl 7522 ssl + language Western Subanon suc 7523 suc + language Western Tamang tdg 7524 tdg + language Western Tawbuid twb 7525 twb + language Western Tlacolula Valley Zapotec zab 7526 zab + language Western Totonac tqt 7527 tqt + language Western Tunebo tnb 7528 tnb + language Western Xiangxi Miao mmr 7529 mmr + language Western Xwla Gbe xwl 7530 xwl + language Western Yiddish yih 7531 yih + language Westphalien wep 7532 wep + language Wetamut wwo 7533 wwo + language Wewaw wea 7534 wea + language Weyto woy 7535 woy + language White Gelao giw 7536 giw + language White Lachi lwh 7537 lwh + language Whitesands tnp 7538 tnp + language Wiarumus tua 7539 tua + language Wichita wic 7540 wic + language Wichí Lhamtés Güisnay mzh 7541 mzh + language Wichí Lhamtés Nocten mtp 7542 mtp + language Wichí Lhamtés Vejoz wlv 7543 wlv + language Wik Ngathan wig 7544 wig + language Wik-Epa wie 7545 wie + language Wik-Iiyanh wij 7546 wij + language Wik-Keyangan wif 7547 wif + language Wik-Me'anha wih 7548 wih + language Wik-Mungkan wim 7549 wim + language Wikalkan wik 7550 wik + language Wikngenchera wua 7551 wua + language Wilawila wil 7552 wil + language Wintu wnw 7553 wnw + language Winyé kst 7554 kst + language Wipi gdr 7555 gdr + language Wiradjuri wrh 7556 wrh + language Wiraféd wir 7557 wir + language Wirangu wgu 7558 wgu + language Wiru wiu 7559 wiu + language Wiyot wiy 7560 wiy + language Woccon xwc 7561 xwc + language Wogamusin wog 7562 wog + language Wogeo woc 7563 woc + language Woi wbw 7564 wbw + language Woiwurrung wyi 7565 wyi + language Wojenaka jod 7566 jod + language Wolane wle 7567 wle + language Wolani wod 7568 wod + language Wolaytta wal 7569 wal + language Woleaian woe 7570 woe + language Wolio wlo 7571 wlo + language Wolof wol 7572 wol wo + language Wom (Nigeria) wom 7573 wom + language Wom (Papua New Guinea) wmo 7574 wmo + language Womo wmx 7575 wmx + language Wongo won 7576 won + language Woods Cree cwd 7577 cwd + language Woria wor 7578 wor + language Worimi kda 7579 kda + language Worodougou jud 7580 jud + language Worrorra wro 7581 wro + language Wotapuri-Katarqalai wsv 7582 wsv + language Wotjobaluk xwt 7583 xwt + language Wotu wtw 7584 wtw + language Woun Meu noa 7585 noa + language Written Oirat xwo 7586 xwo + language Wu Chinese wuu 7587 wuu + language Wuding-Luquan Yi ywq 7588 ywq + language Wudu wud 7589 wud + language Wuliwuli wlu 7590 wlu + language Wulna wux 7591 wux + language Wumboko bqm 7592 bqm + language Wumbvu wum 7593 wum + language Wumeng Nasu ywu 7594 ywu + language Wunai Bunu bwn 7595 bwn + language Wunambal wub 7596 wub + language Wunumara wnn 7597 wnn + language Wurrugu wur 7598 wur + language Wusa Nasu yig 7599 yig + language Wushi bse 7600 bse + language Wusi wsi 7601 wsi + language Wutung wut 7602 wut + language Wutunhua wuh 7603 wuh + language Wuvulu-Aua wuv 7604 wuv + language Wuzlam udl 7605 udl + language Wyandot wyn 7606 wyn + language Wymysorys wym 7607 wym + language Wára tci 7608 tci + language Wãpha juw 7609 juw + language Wè Northern wob 7610 wob + language Wè Southern gxx 7611 gxx + language Wè Western wec 7612 wec + language Xaasongaxango kao 7613 kao + language Xadani Zapotec zax 7614 zax + language Xakriabá xkr 7615 xkr + language Xamtanga xan 7616 xan + language Xanaguía Zapotec ztg 7617 ztg + language Xavánte xav 7618 xav + language Xerénte xer 7619 xer + language Xetá xet 7620 xet + language Xhosa xho 7621 xho xh + language Xiang Chinese hsn 7622 hsn + language Xibe sjo 7623 sjo + language Xicotepec De Juárez Totonac too 7624 too + language Xinca xin 7625 xin + language Xingú Asuriní asn 7626 asn + language Xipaya xiy 7627 xiy + language Xiri xii 7628 xii + language Xiriâna xir 7629 xir + language Xishanba Lalo ywt 7630 ywt + language Xokleng xok 7631 xok + language Xukurú xoo 7632 xoo + language Xwela Gbe xwe 7633 xwe + language Xârâcùù ane 7634 ane + language Xârâgurè axx 7635 axx + language Yaaku muu 7636 muu + language Yabarana yar 7637 yar + language Yabaâna ybn 7638 ybn + language Yabem jae 7639 jae + language Yaben ybm 7640 ybm + language Yabong ybo 7641 ybo + language Yabula Yabula yxy 7642 yxy + language Yace ekr 7643 ekr + language Yaeyama rys 7644 rys + language Yafi wfg 7645 wfg + language Yagara yxg 7646 yxg + language Yagaria ygr 7647 ygr + language Yagnobi yai 7648 yai + language Yagomi ygm 7649 ygm + language Yagua yad 7650 yad + language Yagwoia ygw 7651 ygw + language Yahadian ner 7652 ner + language Yahang rhp 7653 rhp + language Yahuna ynu 7654 ynu + language Yaka (Central African Republic) axk 7655 axk + language Yaka (Congo) iyx 7656 iyx + language Yaka (Democratic Republic of Congo) yaf 7657 yaf + language Yakaikeke ykk 7658 ykk + language Yakama yak 7659 yak + language Yakan yka 7660 yka + language Yakha ybh 7661 ybh + language Yakoma yky 7662 yky + language Yakut sah 7663 sah + language Yala yba 7664 yba + language Yalahatan jal 7665 jal + language Yalakalore xyl 7666 xyl + language Yalarnnga ylr 7667 ylr + language Yale nce 7668 nce + language Yaleba ylb 7669 ylb + language Yalunka yal 7670 yal + language Yalálag Zapotec zpu 7671 zpu + language Yamap ymp 7672 ymp + language Yamba yam 7673 yam + language Yambes ymb 7674 ymb + language Yambeta yat 7675 yat + language Yamdena jmd 7676 jmd + language Yameo yme 7677 yme + language Yami tao 7678 tao + language Yaminahua yaa 7679 yaa + language Yamna ymn 7680 ymn + language Yamongeri ymg 7681 ymg + language Yamphu ybi 7682 ybi + language Yan-nhangu jay 7683 jay + language Yan-nhaŋu Sign Language yhs 7684 yhs + language Yana ynn 7685 ynn + language Yanahuanca Pasco Quechua qur 7686 qur + language Yanda yda 7687 yda + language Yanda Dom Dogon dym 7688 dym + language Yandjibara xyb 7689 xyb + language Yandruwandha ynd 7690 ynd + language Yanesha' ame 7691 ame + language Yang Zhuang zyg 7692 zyg + language Yangben yav 7693 yav + language Yangkam bsx 7694 bsx + language Yangman jng 7695 jng + language Yango yng 7696 yng + language Yangulam ynl 7697 ynl + language Yangum Dey yde 7698 yde + language Yangum Gel ygl 7699 ygl + language Yangum Mon ymo 7700 ymo + language Yankunytjatjara kdd 7701 kdd + language Yanomamö guu 7702 guu + language Yanomámi wca 7703 wca + language Yansi yns 7704 yns + language Yanyuwa jao 7705 jao + language Yao yao 7706 yao + language Yaosakor Asmat asy 7707 asy + language Yaouré yre 7708 yre + language Yapese yap 7709 yap + language Yapunda yev 7710 yev + language Yaqay jaq 7711 jaq + language Yaqui yaq 7712 yaq + language Yarawata yrw 7713 yrw + language Yardliyawarra yxl 7714 yxl + language Yareba yrb 7715 yrb + language Yareni Zapotec zae 7716 zae + language Yarluyandi yry 7717 yry + language Yaroamë yro 7718 yro + language Yarsun yrs 7719 yrs + language Yasa yko 7720 yko + language Yassic ysc 7721 ysc + language Yatay yty 7722 yty + language Yatee Zapotec zty 7723 zty + language Yatzachi Zapotec zav 7724 zav + language Yau (Morobe Province) yuw 7725 yuw + language Yau (Sandaun Province) yyu 7726 yyu + language Yaul yla 7727 yla + language Yauma yax 7728 yax + language Yaur jau 7729 jau + language Yautepec Zapotec zpb 7730 zpb + language Yauyos Quechua qux 7731 qux + language Yavitero yvt 7732 yvt + language Yawa yva 7733 yva + language Yawalapití yaw 7734 yaw + language Yawanawa ywn 7735 ywn + language Yawarawarga yww 7736 yww + language Yaweyuha yby 7737 yby + language Yawijibaya jbw 7738 jbw + language Yawiyo ybx 7739 ybx + language Yawuru ywr 7740 ywr + language Yaygir xya 7741 xya + language Yazgulyam yah 7742 yah + language Yecuatla Totonac tlc 7743 tlc + language Yei jei 7744 jei + language Yekhee ets 7745 ets + language Yekora ykr 7746 ykr + language Yela yel 7747 yel + language Yele yle 7748 yle + language Yelmek jel 7749 jel + language Yelogu ylg 7750 ylg + language Yemba ybb 7751 ybb + language Yemsa jnj 7752 jnj + language Yendang ynq 7753 ynq + language Yeni yei 7754 yei + language Yeniche yec 7755 yec + language Yerakai yra 7756 yra + language Yeretuar gop 7757 gop + language Yerong yrn 7758 yrn + language Yerukula yeu 7759 yeu + language Yessan-Mayo yss 7760 yss + language Yetfa yet 7761 yet + language Yevanic yej 7762 yej + language Yeyi yey 7763 yey + language Yiddish yid 7764 yid yi + language Yidgha ydg 7765 ydg + language Yidiny yii 7766 yii + language Yil yll 7767 yll + language Yilan Creole ycr 7768 ycr + language Yimas yee 7769 yee + language Yimchungru Naga yim 7770 yim + language Yinbaw Karen kvu 7771 kvu + language Yindjibarndi yij 7772 yij + language Yindjilandji yil 7773 yil + language Yine pib 7774 pib + language Yinggarda yia 7775 yia + language Yinhawangka ywg 7776 ywg + language Yiningayi ygi 7777 ygi + language Yintale Karen kvy 7778 kvy + language Yinwum yxm 7779 yxm + language Yir Yoront yyr 7780 yyr + language Yirandali ljw 7781 ljw + language Yirrk-Mel yrm 7782 yrm + language Yis yis 7783 yis + language Yitha Yitha xth 7784 xth + language Yoba yob 7785 yob + language Yocoboué Dida gud 7786 gud + language Yogad yog 7787 yog + language Yoidik ydk 7788 ydk + language Yoke yki 7789 yki + language Yokuts yok 7790 yok + language Yola yol 7791 yol + language Yoloxochitl Mixtec xty 7792 xty + language Yolŋu Sign Language ygs 7793 ygs + language Yom pil 7794 pil + language Yombe yom 7795 yom + language Yonaguni yoi 7796 yoi + language Yong yno 7797 yno + language Yongbei Zhuang zyb 7798 zyb + language Yongkom yon 7799 yon + language Yongnan Zhuang zyn 7800 zyn + language Yopno yut 7801 yut + language Yora mts 7802 mts + language Yoron yox 7803 yox + language Yorta Yorta xyy 7804 xyy + language Yoruba yor 7805 yor yo + language Yosondúa Mixtec mpm 7806 mpm + language Yotti yot 7807 yot + language Youjiang Zhuang zyj 7808 zyj + language Youle Jinuo jiu 7809 jiu + language Younuo Bunu buh 7810 buh + language Yout Wam ytw 7811 ytw + language Yoy yoy 7812 yoy + language Yuanga nua 7813 nua + language Yucatec Maya Sign Language msd 7814 msd + language Yucateco yua 7815 yua + language Yuchi yuc 7816 yuc + language Yucuañe Mixtec mvg 7817 mvg + language Yucuna ycn 7818 ycn + language Yue Chinese yue 7819 yue + language Yug yug 7820 yug + language Yugambal yub 7821 yub + language Yugoslavian Sign Language ysl 7822 ysl + language Yugul ygu 7823 ygu + language Yuhup yab 7824 yab + language Yuki yuk 7825 yuk + language Yukpa yup 7826 yup + language Yukuben ybl 7827 ybl + language Yulu yul 7828 yul + language Yuqui yuq 7829 yuq + language Yuracare yuz 7830 yuz + language Yurats rts 7831 rts + language Yurok yur 7832 yur + language Yuru ljx 7833 ljx + language Yurutí yui 7834 yui + language Yutanduchi Mixtec mab 7835 mab + language Yuwana yau 7836 yau + language Yuyu yxu 7837 yxu + language Ywom gek 7838 gek + language Yámana yag 7839 yag + language Zaachila Zapotec ztx 7840 ztx + language Zabana kji 7841 kji + language Zacatepec Chatino ctz 7842 ctz + language Zacatlán-Ahuacatlán-Tepetzintla Nahuatl nhi 7843 nhi + language Zaghawa zag 7844 zag + language Zaiwa atb 7845 atb + language Zakhring zkr 7846 zkr + language Zambian Sign Language zsl 7847 zsl + language Zan Gula zna 7848 zna + language Zanaki zak 7849 zak + language Zande (individual language) zne 7850 zne + language Zangskari zau 7851 zau + language Zangwal zah 7852 zah + language Zaniza Zapotec zpw 7853 zpw + language Zapotec zap 7854 zap + language Zaramo zaj 7855 zaj + language Zari zaz 7856 zaz + language Zarma dje 7857 dje + language Zarphatic zrp 7858 zrp + language Zauzou zal 7859 zal + language Zay zwa 7860 zwa + language Zayein Karen kxk 7861 kxk + language Zayse-Zergulla zay 7862 zay + language Zaza zza 7863 zza + language Zazao jaj 7864 jaj + language Zeem zem 7865 zem + language Zeeuws zea 7866 zea + language Zemba dhm 7867 dhm + language Zeme Naga nzm 7868 nzm + language Zemgalian xzm 7869 xzm + language Zenag zeg 7870 zeg + language Zenaga zen 7871 zen + language Zenzontepec Chatino czn 7872 czn + language Zerenkel zrn 7873 zrn + language Zhaba zhb 7874 zhb + language Zhang-Zhung xzh 7875 xzh + language Zhire zhi 7876 zhi + language Zhoa zhw 7877 zhw + language Zhuang, Chuang zha 7878 zha za Zhuang Chuang + language Zia zia 7879 zia + language Zialo zil 7880 zil + language Zigula ziw 7881 ziw + language Zimakani zik 7882 zik + language Zimba zmb 7883 zmb + language Zimbabwe Sign Language zib 7884 zib + language Zinza zin 7885 zin + language Zire sih 7886 sih + language Zizilivakan ziz 7887 ziz + language Zo'é pto 7888 pto + language Zokhuo yzk 7889 yzk + language Zoogocho Zapotec zpq 7890 zpq + language Zoroastrian Dari gbz 7891 gbz + language Zotung Chin czt 7892 czt + language Zou zom 7893 zom + language Zul zlu 7894 zlu + language Zula zla 7895 zla + language Zulgo-Gemzek gnd 7896 gnd + language Zulu zul 7897 zul zu + language Zumaya zuy 7898 zuy + language Zumbun jmb 7899 jmb + language Zuni zun 7900 zun + language Zuojiang Zhuang zzj 7901 zzj + language Zyphe Chin zyp 7902 zyp + language Záparo zro 7903 zro + language sTodsde jih 7904 jih + language us-Saare uss 7905 uss + language ut-Hun uth 7906 uth + language ut-Ma'in gel 7907 gel + language Àhàn ahn 7908 ahn + language Áncá acb 7909 acb + language Ömie aom 7910 aom + language Önge oon 7911 oon + language ǀGwi gwj 7912 gwj + language ǀXam xam 7913 xam + language ǁAni hnh 7914 hnh + language ǁGana gnk 7915 gnk + language ǁXegwi xeg 7916 xeg + language ǂHua huc 7917 huc + language ǂUngkue gku 7918 gku + language ǃXóõ nmn 7919 nmn + language Not applicable 7920 diff --git a/scripts/issues/8578/citation_languages_develop.tsv b/scripts/issues/8578/citation_languages_develop.tsv new file mode 100644 index 00000000000..7ce40b389f8 --- /dev/null +++ b/scripts/issues/8578/citation_languages_develop.tsv @@ -0,0 +1,186 @@ + language Abkhaz abk 0 abk ab + language Afar aar 1 aar aa + language Afrikaans afr 2 afr af + language Akan aka 3 aka ak + language Albanian sqi 4 sqi alb sq + language Amharic amh 5 amh am + language Arabic ara 6 ara ar + language Aragonese arg 7 arg an + language Armenian hye 8 hye arm hy + language Assamese asm 9 asm as + language Avaric ava 10 ava av + language Avestan ave 11 ave ae + language Aymara aym 12 aym ay + language Azerbaijani aze 13 aze az + language Bambara bam 14 bam bm + language Bashkir bak 15 bak ba + language Basque eus 16 eus baq eu + language Belarusian bel 17 bel be + language Bengali, Bangla ben 18 ben bn Bengali Bangla + language Bihari bih 19 bih bh + language Bislama bis 20 bis bi + language Bosnian bos 21 bos bs + language Breton bre 22 bre br + language Bulgarian bul 23 bul bg + language Burmese mya 24 mya bur my + language Catalan,Valencian cat 25 cat ca Catalan Valencian + language Chamorro cha 26 cha ch + language Chechen che 27 che ce + language Chichewa, Chewa, Nyanja nya 28 nya ny Chichewa Chewa Nyanja + language Chinese zho 29 zho chi zh + language Chuvash chv 30 chv cv + language Cornish cor 31 cor kw + language Corsican cos 32 cos co + language Cree cre 33 cre cr + language Croatian hrv 34 hrv src hr + language Czech ces 35 ces cze cs + language Danish dan 36 dan da + language Divehi, Dhivehi, Maldivian div 37 div dv Divehi Dhivehi Maldivian + language Dutch nld 38 nld dut nl + language Dzongkha dzo 39 dzo dz + language English eng 40 eng en + language Esperanto epo 41 epo eo + language Estonian est 42 est et + language Ewe ewe 43 ewe ee + language Faroese fao 44 fao fo + language Fijian fij 45 fij fj + language Finnish fin 46 fin fi + language French fra 47 fra fre fr + language Fula, Fulah, Pulaar, Pular ful 48 ful ff Fula Fulah Pulaar Pular + language Galician glg 49 glg gl + language Georgian kat 50 kat geo ka + language German deu 51 deu ger de + language Greek (modern) ell 52 ell gre el Greek + language Guaraní grn 53 grn gn + language Gujarati guj 54 guj gu + language Haitian, Haitian Creole hat 55 hat ht Haitian Haitian Creole + language Hausa hau 56 hau ha + language Hebrew (modern) heb 57 heb he + language Herero her 58 her hz + language Hindi hin 59 hin hi + language Hiri Motu hmo 60 hmo ho + language Hungarian hun 61 hun hu + language Interlingua ina 62 ina ia + language Indonesian ind 63 ind id + language Interlingue ile 64 ile ie + language Irish gle 65 gle ga + language Igbo ibo 66 ibo ig + language Inupiaq ipk 67 ipk ik + language Ido ido 68 ido io + language Icelandic isl 69 isl ice is + language Italian ita 70 ita it + language Inuktitut iku 71 iku iu + language Japanese jpn 72 jpn ja + language Javanese jav 73 jav jv + language Kalaallisut, Greenlandic kal 74 kal kl Kalaallisut Greenlandic + language Kannada kan 75 kan kn + language Kanuri kau 76 kau kr + language Kashmiri kas 77 kas ks + language Kazakh kaz 78 kaz kk + language Khmer khm 79 khm km + language Kikuyu, Gikuyu kik 80 kik ki Kikuyu Gikuyu + language Kinyarwanda kin 81 kin rw + language Kyrgyz kir 82 kir ky Kirghiz + language Komi kom 83 kom kv + language Kongo kon 84 kon kg + language Korean kor 85 kor ko + language Kurdish kur 86 kur ku + language Kwanyama, Kuanyama kua 87 kua kj Kwanyama Kuanyama + language Latin lat 88 lat la + language Luxembourgish, Letzeburgesch ltz 89 ltz lb Luxembourgish Letzeburgesch + language Ganda lug 90 lug lg + language Limburgish, Limburgan, Limburger lim 91 lim li Limburgish Limburgan Limburger + language Lingala lin 92 lin ln + language Lao lao 93 lao lo + language Lithuanian lit 94 lit lt + language Luba-Katanga lub 95 lub lu + language Latvian lav 96 lav lv + language Manx glv 97 glv gv + language Macedonian mkd 98 mkd mac mk + language Malagasy mlg 99 mlg mg + language Malay msa 100 msa may ms + language Malayalam mal 101 mal ml + language Maltese mlt 102 mlt mt + language Māori mri 103 mri mao mi Maori + language Marathi (Marāṭhī) mar 104 mar mr + language Marshallese mah 105 mah mh + language Mixtepec Mixtec mix 106 mix + language Mongolian mon 107 mon mn + language Nauru nau 108 nau na + language Navajo, Navaho nav 109 nav nv Navajo Navaho + language Northern Ndebele nde 110 nde nd + language Nepali nep 111 nep ne + language Ndonga ndo 112 ndo ng + language Norwegian Bokmål nob 113 nob nb + language Norwegian Nynorsk nno 114 nno nn + language Norwegian nor 115 nor no + language Nuosu iii 116 iii ii Sichuan Yi + language Southern Ndebele nbl 117 nbl nr + language Occitan oci 118 oci oc + language Ojibwe, Ojibwa oji 119 oji oj Ojibwe Ojibwa + language Old Church Slavonic,Church Slavonic,Old Bulgarian chu 120 chu cu + language Oromo orm 121 orm om + language Oriya ori 122 ori or + language Ossetian, Ossetic oss 123 oss os Ossetian Ossetic + language Panjabi, Punjabi pan 124 pan pa Panjabi Punjabi + language Pāli pli 125 pli pi + language Persian (Farsi) fas 126 fas per fa + language Polish pol 127 pol pl + language Pashto, Pushto pus 128 pus ps Pashto Pushto + language Portuguese por 129 por pt + language Quechua que 130 que qu + language Romansh roh 131 roh rm + language Kirundi run 132 run rn + language Romanian ron 133 ron rum ro + language Russian rus 134 rus ru + language Sanskrit (Saṁskṛta) san 135 san sa + language Sardinian srd 136 srd sc + language Sindhi snd 137 snd sd + language Northern Sami sme 138 sme se + language Samoan smo 139 smo sm + language Sango sag 140 sag sg + language Serbian srp 141 srp scc sr + language Scottish Gaelic, Gaelic gla 142 gla gd Scottish Gaelic Gaelic + language Shona sna 143 sna sn + language Sinhala, Sinhalese sin 144 sin si Sinhala Sinhalese + language Slovak slk 145 slk slo sk + language Slovene slv 146 slv sl Slovenian + language Somali som 147 som so + language Southern Sotho sot 148 sot st + language Spanish, Castilian spa 149 spa es Spanish Castilian + language Sundanese sun 150 sun su + language Swahili swa 151 swa sw + language Swati ssw 152 ssw ss + language Swedish swe 153 swe sv + language Tamil tam 154 tam ta + language Telugu tel 155 tel te + language Tajik tgk 156 tgk tg + language Thai tha 157 tha th + language Tigrinya tir 158 tir ti + language Tibetan Standard, Tibetan, Central bod 159 bod tib bo Tibetan Standard Tibetan Central + language Turkmen tuk 160 tuk tk + language Tagalog tgl 161 tgl tl + language Tswana tsn 162 tsn tn + language Tonga (Tonga Islands) ton 163 ton to Tonga + language Turkish tur 164 tur tr + language Tsonga tso 165 tso ts + language Tatar tat 166 tat tt + language Twi twi 167 twi tw + language Tahitian tah 168 tah ty + language Uyghur, Uighur uig 169 uig ug Uyghur Uighur + language Ukrainian ukr 170 ukr uk + language Urdu urd 171 urd ur + language Uzbek uzb 172 uzb uz + language Venda ven 173 ven ve + language Vietnamese vie 174 vie vi + language Volapük vol 175 vol vo + language Walloon wln 176 wln wa + language Welsh cym 177 cym wel cy + language Wolof wol 178 wol wo + language Western Frisian fry 179 fry fy + language Xhosa xho 180 xho xh + language Yiddish yid 181 yid yi + language Yoruba yor 182 yor yo + language Zhuang, Chuang zha 183 zha za Zhuang Chuang + language Zulu zul 184 zul zu + language Not applicable 185 diff --git a/scripts/issues/8578/script_check_languages.pl b/scripts/issues/8578/script_check_languages.pl new file mode 100755 index 00000000000..fb85fe9cd3a --- /dev/null +++ b/scripts/issues/8578/script_check_languages.pl @@ -0,0 +1,189 @@ +#!/usr/bin/perl +use utf8; + + +$USAGE = "usage: -check[codes|names] citation_languages_10762.tsv citation_languages_develop.tsv\n"; + +$mode = shift @ARGV; +$citation_languages_file_new = shift @ARGV; +$citation_languages_file_develop = shift @ARGV; + +unless ( $mode eq "-checkcodes" || $mode eq "-checknames" ) +{ + print STDERR $USAGE; + exit 1; +} + + +unless ( $citation_languages_file_new + && $citation_languages_file_develop + && -f $citation_languages_file_new + && -f $citation_languages_file_develop ) +{ + print STDERR $USAGE; + exit 1; +} + +print "Parsing the new languages list...\n"; + +open FINP, $citation_languages_file_new; + +while ( $_ = ) +{ + chop; + + @_ = split ("\t", $_); + + if ($_[1] ne "language") + { + die "illegal entry: $_\n"; + } + + $mainname = $_[2]; + if ( $mainname !~/^[\p{L}\p{M}].*[\p{L}\p{M} \)']$/ ) + { + #print STDERR "warning: (potentially?) illegal language name (main): " . $mainname . "\n"; + } + + if ( $mainname ne "Not applicable" && $_[3] !~/^[a-z][a-z][a-z]$/ ) + { + die "Error: Illegal entry: no 3-letter identifier code in the 3rd column: " . $_ . "\n"; + } + else + { + $identifier = $_[3]; + $LANGUAGE_NAMES{$identifier} = $mainname; + } + + unless ($_[4] >= 0) + { + die "Error: illegal entry: (no CVV ref. number) " . $_ . "\n"; + } + + $threelettercode = $_[5]; + + unless ( $mainname eq "Not applicable" || $threelettercode =~/^[a-z][a-z][a-z]$/ ) + { + die "Error: No 3-letter code in the 5th column: " . join ("***", @_) . "\n"; + } + else + { + $hash = {}; + $hash->{$threelettercode} = 1; + + } + + if ( $identifier ne $threelettercode ) + { + print STDERR "warning: the identifier in the 3rd column is different from the first \"alternate\" in the 5th: " . $_ . "\n"; + } + + + for ( $c = 6; $c <= $#_; $c++ ) + { + $alternate = $_[$c]; + $hash->{$alternate} = 1 if $alternate ne ""; + } + + $LANGUAGE_ALTERNATES{$identifier} = $hash; + $num_new++; +} + +close FINP; +print $num_new . " language entries processed. All entries well-formed.\n"; + + +## Now let's go through the previously supported languages and compare how they are treated in the new CVV: + +print "Processing the previously supported list:\n"; + +open FINP, $citation_languages_file_develop; + +while ($_ = ) +{ + chop; + + @_ = split ("\t", $_); + + if ($_[1] ne "language") + { + die "illegal entry: $_\n"; + } + + $mainname = $_[2]; + + if ($mainname ne "Not applicable") + { + if ($_[3] !~/^[a-z][a-z][a-z]$/) + { + die "Error: Illegal entry: no 3-letter identifier code in the 3rd column: " . $_ . "\n"; + } + else + { + $identifier = $_[3]; + + unless ($LANGUAGE_NAMES{$identifier}) + { + die "Previously supported language " . $identifier . " (" . $mainname . ") is no longer on the list.\n"; + } + + if ( $mode eq "-checknames" ) + { + if ($mainname ne $LANGUAGE_NAMES{$identifier}) + { + print "Language name different for id " . $identifier . ": old: " . $mainname . ", new: " . $LANGUAGE_NAMES{$identifier} . "\n"; + } + } + } + } + + unless ($_[4] >= 0) + { + die "Error: illegal entry: (no CVV ref. number) " . $_ . "\n"; + } + + $threelettercode = $_[5]; + + if ( $mainname ne "Not applicable" ) + { + unless ( $threelettercode =~/^[a-z][a-z][a-z]$/ ) + { + die "Error: No 3-letter code in the 5th column: " . join ("***", @_) . "\n"; + } + else + { + if ( $mode eq "-checkcodes" ) + { + unless ($LANGUAGE_ALTERNATES{$identifier}->{$threelettercode}) + { + print "identifier: " . $identifier . ", main 3-letter code \"" . $threelettercode . "\" is not found in the new list!\n"; + } + } + } + } + + if ( $mainname ne "Not applicable" && $identifier ne $threelettercode ) + { + die "warning: the identifier in the 3rd column is different from the first \"alternate\" in the 5th: " . $_ . "\n"; + } + + if ( $mode eq "-checkcodes" ) + { + for ( $c = 6; $c <= $#_; $c++ ) + { + $alternate = $_[$c]; + + if ( $alternate ne "" ) + { + unless ($LANGUAGE_ALTERNATES{$identifier}->{$alternate}) + { + print "identifier: " . $identifier . ", alternate value " . $alternate . " is missing in the new list!\n"; + } + } + } + } + + $num_develop++; +} + +print $num_develop . " develop branch entries processed.\n"; diff --git a/scripts/search/tests/data/dataset-finch3.json b/scripts/search/tests/data/dataset-finch3.json new file mode 100644 index 00000000000..903b0aa124d --- /dev/null +++ b/scripts/search/tests/data/dataset-finch3.json @@ -0,0 +1,102 @@ +{ + "datasetVersion": { + "license": { + "name": "CC0 1.0", + "uri": "http://creativecommons.org/publicdomain/zero/1.0" + }, + "metadataBlocks": { + "citation": { + "fields": [ + { + "value": "HTML & More", + "typeClass": "primitive", + "multiple": false, + "typeName": "title" + }, + { + "value": [ + { + "authorName": { + "value": "Markup, Marty", + "typeClass": "primitive", + "multiple": false, + "typeName": "authorName" + }, + "authorAffiliation": { + "value": "W4C", + "typeClass": "primitive", + "multiple": false, + "typeName": "authorAffiliation" + } + } + ], + "typeClass": "compound", + "multiple": true, + "typeName": "author" + }, + { + "value": [ + { + "datasetContactEmail": { + "typeClass": "primitive", + "multiple": false, + "typeName": "datasetContactEmail", + "value": "markup@mailinator.com" + }, + "datasetContactName": { + "typeClass": "primitive", + "multiple": false, + "typeName": "datasetContactName", + "value": "Markup, Marty" + } + } + ], + "typeClass": "compound", + "multiple": true, + "typeName": "datasetContact" + }, + { + "value": [ + { + "dsDescriptionValue": { + "value": "BEGIN

END", + "multiple": false, + "typeClass": "primitive", + "typeName": "dsDescriptionValue" + }, + "dsDescriptionDate": { + "typeName": "dsDescriptionDate", + "multiple": false, + "typeClass": "primitive", + "value": "2021-07-13" + } + } + ], + "typeClass": "compound", + "multiple": true, + "typeName": "dsDescription" + }, + { + "value": [ + "Medicine, Health and Life Sciences" + ], + "typeClass": "controlledVocabulary", + "multiple": true, + "typeName": "subject" + }, + { + "typeName": "language", + "multiple": true, + "typeClass": "controlledVocabulary", + "value": [ + "English", + "Afar", + "aar" + ] + } + ], + "displayName": "Citation Metadata" + } + } + } +} diff --git a/src/main/docker/assembly.xml b/src/main/docker/assembly.xml index 9f9b39617a3..62cd910ef9b 100644 --- a/src/main/docker/assembly.xml +++ b/src/main/docker/assembly.xml @@ -3,7 +3,7 @@ - target/${project.artifactId}-${project.version} + target/${project.artifactId} app WEB-INF/lib/**/* @@ -11,7 +11,7 @@ - target/${project.artifactId}-${project.version}/WEB-INF/lib + target/${project.artifactId}/WEB-INF/lib deps diff --git a/src/main/docker/scripts/init_2_configure.sh b/src/main/docker/scripts/init_2_configure.sh index a98f08088c1..5c1075f01f3 100755 --- a/src/main/docker/scripts/init_2_configure.sh +++ b/src/main/docker/scripts/init_2_configure.sh @@ -22,21 +22,35 @@ if [ "${dataverse_files_storage__driver__id}" = "local" ]; then 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}" +# If reload is enable via ENABLE_RELOAD=1, set according Jakarta Faces options +ENABLE_RELOAD=${ENABLE_RELOAD:-0} +if [ "${ENABLE_RELOAD}" = "1" ]; then + export DATAVERSE_JSF_PROJECT_STAGE=${DATAVERSE_JSF_PROJECT_STAGE:-"Development"} + export DATAVERSE_JSF_REFRESH_PERIOD=${DATAVERSE_JSF_REFRESH_PERIOD:-"0"} +fi -# 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. +# Check prerequisites for commands handling +if [ -z "$POSTBOOT_COMMANDS_FILE" ]; then echo "Variable POSTBOOT_COMMANDS_FILE is not set."; exit 1; fi +# Test if postboot file is writeable for us, exit otherwise +touch "$POSTBOOT_COMMANDS_FILE" || exit 1 +# Copy and split the postboot contents to manipulate them +EXISTING_DEPLOY_COMMANDS=$(mktemp) +NEW_POSTBOOT_COMMANDS=$(mktemp) +grep -e "^deploy " "$POSTBOOT_COMMANDS_FILE" > "$EXISTING_DEPLOY_COMMANDS" || true +grep -v -e "^deploy" "$POSTBOOT_COMMANDS_FILE" > "$NEW_POSTBOOT_COMMANDS" || true -# 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}" +function inject() { + if [ -z "$1" ]; then echo "No line specified"; exit 1; fi + # If the line is not yet in the file, try to add it + if ! grep -q "$1" "$NEW_POSTBOOT_COMMANDS"; then + # Check if the line is still not in the file when splitting at the first = + if ! grep -q "$(echo "$1" | cut -f1 -d"=")" "$NEW_POSTBOOT_COMMANDS"; then + echo "$1" >> "$NEW_POSTBOOT_COMMANDS" + fi + fi +} -# 3. Domain based configuration options -# Set Dataverse environment variables +# Domain based configuration options - set from 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 @@ -51,14 +65,12 @@ env -0 | grep -z -Ee "^(dataverse|doi)_" | while IFS='=' read -r -d '' k v; do v=$(echo "${v}" | sed -e 's/:/\\\:/g') echo "DEBUG: Handling ${KEY}=${v}." - echo "create-system-properties ${KEY}=${v}" >> "${DV_POSTBOOT}" + inject "create-system-properties ${KEY}=${v}" 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}" +cat "$NEW_POSTBOOT_COMMANDS" "$EXISTING_DEPLOY_COMMANDS" > "${POSTBOOT_COMMANDS_FILE}" echo "DEBUG: postboot contains the following commands:" echo "--------------------------------------------------" -cat "${POSTBOOT_COMMANDS}" +cat "${POSTBOOT_COMMANDS_FILE}" echo "--------------------------------------------------" - diff --git a/src/main/docker/scripts/init_3_wait_dataverse_db_host.sh b/src/main/docker/scripts/init_3_wait_dataverse_db_host.sh index c234ad33307..06b41d60507 100644 --- a/src/main/docker/scripts/init_3_wait_dataverse_db_host.sh +++ b/src/main/docker/scripts/init_3_wait_dataverse_db_host.sh @@ -1,4 +1,4 @@ #It was reported on 9949 that on the first launch of the containers Dataverse would not be deployed on payara #this was caused by a race condition due postgress not being ready. A solion for docker compose was prepared #but didn't work due a compatibility issue on the Maven pluggin [https://github.com/fabric8io/docker-maven-plugin/issues/888] -wait-for "${DATAVERSE_DB_HOST:-postgres}:${DATAVERSE_DB_PORT:-5432}" -t 120 \ No newline at end of file +wait4x tcp "${DATAVERSE_DB_HOST:-postgres}:${DATAVERSE_DB_PORT:-5432}" -t 120s diff --git a/src/main/java/edu/harvard/iq/dataverse/AbstractGlobalIdServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/AbstractGlobalIdServiceBean.java deleted file mode 100644 index f1bfc3e290b..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/AbstractGlobalIdServiceBean.java +++ /dev/null @@ -1,700 +0,0 @@ -package edu.harvard.iq.dataverse; - -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; -import edu.harvard.iq.dataverse.util.SystemConfig; -import java.io.InputStream; -import jakarta.ejb.EJB; -import jakarta.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; -import org.jsoup.select.Elements; - -public abstract class AbstractGlobalIdServiceBean implements GlobalIdServiceBean { - - private static final Logger logger = Logger.getLogger(AbstractGlobalIdServiceBean.class.getCanonicalName()); - - @Inject - DataverseServiceBean dataverseService; - @EJB - protected - SettingsServiceBean settingsService; - @Inject - protected - DvObjectServiceBean dvObjectService; - @Inject - SystemConfig systemConfig; - - protected Boolean configured = null; - - public static String UNAVAILABLE = ":unav"; - - @Override - public Map getMetadataForCreateIndicator(DvObject dvObjectIn) { - logger.log(Level.FINE,"getMetadataForCreateIndicator(DvObject)"); - Map metadata = new HashMap<>(); - metadata = addBasicMetadata(dvObjectIn, metadata); - metadata.put("datacite.publicationyear", generateYear(dvObjectIn)); - metadata.put("_target", getTargetUrl(dvObjectIn)); - return metadata; - } - - protected Map getUpdateMetadata(DvObject dvObjectIn) { - logger.log(Level.FINE,"getUpdateMetadataFromDataset"); - Map metadata = new HashMap<>(); - metadata = addBasicMetadata(dvObjectIn, metadata); - return metadata; - } - - protected Map addBasicMetadata(DvObject dvObjectIn, Map metadata) { - - String authorString = dvObjectIn.getAuthorString(); - if (authorString.isEmpty() || authorString.contains(DatasetField.NA_VALUE)) { - authorString = UNAVAILABLE; - } - - String producerString = dataverseService.getRootDataverseName(); - - if (producerString.isEmpty() || producerString.equals(DatasetField.NA_VALUE)) { - producerString = UNAVAILABLE; - } - - String titleString = dvObjectIn.getCurrentName(); - - if (titleString.isEmpty() || titleString.equals(DatasetField.NA_VALUE)) { - titleString = UNAVAILABLE; - } - - metadata.put("datacite.creator", authorString); - metadata.put("datacite.title", titleString); - metadata.put("datacite.publisher", producerString); - metadata.put("datacite.publicationyear", generateYear(dvObjectIn)); - return metadata; - } - - protected Map addDOIMetadataForDestroyedDataset(DvObject dvObjectIn) { - Map metadata = new HashMap<>(); - String authorString = UNAVAILABLE; - String producerString = UNAVAILABLE; - String titleString = "This item has been removed from publication"; - - metadata.put("datacite.creator", authorString); - metadata.put("datacite.title", titleString); - metadata.put("datacite.publisher", producerString); - metadata.put("datacite.publicationyear", "9999"); - return metadata; - } - - protected String getTargetUrl(DvObject dvObjectIn) { - logger.log(Level.FINE,"getTargetUrl"); - return systemConfig.getDataverseSiteUrl() + dvObjectIn.getTargetUrl() + dvObjectIn.getGlobalId().asString(); - } - - @Override - public String getIdentifier(DvObject dvObject) { - GlobalId gid = dvObject.getGlobalId(); - return gid != null ? gid.asString() : null; - } - - protected String generateYear (DvObject dvObjectIn){ - return dvObjectIn.getYearPublishedCreated(); - } - - public Map getMetadataForTargetURL(DvObject dvObject) { - logger.log(Level.FINE,"getMetadataForTargetURL"); - HashMap metadata = new HashMap<>(); - metadata.put("_target", getTargetUrl(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(); - if (dvObject.isInstanceofDataset()) { - dvObject.setIdentifier(generateDatasetIdentifier((Dataset) dvObject)); - } else { - dvObject.setIdentifier(generateDataFileIdentifier((DataFile) dvObject)); - } - if (dvObject.getProtocol() == null) { - dvObject.setProtocol(protocol); - } - if (dvObject.getAuthority() == null) { - dvObject.setAuthority(authority); - } - 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; - } - // Occasionally, the protocol separator character ':' comes in still - // URL-encoded as %3A (usually as a result of the URL having been - // encoded twice): - fullIdentifierString = fullIdentifierString.replace("%3A", ":"); - - 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 { - - - private String template; - - public GlobalIdMetadataTemplate(){ - try (InputStream in = GlobalIdMetadataTemplate.class.getResourceAsStream("datacite_metadata_template.xml")) { - template = Util.readAndClose(in, "utf-8"); - } catch (Exception e) { - logger.log(Level.SEVERE, "datacite metadata template load error"); - logger.log(Level.SEVERE, "String " + e.toString()); - logger.log(Level.SEVERE, "localized message " + e.getLocalizedMessage()); - logger.log(Level.SEVERE, "cause " + e.getCause()); - logger.log(Level.SEVERE, "message " + e.getMessage()); - } - } - - private String xmlMetadata; - private String identifier; - private List datafileIdentifiers; - private List creators; - private String title; - private String publisher; - private String publisherYear; - private List authors; - private String description; - private List contacts; - private List producers; - - public List getProducers() { - return producers; - } - - public void setProducers(List producers) { - this.producers = producers; - } - - public List getContacts() { - return contacts; - } - - public void setContacts(List contacts) { - this.contacts = contacts; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public List getAuthors() { - return authors; - } - - public void setAuthors(List authors) { - this.authors = authors; - } - - - public List getDatafileIdentifiers() { - return datafileIdentifiers; - } - - public void setDatafileIdentifiers(List datafileIdentifiers) { - this.datafileIdentifiers = datafileIdentifiers; - } - - public GlobalIdMetadataTemplate(String xmlMetaData) { - this.xmlMetadata = xmlMetaData; - Document doc = Jsoup.parseBodyFragment(xmlMetaData); - Elements identifierElements = doc.select("identifier"); - if (identifierElements.size() > 0) { - identifier = identifierElements.get(0).html(); - } - Elements creatorElements = doc.select("creatorName"); - creators = new ArrayList<>(); - for (Element creatorElement : creatorElements) { - creators.add(creatorElement.html()); - } - Elements titleElements = doc.select("title"); - if (titleElements.size() > 0) { - title = titleElements.get(0).html(); - } - Elements publisherElements = doc.select("publisher"); - if (publisherElements.size() > 0) { - publisher = publisherElements.get(0).html(); - } - Elements publisherYearElements = doc.select("publicationYear"); - if (publisherYearElements.size() > 0) { - publisherYear = publisherYearElements.get(0).html(); - } - } - - public String generateXML(DvObject dvObject) { - // Can't use "UNKNOWN" here because DataCite will respond with "[facet 'pattern'] the value 'unknown' is not accepted by the pattern '[\d]{4}'" - String publisherYearFinal = "9999"; - // FIXME: Investigate why this.publisherYear is sometimes null now that pull request #4606 has been merged. - if (this.publisherYear != null) { - // Added to prevent a NullPointerException when trying to destroy datasets when using DataCite rather than EZID. - publisherYearFinal = this.publisherYear; - } - xmlMetadata = template.replace("${identifier}", getIdentifier().trim()) - .replace("${title}", this.title) - .replace("${publisher}", this.publisher) - .replace("${publisherYear}", publisherYearFinal) - .replace("${description}", this.description); - StringBuilder creatorsElement = new StringBuilder(); - for (DatasetAuthor author : authors) { - creatorsElement.append(""); - creatorsElement.append(author.getName().getDisplayValue()); - creatorsElement.append(""); - - if (author.getIdType() != null && author.getIdValue() != null && !author.getIdType().isEmpty() && !author.getIdValue().isEmpty() && author.getAffiliation() != null && !author.getAffiliation().getDisplayValue().isEmpty()) { - - if (author.getIdType().equals("ORCID")) { - creatorsElement.append("" + author.getIdValue() + ""); - } - if (author.getIdType().equals("ISNI")) { - creatorsElement.append("" + author.getIdValue() + ""); - } - if (author.getIdType().equals("LCNA")) { - creatorsElement.append("" + author.getIdValue() + ""); - } - } - if (author.getAffiliation() != null && !author.getAffiliation().getDisplayValue().isEmpty()) { - creatorsElement.append("" + author.getAffiliation().getDisplayValue() + ""); - } - creatorsElement.append(""); - } - xmlMetadata = xmlMetadata.replace("${creators}", creatorsElement.toString()); - - StringBuilder contributorsElement = new StringBuilder(); - for (String[] contact : this.getContacts()) { - if (!contact[0].isEmpty()) { - contributorsElement.append("" + contact[0] + ""); - if (!contact[1].isEmpty()) { - contributorsElement.append("" + contact[1] + ""); - } - contributorsElement.append(""); - } - } - for (String[] producer : this.getProducers()) { - contributorsElement.append("" + producer[0] + ""); - if (!producer[1].isEmpty()) { - contributorsElement.append("" + producer[1] + ""); - } - contributorsElement.append(""); - } - - String relIdentifiers = generateRelatedIdentifiers(dvObject); - - xmlMetadata = xmlMetadata.replace("${relatedIdentifiers}", relIdentifiers); - - xmlMetadata = xmlMetadata.replace("{$contributors}", contributorsElement.toString()); - return xmlMetadata; - } - - private String generateRelatedIdentifiers(DvObject dvObject) { - - StringBuilder sb = new StringBuilder(); - if (dvObject.isInstanceofDataset()) { - Dataset dataset = (Dataset) dvObject; - if (!dataset.getFiles().isEmpty() && !(dataset.getFiles().get(0).getIdentifier() == null)) { - - datafileIdentifiers = new ArrayList<>(); - for (DataFile dataFile : dataset.getFiles()) { - if (!dataFile.getGlobalId().asString().isEmpty()) { - if (sb.toString().isEmpty()) { - sb.append(""); - } - sb.append("" + dataFile.getGlobalId() + ""); - } - } - - if (!sb.toString().isEmpty()) { - sb.append(""); - } - } - } else if (dvObject.isInstanceofDataFile()) { - DataFile df = (DataFile) dvObject; - sb.append(""); - sb.append("" + df.getOwner().getGlobalId() + ""); - sb.append(""); - } - return sb.toString(); - } - - public void generateFileIdentifiers(DvObject dvObject) { - - if (dvObject.isInstanceofDataset()) { - Dataset dataset = (Dataset) dvObject; - - if (!dataset.getFiles().isEmpty() && !(dataset.getFiles().get(0).getIdentifier() == null)) { - - datafileIdentifiers = new ArrayList<>(); - for (DataFile dataFile : dataset.getFiles()) { - datafileIdentifiers.add(dataFile.getIdentifier()); - int x = xmlMetadata.indexOf("") - 1; - xmlMetadata = xmlMetadata.replace("{relatedIdentifier}", dataFile.getIdentifier()); - xmlMetadata = xmlMetadata.substring(0, x) + "${relatedIdentifier}" + template.substring(x, template.length() - 1); - - } - - } else { - xmlMetadata = xmlMetadata.replace("${relatedIdentifier}", ""); - } - } - } - - public String getTemplate() { - return template; - } - - public void setTemplate(String templateIn) { - template = templateIn; - } - - public String getIdentifier() { - return identifier; - } - - public void setIdentifier(String identifier) { - this.identifier = identifier; - } - - public List getCreators() { - return creators; - } - - public void setCreators(List creators) { - this.creators = creators; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getPublisher() { - return publisher; - } - - public void setPublisher(String publisher) { - this.publisher = publisher; - } - - public String getPublisherYear() { - return publisherYear; - } - - public void setPublisherYear(String publisherYear) { - this.publisherYear = publisherYear; - } -} - public String getMetadataFromDvObject(String identifier, Map metadata, DvObject dvObject) { - - Dataset dataset = null; - - if (dvObject instanceof Dataset) { - dataset = (Dataset) dvObject; - } else { - dataset = (Dataset) dvObject.getOwner(); - } - - GlobalIdMetadataTemplate metadataTemplate = new GlobalIdMetadataTemplate(); - metadataTemplate.setIdentifier(identifier.substring(identifier.indexOf(':') + 1)); - metadataTemplate.setCreators(Util.getListFromStr(metadata.get("datacite.creator"))); - metadataTemplate.setAuthors(dataset.getLatestVersion().getDatasetAuthors()); - if (dvObject.isInstanceofDataset()) { - metadataTemplate.setDescription(dataset.getLatestVersion().getDescriptionPlainText()); - } - if (dvObject.isInstanceofDataFile()) { - DataFile df = (DataFile) dvObject; - String fileDescription = df.getDescription(); - metadataTemplate.setDescription(fileDescription == null ? "" : fileDescription); - } - - metadataTemplate.setContacts(dataset.getLatestVersion().getDatasetContacts()); - metadataTemplate.setProducers(dataset.getLatestVersion().getDatasetProducers()); - metadataTemplate.setTitle(dvObject.getCurrentName()); - String producerString = dataverseService.getRootDataverseName(); - if (producerString.isEmpty() || producerString.equals(DatasetField.NA_VALUE) ) { - producerString = UNAVAILABLE; - } - metadataTemplate.setPublisher(producerString); - metadataTemplate.setPublisherYear(metadata.get("datacite.publicationyear")); - - String xmlMetadata = metadataTemplate.generateXML(dvObject); - 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/BannerMessage.java b/src/main/java/edu/harvard/iq/dataverse/BannerMessage.java index 214e26965fa..003d1057972 100644 --- a/src/main/java/edu/harvard/iq/dataverse/BannerMessage.java +++ b/src/main/java/edu/harvard/iq/dataverse/BannerMessage.java @@ -46,7 +46,7 @@ public void setBannerMessageTexts(Collection bannerMessageTex public String getDisplayValue(){ - String retVal = ""; + String retVal = null; for (BannerMessageText msgTxt : this.getBannerMessageTexts()) { if (msgTxt.getLang().equals(BundleUtil.getCurrentLocale().getLanguage())) { retVal = msgTxt.getMessage(); diff --git a/src/main/java/edu/harvard/iq/dataverse/BannerMessageServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/BannerMessageServiceBean.java index 0e757998d58..3961bd064db 100644 --- a/src/main/java/edu/harvard/iq/dataverse/BannerMessageServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/BannerMessageServiceBean.java @@ -46,8 +46,10 @@ public List findAllBannerMessages() { public void save( BannerMessage message ) { em.persist(message); + em.flush(); } + public void deleteBannerMessage(Object pk) { BannerMessage message = em.find(BannerMessage.class, pk); diff --git a/src/main/java/edu/harvard/iq/dataverse/DOIDataCiteRegisterCache.java b/src/main/java/edu/harvard/iq/dataverse/DOIDataCiteRegisterCache.java deleted file mode 100644 index 7c75b1a4da6..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/DOIDataCiteRegisterCache.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package edu.harvard.iq.dataverse; - - -import java.io.Serializable; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Lob; -import jakarta.persistence.NamedQueries; -import jakarta.persistence.NamedQuery; -import org.hibernate.validator.constraints.NotBlank; - -/** - * - * @author luopc - */ -@NamedQueries( - @NamedQuery( name="DOIDataCiteRegisterCache.findByDoi", - query="SELECT d FROM DOIDataCiteRegisterCache d WHERE d.doi=:doi") -) -@Entity -public class DOIDataCiteRegisterCache implements Serializable{ - - private static final long serialVersionUID = 8030143094734315681L; - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @NotBlank - @Column(unique=true) - private String doi; - - @NotBlank - private String url; - - @NotBlank - private String status; - - @NotBlank - @Lob - private String xml; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getDoi() { - return doi; - } - - public void setDoi(String doi) { - this.doi = doi; - } - - public String getStatus() { - return status; - } - - public void setStatus(String status) { - this.status = status; - } - - public String getXml() { - return xml; - } - - public void setXml(String xml) { - this.xml = xml; - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } -} \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/DOIDataCiteRegisterService.java b/src/main/java/edu/harvard/iq/dataverse/DOIDataCiteRegisterService.java deleted file mode 100644 index 9ecc4a3ecc9..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/DOIDataCiteRegisterService.java +++ /dev/null @@ -1,707 +0,0 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package edu.harvard.iq.dataverse; - -import edu.harvard.iq.dataverse.branding.BrandingUtil; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; -import jakarta.ejb.EJB; -import jakarta.ejb.Stateless; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import jakarta.persistence.TypedQuery; - -import edu.harvard.iq.dataverse.settings.JvmSettings; -import org.apache.commons.text.StringEscapeUtils; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - -/** - * - * @author luopc - */ -@Stateless -public class DOIDataCiteRegisterService { - - private static final Logger logger = Logger.getLogger(DOIDataCiteRegisterService.class.getCanonicalName()); - - @PersistenceContext(unitName = "VDCNet-ejbPU") - private EntityManager em; - - @EJB - DataverseServiceBean dataverseService; - - @EJB - DOIDataCiteServiceBean doiDataCiteServiceBean; - - - //A singleton since it, and the httpClient in it can be reused. - private DataCiteRESTfullClient client=null; - - private DataCiteRESTfullClient getClient() throws IOException { - if (client == null) { - client = new DataCiteRESTfullClient( - JvmSettings.DATACITE_MDS_API_URL.lookup(), - JvmSettings.DATACITE_USERNAME.lookup(), - JvmSettings.DATACITE_PASSWORD.lookup() - ); - } - return client; - } - - /** - * This method is deprecated and unused. We switched away from this method - * when adjusting the code to reserve DOIs from DataCite on dataset create. - * - * Note that the DOIDataCiteRegisterCache entity/table used in this method - * might be a candidate for deprecation as well. Removing it would require - * some refactoring as it is used throughout the DataCite code. - */ - @Deprecated - public String createIdentifierLocal(String identifier, Map metadata, DvObject dvObject) { - - String xmlMetadata = getMetadataFromDvObject(identifier, metadata, dvObject); - String status = metadata.get("_status").trim(); - String target = metadata.get("_target"); - String retString = ""; - DOIDataCiteRegisterCache rc = findByDOI(identifier); - if (rc == null) { - rc = new DOIDataCiteRegisterCache(); - rc.setDoi(identifier); - rc.setXml(xmlMetadata); - rc.setStatus("reserved"); - rc.setUrl(target); - em.persist(rc); - } else { - rc.setDoi(identifier); - rc.setXml(xmlMetadata); - rc.setStatus("reserved"); - rc.setUrl(target); - } - retString = "success to reserved " + identifier; - - return retString; - } - - /** - * This "reserveIdentifier" method is heavily based on the - * "registerIdentifier" method below but doesn't, this one doesn't doesn't - * register a URL, which causes the "state" of DOI to transition from - * "draft" to "findable". Here are some DataCite docs on the matter: - * - * "DOIs can exist in three states: draft, registered, and findable. DOIs - * are in the draft state when metadata have been registered, and will - * transition to the findable state when registering a URL." -- - * https://support.datacite.org/docs/mds-api-guide#doi-states - */ - public String reserveIdentifier(String identifier, Map metadata, DvObject dvObject) throws IOException { - String retString = ""; - String xmlMetadata = getMetadataFromDvObject(identifier, metadata, dvObject); - DOIDataCiteRegisterCache rc = findByDOI(identifier); - String target = metadata.get("_target"); - if (rc != null) { - rc.setDoi(identifier); - rc.setXml(xmlMetadata); - // DataCite uses the term "draft" instead of "reserved". - rc.setStatus("reserved"); - if (target == null || target.trim().length() == 0) { - target = rc.getUrl(); - } else { - rc.setUrl(target); - } - } - - DataCiteRESTfullClient client = getClient(); - retString = client.postMetadata(xmlMetadata); - - return retString; - } - - public String registerIdentifier(String identifier, Map metadata, DvObject dvObject) throws IOException { - String retString = ""; - String xmlMetadata = getMetadataFromDvObject(identifier, metadata, dvObject); - DOIDataCiteRegisterCache rc = findByDOI(identifier); - String target = metadata.get("_target"); - if (rc != null) { - rc.setDoi(identifier); - rc.setXml(xmlMetadata); - rc.setStatus("public"); - if (target == null || target.trim().length() == 0) { - target = rc.getUrl(); - } else { - rc.setUrl(target); - } - } - - DataCiteRESTfullClient client = getClient(); - retString = client.postMetadata(xmlMetadata); - client.postUrl(identifier.substring(identifier.indexOf(":") + 1), target); - - return retString; - } - - public String deactivateIdentifier(String identifier, Map metadata, DvObject dvObject) throws IOException { - String retString = ""; - - String metadataString = getMetadataForDeactivateIdentifier(identifier, metadata, dvObject); - retString = client.postMetadata(metadataString); - retString = client.inactiveDataset(identifier.substring(identifier.indexOf(":") + 1)); - - return retString; - } - - public static String getMetadataFromDvObject(String identifier, Map metadata, DvObject dvObject) { - - Dataset dataset = null; - - if (dvObject instanceof Dataset) { - dataset = (Dataset) dvObject; - } else { - dataset = (Dataset) dvObject.getOwner(); - } - - DataCiteMetadataTemplate metadataTemplate = new DataCiteMetadataTemplate(); - metadataTemplate.setIdentifier(identifier.substring(identifier.indexOf(':') + 1)); - metadataTemplate.setCreators(Util.getListFromStr(metadata.get("datacite.creator"))); - metadataTemplate.setAuthors(dataset.getLatestVersion().getDatasetAuthors()); - if (dvObject.isInstanceofDataset()) { - //While getDescriptionPlainText strips < and > from HTML, it leaves '&' (at least so we need to xml escape as well - String description = StringEscapeUtils.escapeXml10(dataset.getLatestVersion().getDescriptionPlainText()); - if (description.isEmpty() || description.equals(DatasetField.NA_VALUE)) { - description = AbstractGlobalIdServiceBean.UNAVAILABLE; - } - metadataTemplate.setDescription(description); - } - if (dvObject.isInstanceofDataFile()) { - DataFile df = (DataFile) dvObject; - //Note: File metadata is not escaped like dataset metadata is, so adding an xml escape here. - //This could/should be removed if the datafile methods add escaping - String fileDescription = StringEscapeUtils.escapeXml10(df.getDescription()); - metadataTemplate.setDescription(fileDescription == null ? AbstractGlobalIdServiceBean.UNAVAILABLE : fileDescription); - String datasetPid = df.getOwner().getGlobalId().asString(); - metadataTemplate.setDatasetIdentifier(datasetPid); - } else { - metadataTemplate.setDatasetIdentifier(""); - } - - metadataTemplate.setContacts(dataset.getLatestVersion().getDatasetContacts()); - metadataTemplate.setProducers(dataset.getLatestVersion().getDatasetProducers()); - String title = dvObject.getCurrentName(); - if(dvObject.isInstanceofDataFile()) { - //Note file title is not currently escaped the way the dataset title is, so adding it here. - title = StringEscapeUtils.escapeXml10(title); - } - - if (title.isEmpty() || title.equals(DatasetField.NA_VALUE)) { - title = AbstractGlobalIdServiceBean.UNAVAILABLE; - } - - metadataTemplate.setTitle(title); - String producerString = BrandingUtil.getRootDataverseCollectionName(); - if (producerString.isEmpty() || producerString.equals(DatasetField.NA_VALUE)) { - producerString = AbstractGlobalIdServiceBean.UNAVAILABLE; - } - metadataTemplate.setPublisher(producerString); - metadataTemplate.setPublisherYear(metadata.get("datacite.publicationyear")); - - String xmlMetadata = metadataTemplate.generateXML(dvObject); - logger.log(Level.FINE, "XML to send to DataCite: {0}", xmlMetadata); - return xmlMetadata; - } - - public static String getMetadataForDeactivateIdentifier(String identifier, Map metadata, DvObject dvObject) { - - DataCiteMetadataTemplate metadataTemplate = new DataCiteMetadataTemplate(); - metadataTemplate.setIdentifier(identifier.substring(identifier.indexOf(':') + 1)); - metadataTemplate.setCreators(Util.getListFromStr(metadata.get("datacite.creator"))); - - metadataTemplate.setDescription(AbstractGlobalIdServiceBean.UNAVAILABLE); - - String title =metadata.get("datacite.title"); - - System.out.print("Map metadata title: "+ metadata.get("datacite.title")); - - metadataTemplate.setAuthors(null); - - metadataTemplate.setTitle(title); - String producerString = AbstractGlobalIdServiceBean.UNAVAILABLE; - - metadataTemplate.setPublisher(producerString); - metadataTemplate.setPublisherYear(metadata.get("datacite.publicationyear")); - - String xmlMetadata = metadataTemplate.generateXML(dvObject); - logger.log(Level.FINE, "XML to send to DataCite: {0}", xmlMetadata); - return xmlMetadata; - } - - public String modifyIdentifier(String identifier, HashMap metadata, DvObject dvObject) throws IOException { - - String xmlMetadata = getMetadataFromDvObject(identifier, metadata, dvObject); - - logger.fine("XML to send to DataCite: " + xmlMetadata); - - String status = metadata.get("_status").trim(); - String target = metadata.get("_target"); - String retString = ""; - if (status.equals("reserved")) { - DOIDataCiteRegisterCache rc = findByDOI(identifier); - if (rc == null) { - rc = new DOIDataCiteRegisterCache(); - rc.setDoi(identifier); - rc.setXml(xmlMetadata); - rc.setStatus("reserved"); - rc.setUrl(target); - em.persist(rc); - } else { - rc.setDoi(identifier); - rc.setXml(xmlMetadata); - rc.setStatus("reserved"); - rc.setUrl(target); - } - retString = "success to reserved " + identifier; - } else if (status.equals("public")) { - DOIDataCiteRegisterCache rc = findByDOI(identifier); - if (rc != null) { - rc.setDoi(identifier); - rc.setXml(xmlMetadata); - rc.setStatus("public"); - if (target == null || target.trim().length() == 0) { - target = rc.getUrl(); - } else { - rc.setUrl(target); - } - try { - DataCiteRESTfullClient client = getClient(); - retString = client.postMetadata(xmlMetadata); - client.postUrl(identifier.substring(identifier.indexOf(":") + 1), target); - - } catch (UnsupportedEncodingException ex) { - logger.log(Level.SEVERE, null, ex); - - } catch (RuntimeException rte) { - logger.log(Level.SEVERE, "Error creating DOI at DataCite: {0}", rte.getMessage()); - logger.log(Level.SEVERE, "Exception", rte); - - } - } - } else if (status.equals("unavailable")) { - DOIDataCiteRegisterCache rc = findByDOI(identifier); - try { - DataCiteRESTfullClient client = getClient(); - if (rc != null) { - rc.setStatus("unavailable"); - retString = client.inactiveDataset(identifier.substring(identifier.indexOf(":") + 1)); - } - } catch (IOException io) { - - } - } - return retString; - } - - public boolean testDOIExists(String identifier) { - boolean doiExists; - try { - DataCiteRESTfullClient client = getClient(); - doiExists = client.testDOIExists(identifier.substring(identifier.indexOf(":") + 1)); - } catch (Exception e) { - logger.log(Level.INFO, identifier, e); - return false; - } - return doiExists; - } - - public HashMap getMetadata(String identifier) throws IOException { - HashMap metadata = new HashMap<>(); - try { - DataCiteRESTfullClient client = getClient(); - String xmlMetadata = client.getMetadata(identifier.substring(identifier.indexOf(":") + 1)); - DOIDataCiteServiceBean.GlobalIdMetadataTemplate template = doiDataCiteServiceBean.new GlobalIdMetadataTemplate(xmlMetadata); - metadata.put("datacite.creator", Util.getStrFromList(template.getCreators())); - metadata.put("datacite.title", template.getTitle()); - metadata.put("datacite.publisher", template.getPublisher()); - metadata.put("datacite.publicationyear", template.getPublisherYear()); - DOIDataCiteRegisterCache rc = findByDOI(identifier); - if (rc != null) { - metadata.put("_status", rc.getStatus()); - } else { - metadata.put("_status", "public"); - } - } catch (RuntimeException e) { - logger.log(Level.INFO, identifier, e); - } - return metadata; - } - - public DOIDataCiteRegisterCache findByDOI(String doi) { - TypedQuery query = em.createNamedQuery("DOIDataCiteRegisterCache.findByDoi", - DOIDataCiteRegisterCache.class); - query.setParameter("doi", doi); - List rc = query.getResultList(); - if (rc.size() == 1) { - return rc.get(0); - } - return null; - } - - public void deleteIdentifier(String identifier) { - DOIDataCiteRegisterCache rc = findByDOI(identifier); - if (rc != null) { - em.remove(rc); - } - } - -} - -class DataCiteMetadataTemplate { - - private static final Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.DataCiteMetadataTemplate"); - private static String template; - - static { - try (InputStream in = DataCiteMetadataTemplate.class.getResourceAsStream("datacite_metadata_template.xml")) { - template = Util.readAndClose(in, "utf-8"); - } catch (Exception e) { - logger.log(Level.SEVERE, "datacite metadata template load error"); - logger.log(Level.SEVERE, "String " + e.toString()); - logger.log(Level.SEVERE, "localized message " + e.getLocalizedMessage()); - logger.log(Level.SEVERE, "cause " + e.getCause()); - logger.log(Level.SEVERE, "message " + e.getMessage()); - } - } - - private String xmlMetadata; - private String identifier; - private String datasetIdentifier; - private List datafileIdentifiers; - private List creators; - private String title; - private String publisher; - private String publisherYear; - private List authors; - private String description; - private List contacts; - private List producers; - - public List getProducers() { - return producers; - } - - public void setProducers(List producers) { - this.producers = producers; - } - - public List getContacts() { - return contacts; - } - - public void setContacts(List contacts) { - this.contacts = contacts; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public List getAuthors() { - return authors; - } - - public void setAuthors(List authors) { - this.authors = authors; - } - - public DataCiteMetadataTemplate() { - } - - public List getDatafileIdentifiers() { - return datafileIdentifiers; - } - - public void setDatafileIdentifiers(List datafileIdentifiers) { - this.datafileIdentifiers = datafileIdentifiers; - } - - public DataCiteMetadataTemplate(String xmlMetaData) { - this.xmlMetadata = xmlMetaData; - Document doc = Jsoup.parseBodyFragment(xmlMetaData); - Elements identifierElements = doc.select("identifier"); - if (identifierElements.size() > 0) { - identifier = identifierElements.get(0).html(); - } - Elements creatorElements = doc.select("creatorName"); - creators = new ArrayList<>(); - for (Element creatorElement : creatorElements) { - creators.add(creatorElement.html()); - } - Elements titleElements = doc.select("title"); - if (titleElements.size() > 0) { - title = titleElements.get(0).html(); - } - Elements publisherElements = doc.select("publisher"); - if (publisherElements.size() > 0) { - publisher = publisherElements.get(0).html(); - } - Elements publisherYearElements = doc.select("publicationYear"); - if (publisherYearElements.size() > 0) { - publisherYear = publisherYearElements.get(0).html(); - } - } - - public String generateXML(DvObject dvObject) { - // Can't use "UNKNOWN" here because DataCite will respond with "[facet 'pattern'] the value 'unknown' is not accepted by the pattern '[\d]{4}'" - String publisherYearFinal = "9999"; - // FIXME: Investigate why this.publisherYear is sometimes null now that pull request #4606 has been merged. - if (this.publisherYear != null) { - // 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()) - .replace("${title}", this.title) - .replace("${publisher}", this.publisher) - .replace("${publisherYear}", publisherYearFinal) - .replace("${description}", this.description); - - StringBuilder creatorsElement = new StringBuilder(); - if (authors!= null && !authors.isEmpty()) { - for (DatasetAuthor author : authors) { - creatorsElement.append(""); - creatorsElement.append(author.getName().getDisplayValue()); - creatorsElement.append(""); - - if (author.getIdType() != null && author.getIdValue() != null && !author.getIdType().isEmpty() && !author.getIdValue().isEmpty() && author.getAffiliation() != null && !author.getAffiliation().getDisplayValue().isEmpty()) { - - if (author.getIdType().equals("ORCID")) { - creatorsElement.append("" + author.getIdValue() + ""); - } - if (author.getIdType().equals("ISNI")) { - creatorsElement.append("" + author.getIdValue() + ""); - } - if (author.getIdType().equals("LCNA")) { - creatorsElement.append("" + author.getIdValue() + ""); - } - } - if (author.getAffiliation() != null && !author.getAffiliation().getDisplayValue().isEmpty()) { - creatorsElement.append("" + author.getAffiliation().getDisplayValue() + ""); - } - creatorsElement.append(""); - } - - } else { - creatorsElement.append("").append(AbstractGlobalIdServiceBean.UNAVAILABLE).append(""); - } - - xmlMetadata = xmlMetadata.replace("${creators}", creatorsElement.toString()); - - StringBuilder contributorsElement = new StringBuilder(); - if (this.getContacts() != null) { - for (String[] contact : this.getContacts()) { - if (!contact[0].isEmpty()) { - contributorsElement.append("" + contact[0] + ""); - if (!contact[1].isEmpty()) { - contributorsElement.append("" + contact[1] + ""); - } - contributorsElement.append(""); - } - } - } - - if (this.getProducers() != null) { - for (String[] producer : this.getProducers()) { - contributorsElement.append("" + producer[0] + ""); - if (!producer[1].isEmpty()) { - contributorsElement.append("" + producer[1] + ""); - } - contributorsElement.append(""); - } - } - - String relIdentifiers = generateRelatedIdentifiers(dvObject); - - xmlMetadata = xmlMetadata.replace("${relatedIdentifiers}", relIdentifiers); - - xmlMetadata = xmlMetadata.replace("{$contributors}", contributorsElement.toString()); - return xmlMetadata; - } - - private String generateRelatedIdentifiers(DvObject dvObject) { - - StringBuilder sb = new StringBuilder(); - if (dvObject.isInstanceofDataset()) { - Dataset dataset = (Dataset) dvObject; - if (!dataset.getFiles().isEmpty() && !(dataset.getFiles().get(0).getIdentifier() == null)) { - - datafileIdentifiers = new ArrayList<>(); - for (DataFile dataFile : dataset.getFiles()) { - if (dataFile.getGlobalId() != null) { - if (sb.toString().isEmpty()) { - sb.append(""); - } - sb.append("" + dataFile.getGlobalId() + ""); - } - } - - if (!sb.toString().isEmpty()) { - sb.append(""); - } - } - } else if (dvObject.isInstanceofDataFile()) { - DataFile df = (DataFile) dvObject; - sb.append(""); - sb.append("" + df.getOwner().getGlobalId() + ""); - sb.append(""); - } - return sb.toString(); - } - - public void generateFileIdentifiers(DvObject dvObject) { - - if (dvObject.isInstanceofDataset()) { - Dataset dataset = (Dataset) dvObject; - - if (!dataset.getFiles().isEmpty() && !(dataset.getFiles().get(0).getIdentifier() == null)) { - - datafileIdentifiers = new ArrayList<>(); - for (DataFile dataFile : dataset.getFiles()) { - datafileIdentifiers.add(dataFile.getIdentifier()); - int x = xmlMetadata.indexOf("") - 1; - xmlMetadata = xmlMetadata.replace("{relatedIdentifier}", dataFile.getIdentifier()); - xmlMetadata = xmlMetadata.substring(0, x) + "${relatedIdentifier}" + template.substring(x, template.length() - 1); - - } - - } else { - xmlMetadata = xmlMetadata.replace("${relatedIdentifier}", ""); - } - } - } - - public static String getTemplate() { - return template; - } - - public static void setTemplate(String template) { - DataCiteMetadataTemplate.template = template; - } - - public String getIdentifier() { - return identifier; - } - - public void setIdentifier(String identifier) { - this.identifier = identifier; - } - - public void setDatasetIdentifier(String datasetIdentifier) { - this.datasetIdentifier = datasetIdentifier; - } - - public List getCreators() { - return creators; - } - - public void setCreators(List creators) { - this.creators = creators; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getPublisher() { - return publisher; - } - - public void setPublisher(String publisher) { - this.publisher = publisher; - } - - public String getPublisherYear() { - return publisherYear; - } - - public void setPublisherYear(String publisherYear) { - this.publisherYear = publisherYear; - } -} - -class Util { - - public static void close(InputStream in) { - if (in != null) { - try { - in.close(); - } catch (IOException e) { - throw new RuntimeException("Fail to close InputStream"); - } - } - } - - public static String readAndClose(InputStream inStream, String encoding) { - ByteArrayOutputStream outStream = new ByteArrayOutputStream(); - byte[] buf = new byte[128]; - String data; - try { - int cnt; - while ((cnt = inStream.read(buf)) >= 0) { - outStream.write(buf, 0, cnt); - } - data = outStream.toString(encoding); - } catch (IOException ioe) { - throw new RuntimeException("IOException"); - } finally { - close(inStream); - } - return data; - } - - public static List getListFromStr(String str) { - return Arrays.asList(str.split("; ")); -// List authors = new ArrayList(); -// int preIdx = 0; -// for(int i=0;i authors) { - StringBuilder str = new StringBuilder(); - for (String author : authors) { - if (str.length() > 0) { - str.append("; "); - } - str.append(author); - } - return str.toString(); - } - -} diff --git a/src/main/java/edu/harvard/iq/dataverse/DOIDataCiteServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DOIDataCiteServiceBean.java deleted file mode 100644 index 48786b41824..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/DOIDataCiteServiceBean.java +++ /dev/null @@ -1,248 +0,0 @@ -package edu.harvard.iq.dataverse; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.Base64; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; - -import jakarta.ejb.EJB; -import jakarta.ejb.Stateless; - -import edu.harvard.iq.dataverse.settings.JvmSettings; -import org.apache.commons.httpclient.HttpException; -import org.apache.commons.httpclient.HttpStatus; - - -/** - * - * @author luopc - */ -@Stateless -public class DOIDataCiteServiceBean extends DOIServiceBean { - - private static final Logger logger = Logger.getLogger(DOIDataCiteServiceBean.class.getCanonicalName()); - - private static final String PUBLIC = "public"; - private static final String FINDABLE = "findable"; - private static final String RESERVED = "reserved"; - private static final String DRAFT = "draft"; - - @EJB - DOIDataCiteRegisterService doiDataCiteRegisterService; - - @Override - public boolean registerWhenPublished() { - return false; - } - - - - @Override - 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 alreadyRegistered; - String identifier = pid.asString(); - try{ - alreadyRegistered = doiDataCiteRegisterService.testDOIExists(identifier); - } catch (Exception e){ - logger.log(Level.WARNING, "alreadyRegistered failed"); - return false; - } - return alreadyRegistered; - } - - @Override - public String createIdentifier(DvObject dvObject) throws Exception { - logger.log(Level.FINE,"createIdentifier"); - if(dvObject.getIdentifier() == null || dvObject.getIdentifier().isEmpty() ){ - dvObject = generateIdentifier(dvObject); - } - String identifier = getIdentifier(dvObject); - Map metadata = getMetadataForCreateIndicator(dvObject); - metadata.put("_status", "reserved"); - try { - String retString = doiDataCiteRegisterService.reserveIdentifier(identifier, metadata, dvObject); - logger.log(Level.FINE, "create DOI identifier retString : " + retString); - return retString; - } catch (Exception e) { - logger.log(Level.WARNING, "Identifier not created: create failed", e); - throw e; - } - } - - @Override - public Map getIdentifierMetadata(DvObject dvObject) { - logger.log(Level.FINE,"getIdentifierMetadata"); - String identifier = getIdentifier(dvObject); - Map metadata = new HashMap<>(); - try { - metadata = doiDataCiteRegisterService.getMetadata(identifier); - } catch (Exception e) { - logger.log(Level.WARNING, "getIdentifierMetadata failed", e); - } - return metadata; - } - - - /** - * Modifies the DOI metadata for a Dataset - * @param dvObject the dvObject whose metadata needs to be modified - * @return the Dataset identifier, or null if the modification failed - * @throws java.lang.Exception - */ - @Override - public String modifyIdentifierTargetURL(DvObject dvObject) throws Exception { - logger.log(Level.FINE,"modifyIdentifier"); - String identifier = getIdentifier(dvObject); - try { - HashMap metadata = doiDataCiteRegisterService.getMetadata(identifier); - doiDataCiteRegisterService.modifyIdentifier(identifier, metadata, dvObject); - } catch (Exception e) { - logger.log(Level.WARNING, "modifyMetadata failed", e); - throw e; - } - return identifier; - } - - public void deleteRecordFromCache(Dataset datasetIn){ - logger.log(Level.FINE,"deleteRecordFromCache"); - String identifier = getIdentifier(datasetIn); - HashMap doiMetadata = new HashMap(); - try { - doiMetadata = doiDataCiteRegisterService.getMetadata(identifier); - } catch (Exception e) { - logger.log(Level.WARNING, "get matadata failed cannot delete"); - logger.log(Level.WARNING, "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()); - } - - String idStatus = (String) doiMetadata.get("_status"); - - if (idStatus == null || idStatus.equals("reserved")) { - logger.log(Level.WARNING, "Delete status is reserved.."); - try { - doiDataCiteRegisterService.deleteIdentifier(identifier); - } catch (Exception e) { - logger.log(Level.WARNING, "delete failed"); - logger.log(Level.WARNING, "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()); - throw new RuntimeException(e); - } - } - } - - /* - * Deletes a DOI if it is in DRAFT/RESERVED state or removes metadata and changes it from PUBLIC/FINDABLE to REGISTERED. - */ - @Override - public void deleteIdentifier(DvObject dvObject) throws IOException, HttpException { - logger.log(Level.FINE,"deleteIdentifier"); - String identifier = getIdentifier(dvObject); - //ToDo - PidUtils currently has a DataCite API call that would get the status at DataCite for this identifier - that could be more accurate than assuming based on whether the dvObject has been published - String idStatus = DRAFT; - if(dvObject.isReleased()) { - idStatus = PUBLIC; - } - if ( idStatus != null ) { - switch ( idStatus ) { - case RESERVED: - case DRAFT: - logger.log(Level.INFO, "Delete status is reserved.."); - //service only removes the identifier from the cache (since it was written before DOIs could be registered in draft state) - doiDataCiteRegisterService.deleteIdentifier(identifier); - //So we call the deleteDraftIdentifier method below until things are refactored - deleteDraftIdentifier(dvObject); - break; - - case PUBLIC: - case FINDABLE: - //if public then it has been released set to unavailable and reset target to n2t url - Map metadata = addDOIMetadataForDestroyedDataset(dvObject); - metadata.put("_status", "registered"); - metadata.put("_target", getTargetUrl(dvObject)); - doiDataCiteRegisterService.deactivateIdentifier(identifier, metadata, dvObject); - break; - } - } - } - - /** - * Deletes DOI from the DataCite side, if possible. Only "draft" DOIs can be - * deleted. - */ - private void deleteDraftIdentifier(DvObject dvObject) throws IOException { - - //ToDo - incorporate into DataCiteRESTfulClient - 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 - * (only possible for "draft" DOIs), 405 (method not allowed) if the DOI - * wasn't deleted (because it's in "findable" state, for example, 404 if the - * DOI wasn't found, and possibly other status codes such as 500 if DataCite - * is down. - */ - - URL url = new URL(baseUrl + "/dois/" + doi.getAuthority() + "/" + doi.getIdentifier()); - HttpURLConnection connection = null; - connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("DELETE"); - String userpass = username + ":" + password; - String basicAuth = "Basic " + new String(Base64.getEncoder().encode(userpass.getBytes())); - connection.setRequestProperty("Authorization", basicAuth); - int status = connection.getResponseCode(); - if(status!=HttpStatus.SC_NO_CONTENT) { - logger.warning("Incorrect Response Status from DataCite: " + status + " : " + connection.getResponseMessage()); - throw new HttpException("Status: " + status); - } - logger.fine("deleteDoi status for " + doi.asString() + ": " + status); - } - - @Override - public boolean publicizeIdentifier(DvObject dvObject) { - logger.log(Level.FINE,"updateIdentifierStatus"); - if(dvObject.getIdentifier() == null || dvObject.getIdentifier().isEmpty() ){ - dvObject = generateIdentifier(dvObject); - } - String identifier = getIdentifier(dvObject); - Map metadata = getUpdateMetadata(dvObject); - metadata.put("_status", PUBLIC); - metadata.put("datacite.publicationyear", generateYear(dvObject)); - metadata.put("_target", getTargetUrl(dvObject)); - try { - doiDataCiteRegisterService.registerIdentifier(identifier, metadata, dvObject); - return true; - } catch (Exception e) { - logger.log(Level.WARNING, "modifyMetadata failed: " + e.getMessage(), e); - return false; - } - } - - - @Override - public List getProviderInformation(){ - return List.of("DataCite", "https://status.datacite.org"); - } - - - - @Override - protected String getProviderKeyName() { - return "DataCite"; - } -} diff --git a/src/main/java/edu/harvard/iq/dataverse/DOIServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DOIServiceBean.java deleted file mode 100644 index 0182c745cd0..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/DOIServiceBean.java +++ /dev/null @@ -1,78 +0,0 @@ -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 9b4b89db44f..3977023fc4b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataCitation.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataCitation.java @@ -7,6 +7,7 @@ import edu.harvard.iq.dataverse.branding.BrandingUtil; import edu.harvard.iq.dataverse.harvest.client.HarvestingClient; +import edu.harvard.iq.dataverse.pidproviders.AbstractPidProvider; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; @@ -14,6 +15,7 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; +import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; @@ -252,7 +254,7 @@ public String toBibtexString() { public void writeAsBibtexCitation(OutputStream os) throws IOException { // Use UTF-8 - Writer out = new BufferedWriter(new OutputStreamWriter(os, "utf-8")); + Writer out = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8)); if(getFileTitle() !=null && isDirect()) { out.write("@incollection{"); } else { @@ -316,7 +318,7 @@ public String toRISString() { public void writeAsRISCitation(OutputStream os) throws IOException { // Use UTF-8 - Writer out = new BufferedWriter(new OutputStreamWriter(os, "utf-8")); + Writer out = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8)); out.write("Provider: " + publisher + "\r\n"); out.write("Content: text/plain; charset=\"utf-8\"" + "\r\n"); // Using type "DATA" - see https://github.com/IQSS/dataverse/issues/4816 @@ -635,12 +637,12 @@ public Map getDataCiteMetadata() { String authorString = getAuthorsString(); if (authorString.isEmpty()) { - authorString = AbstractGlobalIdServiceBean.UNAVAILABLE; + authorString = AbstractPidProvider.UNAVAILABLE; } String producerString = getPublisher(); if (producerString.isEmpty()) { - producerString = AbstractGlobalIdServiceBean.UNAVAILABLE; + producerString = AbstractPidProvider.UNAVAILABLE; } metadata.put("datacite.creator", authorString); diff --git a/src/main/java/edu/harvard/iq/dataverse/DataFile.java b/src/main/java/edu/harvard/iq/dataverse/DataFile.java index 3d8086b142b..1a610d9ea6e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataFile.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataFile.java @@ -242,6 +242,18 @@ public void setEmbargo(Embargo embargo) { this.embargo = embargo; } + @ManyToOne + @JoinColumn(name="retention_id") + private Retention retention; + + public Retention getRetention() { + return retention; + } + + public void setRetention(Retention retention) { + this.retention = retention; + } + public DataFile() { this.fileMetadatas = new ArrayList<>(); initFileReplaceAttributes(); @@ -545,61 +557,61 @@ public void setDescription(String description) { fmd.setDescription(description); } } + + public FileMetadata getDraftFileMetadata() { + FileMetadata latestFileMetadata = getLatestFileMetadata(); + if (latestFileMetadata.getDatasetVersion().isDraft()) { + return latestFileMetadata; + } + return null; + } public FileMetadata getFileMetadata() { return getLatestFileMetadata(); } - + public FileMetadata getLatestFileMetadata() { - FileMetadata fmd = null; + FileMetadata resultFileMetadata = null; - // for newly added or harvested, just return the one fmd if (fileMetadatas.size() == 1) { return fileMetadatas.get(0); } - + for (FileMetadata fileMetadata : fileMetadatas) { - // if it finds a draft, return it if (fileMetadata.getDatasetVersion().getVersionState().equals(VersionState.DRAFT)) { return fileMetadata; - } - - // otherwise return the one with the latest version number - // duplicate logic in getLatestPublishedFileMetadata() - if (fmd == null || fileMetadata.getDatasetVersion().getVersionNumber().compareTo( fmd.getDatasetVersion().getVersionNumber() ) > 0 ) { - fmd = fileMetadata; - } else if ((fileMetadata.getDatasetVersion().getVersionNumber().compareTo( fmd.getDatasetVersion().getVersionNumber())==0 )&& - ( fileMetadata.getDatasetVersion().getMinorVersionNumber().compareTo( fmd.getDatasetVersion().getMinorVersionNumber()) > 0 ) ) { - fmd = fileMetadata; } + resultFileMetadata = getTheNewerFileMetadata(resultFileMetadata, fileMetadata); } - return fmd; + + return resultFileMetadata; } - -// //Returns null if no published version + public FileMetadata getLatestPublishedFileMetadata() throws UnsupportedOperationException { - FileMetadata fmd = null; - - for (FileMetadata fileMetadata : fileMetadatas) { - // if it finds a draft, skip - if (fileMetadata.getDatasetVersion().getVersionState().equals(VersionState.DRAFT)) { - continue; - } - - // otherwise return the one with the latest version number - // duplicate logic in getLatestFileMetadata() - if (fmd == null || fileMetadata.getDatasetVersion().getVersionNumber().compareTo( fmd.getDatasetVersion().getVersionNumber() ) > 0 ) { - fmd = fileMetadata; - } else if ((fileMetadata.getDatasetVersion().getVersionNumber().compareTo( fmd.getDatasetVersion().getVersionNumber())==0 )&& - ( fileMetadata.getDatasetVersion().getMinorVersionNumber().compareTo( fmd.getDatasetVersion().getMinorVersionNumber()) > 0 ) ) { - fmd = fileMetadata; - } - } - if(fmd == null) { + FileMetadata resultFileMetadata = fileMetadatas.stream() + .filter(metadata -> !metadata.getDatasetVersion().getVersionState().equals(VersionState.DRAFT)) + .reduce(null, DataFile::getTheNewerFileMetadata); + + if (resultFileMetadata == null) { throw new UnsupportedOperationException("No published metadata version for DataFile " + this.getId()); } - return fmd; + return resultFileMetadata; + } + + public static FileMetadata getTheNewerFileMetadata(FileMetadata current, FileMetadata candidate) { + if (current == null) { + return candidate; + } + + DatasetVersion currentVersion = current.getDatasetVersion(); + DatasetVersion candidateVersion = candidate.getDatasetVersion(); + + if (DatasetVersion.compareByVersion.compare(candidateVersion, currentVersion) > 0) { + return candidate; + } + + return current; } /** @@ -610,7 +622,7 @@ public long getFilesize() { if (this.filesize == null) { // -1 means "unknown" return -1; - } + } return this.filesize; } @@ -1111,4 +1123,23 @@ private boolean tagExists(String tagLabel) { } return false; } + + public boolean isDeaccessioned() { + // return true, if all published versions were deaccessioned + boolean inDeaccessionedVersions = false; + for (FileMetadata fmd : getFileMetadatas()) { + DatasetVersion testDsv = fmd.getDatasetVersion(); + if (testDsv.isReleased()) { + return false; + } + // Also check for draft version + if (testDsv.isDraft()) { + return false; + } + if (testDsv.isDeaccessioned()) { + inDeaccessionedVersions = true; + } + } + return inDeaccessionedVersions; // since any published version would have already returned + } } // 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 c9d50bbed9d..98ac8ff387f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataFileServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataFileServiceBean.java @@ -1,5 +1,6 @@ package edu.harvard.iq.dataverse; +import edu.harvard.iq.dataverse.DatasetVersion.VersionState; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.dataaccess.DataAccess; import edu.harvard.iq.dataverse.dataaccess.ImageThumbConverter; @@ -383,7 +384,8 @@ public FileMetadata findMostRecentVersionFileIsIn(DataFile file) { if (fileMetadatas == null || fileMetadatas.isEmpty()) { return null; } else { - return fileMetadatas.get(0); + // This assumes the order of filemetadatas is from first to most recent, which is true as of v6.3 + return fileMetadatas.get(fileMetadatas.size() - 1); } } @@ -759,6 +761,13 @@ public List findAll() { return em.createQuery("select object(o) from DataFile as o order by o.id", DataFile.class).getResultList(); } + public List findVersionStates(Long fileId) { + Query query = em.createQuery( + "select distinct dv.versionState from DatasetVersion dv where dv.id in (select fm.datasetVersion.id from FileMetadata fm where fm.dataFile.id=:fileId)"); + query.setParameter("fileId", fileId); + return query.getResultList(); + } + public DataFile save(DataFile dataFile) { if (dataFile.isMergeable()) { @@ -959,6 +968,7 @@ public boolean isThumbnailAvailable (DataFile file) { return true; } file.setPreviewImageFail(true); + file.setPreviewImageAvailable(false); this.save(file); return false; } @@ -1238,15 +1248,6 @@ public List selectFilesWithMissingOriginalSizes() { } - /** - * 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 the remote PID service if needed - * @param userIdentifier - * @param datafile - * @param idServiceBean - * @return {@code true} iff the global identifier is unique. - */ public void finalizeFileDelete(Long dataFileId, String storageLocation) throws IOException { // Verify that the DataFile no longer exists: if (find(dataFileId) != null) { @@ -1366,7 +1367,10 @@ public Embargo findEmbargo(Long id) { DataFile d = find(id); return d.getEmbargo(); } - + + public boolean isRetentionExpired(FileMetadata fm) { + return FileUtil.isRetentionExpired(fm); + } /** * Checks if the supplied DvObjectContainer (Dataset or Collection; although * only collection-level storage quotas are officially supported as of now) @@ -1401,4 +1405,16 @@ public UploadSessionQuotaLimit getUploadSessionQuotaLimit(DvObjectContainer pare return new UploadSessionQuotaLimit(quota.getAllocation(), currentSize); } + + public boolean isInReleasedVersion(Long id) { + Query query = em.createQuery("SELECT fm.id FROM FileMetadata fm, DvObject dvo WHERE fm.datasetVersion.id=(SELECT dv.id FROM DatasetVersion dv WHERE dv.dataset.id=dvo.owner.id and dv.versionState=edu.harvard.iq.dataverse.DatasetVersion.VersionState.RELEASED ORDER BY dv.versionNumber DESC, dv.minorVersionNumber DESC LIMIT 1) AND dvo.id=fm.dataFile.id AND fm.dataFile.id=:fid"); + query.setParameter("fid", id); + + try { + query.getSingleResult(); + return true; + } catch (Exception ex) { + return false; + } + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/DataTable.java b/src/main/java/edu/harvard/iq/dataverse/DataTable.java index a17d8c65138..95f3aed0f40 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataTable.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataTable.java @@ -112,6 +112,16 @@ public DataTable() { @Column( nullable = true ) private String originalFileName; + + /** + * The physical tab-delimited file is in storage with the list of variable + * names saved as the 1st line. This means that we do not need to generate + * this line on the fly. (Also means that direct download mechanism can be + * used for this file!) + */ + @Column(nullable = false) + private boolean storedWithVariableHeader = false; + /* * Getter and Setter methods: */ @@ -206,6 +216,14 @@ public void setOriginalFileName(String originalFileName) { this.originalFileName = originalFileName; } + public boolean isStoredWithVariableHeader() { + return storedWithVariableHeader; + } + + public void setStoredWithVariableHeader(boolean storedWithVariableHeader) { + this.storedWithVariableHeader = storedWithVariableHeader; + } + /* * Custom overrides for hashCode(), equals() and toString() methods: */ diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataset.java b/src/main/java/edu/harvard/iq/dataverse/Dataset.java index a2f560bc959..52cb7d6f2dc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataset.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataset.java @@ -1,11 +1,13 @@ package edu.harvard.iq.dataverse; import edu.harvard.iq.dataverse.dataset.DatasetThumbnail; +import edu.harvard.iq.dataverse.dataset.DatasetType; import edu.harvard.iq.dataverse.dataset.DatasetUtil; import edu.harvard.iq.dataverse.harvest.client.HarvestingClient; import edu.harvard.iq.dataverse.license.License; import edu.harvard.iq.dataverse.makedatacount.DatasetExternalCitations; import edu.harvard.iq.dataverse.makedatacount.DatasetMetrics; +import edu.harvard.iq.dataverse.settings.FeatureFlags; import java.nio.file.Path; import java.nio.file.Paths; import java.sql.Timestamp; @@ -128,6 +130,10 @@ public class Dataset extends DvObjectContainer { */ private boolean useGenericThumbnail; + @ManyToOne + @JoinColumn(name="datasettype_id", nullable = false) + private DatasetType datasetType; + @OneToOne(cascade = {CascadeType.MERGE, CascadeType.PERSIST}) @JoinColumn(name = "guestbook_id", unique = false, nullable = true, insertable = true, updatable = true) private Guestbook guestbook; @@ -206,6 +212,10 @@ public Dataset(boolean isHarvested) { StorageUse storageUse = new StorageUse(this); this.setStorageUse(storageUse); } + + if (FeatureFlags.DISABLE_DATASET_THUMBNAIL_AUTOSELECT.enabled()) { + this.setUseGenericThumbnail(true); + } } /** @@ -317,6 +327,7 @@ public boolean isDeaccessioned() { } return hasDeaccessionedVersions; // since any published version would have already returned } + public DatasetVersion getLatestVersion() { return getVersions().get(0); @@ -735,7 +746,15 @@ public boolean isUseGenericThumbnail() { public void setUseGenericThumbnail(boolean useGenericThumbnail) { this.useGenericThumbnail = useGenericThumbnail; } - + + public DatasetType getDatasetType() { + return datasetType; + } + + public void setDatasetType(DatasetType datasetType) { + this.datasetType = datasetType; + } + public List getDatasetMetrics() { return datasetMetrics; } @@ -852,6 +871,23 @@ public String getRemoteArchiveURL() { if (StringUtil.nonEmpty(this.getProtocol()) && StringUtil.nonEmpty(this.getAuthority()) && StringUtil.nonEmpty(this.getIdentifier())) { + + // If there is a custom archival url for this Harvesting + // Source, we'll use that + String harvestingUrl = this.getHarvestedFrom().getHarvestingUrl(); + String archivalUrl = this.getHarvestedFrom().getArchiveUrl(); + if (!harvestingUrl.contains(archivalUrl)) { + // When a Harvesting Client is created, the “archive url” is set to + // just the host part of the OAI url automatically. + // For example, if the OAI url was "https://remote.edu/oai", + // the archive url will default to "https://remote.edu/". + // If this is no longer true, we know it means the admin + // went to the trouble of setting it to something else - + // so we should use this url for the redirects back to source, + // instead of the global id resolver. + return archivalUrl + this.getAuthority() + "/" + this.getIdentifier(); + } + // ... if not, we'll redirect to the resolver for the global id: return this.getPersistentURL(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetField.java b/src/main/java/edu/harvard/iq/dataverse/DatasetField.java index c836a20893f..4bf6c00f199 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetField.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetField.java @@ -54,15 +54,18 @@ public int compare(DatasetField o1, DatasetField o2) { o2.getDatasetFieldType().getDisplayOrder() ); }}; - public static DatasetField createNewEmptyDatasetField(DatasetFieldType dsfType, Object dsv) { + public static DatasetField createNewEmptyDatasetField(DatasetFieldType dsfType, DatasetVersion dsv) { DatasetField dsfv = createNewEmptyDatasetField(dsfType); - //TODO - a better way to handle this? - if (dsv.getClass().getName().equals("edu.harvard.iq.dataverse.DatasetVersion")){ - dsfv.setDatasetVersion((DatasetVersion)dsv); - } else { - dsfv.setTemplate((Template)dsv); - } + dsfv.setDatasetVersion(dsv); + + return dsfv; + } + + public static DatasetField createNewEmptyDatasetField(DatasetFieldType dsfType, Template dsv) { + + DatasetField dsfv = createNewEmptyDatasetField(dsfType); + dsfv.setTemplate(dsv); return dsfv; } @@ -545,9 +548,12 @@ public String toString() { return "edu.harvard.iq.dataverse.DatasetField[ id=" + id + " ]"; } - public DatasetField copy(Object version) { + public DatasetField copy(DatasetVersion version) { return copy(version, null); } + public DatasetField copy(Template template) { + return copy(template, null); + } // originally this was an overloaded method, but we renamed it to get around an issue with Bean Validation // (that looked t overloaded methods, when it meant to look at overriden methods @@ -555,15 +561,15 @@ public DatasetField copyChild(DatasetFieldCompoundValue parent) { return copy(null, parent); } - private DatasetField copy(Object version, DatasetFieldCompoundValue parent) { + private DatasetField copy(Object versionOrTemplate, DatasetFieldCompoundValue parent) { DatasetField dsf = new DatasetField(); dsf.setDatasetFieldType(datasetFieldType); - if (version != null) { - if (version.getClass().getName().equals("edu.harvard.iq.dataverse.DatasetVersion")) { - dsf.setDatasetVersion((DatasetVersion) version); + if (versionOrTemplate != null) { + if (versionOrTemplate instanceof DatasetVersion) { + dsf.setDatasetVersion((DatasetVersion) versionOrTemplate); } else { - dsf.setTemplate((Template) version); + dsf.setTemplate((Template) versionOrTemplate); } } @@ -595,7 +601,8 @@ public boolean removeBlankDatasetFieldValues() { return true; } } else { // controlled vocab - if (this.getControlledVocabularyValues().isEmpty()) { + // during harvesting some CVV are put in getDatasetFieldValues. we don't want to remove those + if (this.getControlledVocabularyValues().isEmpty() && this.getDatasetFieldValues().isEmpty()) { return true; } } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldCompoundValue.java b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldCompoundValue.java index c679cd7edad..c03baec73af 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldCompoundValue.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldCompoundValue.java @@ -225,6 +225,15 @@ public Pair getLinkComponents() { return linkComponents.get(parentDatasetField.getDatasetFieldType().getName()); } + public boolean hasChildOfType(String name) { + for (DatasetField child : childDatasetFields) { + if (child.getDatasetFieldType().getName().equals(name)) { + return true; + } + } + return false; + } + private Map removeLastComma(Map mapIn) { Iterator> itr = mapIn.entrySet().iterator(); diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldConstant.java b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldConstant.java index 1621b80df55..abb812d1ba3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldConstant.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldConstant.java @@ -40,6 +40,7 @@ public class DatasetFieldConstant implements java.io.Serializable { public final static String note = "note"; + public final static String publicationRelationType = "publicationRelationType"; public final static String publicationCitation = "publicationCitation"; public final static String publicationIDType = "publicationIDType"; public final static String publicationIDNumber = "publicationIDNumber"; @@ -91,8 +92,9 @@ public class DatasetFieldConstant implements java.io.Serializable { public final static String datasetVersionValue="datasetVersionValue"; public final static String versionDate="versionDate"; public final static String keywordValue="keywordValue"; - public final static String keywordVocab="keywordVocabulary"; //SEK 6/10/2016 to match what is in the db - public final static String keywordVocabURI="keywordVocabularyURI"; //SEK 6/10/2016 to match what is in the db + public final static String keywordTermURI="keywordTermURI"; + public final static String keywordVocab="keywordVocabulary"; + public final static String keywordVocabURI="keywordVocabularyURI"; public final static String topicClassValue="topicClassValue"; public final static String topicClassVocab="topicClassVocab"; public final static String topicClassVocabURI="topicClassVocabURI"; @@ -112,8 +114,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: 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 northLatitude="northLatitude"; + public final static String southLatitude="southLatitude"; public final static String unitOfAnalysis="unitOfAnalysis"; public final static String universe="universe"; public final static String kindOfData="kindOfData"; @@ -156,6 +158,8 @@ public class DatasetFieldConstant implements java.io.Serializable { public final static String confidentialityDeclaration="confidentialityDeclaration"; public final static String specialPermissions="specialPermissions"; public final static String restrictions="restrictions"; + @Deprecated + //Doesn't appear to be used and is not datasetContact public final static String contact="contact"; public final static String citationRequirements="citationRequirements"; public final static String depositorRequirements="depositorRequirements"; @@ -486,6 +490,8 @@ public String getRestrictions() { return restrictions; } + @Deprecated + //Appears to not be used public String getContact() { return contact; } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldServiceBean.java index ce2b00086ec..ff78b0c83ec 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldServiceBean.java @@ -4,7 +4,9 @@ import java.io.StringReader; import java.net.URI; import java.net.URISyntaxException; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.security.InvalidParameterException; import java.sql.Timestamp; import java.text.MessageFormat; import java.time.Instant; @@ -34,10 +36,13 @@ import jakarta.persistence.NoResultException; import jakarta.persistence.NonUniqueResultException; import jakarta.persistence.PersistenceContext; +import jakarta.persistence.PersistenceException; import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.*; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.httpclient.HttpException; +import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpResponse; import org.apache.http.HttpResponseInterceptor; import org.apache.http.client.methods.HttpGet; @@ -46,7 +51,6 @@ import org.apache.http.impl.client.HttpClients; import org.apache.http.protocol.HttpContext; import org.apache.http.util.EntityUtils; - import edu.harvard.iq.dataverse.settings.SettingsServiceBean; /** @@ -81,6 +85,9 @@ public class DatasetFieldServiceBean implements java.io.Serializable { //Note that for primitive fields, the prent and term-uri-field are the same and these maps have the same entry Map cvocMapByTermUri = null; + //Flat list of cvoc term-uri and managed fields by Id + Set cvocFieldSet = null; + //The hash of the existing CVocConf setting. Used to determine when the setting has changed and it needs to be re-parsed to recreate the cvocMaps String oldHash = null; @@ -182,19 +189,14 @@ public ControlledVocabularyValue findControlledVocabularyValueByDatasetFieldType ControlledVocabularyValue cvv = typedQuery.getSingleResult(); return cvv; } catch (NoResultException | NonUniqueResultException ex) { - if (lenient) { - // if the value isn't found, check in the list of alternate values for this datasetFieldType - TypedQuery alternateQuery = em.createQuery("SELECT OBJECT(o) FROM ControlledVocabAlternate as o WHERE o.strValue = :strvalue AND o.datasetFieldType = :dsft", ControlledVocabAlternate.class); - alternateQuery.setParameter("strvalue", strValue); - alternateQuery.setParameter("dsft", dsft); - try { - ControlledVocabAlternate alternateValue = alternateQuery.getSingleResult(); - return alternateValue.getControlledVocabularyValue(); - } catch (NoResultException | NonUniqueResultException ex2) { - return null; - } - - } else { + // if the value isn't found, check in the list of alternate values for this datasetFieldType + TypedQuery alternateQuery = em.createQuery("SELECT OBJECT(o) FROM ControlledVocabAlternate as o WHERE o.strValue = :strvalue AND o.datasetFieldType = :dsft", ControlledVocabAlternate.class); + alternateQuery.setParameter("strvalue", strValue); + alternateQuery.setParameter("dsft", dsft); + try { + ControlledVocabAlternate alternateValue = alternateQuery.getSingleResult(); + return alternateValue.getControlledVocabularyValue(); + } catch (NoResultException | NonUniqueResultException ex2) { return null; } } @@ -279,6 +281,10 @@ public Map getCVocConf(boolean byTermUriField){ String cvocSetting = settingsService.getValueForKey(SettingsServiceBean.Key.CVocConf); if (cvocSetting == null || cvocSetting.isEmpty()) { oldHash=null; + //Release old maps + cvocMap=null; + cvocMapByTermUri=null; + cvocFieldSet = null; return new HashMap<>(); } String newHash = DigestUtils.md5Hex(cvocSetting); @@ -288,6 +294,7 @@ public Map getCVocConf(boolean byTermUriField){ oldHash=newHash; cvocMap=new HashMap<>(); cvocMapByTermUri=new HashMap<>(); + cvocFieldSet = new HashSet<>(); try (JsonReader jsonReader = Json.createReader(new StringReader(settingsService.getValueForKey(SettingsServiceBean.Key.CVocConf)))) { JsonArray cvocConfJsonArray = jsonReader.readArray(); @@ -304,11 +311,13 @@ public Map getCVocConf(boolean byTermUriField){ if (termUriField.equals(dft.getName())) { logger.fine("Found primitive field for term uri : " + dft.getName() + ": " + dft.getId()); cvocMapByTermUri.put(dft.getId(), jo); + cvocFieldSet.add(dft.getId()); } } else { DatasetFieldType childdft = findByNameOpt(jo.getString("term-uri-field")); logger.fine("Found term child field: " + childdft.getName()+ ": " + childdft.getId()); cvocMapByTermUri.put(childdft.getId(), jo); + cvocFieldSet.add(childdft.getId()); if (childdft.getParentDatasetFieldType() != dft) { logger.warning("Term URI field (" + childdft.getDisplayName() + ") not a child of parent: " + dft.getDisplayName()); @@ -319,14 +328,16 @@ public Map getCVocConf(boolean byTermUriField){ + jo.getString("term-uri-field")); } } - if (jo.containsKey("child-fields")) { - JsonArray childFields = jo.getJsonArray("child-fields"); - for (JsonString elm : childFields.getValuesAs(JsonString.class)) { - dft = findByNameOpt(elm.getString()); - logger.info("Found: " + dft.getName()); + if (jo.containsKey("managed-fields")) { + JsonObject managedFields = jo.getJsonObject("managed-fields"); + for (String s : managedFields.keySet()) { + dft = findByNameOpt(managedFields.getString(s)); if (dft == null) { logger.warning("Ignoring External Vocabulary setting for non-existent child field: " - + elm.getString()); + + managedFields.getString(s)); + } else { + logger.fine("Found: " + dft.getName()); + cvocFieldSet.add(dft.getId()); } } } @@ -338,17 +349,25 @@ public Map getCVocConf(boolean byTermUriField){ return byTermUriField ? cvocMapByTermUri : cvocMap; } + public Set getCvocFieldSet() { + return cvocFieldSet; + } + /** * Adds information about the external vocabulary term being used in this DatasetField to the ExternalVocabularyValue table if it doesn't already exist. * @param df - the primitive/parent compound field containing a newly saved value */ public void registerExternalVocabValues(DatasetField df) { - DatasetFieldType dft =df.getDatasetFieldType(); + DatasetFieldType dft = df.getDatasetFieldType(); logger.fine("Registering for field: " + dft.getName()); JsonObject cvocEntry = getCVocConf(true).get(dft.getId()); if (dft.isPrimitive()) { + List siblingsDatasetFields = new ArrayList<>(); + if(dft.getParentDatasetFieldType()!=null) { + siblingsDatasetFields = df.getParentDatasetFieldCompoundValue().getChildDatasetFields(); + } for (DatasetFieldValue dfv : df.getDatasetFieldValues()) { - registerExternalTerm(cvocEntry, dfv.getValue()); + registerExternalTerm(cvocEntry, dfv.getValue(), siblingsDatasetFields); } } else { if (df.getDatasetFieldType().isCompound()) { @@ -357,45 +376,55 @@ public void registerExternalVocabValues(DatasetField df) { 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()); + registerExternalTerm(cvocEntry, cdf.getValue(), cv.getChildDatasetFields()); } } } } } } - + /** - * 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 "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. - * + * Retrieves indexable strings from a cached externalvocabularyvalue entry filtered through retrieval-filtering configuration. + *

+ * This method assumes externalvocabularyvalue entries have been filtered and that they contain a single JsonObject. + * Cases Handled : A String, an Array of Strings, an Array of Objects with "value" or "content" keys, an Object with one or more entries that have String values or Array values with a set of String values. + * The string(s), or the "value/content"s for each language are added to the set. + * Retrieved string values are indexed in the term-uri-field (parameter defined in CVOC configuration) by default, or in the field specified by an optional "indexIn" parameter in the retrieval-filtering defined in the CVOC configuration. + *

* 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 * random info that would be bad to addd for searches, etc.) - * - * @param termUri + * + * @param termUri unique identifier to search in database + * @param cvocEntry related cvoc configuration + * @param indexingField name of solr field that will be filled with getStringsFor while indexing * @return - a set of indexable strings */ - public Set getStringsFor(String termUri) { - Set strings = new HashSet(); + public Set getIndexableStringsByTermUri(String termUri, JsonObject cvocEntry, String indexingField) { + Set strings = new HashSet<>(); JsonObject jo = getExternalVocabularyValue(termUri); + JsonObject filtering = cvocEntry.getJsonObject("retrieval-filtering"); + String termUriField = cvocEntry.getJsonString("term-uri-field").getString(); if (jo != null) { try { for (String key : jo.keySet()) { - if (key.equals("termName") || key.equals("personName")) { + String indexIn = filtering.getJsonObject(key).getString("indexIn", null); + // Either we are in mapping mode so indexingField (solr field) equals indexIn (cvoc config) + // Or we are in default mode indexingField is termUriField, indexIn is not defined then only termName and personName keys are used + if (indexingField.equals(indexIn) || + (indexIn == null && termUriField.equals(indexingField) && (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++) { + } else if (jv.getValueType().equals(JsonValue.ValueType.ARRAY)) { + JsonArray jarr = jv.asJsonArray(); + for (int i = 0; i < jarr.size(); i++) { + if (jarr.get(i).getValueType().equals(JsonValue.ValueType.STRING)) { + strings.add(jarr.getString(i)); + } else if (jarr.get(i).getValueType().equals(ValueType.OBJECT)) { // This condition handles SKOMOS format like [{"lang": "en","value": "non-apis bee"},{"lang": "fr","value": "abeille non apis"}] JsonObject entry = jarr.getJsonObject(i); if (entry.containsKey("value")) { logger.fine("adding " + entry.getString("value") + " for " + termUri); @@ -407,6 +436,22 @@ public Set getStringsFor(String termUri) { } } } + } else if (jv.getValueType().equals(JsonValue.ValueType.OBJECT)) { + JsonObject joo = jv.asJsonObject(); + for (Map.Entry entry : joo.entrySet()) { + if (entry.getValue().getValueType().equals(JsonValue.ValueType.STRING)) { // This condition handles format like { "fr": "association de quartier", "en": "neighborhood associations"} + logger.fine("adding " + joo.getString(entry.getKey()) + " for " + termUri); + strings.add(joo.getString(entry.getKey())); + } else if (entry.getValue().getValueType().equals(ValueType.ARRAY)) { // This condition handles format like {"en": ["neighbourhood societies"]} + JsonArray jarr = entry.getValue().asJsonArray(); + for (int i = 0; i < jarr.size(); i++) { + if (jarr.get(i).getValueType().equals(JsonValue.ValueType.STRING)) { + logger.fine("adding " + jarr.getString(i) + " for " + termUri); + strings.add(jarr.getString(i)); + } + } + } + } } } } @@ -418,7 +463,7 @@ public Set getStringsFor(String termUri) { } logger.fine("Returning " + String.join(",", strings) + " for " + termUri); return strings; - } + } /** * Perform a query to retrieve a cached value from the externalvocabularvalue table @@ -438,23 +483,28 @@ public JsonObject getExternalVocabularyValue(String termUri) { logger.warning("Problem parsing external vocab value for uri: " + termUri + " : " + e.getMessage()); } } catch (NoResultException nre) { - logger.warning("No external vocab value for uri: " + termUri); + //Could just be a plain text value + logger.fine("No external vocab value for uri: " + termUri); } return null; } /** * Perform a call to the external service to retrieve information about the term URI - * @param cvocEntry - the configuration for the DatasetFieldType associated with this term - * @param term - the term uri as a string + * + * @param cvocEntry - the configuration for the DatasetFieldType associated with this term + * @param term - the term uri as a string + * @param relatedDatasetFields - siblings or childs of the term */ - public void registerExternalTerm(JsonObject cvocEntry, String term) { + public void registerExternalTerm(JsonObject cvocEntry, String term, List relatedDatasetFields) { String retrievalUri = cvocEntry.getString("retrieval-uri"); + String termUriFieldName = cvocEntry.getString("term-uri-field"); String prefix = cvocEntry.getString("prefix", null); - if(term.isBlank()) { - logger.fine("Ingoring blank term"); + if(StringUtils.isBlank(term)) { + logger.fine("Ignoring blank term"); return; } + boolean isExternal = false; JsonObject vocabs = cvocEntry.getJsonObject("vocabs"); for (String key: vocabs.keySet()) { @@ -483,7 +533,22 @@ public void registerExternalTerm(JsonObject cvocEntry, String term) { } if (evv.getValue() == null) { String adjustedTerm = (prefix==null)? term: term.replace(prefix, ""); - retrievalUri = retrievalUri.replace("{0}", adjustedTerm); + + try { + retrievalUri = tryToReplaceRetrievalUriParam(retrievalUri, "0", adjustedTerm); + retrievalUri = tryToReplaceRetrievalUriParam(retrievalUri, termUriFieldName, adjustedTerm); + for (DatasetField f : relatedDatasetFields) { + retrievalUri = tryToReplaceRetrievalUriParam(retrievalUri, f.getDatasetFieldType().getName(), f.getValue()); + } + } catch (InvalidParameterException e) { + logger.warning("InvalidParameterException in tryReplaceRetrievalUriParam : " + e.getMessage()); + return; + } + if (retrievalUri.contains("{")) { + logger.severe("Retrieval URI still contains unreplaced parameter :" + retrievalUri); + return; + } + logger.fine("Didn't find " + term + ", calling " + retrievalUri); try (CloseableHttpClient httpClient = HttpClients.custom() .addInterceptorLast(new HttpResponseInterceptor() { @@ -502,14 +567,21 @@ public void process(HttpResponse response, HttpContext context) throws HttpExcep HttpGet httpGet = new HttpGet(retrievalUri); //application/json+ld is for backward compatibility httpGet.addHeader("Accept", "application/ld+json, application/json+ld, application/json"); - + //Adding others custom HTTP request headers if exists + final JsonObject headers = cvocEntry.getJsonObject("headers"); + if (headers != null) { + final Set headerKeys = headers.keySet(); + for (final String hKey: headerKeys) { + httpGet.addHeader(hKey, headers.getString(hKey)); + } + } HttpResponse response = httpClient.execute(httpGet); String data = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200) { logger.fine("Returned data: " + data); try (JsonReader jsonReader = Json.createReader(new StringReader(data))) { - String dataObj =filterResponse(cvocEntry, jsonReader.readObject(), term).toString(); + String dataObj = filterResponse(cvocEntry, jsonReader.readObject(), term).toString(); evv.setValue(dataObj); evv.setLastUpdateDate(Timestamp.from(Instant.now())); logger.fine("JsonObject: " + dataObj); @@ -518,6 +590,8 @@ public void process(HttpResponse response, HttpContext context) throws HttpExcep logger.fine("Wrote value for term: " + term); } catch (JsonException je) { logger.severe("Error retrieving: " + retrievalUri + " : " + je.getMessage()); + } catch (PersistenceException e) { + logger.fine("Problem persisting: " + retrievalUri + " : " + e.getMessage()); } } else { logger.severe("Received response code : " + statusCode + " when retrieving " + retrievalUri @@ -526,19 +600,42 @@ public void process(HttpResponse response, HttpContext context) throws HttpExcep } catch (IOException ioe) { logger.severe("IOException when retrieving url: " + retrievalUri + " : " + ioe.getMessage()); } - } } catch (URISyntaxException e) { logger.fine("Term is not a URI: " + term); } + } + + private String tryToReplaceRetrievalUriParam(String retrievalUri, String paramName, String value) throws InvalidParameterException { + + if(StringUtils.isBlank(paramName)) { + throw new InvalidParameterException("Empty or null paramName is not allowed while replacing retrieval uri parameter"); + } + + if(retrievalUri.contains(paramName)) { + logger.fine("Parameter {" + paramName + "} found in retrievalUri"); + + if(StringUtils.isBlank(value)) { + throw new InvalidParameterException("Empty or null value is not allowed while replacing retrieval uri parameter"); + } + + if(retrievalUri.contains("encodeUrl:" + paramName)) { + retrievalUri = retrievalUri.replace("{encodeUrl:"+paramName+"}", URLEncoder.encode(value, StandardCharsets.UTF_8)); + } else { + retrievalUri = retrievalUri.replace("{"+paramName+"}", value); + } + } else { + logger.fine("Parameter {" + paramName + "} not found in retrievalUri"); + } + return retrievalUri; } /** * Parse the raw value returned by an external service for a give term uri and * filter it according to the 'retrieval-filtering' configuration for this * DatasetFieldType, creating a Json value with the specified structure - * + * * @param cvocEntry - the config for this DatasetFieldType * @param readObject - the raw response from the service * @param termUri - the term uri @@ -597,6 +694,8 @@ private JsonObject filterResponse(JsonObject cvocEntry, JsonObject readObject, S if (pattern.equals("{0}")) { if (vals.get(0) instanceof JsonArray) { job.add(filterKey, (JsonArray) vals.get(0)); + } else if (vals.get(0) instanceof JsonObject) { + job.add(filterKey, (JsonObject) vals.get(0)); } else { job.add(filterKey, (String) vals.get(0)); } @@ -634,7 +733,7 @@ Object processPathSegment(int index, String[] pathParts, JsonValue curPath, Stri 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; @@ -663,7 +762,7 @@ Object processPathSegment(int index, String[] pathParts, JsonValue curPath, Stri } return parts.build(); } - + } else { curPath = ((JsonObject) curPath).get(pathParts[index]); logger.fine("Found next Path object " + curPath.toString()); @@ -764,4 +863,125 @@ public String getFieldLanguage(String languages, String localeCode) { } return null; } + + public List findAllDisplayedOnCreateInMetadataBlock(MetadataBlock metadataBlock) { + CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder(); + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(DatasetFieldType.class); + + Root metadataBlockRoot = criteriaQuery.from(MetadataBlock.class); + Root datasetFieldTypeRoot = criteriaQuery.from(DatasetFieldType.class); + + Predicate requiredInDataversePredicate = buildRequiredInDataversePredicate(criteriaBuilder, datasetFieldTypeRoot); + + criteriaQuery.where( + criteriaBuilder.and( + criteriaBuilder.equal(metadataBlockRoot.get("id"), metadataBlock.getId()), + datasetFieldTypeRoot.in(metadataBlockRoot.get("datasetFieldTypes")), + criteriaBuilder.or( + criteriaBuilder.isTrue(datasetFieldTypeRoot.get("displayOnCreate")), + requiredInDataversePredicate + ) + ) + ); + + criteriaQuery.select(datasetFieldTypeRoot).distinct(true); + + TypedQuery typedQuery = em.createQuery(criteriaQuery); + return typedQuery.getResultList(); + } + + public List findAllInMetadataBlockAndDataverse(MetadataBlock metadataBlock, Dataverse dataverse, boolean onlyDisplayedOnCreate) { + CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder(); + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(DatasetFieldType.class); + + Root metadataBlockRoot = criteriaQuery.from(MetadataBlock.class); + Root datasetFieldTypeRoot = criteriaQuery.from(DatasetFieldType.class); + Root dataverseRoot = criteriaQuery.from(Dataverse.class); + + // Join Dataverse with DataverseFieldTypeInputLevel on the "dataverseFieldTypeInputLevels" attribute, using a LEFT JOIN. + Join datasetFieldTypeInputLevelJoin = dataverseRoot.join("dataverseFieldTypeInputLevels", JoinType.LEFT); + + // Define a predicate to include DatasetFieldTypes that are marked as included in the input level. + Predicate includedAsInputLevelPredicate = criteriaBuilder.and( + criteriaBuilder.equal(datasetFieldTypeRoot, datasetFieldTypeInputLevelJoin.get("datasetFieldType")), + criteriaBuilder.isTrue(datasetFieldTypeInputLevelJoin.get("include")) + ); + + // Define a predicate to include DatasetFieldTypes that are marked as required in the input level. + Predicate requiredAsInputLevelPredicate = criteriaBuilder.and( + criteriaBuilder.equal(datasetFieldTypeRoot, datasetFieldTypeInputLevelJoin.get("datasetFieldType")), + criteriaBuilder.isTrue(datasetFieldTypeInputLevelJoin.get("required")) + ); + + // Create a subquery to check for the absence of a specific DataverseFieldTypeInputLevel. + Subquery subquery = criteriaQuery.subquery(Long.class); + Root subqueryRoot = subquery.from(DataverseFieldTypeInputLevel.class); + subquery.select(criteriaBuilder.literal(1L)) + .where( + criteriaBuilder.equal(subqueryRoot.get("dataverse"), dataverseRoot), + criteriaBuilder.equal(subqueryRoot.get("datasetFieldType"), datasetFieldTypeRoot) + ); + + // Define a predicate to exclude DatasetFieldTypes that have no associated input level (i.e., the subquery does not return a result). + Predicate hasNoInputLevelPredicate = criteriaBuilder.not(criteriaBuilder.exists(subquery)); + + // Define a predicate to include the required fields in Dataverse. + Predicate requiredInDataversePredicate = buildRequiredInDataversePredicate(criteriaBuilder, datasetFieldTypeRoot); + + // Define a predicate for displaying DatasetFieldTypes on create. + // If onlyDisplayedOnCreate is true, include fields that: + // - Are either marked as displayed on create OR marked as required, OR + // - Are required according to the input level. + // Otherwise, use an always-true predicate (conjunction). + Predicate displayedOnCreatePredicate = onlyDisplayedOnCreate + ? criteriaBuilder.or( + criteriaBuilder.or( + criteriaBuilder.isTrue(datasetFieldTypeRoot.get("displayOnCreate")), + requiredInDataversePredicate + ), + requiredAsInputLevelPredicate + ) + : criteriaBuilder.conjunction(); + + // Build the final WHERE clause by combining all the predicates. + criteriaQuery.where( + criteriaBuilder.equal(dataverseRoot.get("id"), dataverse.getId()), // Match the Dataverse ID. + criteriaBuilder.equal(metadataBlockRoot.get("id"), metadataBlock.getId()), // Match the MetadataBlock ID. + metadataBlockRoot.in(dataverseRoot.get("metadataBlocks")), // Ensure the MetadataBlock is part of the Dataverse. + datasetFieldTypeRoot.in(metadataBlockRoot.get("datasetFieldTypes")), // Ensure the DatasetFieldType is part of the MetadataBlock. + criteriaBuilder.or(includedAsInputLevelPredicate, hasNoInputLevelPredicate), // Include DatasetFieldTypes based on the input level predicates. + displayedOnCreatePredicate // Apply the display-on-create filter if necessary. + ); + + criteriaQuery.select(datasetFieldTypeRoot).distinct(true); + + return em.createQuery(criteriaQuery).getResultList(); + } + + private Predicate buildRequiredInDataversePredicate(CriteriaBuilder criteriaBuilder, Root datasetFieldTypeRoot) { + // Predicate to check if the current DatasetFieldType is required. + Predicate isRequired = criteriaBuilder.isTrue(datasetFieldTypeRoot.get("required")); + + // Subquery to check if the parentDatasetFieldType is required or null. + // We need this check to avoid including conditionally required fields. + Subquery subquery = criteriaBuilder.createQuery(Boolean.class).subquery(Boolean.class); + Root parentRoot = subquery.from(DatasetFieldType.class); + + subquery.select(criteriaBuilder.literal(true)) + .where( + criteriaBuilder.equal(parentRoot, datasetFieldTypeRoot.get("parentDatasetFieldType")), + criteriaBuilder.or( + criteriaBuilder.isNull(parentRoot.get("required")), + criteriaBuilder.isTrue(parentRoot.get("required")) + ) + ); + + // Predicate to check that either the parentDatasetFieldType meets the condition or doesn't exist (is null). + Predicate parentCondition = criteriaBuilder.or( + criteriaBuilder.exists(subquery), + criteriaBuilder.isNull(datasetFieldTypeRoot.get("parentDatasetFieldType")) + ); + + return criteriaBuilder.and(isRequired, parentCondition); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldType.java b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldType.java index 824b486a42d..01785359e0e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldType.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldType.java @@ -284,7 +284,7 @@ public void setDisplayOnCreate(boolean displayOnCreate) { } public boolean isControlledVocabulary() { - return controlledVocabularyValues != null && !controlledVocabularyValues.isEmpty(); + return allowControlledVocabulary; } /** diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetKeyword.java b/src/main/java/edu/harvard/iq/dataverse/DatasetKeyword.java deleted file mode 100644 index 747e3c068f1..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetKeyword.java +++ /dev/null @@ -1,68 +0,0 @@ -package edu.harvard.iq.dataverse; - -/** - * - * @author skraffmiller - */ - -public class DatasetKeyword { - - private int displayOrder; - public int getDisplayOrder() { - return this.displayOrder; - } - public void setDisplayOrder(int displayOrder) { - this.displayOrder = displayOrder; - } - - private DatasetField value; - public DatasetField getValue() { - return this.value; - } - public void setValue(DatasetField value) { - this.value = value; - } - - private DatasetVersion datasetVersion; - public DatasetVersion getDatasetVersion() { - return datasetVersion; - } - public void setDatasetVersion(DatasetVersion metadata) { - this.datasetVersion = metadata; - } - /* - @Version - private Long version; - public Long getVersion() { - return this.version; - } - public void setVersion(Long version) { - this.version = version; - } */ - - private DatasetField vocab; - public DatasetField getVocab() { - return this.vocab; - } - public void setVocab(DatasetField vocab) { - this.vocab = vocab; - } - - private DatasetField vocabURI; - public DatasetField getVocabURI() { - return this.vocabURI; - } - public void setVocabURI(DatasetField vocabURI) { - this.vocabURI = vocabURI; - } - - - public boolean isEmpty() { - /*return ((value==null || value.getValue().trim().equals("")) - && (vocab==null || vocab.getValue().trim().equals("")) - && (vocabURI==null || vocabURI.getValue().trim().equals("")));*/ - 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 b79f387f20b..8522f2733c7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -8,10 +8,10 @@ import edu.harvard.iq.dataverse.authorization.users.ApiToken; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; +import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.branding.BrandingUtil; import edu.harvard.iq.dataverse.dataaccess.StorageIO; -import edu.harvard.iq.dataverse.dataaccess.AbstractRemoteOverlayAccessIO; import edu.harvard.iq.dataverse.dataaccess.DataAccess; import edu.harvard.iq.dataverse.dataaccess.GlobusAccessibleStore; import edu.harvard.iq.dataverse.dataaccess.ImageThumbConverter; @@ -25,6 +25,7 @@ import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import edu.harvard.iq.dataverse.engine.command.impl.CheckRateLimitForDatasetPageCommand; import edu.harvard.iq.dataverse.engine.command.impl.CreatePrivateUrlCommand; import edu.harvard.iq.dataverse.engine.command.impl.CuratePublishedDatasetVersionCommand; import edu.harvard.iq.dataverse.engine.command.impl.DeaccessionDatasetVersionCommand; @@ -37,13 +38,17 @@ import edu.harvard.iq.dataverse.engine.command.impl.PublishDataverseCommand; import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; import edu.harvard.iq.dataverse.export.ExportService; +import edu.harvard.iq.dataverse.util.cache.CacheFactoryBean; 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.PidProvider; import edu.harvard.iq.dataverse.pidproviders.PidUtil; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; +import edu.harvard.iq.dataverse.pidproviders.doi.datacite.DataCiteDOIProvider; import edu.harvard.iq.dataverse.privateurl.PrivateUrl; import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; import edu.harvard.iq.dataverse.privateurl.PrivateUrlUtil; @@ -73,6 +78,7 @@ import java.lang.reflect.Method; import java.sql.Timestamp; import java.text.SimpleDateFormat; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -112,6 +118,7 @@ import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; import edu.harvard.iq.dataverse.engine.command.impl.AbstractSubmitToArchiveCommand; import edu.harvard.iq.dataverse.engine.command.impl.CreateNewDatasetCommand; +import edu.harvard.iq.dataverse.engine.command.impl.DeleteDatasetLinkingDataverseCommand; import edu.harvard.iq.dataverse.engine.command.impl.GetLatestPublishedDatasetVersionCommand; import edu.harvard.iq.dataverse.engine.command.impl.RequestRsyncScriptCommand; import edu.harvard.iq.dataverse.engine.command.impl.PublishDatasetResult; @@ -133,6 +140,7 @@ import jakarta.faces.event.AjaxBehaviorEvent; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.text.StringEscapeUtils; import org.apache.commons.lang3.mutable.MutableBoolean; @@ -240,12 +248,16 @@ public enum DisplayMode { SolrClientService solrClientService; @EJB DvObjectServiceBean dvObjectService; + @EJB + CacheFactoryBean cacheFactory; @Inject DataverseRequestServiceBean dvRequestService; @Inject DatasetVersionUI datasetVersionUI; @Inject PermissionsWrapper permissionsWrapper; + @Inject + NavigationWrapper navigationWrapper; @Inject FileDownloadHelper fileDownloadHelper; @Inject @@ -261,6 +273,8 @@ public enum DisplayMode { @Inject EmbargoServiceBean embargoService; @Inject + RetentionServiceBean retentionService; + @Inject LicenseServiceBean licenseServiceBean; @Inject DataFileCategoryServiceBean dataFileCategoryService; @@ -706,6 +720,16 @@ public void setNumberOfFilesToShow(Long numberOfFilesToShow) { this.numberOfFilesToShow = numberOfFilesToShow; } + private String returnReason = ""; + + public String getReturnReason() { + return returnReason; + } + + public void setReturnReason(String returnReason) { + this.returnReason = returnReason; + } + public void showAll(){ setNumberOfFilesToShow(new Long(fileMetadatasSearch.size())); } @@ -766,17 +790,42 @@ public boolean isIndexedVersion() { return isIndexedVersion = false; } + // plus we have mechanisms for disabling the facets selectively, just for + // the guests, or anonymous users: + if (session.getUser() instanceof GuestUser) { + if (settingsWrapper.isTrueForKey(SettingsServiceBean.Key.DisableSolrFacetsForGuestUsers, false)) { + return isIndexedVersion = false; + } + + // An even lower grade of user than Guest is a truly anonymous user - + // a guest user who came without the session cookie: + Map cookies = FacesContext.getCurrentInstance().getExternalContext().getRequestCookieMap(); + if (!(cookies != null && cookies.containsKey("JSESSIONID"))) { + if (settingsWrapper.isTrueForKey(SettingsServiceBean.Key.DisableSolrFacetsWithoutJsession, false)) { + return isIndexedVersion = false; + } + } + + } + + // The version is SUPPOSED to be indexed if it's the latest published version, or a // draft. So if none of the above is true, we can return false right away. if (!(workingVersion.isDraft() || isThisLatestReleasedVersion())) { return isIndexedVersion = false; } // If this is the latest published version, we want to confirm that this - // version was successfully indexed after the last publication - + // version was successfully indexed after the last publication if (isThisLatestReleasedVersion()) { - return isIndexedVersion = (workingVersion.getDataset().getIndexTime() != null) - && workingVersion.getDataset().getIndexTime().after(workingVersion.getReleaseTime()); + if (workingVersion.getDataset().getIndexTime() == null) { + return isIndexedVersion = false; + } + // We add 3 hours to the indexed time to prevent false negatives + // when indexed time gets overwritten in finalizing the publication step + // by a value before the release time + final long duration = 3 * 60 * 60 * 1000; + final Timestamp movedIndexTime = new Timestamp(workingVersion.getDataset().getIndexTime().getTime() + duration); + return isIndexedVersion = movedIndexTime.after(workingVersion.getReleaseTime()); } // Drafts don't have the indextime stamps set/incremented when indexed, @@ -1210,8 +1259,17 @@ public boolean canDownloadFiles() { canDownloadFiles = false; for (FileMetadata fmd : workingVersion.getFileMetadatas()) { if (fileDownloadHelper.canDownloadFile(fmd)) { - canDownloadFiles = true; - break; + if (isVersionHasGlobus()) { + String driverId = DataAccess + .getStorageDriverFromIdentifier(fmd.getDataFile().getStorageIdentifier()); + if (StorageIO.isDataverseAccessible(driverId)) { + canDownloadFiles = true; + break; + } + } else { + canDownloadFiles = true; + break; + } } } } @@ -1918,15 +1976,15 @@ private void setIdByPersistentId() { } private String init(boolean initFull) { - + // Check for rate limit exceeded. Must be done before anything else to prevent unnecessary processing. + if (!cacheFactory.checkRate(session.getUser(), new CheckRateLimitForDatasetPageCommand(null,null))) { + return navigationWrapper.tooManyRequests(); + } //System.out.println("_YE_OLDE_QUERY_COUNTER_"); // for debug purposes setDataverseSiteUrl(systemConfig.getDataverseSiteUrl()); guestbookResponse = new GuestbookResponse(); - String nonNullDefaultIfKeyNotFound = ""; - protocol = settingsWrapper.getValueForKey(SettingsServiceBean.Key.Protocol, nonNullDefaultIfKeyNotFound); - authority = settingsWrapper.getValueForKey(SettingsServiceBean.Key.Authority, nonNullDefaultIfKeyNotFound); String sortOrder = getSortOrder(); if(sortOrder != null) { FileMetadata.setCategorySortOrder(sortOrder); @@ -2026,7 +2084,7 @@ private String init(boolean initFull) { // to the local 404 page, below. logger.warning("failed to issue a redirect to "+originalSourceURL); } - return originalSourceURL; + return null; } return permissionsWrapper.notFound(); @@ -2108,8 +2166,6 @@ private String init(boolean initFull) { editMode = EditMode.CREATE; selectedHostDataverse = dataverseService.find(ownerId); dataset.setOwner(selectedHostDataverse); - dataset.setProtocol(protocol); - dataset.setAuthority(authority); if (dataset.getOwner() == null) { return permissionsWrapper.notFound(); @@ -2119,9 +2175,9 @@ private String init(boolean initFull) { //Wait until the create command before actually getting an identifier, except if we're using directUpload //Need to assign an identifier prior to calls to requestDirectUploadUrl if direct upload is used. if ( isEmpty(dataset.getIdentifier()) && systemConfig.directUploadEnabled(dataset) ) { - CommandContext ctxt = commandEngine.getContext(); - GlobalIdServiceBean idServiceBean = GlobalIdServiceBean.getBean(ctxt); - dataset.setIdentifier(idServiceBean.generateDatasetIdentifier(dataset)); + CommandContext ctxt = commandEngine.getContext(); + PidProvider pidProvider = ctxt.dvObjects().getEffectivePidGenerator(dataset); + pidProvider.generatePid(dataset); } dataverseTemplates.addAll(dataverseService.find(ownerId).getTemplates()); if (!dataverseService.find(ownerId).isTemplateRoot()) { @@ -2181,6 +2237,11 @@ private String init(boolean initFull) { } } + LocalDate minRetentiondate = settingsWrapper.getMinRetentionDate(); + if (minRetentiondate != null){ + selectionRetention.setDateUnavailable(minRetentiondate.plusDays(1L)); + } + displayLockInfo(dataset); displayPublishMessage(); @@ -2257,13 +2318,11 @@ private void displayPublishMessage(){ public boolean isValid() { if (valid == null) { - DatasetVersion version = dataset.getLatestVersion(); - if (!version.isDraft()) { + if (workingVersion.isDraft() || (canUpdateDataset() && JvmSettings.UI_SHOW_VALIDITY_LABEL_WHEN_PUBLISHED.lookupOptional(Boolean.class).orElse(true))) { + valid = workingVersion.isValid(); + } else { valid = true; } - DatasetVersion newVersion = version.cloneDatasetVersion(); - newVersion.setDatasetFields(newVersion.initDatasetFields()); - valid = newVersion.isValid(); } return valid; } @@ -2326,14 +2385,17 @@ private void displayLockInfo(Dataset dataset) { lockedDueToIngestVar = true; } - // With DataCite, we try to reserve the DOI when the dataset is created. Sometimes this - // fails because DataCite is down. We show the message below to set expectations that the - // "Publish" button won't work until the DOI has been reserved using the "Reserve PID" API. - if (settingsWrapper.isDataCiteInstallation() && dataset.getGlobalIdCreateTime() == null && editMode != EditMode.CREATE) { - JH.addMessage(FacesMessage.SEVERITY_WARN, BundleUtil.getStringFromBundle("dataset.locked.pidNotReserved.message"), - BundleUtil.getStringFromBundle("dataset.locked.pidNotReserved.message.details")); + if (dataset.getGlobalIdCreateTime() == null && editMode != EditMode.CREATE) { + // With DataCite, we try to reserve the DOI when the dataset is created. Sometimes this + // fails because DataCite is down. We show the message below to set expectations that the + // "Publish" button won't work until the DOI has been reserved using the "Reserve PID" API. + PidProvider pidProvider = PidUtil.getPidProvider(dataset.getGlobalId().getProviderId()); + if (DataCiteDOIProvider.TYPE.equals(pidProvider.getProviderType())) { + JH.addMessage(FacesMessage.SEVERITY_WARN, + BundleUtil.getStringFromBundle("dataset.locked.pidNotReserved.message"), + BundleUtil.getStringFromBundle("dataset.locked.pidNotReserved.message.details")); + } } - //if necessary refresh publish message also displayPublishMessage(); @@ -2652,8 +2714,7 @@ public void edit(EditMode editMode) { public String sendBackToContributor() { try { - //FIXME - Get Return Comment from sendBackToContributor popup - Command cmd = new ReturnDatasetToAuthorCommand(dvRequestService.getDataverseRequest(), dataset, ""); + Command cmd = new ReturnDatasetToAuthorCommand(dvRequestService.getDataverseRequest(), dataset, returnReason); dataset = commandEngine.submit(cmd); JsfHelper.addSuccessMessage(BundleUtil.getStringFromBundle("dataset.reject.success")); } catch (CommandException ex) { @@ -3236,7 +3297,7 @@ public void startDownloadSelectedOriginal() { private void startDownload(boolean downloadOriginal){ boolean guestbookRequired = isDownloadPopupRequired(); - boolean validate = validateFilesForDownload(downloadOriginal); + boolean validate = validateFilesForDownload(downloadOriginal, false); if (validate) { updateGuestbookResponse(guestbookRequired, downloadOriginal, false); if(!guestbookRequired && !getValidateFilesOutcome().equals("Mixed")){ @@ -3259,7 +3320,7 @@ public void setValidateFilesOutcome(String validateFilesOutcome) { this.validateFilesOutcome = validateFilesOutcome; } - public boolean validateFilesForDownload(boolean downloadOriginal){ + public boolean validateFilesForDownload(boolean downloadOriginal, boolean isGlobusTransfer){ if (this.selectedFiles.isEmpty()) { PrimeFaces.current().executeScript("PF('selectFilesForDownload').show()"); return false; @@ -3276,33 +3337,39 @@ public boolean validateFilesForDownload(boolean downloadOriginal){ return false; } - for (FileMetadata fmd : getSelectedDownloadableFiles()) { - DataFile dataFile = fmd.getDataFile(); - if (downloadOriginal && dataFile.isTabularData()) { - bytes += dataFile.getOriginalFileSize() == null ? 0 : dataFile.getOriginalFileSize(); - } else { - bytes += dataFile.getFilesize(); + if (!isGlobusTransfer) { + for (FileMetadata fmd : getSelectedDownloadableFiles()) { + DataFile dataFile = fmd.getDataFile(); + if (downloadOriginal && dataFile.isTabularData()) { + bytes += dataFile.getOriginalFileSize() == null ? 0 : dataFile.getOriginalFileSize(); + } else { + bytes += dataFile.getFilesize(); + } } - } - //if there are two or more files, with a total size - //over the zip limit, post a "too large" popup - if (bytes > settingsWrapper.getZipDownloadLimit() && selectedDownloadableFiles.size() > 1) { - setValidateFilesOutcome("FailSize"); - return false; + // if there are two or more files, with a total size + // over the zip limit, post a "too large" popup + if (bytes > settingsWrapper.getZipDownloadLimit() && selectedDownloadableFiles.size() > 1) { + setValidateFilesOutcome("FailSize"); + return false; + } } - + // If some of the files were restricted and we had to drop them off the // list, and NONE of the files are left on the downloadable list - // - we show them a "you're out of luck" popup: - if (getSelectedDownloadableFiles().isEmpty() && getSelectedGlobusTransferableFiles().isEmpty() && !getSelectedNonDownloadableFiles().isEmpty()) { + // - we show them a "you're out of luck" popup + // Same for globus transfer + if ((!isGlobusTransfer + && (getSelectedDownloadableFiles().isEmpty() && !getSelectedNonDownloadableFiles().isEmpty())) + || (isGlobusTransfer && (getSelectedGlobusTransferableFiles().isEmpty() + && !getSelectedNonGlobusTransferableFiles().isEmpty()))) { setValidateFilesOutcome("FailRestricted"); return false; } - //Some are selected and there are non-downloadable ones or there are both downloadable and globus transferable files - if ((!(getSelectedDownloadableFiles().isEmpty() && getSelectedGlobusTransferableFiles().isEmpty()) - && (!getSelectedNonDownloadableFiles().isEmpty()) || (!getSelectedDownloadableFiles().isEmpty() && !getSelectedGlobusTransferableFiles().isEmpty()))) { + //For download or transfer, there are some that can be downloaded/transferred and some that can't + if ((!isGlobusTransfer && (!getSelectedNonDownloadableFiles().isEmpty() && !getSelectedDownloadableFiles().isEmpty())) || + (isGlobusTransfer && (!getSelectedNonGlobusTransferableFiles().isEmpty() && !getSelectedGlobusTransferableFiles().isEmpty()))) { setValidateFilesOutcome("Mixed"); return true; } @@ -3362,7 +3429,7 @@ private boolean filterSelectedFiles(){ if(globusDownloadEnabled) { String driverId = DataAccess.getStorageDriverFromIdentifier(fmd.getDataFile().getStorageIdentifier()); globusTransferable = GlobusAccessibleStore.isGlobusAccessible(driverId); - downloadable = downloadable && !AbstractRemoteOverlayAccessIO.isNotDataverseAccessible(driverId); + downloadable = downloadable && StorageIO.isDataverseAccessible(driverId); } if(downloadable){ getSelectedDownloadableFiles().add(fmd); @@ -3496,6 +3563,16 @@ public void saveLinkingDataverses(ActionEvent evt) { } alreadyLinkedDataverses = null; //force update to list of linked dataverses } + public void deleteLinkingDataverses(ActionEvent evt) { + + if (deleteLink(selectedDataverseForLinking)) { + JsfHelper.addSuccessMessage(BundleUtil.getStringFromBundle("dataset.message.unlinkSuccess", getSuccessMessageArguments())); + } else { + FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_INFO, BundleUtil.getStringFromBundle("dataset.notlinked"), linkingDataverseErrorMessage); + FacesContext.getCurrentInstance().addMessage(null, message); + } + alreadyLinkedDataverses = null; //force update to list of linked dataverses + } private String linkingDataverseErrorMessage = ""; @@ -3530,6 +3607,25 @@ private Boolean saveLink(Dataverse dataverse){ } return retVal; } + private Boolean deleteLink(Dataverse dataverse){ + boolean retVal = true; + linkingDataverse = dataverse; + try { + DatasetLinkingDataverse dsld = dsLinkingService.findDatasetLinkingDataverse(dataset.getId(), linkingDataverse.getId()); + DeleteDatasetLinkingDataverseCommand cmd = new DeleteDatasetLinkingDataverseCommand(dvRequestService.getDataverseRequest(), dataset, dsld, true); + commandEngine.submit(cmd); + } catch (CommandException ex) { + String msg = "There was a problem removing the link between this dataset to yours: " + ex; + logger.severe(msg); + msg = BundleUtil.getStringFromBundle("dataset.notlinked.msg") + ex; + /** + * @todo how do we get this message to show up in the GUI? + */ + linkingDataverseErrorMessage = msg; + retVal = false; + } + return retVal; + } private String alreadyLinkedDataverses = null; @@ -3556,6 +3652,14 @@ public List completeLinkingDataverse(String query) { return null; } } + public List completeUnLinkingDataverse(String query) { + dataset = datasetService.find(dataset.getId()); + if (session.getUser().isAuthenticated()) { + return dataverseService.filterDataversesForUnLinking(query, dvRequestService.getDataverseRequest(), dataset); + } else { + return null; + } + } public List completeHostDataverseMenuList(String query) { if (session.getUser().isAuthenticated()) { @@ -3660,6 +3764,25 @@ public String deleteFiles() throws CommandException{ } } + //Remove retentions that are no longer referenced + //Identify which ones are involved here + List orphanedRetentions = new ArrayList(); + if (selectedFiles != null && selectedFiles.size() > 0) { + for (FileMetadata fmd : workingVersion.getFileMetadatas()) { + for (FileMetadata fm : selectedFiles) { + if (fm.getDataFile().equals(fmd.getDataFile()) && !fmd.getDataFile().isReleased()) { + Retention ret = fmd.getDataFile().getRetention(); + if (ret != null) { + ret.getDataFiles().remove(fmd.getDataFile()); + if (ret.getDataFiles().isEmpty()) { + orphanedRetentions.add(ret); + } + } + } + } + } + } + deleteFiles(filesToDelete); String retVal; @@ -3669,12 +3792,14 @@ public String deleteFiles() throws CommandException{ } else { retVal = save(); } - - - //And delete them only after the dataset is updated + + // And delete them only after the dataset is updated for(Embargo emb: orphanedEmbargoes) { embargoService.deleteById(emb.getId(), ((AuthenticatedUser)session.getUser()).getUserIdentifier()); } + for(Retention ret: orphanedRetentions) { + retentionService.delete(ret, ((AuthenticatedUser)session.getUser()).getUserIdentifier()); + } return retVal; } @@ -3864,12 +3989,6 @@ public String save() { ((UpdateDatasetVersionCommand) cmd).setValidateLenient(true); } dataset = commandEngine.submit(cmd); - for (DatasetField df : dataset.getLatestVersion().getFlatDatasetFields()) { - logger.fine("Found id: " + df.getDatasetFieldType().getId()); - if (fieldService.getCVocConf(true).containsKey(df.getDatasetFieldType().getId())) { - fieldService.registerExternalVocabValues(df); - } - } if (editMode == EditMode.CREATE) { if (session.getUser() instanceof AuthenticatedUser) { userNotificationService.sendNotification((AuthenticatedUser) session.getUser(), dataset.getCreateDate(), UserNotification.Type.CREATEDS, dataset.getLatestVersion().getId()); @@ -5341,7 +5460,7 @@ public boolean isFileAccessRequestMultiSignUpButtonEnabled(){ return false; } for (FileMetadata fmd : this.selectedRestrictedFiles){ - if (!this.fileDownloadHelper.canDownloadFile(fmd)&& !FileUtil.isActivelyEmbargoed(fmd)){ + if (!this.fileDownloadHelper.canDownloadFile(fmd) && !FileUtil.isActivelyEmbargoed(fmd)){ return true; } } @@ -5499,12 +5618,19 @@ public void setPrivateUrlJustCreatedToFalse() { public boolean isShowLinkingPopup() { return showLinkingPopup; } + public boolean isShowUnLinkingPopup() { + return showUnLinkingPopup; + } public void setShowLinkingPopup(boolean showLinkingPopup) { this.showLinkingPopup = showLinkingPopup; } + public void setShowUnLinkingPopup(boolean showUnLinkingPopup) { + this.showUnLinkingPopup = showUnLinkingPopup; + } private boolean showLinkingPopup = false; + private boolean showUnLinkingPopup = false; private Boolean anonymizedAccess = null; // @@ -5702,7 +5828,10 @@ public boolean isShowPreviewButton(Long fileId) { public boolean isShowQueryButton(Long fileId) { DataFile dataFile = datafileService.find(fileId); - if(dataFile.isRestricted() || !dataFile.isReleased() || FileUtil.isActivelyEmbargoed(dataFile)){ + if(dataFile.isRestricted() + || !dataFile.isReleased() + || FileUtil.isActivelyEmbargoed(dataFile) + || FileUtil.isRetentionExpired(dataFile)){ return false; } @@ -5800,6 +5929,19 @@ public boolean isThisLatestReleasedVersion() { } + public String getCroissant() { + if (isThisLatestReleasedVersion()) { + final String CROISSANT_SCHEMA_NAME = "croissant"; + ExportService instance = ExportService.getInstance(); + String croissant = instance.getExportAsString(dataset, CROISSANT_SCHEMA_NAME); + if (croissant != null && !croissant.isEmpty()) { + logger.fine("Returning cached CROISSANT."); + return croissant; + } + } + return null; + } + public String getJsonLd() { if (isThisLatestReleasedVersion()) { ExportService instance = ExportService.getInstance(); @@ -6247,12 +6389,18 @@ public void clearSelectionEmbargo() { PrimeFaces.current().resetInputs("datasetForm:embargoInputs"); } - public boolean isCantDownloadDueToEmbargo() { + public boolean isCantDownloadDueToEmbargoOrDVAccess() { if (getSelectedNonDownloadableFiles() != null) { for (FileMetadata fmd : getSelectedNonDownloadableFiles()) { if (FileUtil.isActivelyEmbargoed(fmd)) { return true; } + if (isVersionHasGlobus()) { + if (StorageIO.isDataverseAccessible( + DataAccess.getStorageDriverFromIdentifier(fmd.getDataFile().getStorageIdentifier()))) { + return true; + } + } } } return false; @@ -6278,6 +6426,195 @@ private boolean containsOnlyActivelyEmbargoedFiles(List selectedFi return true; } + public Retention getSelectionRetention() { + return selectionRetention; + } + + public void setSelectionRetention(Retention selectionRetention) { + this.selectionRetention = selectionRetention; + } + + + private Retention selectionRetention = new Retention(); + + public boolean isValidRetentionSelection() { + //If fileMetadataForAction is set, someone is using the kebab/single file menu + if (fileMetadataForAction != null) { + if (!fileMetadataForAction.getDataFile().isReleased()) { + return true; + } else { + return false; + } + } + //Otherwise we check the selected files + for (FileMetadata fmd : selectedFiles) { + if (!fmd.getDataFile().isReleased()) { + return true; + } + } + return false; + } + + /* + * This method checks to see if the selected file/files have a retention that could be removed. It doesn't return true of a released file has a retention. + */ + public boolean isExistingRetention() { + if (fileMetadataForAction != null) { + if (!fileMetadataForAction.getDataFile().isReleased() + && (fileMetadataForAction.getDataFile().getRetention() != null)) { + return true; + } else { + return false; + } + } + for (FileMetadata fmd : selectedFiles) { + if (!fmd.getDataFile().isReleased() && (fmd.getDataFile().getRetention() != null)) { + return true; + } + } + + return false; + } + + public boolean isRetentionExpired(List fmdList) { + return FileUtil.isRetentionExpired(fmdList); + } + + public boolean isRetentionForWholeSelection() { + for (FileMetadata fmd : selectedFiles) { + if (fmd.getDataFile().isReleased()) { + return false; + } + } + return true; + } + + private boolean removeRetention=false; + + public boolean isRemoveRetention() { + return removeRetention; + } + + public void setRemoveRetention(boolean removeRetention) { + boolean existing = this.removeRetention; + this.removeRetention = removeRetention; + //If we flipped the state, update the selectedRetention. Otherwise (e.g. when save is hit) don't make changes + if(existing != this.removeRetention) { + logger.fine("State flip"); + selectionRetention= new Retention(); + if(removeRetention) { + logger.fine("Setting empty retention"); + selectionRetention= new Retention(null, null); + } + PrimeFaces.current().resetInputs("datasetForm:retentionInputs"); + } + } + + public String saveRetention() { + if (workingVersion.isReleased()) { + refreshSelectedFiles(selectedFiles); + } + + if(isRemoveRetention() || (selectionRetention.getDateUnavailable()==null && selectionRetention.getReason()==null)) { + selectionRetention=null; + } + + if(!(selectionRetention==null || (selectionRetention!=null && settingsWrapper.isValidRetentionDate(selectionRetention)))) { + logger.fine("Validation error: " + selectionRetention.getFormattedDateUnavailable()); + FacesContext.getCurrentInstance().validationFailed(); + return ""; + } + List orphanedRetentions = new ArrayList(); + List retentionFMs = null; + if (fileMetadataForAction != null) { + retentionFMs = new ArrayList(); + retentionFMs.add(fileMetadataForAction); + } else if (selectedFiles != null && selectedFiles.size() > 0) { + retentionFMs = selectedFiles; + } + + if(retentionFMs!=null && !retentionFMs.isEmpty()) { + if(selectionRetention!=null) { + selectionRetention = retentionService.merge(selectionRetention); + } + for (FileMetadata fmd : workingVersion.getFileMetadatas()) { + for (FileMetadata fm : retentionFMs) { + if (fm.getDataFile().equals(fmd.getDataFile()) && (isSuperUser()||!fmd.getDataFile().isReleased())) { + Retention ret = fmd.getDataFile().getRetention(); + if (ret != null) { + logger.fine("Before: " + ret.getDataFiles().size()); + ret.getDataFiles().remove(fmd.getDataFile()); + if (ret.getDataFiles().isEmpty()) { + orphanedRetentions.add(ret); + } + logger.fine("After: " + ret.getDataFiles().size()); + } + fmd.getDataFile().setRetention(selectionRetention); + } + } + } + } + if (selectionRetention != null) { + retentionService.save(selectionRetention, ((AuthenticatedUser) session.getUser()).getIdentifier()); + } + // success message: + String successMessage = BundleUtil.getStringFromBundle("file.assignedRetention.success"); + logger.fine(successMessage); + successMessage = successMessage.replace("{0}", "Selected Files"); + JsfHelper.addFlashMessage(successMessage); + selectionRetention = new Retention(); + + save(); + for(Retention ret: orphanedRetentions) { + retentionService.delete(ret, ((AuthenticatedUser)session.getUser()).getUserIdentifier()); + } + return returnToDraftVersion(); + } + + public void clearRetentionPopup() { + logger.fine("clearRetentionPopup called"); + selectionRetention= new Retention(); + setRemoveRetention(false); + PrimeFaces.current().resetInputs("datasetForm:retentionInputs"); + } + + public void clearSelectionRetention() { + logger.fine("clearSelectionRetention called"); + selectionRetention= new Retention(); + PrimeFaces.current().resetInputs("datasetForm:retentionInputs"); + } + + public boolean isCantDownloadDueToRetention() { + if (getSelectedNonDownloadableFiles() != null) { + for (FileMetadata fmd : getSelectedNonDownloadableFiles()) { + if (FileUtil.isRetentionExpired(fmd)) { + return true; + } + } + } + return false; + } + + public boolean isCantRequestDueToRetention() { + if (fileDownloadHelper.getFilesForRequestAccess() != null) { + for (DataFile df : fileDownloadHelper.getFilesForRequestAccess()) { + if (FileUtil.isRetentionExpired(df)) { + return true; + } + } + } + return false; + } + + private boolean containsOnlyRetentionExpiredFiles(List selectedFiles) { + for (FileMetadata fmd : selectedFiles) { + if (!FileUtil.isRetentionExpired(fmd)) { + return false; + } + } + return true; + } + public String getIngestMessage() { return BundleUtil.getStringFromBundle("file.ingestFailed.message", Arrays.asList(settingsWrapper.getGuidesBaseUrl(), settingsWrapper.getGuidesVersion())); } @@ -6344,7 +6681,8 @@ public void startGlobusTransfer(boolean transferAll, boolean popupShown) { } boolean guestbookRequired = isDownloadPopupRequired(); - boolean validated = validateFilesForDownload(true); + boolean validated = validateFilesForDownload(true, true); + if (validated) { globusTransferRequested = true; boolean mixed = "Mixed".equals(getValidateFilesOutcome()); @@ -6395,5 +6733,9 @@ public String getSignpostingLinkHeader() { } return signpostingLinkHeader; } + + public boolean isDOI() { + return AbstractDOIProvider.DOI_PROTOCOL.equals(dataset.getGlobalId().getProtocol()); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetRelPublication.java b/src/main/java/edu/harvard/iq/dataverse/DatasetRelPublication.java index 7680ebc16db..a0696ab38d9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetRelPublication.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetRelPublication.java @@ -6,7 +6,6 @@ package edu.harvard.iq.dataverse; - /** * * @author skraffmiller @@ -25,10 +24,12 @@ public class DatasetRelPublication { private String description; private boolean replicationData; private int displayOrder; + private String relationType; public int getDisplayOrder() { return displayOrder; } + public void setDisplayOrder(int displayOrder) { this.displayOrder = displayOrder; } @@ -64,8 +65,7 @@ public String getUrl() { public void setUrl(String url) { this.url = url; } - - + public String getTitle() { return title; } @@ -82,12 +82,21 @@ public void setDescription(String description) { this.description = description; } - public boolean isEmpty() { - return ((text==null || text.trim().equals("")) - && (!replicationData) - && (idType==null || idType.trim().equals("")) - && (idNumber==null || idNumber.trim().equals("")) - && (url==null || url.trim().equals(""))); - } + public void setRelationType(String type) { + relationType = type; + + } + + public String getRelationType() { + return relationType; + } + + public boolean isEmpty() { + return ((text == null || text.trim().equals("")) + && (!replicationData) + && (idType == null || idType.trim().equals("")) + && (idNumber == null || idNumber.trim().equals("")) + && (url == null || url.trim().equals(""))); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index c6df2a2e1ab..e519614ba55 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -39,11 +39,10 @@ import jakarta.ejb.TransactionAttributeType; import jakarta.inject.Named; import jakarta.persistence.EntityManager; -import jakarta.persistence.LockModeType; import jakarta.persistence.NoResultException; +import jakarta.persistence.NonUniqueResultException; import jakarta.persistence.PersistenceContext; import jakarta.persistence.Query; -import jakarta.persistence.StoredProcedureQuery; import jakarta.persistence.TypedQuery; import org.apache.commons.lang3.StringUtils; @@ -61,9 +60,6 @@ public class DatasetServiceBean implements java.io.Serializable { @EJB IndexServiceBean indexService; - @EJB - DOIEZIdServiceBean doiEZIdServiceBean; - @EJB SettingsServiceBean settingsService; @@ -116,26 +112,32 @@ public Dataset find(Object pk) { * @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(); + try { + 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.fileMetadatas.varGroups") + //.setHint("eclipselink.left-join-fetch", "o.files.guestbookResponses + .setHint("eclipselink.left-join-fetch", "o.files.embargo") + .setHint("eclipselink.left-join-fetch", "o.files.retention") + .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(); + } catch (NoResultException | NonUniqueResultException ex) { + return null; + } } public List findByOwnerId(Long ownerId) { @@ -410,12 +412,20 @@ public boolean checkDatasetLock(Long datasetId) { List lock = lockCounter.getResultList(); return lock.size()>0; } - + + public List getLocksByDatasetId(Long datasetId) { + TypedQuery locksQuery = em.createNamedQuery("DatasetLock.getLocksByDatasetId", DatasetLock.class); + locksQuery.setParameter("datasetId", datasetId); + return locksQuery.getResultList(); + } + public List getDatasetLocksByUser( AuthenticatedUser user) { return listLocks(null, user); } + // @todo: we'll be better off getting rid of this method and using the other + // version of addDatasetLock() (that uses datasetId instead of Dataset). @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) public DatasetLock addDatasetLock(Dataset dataset, DatasetLock lock) { lock.setDataset(dataset); @@ -465,6 +475,7 @@ public DatasetLock addDatasetLock(Long datasetId, DatasetLock.Reason reason, Lon * is {@code aReason}. * @param dataset the dataset whose locks (for {@code aReason}) will be removed. * @param aReason The reason of the locks that will be removed. + * @todo this should probably take dataset_id, not a dataset */ @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) public void removeDatasetLocks(Dataset dataset, DatasetLock.Reason aReason) { @@ -700,7 +711,7 @@ public void exportAllDatasets(boolean forceReExport) { Integer countError = 0; String logTimestamp = logFormatter.format(new Date()); Logger exportLogger = Logger.getLogger("edu.harvard.iq.dataverse.harvest.client.DatasetServiceBean." + "ExportAll" + logTimestamp); - String logFileName = "../logs" + File.separator + "export_" + logTimestamp + ".log"; + String logFileName = System.getProperty("com.sun.aas.instanceRoot") + File.separator + "logs" + File.separator + "export_" + logTimestamp + ".log"; FileHandler fileHandler; boolean fileHandlerSuceeded; try { @@ -849,6 +860,7 @@ public Dataset setNonDatasetFileAsThumbnail(Dataset dataset, InputStream inputSt } dataset = DatasetUtil.persistDatasetLogoToStorageAndCreateThumbnails(dataset, inputStream); dataset.setThumbnailFile(null); + dataset.setUseGenericThumbnail(false); return merge(dataset); } @@ -861,18 +873,33 @@ public Dataset setDatasetFileAsThumbnail(Dataset dataset, DataFile datasetFileTh logger.fine("In setDatasetFileAsThumbnail but dataset is null! Returning null."); return null; } + // Just in case the previously designated thumbnail for the dataset was + // a "custom" kind, i.e. an uploaded "dataset_logo" file, the following method + // will try to delete it, and all the associated caches here (because there + // are no other uses for the file). This method is apparently called in all + // cases, without trying to check if the dataset was in fact using a custom + // logo; probably under the assumption that it can't hurt. DatasetUtil.deleteDatasetLogo(dataset); dataset.setThumbnailFile(datasetFileThumbnailToSwitchTo); dataset.setUseGenericThumbnail(false); return merge(dataset); } - public Dataset removeDatasetThumbnail(Dataset dataset) { + public Dataset clearDatasetLevelThumbnail(Dataset dataset) { if (dataset == null) { - logger.fine("In removeDatasetThumbnail but dataset is null! Returning null."); + logger.fine("In clearDatasetLevelThumbnail but dataset is null! Returning null."); return null; } + + // Just in case the thumbnail that was designated for the dataset was + // a "custom logo" kind, i.e. an uploaded "dataset_logo" file, the following method + // will try to delete it, and all the associated caches here (because there + // are no other uses for the file). This method is apparently called in all + // cases, without trying to check if the dataset was in fact using a custom + // logo; probably under the assumption that it can't hurt. DatasetUtil.deleteDatasetLogo(dataset); + + // Clear any designated thumbnails for the dataset: dataset.setThumbnailFile(null); dataset.setUseGenericThumbnail(true); return merge(dataset); @@ -929,7 +956,7 @@ public void callFinalizePublishCommandAsynchronously(Long datasetId, CommandCont try { Thread.sleep(1000); } catch (Exception ex) { - logger.warning("Failed to sleep for a second."); + logger.warning("Failed to sleep for one second."); } logger.fine("Running FinalizeDatasetPublicationCommand, asynchronously"); Dataset theDataset = find(datasetId); @@ -940,80 +967,6 @@ public void callFinalizePublishCommandAsynchronously(Long datasetId, CommandCont } } - /* - Experimental asynchronous method for requesting persistent identifiers for - datafiles. We decided not to run this method on upload/create (so files - will not have persistent ids while in draft; when the draft is published, - we will force obtaining persistent ids for all the files in the version. - - If we go back to trying to register global ids on create, care will need to - be taken to make sure the asynchronous changes below are not conflicting with - the changes from file ingest (which may be happening in parallel, also - asynchronously). We would also need to lock the dataset (similarly to how - tabular ingest logs the dataset), to prevent the user from publishing the - version before all the identifiers get assigned - otherwise more conflicts - are likely. (It sounds like it would make sense to treat these two tasks - - persistent identifiers for files and ingest - as one post-upload job, so that - they can be run in sequence). -- L.A. Mar. 2018 - */ - @Asynchronous - public void obtainPersistentIdentifiersForDatafiles(Dataset dataset) { - GlobalIdServiceBean idServiceBean = GlobalIdServiceBean.getBean(dataset.getProtocol(), commandEngine.getContext()); - - //If the Id type is sequential and Dependent then write file idenitifiers outside the command - String datasetIdentifier = dataset.getIdentifier(); - Long maxIdentifier = null; - - if (systemConfig.isDataFilePIDSequentialDependent()) { - maxIdentifier = getMaximumExistingDatafileIdentifier(dataset); - } - - for (DataFile datafile : dataset.getFiles()) { - logger.info("Obtaining persistent id for datafile id=" + datafile.getId()); - - if (datafile.getIdentifier() == null || datafile.getIdentifier().isEmpty()) { - - logger.info("Obtaining persistent id for datafile id=" + datafile.getId()); - - if (maxIdentifier != null) { - maxIdentifier++; - datafile.setIdentifier(datasetIdentifier + "/" + maxIdentifier.toString()); - } else { - datafile.setIdentifier(idServiceBean.generateDataFileIdentifier(datafile)); - } - - if (datafile.getProtocol() == null) { - datafile.setProtocol(settingsService.getValueForKey(SettingsServiceBean.Key.Protocol, "")); - } - if (datafile.getAuthority() == null) { - datafile.setAuthority(settingsService.getValueForKey(SettingsServiceBean.Key.Authority, "")); - } - - logger.info("identifier: " + datafile.getIdentifier()); - - String doiRetString; - - try { - logger.log(Level.FINE, "creating identifier"); - doiRetString = idServiceBean.createIdentifier(datafile); - } catch (Throwable e) { - logger.log(Level.WARNING, "Exception while creating Identifier: " + e.getMessage(), e); - doiRetString = ""; - } - - // Check return value to make sure registration succeeded - if (!idServiceBean.registerWhenPublished() && doiRetString.contains(datafile.getIdentifier())) { - datafile.setIdentifierRegistered(true); - datafile.setGlobalIdCreateTime(new Date()); - } - - DataFile merged = em.merge(datafile); - merged = null; - } - - } - } - public long findStorageSize(Dataset dataset) throws IOException { return findStorageSize(dataset, false, GetDatasetStorageSizeCommand.Mode.STORAGE, null); } @@ -1127,4 +1080,16 @@ public void deleteHarvestedDataset(Dataset dataset, DataverseRequest request, Lo } } + public List getVersionStates(long id) { + try { + Query query = em.createNativeQuery("SELECT dv.versionState FROM datasetversion dv WHERE dataset_id=? ORDER BY id"); + query.setParameter(1, id); + return (List) query.getResultList(); + + } catch (Exception ex) { + logger.log(Level.WARNING, "exception trying to get versionstates of dataset " + id + ": {0}", ex); + return null; + } + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java b/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java index 5fd963f3931..0433c425fd2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java @@ -1342,7 +1342,7 @@ public List getGeographicCoverage() { } geoCoverages.add(coverageItem); } - + break; } } return geoCoverages; @@ -1356,24 +1356,42 @@ public List getRelatedPublications() { for (DatasetFieldCompoundValue publication : dsf.getDatasetFieldCompoundValues()) { DatasetRelPublication relatedPublication = new DatasetRelPublication(); for (DatasetField subField : publication.getChildDatasetFields()) { - if (subField.getDatasetFieldType().getName().equals(DatasetFieldConstant.publicationCitation)) { - String citation = subField.getDisplayValue(); - relatedPublication.setText(citation); - } - - - if (subField.getDatasetFieldType().getName().equals(DatasetFieldConstant.publicationURL)) { - // We have to avoid using subField.getDisplayValue() here - because the DisplayFormatType - // for this url metadata field is likely set up so that the display value is automatically - // turned into a clickable HTML HREF block, which we don't want to end in our Schema.org JSON-LD output. - // So we want to use the raw value of the field instead, with - // minimal HTML sanitation, just in case (this would be done on all URLs in getDisplayValue()). - String url = subField.getValue(); - if (StringUtils.isBlank(url) || DatasetField.NA_VALUE.equals(url)) { - relatedPublication.setUrl(""); - } else { - relatedPublication.setUrl(MarkupChecker.sanitizeBasicHTML(url)); - } + switch (subField.getDatasetFieldType().getName()) { + case DatasetFieldConstant.publicationCitation: + relatedPublication.setText(subField.getDisplayValue()); + break; + case DatasetFieldConstant.publicationURL: + // We have to avoid using subField.getDisplayValue() here - because the + // DisplayFormatType + // for this url metadata field is likely set up so that the display value is + // automatically + // turned into a clickable HTML HREF block, which we don't want to end in our + // Schema.org + // JSON-LD output. So we want to use the raw value of the field instead, with + // minimal HTML + // sanitation, just in case (this would be done on all URLs in + // getDisplayValue()). + String url = subField.getValue(); + if (StringUtils.isBlank(url) || DatasetField.NA_VALUE.equals(url)) { + relatedPublication.setUrl(""); + } else { + relatedPublication.setUrl(MarkupChecker.sanitizeBasicHTML(url)); + } + break; + case DatasetFieldConstant.publicationIDType: + // QDR idType has a trailing : now (Aug 2021) + // Get value without any display modifications + subField.getDatasetFieldType().setDisplayFormat("#VALUE"); + relatedPublication.setIdType(subField.getDisplayValue()); + break; + case DatasetFieldConstant.publicationIDNumber: + // Get sanitized value without any display modifications + subField.getDatasetFieldType().setDisplayFormat("#VALUE"); + relatedPublication.setIdNumber(subField.getDisplayValue()); + break; + case DatasetFieldConstant.publicationRelationType: + relatedPublication.setRelationType(subField.getDisplayValue()); + break; } } relatedPublications.add(relatedPublication); @@ -1728,7 +1746,36 @@ public List> validateRequired() { } public boolean isValid() { - return validate().isEmpty(); + // first clone to leave the original untouched + final DatasetVersion newVersion = this.cloneDatasetVersion(); + // initDatasetFields + newVersion.setDatasetFields(newVersion.initDatasetFields()); + // remove special "N/A" values and empty values + newVersion.removeEmptyValues(); + // check validity of present fields and detect missing mandatory fields + return newVersion.validate().isEmpty(); + } + + private void removeEmptyValues() { + if (this.getDatasetFields() != null) { + for (DatasetField dsf : this.getDatasetFields()) { + removeEmptyValues(dsf); + } + } + } + + private void removeEmptyValues(DatasetField dsf) { + if (dsf.getDatasetFieldType().isPrimitive()) { // primitive + final Iterator i = dsf.getDatasetFieldValues().iterator(); + while (i.hasNext()) { + final String v = i.next().getValue(); + if (StringUtils.isBlank(v) || DatasetField.NA_VALUE.equals(v)) { + i.remove(); + } + } + } else { + dsf.getDatasetFieldCompoundValues().forEach(cv -> cv.getChildDatasetFields().forEach(v -> removeEmptyValues(v))); + } } public Set validate() { diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetVersionFilesServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetVersionFilesServiceBean.java index 99c3c65e3b8..afcfafe976c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetVersionFilesServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetVersionFilesServiceBean.java @@ -42,6 +42,16 @@ public enum FileDownloadSizeMode { All, Original, Archival } + /** + * Given a DatasetVersion, returns its total file metadata count + * + * @param datasetVersion the DatasetVersion to access + * @return long value of total file metadata count + */ + public long getFileMetadataCount(DatasetVersion datasetVersion) { + return getFileMetadataCount(datasetVersion, new FileSearchCriteria(null, null, null, null, null)); + } + /** * Given a DatasetVersion, returns its total file metadata count * @@ -189,6 +199,32 @@ public long getFilesDownloadSize(DatasetVersion datasetVersion, FileSearchCriter }; } + /** + * Determines whether or not a DataFile is present in a DatasetVersion + * + * @param datasetVersion the DatasetVersion to check + * @param dataFile the DataFile to check + * @return boolean value + */ + public boolean isDataFilePresentInDatasetVersion(DatasetVersion datasetVersion, DataFile dataFile) { + CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder(); + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Long.class); + Root dataFileRoot = criteriaQuery.from(DataFile.class); + Root fileMetadataRoot = criteriaQuery.from(FileMetadata.class); + Root datasetVersionRoot = criteriaQuery.from(DatasetVersion.class); + criteriaQuery + .select(criteriaBuilder.count(dataFileRoot)) + .where(criteriaBuilder.and( + criteriaBuilder.equal(dataFileRoot.get("id"), dataFile.getId()), + criteriaBuilder.equal(datasetVersionRoot.get("id"), datasetVersion.getId()), + fileMetadataRoot.in(dataFileRoot.get("fileMetadatas")), + fileMetadataRoot.in(datasetVersionRoot.get("fileMetadatas")) + ) + ); + Long count = em.createQuery(criteriaQuery).getSingleResult(); + return count != null && count > 0; + } + private void addAccessStatusCountToTotal(DatasetVersion datasetVersion, Map totalCounts, FileAccessStatus dataFileAccessStatus, FileSearchCriteria searchCriteria) { long fileMetadataCount = getFileMetadataCountByAccessStatus(datasetVersion, dataFileAccessStatus, searchCriteria); if (fileMetadataCount > 0) { @@ -210,6 +246,8 @@ private long getFileMetadataCountByAccessStatus(DatasetVersion datasetVersion, F private Predicate createSearchCriteriaAccessStatusPredicate(FileAccessStatus accessStatus, CriteriaBuilder criteriaBuilder, Root fileMetadataRoot) { Path dataFile = fileMetadataRoot.get("dataFile"); + Path retention = dataFile.get("retention"); + Predicate retentionExpiredPredicate = criteriaBuilder.lessThan(retention.get("dateUnavailable"), criteriaBuilder.currentDate()); Path embargo = dataFile.get("embargo"); Predicate activelyEmbargoedPredicate = criteriaBuilder.greaterThanOrEqualTo(embargo.get("dateAvailable"), criteriaBuilder.currentDate()); Predicate inactivelyEmbargoedPredicate = criteriaBuilder.isNull(embargo); @@ -217,6 +255,7 @@ private Predicate createSearchCriteriaAccessStatusPredicate(FileAccessStatus acc Predicate isRestrictedPredicate = criteriaBuilder.isTrue(isRestricted); Predicate isUnrestrictedPredicate = criteriaBuilder.isFalse(isRestricted); return switch (accessStatus) { + case RetentionPeriodExpired -> criteriaBuilder.and(retentionExpiredPredicate); case EmbargoedThenRestricted -> criteriaBuilder.and(activelyEmbargoedPredicate, isRestrictedPredicate); case EmbargoedThenPublic -> criteriaBuilder.and(activelyEmbargoedPredicate, isUnrestrictedPredicate); case Restricted -> criteriaBuilder.and(inactivelyEmbargoedPredicate, isRestrictedPredicate); diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetVersionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetVersionServiceBean.java index 1ee517c9831..762319884b9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetVersionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetVersionServiceBean.java @@ -9,6 +9,7 @@ import static edu.harvard.iq.dataverse.batch.jobs.importer.filesystem.FileRecordJobListener.SEP; import edu.harvard.iq.dataverse.batch.util.LoggingUtil; import edu.harvard.iq.dataverse.search.SolrSearchResult; +import edu.harvard.iq.dataverse.settings.FeatureFlags; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.MarkupChecker; @@ -163,6 +164,7 @@ public DatasetVersion findDeep(Object pk) { .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.dataFile.retention") .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") @@ -314,6 +316,23 @@ private void msg(String s){ //logger.fine(s); } + public boolean isVersionDefaultCustomTerms(DatasetVersion datasetVersion) { + //SEK - belt and suspenders here, but this is where the bug 10719 first manifested + if (datasetVersion != null && datasetVersion.getId() != null) { + try { + TermsOfUseAndAccess toua = (TermsOfUseAndAccess) em.createNamedQuery("TermsOfUseAndAccess.findByDatasetVersionIdAndDefaultTerms") + .setParameter("id", datasetVersion.getId()).setParameter("defaultTerms", TermsOfUseAndAccess.DEFAULT_NOTERMS).getSingleResult(); + if (toua != null && datasetVersion.getTermsOfUseAndAccess().getLicense() == null) { + return true; + } + + } catch (NoResultException e) { + return false; + } + } + return false; + } + /** * Does the version identifier in the URL ask for a "DRAFT"? * @@ -789,35 +808,11 @@ public Long getThumbnailByVersionId(Long versionId) { return null; } - Long thumbnailFileId; - - // First, let's see if there are thumbnails that have already been - // generated: - try { - thumbnailFileId = (Long) em.createNativeQuery("SELECT df.id " - + "FROM datafile df, filemetadata fm, datasetversion dv, dvobject o " - + "WHERE dv.id = " + versionId + " " - + "AND df.id = o.id " - + "AND fm.datasetversion_id = dv.id " - + "AND fm.datafile_id = df.id " - + "AND df.restricted = false " - + "AND df.embargo_id is null " - + "AND o.previewImageAvailable = true " - + "ORDER BY df.id LIMIT 1;").getSingleResult(); - } catch (Exception ex) { - thumbnailFileId = null; - } - - if (thumbnailFileId != null) { - logger.fine("DatasetVersionService,getThumbnailByVersionid(): found already generated thumbnail for version " + versionId + ": " + thumbnailFileId); - assignDatasetThumbnailByNativeQuery(versionId, thumbnailFileId); - return thumbnailFileId; - } - - if (!systemConfig.isThumbnailGenerationDisabledForImages()) { - // OK, let's try and generate an image thumbnail! - long imageThumbnailSizeLimit = systemConfig.getThumbnailSizeLimitImage(); + if (!FeatureFlags.DISABLE_DATASET_THUMBNAIL_AUTOSELECT.enabled()) { + Long thumbnailFileId; + // First, let's see if there are thumbnails that have already been + // generated: try { thumbnailFileId = (Long) em.createNativeQuery("SELECT df.id " + "FROM datafile df, filemetadata fm, datasetversion dv, dvobject o " @@ -825,61 +820,89 @@ public Long getThumbnailByVersionId(Long versionId) { + "AND df.id = o.id " + "AND fm.datasetversion_id = dv.id " + "AND fm.datafile_id = df.id " - + "AND o.previewimagefail = false " + "AND df.restricted = false " + "AND df.embargo_id is null " - + "AND df.contenttype LIKE 'image/%' " - + "AND NOT df.contenttype = 'image/fits' " - + "AND df.filesize < " + imageThumbnailSizeLimit + " " - + "ORDER BY df.filesize ASC LIMIT 1;").getSingleResult(); + + "AND df.retention_id is null " + + "AND o.previewImageAvailable = true " + + "ORDER BY df.id LIMIT 1;").getSingleResult(); } catch (Exception ex) { thumbnailFileId = null; } if (thumbnailFileId != null) { - logger.fine("obtained file id: " + thumbnailFileId); - DataFile thumbnailFile = datafileService.find(thumbnailFileId); - if (thumbnailFile != null) { - if (datafileService.isThumbnailAvailable(thumbnailFile)) { - assignDatasetThumbnailByNativeQuery(versionId, thumbnailFileId); - return thumbnailFileId; + logger.fine("DatasetVersionService,getThumbnailByVersionid(): found already generated thumbnail for version " + versionId + ": " + thumbnailFileId); + assignDatasetThumbnailByNativeQuery(versionId, thumbnailFileId); + return thumbnailFileId; + } + + if (!systemConfig.isThumbnailGenerationDisabledForImages()) { + // OK, let's try and generate an image thumbnail! + long imageThumbnailSizeLimit = systemConfig.getThumbnailSizeLimitImage(); + + try { + thumbnailFileId = (Long) em.createNativeQuery("SELECT df.id " + + "FROM datafile df, filemetadata fm, datasetversion dv, dvobject o " + + "WHERE dv.id = " + versionId + " " + + "AND df.id = o.id " + + "AND fm.datasetversion_id = dv.id " + + "AND fm.datafile_id = df.id " + + "AND o.previewimagefail = false " + + "AND df.restricted = false " + + "AND df.embargo_id is null " + + "AND df.retention_id is null " + + "AND df.contenttype LIKE 'image/%' " + + "AND NOT df.contenttype = 'image/fits' " + + "AND df.filesize < " + imageThumbnailSizeLimit + " " + + "ORDER BY df.filesize ASC LIMIT 1;").getSingleResult(); + } catch (Exception ex) { + thumbnailFileId = null; + } + + if (thumbnailFileId != null) { + logger.fine("obtained file id: " + thumbnailFileId); + DataFile thumbnailFile = datafileService.find(thumbnailFileId); + if (thumbnailFile != null) { + if (datafileService.isThumbnailAvailable(thumbnailFile)) { + assignDatasetThumbnailByNativeQuery(versionId, thumbnailFileId); + return thumbnailFileId; + } } } } - } - // And if that didn't work, try the same thing for PDFs: - if (!systemConfig.isThumbnailGenerationDisabledForPDF()) { - // OK, let's try and generate an image thumbnail! - long imageThumbnailSizeLimit = systemConfig.getThumbnailSizeLimitPDF(); - try { - thumbnailFileId = (Long) em.createNativeQuery("SELECT df.id " - + "FROM datafile df, filemetadata fm, datasetversion dv, dvobject o " - + "WHERE dv.id = " + versionId + " " - + "AND df.id = o.id " - + "AND fm.datasetversion_id = dv.id " - + "AND fm.datafile_id = df.id " - + "AND o.previewimagefail = false " - + "AND df.restricted = false " - + "AND df.embargo_id is null " - + "AND df.contenttype = 'application/pdf' " - + "AND df.filesize < " + imageThumbnailSizeLimit + " " - + "ORDER BY df.filesize ASC LIMIT 1;").getSingleResult(); - } catch (Exception ex) { - thumbnailFileId = null; - } + // And if that didn't work, try the same thing for PDFs: + if (!systemConfig.isThumbnailGenerationDisabledForPDF()) { + // OK, let's try and generate an image thumbnail! + long imageThumbnailSizeLimit = systemConfig.getThumbnailSizeLimitPDF(); + try { + thumbnailFileId = (Long) em.createNativeQuery("SELECT df.id " + + "FROM datafile df, filemetadata fm, datasetversion dv, dvobject o " + + "WHERE dv.id = " + versionId + " " + + "AND df.id = o.id " + + "AND fm.datasetversion_id = dv.id " + + "AND fm.datafile_id = df.id " + + "AND o.previewimagefail = false " + + "AND df.restricted = false " + + "AND df.embargo_id is null " + + "AND df.retention_id is null " + + "AND df.contenttype = 'application/pdf' " + + "AND df.filesize < " + imageThumbnailSizeLimit + " " + + "ORDER BY df.filesize ASC LIMIT 1;").getSingleResult(); + } catch (Exception ex) { + thumbnailFileId = null; + } - if (thumbnailFileId != null) { - DataFile thumbnailFile = datafileService.find(thumbnailFileId); - if (thumbnailFile != null) { - if (datafileService.isThumbnailAvailable(thumbnailFile)) { - assignDatasetThumbnailByNativeQuery(versionId, thumbnailFileId); - return thumbnailFileId; + if (thumbnailFileId != null) { + DataFile thumbnailFile = datafileService.find(thumbnailFileId); + if (thumbnailFile != null) { + if (datafileService.isThumbnailAvailable(thumbnailFile)) { + assignDatasetThumbnailByNativeQuery(versionId, thumbnailFileId); + return thumbnailFileId; + } } } } } - return null; } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetVersionUI.java b/src/main/java/edu/harvard/iq/dataverse/DatasetVersionUI.java index 55b98c178bb..f1ddf2304b7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetVersionUI.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetVersionUI.java @@ -16,6 +16,8 @@ import java.util.List; import java.util.TreeMap; +import org.apache.commons.lang3.StringUtils; + import jakarta.ejb.EJB; import jakarta.faces.view.ViewScoped; import jakarta.inject.Inject; @@ -62,14 +64,14 @@ public void setMetadataBlocksForEdit(TreeMap> public DatasetVersionUI initDatasetVersionUI(DatasetVersion datasetVersion, boolean createBlanks) { /*takes in the values of a dataset version and apportions them into lists for - viewing and editng in the dataset page. + viewing and editing in the dataset page. */ setDatasetVersion(datasetVersion); //this.setDatasetAuthors(new ArrayList()); this.setDatasetRelPublications(new ArrayList<>()); - // loop through vaues to get fields for view mode + // loop through values to get fields for view mode for (DatasetField dsf : datasetVersion.getDatasetFields()) { //Special Handling for various fields displayed above tabs in dataset page view. if (dsf.getDatasetFieldType().getName().equals(DatasetFieldConstant.title)) { @@ -114,17 +116,23 @@ public DatasetVersionUI initDatasetVersionUI(DatasetVersion datasetVersion, boo datasetRelPublication.setTitle(dsf.getDatasetFieldType().getLocaleTitle()); datasetRelPublication.setDescription(dsf.getDatasetFieldType().getLocaleDescription()); for (DatasetField subField : relPubVal.getChildDatasetFields()) { - if (subField.getDatasetFieldType().getName().equals(DatasetFieldConstant.publicationCitation)) { - datasetRelPublication.setText(subField.getValue()); - } - if (subField.getDatasetFieldType().getName().equals(DatasetFieldConstant.publicationIDNumber)) { - datasetRelPublication.setIdNumber(subField.getValue()); - } - if (subField.getDatasetFieldType().getName().equals(DatasetFieldConstant.publicationIDType)) { - datasetRelPublication.setIdType(subField.getValue()); - } - if (subField.getDatasetFieldType().getName().equals(DatasetFieldConstant.publicationURL)) { - datasetRelPublication.setUrl(subField.getValue()); + String value = subField.getValue(); + switch (subField.getDatasetFieldType().getName()) { + case DatasetFieldConstant.publicationCitation: + datasetRelPublication.setText(subField.getValue()); + break; + case DatasetFieldConstant.publicationIDNumber: + datasetRelPublication.setIdNumber(subField.getValue()); + break; + case DatasetFieldConstant.publicationIDType: + datasetRelPublication.setIdType(subField.getValue()); + break; + case DatasetFieldConstant.publicationURL: + datasetRelPublication.setUrl(subField.getValue()); + break; + case DatasetFieldConstant.publicationRelationType: + datasetRelPublication.setRelationType(subField.getValue()); + break; } } this.getDatasetRelPublications().add(datasetRelPublication); @@ -263,6 +271,18 @@ public String getRelPublicationUrl() { } } + public String getRelPublicationRelationType() { + if (!this.datasetRelPublications.isEmpty()) { + //Add ': ' formatting if relationType exists + String relationType = this.getDatasetRelPublications().get(0).getRelationType(); + if (!StringUtils.isBlank(relationType)) { + return relationType + ": "; + } + } + return ""; + + } + public String getUNF() { //todo get UNF to calculate and display here. return ""; diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java index c1de9d63410..86e2e0207c1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java @@ -30,7 +30,6 @@ import jakarta.persistence.OneToOne; import jakarta.persistence.OrderBy; import jakarta.persistence.Table; -import jakarta.persistence.Transient; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; @@ -411,6 +410,20 @@ public List getDataverseFieldTypeInputLevels() { return dataverseFieldTypeInputLevels; } + public boolean isDatasetFieldTypeRequiredAsInputLevel(Long datasetFieldTypeId) { + return dataverseFieldTypeInputLevels.stream() + .anyMatch(inputLevel -> inputLevel.getDatasetFieldType().getId().equals(datasetFieldTypeId) && inputLevel.isRequired()); + } + + public boolean isDatasetFieldTypeIncludedAsInputLevel(Long datasetFieldTypeId) { + return dataverseFieldTypeInputLevels.stream() + .anyMatch(inputLevel -> inputLevel.getDatasetFieldType().getId().equals(datasetFieldTypeId) && inputLevel.isInclude()); + } + + public boolean isDatasetFieldTypeInInputLevels(Long datasetFieldTypeId) { + return dataverseFieldTypeInputLevels.stream() + .anyMatch(inputLevel -> inputLevel.getDatasetFieldType().getId().equals(datasetFieldTypeId)); + } public Template getDefaultTemplate() { return defaultTemplate; @@ -466,9 +479,6 @@ public void setTemplateRoot(boolean templateRoot) { this.templateRoot = templateRoot; } - - - public List getMetadataBlocks() { return getMetadataBlocks(false); } @@ -582,7 +592,7 @@ public void setDataverseTheme(DataverseTheme dataverseTheme) { } public void setMetadataBlocks(List metadataBlocks) { - this.metadataBlocks = metadataBlocks; + this.metadataBlocks = new ArrayList<>(metadataBlocks); } public List getCitationDatasetFieldTypes() { @@ -823,4 +833,17 @@ public boolean isAncestorOf( DvObject other ) { public String getLocalURL() { return SystemConfig.getDataverseSiteUrlStatic() + "/dataverse/" + this.getAlias(); } + + public void addInputLevelsMetadataBlocksIfNotPresent(List inputLevels) { + for (DataverseFieldTypeInputLevel inputLevel : inputLevels) { + MetadataBlock inputLevelMetadataBlock = inputLevel.getDatasetFieldType().getMetadataBlock(); + if (!hasMetadataBlock(inputLevelMetadataBlock)) { + metadataBlocks.add(inputLevelMetadataBlock); + } + } + } + + private boolean hasMetadataBlock(MetadataBlock metadataBlock) { + return metadataBlocks.stream().anyMatch(block -> block.getId().equals(metadataBlock.getId())); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java index 943a74327d5..351d304bad3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java @@ -9,12 +9,16 @@ import edu.harvard.iq.dataverse.dataverse.DataverseUtil; import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import edu.harvard.iq.dataverse.engine.command.impl.CheckRateLimitForCollectionPageCommand; import edu.harvard.iq.dataverse.engine.command.impl.CreateDataverseCommand; import edu.harvard.iq.dataverse.engine.command.impl.CreateSavedSearchCommand; import edu.harvard.iq.dataverse.engine.command.impl.DeleteDataverseCommand; import edu.harvard.iq.dataverse.engine.command.impl.LinkDataverseCommand; import edu.harvard.iq.dataverse.engine.command.impl.PublishDataverseCommand; import edu.harvard.iq.dataverse.engine.command.impl.UpdateDataverseCommand; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidProviderFactoryBean; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.search.FacetCategory; import edu.harvard.iq.dataverse.search.IndexServiceBean; import edu.harvard.iq.dataverse.search.SearchFields; @@ -28,15 +32,20 @@ import static edu.harvard.iq.dataverse.util.JsfHelper.JH; import edu.harvard.iq.dataverse.util.SystemConfig; import java.util.List; + +import edu.harvard.iq.dataverse.util.cache.CacheFactoryBean; import jakarta.ejb.EJB; import jakarta.faces.application.FacesMessage; import jakarta.faces.context.FacesContext; import jakarta.faces.view.ViewScoped; import jakarta.inject.Inject; import jakarta.inject.Named; + +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -109,7 +118,13 @@ public enum LinkMode { @EJB DataverseLinkingServiceBean linkingService; @Inject PermissionsWrapper permissionsWrapper; - @Inject DataverseHeaderFragment dataverseHeaderFragment; + @Inject + NavigationWrapper navigationWrapper; + @Inject DataverseHeaderFragment dataverseHeaderFragment; + @EJB + PidProviderFactoryBean pidProviderFactoryBean; + @EJB + CacheFactoryBean cacheFactory; private Dataverse dataverse = new Dataverse(); @@ -310,7 +325,10 @@ public void updateOwnerDataverse() { public String init() { //System.out.println("_YE_OLDE_QUERY_COUNTER_"); // for debug purposes - + // Check for rate limit exceeded. Must be done before anything else to prevent unnecessary processing. + if (!cacheFactory.checkRate(session.getUser(), new CheckRateLimitForCollectionPageCommand(null,null))) { + return navigationWrapper.tooManyRequests(); + } if (this.getAlias() != null || this.getId() != null || this.getOwnerId() == null) {// view mode for a dataverse if (this.getAlias() != null) { dataverse = dataverseService.findByAlias(this.getAlias()); @@ -362,7 +380,7 @@ public void initFeaturedDataverses() { List featuredSource = new ArrayList<>(); List featuredTarget = new ArrayList<>(); featuredSource.addAll(dataverseService.findAllPublishedByOwnerId(dataverse.getId())); - featuredSource.addAll(linkingService.findLinkingDataverses(dataverse.getId())); + featuredSource.addAll(linkingService.findLinkedDataverses(dataverse.getId())); List featuredList = featuredDataverseService.findByDataverseId(dataverse.getId()); for (DataverseFeaturedDataverse dfd : featuredList) { Dataverse fd = dfd.getFeaturedDataverse(); @@ -1289,4 +1307,34 @@ public String getCurationLabelSetNameLabel() { public Set> getGuestbookEntryOptions() { return settingsWrapper.getGuestbookEntryOptions(this.dataverse).entrySet(); } + + public Set> getPidProviderOptions() { + PidProvider defaultPidProvider = pidProviderFactoryBean.getDefaultPidGenerator(); + Set providerIds = PidUtil.getManagedProviderIds(); + Set> options = new HashSet>(); + if (providerIds.size() > 1) { + + String label = null; + if (this.dataverse.getOwner() != null && this.dataverse.getOwner().getEffectivePidGenerator()!= null) { + PidProvider inheritedPidProvider = this.dataverse.getOwner().getEffectivePidGenerator(); + label = inheritedPidProvider.getLabel() + " " + BundleUtil.getStringFromBundle("dataverse.inherited") + ": " + + inheritedPidProvider.getProtocol() + ":" + inheritedPidProvider.getAuthority() + + inheritedPidProvider.getSeparator() + inheritedPidProvider.getShoulder(); + } else { + label = defaultPidProvider.getLabel() + " " + BundleUtil.getStringFromBundle("dataverse.default") + ": " + + defaultPidProvider.getProtocol() + ":" + defaultPidProvider.getAuthority() + + defaultPidProvider.getSeparator() + defaultPidProvider.getShoulder(); + } + Entry option = new AbstractMap.SimpleEntry("default", label); + options.add(option); + } + for (String providerId : providerIds) { + PidProvider pidProvider = PidUtil.getPidProvider(providerId); + String label = pidProvider.getLabel() + ": " + pidProvider.getProtocol() + ":" + pidProvider.getAuthority() + + pidProvider.getSeparator() + pidProvider.getShoulder(); + Entry option = new AbstractMap.SimpleEntry(providerId, label); + options.add(option); + } + return options; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseServiceBean.java index 10b5d800c21..91b15f77111 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseServiceBean.java @@ -22,7 +22,7 @@ import edu.harvard.iq.dataverse.storageuse.StorageQuota; import edu.harvard.iq.dataverse.util.StringUtil; import edu.harvard.iq.dataverse.util.SystemConfig; -import edu.harvard.iq.dataverse.util.json.JsonUtil; + import java.io.File; import java.io.IOException; import java.sql.Timestamp; @@ -34,6 +34,7 @@ import java.util.logging.Logger; import java.util.Properties; +import edu.harvard.iq.dataverse.validation.JSONDataValidation; import jakarta.ejb.EJB; import jakarta.ejb.Stateless; import jakarta.inject.Inject; @@ -360,7 +361,15 @@ public String getDataverseLogoThumbnailAsBase64ById(Long dvId) { } return null; } - + + public String getDataverseLogoThumbnailAsUrl(Long dvId) { + File dataverseLogoFile = getLogoById(dvId); + if (dataverseLogoFile != null && dataverseLogoFile.exists()) { + return SystemConfig.getDataverseSiteUrlStatic() + "/api/access/dvCardImage/" + dvId; + } + return null; + } + private File getLogo(Dataverse dataverse) { if (dataverse.getId() == null) { return null; @@ -412,7 +421,7 @@ public DataverseTheme findDataverseThemeByIdQuick(Long id) { Object[] result; try { - result = (Object[]) em.createNativeQuery("SELECT logo, logoFormat FROM dataversetheme WHERE dataverse_id = " + id).getSingleResult(); + result = (Object[]) em.createNativeQuery("SELECT logo, logoFormat, logothumbnail FROM dataversetheme WHERE dataverse_id = " + id).getSingleResult(); } catch (Exception ex) { return null; @@ -439,6 +448,10 @@ public DataverseTheme findDataverseThemeByIdQuick(Long id) { break; } } + + if (result[2] != null) { + theme.setLogoThumbnail((String) result[2]); + } return theme; } @@ -503,7 +516,19 @@ public List filterDataversesForLinking(String query, DataverseRequest return dataverseList; } - + public List filterDataversesForUnLinking(String query, DataverseRequest req, Dataset dataset) { + List alreadyLinkeddv_ids = em.createNativeQuery("SELECT linkingdataverse_id FROM datasetlinkingdataverse WHERE dataset_id = " + dataset.getId()).getResultList(); + List dataverseList = new ArrayList<>(); + if (alreadyLinkeddv_ids != null && !alreadyLinkeddv_ids.isEmpty()) { + alreadyLinkeddv_ids.stream().map((testDVId) -> this.find(testDVId)).forEachOrdered((dataverse) -> { + if (this.permissionService.requestOn(req, dataverse).has(Permission.PublishDataset)) { + dataverseList.add(dataverse); + } + }); + } + return dataverseList; + } + public List filterDataversesForHosting(String pattern, DataverseRequest req) { // Find the dataverses matching the search parameters: @@ -888,14 +913,16 @@ public List getDatasetTitlesWithinDataverse(Long dataverseId) { return em.createNativeQuery(cqString).getResultList(); } - public String getCollectionDatasetSchema(String dataverseAlias) { + return getCollectionDatasetSchema(dataverseAlias, null); + } + public String getCollectionDatasetSchema(String dataverseAlias, Map>> schemaChildMap) { Dataverse testDV = this.findByAlias(dataverseAlias); while (!testDV.isMetadataBlockRoot()) { if (testDV.getOwner() == null) { - break; // we are at the root; which by defintion is metadata blcok root, regarldess of the value + break; // we are at the root; which by definition is metadata block root, regardless of the value } testDV = testDV.getOwner(); } @@ -932,6 +959,8 @@ public String getCollectionDatasetSchema(String dataverseAlias) { dsft.setRequiredDV(dsft.isRequired()); dsft.setInclude(true); } + List childrenRequired = new ArrayList<>(); + List childrenAllowed = new ArrayList<>(); if (dsft.isHasChildren()) { for (DatasetFieldType child : dsft.getChildDatasetFieldTypes()) { DataverseFieldTypeInputLevel dsfIlChild = dataverseFieldTypeInputLevelService.findByDataverseIdDatasetFieldTypeId(testDV.getId(), child.getId()); @@ -944,8 +973,18 @@ public String getCollectionDatasetSchema(String dataverseAlias) { child.setRequiredDV(child.isRequired() && dsft.isRequired()); child.setInclude(true); } + if (child.isRequired()) { + childrenRequired.add(child.getName()); + } + childrenAllowed.add(child.getName()); } } + if (schemaChildMap != null) { + Map> map = new HashMap<>(); + map.put("required", childrenRequired); + map.put("allowed", childrenAllowed); + schemaChildMap.put(dsft.getName(), map); + } if(dsft.isRequiredDV()){ requiredDSFT.add(dsft); } @@ -1021,11 +1060,13 @@ private String getCustomMDBSchema (MetadataBlock mdb, List req } public String isDatasetJsonValid(String dataverseAlias, String jsonInput) { - JSONObject rawSchema = new JSONObject(new JSONTokener(getCollectionDatasetSchema(dataverseAlias))); + Map>> schemaChildMap = new HashMap<>(); + JSONObject rawSchema = new JSONObject(new JSONTokener(getCollectionDatasetSchema(dataverseAlias, schemaChildMap))); - try { + try { Schema schema = SchemaLoader.load(rawSchema); schema.validate(new JSONObject(jsonInput)); // throws a ValidationException if this object is invalid + JSONDataValidation.validate(schema, schemaChildMap, jsonInput); // throws a ValidationException if any objects are invalid } catch (ValidationException vx) { logger.info(BundleUtil.getStringFromBundle("dataverses.api.validate.json.failed") + " " + vx.getErrorMessage()); String accumulatedexceptions = ""; diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseTheme.java b/src/main/java/edu/harvard/iq/dataverse/DataverseTheme.java index 7f57d16b95a..850a75071fb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseTheme.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseTheme.java @@ -52,6 +52,7 @@ public enum Alignment { private Alignment logoAlignment; private String logoBackgroundColor; private String logo; + private String logoThumbnail; private Alignment logoFooterAlignment; private String logoFooterBackgroundColor; private String logoFooter; @@ -97,6 +98,14 @@ public void setLogo(String logo) { this.logo = logo; } + public String getLogoThumbnail() { + return this.logoThumbnail; + } + + public void setLogoThumbnail(String logoThumbnail) { + this.logoThumbnail = logoThumbnail; + } + public Alignment getLogoFooterAlignment() { return logoFooterAlignment; } diff --git a/src/main/java/edu/harvard/iq/dataverse/DvObject.java b/src/main/java/edu/harvard/iq/dataverse/DvObject.java index cc5d7620969..a4882f772d6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DvObject.java +++ b/src/main/java/edu/harvard/iq/dataverse/DvObject.java @@ -24,8 +24,6 @@ query = "SELECT o FROM DvObject o ORDER BY o.id"), @NamedQuery(name = "DvObject.findById", query = "SELECT o FROM DvObject o WHERE o.id=:id"), - @NamedQuery(name = "DvObject.checkExists", - query = "SELECT count(o) from DvObject o WHERE o.id=:id"), @NamedQuery(name = "DvObject.ownedObjectsById", query="SELECT COUNT(obj) FROM DvObject obj WHERE obj.owner.id=:id"), @NamedQuery(name = "DvObject.findByGlobalId", diff --git a/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java b/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java index 82057315fbb..56d26a7260d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java +++ b/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java @@ -1,14 +1,20 @@ package edu.harvard.iq.dataverse; import edu.harvard.iq.dataverse.dataaccess.DataAccess; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.storageuse.StorageUse; import edu.harvard.iq.dataverse.util.SystemConfig; +import edu.harvard.iq.dataverse.util.json.JsonUtil; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; import jakarta.persistence.CascadeType; import java.util.Optional; - import jakarta.persistence.MappedSuperclass; import jakarta.persistence.OneToOne; +import jakarta.persistence.Transient; + import org.apache.commons.lang3.StringUtils; /** @@ -42,6 +48,11 @@ public boolean isEffectivelyPermissionRoot() { private String metadataLanguage=null; private Boolean guestbookAtRequest = null; + + private String pidGeneratorSpecs = null; + + @Transient + private PidProvider pidGenerator = null; @OneToOne(mappedBy = "dvObjectContainer",cascade={ CascadeType.REMOVE, CascadeType.PERSIST}, orphanRemoval=true) private StorageUse storageUse; @@ -175,4 +186,79 @@ public void setCurationLabelSetName(String setName) { public void setStorageUse(StorageUse storageUse) { this.storageUse = storageUse; } + + + /* Dataverse collections and dataset can be configured to use different PidProviders as PID generators for contained objects (datasets or data files). + * This mechanism is similar to others except that the stored value is a JSON object defining the protocol, authority, shoulder, and, optionally, the separator for the PidProvider. + */ + + public String getPidGeneratorSpecs() { + return pidGeneratorSpecs; + } + + public void setPidGeneratorSpecs(String pidGeneratorSpecs) { + this.pidGeneratorSpecs = pidGeneratorSpecs; + } + + // Used in JSF when selecting the PidGenerator + // It only returns an id if this dvObjectContainer has PidGenerator specs set on it, otherwise it returns "default" + public String getPidGeneratorId() { + if (StringUtils.isBlank(getPidGeneratorSpecs())) { + return "default"; + } else { + return getEffectivePidGenerator().getId(); + } + } + + //Used in JSF when setting the PidGenerator + public void setPidGeneratorId(String pidGeneratorId) { + // Note that the "default" provider will not be found so will result in + // setPidGenerator(null), which unsets the pidGenerator/Specs as desired + setPidGenerator(PidUtil.getPidProvider(pidGeneratorId)); + } + + public void setPidGenerator(PidProvider pidGenerator) { + this.pidGenerator = pidGenerator; + if (pidGenerator != null) { + JsonObjectBuilder job = jakarta.json.Json.createObjectBuilder(); + this.pidGeneratorSpecs = job.add("protocol", pidGenerator.getProtocol()) + .add("authority", pidGenerator.getAuthority()).add("shoulder", pidGenerator.getShoulder()) + .add("separator", pidGenerator.getSeparator()).build().toString(); + } else { + this.pidGeneratorSpecs = null; + } + } + + public PidProvider getEffectivePidGenerator() { + if (pidGenerator == null) { + String specs = getPidGeneratorSpecs(); + if (StringUtils.isBlank(specs)) { + GlobalId pid = getGlobalId(); + if ((pid != null) && PidUtil.getPidProvider(pid.getProviderId()).canCreatePidsLike(pid)) { + pidGenerator = PidUtil.getPidProvider(pid.getProviderId()); + } else { + if (getOwner() != null) { + pidGenerator = getOwner().getEffectivePidGenerator(); + } + } + } else { + JsonObject providerSpecs = JsonUtil.getJsonObject(specs); + if (providerSpecs.containsKey("separator")) { + pidGenerator = PidUtil.getPidProvider(providerSpecs.getString("protocol"), + providerSpecs.getString("authority"), providerSpecs.getString("shoulder"), + providerSpecs.getString("separator")); + } else { + pidGenerator = PidUtil.getPidProvider(providerSpecs.getString("protocol"), + providerSpecs.getString("authority"), providerSpecs.getString("shoulder")); + } + } + if(pidGenerator!=null && pidGenerator.canManagePID()) { + setPidGenerator(pidGenerator); + } else { + setPidGenerator(null); + } + } + return pidGenerator; + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/DvObjectServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DvObjectServiceBean.java index d4219c36149..dc3393d82f5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DvObjectServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DvObjectServiceBean.java @@ -1,8 +1,9 @@ package edu.harvard.iq.dataverse; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidProviderFactoryBean; import edu.harvard.iq.dataverse.pidproviders.PidUtil; - import java.sql.Timestamp; import java.util.ArrayList; import java.util.Date; @@ -12,6 +13,8 @@ import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; + +import jakarta.ejb.EJB; import jakarta.ejb.Stateless; import jakarta.ejb.TransactionAttribute; import static jakarta.ejb.TransactionAttributeType.REQUIRES_NEW; @@ -38,6 +41,9 @@ public class DvObjectServiceBean implements java.io.Serializable { @PersistenceContext(unitName = "VDCNet-ejbPU") private EntityManager em; + @EJB + PidProviderFactoryBean pidProviderFactoryBean; + private static final Logger logger = Logger.getLogger(DvObjectServiceBean.class.getCanonicalName()); /** * @param dvoc The object we check @@ -76,12 +82,15 @@ public List findByAuthenticatedUserId(AuthenticatedUser user) { return query.getResultList(); } - public boolean checkExists(Long id) { - Query query = em.createNamedQuery("DvObject.checkExists"); - query.setParameter("id", id); - Long result =(Long)query.getSingleResult(); - return result > 0; - } + public String getDtype(Long id) { + Query query = em.createNativeQuery("SELECT dvo.dtype FROM dvobject dvo WHERE dvo.id=?"); + query.setParameter(1, id); + try { + return (String) query.getSingleResult(); + } catch (NoResultException e) { + return null; + } + } public DvObject findByGlobalId(String globalIdString, DvObject.DType dtype) { try { @@ -389,4 +398,19 @@ public String generateNewIdentifierByStoredProcedure() { return (String) query.getOutputParameterValue(1); } + /** @deprecated Backward-compatibility method to get the effective pid generator for a DvObjectContainer. + * If the dvObjectContainer method fails, this method will check for the old global default settings. + * If/when those are no longer supported, this method can be removed and replaced with calls directly + * to dvObjectContainer.getEffectivePidGenerator(); + * + */ + @Deprecated(forRemoval = true, since = "2024-02-09") + public PidProvider getEffectivePidGenerator(DvObjectContainer dvObjectContainer) { + PidProvider pidGenerator = dvObjectContainer.getEffectivePidGenerator(); + if (pidGenerator == null) { + pidGenerator = pidProviderFactoryBean.getDefaultPidGenerator(); + } + return pidGenerator; + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java index a6f31e24764..87997731642 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java @@ -1237,9 +1237,6 @@ public String save() { - We decided not to bother obtaining persistent ids for new files as they are uploaded and created. The identifiers will be assigned later, when the version is published. - - logger.info("starting async job for obtaining persistent ids for files."); - datasetService.obtainPersistentIdentifiersForDatafiles(dataset); */ } @@ -2095,6 +2092,12 @@ public void handleFileUpload(FileUploadEvent event) throws IOException { errorMessages.add(cex.getMessage()); uploadComponentId = event.getComponent().getClientId(); return; + } finally { + try { + uFile.delete(); + } catch (IOException ioex) { + logger.warning("Failed to delete temp file uploaded via PrimeFaces " + uFile.getFileName()); + } } /*catch (FileExceedsMaxSizeException ex) { logger.warning("Failed to process and/or save the file " + uFile.getFileName() + "; " + ex.getMessage()); @@ -2124,8 +2127,12 @@ public void handleFileUpload(FileUploadEvent event) throws IOException { } /** - * Using information from the DropBox choose, ingest the chosen files - * https://www.dropbox.com/developers/dropins/chooser/js + * External, aka "Direct" Upload. + * The file(s) have been uploaded to physical storage (such as S3) directly, + * this call is to create and add the DataFiles to the Dataset on the Dataverse + * side. The method does NOT finalize saving the datafiles in the database - + * that will happen when the user clicks 'Save', similar to how the "normal" + * uploads are handled. * * @param event */ diff --git a/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java b/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java index 5a689c06019..0561fed8a97 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java +++ b/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java @@ -4,6 +4,7 @@ import edu.harvard.iq.dataverse.actionlogging.ActionLogServiceBean; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUserServiceBean; +import edu.harvard.iq.dataverse.util.cache.CacheFactoryBean; import edu.harvard.iq.dataverse.engine.DataverseEngine; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean; @@ -11,14 +12,15 @@ import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean; import edu.harvard.iq.dataverse.datacapturemodule.DataCaptureModuleServiceBean; +import edu.harvard.iq.dataverse.dataset.DatasetTypeServiceBean; import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; +import edu.harvard.iq.dataverse.engine.command.exception.RateLimitCommandException; 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.pidproviders.PidProviderFactoryBean; import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; import edu.harvard.iq.dataverse.search.IndexBatchServiceBean; import edu.harvard.iq.dataverse.search.IndexServiceBean; @@ -49,7 +51,6 @@ import static jakarta.ejb.TransactionAttributeType.SUPPORTS; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; -import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; /** @@ -114,20 +115,8 @@ public class EjbDataverseEngine { DataverseFieldTypeInputLevelServiceBean fieldTypeInputLevels; @EJB - DOIEZIdServiceBean doiEZId; - - @EJB - DOIDataCiteServiceBean doiDataCite; - - @EJB - FakePidProviderServiceBean fakePidProvider; + PidProviderFactoryBean pidProviderFactory; - @EJB - HandlenetServiceBean handleNet; - - @EJB - PermaLinkPidProviderServiceBean permaLinkProvider; - @EJB SettingsServiceBean settings; @@ -136,13 +125,22 @@ public class EjbDataverseEngine { @EJB GuestbookResponseServiceBean responses; - + + @EJB + MetadataBlockServiceBean metadataBlockService; + + @EJB + DatasetTypeServiceBean datasetTypeService; + @EJB DataverseLinkingServiceBean dvLinking; @EJB DatasetLinkingServiceBean dsLinking; + @EJB + DatasetFieldServiceBean dsField; + @EJB ExplicitGroupServiceBean explicitGroups; @@ -190,7 +188,9 @@ public class EjbDataverseEngine { @EJB EjbDataverseEngineInner innerEngine; - + + @EJB + CacheFactoryBean cacheFactory; @Resource EJBContext ejbCtxt; @@ -216,7 +216,11 @@ public R submit(Command aCommand) throws CommandException { try { logRec.setUserIdentifier( aCommand.getRequest().getUser().getIdentifier() ); - + // Check for rate limit exceeded. Must be done before anything else to prevent unnecessary processing. + if (!cacheFactory.checkRate(aCommand.getRequest().getUser(), aCommand)) { + throw new RateLimitCommandException(BundleUtil.getStringFromBundle("command.exception.user.ratelimited", Arrays.asList(aCommand.getClass().getSimpleName())), aCommand); + } + // Check permissions - or throw an exception Map> requiredMap = aCommand.getRequiredPermissions(); if (requiredMap == null) { @@ -484,28 +488,8 @@ public DataverseFieldTypeInputLevelServiceBean fieldTypeInputLevels() { } @Override - public DOIEZIdServiceBean doiEZId() { - return doiEZId; - } - - @Override - public DOIDataCiteServiceBean doiDataCite() { - return doiDataCite; - } - - @Override - public FakePidProviderServiceBean fakePidProvider() { - return fakePidProvider; - } - - @Override - public HandlenetServiceBean handleNet() { - return handleNet; - } - - @Override - public PermaLinkPidProviderServiceBean permaLinkProvider() { - return permaLinkProvider; + public PidProviderFactoryBean pidProviderFactory() { + return pidProviderFactory; } @Override @@ -532,7 +516,12 @@ public DataverseLinkingServiceBean dvLinking() { public DatasetLinkingServiceBean dsLinking() { return dsLinking; } - + + @Override + public DatasetFieldServiceBean dsField() { + return dsField; + } + @Override public StorageUseServiceBean storageUse() { return storageUseService; @@ -613,6 +602,16 @@ public ActionLogServiceBean actionLog() { return logSvc; } + @Override + public MetadataBlockServiceBean metadataBlocks() { + return metadataBlockService; + } + + @Override + public DatasetTypeServiceBean datasetTypes() { + return datasetTypeService; + } + @Override public void beginCommandSequence() { this.commandsCalled = new Stack(); diff --git a/src/main/java/edu/harvard/iq/dataverse/ExternalFileUploadInProgress.java b/src/main/java/edu/harvard/iq/dataverse/ExternalFileUploadInProgress.java new file mode 100644 index 00000000000..c90fdc6edc2 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/ExternalFileUploadInProgress.java @@ -0,0 +1,110 @@ +package edu.harvard.iq.dataverse; + +import jakarta.persistence.Column; +import jakarta.persistence.Index; +import jakarta.persistence.NamedQueries; +import jakarta.persistence.NamedQuery; +import jakarta.persistence.Table; +import java.io.Serializable; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +/** + * + * @author landreev + * + * The name of the class is provisional. I'm open to better-sounding alternatives, + * if anyone can think of any. + * But I wanted to avoid having the word "Globus" in the entity name. I'm adding + * it specifically for the Globus use case. But I'm guessing there's a chance + * this setup may come in handy for other types of datafile uploads that happen + * externally. (?) + */ +@NamedQueries({ + @NamedQuery(name = "ExternalFileUploadInProgress.deleteByTaskId", + query = "DELETE FROM ExternalFileUploadInProgress f WHERE f.taskId=:taskId"), + @NamedQuery(name = "ExternalFileUploadInProgress.findByTaskId", + query = "SELECT f FROM ExternalFileUploadInProgress f WHERE f.taskId=:taskId")}) +@Entity +@Table(indexes = {@Index(columnList="taskid")}) +public class ExternalFileUploadInProgress implements Serializable { + + private static final long serialVersionUID = 1L; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + /** + * Rather than saving various individual fields defining the datafile, + * which would essentially replicate the DataFile table, we are simply + * storing the full json record as passed to the API here. + */ + @Column(columnDefinition = "TEXT", nullable=false) + private String fileInfo; + + /** + * This is Globus-specific task id associated with the upload in progress + */ + @Column(nullable=false) + private String taskId; + + public ExternalFileUploadInProgress() { + } + + public ExternalFileUploadInProgress(String taskId, String fileInfo) { + this.taskId = taskId; + this.fileInfo = fileInfo; + } + + public String getFileInfo() { + return fileInfo; + } + + public void setFileInfo(String fileInfo) { + this.fileInfo = fileInfo; + } + + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + @Override + public int hashCode() { + int hash = 0; + hash += (id != null ? id.hashCode() : 0); + return hash; + } + + @Override + public boolean equals(Object object) { + // TODO: Warning - this method won't work in the case the id fields are not set + if (!(object instanceof ExternalFileUploadInProgress)) { + return false; + } + ExternalFileUploadInProgress other = (ExternalFileUploadInProgress) object; + if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) { + return false; + } + return true; + } + + @Override + public String toString() { + return "edu.harvard.iq.dataverse.ExternalFileUploadInProgress[ id=" + id + " ]"; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/ExternalIdentifier.java b/src/main/java/edu/harvard/iq/dataverse/ExternalIdentifier.java index 0b7285c017e..8c4fb6b1325 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ExternalIdentifier.java +++ b/src/main/java/edu/harvard/iq/dataverse/ExternalIdentifier.java @@ -12,7 +12,9 @@ public enum ExternalIdentifier { GND("GND", "https://d-nb.info/gnd/%s", "^1[01]?\\d{7}[0-9X]|[47]\\d{6}-\\d|[1-9]\\d{0,7}-[0-9X]|3\\d{7}[0-9X]$"), // note: DAI is missing from this list, because it doesn't have resolvable URL ResearcherID("ResearcherID", "https://publons.com/researcher/%s/", "^[A-Z\\d][A-Z\\d-]+[A-Z\\d]$"), - ScopusID("ScopusID", "https://www.scopus.com/authid/detail.uri?authorId=%s", "^\\d*$"); + ScopusID("ScopusID", "https://www.scopus.com/authid/detail.uri?authorId=%s", "^\\d*$"), + //Requiring ROR to be URL form as we use it where there is no id type field and matching any 9 digit number starting with 0 seems a bit aggressive + ROR("ROR", "https://ror.org/%s", "^(https:\\/\\/ror.org\\/)0[a-hj-km-np-tv-z|0-9]{6}[0-9]{2}$"); private String name; private String template; diff --git a/src/main/java/edu/harvard/iq/dataverse/FeaturedDataverseServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/FeaturedDataverseServiceBean.java index d4d701cb02f..8e3ff0aa581 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FeaturedDataverseServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/FeaturedDataverseServiceBean.java @@ -66,8 +66,11 @@ public List findByDataverseIdQuick(Long dataverseId) { } dataverse.setDataverseTheme(dataverseService.findDataverseThemeByIdQuick(id)); - if (dataverse.getDataverseTheme()!=null){ - logger.fine("THEME: "+dataverse.getDataverseTheme().getLogo()+", "+dataverse.getDataverseTheme().getLogoFormat()); + if (dataverse.getDataverseTheme() != null) { + logger.fine("THEME: " + + dataverse.getDataverseTheme().getLogo() + ", " + + dataverse.getDataverseTheme().getLogoFormat() + ", " + + dataverse.getDataverseTheme().getLogoThumbnail()); } ret.add(dataverse); } diff --git a/src/main/java/edu/harvard/iq/dataverse/FileDownloadHelper.java b/src/main/java/edu/harvard/iq/dataverse/FileDownloadHelper.java index 4d8100124ec..80cf3db8d53 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FileDownloadHelper.java +++ b/src/main/java/edu/harvard/iq/dataverse/FileDownloadHelper.java @@ -8,6 +8,8 @@ import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; +import edu.harvard.iq.dataverse.dataaccess.DataAccess; +import edu.harvard.iq.dataverse.dataaccess.StorageIO; import edu.harvard.iq.dataverse.externaltools.ExternalTool; import edu.harvard.iq.dataverse.globus.GlobusServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; @@ -221,7 +223,10 @@ public boolean canDownloadFile(FileMetadata fileMetadata){ // Always allow download for PrivateUrlUser return true; } - + + // Retention expired files are always made unavailable, because they might be destroyed + if (FileUtil.isRetentionExpired(fileMetadata)) return false; + Long fid = fileMetadata.getId(); //logger.info("calling candownloadfile on filemetadata "+fid); // Note that `isRestricted` at the FileMetadata level is for expressing intent by version. Enforcement is done with `isRestricted` at the DataFile level. @@ -244,7 +249,9 @@ public boolean canDownloadFile(FileMetadata fileMetadata){ } } - if (!isRestrictedFile && !FileUtil.isActivelyEmbargoed(fileMetadata)){ + if (!isRestrictedFile + && !FileUtil.isActivelyEmbargoed(fileMetadata) + && !FileUtil.isRetentionExpired(fileMetadata)) { // Yes, save answer and return true this.fileDownloadPermissionMap.put(fid, true); 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 de3f4d2ab56..5370e9ac564 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FileDownloadServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/FileDownloadServiceBean.java @@ -354,7 +354,8 @@ public void explore(GuestbookResponse guestbookResponse, FileMetadata fmd, Exter ApiToken apiToken = null; User user = session.getUser(); DatasetVersion version = fmd.getDatasetVersion(); - if (version.isDraft() || fmd.getDatasetVersion().isDeaccessioned() || (fmd.getDataFile().isRestricted()) || (FileUtil.isActivelyEmbargoed(fmd))) { + if (version.isDraft() || fmd.getDatasetVersion().isDeaccessioned() || (fmd.getDataFile().isRestricted()) + || (FileUtil.isActivelyEmbargoed(fmd)) || (FileUtil.isRetentionExpired(fmd))) { apiToken = authService.getValidApiTokenForUser(user); } DataFile dataFile = null; @@ -382,28 +383,26 @@ public void explore(GuestbookResponse guestbookResponse, FileMetadata fmd, Exter } } - public void downloadDatasetCitationXML(Dataset dataset) { - downloadCitationXML(null, dataset, false); + public void downloadDatasetCitationXML(DatasetVersion version) { + // DatasetVersion-level citation: + DataCitation citation=null; + citation = new DataCitation(version); + String fileNameString; + fileNameString = "attachment;filename=" + getFileNameFromPid(citation.getPersistentId()) + ".xml"; + downloadXML(citation, fileNameString); } public void downloadDatafileCitationXML(FileMetadata fileMetadata) { - downloadCitationXML(fileMetadata, null, false); + downloadCitationXML(fileMetadata, false); } public void downloadDirectDatafileCitationXML(FileMetadata fileMetadata) { - downloadCitationXML(fileMetadata, null, true); + downloadCitationXML(fileMetadata, true); } - public void downloadCitationXML(FileMetadata fileMetadata, Dataset dataset, boolean direct) { - DataCitation citation=null; - if (dataset != null){ - citation = new DataCitation(dataset.getLatestVersion()); - } else { - citation= new DataCitation(fileMetadata, direct); - } - FacesContext ctx = FacesContext.getCurrentInstance(); - HttpServletResponse response = (HttpServletResponse) ctx.getExternalContext().getResponse(); - response.setContentType("text/xml"); + public void downloadCitationXML(FileMetadata fileMetadata, boolean direct) { + DataCitation citation=null; + citation= new DataCitation(fileMetadata, direct); String fileNameString; if (fileMetadata == null || fileMetadata.getLabel() == null) { // Dataset-level citation: @@ -412,43 +411,46 @@ public void downloadCitationXML(FileMetadata fileMetadata, Dataset dataset, bool // Datafile-level citation: fileNameString = "attachment;filename=" + getFileNameFromPid(citation.getPersistentId()) + "-" + FileUtil.getCiteDataFileFilename(citation.getFileTitle(), FileUtil.FileCitationExtension.ENDNOTE); } + downloadXML(citation, fileNameString); + } + + public void downloadXML(DataCitation citation, String fileNameString) { + FacesContext ctx = FacesContext.getCurrentInstance(); + HttpServletResponse response = (HttpServletResponse) ctx.getExternalContext().getResponse(); + response.setContentType("text/xml"); response.setHeader("Content-Disposition", fileNameString); + try { ServletOutputStream out = response.getOutputStream(); citation.writeAsEndNoteCitation(out); out.flush(); ctx.responseComplete(); } catch (IOException e) { - } } - - public void downloadDatasetCitationRIS(Dataset dataset) { - downloadCitationRIS(null, dataset, false); + public void downloadDatasetCitationRIS(DatasetVersion version) { + // DatasetVersion-level citation: + DataCitation citation=null; + citation = new DataCitation(version); + String fileNameString; + fileNameString = "attachment;filename=" + getFileNameFromPid(citation.getPersistentId()) + ".ris"; + downloadRIS(citation, fileNameString); } public void downloadDatafileCitationRIS(FileMetadata fileMetadata) { - downloadCitationRIS(fileMetadata, null, false); + downloadCitationRIS(fileMetadata, false); } public void downloadDirectDatafileCitationRIS(FileMetadata fileMetadata) { - downloadCitationRIS(fileMetadata, null, true); + downloadCitationRIS(fileMetadata, true); } - public void downloadCitationRIS(FileMetadata fileMetadata, Dataset dataset, boolean direct) { - DataCitation citation=null; - if (dataset != null){ - citation = new DataCitation(dataset.getLatestVersion()); - } else { - citation= new DataCitation(fileMetadata, direct); - } - - FacesContext ctx = FacesContext.getCurrentInstance(); - HttpServletResponse response = (HttpServletResponse) ctx.getExternalContext().getResponse(); - response.setContentType("application/download"); - + public void downloadCitationRIS(FileMetadata fileMetadata, boolean direct) { + DataCitation citation=null; + citation= new DataCitation(fileMetadata, direct); + String fileNameString; if (fileMetadata == null || fileMetadata.getLabel() == null) { // Dataset-level citation: @@ -457,6 +459,14 @@ public void downloadCitationRIS(FileMetadata fileMetadata, Dataset dataset, bool // Datafile-level citation: fileNameString = "attachment;filename=" + getFileNameFromPid(citation.getPersistentId()) + "-" + FileUtil.getCiteDataFileFilename(citation.getFileTitle(), FileUtil.FileCitationExtension.RIS); } + downloadRIS(citation, fileNameString); + } + + public void downloadRIS(DataCitation citation, String fileNameString) { + //SEK 12/3/2018 changing this to open the json in a new tab. + FacesContext ctx = FacesContext.getCurrentInstance(); + HttpServletResponse response = (HttpServletResponse) ctx.getExternalContext().getResponse(); + response.setContentType("application/download"); response.setHeader("Content-Disposition", fileNameString); try { @@ -468,38 +478,33 @@ public void downloadCitationRIS(FileMetadata fileMetadata, Dataset dataset, bool } } - + private String getFileNameFromPid(GlobalId id) { return id.asString(); } - public void downloadDatasetCitationBibtex(Dataset dataset) { - downloadCitationBibtex(null, dataset, false); + public void downloadDatasetCitationBibtex(DatasetVersion version) { + // DatasetVersion-level citation: + DataCitation citation=null; + citation = new DataCitation(version); + String fileNameString; + fileNameString = "inline;filename=" + getFileNameFromPid(citation.getPersistentId()) + ".bib"; + downloadBibtex(citation, fileNameString); } public void downloadDatafileCitationBibtex(FileMetadata fileMetadata) { - downloadCitationBibtex(fileMetadata, null, false); + downloadCitationBibtex(fileMetadata, false); } public void downloadDirectDatafileCitationBibtex(FileMetadata fileMetadata) { - downloadCitationBibtex(fileMetadata, null, true); + downloadCitationBibtex(fileMetadata, true); } - public void downloadCitationBibtex(FileMetadata fileMetadata, Dataset dataset, boolean direct) { - DataCitation citation=null; - if (dataset != null){ - citation = new DataCitation(dataset.getLatestVersion()); - } else { - citation= new DataCitation(fileMetadata, direct); - } - //SEK 12/3/2018 changing this to open the json in a new tab. - FacesContext ctx = FacesContext.getCurrentInstance(); - HttpServletResponse response = (HttpServletResponse) ctx.getExternalContext().getResponse(); - - //Fix for 6029 FireFox was failing to parse it when content type was set to json - response.setContentType("text/plain"); + public void downloadCitationBibtex(FileMetadata fileMetadata, boolean direct) { + DataCitation citation=null; + citation= new DataCitation(fileMetadata, direct); String fileNameString; if (fileMetadata == null || fileMetadata.getLabel() == null) { @@ -509,6 +514,16 @@ public void downloadCitationBibtex(FileMetadata fileMetadata, Dataset dataset, b // Datafile-level citation: fileNameString = "inline;filename=" + getFileNameFromPid(citation.getPersistentId()) + "-" + FileUtil.getCiteDataFileFilename(citation.getFileTitle(), FileUtil.FileCitationExtension.BIBTEX); } + downloadBibtex(citation, fileNameString); + } + + public void downloadBibtex(DataCitation citation, String fileNameString) { + //SEK 12/3/2018 changing this to open the json in a new tab. + FacesContext ctx = FacesContext.getCurrentInstance(); + HttpServletResponse response = (HttpServletResponse) ctx.getExternalContext().getResponse(); + + //Fix for 6029 FireFox was failing to parse it when content type was set to json + response.setContentType("text/plain"); response.setHeader("Content-Disposition", fileNameString); try { @@ -558,7 +573,7 @@ public boolean requestAccess(DataFile dataFile, GuestbookResponse gbr){ public void sendRequestFileAccessNotification(Dataset dataset, Long fileId, AuthenticatedUser requestor) { Timestamp ts = new Timestamp(new Date().getTime()); - permissionService.getUsersWithPermissionOn(Permission.ManageDatasetPermissions, dataset).stream().forEach((au) -> { + permissionService.getUsersWithPermissionOn(Permission.ManageFilePermissions, dataset).stream().forEach((au) -> { userNotificationService.sendNotification(au, ts, UserNotification.Type.REQUESTFILEACCESS, fileId, null, requestor, true); }); //send the user that requested access a notification that they requested the access diff --git a/src/main/java/edu/harvard/iq/dataverse/FilePage.java b/src/main/java/edu/harvard/iq/dataverse/FilePage.java index 479c8a429c6..5717da38f29 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FilePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/FilePage.java @@ -21,6 +21,7 @@ import edu.harvard.iq.dataverse.engine.command.impl.CreateNewDatasetCommand; 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.UningestFileCommand; import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; import edu.harvard.iq.dataverse.export.ExportService; import io.gdcc.spi.export.ExportException; @@ -28,24 +29,33 @@ import edu.harvard.iq.dataverse.externaltools.ExternalTool; import edu.harvard.iq.dataverse.externaltools.ExternalToolHandler; import edu.harvard.iq.dataverse.externaltools.ExternalToolServiceBean; +import edu.harvard.iq.dataverse.ingest.IngestRequest; +import edu.harvard.iq.dataverse.ingest.IngestServiceBean; import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean; import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean.MakeDataCountEntry; import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; +import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.JsfHelper; +import edu.harvard.iq.dataverse.util.StringUtil; + import static edu.harvard.iq.dataverse.util.JsfHelper.JH; import edu.harvard.iq.dataverse.util.SystemConfig; import java.io.IOException; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Set; +import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; + import jakarta.ejb.EJB; import jakarta.ejb.EJBException; import jakarta.faces.application.FacesMessage; @@ -112,10 +122,10 @@ public class FilePage implements java.io.Serializable { GuestbookResponseServiceBean guestbookResponseService; @EJB AuthenticationServiceBean authService; - @EJB DatasetServiceBean datasetService; - + @EJB + IngestServiceBean ingestService; @EJB SystemConfig systemConfig; @@ -144,6 +154,9 @@ public class FilePage implements java.io.Serializable { @Inject EmbargoServiceBean embargoService; + @Inject + RetentionServiceBean retentionService; + private static final Logger logger = Logger.getLogger(FilePage.class.getCanonicalName()); private boolean fileDeleteInProgress = false; @@ -209,7 +222,7 @@ public String init() { // If this DatasetVersion is unpublished and permission is doesn't have permissions: // > Go to the Login page // - // Check permisisons + // Check permissions Boolean authorized = (fileMetadata.getDatasetVersion().isReleased()) || (!fileMetadata.getDatasetVersion().isReleased() && this.canViewUnpublishedDataset()); @@ -238,12 +251,10 @@ public String init() { if (file.isTabularData()) { contentType=DataFileServiceBean.MIME_TYPE_TSV_ALT; } - 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(); - + loadExternalTools(); + + + if (toolType != null) { if (toolType.equals("PREVIEW")) { if (!toolsWithPreviews.isEmpty()) { @@ -271,11 +282,42 @@ public String init() { if(!hasValidTermsOfAccess && canUpdateDataset() ){ JsfHelper.addWarningMessage(BundleUtil.getStringFromBundle("dataset.message.editMetadata.invalid.TOUA.message")); } - + + LocalDate minRetentiondate = settingsWrapper.getMinRetentionDate(); + if (minRetentiondate != null){ + selectionRetention.setDateUnavailable(minRetentiondate.plusDays(1L)); + } + displayPublishMessage(); return null; } + private void loadExternalTools() { + String contentType= file.getContentType(); + 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(); + //For inaccessible files, only show the tools that have access to aux files (which are currently always accessible) + if(!StorageIO.isDataverseAccessible(DataAccess.getStorageDriverFromIdentifier(file.getStorageIdentifier()))) { + configureTools = configureTools.stream().filter(tool ->tool.accessesAuxFiles()).collect(Collectors.toList()); + exploreTools = exploreTools.stream().filter(tool ->tool.accessesAuxFiles()).collect(Collectors.toList()); + queryTools = queryTools.stream().filter(tool ->tool.accessesAuxFiles()).collect(Collectors.toList()); + toolsWithPreviews = toolsWithPreviews.stream().filter(tool ->tool.accessesAuxFiles()).collect(Collectors.toList()); + } else { + // Don't list queryTools for non-public files + // Note - this logic is not the same as isPubliclyDownloadable which appears to be true for a draft-only file + // It is the same as in the DatasetPage.isShowQueryButton() method + if(file.isRestricted() + || !file.isReleased() + || FileUtil.isActivelyEmbargoed(file) + || FileUtil.isRetentionExpired(file)){ + queryTools = new ArrayList<>(); + } + } + } + private void displayPublishMessage(){ if (fileMetadata.getDatasetVersion().isDraft() && canUpdateDataset() && (canPublishDataset() || !fileMetadata.getDatasetVersion().getDataset().isLockedFor(DatasetLock.Reason.InReview))){ @@ -283,13 +325,18 @@ private void displayPublishMessage(){ } } + Boolean valid = null; + public boolean isValid() { - if (!fileMetadata.getDatasetVersion().isDraft()) { - return true; + if (valid == null) { + final DatasetVersion workingVersion = fileMetadata.getDatasetVersion(); + if (workingVersion.isDraft() || (canUpdateDataset() && JvmSettings.UI_SHOW_VALIDITY_LABEL_WHEN_PUBLISHED.lookupOptional(Boolean.class).orElse(true))) { + valid = workingVersion.isValid(); + } else { + valid = true; + } } - DatasetVersion newVersion = fileMetadata.getDatasetVersion().cloneDatasetVersion(); - newVersion.setDatasetFields(newVersion.initDatasetFields()); - return newVersion.isValid(); + return valid; } private boolean canViewUnpublishedDataset() { @@ -475,6 +522,119 @@ public String restrictFile(boolean restricted) throws CommandException{ return returnToDraftVersion(); } + public String ingestFile() throws CommandException{ + + User u = session.getUser(); + if(!u.isAuthenticated() || !u.isSuperuser()) { + //Shouldn't happen (choice not displayed for users who don't have the right permission), but check anyway + logger.warning("User: " + u.getIdentifier() + " tried to ingest a file"); + JH.addMessage(FacesMessage.SEVERITY_WARN, BundleUtil.getStringFromBundle("file.ingest.cantIngestFileWarning")); + return null; + } + + editDataset = file.getOwner(); + + if (file.isTabularData()) { + JH.addMessage(FacesMessage.SEVERITY_WARN, BundleUtil.getStringFromBundle("file.ingest.alreadyIngestedWarning")); + return null; + } + + boolean ingestLock = dataset.isLockedFor(DatasetLock.Reason.Ingest); + + if (ingestLock) { + JH.addMessage(FacesMessage.SEVERITY_WARN, BundleUtil.getStringFromBundle("file.ingest.ingestInProgressWarning")); + return null; + } + + if (!FileUtil.canIngestAsTabular(file)) { + JH.addMessage(FacesMessage.SEVERITY_WARN, BundleUtil.getStringFromBundle("file.ingest.cantIngestFileWarning")); + return null; + + } + + file.SetIngestScheduled(); + + if (file.getIngestRequest() == null) { + file.setIngestRequest(new IngestRequest(file)); + } + + file.getIngestRequest().setForceTypeCheck(true); + + // update the datafile, to save the newIngest request in the database: + datafileService.save(file); + + // queue the data ingest job for asynchronous execution: + String status = ingestService.startIngestJobs(editDataset.getId(), new ArrayList<>(Arrays.asList(file)), (AuthenticatedUser) session.getUser()); + + if (!StringUtil.isEmpty(status)) { + // This most likely indicates some sort of a problem (for example, + // the ingest job was not put on the JMS queue because of the size + // of the file). But we are still returning the OK status - because + // from the point of view of the API, it's a success - we have + // successfully gone through the process of trying to schedule the + // ingest job... + + logger.warning("Ingest Status for file: " + file.getId() + " : " + status); + } + logger.fine("File: " + file.getId() + " ingest queued"); + + init(); + JsfHelper.addInfoMessage(BundleUtil.getStringFromBundle("file.ingest.ingestQueued")); + return returnToDraftVersion(); + } + + public String uningestFile() throws CommandException { + + if (!file.isTabularData()) { + //Ingest never succeeded, either there was a failure or this is not a tabular data file + if (file.isIngestProblem()) { + //We allow anyone who can publish to uningest in order to clear a problem + User u = session.getUser(); + if (!u.isAuthenticated() || !(permissionService.permissionsFor(u, file).contains(Permission.PublishDataset))) { + logger.warning("User: " + u.getIdentifier() + " tried to uningest a file"); + // Shouldn't happen (choice not displayed for users who don't have the right + // permission), but check anyway + JH.addMessage(FacesMessage.SEVERITY_WARN, + BundleUtil.getStringFromBundle("file.ingest.cantUningestFileWarning")); + return null; + } + file.setIngestDone(); + file.setIngestReport(null); + } else { + //Shouldn't happen - got called when there is no tabular data or an ingest problem + JH.addMessage(FacesMessage.SEVERITY_WARN, + BundleUtil.getStringFromBundle("file.ingest.cantUningestFileWarning")); + return null; + } + } else { + //Superuser required to uningest after a success + //Uningest command does it's own check for isSuperuser + commandEngine.submit(new UningestFileCommand(dvRequestService.getDataverseRequest(), file)); + Long dataFileId = file.getId(); + file = datafileService.find(dataFileId); + } + editDataset = file.getOwner(); + if (editDataset.isReleased()) { + try { + ExportService instance = ExportService.getInstance(); + instance.exportAllFormats(editDataset); + + } catch (ExportException ex) { + // Something went wrong! + // Just like with indexing, a failure to export is not a fatal + // condition. We'll just log the error as a warning and keep + // going: + logger.log(Level.WARNING, "Uningest: Exception while exporting:{0}", ex.getMessage()); + } + } + datafileService.save(file); + + // Refresh filemetadata with file title, etc. + init(); + JH.addMessage(FacesMessage.SEVERITY_INFO, BundleUtil.getStringFromBundle("file.uningest.complete")); + return returnToDraftVersion(); + } + private List filesToBeDeleted = new ArrayList<>(); public String deleteFile() { @@ -948,6 +1108,12 @@ public boolean isPubliclyDownloadable() { return FileUtil.isPubliclyDownloadable(fileMetadata); } + public boolean isIngestable() { + DataFile f = fileMetadata.getDataFile(); + //Datafile is an ingestable type and hasn't been ingested yet or had an ingest fail + return (FileUtil.canIngestAsTabular(f)&&!(f.isTabularData() || f.isIngestProblem())); + } + private Boolean lockedFromEditsVar; private Boolean lockedFromDownloadVar; @@ -1247,7 +1413,129 @@ public String getEmbargoPhrase() { return BundleUtil.getStringFromBundle("embargoed.willbeuntil"); } } - + + public boolean isValidRetentionSelection() { + if (!fileMetadata.getDataFile().isReleased()) { + return true; + } + return false; + } + + public boolean isExistingRetention() { + if (!fileMetadata.getDataFile().isReleased() && (fileMetadata.getDataFile().getRetention() != null)) { + return true; + } + return false; + } + + public boolean isRetentionForWholeSelection() { + return isValidRetentionSelection(); + } + + public Retention getSelectionRetention() { + return selectionRetention; + } + + public void setSelectionRetention(Retention selectionRetention) { + this.selectionRetention = selectionRetention; + } + + private Retention selectionRetention = new Retention(); + + private boolean removeRetention=false; + + public boolean isRemoveRetention() { + return removeRetention; + } + + public void setRemoveRetention(boolean removeRetention) { + boolean existing = this.removeRetention; + this.removeRetention = removeRetention; + if (existing != this.removeRetention) { + logger.info("State flip"); + selectionRetention = new Retention(); + if (removeRetention) { + selectionRetention = new Retention(null, null); + } + } + PrimeFaces.current().resetInputs("fileForm:retentionInputs"); + } + + public String saveRetention() { + + if(isRemoveRetention() || (selectionRetention.getDateUnavailable()==null && selectionRetention.getReason()==null)) { + selectionRetention=null; + } + + Retention ret = null; + // Note: this.fileMetadata.getDataFile() is not the same object as this.file. + // (Not sure there's a good reason for this other than that's the way it is.) + // So changes to this.fileMetadata.getDataFile() will not be saved with + // editDataset = this.file.getOwner() set as it is below. + if (!file.isReleased()) { + ret = file.getRetention(); + if (ret != null) { + logger.fine("Before: " + ret.getDataFiles().size()); + ret.getDataFiles().remove(fileMetadata.getDataFile()); + logger.fine("After: " + ret.getDataFiles().size()); + } + if (selectionRetention != null) { + retentionService.merge(selectionRetention); + } + file.setRetention(selectionRetention); + if (ret != null && !ret.getDataFiles().isEmpty()) { + ret = null; + } + } + if(selectionRetention!=null) { + retentionService.save(selectionRetention, ((AuthenticatedUser)session.getUser()).getIdentifier()); + } + // success message: + String successMessage = BundleUtil.getStringFromBundle("file.assignedRetention.success"); + logger.fine(successMessage); + successMessage = successMessage.replace("{0}", "Selected Files"); + JsfHelper.addFlashMessage(successMessage); + selectionRetention = new Retention(); + + //Caller has to set editDataset before calling save() + editDataset = this.file.getOwner(); + + save(); + init(); + if(ret!=null) { + retentionService.delete(ret,((AuthenticatedUser)session.getUser()).getIdentifier()); + } + return returnToDraftVersion(); + } + + public void clearRetentionPopup() { + setRemoveRetention(false); + selectionRetention = new Retention(); + PrimeFaces.current().resetInputs("fileForm:retentionInputs"); + } + + public void clearSelectionRetention() { + selectionRetention = new Retention(); + PrimeFaces.current().resetInputs("fileForm:retentionInputs"); + } + + public boolean isCantRequestDueToRetention() { + return FileUtil.isRetentionExpired(fileMetadata); + } + + public String getRetentionPhrase() { + //Should only be getting called when there is a retention + if(file.isReleased()) { + if(FileUtil.isRetentionExpired(file)) { + return BundleUtil.getStringFromBundle("retention.after"); + } else { + return BundleUtil.getStringFromBundle("retention.isfrom"); + } + } else { + return BundleUtil.getStringFromBundle("retention.willbeafter"); + } + } + public String getToolTabTitle(){ if (getAllAvailableTools().size() > 1) { return BundleUtil.getStringFromBundle("file.toolTab.header"); diff --git a/src/main/java/edu/harvard/iq/dataverse/FileSearchCriteria.java b/src/main/java/edu/harvard/iq/dataverse/FileSearchCriteria.java index 62f10c18bdf..e3ed507a9c2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FileSearchCriteria.java +++ b/src/main/java/edu/harvard/iq/dataverse/FileSearchCriteria.java @@ -12,7 +12,7 @@ public class FileSearchCriteria { * Status of the particular DataFile based on active embargoes and restriction state */ public enum FileAccessStatus { - Public, Restricted, EmbargoedThenRestricted, EmbargoedThenPublic + Public, Restricted, EmbargoedThenRestricted, EmbargoedThenPublic, RetentionPeriodExpired } public FileSearchCriteria(String contentType, FileAccessStatus accessStatus, String categoryName, String tabularTagName, String searchText) { diff --git a/src/main/java/edu/harvard/iq/dataverse/GlobalId.java b/src/main/java/edu/harvard/iq/dataverse/GlobalId.java index 890b146a61c..1c8783c5bd5 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.pidproviders.PermaLinkPidProviderServiceBean; +import edu.harvard.iq.dataverse.pidproviders.perma.PermaLinkPidProvider; import edu.harvard.iq.dataverse.util.BundleUtil; import static edu.harvard.iq.dataverse.util.StringUtil.isEmpty; import java.net.MalformedURLException; @@ -33,7 +33,7 @@ public GlobalId(String protocol, String authority, String identifier, String sep this.separator = separator; } this.urlPrefix = urlPrefix; - this.managingProviderName = providerName; + this.managingProviderId = providerName; } // protocol the identifier system, e.g. "doi" @@ -42,7 +42,7 @@ public GlobalId(String protocol, String authority, String identifier, String sep private String protocol; private String authority; private String identifier; - private String managingProviderName; + private String managingProviderId; private String separator = "/"; private String urlPrefix; @@ -67,8 +67,8 @@ public String getIdentifier() { return identifier; } - public String getProvider() { - return managingProviderName; + public String getProviderId() { + return managingProviderId; } public String toString() { @@ -100,6 +100,13 @@ public String asURL() { } return null; } + + public String asRawIdentifier() { + if (protocol == null || authority == null || identifier == null) { + return ""; + } + return authority + separator + identifier; + } diff --git a/src/main/java/edu/harvard/iq/dataverse/GuestbookResponse.java b/src/main/java/edu/harvard/iq/dataverse/GuestbookResponse.java index 9041ccf887c..1ea7d02791d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/GuestbookResponse.java +++ b/src/main/java/edu/harvard/iq/dataverse/GuestbookResponse.java @@ -17,6 +17,8 @@ import java.util.List; import jakarta.persistence.*; import jakarta.validation.constraints.Size; +import java.util.Collections; +import java.util.Comparator; /** * @@ -178,7 +180,7 @@ public GuestbookResponse(GuestbookResponse source){ this.setSessionId(source.getSessionId()); List customQuestionResponses = new ArrayList<>(); if (!source.getCustomQuestionResponses().isEmpty()){ - for (CustomQuestionResponse customQuestionResponse : source.getCustomQuestionResponses() ){ + for (CustomQuestionResponse customQuestionResponse : source.getCustomQuestionResponsesSorted() ){ CustomQuestionResponse customQuestionResponseAdd = new CustomQuestionResponse(); customQuestionResponseAdd.setResponse(customQuestionResponse.getResponse()); customQuestionResponseAdd.setCustomQuestion(customQuestionResponse.getCustomQuestion()); @@ -254,6 +256,18 @@ public String getResponseDate() { public List getCustomQuestionResponses() { return customQuestionResponses; } + + public List getCustomQuestionResponsesSorted(){ + + Collections.sort(customQuestionResponses, (CustomQuestionResponse cqr1, CustomQuestionResponse cqr2) -> { + int a = cqr1.getCustomQuestion().getDisplayOrder(); + int b = cqr2.getCustomQuestion().getDisplayOrder(); + return Integer.valueOf(a).compareTo(b); + }); + + + return customQuestionResponses; + } public void setCustomQuestionResponses(List customQuestionResponses) { this.customQuestionResponses = customQuestionResponses; @@ -317,7 +331,11 @@ public void setSessionId(String sessionId) { this.sessionId= sessionId; } - public String toHtmlFormattedResponse() { + public String toHtmlFormattedResponse(){ + return toHtmlFormattedResponse(null); + } + + public String toHtmlFormattedResponse(AuthenticatedUser requestor) { StringBuilder sb = new StringBuilder(); @@ -326,17 +344,25 @@ public String toHtmlFormattedResponse() { sb.append(BundleUtil.getStringFromBundle("dataset.guestbookResponse.respondent") + "
    \n
  • " + BundleUtil.getStringFromBundle("name") + ": " + getName() + "
  • \n
  • "); sb.append(" " + BundleUtil.getStringFromBundle("email") + ": " + getEmail() + "
  • \n
  • "); - sb.append( - " " + BundleUtil.getStringFromBundle("institution") + ": " + wrapNullAnswer(getInstitution()) + "
  • \n
  • "); - sb.append(" " + BundleUtil.getStringFromBundle("position") + ": " + wrapNullAnswer(getPosition()) + "
\n"); + sb.append(" " + BundleUtil.getStringFromBundle("institution") + ": " + wrapNullAnswer(getInstitution()) + "\n
  • "); + sb.append(" " + BundleUtil.getStringFromBundle("position") + ": " + wrapNullAnswer(getPosition()) + "
  • "); + + //Add requestor information to response to help dataset admin with request processing + if (requestor != null){ + sb.append("\n
  • " + BundleUtil.getStringFromBundle("dataset.guestbookResponse.requestor.id") + ": " + requestor.getId()+ "
  • "); + sb.append("\n
  • " + BundleUtil.getStringFromBundle("dataset.guestbookResponse.requestor.identifier") + ": " + requestor.getIdentifier()+ "
  • \n"); + } else { + sb.append("\n"); + } + sb.append(BundleUtil.getStringFromBundle("dataset.guestbookResponse.guestbook.additionalQuestions") + ":
      \n"); - for (CustomQuestionResponse cqr : getCustomQuestionResponses()) { + for (CustomQuestionResponse cqr : getCustomQuestionResponsesSorted()) { sb.append("
    • " + BundleUtil.getStringFromBundle("dataset.guestbookResponse.question") + ": " + cqr.getCustomQuestion().getQuestionString() + "
      " + BundleUtil.getStringFromBundle("dataset.guestbookResponse.answer") + ": " - + wrapNullAnswer(cqr.getResponse()) + "
    • \n"); + + wrapNullAnswer(cqr.getResponse()) + "\n
      "); } sb.append("
    "); return sb.toString(); diff --git a/src/main/java/edu/harvard/iq/dataverse/GuestbookResponsesPage.java b/src/main/java/edu/harvard/iq/dataverse/GuestbookResponsesPage.java index c53df93def8..4276eb02882 100644 --- a/src/main/java/edu/harvard/iq/dataverse/GuestbookResponsesPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/GuestbookResponsesPage.java @@ -8,6 +8,7 @@ import edu.harvard.iq.dataverse.engine.command.impl.UpdateDataverseCommand; import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.SystemConfig; import java.util.List; import java.util.logging.Logger; @@ -101,8 +102,9 @@ public String init() { private String getFileName(){ // The fix below replaces any spaces in the name of the dataverse with underscores; // without it, the filename was chopped off (by the browser??), and the user - // was getting the file name "Foo", instead of "Foo and Bar in Social Sciences.csv". -- L.A. - return dataverse.getName().replace(' ', '_') + "_" + guestbook.getId() + "_GuestbookReponses.csv"; + // was getting the file name "Foo", instead of "Foo and Bar in Social Sciences.csv". -- L.A. + // Also removing some chars that have been reported to cause issues with certain browsers + return FileUtil.sanitizeFileName(dataverse.getName() + "_" + guestbook.getId() + "_GuestbookResponses.csv"); } public void streamResponsesByDataverseAndGuestbook(){ diff --git a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java index 72fc6ee6d64..2995c0c5f47 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.branding.BrandingUtil; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean; import edu.harvard.iq.dataverse.dataset.DatasetUtil; +import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.settings.SettingsServiceBean.Key; import edu.harvard.iq.dataverse.util.BundleUtil; @@ -24,11 +25,15 @@ import java.util.Arrays; import java.util.Date; import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.Set; +import java.util.logging.Level; import java.util.logging.Logger; -import jakarta.annotation.Resource; import jakarta.ejb.EJB; import jakarta.ejb.Stateless; +import jakarta.inject.Inject; +import jakarta.inject.Named; import jakarta.mail.Address; import jakarta.mail.Message; import jakarta.mail.MessagingException; @@ -78,77 +83,134 @@ public class MailServiceBean implements java.io.Serializable { */ public MailServiceBean() { } + + /** + * Creates a new instance of MailServiceBean with explicit injection, as used during testing. + */ + public MailServiceBean(Session session, SettingsServiceBean settingsService) { + this.session = session; + this.settingsService = settingsService; + } - @Resource(name = "mail/notifyMailSession") + @Inject + @Named("mail/systemSession") private Session session; public boolean sendSystemEmail(String to, String subject, String messageText) { return sendSystemEmail(to, subject, messageText, false); } - + + /** + * Send a system notification to one or multiple recipients by email. + * Will skip sending when {@link #getSystemAddress()} doesn't return a configured "from" address. + * @param to A comma separated list of one or multiple recipients' addresses. May contain a "personal name" and + * the recipients address in <>. See also {@link InternetAddress}. + * @param subject The message's subject + * @param messageText The message's text + * @param isHtmlContent Determine if the message text is formatted using HTML or plain text. + * @return Status: true if sent successfully, false otherwise + */ public boolean sendSystemEmail(String to, String subject, String messageText, boolean isHtmlContent) { + Optional optionalAddress = getSystemAddress(); + if (optionalAddress.isEmpty()) { + logger.fine(() -> "Skipping sending mail to " + to + ", because no system address has been set."); + return false; + } + InternetAddress systemAddress = optionalAddress.get(); + InternetAddress supportAddress = getSupportAddress().orElse(systemAddress); - boolean sent = false; - InternetAddress systemAddress = getSystemAddress(); - - String body = messageText - + (isHtmlContent ? BundleUtil.getStringFromBundle("notification.email.closing.html", Arrays.asList(BrandingUtil.getSupportTeamEmailAddress(systemAddress), BrandingUtil.getSupportTeamName(systemAddress))) - : BundleUtil.getStringFromBundle("notification.email.closing", Arrays.asList(BrandingUtil.getSupportTeamEmailAddress(systemAddress), BrandingUtil.getSupportTeamName(systemAddress)))); + String body = messageText + + BundleUtil.getStringFromBundle(isHtmlContent ? "notification.email.closing.html" : "notification.email.closing", + List.of(BrandingUtil.getSupportTeamEmailAddress(supportAddress), BrandingUtil.getSupportTeamName(supportAddress))); - logger.fine("Sending email to " + to + ". Subject: <<<" + subject + ">>>. Body: " + body); + logger.fine(() -> "Sending email to %s. Subject: <<<%s>>>. Body: %s".formatted(to, subject, body)); try { + // Since JavaMail 1.6, we have support for UTF-8 mail addresses and do not need to handle these ourselves. + InternetAddress[] recipients = InternetAddress.parse(to); + MimeMessage msg = new MimeMessage(session); - if (systemAddress != null) { - msg.setFrom(systemAddress); - msg.setSentDate(new Date()); - String[] recipientStrings = to.split(","); - InternetAddress[] recipients = new InternetAddress[recipientStrings.length]; - for (int i = 0; i < recipients.length; i++) { - try { - recipients[i] = new InternetAddress(recipientStrings[i], "", charset); - } catch (UnsupportedEncodingException ex) { - logger.severe(ex.getMessage()); - } - } - msg.setRecipients(Message.RecipientType.TO, recipients); - msg.setSubject(subject, charset); - if (isHtmlContent) { - msg.setText(body, charset, "html"); - } else { - msg.setText(body, charset); - } - - try { - Transport.send(msg, recipients); - sent = true; - } catch (MessagingException ssfe) { - logger.warning("Failed to send mail to: " + to); - logger.warning("MessagingException Message: " + ssfe); - } + msg.setFrom(systemAddress); + msg.setSentDate(new Date()); + msg.setRecipients(Message.RecipientType.TO, recipients); + msg.setSubject(subject, charset); + if (isHtmlContent) { + msg.setText(body, charset, "html"); } else { - logger.fine("Skipping sending mail to " + to + ", because the \"no-reply\" address not set (" + Key.SystemEmail + " setting)."); + msg.setText(body, charset); } - } catch (AddressException ae) { - logger.warning("Failed to send mail to " + to); - ae.printStackTrace(System.out); - } catch (MessagingException me) { - logger.warning("Failed to send mail to " + to); - me.printStackTrace(System.out); + + Transport.send(msg, recipients); + return true; + } catch (MessagingException ae) { + logger.log(Level.WARNING, "Failed to send mail to %s: %s".formatted(to, ae.getMessage()), ae); + logger.info("When UTF-8 characters in recipients: make sure MTA supports it and JVM option " + JvmSettings.MAIL_MTA_SUPPORT_UTF8.getScopedKey() + "=true"); } - return sent; + return false; } - - public InternetAddress getSystemAddress() { - String systemEmail = settingsService.getValueForKey(Key.SystemEmail); - return MailUtil.parseSystemAddress(systemEmail); + + /** + * Lookup the system mail address ({@code InternetAddress} may contain personal and actual address). + * @return The system mail address or an empty {@code Optional} if not configured. + */ + public Optional getSystemAddress() { + boolean providedByDB = false; + String mailAddress = JvmSettings.SYSTEM_EMAIL.lookupOptional().orElse(null); + + // Try lookup of (deprecated) database setting only if not configured via MPCONFIG + if (mailAddress == null) { + mailAddress = settingsService.getValueForKey(Key.SystemEmail); + // Encourage people to migrate from deprecated setting + if (mailAddress != null) { + providedByDB = true; + logger.warning("The :SystemMail DB setting has been deprecated, please reconfigure using JVM option " + JvmSettings.SYSTEM_EMAIL.getScopedKey()); + } + } + + try { + // Parse and return. + return Optional.of(new InternetAddress(Objects.requireNonNull(mailAddress), true)); + } catch (AddressException e) { + logger.log(Level.WARNING, "Could not parse system mail address '%s' provided by %s: " + .formatted(providedByDB ? "DB setting" : "JVM option", mailAddress), e); + } catch (NullPointerException e) { + // Do not pester the logs - no configuration may mean someone wants to disable mail notifications + logger.fine("Could not find a system mail setting in database (key :SystemEmail, deprecated) or JVM option '" + JvmSettings.SYSTEM_EMAIL.getScopedKey() + "'"); + } + // We define the system email address as an optional setting, in case people do not want to enable mail + // notifications (like in a development context, but might be useful elsewhere, too). + return Optional.empty(); + } + + /** + * Lookup the support team mail address ({@code InternetAddress} may contain personal and actual address). + * Will default to return {@code #getSystemAddress} if not configured. + * @return Support team mail address + */ + public Optional getSupportAddress() { + Optional supportMailAddress = JvmSettings.SUPPORT_EMAIL.lookupOptional(); + if (supportMailAddress.isPresent()) { + try { + return Optional.of(new InternetAddress(supportMailAddress.get(), true)); + } catch (AddressException e) { + logger.log(Level.WARNING, "Could not parse support mail address '%s', defaulting to system address: ".formatted(supportMailAddress.get()), e); + } + } + return getSystemAddress(); } //@Resource(name="mail/notifyMailSession") public void sendMail(String reply, String to, String cc, String subject, String messageText) { + Optional optionalAddress = getSystemAddress(); + if (optionalAddress.isEmpty()) { + logger.fine(() -> "Skipping sending mail to " + to + ", because no system address has been set."); + return; + } + // Always send from system address to avoid email being blocked + InternetAddress fromAddress = optionalAddress.get(); + try { MimeMessage msg = new MimeMessage(session); - // Always send from system address to avoid email being blocked - InternetAddress fromAddress = getSystemAddress(); + try { setContactDelegation(reply, fromAddress); } catch (UnsupportedEncodingException ex) { @@ -395,7 +457,7 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio GuestbookResponse gbr = far.getGuestbookResponse(); if (gbr != null) { messageText += MessageFormat.format( - BundleUtil.getStringFromBundle("notification.email.requestFileAccess.guestbookResponse"), gbr.toHtmlFormattedResponse()); + BundleUtil.getStringFromBundle("notification.email.requestFileAccess.guestbookResponse"), gbr.toHtmlFormattedResponse(requestor)); } return messageText; case GRANTFILEACCESS: @@ -466,18 +528,24 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio case RETURNEDDS: version = (DatasetVersion) targetObject; pattern = BundleUtil.getStringFromBundle("notification.email.wasReturnedByReviewer"); - String optionalReturnReason = ""; - /* - FIXME - Setting up to add single comment when design completed - optionalReturnReason = "."; - if (comment != null && !comment.isEmpty()) { - optionalReturnReason = ".\n\n" + BundleUtil.getStringFromBundle("wasReturnedReason") + "\n\n" + comment; - } - */ + String[] paramArrayReturnedDataset = {version.getDataset().getDisplayName(), getDatasetDraftLink(version.getDataset()), - version.getDataset().getOwner().getDisplayName(), getDataverseLink(version.getDataset().getOwner()), optionalReturnReason}; + version.getDataset().getOwner().getDisplayName(), getDataverseLink(version.getDataset().getOwner())}; messageText += MessageFormat.format(pattern, paramArrayReturnedDataset); + + if (comment != null && !comment.isEmpty()) { + messageText += "\n\n" + MessageFormat.format(BundleUtil.getStringFromBundle("notification.email.wasReturnedByReviewerReason"), comment); + } + + Dataverse d = (Dataverse) version.getDataset().getOwner(); + List contactEmailList = new ArrayList(); + for (DataverseContact dc : d.getDataverseContacts()) { + contactEmailList.add(dc.getContactEmail()); + } + if (!contactEmailList.isEmpty()) { + String contactEmails = String.join(", ", contactEmailList); + messageText += "\n\n" + MessageFormat.format(BundleUtil.getStringFromBundle("notification.email.wasReturnedByReviewer.collectionContacts"), contactEmails); + } return messageText; case WORKFLOW_SUCCESS: @@ -505,13 +573,12 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio messageText += MessageFormat.format(pattern, paramArrayStatus); return messageText; case CREATEACC: - InternetAddress systemAddress = getSystemAddress(); String accountCreatedMessage = BundleUtil.getStringFromBundle("notification.email.welcome", Arrays.asList( BrandingUtil.getInstallationBrandName(), systemConfig.getGuidesBaseUrl(), systemConfig.getGuidesVersion(), - BrandingUtil.getSupportTeamName(systemAddress), - BrandingUtil.getSupportTeamEmailAddress(systemAddress) + BrandingUtil.getSupportTeamName(getSystemAddress().orElse(null)), + BrandingUtil.getSupportTeamEmailAddress(getSystemAddress().orElse(null)) )); String optionalConfirmEmailAddon = confirmEmailService.optionalConfirmEmailAddonMsg(userNotification.getUser()); accountCreatedMessage += optionalConfirmEmailAddon; @@ -557,6 +624,7 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio comment )) ; return downloadCompletedMessage; + case GLOBUSUPLOADCOMPLETEDWITHERRORS: dataset = (Dataset) targetObject; messageText = BundleUtil.getStringFromBundle("notification.email.greeting.html"); @@ -567,8 +635,30 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio comment )) ; return uploadCompletedWithErrorsMessage; + + case GLOBUSUPLOADREMOTEFAILURE: + dataset = (Dataset) targetObject; + messageText = BundleUtil.getStringFromBundle("notification.email.greeting.html"); + String uploadFailedRemotelyMessage = messageText + BundleUtil.getStringFromBundle("notification.mail.globus.upload.failedRemotely", Arrays.asList( + systemConfig.getDataverseSiteUrl(), + dataset.getGlobalId().asString(), + dataset.getDisplayName(), + comment + )) ; + return uploadFailedRemotelyMessage; - case GLOBUSDOWNLOADCOMPLETEDWITHERRORS: + case GLOBUSUPLOADLOCALFAILURE: + dataset = (Dataset) targetObject; + messageText = BundleUtil.getStringFromBundle("notification.email.greeting.html"); + String uploadFailedLocallyMessage = messageText + BundleUtil.getStringFromBundle("notification.mail.globus.upload.failedLocally", Arrays.asList( + systemConfig.getDataverseSiteUrl(), + dataset.getGlobalId().asString(), + dataset.getDisplayName(), + comment + )) ; + return uploadFailedLocallyMessage; + + case GLOBUSDOWNLOADCOMPLETEDWITHERRORS: dataset = (Dataset) targetObject; messageText = BundleUtil.getStringFromBundle("notification.email.greeting.html"); String downloadCompletedWithErrorsMessage = messageText + BundleUtil.getStringFromBundle("notification.mail.globus.download.completedWithErrors", Arrays.asList( @@ -697,6 +787,8 @@ public Object getObjectOfNotification (UserNotification userNotification){ return versionService.find(userNotification.getObjectId()); case GLOBUSUPLOADCOMPLETED: case GLOBUSUPLOADCOMPLETEDWITHERRORS: + case GLOBUSUPLOADREMOTEFAILURE: + case GLOBUSUPLOADLOCALFAILURE: case GLOBUSDOWNLOADCOMPLETED: case GLOBUSDOWNLOADCOMPLETEDWITHERRORS: return datasetService.find(userNotification.getObjectId()); diff --git a/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java b/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java index ca2f6145cba..1ead0b13cdc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java @@ -422,7 +422,10 @@ public void grantAccess(ActionEvent evt) { // set request(s) granted, if they exist for (AuthenticatedUser au : roleAssigneeService.getExplicitUsers(roleAssignee)) { FileAccessRequest far = file.getAccessRequestForAssignee(au); - far.setStateGranted(); + //There may not be a request, so do the null check + if (far != null) { + far.setStateGranted(); + } } datafileService.save(file); } diff --git a/src/main/java/edu/harvard/iq/dataverse/ManageGuestbooksPage.java b/src/main/java/edu/harvard/iq/dataverse/ManageGuestbooksPage.java index cc89cfd9d56..d1cc515fd01 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ManageGuestbooksPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/ManageGuestbooksPage.java @@ -5,6 +5,7 @@ import edu.harvard.iq.dataverse.engine.command.impl.UpdateDataverseCommand; import edu.harvard.iq.dataverse.engine.command.impl.UpdateDataverseGuestbookRootCommand; import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.JsfHelper; import static edu.harvard.iq.dataverse.util.JsfHelper.JH; import java.util.LinkedList; @@ -220,7 +221,8 @@ private String getFileName(){ // The fix below replaces any spaces in the name of the dataverse with underscores; // without it, the filename was chopped off (by the browser??), and the user // was getting the file name "Foo", instead of "Foo and Bar in Social Sciences.csv". -- L.A. - return dataverse.getName().replace(' ', '_') + "_GuestbookReponses.csv"; + // Also removing some chars that have been reported to cause issues with certain browsers + return FileUtil.sanitizeFileName(dataverse.getName() + "_GuestbookResponses.csv"); } public void deleteGuestbook() { diff --git a/src/main/java/edu/harvard/iq/dataverse/MetadataBlockServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MetadataBlockServiceBean.java index bb6daa264ba..1e2a34f5472 100644 --- a/src/main/java/edu/harvard/iq/dataverse/MetadataBlockServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/MetadataBlockServiceBean.java @@ -1,43 +1,82 @@ package edu.harvard.iq.dataverse; -import java.util.List; import jakarta.ejb.Stateless; import jakarta.inject.Named; import jakarta.persistence.EntityManager; import jakarta.persistence.NoResultException; import jakarta.persistence.PersistenceContext; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.*; + +import java.util.List; /** - * * @author michael */ @Stateless @Named public class MetadataBlockServiceBean { - + @PersistenceContext(unitName = "VDCNet-ejbPU") private EntityManager em; - + public MetadataBlock save(MetadataBlock mdb) { - return em.merge(mdb); - } - - + return em.merge(mdb); + } + public List listMetadataBlocks() { + return listMetadataBlocks(false); + } + + public List listMetadataBlocks(boolean onlyDisplayedOnCreate) { + if (onlyDisplayedOnCreate) { + return listMetadataBlocksDisplayedOnCreate(null); + } return em.createNamedQuery("MetadataBlock.listAll", MetadataBlock.class).getResultList(); } - - public MetadataBlock findById( Long id ) { + + public MetadataBlock findById(Long id) { return em.find(MetadataBlock.class, id); } - - public MetadataBlock findByName( String name ) { + + public MetadataBlock findByName(String name) { try { return em.createNamedQuery("MetadataBlock.findByName", MetadataBlock.class) - .setParameter("name", name) - .getSingleResult(); - } catch ( NoResultException nre ) { + .setParameter("name", name) + .getSingleResult(); + } catch (NoResultException nre) { return null; } } + + public List listMetadataBlocksDisplayedOnCreate(Dataverse ownerDataverse) { + CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder(); + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(MetadataBlock.class); + Root metadataBlockRoot = criteriaQuery.from(MetadataBlock.class); + Join datasetFieldTypeJoin = metadataBlockRoot.join("datasetFieldTypes"); + Predicate displayOnCreatePredicate = criteriaBuilder.isTrue(datasetFieldTypeJoin.get("displayOnCreate")); + + if (ownerDataverse != null) { + Root dataverseRoot = criteriaQuery.from(Dataverse.class); + Join datasetFieldTypeInputLevelJoin = dataverseRoot.join("dataverseFieldTypeInputLevels", JoinType.LEFT); + + Predicate requiredPredicate = criteriaBuilder.and( + datasetFieldTypeInputLevelJoin.get("datasetFieldType").in(metadataBlockRoot.get("datasetFieldTypes")), + criteriaBuilder.isTrue(datasetFieldTypeInputLevelJoin.get("required"))); + + Predicate unionPredicate = criteriaBuilder.or(displayOnCreatePredicate, requiredPredicate); + + criteriaQuery.where(criteriaBuilder.and( + criteriaBuilder.equal(dataverseRoot.get("id"), ownerDataverse.getId()), + metadataBlockRoot.in(dataverseRoot.get("metadataBlocks")), + unionPredicate + )); + } else { + criteriaQuery.where(displayOnCreatePredicate); + } + + criteriaQuery.select(metadataBlockRoot).distinct(true); + TypedQuery typedQuery = em.createQuery(criteriaQuery); + return typedQuery.getResultList(); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/NavigationWrapper.java b/src/main/java/edu/harvard/iq/dataverse/NavigationWrapper.java index 832d7ec19ef..54fb8f211a6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/NavigationWrapper.java +++ b/src/main/java/edu/harvard/iq/dataverse/NavigationWrapper.java @@ -16,6 +16,7 @@ import java.util.logging.Logger; import jakarta.faces.context.FacesContext; import jakarta.faces.view.ViewScoped; +import jakarta.ws.rs.core.Response.Status; import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.servlet.http.HttpServletRequest; @@ -87,6 +88,10 @@ public String notAuthorized(){ } } + public String tooManyRequests() { + return sendError(Status.TOO_MANY_REQUESTS.getStatusCode()); + } + public String notFound() { return sendError(HttpServletResponse.SC_NOT_FOUND); } diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index 8fb762e3e5b..a389cbc735b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -97,6 +97,9 @@ public class PermissionServiceBean { @Inject DataverseRequestServiceBean dvRequestService; + @Inject + DatasetVersionFilesServiceBean datasetVersionFilesServiceBean; + /** * A request-level permission query (e.g includes IP ras). */ @@ -442,23 +445,14 @@ private Set getInferredPermissions(DvObject dvo) { * download permission for everybody: */ private boolean isPublicallyDownloadable(DvObject dvo) { - if (dvo instanceof DataFile) { + if (dvo instanceof DataFile df) { // unrestricted files that are part of a release dataset // automatically get download permission for everybody: // -- L.A. 4.0 beta12 - - DataFile df = (DataFile) dvo; - if (!df.isRestricted()) { - if (df.getOwner().getReleasedVersion() != null) { - List fileMetadatas = df.getOwner().getReleasedVersion().getFileMetadatas(); - if (fileMetadatas != null) { - for (FileMetadata fm : fileMetadatas) { - if (df.equals(fm.getDataFile())) { - return true; - } - } - } + DatasetVersion releasedVersion = df.getOwner().getReleasedVersion(); + if (releasedVersion != null) { + return datasetVersionFilesServiceBean.isDataFilePresentInDatasetVersion(releasedVersion, df); } } } diff --git a/src/main/java/edu/harvard/iq/dataverse/Retention.java b/src/main/java/edu/harvard/iq/dataverse/Retention.java new file mode 100644 index 00000000000..e1bd2231570 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/Retention.java @@ -0,0 +1,102 @@ +package edu.harvard.iq.dataverse; + +import edu.harvard.iq.dataverse.util.BundleUtil; +import jakarta.persistence.*; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Objects; + +@NamedQueries({ + @NamedQuery( name="Retention.findAll", + query = "SELECT r FROM Retention r"), + @NamedQuery( name="Retention.findById", + query = "SELECT r FROM Retention r WHERE r.id=:id"), + @NamedQuery( name="Retention.findByDateUnavailable", + query = "SELECT r FROM Retention r WHERE r.dateUnavailable=:dateUnavailable"), + @NamedQuery( name="Retention.deleteById", + query = "DELETE FROM Retention r WHERE r.id=:id") +}) +@Entity +public class Retention { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private LocalDate dateUnavailable; + + @Column(columnDefinition="TEXT") + private String reason; + + @OneToMany(mappedBy="retention", cascade={ CascadeType.REMOVE, CascadeType.PERSIST}) + private List dataFiles; + + public Retention(){ + dateUnavailable = LocalDate.now().plusYears(1000); // Most likely valid with respect to configuration + } + + public Retention(LocalDate dateUnavailable, String reason) { + this.dateUnavailable = dateUnavailable; + this.reason = reason; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public LocalDate getDateUnavailable() { + return dateUnavailable; + } + + public void setDateUnavailable(LocalDate dateUnavailable) { + this.dateUnavailable = dateUnavailable; + } + + public String getFormattedDateUnavailable() { + return getDateUnavailable().format(DateTimeFormatter.ISO_LOCAL_DATE.withLocale(BundleUtil.getCurrentLocale())); + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + public List getDataFiles() { + return dataFiles; + } + + public void setDataFiles(List dataFiles) { + this.dataFiles = dataFiles; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Retention retention = (Retention) o; + return id.equals(retention.id) && dateUnavailable.equals(retention.dateUnavailable) && Objects.equals(reason, retention.reason); + } + + @Override + public int hashCode() { + return Objects.hash(id, dateUnavailable, reason); + } + + @Override + public String toString() { + return "Retention{" + + "id=" + id + + ", dateUnavailable=" + dateUnavailable + + ", reason='" + reason + '\'' + + '}'; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/RetentionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/RetentionServiceBean.java new file mode 100644 index 00000000000..1421ac61120 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/RetentionServiceBean.java @@ -0,0 +1,66 @@ +package edu.harvard.iq.dataverse; + +import edu.harvard.iq.dataverse.actionlogging.ActionLogRecord; +import edu.harvard.iq.dataverse.actionlogging.ActionLogServiceBean; +import jakarta.ejb.EJB; +import jakarta.ejb.Stateless; +import jakarta.inject.Named; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; + +import java.util.List; + + +@Stateless +@Named +public class RetentionServiceBean { + + @PersistenceContext + EntityManager em; + + @EJB + ActionLogServiceBean actionLogSvc; + + public List findAllRetentions() { + return em.createNamedQuery("Retention.findAll", Retention.class).getResultList(); + } + + public Retention findByRetentionId(Long id) { + Query query = em.createNamedQuery("Retention.findById", Retention.class); + query.setParameter("id", id); + try { + return (Retention) query.getSingleResult(); + } catch (Exception ex) { + return null; + } + } + + public Retention merge(Retention r) { + return em.merge(r); + } + + public Long save(Retention retention, String userIdentifier) { + if (retention.getId() == null) { + em.persist(retention); + em.flush(); + } + //Not quite from a command, but this action can be done by anyone, so command seems better than Admin or other alternatives + actionLogSvc.log(new ActionLogRecord(ActionLogRecord.ActionType.Command, "retentionCreate") + .setInfo("id: " + retention.getId() + " date unavailable: " + retention.getDateUnavailable() + " reason: " + retention.getReason()).setUserIdentifier(userIdentifier)); + return retention.getId(); + } + + private int deleteById(long id, String userIdentifier) { + //Not quite from a command, but this action can be done by anyone, so command seems better than Admin or other alternatives + actionLogSvc.log(new ActionLogRecord(ActionLogRecord.ActionType.Command, "retentionDelete") + .setInfo(Long.toString(id)) + .setUserIdentifier(userIdentifier)); + return em.createNamedQuery("Retention.deleteById") + .setParameter("id", id) + .executeUpdate(); + } + public int delete(Retention retention, String userIdentifier) { + return deleteById(retention.getId(), userIdentifier); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/S3PackageImporter.java b/src/main/java/edu/harvard/iq/dataverse/S3PackageImporter.java index 71318a0184a..a387b27d98b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/S3PackageImporter.java +++ b/src/main/java/edu/harvard/iq/dataverse/S3PackageImporter.java @@ -17,6 +17,7 @@ import com.amazonaws.services.s3.model.S3Object; import com.amazonaws.services.s3.model.S3ObjectSummary; import edu.harvard.iq.dataverse.api.AbstractApiBean; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.FileUtil; import java.io.BufferedReader; @@ -203,35 +204,21 @@ public DataFile createPackageDataFile(Dataset dataset, String folderName, long t fmd.setDatasetVersion(dataset.getLatestVersion()); FileUtil.generateS3PackageStorageIdentifier(packageFile); - - GlobalIdServiceBean idServiceBean = GlobalIdServiceBean.getBean(packageFile.getProtocol(), commandEngine.getContext()); + PidProvider pidProvider = commandEngine.getContext().dvObjects().getEffectivePidGenerator(dataset); if (packageFile.getIdentifier() == null || packageFile.getIdentifier().isEmpty()) { - String packageIdentifier = idServiceBean.generateDataFileIdentifier(packageFile); - packageFile.setIdentifier(packageIdentifier); - } - - String nonNullDefaultIfKeyNotFound = ""; - String protocol = commandEngine.getContext().settings().getValueForKey(SettingsServiceBean.Key.Protocol, nonNullDefaultIfKeyNotFound); - String authority = commandEngine.getContext().settings().getValueForKey(SettingsServiceBean.Key.Authority, nonNullDefaultIfKeyNotFound); - - if (packageFile.getProtocol() == null) { - packageFile.setProtocol(protocol); - } - if (packageFile.getAuthority() == null) { - packageFile.setAuthority(authority); + pidProvider.generatePid(packageFile); } if (!packageFile.isIdentifierRegistered()) { String doiRetString = ""; - idServiceBean = GlobalIdServiceBean.getBean(commandEngine.getContext()); try { - doiRetString = idServiceBean.createIdentifier(packageFile); + doiRetString = pidProvider.createIdentifier(packageFile); } catch (Throwable e) { } // Check return value to make sure registration succeeded - if (!idServiceBean.registerWhenPublished() && doiRetString.contains(packageFile.getIdentifier())) { + if (!pidProvider.registerWhenPublished() && doiRetString.contains(packageFile.getIdentifier())) { packageFile.setIdentifierRegistered(true); packageFile.setGlobalIdCreateTime(new Date()); } diff --git a/src/main/java/edu/harvard/iq/dataverse/SendFeedbackDialog.java b/src/main/java/edu/harvard/iq/dataverse/SendFeedbackDialog.java index 68912969003..46941c8b5b6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/SendFeedbackDialog.java +++ b/src/main/java/edu/harvard/iq/dataverse/SendFeedbackDialog.java @@ -7,7 +7,6 @@ import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.JsfHelper; -import edu.harvard.iq.dataverse.util.MailUtil; import edu.harvard.iq.dataverse.util.SystemConfig; import java.util.Optional; import java.util.Random; @@ -102,8 +101,7 @@ public void initUserInput(ActionEvent ae) { op1 = Long.valueOf(random.nextInt(10)); op2 = Long.valueOf(random.nextInt(10)); userSum = null; - String supportEmail = JvmSettings.SUPPORT_EMAIL.lookupOptional().orElse(settingsService.getValueForKey(SettingsServiceBean.Key.SystemEmail)); - systemAddress = MailUtil.parseSystemAddress(supportEmail); + systemAddress = mailService.getSupportAddress().orElse(null); } public Long getOp1() { @@ -131,6 +129,10 @@ public void setUserSum(Long userSum) { } public String getMessageTo() { + if (op1 == null || op2 == null) { + // Fix for 403 error page: initUserInput method doesn't call before + initUserInput(null); + } if (feedbackTarget == null) { return BrandingUtil.getSupportTeamName(systemAddress); } else if (feedbackTarget.isInstanceofDataverse()) { diff --git a/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java b/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java index 8ab1e87aef2..222d2881cd2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java +++ b/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java @@ -6,9 +6,9 @@ package edu.harvard.iq.dataverse; import edu.harvard.iq.dataverse.branding.BrandingUtil; -import edu.harvard.iq.dataverse.dataaccess.AbstractRemoteOverlayAccessIO; import edu.harvard.iq.dataverse.dataaccess.DataAccess; import edu.harvard.iq.dataverse.dataaccess.GlobusAccessibleStore; +import edu.harvard.iq.dataverse.dataaccess.StorageIO; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.settings.Setting; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; @@ -66,6 +66,9 @@ public class SettingsWrapper implements java.io.Serializable { @EJB MetadataBlockServiceBean mdbService; + + @EJB + MailServiceBean mailServiceBean; private Map settingsMap; @@ -75,6 +78,9 @@ public class SettingsWrapper implements java.io.Serializable { private boolean embargoDateChecked = false; private LocalDate maxEmbargoDate = null; + private boolean retentionDateChecked = false; + private LocalDate minRetentionDate = null; + private String siteUrl = null; private Dataverse rootDataverse = null; @@ -92,6 +98,7 @@ public class SettingsWrapper implements java.io.Serializable { //External Vocabulary support private Map cachedCvocMap = null; private Map cachedCvocByTermFieldMap = null; + private Set cvocFieldSet; private Long zipDownloadLimit = null; @@ -299,14 +306,16 @@ public boolean isPublicInstall(){ } return publicInstall; } - + + @Deprecated(forRemoval = true, since = "2024-07-07") public boolean isRsyncUpload() { if (rsyncUpload == null) { rsyncUpload = getUploadMethodAvailable(SystemConfig.FileUploadMethods.RSYNC.toString()); } return rsyncUpload; } - + + @Deprecated(forRemoval = true, since = "2024-07-07") public boolean isRsyncDownload() { if (rsyncDownload == null) { rsyncDownload = systemConfig.isRsyncDownload(); @@ -344,7 +353,7 @@ public boolean isDownloadable(FileMetadata fmd) { if(isGlobusFileDownload()) { String driverId = DataAccess.getStorageDriverFromIdentifier(fmd.getDataFile().getStorageIdentifier()); - downloadable = downloadable && !AbstractRemoteOverlayAccessIO.isNotDataverseAccessible(driverId); + downloadable = downloadable && StorageIO.isDataverseAccessible(driverId); } return downloadable; } @@ -373,7 +382,8 @@ public boolean isWebloaderUpload() { } return webloaderUpload; } - + + @Deprecated(forRemoval = true, since = "2024-07-07") public boolean isRsyncOnly() { if (rsyncOnly == null) { String downloadMethods = getValueForKey(SettingsServiceBean.Key.DownloadMethods); @@ -392,7 +402,7 @@ public boolean isRsyncOnly() { } return rsyncOnly; } - + public boolean isHTTPUpload(){ if (httpUpload == null) { httpUpload = getUploadMethodAvailable(SystemConfig.FileUploadMethods.NATIVE.toString()); @@ -400,22 +410,15 @@ public boolean isHTTPUpload(){ return httpUpload; } - public boolean isDataFilePIDSequentialDependent(){ - if (dataFilePIDSequentialDependent == null) { - dataFilePIDSequentialDependent = systemConfig.isDataFilePIDSequentialDependent(); - } - return dataFilePIDSequentialDependent; - } - public String getSupportTeamName() { - String systemEmail = getValueForKey(SettingsServiceBean.Key.SystemEmail); - InternetAddress systemAddress = MailUtil.parseSystemAddress(systemEmail); + // TODO: should this be replaced with mailServiceBean.getSupportAddress() to expose a configured support team? + InternetAddress systemAddress = mailServiceBean.getSystemAddress().orElse(null); return BrandingUtil.getSupportTeamName(systemAddress); } public String getSupportTeamEmail() { - String systemEmail = getValueForKey(SettingsServiceBean.Key.SystemEmail); - InternetAddress systemAddress = MailUtil.parseSystemAddress(systemEmail); + // TODO: should this be replaced with mailServiceBean.getSupportAddress() to expose a configured support team? + InternetAddress systemAddress = mailServiceBean.getSystemAddress().orElse(null); return BrandingUtil.getSupportTeamEmailAddress(systemAddress) != null ? BrandingUtil.getSupportTeamEmailAddress(systemAddress) : BrandingUtil.getSupportTeamName(systemAddress); } @@ -470,23 +473,6 @@ public Map getConfiguredLocales() { return configuredLocales; } - public boolean isDoiInstallation() { - String protocol = getValueForKey(SettingsServiceBean.Key.Protocol); - if ("doi".equals(protocol)) { - return true; - } else { - return false; - } - } - - public boolean isDataCiteInstallation() { - String protocol = getValueForKey(SettingsServiceBean.Key.DoiProvider); - if ("DataCite".equals(protocol)) { - return true; - } else { - return false; - } - } public boolean isMakeDataCountDisplayEnabled() { boolean safeDefaultIfKeyNotFound = (getValueForKey(SettingsServiceBean.Key.MDCLogPath)!=null); //Backward compatible @@ -603,6 +589,89 @@ public void validateEmbargoDate(FacesContext context, UIComponent component, Obj } } + public LocalDate getMinRetentionDate() { + if (!retentionDateChecked) { + String months = getValueForKey(Key.MinRetentionDurationInMonths); + Long minMonths = null; + if (months != null) { + try { + minMonths = Long.parseLong(months); + } catch (NumberFormatException nfe) { + logger.warning("Cant interpret :MinRetentionDurationInMonths as a long"); + } + } + + if (minMonths != null && minMonths != 0) { + if (minMonths == -1) { + minMonths = 0l; // Absolute minimum is 0 + } + minRetentionDate = LocalDate.now().plusMonths(minMonths); + } + retentionDateChecked = true; + } + return minRetentionDate; + } + + public LocalDate getMaxRetentionDate() { + Long maxMonths = 12000l; // Arbitrary cutoff at 1000 years - needs to keep maxDate < year 999999999 and + // somehwere 1K> x >10K years the datepicker widget stops showing a popup + // calendar + return LocalDate.now().plusMonths(maxMonths); + } + + public boolean isValidRetentionDate(Retention r) { + + if (r.getDateUnavailable()==null || + isRetentionAllowed() && r.getDateUnavailable().isAfter(getMinRetentionDate())) { + return true; + } + + return false; + } + + public boolean isRetentionAllowed() { + //Need a valid :MinRetentionDurationInMonths setting to allow retentions + return getMinRetentionDate()!=null; + } + + public void validateRetentionDate(FacesContext context, UIComponent component, Object value) + throws ValidatorException { + if (isRetentionAllowed()) { + UIComponent cb = component.findComponent("retentionCheckbox"); + UIInput endComponent = (UIInput) cb; + boolean removedState = false; + if (endComponent != null) { + try { + removedState = (Boolean) endComponent.getSubmittedValue(); + } catch (NullPointerException npe) { + // Do nothing - checkbox is not being shown (and is therefore not checked) + } + } + if (!removedState && value == null) { + String msgString = BundleUtil.getStringFromBundle("retention.date.required"); + FacesMessage msg = new FacesMessage(msgString); + msg.setSeverity(FacesMessage.SEVERITY_ERROR); + throw new ValidatorException(msg); + } + Retention newR = new Retention(((LocalDate) value), null); + if (!isValidRetentionDate(newR)) { + String minDate = getMinRetentionDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + String maxDate = getMaxRetentionDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + String msgString = BundleUtil.getStringFromBundle("retention.date.invalid", + Arrays.asList(minDate, maxDate)); + // If we don't throw an exception here, the datePicker will use it's own + // vaidator and display a default message. The value for that can be set by + // adding validatorMessage="#{bundle['retention.date.invalid']}" (a version with + // no params) to the datepicker + // element in file-edit-popup-fragment.html, but it would be better to catch all + // problems here (so we can show a message with the min/max dates). + FacesMessage msg = new FacesMessage(msgString); + msg.setSeverity(FacesMessage.SEVERITY_ERROR); + throw new ValidatorException(msg); + } + } + } + Map languageMap = null; public Map getBaseMetadataLanguageMap(boolean refresh) { @@ -738,6 +807,17 @@ public Map getCVocConf(boolean byTermField) { } } + public boolean isCvocField(Long fieldId) { + + if(cvocFieldSet == null) { + cvocFieldSet = fieldService.getCvocFieldSet(); + } + if(cvocFieldSet == null) { + return false; + } + return cvocFieldSet.contains(fieldId); + } + public String getMetricsUrl() { if (metricsUrl == null) { metricsUrl = getValueForKey(SettingsServiceBean.Key.MetricsUrl); diff --git a/src/main/java/edu/harvard/iq/dataverse/Shib.java b/src/main/java/edu/harvard/iq/dataverse/Shib.java index 24c0f9d7926..a3dfbf81512 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Shib.java +++ b/src/main/java/edu/harvard/iq/dataverse/Shib.java @@ -19,6 +19,7 @@ import org.apache.commons.lang3.StringUtils; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; @@ -59,6 +60,8 @@ public class Shib implements java.io.Serializable { SettingsServiceBean settingsService; @EJB SystemConfig systemConfig; + @EJB + UserServiceBean userService; HttpServletRequest request; @@ -259,6 +262,7 @@ else if (ShibAffiliationOrder.equals("firstAffiliation")) { state = State.REGULAR_LOGIN_INTO_EXISTING_SHIB_ACCOUNT; logger.fine("Found user based on " + userPersistentId + ". Logging in."); logger.fine("Updating display info for " + au.getName()); + userService.updateLastLogin(au); authSvc.updateAuthenticatedUser(au, displayInfo); logInUserAndSetShibAttributes(au); String prettyFacesHomePageString = getPrettyFacesHomePageString(false); @@ -455,9 +459,9 @@ private String getRequiredValueFromAssertion(String key) throws Exception { if (attributeValue.isEmpty()) { throw new Exception(key + " was empty"); } - if(systemConfig.isShibAttributeCharacterSetConversionEnabled()) { - attributeValue= new String( attributeValue.getBytes("ISO-8859-1"), "UTF-8"); - } + if (systemConfig.isShibAttributeCharacterSetConversionEnabled()) { + attributeValue= new String( attributeValue.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8); + } String trimmedValue = attributeValue.trim(); logger.fine("The SAML assertion for \"" + key + "\" (required) was \"" + attributeValue + "\" and was trimmed to \"" + trimmedValue + "\"."); return trimmedValue; diff --git a/src/main/java/edu/harvard/iq/dataverse/TermsOfUseAndAccess.java b/src/main/java/edu/harvard/iq/dataverse/TermsOfUseAndAccess.java index ee865770dbe..9e48c6c0165 100644 --- a/src/main/java/edu/harvard/iq/dataverse/TermsOfUseAndAccess.java +++ b/src/main/java/edu/harvard/iq/dataverse/TermsOfUseAndAccess.java @@ -17,6 +17,28 @@ import jakarta.persistence.Transient; import edu.harvard.iq.dataverse.license.License; +import jakarta.persistence.NamedQueries; +import jakarta.persistence.NamedQuery; + +@NamedQueries({ + // TermsOfUseAndAccess.findByDatasetVersionIdAndDefaultTerms + // is used to determine if the dataset terms were set by the multi license support update + // as part of the 5.10 release. + + @NamedQuery(name = "TermsOfUseAndAccess.findByDatasetVersionIdAndDefaultTerms", + query = "SELECT o FROM TermsOfUseAndAccess o, DatasetVersion dv WHERE " + + "dv.id =:id " + + "AND dv.termsOfUseAndAccess.id = o.id " + + "AND o.termsOfUse =:defaultTerms " + + "AND o.confidentialityDeclaration IS null " + + "AND o.specialPermissions IS null " + + "AND o.restrictions IS null " + + "AND o.citationRequirements IS null " + + "AND o.depositorRequirements IS null " + + "AND o.conditions IS null " + + "AND o.disclaimer IS null " + ) +}) /** * @@ -26,6 +48,8 @@ @Entity @ValidateTermsOfUseAndAccess public class TermsOfUseAndAccess implements Serializable { + + public static final String DEFAULT_NOTERMS = "This dataset is made available without information on how it can be used. You should communicate with the Contact(s) specified before use."; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/edu/harvard/iq/dataverse/ThemeWidgetFragment.java b/src/main/java/edu/harvard/iq/dataverse/ThemeWidgetFragment.java index f30051e26ae..2474b43183f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ThemeWidgetFragment.java +++ b/src/main/java/edu/harvard/iq/dataverse/ThemeWidgetFragment.java @@ -16,7 +16,6 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.logging.Level; import java.util.logging.Logger; @@ -33,7 +32,6 @@ import org.apache.commons.lang3.StringUtils; import org.primefaces.PrimeFaces; -//import org.primefaces.context.RequestContext; import org.primefaces.event.FileUploadEvent; import org.primefaces.model.file.UploadedFile; @@ -56,6 +54,7 @@ public class ThemeWidgetFragment implements java.io.Serializable { private File tempDir; private File uploadedFile; + private File uploadedFileThumbnail; private File uploadedFileFooter; private Dataverse editDv= new Dataverse(); private HtmlInputText linkUrlInput; @@ -124,6 +123,7 @@ public void cleanupTempDirectory() { throw new RuntimeException("Error deleting temp directory", e); // improve error handling } uploadedFile=null; + uploadedFileThumbnail=null; uploadedFileFooter=null; tempDir=null; } @@ -213,21 +213,46 @@ public boolean uploadExists() { return uploadedFile!=null; } + public boolean uploadExistsThumbnail() { + return uploadedFileThumbnail != null; + } + public boolean uploadExistsFooter() { return uploadedFileFooter!=null; } + public void handleImageThumbnailFileUpload(FileUploadEvent event) { + logger.finer("entering handleImageFooterFileUpload"); + if (this.tempDir == null) { + createTempDir(); + logger.finer("created tempDir"); + } + final UploadedFile uFile = event.getFile(); + try { + this.uploadedFileThumbnail = new File(tempDir, uFile.getFileName()); + if (!this.uploadedFileThumbnail.exists()) { + this.uploadedFileThumbnail.createNewFile(); + } + logger.finer("created file"); + Files.copy(uFile.getInputStream(), this.uploadedFileThumbnail.toPath(), StandardCopyOption.REPLACE_EXISTING); + logger.finer("copied inputstream to file"); + this.editDv.getDataverseTheme().setLogoThumbnail(uFile.getFileName()); + } catch (IOException e) { + logger.finer("caught IOException"); + logger.throwing("ThemeWidgetFragment", "handleImageFileUpload", e); + throw new RuntimeException("Error uploading logo file", e); // improve error handling + } + logger.finer("end handleImageFooterFileUpload"); + } + /** + * This method is for footer image. * Copy uploaded file to temp area, until we are ready to save * Copy filename into Dataverse logo * @param event */ - - // This method is for footer image. The syntax is same that handleImageFileUpload for header image - public void handleImageFooterFileUpload(FileUploadEvent event) { - - logger.finer("entering fileUpload"); + logger.finer("entering handleImageFooterFileUpload"); if (this.tempDir==null) { createTempDir(); logger.finer("created tempDir"); @@ -248,18 +273,16 @@ public void handleImageFooterFileUpload(FileUploadEvent event) { logger.throwing("ThemeWidgetFragment", "handleImageFileUpload", e); throw new RuntimeException("Error uploading logo file", e); // improve error handling } - logger.finer("end handelImageFileUpload"); + logger.finer("end handleImageFooterFileUpload"); } - public void handleImageFileUpload(FileUploadEvent event) { - - logger.finer("entering fileUpload"); - if (this.tempDir==null) { - createTempDir(); - logger.finer("created tempDir"); - } - UploadedFile uFile = event.getFile(); + logger.finer("entering handleImageFileUpload"); + if (this.tempDir==null) { + createTempDir(); + logger.finer("created tempDir"); + } + UploadedFile uFile = event.getFile(); try { uploadedFile = new File(tempDir, uFile.getFileName()); if (!uploadedFile.exists()) { @@ -279,9 +302,9 @@ public void handleImageFileUpload(FileUploadEvent event) { if (editDv.getDataverseTheme().getLogoFormat()==null) { editDv.getDataverseTheme().setLogoFormat(DataverseTheme.ImageFormat.SQUARE); } - logger.finer("end handelImageFileUpload"); + logger.finer("end handleImageFileUpload"); } - + public void removeLogo() { editDv.getDataverseTheme().setLogo(null); this.cleanupTempDirectory(); @@ -292,6 +315,11 @@ public void removeLogoFooter() { this.cleanupTempDirectory(); } + public void removeLogoThumbnail() { + editDv.getDataverseTheme().setLogoThumbnail(null); + this.cleanupTempDirectory(); + } + public boolean getInheritCustomization() { boolean inherit= editDv==null ? true : !editDv.getThemeRoot(); return inherit; @@ -324,32 +352,70 @@ public String save() { editDv.setDataverseTheme(null); } - Command cmd; + // Update files : logo, footer, thumbnail + final Dataverse currentDv = dataverseServiceBean.find(editDv.getId()); + final Path logoDir = getLogoDir(editDv.getId().toString()); + String currentLogo = null; + String editedLogo = null; + String currentLogoFooter = null; + String editedLogoFooter = null; + String currentLogoThumbnail = null; + String editedLogoThumbnail = null; + if (currentDv.getDataverseTheme() != null) { + currentLogo = currentDv.getDataverseTheme().getLogo(); + currentLogoFooter = currentDv.getDataverseTheme().getLogoFooter(); + currentLogoThumbnail = currentDv.getDataverseTheme().getLogoThumbnail(); + } + if (editDv.getDataverseTheme() != null) { + editedLogo = editDv.getDataverseTheme().getLogo(); + editedLogoFooter = editDv.getDataverseTheme().getLogoFooter(); + editedLogoThumbnail = editDv.getDataverseTheme().getLogoThumbnail(); + } + updateFile(this.uploadedFile, currentLogo, editedLogo, logoDir); + updateFile(this.uploadedFileFooter, currentLogoFooter, editedLogoFooter, logoDir); + updateFile(this.uploadedFileThumbnail, currentLogoThumbnail, editedLogoThumbnail, logoDir); - cmd = new UpdateDataverseThemeCommand(editDv, this.uploadedFile, dvRequestService.getDataverseRequest(), "HEADER"); - if (!exectThemeCommand(cmd)) + // Save dataverse theme into db + final Command cmd = new UpdateDataverseThemeCommand(editDv, dvRequestService.getDataverseRequest()); + if (!exectThemeCommand(cmd)) { return null; - - if (uploadedFileFooter!=null){ - cmd = new UpdateDataverseThemeCommand(editDv, this.uploadedFileFooter, dvRequestService.getDataverseRequest(), "FOOTER"); - if (!exectThemeCommand(cmd)) - return null; } - try { - commandEngine.submit(cmd); - } catch (Exception ex) { - logger.log(Level.SEVERE, "error updating dataverse theme", ex); - FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_FATAL, BundleUtil.getStringFromBundle("dataverse.save.failed"), BundleUtil.getStringFromBundle("dataverse.theme.failure"))); - - return null; - } finally { - this.cleanupTempDirectory(); - } JsfHelper.addSuccessMessage(BundleUtil.getStringFromBundle("dataverse.theme.success")); return "dataverse.xhtml?faces-redirect=true&alias="+editDv.getAlias(); // go to dataverse page } + /** + * Create, update, or delete file logo. + * + * @param uploadedLogoFile the logo physical file to update on disk + * @param currentLogo logo file name from database before update. {@code null} if absent. + * @param editedLogo logo file name updated. {@code null} if absent. + * @param logoDir folder path containing all collection logos + */ + private void updateFile(File uploadedLogoFile, String currentLogo, String editedLogo, Path logoDir) { + try { + if (!Files.isDirectory(logoDir)) { + Files.createDirectory(logoDir); + } + if (StringUtils.isBlank(editedLogo)) { + // If edited logo field is empty, and a logoFile currently exists, delete it + if (StringUtils.isNotBlank(currentLogo)) { + Files.deleteIfExists(Path.of(logoDir.toString(), currentLogo)); + } + } else if (uploadedLogoFile != null) { + // If edited logo file isn't empty, and uploaded File exists, delete currentFile and copy uploaded file from temp dir to logos dir + if (StringUtils.isNotBlank(currentLogo)) { + Files.deleteIfExists(Path.of(logoDir.toString(), currentLogo)); + } + final Path newFile = Path.of(logoDir.toString(), editedLogo); + Files.copy(uploadedLogoFile.toPath(), newFile, StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException e) { + throw new RuntimeException("Error saving logo file", e); // improve error handling + } + } + public boolean exectThemeCommand(Command cmd){ try { @@ -358,6 +424,8 @@ public boolean exectThemeCommand(Command cmd){ logger.log(Level.SEVERE, "error updating dataverse theme", ex); FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_FATAL, BundleUtil.getStringFromBundle("dataverse.save.failed"), BundleUtil.getStringFromBundle("dataverse.theme.failure"))); return false; + } finally { + this.cleanupTempDirectory(); } return true; } diff --git a/src/main/java/edu/harvard/iq/dataverse/ThumbnailServiceWrapper.java b/src/main/java/edu/harvard/iq/dataverse/ThumbnailServiceWrapper.java index b6ab23848e2..542cf39cfbe 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ThumbnailServiceWrapper.java +++ b/src/main/java/edu/harvard/iq/dataverse/ThumbnailServiceWrapper.java @@ -10,6 +10,7 @@ import edu.harvard.iq.dataverse.dataaccess.StorageIO; import edu.harvard.iq.dataverse.dataset.DatasetUtil; import edu.harvard.iq.dataverse.search.SolrSearchResult; +import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.SystemConfig; import java.io.IOException; @@ -48,6 +49,19 @@ public class ThumbnailServiceWrapper implements java.io.Serializable { private Map dvobjectViewMap = new HashMap<>(); private Map hasThumbMap = new HashMap<>(); + public String getFileCardImageAsUrl(SolrSearchResult result) { + DataFile dataFile = result != null && result.getEntity() != null ? ((DataFile) result.getEntity()) : null; + if (dataFile == null || result.isHarvested() + || !isThumbnailAvailable(dataFile) + || dataFile.isRestricted() + || !dataFile.isReleased() + || FileUtil.isActivelyEmbargoed(dataFile) + || FileUtil.isRetentionExpired(dataFile)) { + return null; + } + return SystemConfig.getDataverseSiteUrlStatic() + "/api/access/datafile/" + dataFile.getId() + "?imageThumb=true"; + } + // it's the responsibility of the user - to make sure the search result // passed to this method is of the Datafile type! public String getFileCardImageAsBase64Url(SolrSearchResult result) { @@ -58,6 +72,10 @@ public String getFileCardImageAsBase64Url(SolrSearchResult result) { if (result.isHarvested()) { return null; } + + if (result.getEntity() == null) { + return null; + } Long imageFileId = result.getEntity().getId(); @@ -177,7 +195,7 @@ public String getDatasetCardImageAsUrl(Dataset dataset, Long versionId, boolean StorageIO storageIO = null; try { storageIO = DataAccess.getStorageIO(dataset); - if (storageIO.isAuxObjectCached(DatasetUtil.datasetLogoFilenameFinal)) { + if (storageIO != null && storageIO.isAuxObjectCached(DatasetUtil.datasetLogoFilenameFinal)) { // If not, return null/use the default, otherwise pass the logo URL hasDatasetLogo = true; } @@ -204,7 +222,13 @@ public String getDatasetCardImageAsUrl(Dataset dataset, Long versionId, boolean public String getDataverseCardImageAsBase64Url(SolrSearchResult result) { return dataverseService.getDataverseLogoThumbnailAsBase64ById(result.getEntityId()); } - + + // it's the responsibility of the user - to make sure the search result + // passed to this method is of the Dataverse type! + public String getDataverseCardImageAsUrl(SolrSearchResult result) { + return dataverseService.getDataverseLogoThumbnailAsUrl(result.getEntityId()); + } + public void resetObjectMaps() { dvobjectThumbnailsMap = new HashMap<>(); dvobjectViewMap = new HashMap<>(); diff --git a/src/main/java/edu/harvard/iq/dataverse/UserNotification.java b/src/main/java/edu/harvard/iq/dataverse/UserNotification.java index 280c2075494..2d37540fab3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/UserNotification.java +++ b/src/main/java/edu/harvard/iq/dataverse/UserNotification.java @@ -39,7 +39,8 @@ public enum Type { CHECKSUMIMPORT, CHECKSUMFAIL, CONFIRMEMAIL, APIGENERATED, INGESTCOMPLETED, INGESTCOMPLETEDWITHERRORS, PUBLISHFAILED_PIDREG, WORKFLOW_SUCCESS, WORKFLOW_FAILURE, STATUSUPDATED, DATASETCREATED, DATASETMENTIONED, GLOBUSUPLOADCOMPLETED, GLOBUSUPLOADCOMPLETEDWITHERRORS, - GLOBUSDOWNLOADCOMPLETED, GLOBUSDOWNLOADCOMPLETEDWITHERRORS, REQUESTEDFILEACCESS; + GLOBUSDOWNLOADCOMPLETED, GLOBUSDOWNLOADCOMPLETEDWITHERRORS, REQUESTEDFILEACCESS, + GLOBUSUPLOADREMOTEFAILURE, GLOBUSUPLOADLOCALFAILURE; public String getDescription() { return BundleUtil.getStringFromBundle("notification.typeDescription." + this.name()); diff --git a/src/main/java/edu/harvard/iq/dataverse/UserServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/UserServiceBean.java index 93892376edc..d63fcfa3e34 100644 --- a/src/main/java/edu/harvard/iq/dataverse/UserServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/UserServiceBean.java @@ -147,6 +147,8 @@ private AuthenticatedUser createAuthenticatedUserForView (Object[] dbRowValues, user.setMutedEmails(Type.tokenizeToSet((String) dbRowValues[15])); user.setMutedNotifications(Type.tokenizeToSet((String) dbRowValues[15])); + user.setRateLimitTier((int)dbRowValues[17]); + user.setRoles(roles); return user; } @@ -419,7 +421,7 @@ private List getUserListCore(String searchTerm, qstr += " u.createdtime, u.lastlogintime, u.lastapiusetime, "; qstr += " prov.id, prov.factoryalias, "; qstr += " u.deactivated, u.deactivatedtime, "; - qstr += " u.mutedEmails, u.mutedNotifications "; + qstr += " u.mutedEmails, u.mutedNotifications, u.rateLimitTier "; qstr += " FROM authenticateduser u,"; qstr += " authenticateduserlookup prov_lookup,"; qstr += " authenticationproviderrow prov"; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index bc94d7f0bcc..3257a3cc7ac 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -2,6 +2,7 @@ import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.actionlogging.ActionLogServiceBean; +import static edu.harvard.iq.dataverse.api.Datasets.handleVersion; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.authorization.RoleAssignee; @@ -10,13 +11,20 @@ import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean; import edu.harvard.iq.dataverse.datacapturemodule.DataCaptureModuleServiceBean; +import edu.harvard.iq.dataverse.dataset.DatasetTypeServiceBean; import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; +import edu.harvard.iq.dataverse.engine.command.impl.GetDraftDatasetVersionCommand; +import edu.harvard.iq.dataverse.engine.command.impl.GetLatestAccessibleDatasetVersionCommand; +import edu.harvard.iq.dataverse.engine.command.impl.GetLatestPublishedDatasetVersionCommand; +import edu.harvard.iq.dataverse.engine.command.impl.GetSpecificPublishedDatasetVersionCommand; +import edu.harvard.iq.dataverse.engine.command.exception.RateLimitCommandException; import edu.harvard.iq.dataverse.externaltools.ExternalToolServiceBean; import edu.harvard.iq.dataverse.license.LicenseServiceBean; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.locality.StorageSiteServiceBean; import edu.harvard.iq.dataverse.metrics.MetricsServiceBean; import edu.harvard.iq.dataverse.search.savedsearch.SavedSearchServiceBean; @@ -36,6 +44,7 @@ import jakarta.persistence.NoResultException; import jakarta.persistence.PersistenceContext; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; @@ -156,6 +165,9 @@ String getWrappedMessageWhenJson() { @EJB protected LicenseServiceBean licenseSvc; + @EJB + protected DatasetTypeServiceBean datasetTypeSvc; + @EJB protected UserServiceBean userSvc; @@ -239,7 +251,7 @@ public enum Format { private final LazyRef jsonParserRef = new LazyRef<>(new Callable() { @Override public JsonParser call() throws Exception { - return new JsonParser(datasetFieldSvc, metadataBlockSvc,settingsSvc, licenseSvc); + return new JsonParser(datasetFieldSvc, metadataBlockSvc,settingsSvc, licenseSvc, datasetTypeSvc); } }); @@ -364,6 +376,11 @@ protected DataverseLinkingDataverse findDataverseLinkingDataverseOrDie(String da } protected Dataset findDatasetOrDie(String id) throws WrappedResponse { + return findDatasetOrDie(id, false); + } + + protected Dataset findDatasetOrDie(String id, boolean deep) throws WrappedResponse { + Long datasetId; Dataset dataset; if (id.equals(PERSISTENT_ID_KEY)) { String persistentId = getRequestParameter(PERSISTENT_ID_KEY.substring(1)); @@ -371,28 +388,67 @@ protected Dataset findDatasetOrDie(String id) throws WrappedResponse { throw new WrappedResponse( badRequest(BundleUtil.getStringFromBundle("find.dataset.error.dataset_id_is_null", Collections.singletonList(PERSISTENT_ID_KEY.substring(1))))); } - dataset = datasetSvc.findByGlobalId(persistentId); - if (dataset == null) { - throw new WrappedResponse(notFound(BundleUtil.getStringFromBundle("find.dataset.error.dataset.not.found.persistentId", Collections.singletonList(persistentId)))); + GlobalId globalId; + try { + globalId = PidUtil.parseAsGlobalID(persistentId); + } catch (IllegalArgumentException e) { + throw new WrappedResponse( + badRequest(BundleUtil.getStringFromBundle("find.dataset.error.dataset.not.found.bad.id", Collections.singletonList(persistentId)))); + } + datasetId = dvObjSvc.findIdByGlobalId(globalId, DvObject.DType.Dataset); + if (datasetId == null) { + datasetId = dvObjSvc.findIdByAltGlobalId(globalId, DvObject.DType.Dataset); + } + if (datasetId == null) { + throw new WrappedResponse( + notFound(BundleUtil.getStringFromBundle("find.dataset.error.dataset_id_is_null", Collections.singletonList(PERSISTENT_ID_KEY.substring(1))))); } - return dataset; - } else { try { - dataset = datasetSvc.find(Long.parseLong(id)); - if (dataset == null) { - throw new WrappedResponse(notFound(BundleUtil.getStringFromBundle("find.dataset.error.dataset.not.found.id", Collections.singletonList(id)))); - } - return dataset; + datasetId = Long.parseLong(id); } catch (NumberFormatException nfe) { throw new WrappedResponse( badRequest(BundleUtil.getStringFromBundle("find.dataset.error.dataset.not.found.bad.id", Collections.singletonList(id)))); } } + if (deep) { + dataset = datasetSvc.findDeep(datasetId); + } else { + dataset = datasetSvc.find(datasetId); + } + if (dataset == null) { + throw new WrappedResponse(notFound(BundleUtil.getStringFromBundle("find.dataset.error.dataset.not.found.id", Collections.singletonList(id)))); + } + return dataset; } - + + protected DatasetVersion findDatasetVersionOrDie(final DataverseRequest req, String versionNumber, final Dataset ds, boolean includeDeaccessioned, boolean checkPermsWhenDeaccessioned) throws WrappedResponse { + DatasetVersion dsv = execCommand(handleVersion(versionNumber, new Datasets.DsVersionHandler>() { + + @Override + public Command handleLatest() { + return new GetLatestAccessibleDatasetVersionCommand(req, ds, includeDeaccessioned, checkPermsWhenDeaccessioned); + } + + @Override + public Command handleDraft() { + return new GetDraftDatasetVersionCommand(req, ds); + } + + @Override + public Command handleSpecific(long major, long minor) { + return new GetSpecificPublishedDatasetVersionCommand(req, ds, major, minor, includeDeaccessioned, checkPermsWhenDeaccessioned); + } + + @Override + public Command handleLatestPublished() { + return new GetLatestPublishedDatasetVersionCommand(req, ds, includeDeaccessioned, checkPermsWhenDeaccessioned); + } + })); + return dsv; + } + protected DataFile findDataFileOrDie(String id) throws WrappedResponse { - DataFile datafile; if (id.equals(PERSISTENT_ID_KEY)) { String persistentId = getRequestParameter(PERSISTENT_ID_KEY.substring(1)); @@ -500,17 +556,21 @@ protected DvObject findDvo( Long id ) { * with that alias. If that fails, tries to get a {@link Dataset} with that global id. * @param id a value identifying the DvObject, either numeric of textual. * @return A DvObject, or {@code null} + * @throws WrappedResponse */ - protected DvObject findDvo( String id ) { - if ( isNumeric(id) ) { - return findDvo( Long.valueOf(id)) ; + @NotNull + protected DvObject findDvo(@NotNull final String id) throws WrappedResponse { + DvObject d = null; + if (isNumeric(id)) { + d = findDvo(Long.valueOf(id)); } else { - Dataverse d = dataverseSvc.findByAlias(id); - return ( d != null ) ? - d : datasetSvc.findByGlobalId(id); - + d = dataverseSvc.findByAlias(id); } - } + if (d == null) { + return findDatasetOrDie(id); + } + return d; + } protected T failIfNull( T t, String errorMessage ) throws WrappedResponse { if ( t != null ) return t; @@ -545,6 +605,8 @@ protected T execCommand( Command cmd ) throws WrappedResponse { try { return engineSvc.submit(cmd); + } catch (RateLimitCommandException ex) { + throw new WrappedResponse(rateLimited(ex.getMessage())); } catch (IllegalCommandException ex) { //for 8859 for api calls that try to update datasets with TOA out of compliance if (ex.getMessage().toLowerCase().contains("terms of use")){ @@ -746,11 +808,15 @@ protected Response notFound( String msg ) { protected Response badRequest( String msg ) { return error( Status.BAD_REQUEST, msg ); } - + protected Response forbidden( String msg ) { return error( Status.FORBIDDEN, msg ); } - + + protected Response rateLimited( String msg ) { + return error( Status.TOO_MANY_REQUESTS, msg ); + } + protected Response conflict( String msg ) { return error( Status.CONFLICT, msg ); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Access.java b/src/main/java/edu/harvard/iq/dataverse/api/Access.java index 297ec2d3681..16ac884180b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Access.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Access.java @@ -130,6 +130,14 @@ import jakarta.ws.rs.core.MediaType; import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataParam; @@ -278,7 +286,7 @@ private DataFile findDataFileOrDieWrapper(String fileId){ @GET @AuthRequired @Path("datafile/{fileId:.+}") - @Produces({"application/xml"}) + @Produces({"application/xml","*/*"}) public Response datafile(@Context ContainerRequestContext crc, @PathParam("fileId") String fileId, @QueryParam("gbrecs") boolean gbrecs, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) /*throws NotFoundException, ServiceUnavailableException, PermissionDeniedException, AuthorizationRequiredException*/ { // check first if there's a trailing slash, and chop it: @@ -466,7 +474,9 @@ public String tabularDatafileMetadataDDI(@Context ContainerRequestContext crc, @ if (!dataFile.isTabularData()) { throw new BadRequestException("tabular data required"); } - + if (FileUtil.isRetentionExpired(dataFile)) { + throw new BadRequestException("unable to download file with expired retention"); + } if (dataFile.isRestricted() || FileUtil.isActivelyEmbargoed(dataFile)) { boolean hasPermissionToDownloadFile = false; DataverseRequest dataverseRequest; @@ -921,14 +931,15 @@ public void write(OutputStream os) throws IOException, } } else { boolean embargoed = FileUtil.isActivelyEmbargoed(file); - if (file.isRestricted() || embargoed) { + boolean retentionExpired = FileUtil.isRetentionExpired(file); + if (file.isRestricted() || embargoed || retentionExpired) { if (zipper == null) { fileManifest = fileManifest + file.getFileMetadata().getLabel() + " IS " - + (embargoed ? "EMBARGOED" : "RESTRICTED") + + (embargoed ? "EMBARGOED" : retentionExpired ? "RETENTIONEXPIRED" : "RESTRICTED") + " AND CANNOT BE DOWNLOADED\r\n"; } else { zipper.addToManifest(file.getFileMetadata().getLabel() + " IS " - + (embargoed ? "EMBARGOED" : "RESTRICTED") + + (embargoed ? "EMBARGOED" : retentionExpired ? "RETENTIONEXPIRED" : "RESTRICTED") + " AND CANNOT BE DOWNLOADED\r\n"); } } else { @@ -1245,6 +1256,20 @@ private String getWebappImageResource(String imageName) { @AuthRequired @Path("datafile/{fileId}/auxiliary/{formatTag}/{formatVersion}") @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces("application/json") + @Operation(summary = "Save auxiliary file with version", + description = "Saves an auxiliary file") + @APIResponses(value = { + @APIResponse(responseCode = "200", + description = "File saved response"), + @APIResponse(responseCode = "403", + description = "User not authorized to edit the dataset."), + @APIResponse(responseCode = "400", + description = "File not found based on id.") + }) + @Tag(name = "saveAuxiliaryFileWithVersion", + description = "Save Auxiliary File With Version") + @RequestBody(content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA)) public Response saveAuxiliaryFileWithVersion(@Context ContainerRequestContext crc, @PathParam("fileId") Long fileId, @PathParam("formatTag") String formatTag, @@ -1402,6 +1427,10 @@ public Response requestFileAccess(@Context ContainerRequestContext crc, @PathPar return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.requestAccess.fileNotFound", args)); } + if (FileUtil.isRetentionExpired(dataFile)) { + return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.requestAccess.failure.retentionExpired")); + } + if (!dataFile.getOwner().isFileAccessRequest()) { return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.requestAccess.requestsNotAccepted")); } @@ -1735,8 +1764,11 @@ private boolean isAccessAuthorized(User requestUser, DataFile df) { //True if there's an embargo that hasn't yet expired //In this state, we block access as though the file is restricted (even if it is not restricted) boolean embargoed = FileUtil.isActivelyEmbargoed(df); - - + // access is also blocked for retention expired files + boolean retentionExpired = FileUtil.isRetentionExpired(df); + // No access ever if retention is expired + if(retentionExpired) return false; + /* SEK 7/26/2018 for 3661 relying on the version state of the dataset versions to which this file is attached check to see if at least one is RELEASED @@ -1801,7 +1833,7 @@ private boolean isAccessAuthorized(User requestUser, DataFile df) { //The one case where we don't need to check permissions - if (!restricted && !embargoed && published) { + if (!restricted && !embargoed && !retentionExpired && published) { // If they are not published, they can still be downloaded, if the user // has the permission to view unpublished versions! (this case will // be handled below) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java index 48f9e19d835..54e5eaf7b84 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java @@ -14,11 +14,12 @@ import edu.harvard.iq.dataverse.DataverseServiceBean; import edu.harvard.iq.dataverse.DataverseSession; import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.DvObjectServiceBean; import edu.harvard.iq.dataverse.api.auth.AuthRequired; import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.util.StringUtil; import edu.harvard.iq.dataverse.validation.EMailValidator; import edu.harvard.iq.dataverse.EjbDataverseEngine; -import edu.harvard.iq.dataverse.HandlenetServiceBean; import edu.harvard.iq.dataverse.Template; import edu.harvard.iq.dataverse.TemplateServiceBean; import edu.harvard.iq.dataverse.UserServiceBean; @@ -64,6 +65,7 @@ import java.io.InputStream; import java.io.StringReader; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Level; @@ -97,6 +99,8 @@ import edu.harvard.iq.dataverse.engine.command.impl.DeleteTemplateCommand; import edu.harvard.iq.dataverse.engine.command.impl.RegisterDvObjectCommand; import edu.harvard.iq.dataverse.ingest.IngestServiceBean; +import edu.harvard.iq.dataverse.pidproviders.handle.HandlePidProvider; +import edu.harvard.iq.dataverse.settings.FeatureFlags; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.userdata.UserListMaker; import edu.harvard.iq.dataverse.userdata.UserListResult; @@ -124,6 +128,7 @@ import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.StreamingOutput; import java.nio.file.Paths; +import java.util.TreeMap; /** * Where the secure, setup API calls live. @@ -136,46 +141,48 @@ public class Admin extends AbstractApiBean { private static final Logger logger = Logger.getLogger(Admin.class.getName()); - @EJB - AuthenticationProvidersRegistrationServiceBean authProvidersRegistrationSvc; - @EJB - BuiltinUserServiceBean builtinUserService; - @EJB - ShibServiceBean shibService; - @EJB - AuthTestDataServiceBean authTestDataService; - @EJB - UserServiceBean userService; - @EJB - IngestServiceBean ingestService; - @EJB - DataFileServiceBean fileService; - @EJB - DatasetServiceBean datasetService; - @EJB - DataverseServiceBean dataverseService; - @EJB - DatasetVersionServiceBean datasetversionService; - @Inject - DataverseRequestServiceBean dvRequestService; - @EJB - EjbDataverseEngine commandEngine; - @EJB - GroupServiceBean groupService; - @EJB - SettingsServiceBean settingsService; - @EJB - DatasetVersionServiceBean datasetVersionService; - @EJB - ExplicitGroupServiceBean explicitGroupService; - @EJB - BannerMessageServiceBean bannerMessageService; - @EJB - TemplateServiceBean templateService; - - // Make the session available - @Inject - DataverseSession session; + @EJB + AuthenticationProvidersRegistrationServiceBean authProvidersRegistrationSvc; + @EJB + BuiltinUserServiceBean builtinUserService; + @EJB + ShibServiceBean shibService; + @EJB + AuthTestDataServiceBean authTestDataService; + @EJB + UserServiceBean userService; + @EJB + IngestServiceBean ingestService; + @EJB + DataFileServiceBean fileService; + @EJB + DatasetServiceBean datasetService; + @EJB + DataverseServiceBean dataverseService; + @EJB + DvObjectServiceBean dvObjectService; + @EJB + DatasetVersionServiceBean datasetversionService; + @Inject + DataverseRequestServiceBean dvRequestService; + @EJB + EjbDataverseEngine commandEngine; + @EJB + GroupServiceBean groupService; + @EJB + SettingsServiceBean settingsService; + @EJB + DatasetVersionServiceBean datasetVersionService; + @EJB + ExplicitGroupServiceBean explicitGroupService; + @EJB + BannerMessageServiceBean bannerMessageService; + @EJB + TemplateServiceBean templateService; + + // Make the session available + @Inject + DataverseSession session; public static final String listUsersPartialAPIPath = "list-users"; public static final String listUsersFullAPIPath = "/api/admin/" + listUsersPartialAPIPath; @@ -197,7 +204,7 @@ public Response putSetting(@PathParam("name") String name, String content) { @Path("settings/{name}/lang/{lang}") @PUT - public Response putSetting(@PathParam("name") String name, @PathParam("lang") String lang, String content) { + public Response putSettingLang(@PathParam("name") String name, @PathParam("lang") String lang, String content) { Setting s = settingsSvc.set(name, lang, content); return ok("Setting " + name + " - " + lang + " - added."); } @@ -220,7 +227,7 @@ public Response deleteSetting(@PathParam("name") String name) { @Path("settings/{name}/lang/{lang}") @DELETE - public Response deleteSetting(@PathParam("name") String name, @PathParam("lang") String lang) { + public Response deleteSettingLang(@PathParam("name") String name, @PathParam("lang") String lang) { settingsSvc.delete(name, lang); return ok("Setting " + name + " - " + lang + " deleted."); } @@ -1026,29 +1033,49 @@ public Response deleteRole(@Context ContainerRequestContext crc, @PathParam("id" }, getRequestUser(crc)); } - @Path("superuser/{identifier}") - @POST - public Response toggleSuperuser(@PathParam("identifier") String identifier) { - ActionLogRecord alr = new ActionLogRecord(ActionLogRecord.ActionType.Admin, "toggleSuperuser") - .setInfo(identifier); - try { - AuthenticatedUser user = authSvc.getAuthenticatedUser(identifier); - if (user.isDeactivated()) { - return error(Status.BAD_REQUEST, "You cannot make a deactivated user a superuser."); - } + @Path("superuser/{identifier}") + @Deprecated + @POST + public Response toggleSuperuser(@PathParam("identifier") String identifier) { + ActionLogRecord alr = new ActionLogRecord(ActionLogRecord.ActionType.Admin, "toggleSuperuser") + .setInfo(identifier); + try { + final AuthenticatedUser user = authSvc.getAuthenticatedUser(identifier); + return setSuperuserStatus(user, !user.isSuperuser()); + } catch (Exception e) { + alr.setActionResult(ActionLogRecord.Result.InternalError); + alr.setInfo(alr.getInfo() + "// " + e.getMessage()); + return error(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage()); + } finally { + actionLogSvc.log(alr); + } + } - user.setSuperuser(!user.isSuperuser()); + private Response setSuperuserStatus(AuthenticatedUser user, Boolean isSuperuser) { + if (user.isDeactivated()) { + return error(Status.BAD_REQUEST, "You cannot make a deactivated user a superuser."); + } + user.setSuperuser(isSuperuser); + return ok("User " + user.getIdentifier() + " " + (user.isSuperuser() ? "set" : "removed") + + " as a superuser."); + } - return ok("User " + user.getIdentifier() + " " + (user.isSuperuser() ? "set" : "removed") - + " as a superuser."); - } catch (Exception e) { - alr.setActionResult(ActionLogRecord.Result.InternalError); - alr.setInfo(alr.getInfo() + "// " + e.getMessage()); - return error(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage()); - } finally { - actionLogSvc.log(alr); - } - } + @Path("superuser/{identifier}") + @PUT + // Using string instead of boolean so user doesn't need to add a Content-type header in their request + public Response setSuperuserStatus(@PathParam("identifier") String identifier, String isSuperuser) { + ActionLogRecord alr = new ActionLogRecord(ActionLogRecord.ActionType.Admin, "setSuperuserStatus") + .setInfo(identifier + ":" + isSuperuser); + try { + return setSuperuserStatus(authSvc.getAuthenticatedUser(identifier), StringUtil.isTrue(isSuperuser)); + } catch (Exception e) { + alr.setActionResult(ActionLogRecord.Result.InternalError); + alr.setInfo(alr.getInfo() + "// " + e.getMessage()); + return error(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage()); + } finally { + actionLogSvc.log(alr); + } + } @GET @Path("validate/datasets") @@ -1129,7 +1156,7 @@ public void write(OutputStream os) throws IOException, os.write(",\n".getBytes()); } - os.write(output.build().toString().getBytes("UTF8")); + os.write(output.build().toString().getBytes(StandardCharsets.UTF_8)); if (!wroteObject) { wroteObject = true; @@ -1243,7 +1270,7 @@ public void write(OutputStream os) throws IOException, os.write(",\n".getBytes()); } - os.write(output.build().toString().getBytes("UTF8")); + os.write(output.build().toString().getBytes(StandardCharsets.UTF_8)); if (!wroteObject) { wroteObject = true; @@ -1329,26 +1356,24 @@ public Response convertUserFromBcryptToSha1(String json) { } - @Path("permissions/{dvo}") - @AuthRequired - @GET - public Response findPermissonsOn(@Context ContainerRequestContext crc, @PathParam("dvo") String dvo) { - try { - DvObject dvObj = findDvo(dvo); - if (dvObj == null) { - return notFound("DvObject " + dvo + " not found"); - } - User aUser = getRequestUser(crc); - JsonObjectBuilder bld = Json.createObjectBuilder(); - bld.add("user", aUser.getIdentifier()); - bld.add("permissions", json(permissionSvc.permissionsFor(createDataverseRequest(aUser), dvObj))); - return ok(bld); - - } catch (Exception e) { - logger.log(Level.SEVERE, "Error while testing permissions", e); - return error(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage()); - } - } + @Path("permissions/{dvo}") + @AuthRequired + @GET + public Response findPermissonsOn(@Context final ContainerRequestContext crc, @PathParam("dvo") final String dvo) { + try { + final DvObject dvObj = findDvo(dvo); + final User aUser = getRequestUser(crc); + final JsonObjectBuilder bld = Json.createObjectBuilder(); + bld.add("user", aUser.getIdentifier()); + bld.add("permissions", json(permissionSvc.permissionsFor(createDataverseRequest(aUser), dvObj))); + return ok(bld); + } catch (WrappedResponse r) { + return r.getResponse(); + } catch (Exception e) { + logger.log(Level.SEVERE, "Error while testing permissions", e); + return error(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage()); + } + } @Path("assignee/{idtf}") @GET @@ -1474,10 +1499,7 @@ public Response isOrcidEnabled() { public Response reregisterHdlToPID(@Context ContainerRequestContext crc, @PathParam("id") String id) { logger.info("Starting to reregister " + id + " Dataset Id. (from hdl to doi)" + new Date()); try { - if (settingsSvc.get(SettingsServiceBean.Key.Protocol.toString()).equals(HandlenetServiceBean.HDL_PROTOCOL)) { - logger.info("Bad Request protocol set to handle " ); - return error(Status.BAD_REQUEST, BundleUtil.getStringFromBundle("admin.api.migrateHDL.failure.must.be.set.for.doi")); - } + User u = getRequestUser(crc); if (!u.isSuperuser()) { @@ -1487,7 +1509,12 @@ public Response reregisterHdlToPID(@Context ContainerRequestContext crc, @PathPa DataverseRequest r = createDataverseRequest(u); Dataset ds = findDatasetOrDie(id); - if (ds.getIdentifier() != null && !ds.getIdentifier().isEmpty() && ds.getProtocol().equals(HandlenetServiceBean.HDL_PROTOCOL)) { + + if (HandlePidProvider.HDL_PROTOCOL.equals(dvObjectService.getEffectivePidGenerator(ds).getProtocol())) { + logger.info("Bad Request protocol set to handle " ); + return error(Status.BAD_REQUEST, BundleUtil.getStringFromBundle("admin.api.migrateHDL.failure.must.be.set.for.doi")); + } + if (ds.getIdentifier() != null && !ds.getIdentifier().isEmpty() && ds.getProtocol().equals(HandlePidProvider.HDL_PROTOCOL)) { execCommand(new RegisterDvObjectCommand(r, ds, true)); } else { return error(Status.BAD_REQUEST, BundleUtil.getStringFromBundle("admin.api.migrateHDL.failure.must.be.hdl.dataset")); @@ -2313,6 +2340,7 @@ public Response addBannerMessage(JsonObject jsonObject) throws WrappedResponse { BannerMessage toAdd = new BannerMessage(); try { + String dismissible = jsonObject.getString("dismissibleByUser"); boolean dismissibleByUser = false; @@ -2333,12 +2361,17 @@ public Response addBannerMessage(JsonObject jsonObject) throws WrappedResponse { messageText.setBannerMessage(toAdd); toAdd.getBannerMessageTexts().add(messageText); } - bannerMessageService.save(toAdd); - return ok("Banner Message added successfully."); + bannerMessageService.save(toAdd); + + JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder() + .add("message", "Banner Message added successfully.") + .add("id", toAdd.getId()); + + return ok(jsonObjectBuilder); } catch (Exception e) { logger.warning("Unexpected Exception: " + e.getMessage()); - return error(Status.BAD_REQUEST, "Add Banner Message unexpected exception: " + e.getMessage()); + return error(Status.BAD_REQUEST, "Add Banner Message unexpected exception: invalid or missing JSON object."); } } @@ -2374,10 +2407,19 @@ public Response deactivateBannerMessage(@PathParam("id") Long id) throws Wrapped @Path("/bannerMessage") public Response getBannerMessages(@PathParam("id") Long id) throws WrappedResponse { - return ok(bannerMessageService.findAllBannerMessages().stream() - .map(m -> jsonObjectBuilder().add("id", m.getId()).add("displayValue", m.getDisplayValue())) - .collect(toJsonArray())); + List messagesList = bannerMessageService.findAllBannerMessages(); + + for (BannerMessage message : messagesList) { + if ("".equals(message.getDisplayValue())) { + return error(Response.Status.INTERNAL_SERVER_ERROR, "No banner messages found for this locale."); + } + } + JsonArrayBuilder messages = messagesList.stream() + .map(m -> jsonObjectBuilder().add("id", m.getId()).add("displayValue", m.getDisplayValue())) + .collect(toJsonArray()); + + return ok(messages); } @POST @@ -2476,4 +2518,27 @@ public Response downloadTmpFile(@Context ContainerRequestContext crc, @QueryPara } } + @GET + @Path("/featureFlags") + public Response getFeatureFlags() { + Map map = new TreeMap<>(); + for (FeatureFlags flag : FeatureFlags.values()) { + map.put(flag.name(), flag.enabled() ? "enabled" : "disabled"); + } + return ok(Json.createObjectBuilder(map)); + } + + @GET + @Path("/featureFlags/{flag}") + public Response getFeatureFlag(@PathParam("flag") String flagIn) { + try { + FeatureFlags flag = FeatureFlags.valueOf(flagIn); + JsonObjectBuilder job = Json.createObjectBuilder(); + job.add("enabled", flag.enabled()); + return ok(job); + } catch (IllegalArgumentException ex) { + return error(Status.NOT_FOUND, "Feature flag not found. Try listing all feature flags."); + } + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/ApiBlockingFilter.java b/src/main/java/edu/harvard/iq/dataverse/api/ApiBlockingFilter.java index 0e5b8226310..b51b1aa2612 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/ApiBlockingFilter.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/ApiBlockingFilter.java @@ -49,7 +49,7 @@ public void doBlock(ServletRequest sr, ServletResponse sr1, FilterChain fc) thro @Override public void doBlock(ServletRequest sr, ServletResponse sr1, FilterChain fc) throws IOException, ServletException { HttpServletResponse httpResponse = (HttpServletResponse) sr1; - httpResponse.getWriter().println("{ status:\"error\", message:\"Endpoint blocked. Please contact the dataverse administrator\"}" ); + httpResponse.getWriter().println("{ \"status\":\"error\", \"message\":\"Endpoint blocked. Please contact the dataverse administrator\"}" ); httpResponse.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); httpResponse.setContentType("application/json"); } @@ -67,7 +67,7 @@ public void doBlock(ServletRequest sr, ServletResponse sr1, FilterChain fc) thro fc.doFilter(sr, sr1); } else { HttpServletResponse httpResponse = (HttpServletResponse) sr1; - httpResponse.getWriter().println("{ status:\"error\", message:\"Endpoint available from localhost only. Please contact the dataverse administrator\"}" ); + httpResponse.getWriter().println("{ \"status\":\"error\", \"message\":\"Endpoint available from localhost only. Please contact the dataverse administrator\"}" ); httpResponse.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); httpResponse.setContentType("application/json"); } @@ -102,7 +102,7 @@ public void doBlock(ServletRequest sr, ServletResponse sr1, FilterChain fc) thro if ( block ) { HttpServletResponse httpResponse = (HttpServletResponse) sr1; - httpResponse.getWriter().println("{ status:\"error\", message:\"Endpoint available using API key only. Please contact the dataverse administrator\"}" ); + httpResponse.getWriter().println("{ \"status\":\"error\", \"message\":\"Endpoint available using API key only. Please contact the dataverse administrator\"}" ); httpResponse.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); httpResponse.setContentType("application/json"); } else { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/ApiConstants.java b/src/main/java/edu/harvard/iq/dataverse/api/ApiConstants.java index 347a8946a46..15114085c21 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/ApiConstants.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/ApiConstants.java @@ -17,4 +17,8 @@ private ApiConstants() { public static final String DS_VERSION_LATEST = ":latest"; public static final String DS_VERSION_DRAFT = ":draft"; public static final String DS_VERSION_LATEST_PUBLISHED = ":latest-published"; + + // addFiles call + public static final String API_ADD_FILES_COUNT_PROCESSED = "Total number of files"; + public static final String API_ADD_FILES_COUNT_SUCCESSFUL = "Number of files successfully added"; } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/BatchServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/api/BatchServiceBean.java index daddc447117..fda698d6f5c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/BatchServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/BatchServiceBean.java @@ -46,8 +46,8 @@ public void processFilePath(String fileDir, String parentIdtf, DataverseRequest SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss"); - validationLog = new PrintWriter(new FileWriter( "../logs/validationLog"+ formatter.format(timestamp)+".txt")); - cleanupLog = new PrintWriter(new FileWriter( "../logs/cleanupLog"+ formatter.format(timestamp)+".txt")); + validationLog = new PrintWriter(new FileWriter( System.getProperty("com.sun.aas.instanceRoot") + File.separator + "logs" + File.separator + "validationLog"+ formatter.format(timestamp)+".txt")); + cleanupLog = new PrintWriter(new FileWriter( System.getProperty("com.sun.aas.instanceRoot") + File.separator + "logs" + File.separator + "cleanupLog"+ formatter.format(timestamp)+".txt")); File dir = new File(fileDir); if (dir.isDirectory()) { for (File file : dir.listFiles()) { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java b/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java index 50862bc0d35..ba99cf33c5b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java @@ -119,7 +119,7 @@ public Response create(BuiltinUser user, @PathParam("password") String password, */ @POST @Path("{password}/{key}/{sendEmailNotification}") - public Response create(BuiltinUser user, @PathParam("password") String password, @PathParam("key") String key, @PathParam("sendEmailNotification") Boolean sendEmailNotification) { + public Response createWithNotification(BuiltinUser user, @PathParam("password") String password, @PathParam("key") String key, @PathParam("sendEmailNotification") Boolean sendEmailNotification) { return internalSave(user, password, key, sendEmailNotification); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/DatasetFieldServiceApi.java b/src/main/java/edu/harvard/iq/dataverse/api/DatasetFieldServiceApi.java index 00b7dfa6e36..01c51dc2b4c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/DatasetFieldServiceApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/DatasetFieldServiceApi.java @@ -24,7 +24,6 @@ import jakarta.ejb.EJBException; import jakarta.json.Json; import jakarta.json.JsonArrayBuilder; -import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; @@ -488,9 +487,7 @@ private String parseControlledVocabulary(String[] values) { @Consumes("application/zip") @Path("loadpropertyfiles") public Response loadLanguagePropertyFile(File inputFile) { - try - { - ZipFile file = new ZipFile(inputFile); + try (ZipFile file = new ZipFile(inputFile)) { //Get file entries Enumeration entries = file.entries(); @@ -502,20 +499,26 @@ public Response loadLanguagePropertyFile(File inputFile) { { ZipEntry entry = entries.nextElement(); String dataverseLangFileName = dataverseLangDirectory + "/" + entry.getName(); - FileOutputStream fileOutput = new FileOutputStream(dataverseLangFileName); + File entryFile = new File(dataverseLangFileName); + String canonicalPath = entryFile.getCanonicalPath(); + if (canonicalPath.startsWith(dataverseLangDirectory + "/")) { + try (FileOutputStream fileOutput = new FileOutputStream(dataverseLangFileName)) { - InputStream is = file.getInputStream(entry); - BufferedInputStream bis = new BufferedInputStream(is); + InputStream is = file.getInputStream(entry); + BufferedInputStream bis = new BufferedInputStream(is); - while (bis.available() > 0) { - fileOutput.write(bis.read()); + while (bis.available() > 0) { + fileOutput.write(bis.read()); + } + } + } else { + logger.log(Level.SEVERE, "Zip Slip prevented: uploaded zip file tried to write to {}", canonicalPath); + return Response.status(400).entity("The zip file includes an illegal file path").build(); } - fileOutput.close(); } } - catch(IOException e) - { - e.printStackTrace(); + catch(IOException e) { + logger.log(Level.SEVERE, "Reading the language property zip file failed", e); return Response.status(500).entity("Internal server error. More details available at the server logs.").build(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/DatasetFields.java b/src/main/java/edu/harvard/iq/dataverse/api/DatasetFields.java new file mode 100644 index 00000000000..2ec35c896d9 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/api/DatasetFields.java @@ -0,0 +1,29 @@ +package edu.harvard.iq.dataverse.api; + +import edu.harvard.iq.dataverse.DatasetFieldServiceBean; +import edu.harvard.iq.dataverse.DatasetFieldType; +import jakarta.ejb.EJB; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; + +import java.util.List; + +import static edu.harvard.iq.dataverse.util.json.JsonPrinter.jsonDatasetFieldTypes; + +/** + * Api bean for managing dataset fields. + */ +@Path("datasetfields") +@Produces("application/json") +public class DatasetFields extends AbstractApiBean { + + @EJB + DatasetFieldServiceBean datasetFieldService; + + @GET + @Path("facetables") + public Response listAllFacetableDatasetFields() { + List datasetFieldTypes = datasetFieldService.findAllFacetableFieldTypes(); + return ok(jsonDatasetFieldTypes(datasetFieldTypes)); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index ea74368d110..369a22fe8d7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -39,11 +39,15 @@ import edu.harvard.iq.dataverse.makedatacount.*; import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean.MakeDataCountEntry; import edu.harvard.iq.dataverse.metrics.MetricsUtil; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +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.search.IndexServiceBean; +import edu.harvard.iq.dataverse.settings.FeatureFlags; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.storageuse.UploadSessionQuotaLimit; import edu.harvard.iq.dataverse.util.*; import edu.harvard.iq.dataverse.util.bagit.OREMap; import edu.harvard.iq.dataverse.util.json.*; @@ -63,6 +67,12 @@ import jakarta.ws.rs.core.*; import jakarta.ws.rs.core.Response.Status; import org.apache.commons.lang3.StringUtils; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataContentDisposition; import org.glassfish.jersey.media.multipart.FormDataParam; @@ -77,6 +87,7 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.ExecutionException; @@ -87,9 +98,13 @@ import java.util.stream.Collectors; import static edu.harvard.iq.dataverse.api.ApiConstants.*; +import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; +import edu.harvard.iq.dataverse.dataset.DatasetType; +import edu.harvard.iq.dataverse.dataset.DatasetTypeServiceBean; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*; import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; +import static jakarta.ws.rs.core.Response.Status.NOT_FOUND; @Path("datasets") public class Datasets extends AbstractApiBean { @@ -151,6 +166,9 @@ public class Datasets extends AbstractApiBean { @EJB EmbargoServiceBean embargoService; + @EJB + RetentionServiceBean retentionService; + @Inject MakeDataCountLoggingServiceBean mdcLogService; @@ -172,6 +190,9 @@ public class Datasets extends AbstractApiBean { @Inject DatasetVersionFilesServiceBean datasetVersionFilesServiceBean; + @Inject + DatasetTypeServiceBean datasetTypeSvc; + /** * Used to consolidate the way we parse and handle dataset versions. * @param @@ -186,11 +207,11 @@ public interface DsVersionHandler { @GET @AuthRequired @Path("{id}") - public Response getDataset(@Context ContainerRequestContext crc, @PathParam("id") String id, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) { + public Response getDataset(@Context ContainerRequestContext crc, @PathParam("id") String id, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, @QueryParam("returnOwners") boolean returnOwners) { return response( req -> { - final Dataset retrieved = execCommand(new GetDatasetCommand(req, findDatasetOrDie(id))); + final Dataset retrieved = execCommand(new GetDatasetCommand(req, findDatasetOrDie(id, true))); final DatasetVersion latest = execCommand(new GetLatestAccessibleDatasetVersionCommand(req, retrieved)); - final JsonObjectBuilder jsonbuilder = json(retrieved); + final JsonObjectBuilder jsonbuilder = json(retrieved, returnOwners); //Report MDC if this is a released version (could be draft if user has access, or user may not have access at all and is not getting metadata beyond the minimum) if((latest != null) && latest.isReleased()) { MakeDataCountLoggingServiceBean.MakeDataCountEntry entry = new MakeDataCountEntry(uriInfo, headers, dvRequestService, retrieved); @@ -206,7 +227,7 @@ public Response getDataset(@Context ContainerRequestContext crc, @PathParam("id" // WORKS on published datasets, which are open to the world. -- L.A. 4.5 @GET @Path("/export") - @Produces({"application/xml", "application/json", "application/html", "application/ld+json" }) + @Produces({"application/xml", "application/json", "application/html", "application/ld+json", "*/*" }) public Response exportDataset(@QueryParam("persistentId") String persistentId, @QueryParam("exporter") String exporter, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) { try { @@ -421,25 +442,37 @@ public Response getVersion(@Context ContainerRequestContext crc, @PathParam("versionId") String versionId, @QueryParam("excludeFiles") Boolean excludeFiles, @QueryParam("includeDeaccessioned") boolean includeDeaccessioned, + @QueryParam("returnOwners") boolean returnOwners, @Context UriInfo uriInfo, @Context HttpHeaders headers) { return response( req -> { - //If excludeFiles is null the default is to provide the files and because of this we need to check permissions. boolean checkPerms = excludeFiles == null ? true : !excludeFiles; - - Dataset dst = findDatasetOrDie(datasetId); - DatasetVersion dsv = getDatasetVersionOrDie(req, versionId, dst, uriInfo, headers, includeDeaccessioned, checkPerms); - - if (dsv == null || dsv.getId() == null) { + + Dataset dataset = findDatasetOrDie(datasetId); + DatasetVersion requestedDatasetVersion = getDatasetVersionOrDie(req, + versionId, + dataset, + uriInfo, + headers, + includeDeaccessioned, + checkPerms); + + if (requestedDatasetVersion == null || requestedDatasetVersion.getId() == null) { return notFound("Dataset version not found"); } if (excludeFiles == null ? true : !excludeFiles) { - dsv = datasetversionService.findDeep(dsv.getId()); + requestedDatasetVersion = datasetversionService.findDeep(requestedDatasetVersion.getId()); } - return ok(json(dsv, excludeFiles == null ? true : !excludeFiles)); + + JsonObjectBuilder jsonBuilder = json(requestedDatasetVersion, + null, + excludeFiles == null ? true : !excludeFiles, + returnOwners); + return ok(jsonBuilder); + }, getRequestUser(crc)); } @@ -461,7 +494,7 @@ public Response getVersionFiles(@Context ContainerRequestContext crc, @Context UriInfo uriInfo, @Context HttpHeaders headers) { return response(req -> { - DatasetVersion datasetVersion = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers, includeDeaccessioned); + DatasetVersion datasetVersion = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId, false), uriInfo, headers, includeDeaccessioned); DatasetVersionFilesServiceBean.FileOrderCriteria fileOrderCriteria; try { fileOrderCriteria = orderCriteria != null ? DatasetVersionFilesServiceBean.FileOrderCriteria.valueOf(orderCriteria) : DatasetVersionFilesServiceBean.FileOrderCriteria.NameAZ; @@ -626,7 +659,7 @@ public Response getLinkset(@Context ContainerRequestContext crc, @PathParam("id" } } - @GET + @POST @AuthRequired @Path("{id}/modifyRegistration") public Response updateDatasetTargetURL(@Context ContainerRequestContext crc, @PathParam("id") String id ) { @@ -667,20 +700,28 @@ public Response updateDatasetPIDMetadata(@Context ContainerRequestContext crc, @ } return response(req -> { - execCommand(new UpdateDvObjectPIDMetadataCommand(findDatasetOrDie(id), req)); - List args = Arrays.asList(id); + Dataset dataset = findDatasetOrDie(id); + execCommand(new UpdateDvObjectPIDMetadataCommand(dataset, req)); + List args = Arrays.asList(dataset.getIdentifier()); return ok(BundleUtil.getStringFromBundle("datasets.api.updatePIDMetadata.success.for.single.dataset", args)); }, getRequestUser(crc)); } - @GET + @POST @AuthRequired @Path("/modifyRegistrationPIDMetadataAll") public Response updateDatasetPIDMetadataAll(@Context ContainerRequestContext crc) { return response( req -> { datasetService.findAll().forEach( ds -> { try { + logger.fine("ReRegistering: " + ds.getId() + " : " + ds.getIdentifier()); + if (!ds.isReleased() || (!ds.isIdentifierRegistered() || (ds.getIdentifier() == null))) { + if (ds.isReleased()) { + logger.warning("Dataset id=" + ds.getId() + " is in an inconsistent state (publicationdate but no identifier/identifier not registered"); + } + } else { execCommand(new UpdateDvObjectPIDMetadataCommand(findDatasetOrDie(ds.getId().toString()), req)); + } } catch (WrappedResponse ex) { Logger.getLogger(Datasets.class.getName()).log(Level.SEVERE, null, ex); } @@ -777,8 +818,8 @@ public Response getVersionJsonLDMetadata(@Context ContainerRequestContext crc, @ @AuthRequired @Path("{id}/metadata") @Produces("application/ld+json, application/json-ld") - public Response getVersionJsonLDMetadata(@Context ContainerRequestContext crc, @PathParam("id") String id, @Context UriInfo uriInfo, @Context HttpHeaders headers) { - return getVersionJsonLDMetadata(crc, id, DS_VERSION_DRAFT, uriInfo, headers); + public Response getJsonLDMetadata(@Context ContainerRequestContext crc, @PathParam("id") String id, @Context UriInfo uriInfo, @Context HttpHeaders headers) { + return getVersionJsonLDMetadata(crc, id, DS_VERSION_LATEST, uriInfo, headers); } @PUT @@ -1675,6 +1716,306 @@ public Response removeFileEmbargo(@Context ContainerRequestContext crc, @PathPar } } + @POST + @AuthRequired + @Path("{id}/files/actions/:set-retention") + public Response createFileRetention(@Context ContainerRequestContext crc, @PathParam("id") String id, String jsonBody){ + + // user is authenticated + AuthenticatedUser authenticatedUser = null; + try { + authenticatedUser = getRequestAuthenticatedUserOrDie(crc); + } catch (WrappedResponse ex) { + return error(Status.UNAUTHORIZED, "Authentication is required."); + } + + Dataset dataset; + try { + dataset = findDatasetOrDie(id); + } catch (WrappedResponse ex) { + return ex.getResponse(); + } + + boolean hasValidTerms = TermsOfUseAndAccessValidator.isTOUAValid(dataset.getLatestVersion().getTermsOfUseAndAccess(), null); + + if (!hasValidTerms){ + return error(Status.CONFLICT, BundleUtil.getStringFromBundle("dataset.message.toua.invalid")); + } + + // client is superadmin or (client has EditDataset permission on these files and files are unreleased) + // check if files are unreleased(DRAFT?) + if ((!authenticatedUser.isSuperuser() && (dataset.getLatestVersion().getVersionState() != DatasetVersion.VersionState.DRAFT) ) || !permissionService.userOn(authenticatedUser, dataset).has(Permission.EditDataset)) { + return error(Status.FORBIDDEN, "Either the files are released and user is not a superuser or user does not have EditDataset permissions"); + } + + // check if retentions are allowed(:MinRetentionDurationInMonths), gets the :MinRetentionDurationInMonths setting variable, if 0 or not set(null) return 400 + long minRetentionDurationInMonths = 0; + try { + minRetentionDurationInMonths = Long.parseLong(settingsService.get(SettingsServiceBean.Key.MinRetentionDurationInMonths.toString())); + } catch (NumberFormatException nfe){ + if (nfe.getMessage().contains("null")) { + return error(Status.BAD_REQUEST, "No Retention periods allowed"); + } + } + if (minRetentionDurationInMonths == 0){ + return error(Status.BAD_REQUEST, "No Retention periods allowed"); + } + + JsonObject json; + try { + json = JsonUtil.getJsonObject(jsonBody); + } catch (JsonException ex) { + return error(Status.BAD_REQUEST, "Invalid JSON; error message: " + ex.getMessage()); + } + + Retention retention = new Retention(); + + + LocalDate currentDateTime = LocalDate.now(); + + // Extract the dateUnavailable - check if specified and valid + String dateUnavailableStr = ""; + LocalDate dateUnavailable; + try { + dateUnavailableStr = json.getString("dateUnavailable"); + dateUnavailable = LocalDate.parse(dateUnavailableStr); + } catch (NullPointerException npex) { + return error(Status.BAD_REQUEST, "Invalid retention period; no dateUnavailable specified"); + } catch (ClassCastException ccex) { + return error(Status.BAD_REQUEST, "Invalid retention period; dateUnavailable must be a string"); + } catch (DateTimeParseException dtpex) { + return error(Status.BAD_REQUEST, "Invalid date format for dateUnavailable: " + dateUnavailableStr); + } + + // check :MinRetentionDurationInMonths if -1 + LocalDate minRetentionDateTime = minRetentionDurationInMonths != -1 ? LocalDate.now().plusMonths(minRetentionDurationInMonths) : null; + // dateUnavailable is not in the past + if (dateUnavailable.isAfter(currentDateTime)){ + retention.setDateUnavailable(dateUnavailable); + } else { + return error(Status.BAD_REQUEST, "Date unavailable can not be in the past"); + } + + // dateAvailable is within limits + if (minRetentionDateTime != null){ + if (dateUnavailable.isBefore(minRetentionDateTime)){ + return error(Status.BAD_REQUEST, "Date unavailable can not be earlier than MinRetentionDurationInMonths: "+minRetentionDurationInMonths + " from now"); + } + } + + try { + String reason = json.getString("reason"); + retention.setReason(reason); + } catch (NullPointerException npex) { + // ignoring; no reason specified is OK, it is optional + } catch (ClassCastException ccex) { + return error(Status.BAD_REQUEST, "Invalid retention period; reason must be a string"); + } + + + List datasetFiles = dataset.getFiles(); + List filesToRetention = new LinkedList<>(); + + // extract fileIds from json, find datafiles and add to list + if (json.containsKey("fileIds")){ + try { + JsonArray fileIds = json.getJsonArray("fileIds"); + for (JsonValue jsv : fileIds) { + try { + DataFile dataFile = findDataFileOrDie(jsv.toString()); + filesToRetention.add(dataFile); + } catch (WrappedResponse ex) { + return ex.getResponse(); + } + } + } catch (ClassCastException ccex) { + return error(Status.BAD_REQUEST, "Invalid retention period; fileIds must be an array of id strings"); + } catch (NullPointerException npex) { + return error(Status.BAD_REQUEST, "Invalid retention period; no fileIds specified"); + } + } else { + return error(Status.BAD_REQUEST, "No fileIds specified"); + } + + List orphanedRetentions = new ArrayList(); + // check if files belong to dataset + if (datasetFiles.containsAll(filesToRetention)) { + JsonArrayBuilder restrictedFiles = Json.createArrayBuilder(); + boolean badFiles = false; + for (DataFile datafile : filesToRetention) { + // superuser can overrule an existing retention, even on released files + if (datafile.isReleased() && !authenticatedUser.isSuperuser()) { + restrictedFiles.add(datafile.getId()); + badFiles = true; + } + } + if (badFiles) { + return Response.status(Status.FORBIDDEN) + .entity(NullSafeJsonBuilder.jsonObjectBuilder().add("status", ApiConstants.STATUS_ERROR) + .add("message", "You do not have permission to set a retention period for the following files") + .add("files", restrictedFiles).build()) + .type(MediaType.APPLICATION_JSON_TYPE).build(); + } + retention=retentionService.merge(retention); + // Good request, so add the retention. Track any existing retentions so we can + // delete them if there are no files left that reference them. + for (DataFile datafile : filesToRetention) { + Retention ret = datafile.getRetention(); + if (ret != null) { + ret.getDataFiles().remove(datafile); + if (ret.getDataFiles().isEmpty()) { + orphanedRetentions.add(ret); + } + } + // Save merges the datafile with an retention into the context + datafile.setRetention(retention); + fileService.save(datafile); + } + //Call service to get action logged + long retentionId = retentionService.save(retention, authenticatedUser.getIdentifier()); + if (orphanedRetentions.size() > 0) { + for (Retention ret : orphanedRetentions) { + retentionService.delete(ret, authenticatedUser.getIdentifier()); + } + } + //If superuser, report changes to any released files + if (authenticatedUser.isSuperuser()) { + String releasedFiles = filesToRetention.stream().filter(d -> d.isReleased()) + .map(d -> d.getId().toString()).collect(Collectors.joining(",")); + if (!releasedFiles.isBlank()) { + actionLogSvc + .log(new ActionLogRecord(ActionLogRecord.ActionType.Admin, "retentionAddedTo") + .setInfo("Retention id: " + retention.getId() + " added for released file(s), id(s) " + + releasedFiles + ".") + .setUserIdentifier(authenticatedUser.getIdentifier())); + } + } + return ok(Json.createObjectBuilder().add("message", "File(s) retention period has been set or updated")); + } else { + return error(BAD_REQUEST, "Not all files belong to dataset"); + } + } + + @POST + @AuthRequired + @Path("{id}/files/actions/:unset-retention") + public Response removeFileRetention(@Context ContainerRequestContext crc, @PathParam("id") String id, String jsonBody){ + + // user is authenticated + AuthenticatedUser authenticatedUser = null; + try { + authenticatedUser = getRequestAuthenticatedUserOrDie(crc); + } catch (WrappedResponse ex) { + return error(Status.UNAUTHORIZED, "Authentication is required."); + } + + Dataset dataset; + try { + dataset = findDatasetOrDie(id); + } catch (WrappedResponse ex) { + return ex.getResponse(); + } + + // client is superadmin or (client has EditDataset permission on these files and files are unreleased) + // check if files are unreleased(DRAFT?) + //ToDo - here and below - check the release status of files and not the dataset state (draft dataset version still can have released files) + if ((!authenticatedUser.isSuperuser() && (dataset.getLatestVersion().getVersionState() != DatasetVersion.VersionState.DRAFT) ) || !permissionService.userOn(authenticatedUser, dataset).has(Permission.EditDataset)) { + return error(Status.FORBIDDEN, "Either the files are released and user is not a superuser or user does not have EditDataset permissions"); + } + + // check if retentions are allowed(:MinRetentionDurationInMonths), gets the :MinRetentionDurationInMonths setting variable, if 0 or not set(null) return 400 + int minRetentionDurationInMonths = 0; + try { + minRetentionDurationInMonths = Integer.parseInt(settingsService.get(SettingsServiceBean.Key.MinRetentionDurationInMonths.toString())); + } catch (NumberFormatException nfe){ + if (nfe.getMessage().contains("null")) { + return error(Status.BAD_REQUEST, "No Retention periods allowed"); + } + } + if (minRetentionDurationInMonths == 0){ + return error(Status.BAD_REQUEST, "No Retention periods allowed"); + } + + JsonObject json; + try { + json = JsonUtil.getJsonObject(jsonBody); + } catch (JsonException ex) { + return error(Status.BAD_REQUEST, "Invalid JSON; error message: " + ex.getMessage()); + } + + List datasetFiles = dataset.getFiles(); + List retentionFilesToUnset = new LinkedList<>(); + + // extract fileIds from json, find datafiles and add to list + if (json.containsKey("fileIds")){ + try { + JsonArray fileIds = json.getJsonArray("fileIds"); + for (JsonValue jsv : fileIds) { + try { + DataFile dataFile = findDataFileOrDie(jsv.toString()); + retentionFilesToUnset.add(dataFile); + } catch (WrappedResponse ex) { + return ex.getResponse(); + } + } + } catch (ClassCastException ccex) { + return error(Status.BAD_REQUEST, "fileIds must be an array of id strings"); + } catch (NullPointerException npex) { + return error(Status.BAD_REQUEST, "No fileIds specified"); + } + } else { + return error(Status.BAD_REQUEST, "No fileIds specified"); + } + + List orphanedRetentions = new ArrayList(); + // check if files belong to dataset + if (datasetFiles.containsAll(retentionFilesToUnset)) { + JsonArrayBuilder restrictedFiles = Json.createArrayBuilder(); + boolean badFiles = false; + for (DataFile datafile : retentionFilesToUnset) { + // superuser can overrule an existing retention, even on released files + if (datafile.getRetention()==null || ((datafile.isReleased() && datafile.getRetention() != null) && !authenticatedUser.isSuperuser())) { + restrictedFiles.add(datafile.getId()); + badFiles = true; + } + } + if (badFiles) { + return Response.status(Status.FORBIDDEN) + .entity(NullSafeJsonBuilder.jsonObjectBuilder().add("status", ApiConstants.STATUS_ERROR) + .add("message", "The following files do not have retention periods or you do not have permission to remove their retention periods") + .add("files", restrictedFiles).build()) + .type(MediaType.APPLICATION_JSON_TYPE).build(); + } + // Good request, so remove the retention from the files. Track any existing retentions so we can + // delete them if there are no files left that reference them. + for (DataFile datafile : retentionFilesToUnset) { + Retention ret = datafile.getRetention(); + if (ret != null) { + ret.getDataFiles().remove(datafile); + if (ret.getDataFiles().isEmpty()) { + orphanedRetentions.add(ret); + } + } + // Save merges the datafile with an retention into the context + datafile.setRetention(null); + fileService.save(datafile); + } + if (orphanedRetentions.size() > 0) { + for (Retention ret : orphanedRetentions) { + retentionService.delete(ret, authenticatedUser.getIdentifier()); + } + } + String releasedFiles = retentionFilesToUnset.stream().filter(d -> d.isReleased()).map(d->d.getId().toString()).collect(Collectors.joining(",")); + if(!releasedFiles.isBlank()) { + ActionLogRecord removeRecord = new ActionLogRecord(ActionLogRecord.ActionType.Admin, "retentionRemovedFrom").setInfo("Retention removed from released file(s), id(s) " + releasedFiles + "."); + removeRecord.setUserIdentifier(authenticatedUser.getIdentifier()); + actionLogSvc.log(removeRecord); + } + return ok(Json.createObjectBuilder().add("message", "Retention periods were removed from file(s)")); + } else { + return error(BAD_REQUEST, "Not all files belong to dataset"); + } + } @PUT @AuthRequired @@ -1736,10 +2077,16 @@ public Response getLinks(@Context ContainerRequestContext crc, @PathParam("id") List dvsThatLinkToThisDatasetId = dataverseSvc.findDataversesThatLinkToThisDatasetId(datasetId); JsonArrayBuilder dataversesThatLinkToThisDatasetIdBuilder = Json.createArrayBuilder(); for (Dataverse dataverse : dvsThatLinkToThisDatasetId) { - dataversesThatLinkToThisDatasetIdBuilder.add(dataverse.getAlias() + " (id " + dataverse.getId() + ")"); + JsonObjectBuilder datasetBuilder = Json.createObjectBuilder(); + datasetBuilder.add("id", dataverse.getId()); + datasetBuilder.add("alias", dataverse.getAlias()); + datasetBuilder.add("displayName", dataverse.getDisplayName()); + dataversesThatLinkToThisDatasetIdBuilder.add(datasetBuilder.build()); } JsonObjectBuilder response = Json.createObjectBuilder(); - response.add("dataverses that link to dataset id " + datasetId, dataversesThatLinkToThisDatasetIdBuilder); + response.add("id", datasetId); + response.add("identifier", dataset.getIdentifier()); + response.add("linked-dataverses", dataversesThatLinkToThisDatasetIdBuilder); return ok(response); } catch (WrappedResponse wr) { return wr.getResponse(); @@ -1942,6 +2289,14 @@ public Response setDataFileAsThumbnail(@Context ContainerRequestContext crc, @Pa @AuthRequired @Path("{id}/thumbnail") @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces("application/json") + @Operation(summary = "Uploads a logo for a dataset", + description = "Uploads a logo for a dataset") + @APIResponse(responseCode = "200", + description = "Dataset logo uploaded successfully") + @Tag(name = "uploadDatasetLogo", + description = "Uploads a logo for a dataset") + @RequestBody(content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA)) public Response uploadDatasetLogo(@Context ContainerRequestContext crc, @PathParam("id") String idSupplied, @FormDataParam("file") InputStream inputStream) { try { DatasetThumbnail datasetThumbnail = execCommand(new UpdateDatasetThumbnailCommand(createDataverseRequest(getRequestUser(crc)), findDatasetOrDie(idSupplied), UpdateDatasetThumbnailCommand.UserIntent.setNonDatasetFileAsThumbnail, null, inputStream)); @@ -1963,6 +2318,7 @@ public Response removeDatasetLogo(@Context ContainerRequestContext crc, @PathPar } } + @Deprecated(forRemoval = true, since = "2024-07-07") @GET @AuthRequired @Path("{identifier}/dataCaptureModule/rsync") @@ -2143,9 +2499,9 @@ public Response returnToAuthor(@Context ContainerRequestContext crc, @PathParam( Dataset dataset = findDatasetOrDie(idSupplied); String reasonForReturn = null; reasonForReturn = json.getString("reasonForReturn"); - // TODO: Once we add a box for the curator to type into, pass the reason for return to the ReturnDatasetToAuthorCommand and delete this check and call to setReturnReason on the API side. - if (reasonForReturn == null || reasonForReturn.isEmpty()) { - return error(Response.Status.BAD_REQUEST, "You must enter a reason for returning a dataset to the author(s)."); + if ((reasonForReturn == null || reasonForReturn.isEmpty()) + && !FeatureFlags.DISABLE_RETURN_TO_AUTHOR_REASON.enabled()) { + return error(Response.Status.BAD_REQUEST, BundleUtil.getStringFromBundle("dataset.reject.datasetNotInReview")); } AuthenticatedUser authenticatedUser = getRequestAuthenticatedUserOrDie(crc); Dataset updatedDataset = execCommand(new ReturnDatasetToAuthorCommand(createDataverseRequest(authenticatedUser), dataset, reasonForReturn )); @@ -2219,42 +2575,6 @@ public Response deleteCurationStatus(@Context ContainerRequestContext crc, @Path } } - @GET - @AuthRequired - @Path("{id}/uploadsid") - @Deprecated - public Response getUploadUrl(@Context ContainerRequestContext crc, @PathParam("id") String idSupplied) { - try { - Dataset dataset = findDatasetOrDie(idSupplied); - - boolean canUpdateDataset = false; - canUpdateDataset = permissionSvc.requestOn(createDataverseRequest(getRequestUser(crc)), dataset).canIssue(UpdateDatasetVersionCommand.class); - if (!canUpdateDataset) { - return error(Response.Status.FORBIDDEN, "You are not permitted to upload files to this dataset."); - } - S3AccessIO s3io = FileUtil.getS3AccessForDirectUpload(dataset); - if (s3io == null) { - return error(Response.Status.NOT_FOUND, "Direct upload not supported for files in this dataset: " + dataset.getId()); - } - String url = null; - String storageIdentifier = null; - try { - url = s3io.generateTemporaryS3UploadUrl(); - storageIdentifier = FileUtil.getStorageIdentifierFromLocation(s3io.getStorageLocation()); - } catch (IOException io) { - logger.warning(io.getMessage()); - throw new WrappedResponse(io, error(Response.Status.INTERNAL_SERVER_ERROR, "Could not create process direct upload request")); - } - - JsonObjectBuilder response = Json.createObjectBuilder() - .add("url", url) - .add("storageIdentifier", storageIdentifier); - return ok(response); - } catch (WrappedResponse wr) { - return wr.getResponse(); - } - } - @GET @AuthRequired @Path("{id}/uploadurls") @@ -2273,6 +2593,22 @@ public Response getMPUploadUrls(@Context ContainerRequestContext crc, @PathParam return error(Response.Status.NOT_FOUND, "Direct upload not supported for files in this dataset: " + dataset.getId()); } + Long maxSize = systemConfig.getMaxFileUploadSizeForStore(dataset.getEffectiveStorageDriverId()); + if (maxSize != null) { + if(fileSize > maxSize) { + return error(Response.Status.BAD_REQUEST, + "The file you are trying to upload is too large to be uploaded to this dataset. " + + "The maximum allowed file size is " + maxSize + " bytes."); + } + } + UploadSessionQuotaLimit limit = fileService.getUploadSessionQuotaLimit(dataset); + if (limit != null) { + if(fileSize > limit.getRemainingQuotaInBytes()) { + return error(Response.Status.BAD_REQUEST, + "The file you are trying to upload is too large to be uploaded to this dataset. " + + "The remaing file size quota is " + limit.getRemainingQuotaInBytes() + " bytes."); + } + } JsonObjectBuilder response = null; String storageIdentifier = null; try { @@ -2435,6 +2771,14 @@ public Response completeMPUpload(@Context ContainerRequestContext crc, String pa @AuthRequired @Path("{id}/add") @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces("application/json") + @Operation(summary = "Uploads a file for a dataset", + description = "Uploads a file for a dataset") + @APIResponse(responseCode = "200", + description = "File uploaded successfully to dataset") + @Tag(name = "addFileToDataset", + description = "Uploads a file for a dataset") + @RequestBody(content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA)) public Response addFileToDataset(@Context ContainerRequestContext crc, @PathParam("id") String idSupplied, @FormDataParam("jsonData") String jsonData, @@ -2711,46 +3055,49 @@ public static T handleVersion(String versionId, DsVersionHandler hdl) * includeDeaccessioned default to false and checkPermsWhenDeaccessioned to false. Use it only when you are sure that the you don't need to work with * a deaccessioned dataset. */ - private DatasetVersion getDatasetVersionOrDie(final DataverseRequest req, String versionNumber, final Dataset ds, UriInfo uriInfo, HttpHeaders headers) throws WrappedResponse { + private DatasetVersion getDatasetVersionOrDie(final DataverseRequest req, + String versionNumber, + final Dataset ds, + UriInfo uriInfo, + HttpHeaders headers) throws WrappedResponse { //The checkPerms was added to check the permissions ONLY when the dataset is deaccessioned. - return getDatasetVersionOrDie(req, versionNumber, ds, uriInfo, headers, false, false); + boolean checkFilePerms = false; + boolean includeDeaccessioned = false; + return getDatasetVersionOrDie(req, versionNumber, ds, uriInfo, headers, includeDeaccessioned, checkFilePerms); } /* * checkPermsWhenDeaccessioned default to true. Be aware that the version will be only be obtainable if the user has edit permissions. */ - private DatasetVersion getDatasetVersionOrDie(final DataverseRequest req, String versionNumber, final Dataset ds, UriInfo uriInfo, HttpHeaders headers, boolean includeDeaccessioned) throws WrappedResponse{ - return getDatasetVersionOrDie(req, versionNumber, ds, uriInfo, headers, includeDeaccessioned, true); + private DatasetVersion getDatasetVersionOrDie(final DataverseRequest req, String versionNumber, final Dataset ds, + UriInfo uriInfo, HttpHeaders headers, boolean includeDeaccessioned) throws WrappedResponse { + boolean checkPermsWhenDeaccessioned = true; + boolean bypassAccessCheck = false; + return getDatasetVersionOrDie(req, versionNumber, ds, uriInfo, headers, includeDeaccessioned, checkPermsWhenDeaccessioned, bypassAccessCheck); } /* - * Will allow to define when the permissions should be checked when a deaccesioned dataset is requested. If the user doesn't have edit permissions will result in an error. + * checkPermsWhenDeaccessioned default to true. Be aware that the version will be only be obtainable if the user has edit permissions. */ - private DatasetVersion getDatasetVersionOrDie(final DataverseRequest req, String versionNumber, final Dataset ds, UriInfo uriInfo, HttpHeaders headers, boolean includeDeaccessioned, boolean checkPermsWhenDeaccessioned) throws WrappedResponse { - DatasetVersion dsv = execCommand(handleVersion(versionNumber, new DsVersionHandler>() { - - @Override - public Command handleLatest() { - return new GetLatestAccessibleDatasetVersionCommand(req, ds, includeDeaccessioned, checkPermsWhenDeaccessioned); - } + private DatasetVersion getDatasetVersionOrDie(final DataverseRequest req, String versionNumber, final Dataset ds, + UriInfo uriInfo, HttpHeaders headers, boolean includeDeaccessioned, boolean checkPermsWhenDeaccessioned) throws WrappedResponse { + boolean bypassAccessCheck = false; + return getDatasetVersionOrDie(req, versionNumber, ds, uriInfo, headers, includeDeaccessioned, checkPermsWhenDeaccessioned, bypassAccessCheck); + } - @Override - public Command handleDraft() { - return new GetDraftDatasetVersionCommand(req, ds); - } + /* + * Will allow to define when the permissions should be checked when a deaccesioned dataset is requested. If the user doesn't have edit permissions will result in an error. + */ + private DatasetVersion getDatasetVersionOrDie(final DataverseRequest req, String versionNumber, final Dataset ds, + UriInfo uriInfo, HttpHeaders headers, boolean includeDeaccessioned, boolean checkPermsWhenDeaccessioned, + boolean bypassAccessCheck) + throws WrappedResponse { - @Override - public Command handleSpecific(long major, long minor) { - return new GetSpecificPublishedDatasetVersionCommand(req, ds, major, minor, includeDeaccessioned, checkPermsWhenDeaccessioned); - } + DatasetVersion dsv = findDatasetVersionOrDie(req, versionNumber, ds, includeDeaccessioned, checkPermsWhenDeaccessioned); - @Override - public Command handleLatestPublished() { - return new GetLatestPublishedDatasetVersionCommand(req, ds, includeDeaccessioned, checkPermsWhenDeaccessioned); - } - })); if (dsv == null || dsv.getId() == null) { - throw new WrappedResponse(notFound("Dataset version " + versionNumber + " of dataset " + ds.getId() + " not found")); + throw new WrappedResponse( + notFound("Dataset version " + versionNumber + " of dataset " + ds.getId() + " not found")); } if (dsv.isReleased()&& uriInfo!=null) { MakeDataCountLoggingServiceBean.MakeDataCountEntry entry = new MakeDataCountEntry(uriInfo, headers, dvRequestService, ds); @@ -2758,7 +3105,7 @@ public Command handleLatestPublished() { } return dsv; } - + @GET @Path("{identifier}/locks") public Response getLocksForDataset(@PathParam("identifier") String id, @QueryParam("type") DatasetLock.Reason lockType) { @@ -3505,6 +3852,16 @@ public Response getGlobusUploadParams(@Context ContainerRequestContext crc, @Pat params.add(key, substitutedParams.get(key)); }); params.add("managed", Boolean.toString(managed)); + if (managed) { + Long maxSize = systemConfig.getMaxFileUploadSizeForStore(storeId); + if (maxSize != null) { + params.add("fileSizeLimit", maxSize); + } + UploadSessionQuotaLimit limit = fileService.getUploadSessionQuotaLimit(dataset); + if (limit != null) { + params.add("remainingQuota", limit.getRemainingQuotaInBytes()); + } + } if (transferEndpoint != null) { params.add("endpoint", transferEndpoint); } else { @@ -3563,7 +3920,7 @@ public Response requestGlobusUpload(@Context ContainerRequestContext crc, @PathP if (!systemConfig.isGlobusUpload()) { return error(Response.Status.SERVICE_UNAVAILABLE, - BundleUtil.getStringFromBundle("datasets.api.globusdownloaddisabled")); + BundleUtil.getStringFromBundle("file.api.globusUploadDisabled")); } // ------------------------------------- @@ -3647,6 +4004,14 @@ public Response requestGlobusUpload(@Context ContainerRequestContext crc, @PathP @AuthRequired @Path("{id}/addGlobusFiles") @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces("application/json") + @Operation(summary = "Uploads a Globus file for a dataset", + description = "Uploads a Globus file for a dataset") + @APIResponse(responseCode = "200", + description = "Globus file uploaded successfully to dataset") + @Tag(name = "addGlobusFilesToDataset", + description = "Uploads a Globus file for a dataset") + @RequestBody(content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA)) public Response addGlobusFilesToDataset(@Context ContainerRequestContext crc, @PathParam("id") String datasetId, @FormDataParam("jsonData") String jsonData, @@ -3655,10 +4020,6 @@ public Response addGlobusFilesToDataset(@Context ContainerRequestContext crc, logger.info(" ==== (api addGlobusFilesToDataset) jsonData ====== " + jsonData); - if (!systemConfig.isHTTPUpload()) { - return error(Response.Status.SERVICE_UNAVAILABLE, BundleUtil.getStringFromBundle("file.api.httpDisabled")); - } - // ------------------------------------- // (1) Get the user from the API key // ------------------------------------- @@ -3681,6 +4042,32 @@ public Response addGlobusFilesToDataset(@Context ContainerRequestContext crc, return wr.getResponse(); } + // Is Globus upload service available? + + // ... on this Dataverse instance? + if (!systemConfig.isGlobusUpload()) { + return error(Response.Status.SERVICE_UNAVAILABLE, BundleUtil.getStringFromBundle("file.api.globusUploadDisabled")); + } + + // ... and on this specific Dataset? + String storeId = dataset.getEffectiveStorageDriverId(); + // acceptsGlobusTransfers should only be true for an S3 or globus store + if (!GlobusAccessibleStore.acceptsGlobusTransfers(storeId) + && !GlobusAccessibleStore.allowsGlobusReferences(storeId)) { + return badRequest(BundleUtil.getStringFromBundle("datasets.api.globusuploaddisabled")); + } + + // Check if the dataset is already locked + // We are reusing the code and logic used by various command to determine + // if there are any locks on the dataset that would prevent the current + // users from modifying it: + try { + DataverseRequest dataverseRequest = createDataverseRequest(authUser); + permissionService.checkEditDatasetLock(dataset, dataverseRequest, null); + } catch (IllegalCommandException icex) { + return error(Response.Status.FORBIDDEN, "Dataset " + datasetId + " is locked: " + icex.getLocalizedMessage()); + } + JsonObject jsonObject = null; try { jsonObject = JsonUtil.getJsonObject(jsonData); @@ -3711,18 +4098,18 @@ public Response addGlobusFilesToDataset(@Context ContainerRequestContext crc, logger.log(Level.WARNING, "Failed to lock the dataset (dataset id={0})", dataset.getId()); } - - ApiToken token = authSvc.findApiTokenByUser(authUser); - if(uriInfo != null) { logger.info(" ==== (api uriInfo.getRequestUri()) jsonData ====== " + uriInfo.getRequestUri().toString()); } - String requestUrl = SystemConfig.getDataverseSiteUrlStatic(); // Async Call - globusService.globusUpload(jsonObject, token, dataset, requestUrl, authUser); + try { + globusService.globusUpload(jsonObject, dataset, requestUrl, authUser); + } catch (IllegalArgumentException ex) { + return badRequest("Invalid parameters: "+ex.getMessage()); + } return ok("Async call to Globus Upload started "); @@ -3749,11 +4136,11 @@ public Response getGlobusDownloadParams(@Context ContainerRequestContext crc, @P // ------------------------------------- // (1) Get the user from the ContainerRequestContext // ------------------------------------- - AuthenticatedUser authUser; + AuthenticatedUser authUser = null; try { authUser = getRequestAuthenticatedUserOrDie(crc); } catch (WrappedResponse e) { - return e.getResponse(); + logger.fine("guest user globus download"); } // ------------------------------------- // (2) Get the Dataset Id @@ -4029,6 +4416,14 @@ public Response monitorGlobusDownload(@Context ContainerRequestContext crc, @Pat @AuthRequired @Path("{id}/addFiles") @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces("application/json") + @Operation(summary = "Uploads a set of files to a dataset", + description = "Uploads a set of files to a dataset") + @APIResponse(responseCode = "200", + description = "Files uploaded successfully to dataset") + @Tag(name = "addFilesToDataset", + description = "Uploads a set of files to a dataset") + @RequestBody(content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA)) public Response addFilesToDataset(@Context ContainerRequestContext crc, @PathParam("id") String idSupplied, @FormDataParam("jsonData") String jsonData) { @@ -4096,6 +4491,14 @@ public Response addFilesToDataset(@Context ContainerRequestContext crc, @PathPar @AuthRequired @Path("{id}/replaceFiles") @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces("application/json") + @Operation(summary = "Replace a set of files to a dataset", + description = "Replace a set of files to a dataset") + @APIResponse(responseCode = "200", + description = "Files replaced successfully to dataset") + @Tag(name = "replaceFilesInDataset", + description = "Replace a set of files to a dataset") + @RequestBody(content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA)) public Response replaceFilesInDataset(@Context ContainerRequestContext crc, @PathParam("id") String idSupplied, @FormDataParam("jsonData") String jsonData) { @@ -4407,7 +4810,7 @@ public Response getDatasetSummaryFieldNames() { @GET @Path("privateUrlDatasetVersion/{privateUrlToken}") - public Response getPrivateUrlDatasetVersion(@PathParam("privateUrlToken") String privateUrlToken) { + public Response getPrivateUrlDatasetVersion(@PathParam("privateUrlToken") String privateUrlToken, @QueryParam("returnOwners") boolean returnOwners) { PrivateUrlUser privateUrlUser = privateUrlService.getPrivateUrlUserFromToken(privateUrlToken); if (privateUrlUser == null) { return notFound("Private URL user not found"); @@ -4424,9 +4827,9 @@ public Response getPrivateUrlDatasetVersion(@PathParam("privateUrlToken") String JsonObjectBuilder responseJson; if (isAnonymizedAccess) { List anonymizedFieldTypeNamesList = new ArrayList<>(Arrays.asList(anonymizedFieldTypeNames.split(",\\s"))); - responseJson = json(dsv, anonymizedFieldTypeNamesList, true); + responseJson = json(dsv, anonymizedFieldTypeNamesList, true, returnOwners); } else { - responseJson = json(dsv, true); + responseJson = json(dsv, null, true, returnOwners); } return ok(responseJson); } @@ -4452,8 +4855,11 @@ public Response getDatasetVersionCitation(@Context ContainerRequestContext crc, @QueryParam("includeDeaccessioned") boolean includeDeaccessioned, @Context UriInfo uriInfo, @Context HttpHeaders headers) { + boolean checkFilePerms = false; return response(req -> ok( - getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers, includeDeaccessioned, false).getCitation(true, false)), getRequestUser(crc)); + getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers, + includeDeaccessioned, checkFilePerms).getCitation(true, false)), + getRequestUser(crc)); } @POST @@ -4477,7 +4883,10 @@ public Response deaccessionDataset(@Context ContainerRequestContext crc, @PathPa } } execCommand(new DeaccessionDatasetVersionCommand(req, datasetVersion, false)); - return ok("Dataset " + datasetId + " deaccessioned for version " + versionId); + + return ok("Dataset " + + (":persistentId".equals(datasetId) ? datasetVersion.getDataset().getGlobalId().asString() : datasetId) + + " deaccessioned for version " + versionId); } catch (JsonParsingException jpe) { return error(Response.Status.BAD_REQUEST, "Error parsing Json: " + jpe.getMessage()); } @@ -4603,4 +5012,223 @@ public Response getCanDownloadAtLeastOneFile(@Context ContainerRequestContext cr return ok(permissionService.canDownloadAtLeastOneFile(req, datasetVersion)); }, getRequestUser(crc)); } + + /** + * Get the PidProvider that will be used for generating new DOIs in this dataset + * + * @return - the id of the effective PID generator for the given dataset + * @throws WrappedResponse + */ + @GET + @AuthRequired + @Path("{identifier}/pidGenerator") + public Response getPidGenerator(@Context ContainerRequestContext crc, @PathParam("identifier") String dvIdtf, + @Context HttpHeaders headers) throws WrappedResponse { + + Dataset dataset; + + try { + dataset = findDatasetOrDie(dvIdtf); + } catch (WrappedResponse ex) { + return error(Response.Status.NOT_FOUND, "No such dataset"); + } + PidProvider pidProvider = dataset.getEffectivePidGenerator(); + if(pidProvider == null) { + //This is basically a config error, e.g. if a valid pid provider was removed after this dataset used it + return error(Response.Status.NOT_FOUND, BundleUtil.getStringFromBundle("datasets.api.pidgenerator.notfound")); + } + String pidGeneratorId = pidProvider.getId(); + return ok(pidGeneratorId); + } + + @PUT + @AuthRequired + @Path("{identifier}/pidGenerator") + public Response setPidGenerator(@Context ContainerRequestContext crc, @PathParam("identifier") String datasetId, + String generatorId, @Context HttpHeaders headers) throws WrappedResponse { + + // Superuser-only: + AuthenticatedUser user; + try { + user = getRequestAuthenticatedUserOrDie(crc); + } catch (WrappedResponse ex) { + return error(Response.Status.UNAUTHORIZED, "Authentication is required."); + } + if (!user.isSuperuser()) { + return error(Response.Status.FORBIDDEN, "Superusers only."); + } + + Dataset dataset; + + try { + dataset = findDatasetOrDie(datasetId); + } catch (WrappedResponse ex) { + return error(Response.Status.NOT_FOUND, "No such dataset"); + } + if (PidUtil.getManagedProviderIds().contains(generatorId)) { + dataset.setPidGeneratorId(generatorId); + datasetService.merge(dataset); + return ok("PID Generator set to: " + generatorId); + } else { + return error(Response.Status.NOT_FOUND, "No PID Generator found for the give id"); + } + + } + + @DELETE + @AuthRequired + @Path("{identifier}/pidGenerator") + public Response resetPidGenerator(@Context ContainerRequestContext crc, @PathParam("identifier") String dvIdtf, + @Context HttpHeaders headers) throws WrappedResponse { + + // Superuser-only: + AuthenticatedUser user; + try { + user = getRequestAuthenticatedUserOrDie(crc); + } catch (WrappedResponse ex) { + return error(Response.Status.BAD_REQUEST, "Authentication is required."); + } + if (!user.isSuperuser()) { + return error(Response.Status.FORBIDDEN, "Superusers only."); + } + + Dataset dataset; + + try { + dataset = findDatasetOrDie(dvIdtf); + } catch (WrappedResponse ex) { + return error(Response.Status.NOT_FOUND, "No such dataset"); + } + + dataset.setPidGenerator(null); + datasetService.merge(dataset); + return ok("Pid Generator reset to default: " + dataset.getEffectivePidGenerator().getId()); + } + + @GET + @Path("datasetTypes") + public Response getDatasetTypes() { + JsonArrayBuilder jab = Json.createArrayBuilder(); + List datasetTypes = datasetTypeSvc.listAll(); + for (DatasetType datasetType : datasetTypes) { + JsonObjectBuilder job = Json.createObjectBuilder(); + job.add("id", datasetType.getId()); + job.add("name", datasetType.getName()); + jab.add(job); + } + return ok(jab.build()); + } + + @GET + @Path("datasetTypes/{idOrName}") + public Response getDatasetTypes(@PathParam("idOrName") String idOrName) { + DatasetType datasetType = null; + if (StringUtils.isNumeric(idOrName)) { + try { + long id = Long.parseLong(idOrName); + datasetType = datasetTypeSvc.getById(id); + } catch (NumberFormatException ex) { + return error(NOT_FOUND, "Could not find a dataset type with id " + idOrName); + } + } else { + datasetType = datasetTypeSvc.getByName(idOrName); + } + if (datasetType != null) { + return ok(datasetType.toJson()); + } else { + return error(NOT_FOUND, "Could not find a dataset type with name " + idOrName); + } + } + + @POST + @AuthRequired + @Path("datasetTypes") + public Response addDatasetType(@Context ContainerRequestContext crc, String jsonIn) { + AuthenticatedUser user; + try { + user = getRequestAuthenticatedUserOrDie(crc); + } catch (WrappedResponse ex) { + return error(Response.Status.BAD_REQUEST, "Authentication is required."); + } + if (!user.isSuperuser()) { + return error(Response.Status.FORBIDDEN, "Superusers only."); + } + + if (jsonIn == null || jsonIn.isEmpty()) { + return error(BAD_REQUEST, "JSON input was null or empty!"); + } + + String nameIn = null; + try { + JsonObject jsonObject = JsonUtil.getJsonObject(jsonIn); + nameIn = jsonObject.getString("name", null); + } catch (JsonParsingException ex) { + return error(BAD_REQUEST, "Problem parsing supplied JSON: " + ex.getLocalizedMessage()); + } + if (nameIn == null) { + return error(BAD_REQUEST, "A name for the dataset type is required"); + } + if (StringUtils.isNumeric(nameIn)) { + // getDatasetTypes supports id or name so we don't want a names that looks like an id + return error(BAD_REQUEST, "The name of the type cannot be only digits."); + } + + try { + DatasetType datasetType = new DatasetType(); + datasetType.setName(nameIn); + DatasetType saved = datasetTypeSvc.save(datasetType); + Long typeId = saved.getId(); + String name = saved.getName(); + return ok(saved.toJson()); + } catch (WrappedResponse ex) { + return error(BAD_REQUEST, ex.getMessage()); + } + } + + @DELETE + @AuthRequired + @Path("datasetTypes/{id}") + public Response deleteDatasetType(@Context ContainerRequestContext crc, @PathParam("id") String doomed) { + AuthenticatedUser user; + try { + user = getRequestAuthenticatedUserOrDie(crc); + } catch (WrappedResponse ex) { + return error(Response.Status.BAD_REQUEST, "Authentication is required."); + } + if (!user.isSuperuser()) { + return error(Response.Status.FORBIDDEN, "Superusers only."); + } + + if (doomed == null || doomed.isEmpty()) { + throw new IllegalArgumentException("ID is required!"); + } + + long idToDelete; + try { + idToDelete = Long.parseLong(doomed); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("ID must be a number"); + } + + DatasetType datasetTypeToDelete = datasetTypeSvc.getById(idToDelete); + if (datasetTypeToDelete == null) { + return error(BAD_REQUEST, "Could not find dataset type with id " + idToDelete); + } + + if (DatasetType.DEFAULT_DATASET_TYPE.equals(datasetTypeToDelete.getName())) { + return error(Status.FORBIDDEN, "You cannot delete the default dataset type: " + DatasetType.DEFAULT_DATASET_TYPE); + } + + try { + int numDeleted = datasetTypeSvc.deleteById(idToDelete); + if (numDeleted == 1) { + return ok("deleted"); + } else { + return error(BAD_REQUEST, "Something went wrong. Number of dataset types deleted: " + numDeleted); + } + } catch (WrappedResponse ex) { + return error(BAD_REQUEST, ex.getMessage()); + } + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index 6c1bf42c02a..0ee146ed99b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -1,25 +1,10 @@ package edu.harvard.iq.dataverse.api; -import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.Dataset; -import edu.harvard.iq.dataverse.DatasetFieldType; -import edu.harvard.iq.dataverse.DatasetVersion; -import edu.harvard.iq.dataverse.Dataverse; -import edu.harvard.iq.dataverse.DataverseFacet; -import edu.harvard.iq.dataverse.DataverseContact; -import edu.harvard.iq.dataverse.DataverseMetadataBlockFacet; -import edu.harvard.iq.dataverse.DataverseServiceBean; +import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.api.auth.AuthRequired; import edu.harvard.iq.dataverse.api.datadeposit.SwordServiceBean; import edu.harvard.iq.dataverse.api.dto.DataverseMetadataBlockFacetDTO; import edu.harvard.iq.dataverse.authorization.DataverseRole; -import edu.harvard.iq.dataverse.DvObject; -import edu.harvard.iq.dataverse.GlobalId; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; -import edu.harvard.iq.dataverse.GuestbookResponseServiceBean; -import edu.harvard.iq.dataverse.GuestbookServiceBean; -import edu.harvard.iq.dataverse.MetadataBlock; -import edu.harvard.iq.dataverse.RoleAssignment; import edu.harvard.iq.dataverse.api.dto.ExplicitGroupDTO; import edu.harvard.iq.dataverse.api.dto.RoleAssignmentDTO; @@ -35,74 +20,30 @@ import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.dataverse.DataverseUtil; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; -import edu.harvard.iq.dataverse.engine.command.impl.AddRoleAssigneesToExplicitGroupCommand; -import edu.harvard.iq.dataverse.engine.command.impl.AssignRoleCommand; -import edu.harvard.iq.dataverse.engine.command.impl.CreateDataverseCommand; -import edu.harvard.iq.dataverse.engine.command.impl.CreateExplicitGroupCommand; -import edu.harvard.iq.dataverse.engine.command.impl.CreateNewDatasetCommand; -import edu.harvard.iq.dataverse.engine.command.impl.CreateRoleCommand; -import edu.harvard.iq.dataverse.engine.command.impl.DeleteCollectionQuotaCommand; -import edu.harvard.iq.dataverse.engine.command.impl.DeleteDataverseCommand; -import edu.harvard.iq.dataverse.engine.command.impl.DeleteDataverseLinkingDataverseCommand; -import edu.harvard.iq.dataverse.engine.command.impl.DeleteExplicitGroupCommand; -import edu.harvard.iq.dataverse.engine.command.impl.GetDatasetSchemaCommand; -import edu.harvard.iq.dataverse.engine.command.impl.GetCollectionQuotaCommand; -import edu.harvard.iq.dataverse.engine.command.impl.GetCollectionStorageUseCommand; -import edu.harvard.iq.dataverse.engine.command.impl.UpdateMetadataBlockFacetRootCommand; -import edu.harvard.iq.dataverse.engine.command.impl.GetDataverseCommand; -import edu.harvard.iq.dataverse.engine.command.impl.GetDataverseStorageSizeCommand; -import edu.harvard.iq.dataverse.engine.command.impl.GetExplicitGroupCommand; -import edu.harvard.iq.dataverse.engine.command.impl.ImportDatasetCommand; -import edu.harvard.iq.dataverse.engine.command.impl.LinkDataverseCommand; -import edu.harvard.iq.dataverse.engine.command.impl.ListDataverseContentCommand; -import edu.harvard.iq.dataverse.engine.command.impl.ListExplicitGroupsCommand; -import edu.harvard.iq.dataverse.engine.command.impl.ListFacetsCommand; -import edu.harvard.iq.dataverse.engine.command.impl.ListMetadataBlockFacetsCommand; -import edu.harvard.iq.dataverse.engine.command.impl.ListMetadataBlocksCommand; -import edu.harvard.iq.dataverse.engine.command.impl.ListRoleAssignments; -import edu.harvard.iq.dataverse.engine.command.impl.ListRolesCommand; -import edu.harvard.iq.dataverse.engine.command.impl.PublishDatasetCommand; -import edu.harvard.iq.dataverse.engine.command.impl.PublishDatasetResult; -import edu.harvard.iq.dataverse.engine.command.impl.MoveDataverseCommand; -import edu.harvard.iq.dataverse.engine.command.impl.PublishDataverseCommand; -import edu.harvard.iq.dataverse.engine.command.impl.RemoveRoleAssigneesFromExplicitGroupCommand; -import edu.harvard.iq.dataverse.engine.command.impl.RevokeRoleCommand; -import edu.harvard.iq.dataverse.engine.command.impl.SetCollectionQuotaCommand; -import edu.harvard.iq.dataverse.engine.command.impl.UpdateDataverseCommand; -import edu.harvard.iq.dataverse.engine.command.impl.UpdateDataverseDefaultContributorRoleCommand; -import edu.harvard.iq.dataverse.engine.command.impl.UpdateDataverseMetadataBlocksCommand; -import edu.harvard.iq.dataverse.engine.command.impl.UpdateExplicitGroupCommand; -import edu.harvard.iq.dataverse.engine.command.impl.UpdateMetadataBlockFacetsCommand; -import edu.harvard.iq.dataverse.engine.command.impl.ValidateDatasetJsonCommand; +import edu.harvard.iq.dataverse.engine.command.impl.*; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.ConstraintViolationUtil; import edu.harvard.iq.dataverse.util.StringUtil; import static edu.harvard.iq.dataverse.util.StringUtil.nonEmpty; +import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*; import edu.harvard.iq.dataverse.util.json.JSONLDUtil; import edu.harvard.iq.dataverse.util.json.JsonParseException; import edu.harvard.iq.dataverse.util.json.JsonPrinter; import edu.harvard.iq.dataverse.util.json.JsonUtil; -import static edu.harvard.iq.dataverse.util.json.JsonPrinter.brief; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.TreeSet; +import java.io.StringReader; +import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import jakarta.ejb.EJB; import jakarta.ejb.EJBException; import jakarta.ejb.Stateless; -import jakarta.json.Json; -import jakarta.json.JsonArrayBuilder; -import jakarta.json.JsonNumber; -import jakarta.json.JsonObject; -import jakarta.json.JsonObjectBuilder; -import jakarta.json.JsonString; -import jakarta.json.JsonValue; +import jakarta.json.*; import jakarta.json.JsonValue.ValueType; import jakarta.json.stream.JsonParsingException; import jakarta.validation.ConstraintViolationException; @@ -120,16 +61,11 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; -import static edu.harvard.iq.dataverse.util.json.JsonPrinter.toJsonArray; -import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; + import java.io.IOException; import java.io.OutputStream; import java.text.MessageFormat; import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.Date; -import java.util.Map; -import java.util.Optional; import java.util.stream.Collectors; import jakarta.servlet.http.HttpServletResponse; import jakarta.ws.rs.WebApplicationException; @@ -167,8 +103,17 @@ public class Dataverses extends AbstractApiBean { @EJB DataverseServiceBean dataverseService; + @EJB + DataverseLinkingServiceBean linkingService; + + @EJB + FeaturedDataverseServiceBean featuredDataverseService; + @EJB SwordServiceBean swordService; + + @EJB + PermissionServiceBean permissionService; @POST @AuthRequired @@ -181,35 +126,48 @@ public Response addRoot(@Context ContainerRequestContext crc, String body) { @AuthRequired @Path("{identifier}") public Response addDataverse(@Context ContainerRequestContext crc, String body, @PathParam("identifier") String parentIdtf) { - - Dataverse d; - JsonObject dvJson; + Dataverse newDataverse; + JsonObject newDataverseJson; try { - dvJson = JsonUtil.getJsonObject(body); - d = jsonParser().parseDataverse(dvJson); + newDataverseJson = JsonUtil.getJsonObject(body); + newDataverse = jsonParser().parseDataverse(newDataverseJson); } catch (JsonParsingException jpe) { logger.log(Level.SEVERE, "Json: {0}", body); - return error(Status.BAD_REQUEST, "Error parsing Json: " + jpe.getMessage()); + return error(Status.BAD_REQUEST, MessageFormat.format(BundleUtil.getStringFromBundle("dataverse.create.error.jsonparse"), jpe.getMessage())); } catch (JsonParseException ex) { logger.log(Level.SEVERE, "Error parsing dataverse from json: " + ex.getMessage(), ex); - return error(Response.Status.BAD_REQUEST, - "Error parsing the POSTed json into a dataverse: " + ex.getMessage()); + return error(Status.BAD_REQUEST, MessageFormat.format(BundleUtil.getStringFromBundle("dataverse.create.error.jsonparsetodataverse"), ex.getMessage())); } try { + JsonObject metadataBlocksJson = newDataverseJson.getJsonObject("metadataBlocks"); + List inputLevels = null; + List metadataBlocks = null; + List facetList = null; + if (metadataBlocksJson != null) { + JsonArray inputLevelsArray = metadataBlocksJson.getJsonArray("inputLevels"); + inputLevels = inputLevelsArray != null ? parseInputLevels(inputLevelsArray, newDataverse) : null; + + JsonArray metadataBlockNamesArray = metadataBlocksJson.getJsonArray("metadataBlockNames"); + metadataBlocks = metadataBlockNamesArray != null ? parseNewDataverseMetadataBlocks(metadataBlockNamesArray) : null; + + JsonArray facetIdsArray = metadataBlocksJson.getJsonArray("facetIds"); + facetList = facetIdsArray != null ? parseFacets(facetIdsArray) : null; + } + if (!parentIdtf.isEmpty()) { Dataverse owner = findDataverseOrDie(parentIdtf); - d.setOwner(owner); + newDataverse.setOwner(owner); } // set the dataverse - contact relationship in the contacts - for (DataverseContact dc : d.getDataverseContacts()) { - dc.setDataverse(d); + for (DataverseContact dc : newDataverse.getDataverseContacts()) { + dc.setDataverse(newDataverse); } AuthenticatedUser u = getRequestAuthenticatedUserOrDie(crc); - d = execCommand(new CreateDataverseCommand(d, createDataverseRequest(u), null, null)); - return created("/dataverses/" + d.getAlias(), json(d)); + newDataverse = execCommand(new CreateDataverseCommand(newDataverse, createDataverseRequest(u), facetList, inputLevels, metadataBlocks)); + return created("/dataverses/" + newDataverse.getAlias(), json(newDataverse)); } catch (WrappedResponse ww) { String error = ConstraintViolationUtil.getErrorStringForConstraintViolations(ww.getCause()); @@ -237,7 +195,21 @@ public Response addDataverse(@Context ContainerRequestContext crc, String body, } } - + + private List parseNewDataverseMetadataBlocks(JsonArray metadataBlockNamesArray) throws WrappedResponse { + List selectedMetadataBlocks = new ArrayList<>(); + for (JsonString metadataBlockName : metadataBlockNamesArray.getValuesAs(JsonString.class)) { + MetadataBlock metadataBlock = metadataBlockSvc.findByName(metadataBlockName.getString()); + if (metadataBlock == null) { + String errorMessage = MessageFormat.format(BundleUtil.getStringFromBundle("dataverse.metadatablocks.error.invalidmetadatablockname"), metadataBlockName); + throw new WrappedResponse(badRequest(errorMessage)); + } + selectedMetadataBlocks.add(metadataBlock); + } + + return selectedMetadataBlocks; + } + @POST @AuthRequired @Path("{identifier}/validateDatasetJson") @@ -360,7 +332,7 @@ public Response createDatasetFromJsonLd(@Context ContainerRequestContext crc, St Dataset ds = new Dataset(); ds.setOwner(owner); - ds = JSONLDUtil.updateDatasetMDFromJsonLD(ds, jsonLDBody, metadataBlockSvc, datasetFieldSvc, false, false, licenseSvc); + ds = JSONLDUtil.updateDatasetMDFromJsonLD(ds, jsonLDBody, metadataBlockSvc, datasetFieldSvc, false, false, licenseSvc, datasetTypeSvc); ds.setOwner(owner); @@ -389,6 +361,8 @@ public Response createDatasetFromJsonLd(@Context ContainerRequestContext crc, St } catch (WrappedResponse ex) { return ex.getResponse(); + } catch (Exception ex) { + return error(Status.BAD_REQUEST, ex.getLocalizedMessage()); } } @@ -421,7 +395,7 @@ public Response importDataset(@Context ContainerRequestContext crc, String jsonB if (!GlobalId.verifyImportCharacters(pidParam)) { return badRequest("PID parameter contains characters that are not allowed by the Dataverse application. On import, the PID must only contain characters specified in this regex: " + BundleUtil.getStringFromBundle("pid.allowedCharacters")); } - Optional maybePid = GlobalIdServiceBean.parse(pidParam); + Optional maybePid = PidProvider.parse(pidParam); if (maybePid.isPresent()) { ds.setGlobalId(maybePid.get()); } else { @@ -433,6 +407,12 @@ public Response importDataset(@Context ContainerRequestContext crc, String jsonB if (ds.getIdentifier() == null) { return badRequest("Please provide a persistent identifier, either by including it in the JSON, or by using the pid query parameter."); } + + PidProvider pidProvider = PidUtil.getPidProvider(ds.getGlobalId().getProviderId()); + if (pidProvider == null || !pidProvider.canManagePID()) { + return badRequest("Cannot import a dataset that has a PID that doesn't match the server's settings"); + } + boolean shouldRelease = StringUtil.isTrue(releaseParam); DataverseRequest request = createDataverseRequest(u); @@ -496,7 +476,7 @@ public Response importDatasetDdi(@Context ContainerRequestContext crc, String xm if (!GlobalId.verifyImportCharacters(pidParam)) { return badRequest("PID parameter contains characters that are not allowed by the Dataverse application. On import, the PID must only contain characters specified in this regex: " + BundleUtil.getStringFromBundle("pid.allowedCharacters")); } - Optional maybePid = GlobalIdServiceBean.parse(pidParam); + Optional maybePid = PidProvider.parse(pidParam); if (maybePid.isPresent()) { ds.setGlobalId(maybePid.get()); } else { @@ -541,7 +521,7 @@ public Response importDatasetDdi(@Context ContainerRequestContext crc, String xm return ex.getResponse(); } } - + @POST @AuthRequired @Path("{identifier}/datasets/:startmigration") @@ -557,14 +537,12 @@ public Response recreateDataset(@Context ContainerRequestContext crc, String jso Dataset ds = new Dataset(); ds.setOwner(owner); - ds = JSONLDUtil.updateDatasetMDFromJsonLD(ds, jsonLDBody, metadataBlockSvc, datasetFieldSvc, false, true, licenseSvc); + ds = JSONLDUtil.updateDatasetMDFromJsonLD(ds, jsonLDBody, metadataBlockSvc, datasetFieldSvc, false, true, licenseSvc, datasetTypeSvc); //ToDo - verify PID is one Dataverse can manage (protocol/authority/shoulder match) - if(! - (ds.getAuthority().equals(settingsService.getValueForKey(SettingsServiceBean.Key.Authority))&& - ds.getProtocol().equals(settingsService.getValueForKey(SettingsServiceBean.Key.Protocol))&& - ds.getIdentifier().startsWith(settingsService.getValueForKey(SettingsServiceBean.Key.Shoulder)))) { - throw new BadRequestException("Cannot recreate a dataset that has a PID that doesn't match the server's settings"); - } + if (!PidUtil.getPidProvider(ds.getGlobalId().getProviderId()).canManagePID()) { + throw new BadRequestException( + "Cannot recreate a dataset that has a PID that doesn't match the server's settings"); + } if(!dvObjectSvc.isGlobalIdLocallyUnique(ds.getGlobalId())) { throw new BadRequestException("Cannot recreate a dataset whose PID is already in use"); } @@ -602,6 +580,8 @@ private Dataset parseDataset(String datasetJson) throws WrappedResponse { try { return jsonParser().parseDataset(JsonUtil.getJsonObject(datasetJson)); } catch (JsonParsingException | JsonParseException jpe) { + String message = jpe.getLocalizedMessage(); + logger.log(Level.SEVERE, "Error parsing dataset JSON. message: {0}", message); logger.log(Level.SEVERE, "Error parsing dataset json. Json: {0}", datasetJson); throw new WrappedResponse(error(Status.BAD_REQUEST, "Error parsing Json: " + jpe.getMessage())); } @@ -610,10 +590,11 @@ private Dataset parseDataset(String datasetJson) throws WrappedResponse { @GET @AuthRequired @Path("{identifier}") - public Response viewDataverse(@Context ContainerRequestContext crc, @PathParam("identifier") String idtf) { + public Response getDataverse(@Context ContainerRequestContext crc, @PathParam("identifier") String idtf, @QueryParam("returnOwners") boolean returnOwners) { return response(req -> ok( json(execCommand(new GetDataverseCommand(req, findDataverseOrDie(idtf))), - settingsService.isTrueForKey(SettingsServiceBean.Key.ExcludeEmailFromExport, false) + settingsService.isTrueForKey(SettingsServiceBean.Key.ExcludeEmailFromExport, false), + returnOwners )), getRequestUser(crc)); } @@ -696,6 +677,73 @@ public Response updateAttribute(@Context ContainerRequestContext crc, @PathParam } } + @GET + @AuthRequired + @Path("{identifier}/inputLevels") + public Response getInputLevels(@Context ContainerRequestContext crc, @PathParam("identifier") String identifier) { + try { + Dataverse dataverse = findDataverseOrDie(identifier); + List inputLevels = execCommand(new ListDataverseInputLevelsCommand(createDataverseRequest(getRequestUser(crc)), dataverse)); + return ok(jsonDataverseInputLevels(inputLevels)); + } catch (WrappedResponse e) { + return e.getResponse(); + } + } + + @PUT + @AuthRequired + @Path("{identifier}/inputLevels") + public Response updateInputLevels(@Context ContainerRequestContext crc, @PathParam("identifier") String identifier, String jsonBody) { + try { + Dataverse dataverse = findDataverseOrDie(identifier); + List newInputLevels = parseInputLevels(Json.createReader(new StringReader(jsonBody)).readArray(), dataverse); + execCommand(new UpdateDataverseInputLevelsCommand(dataverse, createDataverseRequest(getRequestUser(crc)), newInputLevels)); + return ok(BundleUtil.getStringFromBundle("dataverse.update.success"), JsonPrinter.json(dataverse)); + } catch (WrappedResponse e) { + return e.getResponse(); + } + } + + private List parseInputLevels(JsonArray inputLevelsArray, Dataverse dataverse) throws WrappedResponse { + List newInputLevels = new ArrayList<>(); + for (JsonValue value : inputLevelsArray) { + JsonObject inputLevel = (JsonObject) value; + String datasetFieldTypeName = inputLevel.getString("datasetFieldTypeName"); + DatasetFieldType datasetFieldType = datasetFieldSvc.findByName(datasetFieldTypeName); + + if (datasetFieldType == null) { + String errorMessage = MessageFormat.format(BundleUtil.getStringFromBundle("dataverse.inputlevels.error.invalidfieldtypename"), datasetFieldTypeName); + throw new WrappedResponse(badRequest(errorMessage)); + } + + boolean required = inputLevel.getBoolean("required"); + boolean include = inputLevel.getBoolean("include"); + + if (required && !include) { + String errorMessage = MessageFormat.format(BundleUtil.getStringFromBundle("dataverse.inputlevels.error.cannotberequiredifnotincluded"), datasetFieldTypeName); + throw new WrappedResponse(badRequest(errorMessage)); + } + + newInputLevels.add(new DataverseFieldTypeInputLevel(datasetFieldType, dataverse, required, include)); + } + + return newInputLevels; + } + + private List parseFacets(JsonArray facetsArray) throws WrappedResponse { + List facets = new LinkedList<>(); + for (JsonString facetId : facetsArray.getValuesAs(JsonString.class)) { + DatasetFieldType dsfType = findDatasetFieldType(facetId.getString()); + if (dsfType == null) { + throw new WrappedResponse(badRequest(MessageFormat.format(BundleUtil.getStringFromBundle("dataverse.facets.error.fieldtypenotfound"), facetId))); + } else if (!dsfType.isFacetable()) { + throw new WrappedResponse(badRequest(MessageFormat.format(BundleUtil.getStringFromBundle("dataverse.facets.error.fieldtypenotfacetable"), facetId))); + } + facets.add(dsfType); + } + return facets; + } + @DELETE @AuthRequired @Path("{linkingDataverseId}/deleteLink/{linkedDataverseId}") @@ -710,14 +758,20 @@ public Response deleteDataverseLinkingDataverse(@Context ContainerRequestContext @GET @AuthRequired @Path("{identifier}/metadatablocks") - public Response listMetadataBlocks(@Context ContainerRequestContext crc, @PathParam("identifier") String dvIdtf) { + public Response listMetadataBlocks(@Context ContainerRequestContext crc, + @PathParam("identifier") String dvIdtf, + @QueryParam("onlyDisplayedOnCreate") boolean onlyDisplayedOnCreate, + @QueryParam("returnDatasetFieldTypes") boolean returnDatasetFieldTypes) { try { - JsonArrayBuilder arr = Json.createArrayBuilder(); - final List blocks = execCommand(new ListMetadataBlocksCommand(createDataverseRequest(getRequestUser(crc)), findDataverseOrDie(dvIdtf))); - for (MetadataBlock mdb : blocks) { - arr.add(brief.json(mdb)); - } - return ok(arr); + Dataverse dataverse = findDataverseOrDie(dvIdtf); + final List metadataBlocks = execCommand( + new ListMetadataBlocksCommand( + createDataverseRequest(getRequestUser(crc)), + dataverse, + onlyDisplayedOnCreate + ) + ); + return ok(json(metadataBlocks, returnDatasetFieldTypes, onlyDisplayedOnCreate, dataverse)); } catch (WrappedResponse we) { return we.getResponse(); } @@ -806,14 +860,45 @@ public Response setMetadataRoot(@Context ContainerRequestContext crc, @PathParam /** * return list of facets for the dataverse with alias `dvIdtf` */ - public Response listFacets(@Context ContainerRequestContext crc, @PathParam("identifier") String dvIdtf) { + public Response listFacets(@Context ContainerRequestContext crc, + @PathParam("identifier") String dvIdtf, + @QueryParam("returnDetails") boolean returnDetails) { + try { + User user = getRequestUser(crc); + DataverseRequest request = createDataverseRequest(user); + Dataverse dataverse = findDataverseOrDie(dvIdtf); + List dataverseFacets = execCommand(new ListFacetsCommand(request, dataverse)); + + if (returnDetails) { + return ok(jsonDataverseFacets(dataverseFacets)); + } else { + JsonArrayBuilder facetsBuilder = Json.createArrayBuilder(); + for (DataverseFacet facet : dataverseFacets) { + facetsBuilder.add(facet.getDatasetFieldType().getName()); + } + return ok(facetsBuilder); + } + } catch (WrappedResponse e) { + return e.getResponse(); + } + } + + @GET + @AuthRequired + @Path("{identifier}/featured") + /* + Allows user to get the collections that are featured by a given collection + probably more for SPA than end user + */ + public Response getFeaturedDataverses(@Context ContainerRequestContext crc, @PathParam("identifier") String dvIdtf, String dvAliases) { + try { User u = getRequestUser(crc); DataverseRequest r = createDataverseRequest(u); Dataverse dataverse = findDataverseOrDie(dvIdtf); JsonArrayBuilder fs = Json.createArrayBuilder(); - for (DataverseFacet f : execCommand(new ListFacetsCommand(r, dataverse))) { - fs.add(f.getDatasetFieldType().getName()); + for (Dataverse f : execCommand(new ListFeaturedCollectionsCommand(r, dataverse))) { + fs.add(f.getAlias()); } return ok(fs); } catch (WrappedResponse e) { @@ -821,6 +906,87 @@ public Response listFacets(@Context ContainerRequestContext crc, @PathParam("ide } } + + @POST + @AuthRequired + @Path("{identifier}/featured") + /** + * Allows user to set featured dataverses - must have edit dataverse permission + * + */ + public Response setFeaturedDataverses(@Context ContainerRequestContext crc, @PathParam("identifier") String dvIdtf, String dvAliases) { + List dvsFromInput = new LinkedList<>(); + + + try { + + for (JsonString dvAlias : Util.asJsonArray(dvAliases).getValuesAs(JsonString.class)) { + Dataverse dvToBeFeatured = dataverseService.findByAlias(dvAlias.getString()); + if (dvToBeFeatured == null) { + return error(Response.Status.BAD_REQUEST, "Can't find dataverse collection with alias '" + dvAlias + "'"); + } + dvsFromInput.add(dvToBeFeatured); + } + + if (dvsFromInput.isEmpty()) { + return error(Response.Status.BAD_REQUEST, "Please provide a valid Json array of dataverse collection aliases to be featured."); + } + + Dataverse dataverse = findDataverseOrDie(dvIdtf); + List featuredSource = new ArrayList<>(); + List featuredTarget = new ArrayList<>(); + featuredSource.addAll(dataverseService.findAllPublishedByOwnerId(dataverse.getId())); + featuredSource.addAll(linkingService.findLinkedDataverses(dataverse.getId())); + List featuredList = featuredDataverseService.findByDataverseId(dataverse.getId()); + + if (featuredSource.isEmpty()) { + return error(Response.Status.BAD_REQUEST, "There are no collections avaialble to be featured in Dataverse collection '" + dataverse.getDisplayName() + "'."); + } + + for (DataverseFeaturedDataverse dfd : featuredList) { + Dataverse fd = dfd.getFeaturedDataverse(); + featuredTarget.add(fd); + featuredSource.remove(fd); + } + + for (Dataverse test : dvsFromInput) { + if (featuredTarget.contains(test)) { + return error(Response.Status.BAD_REQUEST, "Dataverse collection '" + test.getDisplayName() + "' is already featured in Dataverse collection '" + dataverse.getDisplayName() + "'."); + } + + if (featuredSource.contains(test)) { + featuredTarget.add(test); + } else { + return error(Response.Status.BAD_REQUEST, "Dataverse collection '" + test.getDisplayName() + "' may not be featured in Dataverse collection '" + dataverse.getDisplayName() + "'."); + } + + } + // by passing null for Facets and DataverseFieldTypeInputLevel, those are not changed + execCommand(new UpdateDataverseCommand(dataverse, null, featuredTarget, createDataverseRequest(getRequestUser(crc)), null)); + return ok("Featured Dataverses of dataverse " + dvIdtf + " updated."); + + } catch (WrappedResponse ex) { + return ex.getResponse(); + } catch (JsonParsingException jpe){ + return error(Response.Status.BAD_REQUEST, "Please provide a valid Json array of dataverse collection aliases to be featured."); + } + + } + + @DELETE + @AuthRequired + @Path("{identifier}/featured") + public Response deleteFeaturedCollections(@Context ContainerRequestContext crc, @PathParam("identifier") String dvIdtf) throws WrappedResponse { + try { + Dataverse dataverse = findDataverseOrDie(dvIdtf); + List featuredTarget = new ArrayList<>(); + execCommand(new UpdateDataverseCommand(dataverse, null, featuredTarget, createDataverseRequest(getRequestUser(crc)), null)); + return ok(BundleUtil.getStringFromBundle("dataverses.api.delete.featured.collections.successful")); + } catch (WrappedResponse ex) { + return ex.getResponse(); + } + } + @POST @AuthRequired @Path("{identifier}/facets") @@ -833,16 +999,12 @@ public Response listFacets(@Context ContainerRequestContext crc, @PathParam("ide * (judging by the UI). This triggers a 500 when '-d @foo.json' is used. */ public Response setFacets(@Context ContainerRequestContext crc, @PathParam("identifier") String dvIdtf, String facetIds) { - - List facets = new LinkedList<>(); - for (JsonString facetId : Util.asJsonArray(facetIds).getValuesAs(JsonString.class)) { - DatasetFieldType dsfType = findDatasetFieldType(facetId.getString()); - if (dsfType == null) { - return error(Response.Status.BAD_REQUEST, "Can't find dataset field type '" + facetId + "'"); - } else if (!dsfType.isFacetable()) { - return error(Response.Status.BAD_REQUEST, "Dataset field type '" + facetId + "' is not facetable"); - } - facets.add(dsfType); + JsonArray jsonArray = Util.asJsonArray(facetIds); + List facets; + try { + facets = parseFacets(jsonArray); + } catch (WrappedResponse e) { + return e.getResponse(); } try { @@ -1506,4 +1668,25 @@ public Response linkDataverse(@Context ContainerRequestContext crc, @PathParam(" } } + @GET + @AuthRequired + @Path("{identifier}/userPermissions") + public Response getUserPermissionsOnDataverse(@Context ContainerRequestContext crc, @PathParam("identifier") String dvIdtf) { + Dataverse dataverse; + try { + dataverse = findDataverseOrDie(dvIdtf); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + User requestUser = getRequestUser(crc); + JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder(); + jsonObjectBuilder.add("canAddDataverse", permissionService.userOn(requestUser, dataverse).has(Permission.AddDataverse)); + jsonObjectBuilder.add("canAddDataset", permissionService.userOn(requestUser, dataverse).has(Permission.AddDataset)); + jsonObjectBuilder.add("canViewUnpublishedDataverse", permissionService.userOn(requestUser, dataverse).has(Permission.ViewUnpublishedDataverse)); + jsonObjectBuilder.add("canEditDataverse", permissionService.userOn(requestUser, dataverse).has(Permission.EditDataverse)); + jsonObjectBuilder.add("canManageDataversePermissions", permissionService.userOn(requestUser, dataverse).has(Permission.ManageDataversePermissions)); + jsonObjectBuilder.add("canPublishDataverse", permissionService.userOn(requestUser, dataverse).has(Permission.PublishDataverse)); + jsonObjectBuilder.add("canDeleteDataverse", permissionService.userOn(requestUser, dataverse).has(Permission.DeleteDataverse)); + return ok(jsonObjectBuilder); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/DownloadInstanceWriter.java b/src/main/java/edu/harvard/iq/dataverse/api/DownloadInstanceWriter.java index bcb8799ec9e..c815caa09eb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/DownloadInstanceWriter.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/DownloadInstanceWriter.java @@ -22,7 +22,6 @@ import jakarta.ws.rs.ext.Provider; import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.FileMetadata; import edu.harvard.iq.dataverse.dataaccess.*; import edu.harvard.iq.dataverse.datavariable.DataVariable; import edu.harvard.iq.dataverse.engine.command.Command; @@ -104,8 +103,10 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] String auxiliaryTag = null; String auxiliaryType = null; String auxiliaryFileName = null; + // Before we do anything else, check if this download can be handled // by a redirect to remote storage (only supported on S3, as of 5.4): + if (storageIO.downloadRedirectEnabled()) { // Even if the above is true, there are a few cases where a @@ -159,7 +160,7 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] } } else if (dataFile.isTabularData()) { - // Many separate special cases here. + // Many separate special cases here. if (di.getConversionParam() != null) { if (di.getConversionParam().equals("format")) { @@ -180,12 +181,26 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] redirectSupported = false; } } - } else if (!di.getConversionParam().equals("noVarHeader")) { - // This is a subset request - can't do. + } else if (di.getConversionParam().equals("noVarHeader")) { + // This will work just fine, if the tab. file is + // stored without the var. header. Throw "unavailable" + // exception otherwise. + // @todo: should we actually drop support for this "noVarHeader" flag? + if (dataFile.getDataTable().isStoredWithVariableHeader()) { + throw new ServiceUnavailableException(); + } + // ... defaults to redirectSupported = true + } else { + // This must be a subset request then - can't do. + redirectSupported = false; + } + } else { + // "straight" download of the full tab-delimited file. + // can redirect, but only if stored with the variable + // header already added: + if (!dataFile.getDataTable().isStoredWithVariableHeader()) { redirectSupported = false; } - } else { - redirectSupported = false; } } } @@ -247,11 +262,16 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] // finally, issue the redirect: Response response = Response.seeOther(redirect_uri).build(); logger.fine("Issuing redirect to the file location."); + // Yes, this throws an exception. It's not an exception + // as in, "bummer, something went wrong". This is how a + // redirect is produced here! throw new RedirectionException(response); } throw new ServiceUnavailableException(); } + // Past this point, this is a locally served/streamed download + if (di.getConversionParam() != null) { // Image Thumbnail and Tabular data conversion: // NOTE: only supported on local files, as of 4.0.2! @@ -263,7 +283,7 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] storageIO = ImageThumbConverter.getImageThumbnailAsInputStream(storageIO, ImageThumbConverter.DEFAULT_THUMBNAIL_SIZE); } else { try { - int size = new Integer(di.getConversionParamValue()); + int size = Integer.parseInt(di.getConversionParamValue()); if (size > 0) { storageIO = ImageThumbConverter.getImageThumbnailAsInputStream(storageIO, size); } @@ -274,8 +294,10 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] // and, since we now have tabular data files that can // have thumbnail previews... obviously, we don't want to // add the variable header to the image stream! - storageIO.setNoVarHeader(Boolean.TRUE); - storageIO.setVarHeader(null); + if (storageIO != null) { // ImageThumbConverter returns null if thumbnail conversion fails + storageIO.setNoVarHeader(Boolean.TRUE); + storageIO.setVarHeader(null); + } } } else if (dataFile.isTabularData()) { logger.fine("request for tabular data download;"); @@ -285,9 +307,14 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] // request any tabular-specific services. if (di.getConversionParam().equals("noVarHeader")) { - logger.fine("tabular data with no var header requested"); - storageIO.setNoVarHeader(Boolean.TRUE); - storageIO.setVarHeader(null); + if (!dataFile.getDataTable().isStoredWithVariableHeader()) { + logger.fine("tabular data with no var header requested"); + storageIO.setNoVarHeader(Boolean.TRUE); + storageIO.setVarHeader(null); + } else { + logger.fine("can't serve request for tabular data without varheader, since stored with it"); + throw new ServiceUnavailableException(); + } } else if (di.getConversionParam().equals("format")) { // Conversions, and downloads of "stored originals" are // now supported on all DataFiles for which StorageIO @@ -329,11 +356,10 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] if (variable.getDataTable().getDataFile().getId().equals(dataFile.getId())) { logger.fine("adding variable id " + variable.getId() + " to the list."); variablePositionIndex.add(variable.getFileOrder()); - if (subsetVariableHeader == null) { - subsetVariableHeader = variable.getName(); - } else { - subsetVariableHeader = subsetVariableHeader.concat("\t"); - subsetVariableHeader = subsetVariableHeader.concat(variable.getName()); + if (!dataFile.getDataTable().isStoredWithVariableHeader()) { + subsetVariableHeader = subsetVariableHeader == null + ? variable.getName() + : subsetVariableHeader.concat("\t" + variable.getName()); } } else { logger.warning("variable does not belong to this data file."); @@ -346,7 +372,17 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] try { File tempSubsetFile = File.createTempFile("tempSubsetFile", ".tmp"); TabularSubsetGenerator tabularSubsetGenerator = new TabularSubsetGenerator(); - tabularSubsetGenerator.subsetFile(storageIO.getInputStream(), tempSubsetFile.getAbsolutePath(), variablePositionIndex, dataFile.getDataTable().getCaseQuantity(), "\t"); + + long numberOfLines = dataFile.getDataTable().getCaseQuantity(); + if (dataFile.getDataTable().isStoredWithVariableHeader()) { + numberOfLines++; + } + + tabularSubsetGenerator.subsetFile(storageIO.getInputStream(), + tempSubsetFile.getAbsolutePath(), + variablePositionIndex, + numberOfLines, + "\t"); if (tempSubsetFile.exists()) { FileInputStream subsetStream = new FileInputStream(tempSubsetFile); @@ -354,8 +390,11 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] InputStreamIO subsetStreamIO = new InputStreamIO(subsetStream, subsetSize); logger.fine("successfully created subset output stream."); - subsetVariableHeader = subsetVariableHeader.concat("\n"); - subsetStreamIO.setVarHeader(subsetVariableHeader); + + if (subsetVariableHeader != null) { + subsetVariableHeader = subsetVariableHeader.concat("\n"); + subsetStreamIO.setVarHeader(subsetVariableHeader); + } String tabularFileName = storageIO.getFileName(); @@ -380,8 +419,13 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] } else { logger.fine("empty list of extra arguments."); } + // end of tab. data subset case + } else if (dataFile.getDataTable().isStoredWithVariableHeader()) { + logger.fine("tabular file stored with the var header included, no need to generate it on the fly"); + storageIO.setNoVarHeader(Boolean.TRUE); + storageIO.setVarHeader(null); } - } + } // end of tab. data file case if (storageIO == null) { //throw new WebApplicationException(Response.Status.SERVICE_UNAVAILABLE); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/FeedbackApi.java b/src/main/java/edu/harvard/iq/dataverse/api/FeedbackApi.java index 8a178f8da62..56c5ca95ce6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/FeedbackApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/FeedbackApi.java @@ -7,9 +7,6 @@ import edu.harvard.iq.dataverse.branding.BrandingUtil; import edu.harvard.iq.dataverse.feedback.Feedback; import edu.harvard.iq.dataverse.feedback.FeedbackUtil; -import edu.harvard.iq.dataverse.settings.JvmSettings; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; -import edu.harvard.iq.dataverse.util.MailUtil; import jakarta.ejb.EJB; import jakarta.json.Json; @@ -40,7 +37,7 @@ public class FeedbackApi extends AbstractApiBean { * user input (e.g. to strip potentially malicious html, etc.)!!!! **/ @POST - public Response submitFeedback(JsonObject jsonObject) throws AddressException { + public Response submitFeedback(JsonObject jsonObject) { JsonNumber jsonNumber = jsonObject.getJsonNumber("targetId"); DvObject feedbackTarget = null; if (jsonNumber != null) { @@ -51,8 +48,7 @@ public Response submitFeedback(JsonObject jsonObject) throws AddressException { } DataverseSession dataverseSession = null; String userMessage = jsonObject.getString("body"); - String systemEmail = JvmSettings.SUPPORT_EMAIL.lookupOptional().orElse(settingsSvc.getValueForKey(SettingsServiceBean.Key.SystemEmail)); - InternetAddress systemAddress = MailUtil.parseSystemAddress(systemEmail); + InternetAddress systemAddress = mailService.getSupportAddress().orElse(null); String userEmail = jsonObject.getString("fromEmail"); String messageSubject = jsonObject.getString("subject"); String baseUrl = systemConfig.getDataverseSiteUrl(); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index 5d400ee1438..d786aab35a8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -2,22 +2,7 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; -import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.DataFileServiceBean; -import edu.harvard.iq.dataverse.DataFileTag; -import edu.harvard.iq.dataverse.Dataset; -import edu.harvard.iq.dataverse.DatasetLock; -import edu.harvard.iq.dataverse.DatasetServiceBean; -import edu.harvard.iq.dataverse.DatasetVersion; -import edu.harvard.iq.dataverse.DatasetVersionServiceBean; -import edu.harvard.iq.dataverse.DataverseRequestServiceBean; -import edu.harvard.iq.dataverse.DataverseServiceBean; -import edu.harvard.iq.dataverse.EjbDataverseEngine; -import edu.harvard.iq.dataverse.FileDownloadServiceBean; -import edu.harvard.iq.dataverse.FileMetadata; -import edu.harvard.iq.dataverse.GuestbookResponseServiceBean; -import edu.harvard.iq.dataverse.TermsOfUseAndAccessValidator; -import edu.harvard.iq.dataverse.UserNotificationServiceBean; +import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.api.auth.AuthRequired; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.ApiToken; @@ -27,15 +12,11 @@ import edu.harvard.iq.dataverse.datasetutility.DataFileTagException; import edu.harvard.iq.dataverse.datasetutility.NoFilesException; import edu.harvard.iq.dataverse.datasetutility.OptionalFileParams; +import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; -import edu.harvard.iq.dataverse.engine.command.impl.GetDataFileCommand; -import edu.harvard.iq.dataverse.engine.command.impl.GetDraftFileMetadataIfAvailableCommand; -import edu.harvard.iq.dataverse.engine.command.impl.RedetectFileTypeCommand; -import edu.harvard.iq.dataverse.engine.command.impl.RestrictFileCommand; -import edu.harvard.iq.dataverse.engine.command.impl.UningestFileCommand; -import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; +import edu.harvard.iq.dataverse.engine.command.impl.*; import edu.harvard.iq.dataverse.export.ExportService; import io.gdcc.spi.export.ExportException; import edu.harvard.iq.dataverse.externaltools.ExternalTool; @@ -51,6 +32,8 @@ import edu.harvard.iq.dataverse.util.SystemConfig; import edu.harvard.iq.dataverse.util.URLTokenUtil; +import static edu.harvard.iq.dataverse.api.ApiConstants.*; +import static edu.harvard.iq.dataverse.api.Datasets.handleVersion; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; import edu.harvard.iq.dataverse.util.json.JsonUtil; import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; @@ -66,10 +49,7 @@ import jakarta.ejb.EJB; import jakarta.ejb.EJBException; import jakarta.inject.Inject; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonString; -import jakarta.json.JsonValue; +import jakarta.json.*; import jakarta.json.stream.JsonParsingException; import jakarta.servlet.http.HttpServletResponse; import jakarta.ws.rs.*; @@ -84,6 +64,13 @@ import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; import jakarta.ws.rs.core.UriInfo; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataContentDisposition; import org.glassfish.jersey.media.multipart.FormDataParam; @@ -196,6 +183,14 @@ public Response restrictFileInDataset(@Context ContainerRequestContext crc, @Pat @AuthRequired @Path("{id}/replace") @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces("application/json") + @Operation(summary = "Replace a file on a dataset", + description = "Replace a file to a dataset") + @APIResponse(responseCode = "200", + description = "File replaced successfully on the dataset") + @Tag(name = "replaceFilesInDataset", + description = "Replace a file to a dataset") + @RequestBody(content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA)) public Response replaceFileInDataset( @Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @@ -230,10 +225,10 @@ public Response replaceFileInDataset( // - Will skip extra attributes which includes fileToReplaceId and forceReplace optionalFileParams = new OptionalFileParams(jsonData); } catch (DataFileTagException ex) { - return error(Response.Status.BAD_REQUEST, ex.getMessage()); + return error(BAD_REQUEST, ex.getMessage()); } } catch (ClassCastException | com.google.gson.JsonParseException ex) { - return error(Response.Status.BAD_REQUEST, BundleUtil.getStringFromBundle("file.addreplace.error.parsing")); + return error(BAD_REQUEST, BundleUtil.getStringFromBundle("file.addreplace.error.parsing")); } } @@ -322,7 +317,7 @@ public Response replaceFileInDataset( //"Look at that! You added a file! (hey hey, it may have worked)"); } catch (NoFilesException ex) { Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, ex); - return error(Response.Status.BAD_REQUEST, "NoFileException! Serious Error! See administrator!"); + return error(BAD_REQUEST, "NoFileException! Serious Error! See administrator!"); } } @@ -412,7 +407,7 @@ public Response updateFileMetadata(@Context ContainerRequestContext crc, @FormDa //we get the data file to do a permissions check, if this fails it'll go to the WrappedResponse below for an ugly unpermitted error execCommand(new GetDataFileCommand(req, findDataFileOrDie(result.get(0).toString()))); - return error(Response.Status.BAD_REQUEST, "You cannot edit metadata on a dataFile that has been replaced. Please try again with the newest file id."); + return error(BAD_REQUEST, "You cannot edit metadata on a dataFile that has been replaced. Please try again with the newest file id."); } // (2) Check/Parse the JSON (if uploaded) @@ -434,10 +429,10 @@ public Response updateFileMetadata(@Context ContainerRequestContext crc, @FormDa // - Will skip extra attributes which includes fileToReplaceId and forceReplace optionalFileParams = new OptionalFileParams(jsonData); } catch (DataFileTagException ex) { - return error(Response.Status.BAD_REQUEST, ex.getMessage()); + return error(BAD_REQUEST, ex.getMessage()); } } catch (ClassCastException | com.google.gson.JsonParseException ex) { - return error(Response.Status.BAD_REQUEST, BundleUtil.getStringFromBundle("file.addreplace.error.parsing")); + return error(BAD_REQUEST, BundleUtil.getStringFromBundle("file.addreplace.error.parsing")); } } @@ -458,7 +453,7 @@ public Response updateFileMetadata(@Context ContainerRequestContext crc, @FormDa } if (upFmd == null){ - return error(Response.Status.BAD_REQUEST, "An error has occurred attempting to update the requested DataFile. It is not part of the current version of the Dataset."); + return error(BAD_REQUEST, "An error has occurred attempting to update the requested DataFile. It is not part of the current version of the Dataset."); } jakarta.json.JsonObject jsonObject = JsonUtil.getJsonObject(jsonData); @@ -489,7 +484,7 @@ public Response updateFileMetadata(@Context ContainerRequestContext crc, @FormDa } } catch (WrappedResponse wr) { - return error(Response.Status.BAD_REQUEST, "An error has occurred attempting to update the requested DataFile, likely related to permissions."); + return error(BAD_REQUEST, "An error has occurred attempting to update the requested DataFile, likely related to permissions."); } String jsonString = upFmd.asGsonObject(true).toString(); @@ -500,79 +495,82 @@ public Response updateFileMetadata(@Context ContainerRequestContext crc, @FormDa .type(MediaType.TEXT_PLAIN) //Our plain text string is already json .build(); } - + @GET @AuthRequired - @Path("{id}/draft") - public Response getFileDataDraft(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) throws WrappedResponse, Exception { - return getFileDataResponse(getRequestUser(crc), fileIdOrPersistentId, uriInfo, headers, response, true); + @Path("{id}") + public Response getFileData(@Context ContainerRequestContext crc, + @PathParam("id") String fileIdOrPersistentId, + @QueryParam("includeDeaccessioned") boolean includeDeaccessioned, + @QueryParam("returnDatasetVersion") boolean returnDatasetVersion, + @QueryParam("returnOwners") boolean returnOwners, + @Context UriInfo uriInfo, + @Context HttpHeaders headers) { + return response( req -> getFileDataResponse(req, fileIdOrPersistentId, DS_VERSION_LATEST, includeDeaccessioned, returnDatasetVersion, returnOwners, uriInfo, headers), getRequestUser(crc)); } - + @GET @AuthRequired - @Path("{id}") - public Response getFileData(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) throws WrappedResponse, Exception { - return getFileDataResponse(getRequestUser(crc), fileIdOrPersistentId, uriInfo, headers, response, false); + @Path("{id}/versions/{datasetVersionId}") + public Response getFileDataForVersion(@Context ContainerRequestContext crc, + @PathParam("id") String fileIdOrPersistentId, + @PathParam("datasetVersionId") String datasetVersionId, + @QueryParam("includeDeaccessioned") boolean includeDeaccessioned, + @QueryParam("returnDatasetVersion") boolean returnDatasetVersion, + @QueryParam("returnOwners") boolean returnOwners, + @Context UriInfo uriInfo, + @Context HttpHeaders headers) { + return response( req -> getFileDataResponse(req, fileIdOrPersistentId, datasetVersionId, includeDeaccessioned, returnDatasetVersion, returnOwners, uriInfo, headers), getRequestUser(crc)); } - - private Response getFileDataResponse(User user, String fileIdOrPersistentId, UriInfo uriInfo, HttpHeaders headers, HttpServletResponse response, boolean draft ){ - - DataverseRequest req; - try { - req = createDataverseRequest(user); - } catch (Exception e) { - return error(BAD_REQUEST, "Error attempting to request information. Maybe a bad API token?"); - } - final DataFile df; - try { - df = execCommand(new GetDataFileCommand(req, findDataFileOrDie(fileIdOrPersistentId))); - } catch (Exception e) { - return error(BAD_REQUEST, "Error attempting get the requested data file."); - } - FileMetadata fm; + private Response getFileDataResponse(final DataverseRequest req, + String fileIdOrPersistentId, + String datasetVersionId, + boolean includeDeaccessioned, + boolean returnDatasetVersion, + boolean returnOwners, + UriInfo uriInfo, + HttpHeaders headers) throws WrappedResponse { + final DataFile dataFile = execCommand(new GetDataFileCommand(req, findDataFileOrDie(fileIdOrPersistentId))); + FileMetadata fileMetadata = execCommand(handleVersion(datasetVersionId, new Datasets.DsVersionHandler<>() { + @Override + public Command handleLatest() { + return new GetLatestAccessibleFileMetadataCommand(req, dataFile, includeDeaccessioned); + } - if (draft) { - try { - fm = execCommand(new GetDraftFileMetadataIfAvailableCommand(req, df)); - } catch (WrappedResponse w) { - return error(BAD_REQUEST, "An error occurred getting a draft version, you may not have permission to access unpublished data on this dataset."); + @Override + public Command handleDraft() { + return new GetDraftFileMetadataIfAvailableCommand(req, dataFile); } - if (null == fm) { - return error(BAD_REQUEST, BundleUtil.getStringFromBundle("files.api.no.draft")); + + @Override + public Command handleSpecific(long major, long minor) { + return new GetSpecificPublishedFileMetadataByDatasetVersionCommand(req, dataFile, major, minor, includeDeaccessioned); } - } else { - //first get latest published - //if not available get draft if permissible - try { - fm = df.getLatestPublishedFileMetadata(); - - } catch (UnsupportedOperationException e) { - try { - fm = execCommand(new GetDraftFileMetadataIfAvailableCommand(req, df)); - } catch (WrappedResponse w) { - return error(BAD_REQUEST, "An error occurred getting a draft version, you may not have permission to access unpublished data on this dataset."); - } - if (null == fm) { - return error(BAD_REQUEST, BundleUtil.getStringFromBundle("files.api.no.draft")); - } + @Override + public Command handleLatestPublished() { + return new GetLatestPublishedFileMetadataCommand(req, dataFile, includeDeaccessioned); } + })); + if (fileMetadata == null) { + throw new WrappedResponse(notFound(BundleUtil.getStringFromBundle("files.api.notFoundInVersion", Arrays.asList(fileIdOrPersistentId, datasetVersionId)))); } - - if (fm.getDatasetVersion().isReleased()) { - MakeDataCountLoggingServiceBean.MakeDataCountEntry entry = new MakeDataCountLoggingServiceBean.MakeDataCountEntry(uriInfo, headers, dvRequestService, df); + + if (fileMetadata.getDatasetVersion().isReleased()) { + MakeDataCountLoggingServiceBean.MakeDataCountEntry entry = new MakeDataCountLoggingServiceBean.MakeDataCountEntry(uriInfo, headers, dvRequestService, dataFile); mdcLogService.logEntry(entry); } - + return Response.ok(Json.createObjectBuilder() .add("status", ApiConstants.STATUS_OK) - .add("data", json(fm)).build()) + .add("data", json(fileMetadata, returnOwners, returnDatasetVersion)).build()) .type(MediaType.APPLICATION_JSON) .build(); } + @GET @AuthRequired @Path("{id}/metadata") @@ -637,23 +635,41 @@ public Response uningestDatafile(@Context ContainerRequestContext crc, @PathPara if (dataFile == null) { return error(Response.Status.NOT_FOUND, "File not found for given id."); } - if (!dataFile.isTabularData()) { - return error(Response.Status.BAD_REQUEST, "Cannot uningest non-tabular file."); - } - - try { - DataverseRequest req = createDataverseRequest(getRequestUser(crc)); - execCommand(new UningestFileCommand(req, dataFile)); - Long dataFileId = dataFile.getId(); - dataFile = fileService.find(dataFileId); - Dataset theDataset = dataFile.getOwner(); - exportDatasetMetadata(settingsService, theDataset); - return ok("Datafile " + dataFileId + " uningested."); - } catch (WrappedResponse wr) { - return wr.getResponse(); + // Ingest never succeeded, either there was a failure or this is not a tabular + // data file + // We allow anyone who can publish to uningest in order to clear a problem + if (dataFile.isIngestProblem()) { + try { + AuthenticatedUser au = getRequestAuthenticatedUserOrDie(crc); + if (!(permissionSvc.permissionsFor(au, dataFile).contains(Permission.PublishDataset))) { + return forbidden( + "Uningesting to remove an ingest problem can only be done by those who can publish the dataset"); + } + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + dataFile.setIngestDone(); + dataFile.setIngestReport(null); + fileService.save(dataFile); + return ok("Datafile " + dataFile.getId() + " uningested."); + } else { + return error(BAD_REQUEST, + BundleUtil.getStringFromBundle("Cannot uningest non-tabular file.")); + } + } else { + try { + DataverseRequest req = createDataverseRequest(getRequestUser(crc)); + execCommand(new UningestFileCommand(req, dataFile)); + Long dataFileId = dataFile.getId(); + dataFile = fileService.find(dataFileId); + Dataset theDataset = dataFile.getOwner(); + exportDatasetMetadata(settingsService, theDataset); + return ok("Datafile " + dataFileId + " uningested."); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } } - } // reingest attempts to queue an *existing* DataFile @@ -672,7 +688,7 @@ public Response reingest(@Context ContainerRequestContext crc, @PathParam("id") try { u = getRequestAuthenticatedUserOrDie(crc); if (!u.isSuperuser()) { - return error(Response.Status.FORBIDDEN, "This API call can be used by superusers only"); + return error(FORBIDDEN, "This API call can be used by superusers only"); } } catch (WrappedResponse wr) { return wr.getResponse(); @@ -688,21 +704,21 @@ public Response reingest(@Context ContainerRequestContext crc, @PathParam("id") Dataset dataset = dataFile.getOwner(); if (dataset == null) { - return error(Response.Status.BAD_REQUEST, "Failed to locate the parent dataset for the datafile."); + return error(BAD_REQUEST, "Failed to locate the parent dataset for the datafile."); } if (dataFile.isTabularData()) { - return error(Response.Status.BAD_REQUEST, "The datafile is already ingested as Tabular."); + return error(BAD_REQUEST, "The datafile is already ingested as Tabular."); } boolean ingestLock = dataset.isLockedFor(DatasetLock.Reason.Ingest); if (ingestLock) { - return error(Response.Status.FORBIDDEN, "Dataset already locked with an Ingest lock"); + return error(FORBIDDEN, "Dataset already locked with an Ingest lock"); } if (!FileUtil.canIngestAsTabular(dataFile)) { - return error(Response.Status.BAD_REQUEST, "Tabular ingest is not supported for this file type (id: "+id+", type: "+dataFile.getContentType()+")"); + return error(BAD_REQUEST, "Tabular ingest is not supported for this file type (id: "+id+", type: "+dataFile.getContentType()+")"); } dataFile.SetIngestScheduled(); @@ -742,7 +758,7 @@ public Response redetectDatafile(@Context ContainerRequestContext crc, @PathPara // Ingested Files have mimetype = text/tab-separated-values // No need to redetect if (dataFileIn.isTabularData()) { - return error(Response.Status.BAD_REQUEST, "The file is an ingested tabular file."); + return error(BAD_REQUEST, "The file is an ingested tabular file."); } String originalContentType = dataFileIn.getContentType(); DataFile dataFileOut = execCommand(new RedetectFileTypeCommand(createDataverseRequest(getRequestUser(crc)), dataFileIn, dryRun)); @@ -765,7 +781,7 @@ public Response extractNcml(@Context ContainerRequestContext crc, @PathParam("id if (!au.isSuperuser()) { // We can always make a command in the future if there's a need // for non-superusers to call this API. - return error(Response.Status.FORBIDDEN, "This API call can be used by superusers only"); + return error(FORBIDDEN, "This API call can be used by superusers only"); } DataFile dataFileIn = findDataFileOrDie(id); java.nio.file.Path tempLocationPath = null; @@ -931,4 +947,37 @@ public Response getHasBeenDeleted(@Context ContainerRequestContext crc, @PathPar return ok(dataFileServiceBean.hasBeenDeleted(dataFile)); }, getRequestUser(crc)); } + + /** + * @param fileIdOrPersistentId Database ID or PID of the data file. + * @param versionNumber The version of the dataset, such as 1.0, :draft, + * :latest-published, etc. + * @param includeDeaccessioned Defaults to false. + */ + @GET + @AuthRequired + @Path("{id}/versions/{dsVersionString}/citation") + public Response getFileCitationByVersion(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @PathParam("dsVersionString") String versionNumber, @QueryParam("includeDeaccessioned") boolean includeDeaccessioned) { + try { + DataverseRequest req = createDataverseRequest(getRequestUser(crc)); + final DataFile df = execCommand(new GetDataFileCommand(req, findDataFileOrDie(fileIdOrPersistentId))); + Dataset ds = df.getOwner(); + DatasetVersion dsv = findDatasetVersionOrDie(req, versionNumber, ds, includeDeaccessioned, true); + if (dsv == null) { + return unauthorized(BundleUtil.getStringFromBundle("files.api.no.draftOrUnauth")); + } + + Long getDatasetVersionID = dsv.getId(); + FileMetadata fm = dataFileServiceBean.findFileMetadataByDatasetVersionIdAndDataFileId(getDatasetVersionID, df.getId()); + if (fm == null) { + return notFound(BundleUtil.getStringFromBundle("files.api.fileNotFound")); + } + boolean direct = df.isIdentifierRegistered(); + DataCitation citation = new DataCitation(fm, direct); + return ok(citation.toString(true)); + } catch (WrappedResponse ex) { + return ex.getResponse(); + } + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Groups.java b/src/main/java/edu/harvard/iq/dataverse/api/Groups.java index d56a787c7ff..ed996b8ecf9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Groups.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Groups.java @@ -88,8 +88,8 @@ public Response postIpGroup( JsonObject dto ){ * that group from being created. */ @PUT - @Path("ip/{groupName}") - public Response putIpGroups( @PathParam("groupName") String groupName, JsonObject dto ){ + @Path("ip/{group}") + public Response putIpGroups( @PathParam("group") String groupName, JsonObject dto ){ try { if ( groupName == null || groupName.trim().isEmpty() ) { return badRequest("Group name cannot be empty"); @@ -118,8 +118,8 @@ public Response listIpGroups() { } @GET - @Path("ip/{groupIdtf}") - public Response getIpGroup( @PathParam("groupIdtf") String groupIdtf ) { + @Path("ip/{group}") + public Response getIpGroup( @PathParam("group") String groupIdtf ) { IpGroup grp; if ( isNumeric(groupIdtf) ) { grp = ipGroupPrv.get( Long.parseLong(groupIdtf) ); @@ -131,8 +131,8 @@ public Response getIpGroup( @PathParam("groupIdtf") String groupIdtf ) { } @DELETE - @Path("ip/{groupIdtf}") - public Response deleteIpGroup( @PathParam("groupIdtf") String groupIdtf ) { + @Path("ip/{group}") + public Response deleteIpGroup( @PathParam("group") String groupIdtf ) { IpGroup grp; if ( isNumeric(groupIdtf) ) { grp = ipGroupPrv.get( Long.parseLong(groupIdtf) ); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Index.java b/src/main/java/edu/harvard/iq/dataverse/api/Index.java index 4910c460b6a..c30a77acb58 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Index.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Index.java @@ -215,7 +215,7 @@ public Response clearSolrIndex() { return error(Status.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage()); } } - + @GET @Path("{type}/{id}") public Response indexTypeById(@PathParam("type") String type, @PathParam("id") Long id) { @@ -326,6 +326,29 @@ public Response indexDatasetByPersistentId(@QueryParam("persistentId") String pe } } + /** + * Clears the entry for a dataset from Solr + * + * @param id numer id of the dataset + * @return response; + * will return 404 if no such dataset in the database; but will attempt to + * clear the entry from Solr regardless. + */ + @DELETE + @Path("datasets/{id}") + public Response clearDatasetFromIndex(@PathParam("id") Long id) { + Dataset dataset = datasetService.find(id); + // We'll attempt to delete the Solr document regardless of whether the + // dataset exists in the database: + String response = indexService.removeSolrDocFromIndex(IndexServiceBean.solrDocIdentifierDataset + id); + if (dataset != null) { + return ok("Sent request to clear Solr document for dataset " + id + ": " + response); + } else { + return notFound("Could not find dataset " + id + " in the database. Requested to clear from Solr anyway: " + response); + } + } + + /** * This is just a demo of the modular math logic we use for indexAll. */ diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Info.java b/src/main/java/edu/harvard/iq/dataverse/api/Info.java index 40ce6cd25b7..257519677d3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Info.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Info.java @@ -1,16 +1,35 @@ package edu.harvard.iq.dataverse.api; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import jakarta.ws.rs.Produces; +import org.apache.commons.io.IOUtils; + import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.SystemConfig; import jakarta.ejb.EJB; import jakarta.json.Json; import jakarta.json.JsonValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; @Path("info") +@Tag(name = "info", description = "General information about the Dataverse installation.") public class Info extends AbstractApiBean { @EJB @@ -19,6 +38,8 @@ public class Info extends AbstractApiBean { @EJB SystemConfig systemConfig; + private static final Logger logger = Logger.getLogger(Info.class.getCanonicalName()); + @GET @Path("settings/:DatasetPublishPopupCustomText") public Response getDatasetPublishPopupCustomText() { @@ -33,6 +54,9 @@ public Response getMaxEmbargoDurationInMonths() { @GET @Path("version") + @Operation(summary = "Get version and build information", description = "Get version and build information") + @APIResponse(responseCode = "200", + description = "Version and build information") public Response getInfo() { String versionStr = systemConfig.getVersion(true); String[] comps = versionStr.split("build",2); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java b/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java index 05d12f1083c..6a9c608dc13 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java @@ -1,12 +1,9 @@ package edu.harvard.iq.dataverse.api; -import edu.harvard.iq.dataverse.DOIServiceBean; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetServiceBean; import edu.harvard.iq.dataverse.DataverseRoleServiceBean; import edu.harvard.iq.dataverse.GlobalId; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; -import edu.harvard.iq.dataverse.HandlenetServiceBean; import edu.harvard.iq.dataverse.MailServiceBean; import edu.harvard.iq.dataverse.RoleAssigneeServiceBean; import edu.harvard.iq.dataverse.RoleAssignment; @@ -15,6 +12,9 @@ import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; +import edu.harvard.iq.dataverse.pidproviders.handle.HandlePidProvider; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.json.JSONLDUtil; import edu.harvard.iq.dataverse.util.json.JsonLDNamespace; @@ -134,13 +134,13 @@ public Response acceptMessage(String body) { .getString("@id"); if (citedResource.getString("@type").equals(JsonLDTerm.schemaOrg("Dataset").getUrl())) { logger.fine("Raw PID: " + pid); - if (pid.startsWith(DOIServiceBean.DOI_RESOLVER_URL)) { - pid = pid.replace(DOIServiceBean.DOI_RESOLVER_URL, DOIServiceBean.DOI_PROTOCOL + ":"); - } else if (pid.startsWith(HandlenetServiceBean.HDL_RESOLVER_URL)) { - pid = pid.replace(HandlenetServiceBean.HDL_RESOLVER_URL, HandlenetServiceBean.HDL_PROTOCOL + ":"); + if (pid.startsWith(AbstractDOIProvider.DOI_RESOLVER_URL)) { + pid = pid.replace(AbstractDOIProvider.DOI_RESOLVER_URL, AbstractDOIProvider.DOI_PROTOCOL + ":"); + } else if (pid.startsWith(HandlePidProvider.HDL_RESOLVER_URL)) { + pid = pid.replace(HandlePidProvider.HDL_RESOLVER_URL, HandlePidProvider.HDL_PROTOCOL + ":"); } logger.fine("Protocol PID: " + pid); - Optional id = GlobalIdServiceBean.parse(pid); + Optional id = PidProvider.parse(pid); Dataset dataset = datasetSvc.findByGlobalId(pid); if (dataset != null) { JsonObject citingResource = Json.createObjectBuilder().add("@id", citingPID) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/MakeDataCountApi.java b/src/main/java/edu/harvard/iq/dataverse/api/MakeDataCountApi.java index b2696757220..1f2f1039327 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/MakeDataCountApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/MakeDataCountApi.java @@ -2,10 +2,16 @@ import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetServiceBean; +import edu.harvard.iq.dataverse.GlobalId; import edu.harvard.iq.dataverse.makedatacount.DatasetExternalCitations; import edu.harvard.iq.dataverse.makedatacount.DatasetExternalCitationsServiceBean; import edu.harvard.iq.dataverse.makedatacount.DatasetMetrics; import edu.harvard.iq.dataverse.makedatacount.DatasetMetricsServiceBean; +import edu.harvard.iq.dataverse.makedatacount.MakeDataCountProcessState; +import edu.harvard.iq.dataverse.makedatacount.MakeDataCountProcessStateServiceBean; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; +import edu.harvard.iq.dataverse.pidproviders.doi.datacite.DataCiteDOIProvider; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.util.SystemConfig; import edu.harvard.iq.dataverse.util.json.JsonUtil; @@ -25,6 +31,8 @@ import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; import jakarta.json.JsonValue; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; @@ -43,6 +51,8 @@ public class MakeDataCountApi extends AbstractApiBean { @EJB DatasetMetricsServiceBean datasetMetricsService; @EJB + MakeDataCountProcessStateServiceBean makeDataCountProcessStateService; + @EJB DatasetExternalCitationsServiceBean datasetExternalCitationsService; @EJB DatasetServiceBean datasetService; @@ -106,7 +116,7 @@ public Response addUsageMetricsFromSushiReport(@PathParam("id") String id, @Quer @POST @Path("/addUsageMetricsFromSushiReport") - public Response addUsageMetricsFromSushiReportAll(@PathParam("id") String id, @QueryParam("reportOnDisk") String reportOnDisk) { + public Response addUsageMetricsFromSushiReportAll(@QueryParam("reportOnDisk") String reportOnDisk) { try { JsonObject report = JsonUtil.getJsonObjectFromFile(reportOnDisk); @@ -131,8 +141,14 @@ public Response addUsageMetricsFromSushiReportAll(@PathParam("id") String id, @Q public Response updateCitationsForDataset(@PathParam("id") String id) throws IOException { try { Dataset dataset = findDatasetOrDie(id); - String persistentId = dataset.getGlobalId().toString(); - //ToDo - if this isn't a DOI? + GlobalId pid = dataset.getGlobalId(); + PidProvider pidProvider = PidUtil.getPidProvider(pid.getProviderId()); + // Only supported for DOIs and for DataCite DOI providers + if(!DataCiteDOIProvider.TYPE.equals(pidProvider.getProviderType())) { + return error(Status.BAD_REQUEST, "Only DataCite DOI providers are supported"); + } + String persistentId = pid.toString(); + // DataCite wants "doi=", not "doi:". String authorityPlusIdentifier = persistentId.replaceFirst("doi:", ""); // Request max page size and then loop to handle multiple pages @@ -190,5 +206,51 @@ public Response updateCitationsForDataset(@PathParam("id") String id) throws IOE return wr.getResponse(); } } + @GET + @Path("{yearMonth}/processingState") + public Response getProcessingState(@PathParam("yearMonth") String yearMonth) { + MakeDataCountProcessState mdcps; + try { + mdcps = makeDataCountProcessStateService.getMakeDataCountProcessState(yearMonth); + } catch (IllegalArgumentException e) { + return error(Status.BAD_REQUEST,e.getMessage()); + } + if (mdcps != null) { + JsonObjectBuilder output = Json.createObjectBuilder(); + output.add("yearMonth", mdcps.getYearMonth()); + output.add("state", mdcps.getState().name()); + output.add("stateChangeTimestamp", mdcps.getStateChangeTime().toString()); + return ok(output); + } else { + return error(Status.NOT_FOUND, "Could not find an existing process state for " + yearMonth); + } + } + @POST + @Path("{yearMonth}/processingState") + public Response updateProcessingState(@PathParam("yearMonth") String yearMonth, @QueryParam("state") String state) { + MakeDataCountProcessState mdcps; + try { + mdcps = makeDataCountProcessStateService.setMakeDataCountProcessState(yearMonth, state); + } catch (Exception e) { + return badRequest(e.getMessage()); + } + + JsonObjectBuilder output = Json.createObjectBuilder(); + output.add("yearMonth", mdcps.getYearMonth()); + output.add("state", mdcps.getState().name()); + output.add("stateChangeTimestamp", mdcps.getStateChangeTime().toString()); + return ok(output); + } + + @DELETE + @Path("{yearMonth}/processingState") + public Response deleteProcessingState(@PathParam("yearMonth") String yearMonth) { + boolean deleted = makeDataCountProcessStateService.deleteMakeDataCountProcessState(yearMonth); + if (deleted) { + return ok("Processing State deleted for " + yearMonth); + } else { + return notFound("Processing State not found for " + yearMonth); + } + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/MetadataBlocks.java b/src/main/java/edu/harvard/iq/dataverse/api/MetadataBlocks.java index 448fb48e389..8861abd4803 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/MetadataBlocks.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/MetadataBlocks.java @@ -1,34 +1,33 @@ package edu.harvard.iq.dataverse.api; import edu.harvard.iq.dataverse.MetadataBlock; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; +import jakarta.ws.rs.*; import jakarta.ws.rs.core.Response; -import static edu.harvard.iq.dataverse.util.json.JsonPrinter.brief; -import jakarta.ws.rs.PathParam; + +import java.util.List; + import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; -import static edu.harvard.iq.dataverse.util.json.JsonPrinter.toJsonArray; /** * Api bean for managing metadata blocks. + * * @author michael */ @Path("metadatablocks") @Produces("application/json") public class MetadataBlocks extends AbstractApiBean { - + @GET - public Response list() { - return ok(metadataBlockSvc.listMetadataBlocks().stream().map(brief::json).collect(toJsonArray())); + public Response listMetadataBlocks(@QueryParam("onlyDisplayedOnCreate") boolean onlyDisplayedOnCreate, + @QueryParam("returnDatasetFieldTypes") boolean returnDatasetFieldTypes) { + List metadataBlocks = metadataBlockSvc.listMetadataBlocks(onlyDisplayedOnCreate); + return ok(json(metadataBlocks, returnDatasetFieldTypes, onlyDisplayedOnCreate)); } - + @Path("{identifier}") @GET - public Response getBlock( @PathParam("identifier") String idtf ) { + public Response getMetadataBlock(@PathParam("identifier") String idtf) { MetadataBlock b = findMetadataBlock(idtf); - - return (b != null ) ? ok(json(b)) : notFound("Can't find metadata block '" + idtf + "'"); + return (b != null) ? ok(json(b)) : notFound("Can't find metadata block '" + idtf + "'"); } - } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Metrics.java b/src/main/java/edu/harvard/iq/dataverse/api/Metrics.java index 7bb2570334b..452e5df9f9a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Metrics.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Metrics.java @@ -547,6 +547,98 @@ public Response getDownloadsPastDays(@Context UriInfo uriInfo, @PathParam("days" return ok(jsonObj); } + /** Accounts */ + + @GET + @Path("accounts") + public Response getAccountsAllTime(@Context UriInfo uriInfo) { + return getAccountsToMonth(uriInfo, MetricsUtil.getCurrentMonth()); + } + + @GET + @Path("accounts/toMonth/{yyyymm}") + public Response getAccountsToMonth(@Context UriInfo uriInfo, @PathParam("yyyymm") String yyyymm) { + + try { + errorIfUnrecongizedQueryParamPassed(uriInfo, new String[] { }); + } catch (IllegalArgumentException ia) { + return error(BAD_REQUEST, ia.getLocalizedMessage()); + } + + String metricName = "accountsToMonth"; + String sanitizedyyyymm = MetricsUtil.sanitizeYearMonthUserInput(yyyymm); + JsonObject jsonObj = MetricsUtil.stringToJsonObject(metricsSvc.returnUnexpiredCacheMonthly(metricName, sanitizedyyyymm, null, null)); + + if (null == jsonObj) { // run query and save + Long count; + try { + count = metricsSvc.accountsToMonth(sanitizedyyyymm); + } catch (ParseException e) { + return error(BAD_REQUEST, "Unable to parse supplied date: " + e.getLocalizedMessage()); + } + jsonObj = MetricsUtil.countToJson(count).build(); + metricsSvc.save(new Metric(metricName, sanitizedyyyymm, null, null, jsonObj.toString())); + } + + return ok(jsonObj); + } + + @GET + @Path("accounts/pastDays/{days}") + public Response getAccountsPastDays(@Context UriInfo uriInfo, @PathParam("days") int days) { + + try { + errorIfUnrecongizedQueryParamPassed(uriInfo, new String[] { }); + } catch (IllegalArgumentException ia) { + return error(BAD_REQUEST, ia.getLocalizedMessage()); + } + + String metricName = "accountsPastDays"; + + if (days < 1) { + return error(BAD_REQUEST, "Invalid parameter for number of days."); + } + + JsonObject jsonObj = MetricsUtil.stringToJsonObject(metricsSvc.returnUnexpiredCacheDayBased(metricName, String.valueOf(days), null, null)); + + if (null == jsonObj) { // run query and save + Long count = metricsSvc.accountsPastDays(days); + jsonObj = MetricsUtil.countToJson(count).build(); + metricsSvc.save(new Metric(metricName, String.valueOf(days), null, null, jsonObj.toString())); + } + + return ok(jsonObj); + } + + @GET + @Path("accounts/monthly") + @Produces("text/csv, application/json") + public Response getAccountsTimeSeries(@Context Request req, @Context UriInfo uriInfo) { + + try { + errorIfUnrecongizedQueryParamPassed(uriInfo, new String[] { }); + } catch (IllegalArgumentException ia) { + return error(BAD_REQUEST, ia.getLocalizedMessage()); + } + + String metricName = "accounts"; + JsonArray jsonArray = MetricsUtil.stringToJsonArray(metricsSvc.returnUnexpiredCacheAllTime(metricName, null, null)); + + if (null == jsonArray) { // run query and save + // Only handling published right now + jsonArray = metricsSvc.accountsTimeSeries(); + metricsSvc.save(new Metric(metricName, null, null, null, jsonArray.toString())); + } + + MediaType requestedType = getVariant(req, MediaType.valueOf(FileUtil.MIME_TYPE_CSV), MediaType.APPLICATION_JSON_TYPE); + if ((requestedType != null) && (requestedType.equals(MediaType.APPLICATION_JSON_TYPE))) { + return ok(jsonArray); + } + return ok(FileUtil.jsonArrayOfObjectsToCSV(jsonArray, MetricsUtil.DATE, MetricsUtil.COUNT), MediaType.valueOf(FileUtil.MIME_TYPE_CSV), "accounts.timeseries.csv"); + } + + /** MakeDataCount */ + @GET @Path("makeDataCount/{metric}") public Response getMakeDataCountMetricCurrentMonth(@Context UriInfo uriInfo, @PathParam("metric") String metricSupplied, @QueryParam("country") String country, @QueryParam("parentAlias") String parentAlias) { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Notifications.java b/src/main/java/edu/harvard/iq/dataverse/api/Notifications.java index 37c894d3071..df172f36973 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Notifications.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Notifications.java @@ -55,7 +55,6 @@ public Response getAllNotificationsForUser(@Context ContainerRequestContext crc) notificationObjectBuilder.add("id", notification.getId()); notificationObjectBuilder.add("type", type.toString()); /* FIXME - Re-add reasons for return if/when they are added to the notifications page. - if (Type.RETURNEDDS.equals(type) || Type.SUBMITTEDDS.equals(type)) { JsonArrayBuilder reasons = getReasonsForReturn(notification); for (JsonValue reason : reasons.build()) { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Pids.java b/src/main/java/edu/harvard/iq/dataverse/api/Pids.java index 534e42fd505..4ad57bceb58 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Pids.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Pids.java @@ -130,4 +130,41 @@ public Response deletePid(@Context ContainerRequestContext crc, @PathParam("id") } } + @GET + @AuthRequired + @Path("providers") + @Produces(MediaType.APPLICATION_JSON) + public Response getPidProviders(@Context ContainerRequestContext crc) throws WrappedResponse { + try { + getRequestAuthenticatedUserOrDie(crc); + } catch (WrappedResponse ex) { + return ex.getResponse(); + } + return ok(PidUtil.getProviders()); + } + + @GET + @AuthRequired + // The :.+ suffix allows PIDs with a / char to be entered w/o escaping + @Path("providers/{persistentId:.+}") + @Produces(MediaType.APPLICATION_JSON) + public Response getPidProviderId(@Context ContainerRequestContext crc, @PathParam("persistentId") String persistentId) throws WrappedResponse { + try { + getRequestAuthenticatedUserOrDie(crc); + } catch (WrappedResponse ex) { + return ex.getResponse(); + } + GlobalId globalId = PidUtil.parseAsGlobalID(persistentId); + if(globalId== null) { + return error(Response.Status.NOT_FOUND, "No provider found for PID"); + } else { + String providerId = globalId.getProviderId(); + if(PidUtil.getManagedProviderIds().contains(providerId)) { + return ok(globalId.getProviderId()); + } else { + return ok("PID recognized as an unmanaged " + globalId.getProtocol()); + } + } + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/SavedSearches.java b/src/main/java/edu/harvard/iq/dataverse/api/SavedSearches.java index 5d0365d022e..33a11a2df23 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/SavedSearches.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/SavedSearches.java @@ -55,7 +55,7 @@ public Response list() { savedSearchesBuilder.add(thisSavedSearch); } JsonObjectBuilder response = Json.createObjectBuilder(); - response.add("saved searches", savedSearchesBuilder); + response.add("savedSearches", savedSearchesBuilder); return ok(response); } @@ -90,7 +90,6 @@ private JsonObjectBuilder toJson(SavedSearch savedSearch) { @POST public Response add(JsonObject body) { - if (body == null) { return error(BAD_REQUEST, "JSON is expected."); } @@ -159,7 +158,7 @@ public Response add(JsonObject body) { try { SavedSearch persistedSavedSearch = savedSearchSvc.add(toPersist); - return ok("Added: " + persistedSavedSearch); + return ok("Added: " + persistedSavedSearch, Json.createObjectBuilder().add("id", persistedSavedSearch.getId())); } catch (EJBException ex) { StringBuilder errors = new StringBuilder(); Throwable throwable = ex.getCause(); @@ -173,16 +172,18 @@ public Response add(JsonObject body) { @DELETE @Path("{id}") - public Response delete(@PathParam("id") long doomedId) { - boolean disabled = true; - if (disabled) { - return error(BAD_REQUEST, "Saved Searches can not safely be deleted because links can not safely be deleted. See https://github.com/IQSS/dataverse/issues/1364 for details."); - } + public Response delete(@PathParam("id") long doomedId, @QueryParam("unlink") boolean unlink) { SavedSearch doomed = savedSearchSvc.find(doomedId); if (doomed == null) { return error(NOT_FOUND, "Could not find saved search id " + doomedId); } - boolean wasDeleted = savedSearchSvc.delete(doomedId); + boolean wasDeleted; + try { + wasDeleted = savedSearchSvc.delete(doomedId, unlink); + } catch (Exception e) { + return error(INTERNAL_SERVER_ERROR, "Problem while trying to unlink links of saved search id " + doomedId); + } + if (wasDeleted) { return ok(Json.createObjectBuilder().add("Deleted", doomedId)); } else { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Search.java b/src/main/java/edu/harvard/iq/dataverse/api/Search.java index 71e2865ca4d..6b9fcb38305 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Search.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Search.java @@ -1,10 +1,8 @@ package edu.harvard.iq.dataverse.api; -import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.api.auth.AuthRequired; import edu.harvard.iq.dataverse.search.SearchFields; -import edu.harvard.iq.dataverse.DataverseServiceBean; -import edu.harvard.iq.dataverse.DvObjectServiceBean; import edu.harvard.iq.dataverse.search.FacetCategory; import edu.harvard.iq.dataverse.search.FacetLabel; import edu.harvard.iq.dataverse.search.SolrSearchResult; @@ -16,7 +14,6 @@ import edu.harvard.iq.dataverse.search.SearchConstants; import edu.harvard.iq.dataverse.search.SearchException; import edu.harvard.iq.dataverse.search.SearchUtil; -import edu.harvard.iq.dataverse.search.SolrIndexServiceBean; import edu.harvard.iq.dataverse.search.SortBy; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import java.io.IOException; @@ -26,6 +23,7 @@ import java.util.Map; import java.util.logging.Logger; import jakarta.ejb.EJB; +import jakarta.inject.Inject; import jakarta.json.Json; import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonObjectBuilder; @@ -51,10 +49,8 @@ public class Search extends AbstractApiBean { SearchServiceBean searchService; @EJB DataverseServiceBean dataverseService; - @EJB - DvObjectServiceBean dvObjectService; - @EJB - SolrIndexServiceBean SolrIndexService; + @Inject + DatasetVersionFilesServiceBean datasetVersionFilesServiceBean; @GET @AuthRequired @@ -179,7 +175,7 @@ public Response search( JsonArrayBuilder itemsArrayBuilder = Json.createArrayBuilder(); List solrSearchResults = solrQueryResponse.getSolrSearchResults(); for (SolrSearchResult solrSearchResult : solrSearchResults) { - itemsArrayBuilder.add(solrSearchResult.toJsonObject(showRelevance, showEntityIds, showApiUrls, metadataFields)); + itemsArrayBuilder.add(solrSearchResult.json(showRelevance, showEntityIds, showApiUrls, metadataFields, getDatasetFileCount(solrSearchResult))); } JsonObjectBuilder spelling_alternatives = Json.createObjectBuilder(); @@ -187,31 +183,32 @@ public Response search( spelling_alternatives.add(entry.getKey(), entry.getValue().toString()); } - JsonArrayBuilder facets = Json.createArrayBuilder(); - JsonObjectBuilder facetCategoryBuilder = Json.createObjectBuilder(); - for (FacetCategory facetCategory : solrQueryResponse.getFacetCategoryList()) { - JsonObjectBuilder facetCategoryBuilderFriendlyPlusData = Json.createObjectBuilder(); - JsonArrayBuilder facetLabelBuilderData = Json.createArrayBuilder(); - for (FacetLabel facetLabel : facetCategory.getFacetLabel()) { - JsonObjectBuilder countBuilder = Json.createObjectBuilder(); - countBuilder.add(facetLabel.getName(), facetLabel.getCount()); - facetLabelBuilderData.add(countBuilder); - } - facetCategoryBuilderFriendlyPlusData.add("friendly", facetCategory.getFriendlyName()); - facetCategoryBuilderFriendlyPlusData.add("labels", facetLabelBuilderData); - facetCategoryBuilder.add(facetCategory.getName(), facetCategoryBuilderFriendlyPlusData); - } - facets.add(facetCategoryBuilder); - JsonObjectBuilder value = Json.createObjectBuilder() .add("q", query) .add("total_count", solrQueryResponse.getNumResultsFound()) .add("start", solrQueryResponse.getResultsStart()) .add("spelling_alternatives", spelling_alternatives) .add("items", itemsArrayBuilder.build()); + if (showFacets) { + JsonArrayBuilder facets = Json.createArrayBuilder(); + JsonObjectBuilder facetCategoryBuilder = Json.createObjectBuilder(); + for (FacetCategory facetCategory : solrQueryResponse.getFacetCategoryList()) { + JsonObjectBuilder facetCategoryBuilderFriendlyPlusData = Json.createObjectBuilder(); + JsonArrayBuilder facetLabelBuilderData = Json.createArrayBuilder(); + for (FacetLabel facetLabel : facetCategory.getFacetLabel()) { + JsonObjectBuilder countBuilder = Json.createObjectBuilder(); + countBuilder.add(facetLabel.getName(), facetLabel.getCount()); + facetLabelBuilderData.add(countBuilder); + } + facetCategoryBuilderFriendlyPlusData.add("friendly", facetCategory.getFriendlyName()); + facetCategoryBuilderFriendlyPlusData.add("labels", facetLabelBuilderData); + facetCategoryBuilder.add(facetCategory.getName(), facetCategoryBuilderFriendlyPlusData); + } + facets.add(facetCategoryBuilder); value.add("facets", facets); } + value.add("count_in_response", solrSearchResults.size()); /** * @todo Returning the fq might be useful as a troubleshooting aid @@ -232,6 +229,15 @@ public Response search( } } + private Long getDatasetFileCount(SolrSearchResult solrSearchResult) { + DvObject dvObject = solrSearchResult.getEntity(); + if (dvObject.isInstanceofDataset()) { + DatasetVersion datasetVersion = ((Dataset) dvObject).getVersionFromId(solrSearchResult.getDatasetVersionId()); + return datasetVersionFilesServiceBean.getFileMetadataCount(datasetVersion); + } + return null; + } + private User getUser(ContainerRequestContext crc) throws WrappedResponse { User userToExecuteSearchAs = GuestUser.get(); try { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java b/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java index 10510013495..46747b50c29 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java @@ -21,7 +21,7 @@ public class TestApi extends AbstractApiBean { @GET @Path("datasets/{id}/externalTools") - public Response getExternalToolsforFile(@PathParam("id") String idSupplied, @QueryParam("type") String typeSupplied) { + public Response getDatasetExternalToolsforFile(@PathParam("id") String idSupplied, @QueryParam("type") String typeSupplied) { ExternalTool.Type type; try { type = ExternalTool.Type.fromString(typeSupplied); @@ -44,6 +44,34 @@ public Response getExternalToolsforFile(@PathParam("id") String idSupplied, @Que return wr.getResponse(); } } + + @GET + @Path("datasets/{id}/externalTool/{toolId}") + public Response getExternalToolforDatasetById(@PathParam("id") String idSupplied, @PathParam("toolId") String toolId, @QueryParam("type") String typeSupplied) { + ExternalTool.Type type; + try { + type = ExternalTool.Type.fromString(typeSupplied); + } catch (IllegalArgumentException ex) { + return error(BAD_REQUEST, ex.getLocalizedMessage()); + } + Dataset dataset; + try { + dataset = findDatasetOrDie(idSupplied); + JsonArrayBuilder tools = Json.createArrayBuilder(); + List datasetTools = externalToolService.findDatasetToolsByType(type); + for (ExternalTool tool : datasetTools) { + ApiToken apiToken = externalToolService.getApiToken(getRequestApiKey()); + ExternalToolHandler externalToolHandler = new ExternalToolHandler(tool, dataset, apiToken, null); + JsonObjectBuilder toolToJson = externalToolService.getToolAsJsonWithQueryParameters(externalToolHandler); + if (tool.getId().toString().equals(toolId)) { + return ok(toolToJson); + } + } + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + return error(BAD_REQUEST, "Could not find external tool with id of " + toolId); + } @Path("files/{id}/externalTools") @GET diff --git a/src/main/java/edu/harvard/iq/dataverse/api/TestIngest.java b/src/main/java/edu/harvard/iq/dataverse/api/TestIngest.java index 05ba150df8e..add43ea2091 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/TestIngest.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/TestIngest.java @@ -100,7 +100,7 @@ public String datafile(@QueryParam("fileName") String fileName, @QueryParam("fil TabularDataIngest tabDataIngest = null; try { - tabDataIngest = ingestPlugin.read(fileInputStream, null); + tabDataIngest = ingestPlugin.read(fileInputStream, false, null); } catch (IOException ingestEx) { output = output.concat("Caught an exception trying to ingest file " + fileName + ": " + ingestEx.getLocalizedMessage()); return output; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Users.java b/src/main/java/edu/harvard/iq/dataverse/api/Users.java index 791fc7aa774..c1a7c95dbff 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Users.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Users.java @@ -24,13 +24,7 @@ import jakarta.ejb.Stateless; import jakarta.json.JsonArray; import jakarta.json.JsonObjectBuilder; -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; +import jakarta.ws.rs.*; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; @@ -157,7 +151,7 @@ public Response getTokenExpirationDate() { @Path("token/recreate") @AuthRequired @POST - public Response recreateToken(@Context ContainerRequestContext crc) { + public Response recreateToken(@Context ContainerRequestContext crc, @QueryParam("returnExpiration") boolean returnExpiration) { User u = getRequestUser(crc); AuthenticatedUser au; @@ -174,8 +168,12 @@ public Response recreateToken(@Context ContainerRequestContext crc) { ApiToken newToken = authSvc.generateApiTokenForUser(au); authSvc.save(newToken); - return ok("New token for " + au.getUserIdentifier() + " is " + newToken.getTokenString()); + String message = "New token for " + au.getUserIdentifier() + " is " + newToken.getTokenString(); + if (returnExpiration) { + message += " and expires on " + newToken.getExpireTime(); + } + return ok(message); } @GET @@ -234,7 +232,7 @@ public Response getTraces(@Context ContainerRequestContext crc, @PathParam("iden @AuthRequired @Path("{identifier}/traces/{element}") @Produces("text/csv, application/json") - public Response getTraces(@Context ContainerRequestContext crc, @Context Request req, @PathParam("identifier") String identifier, @PathParam("element") String element) { + public Response getTracesElement(@Context ContainerRequestContext crc, @Context Request req, @PathParam("identifier") String identifier, @PathParam("element") String element) { try { AuthenticatedUser userToQuery = authSvc.getAuthenticatedUser(identifier); if(!elements.contains(element)) { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/WorkflowsAdmin.java b/src/main/java/edu/harvard/iq/dataverse/api/WorkflowsAdmin.java index 8d5024c1c14..15478aacff7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/WorkflowsAdmin.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/WorkflowsAdmin.java @@ -111,9 +111,9 @@ public Response deleteDefault(@PathParam("triggerType") String triggerType) { } } - @Path("/{identifier}") + @Path("/{id}") @GET - public Response getWorkflow(@PathParam("identifier") String identifier ) { + public Response getWorkflow(@PathParam("id") String identifier ) { try { long idtf = Long.parseLong(identifier); return workflows.getWorkflow(idtf) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/datadeposit/CollectionDepositManagerImpl.java b/src/main/java/edu/harvard/iq/dataverse/api/datadeposit/CollectionDepositManagerImpl.java index 5bc50903be8..a81848bd7af 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/datadeposit/CollectionDepositManagerImpl.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/datadeposit/CollectionDepositManagerImpl.java @@ -6,6 +6,7 @@ import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.DataverseServiceBean; +import edu.harvard.iq.dataverse.DvObjectServiceBean; import edu.harvard.iq.dataverse.EjbDataverseEngine; import edu.harvard.iq.dataverse.PermissionServiceBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; @@ -14,6 +15,7 @@ import edu.harvard.iq.dataverse.api.imports.ImportGenericServiceBean; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.impl.CreateNewDatasetCommand; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.ConstraintViolationUtil; import java.util.logging.Level; @@ -44,6 +46,8 @@ public class CollectionDepositManagerImpl implements CollectionDepositManager { @EJB DatasetServiceBean datasetService; @EJB + DvObjectServiceBean dvObjectService; + @EJB PermissionServiceBean permissionService; @Inject SwordAuth swordAuth; @@ -96,13 +100,10 @@ public DepositReceipt createNew(String collectionUri, Deposit deposit, AuthCrede Dataset dataset = new Dataset(); dataset.setOwner(dvThatWillOwnDataset); - String nonNullDefaultIfKeyNotFound = ""; - String protocol = settingsService.getValueForKey(SettingsServiceBean.Key.Protocol, nonNullDefaultIfKeyNotFound); - String authority = settingsService.getValueForKey(SettingsServiceBean.Key.Authority, nonNullDefaultIfKeyNotFound); - - dataset.setProtocol(protocol); - dataset.setAuthority(authority); - //Wait until the create command before actually getting an identifier + PidProvider pidProvider = dvObjectService.getEffectivePidGenerator(dataset); + dataset.setProtocol(pidProvider.getProtocol()); + dataset.setAuthority(pidProvider.getAuthority()); + //Wait until the create command before actually getting an identifier logger.log(Level.FINE, "DS Deposit identifier: {0}", dataset.getIdentifier()); AbstractCreateDatasetCommand createDatasetCommand = new CreateNewDatasetCommand(dataset, dvReq); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java index e67e91e63c9..af9aeffa1c9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java @@ -18,6 +18,8 @@ import java.util.logging.Level; import java.util.logging.Logger; +import org.apache.commons.lang3.StringUtils; + /** * Catches all types of web application exceptions like NotFoundException, etc etc and handles them properly. */ @@ -49,7 +51,14 @@ public Response toResponse(WebApplicationException ex) { } else if ((ex.getMessage() + "").toLowerCase().startsWith("no permission to download file")) { jrb.message(BundleUtil.getStringFromBundle("access.api.exception.metadata.restricted.no.permission")); } else { - jrb.message("Bad Request. The API request cannot be completed with the parameters supplied. Please check your code for typos, or consult our API guide at http://guides.dataverse.org."); + String msg = ex.getMessage(); + msg = StringUtils.isEmpty(msg) + ? "Bad Request. The API request cannot be completed with the parameters supplied. Please check your code for typos, or consult our API guide at http://guides.dataverse.org." + : "Bad Request. The API request cannot be completed with the parameters supplied. Details: " + + msg + + " - please check your code for typos, or consult our API guide at http://guides.dataverse.org."; + + jrb.message(msg); jrb.request(request); } break; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportDDIServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportDDIServiceBean.java index 73a83035fc5..85d4868605d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportDDIServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportDDIServiceBean.java @@ -828,9 +828,9 @@ private HashSet processGeoBndBox(XMLStreamReader xmlr) throws XMLStrea } else if (xmlr.getLocalName().equals("eastBL")) { addToSet(set,"eastLongitude", parseText(xmlr)); } else if (xmlr.getLocalName().equals("southBL")) { - addToSet(set,"southLongitude", parseText(xmlr)); + addToSet(set,"southLatitude", parseText(xmlr)); } else if (xmlr.getLocalName().equals("northBL")) { - addToSet(set,"northLongitude", parseText(xmlr)); + addToSet(set,"northLatitude", parseText(xmlr)); } } else if (event == XMLStreamConstants.END_ELEMENT) { if (xmlr.getLocalName().equals("geoBndBox")) break; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportGenericServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportGenericServiceBean.java index f7a6cf54dd5..d32a548c8bf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportGenericServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportGenericServiceBean.java @@ -2,7 +2,6 @@ import com.google.gson.Gson; -import edu.harvard.iq.dataverse.DOIServiceBean; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetFieldCompoundValue; import edu.harvard.iq.dataverse.DatasetFieldConstant; @@ -11,13 +10,15 @@ import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.ForeignMetadataFieldMapping; import edu.harvard.iq.dataverse.ForeignMetadataFormatMapping; -import edu.harvard.iq.dataverse.HandlenetServiceBean; import edu.harvard.iq.dataverse.MetadataBlockServiceBean; import edu.harvard.iq.dataverse.api.dto.*; import edu.harvard.iq.dataverse.api.dto.FieldDTO; import edu.harvard.iq.dataverse.api.dto.MetadataBlockDTO; +import edu.harvard.iq.dataverse.dataset.DatasetTypeServiceBean; import edu.harvard.iq.dataverse.license.LicenseServiceBean; -import edu.harvard.iq.dataverse.pidproviders.PermaLinkPidProviderServiceBean; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; +import edu.harvard.iq.dataverse.pidproviders.handle.HandlePidProvider; +import edu.harvard.iq.dataverse.pidproviders.perma.PermaLinkPidProvider; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.StringUtil; import edu.harvard.iq.dataverse.util.json.JsonParseException; @@ -71,9 +72,13 @@ public class ImportGenericServiceBean { @EJB SettingsServiceBean settingsService; + @EJB LicenseServiceBean licenseService; + @EJB + DatasetTypeServiceBean datasetTypeService; + @PersistenceContext(unitName = "VDCNet-ejbPU") private EntityManager em; @@ -110,7 +115,7 @@ public void importXML(String xmlToParse, String foreignFormat, DatasetVersion da logger.fine(json); JsonReader jsonReader = Json.createReader(new StringReader(json)); JsonObject obj = jsonReader.readObject(); - DatasetVersion dv = new JsonParser(datasetFieldSvc, blockService, settingsService, licenseService).parseDatasetVersion(obj, datasetVersion); + DatasetVersion dv = new JsonParser(datasetFieldSvc, blockService, settingsService, licenseService, datasetTypeService).parseDatasetVersion(obj, datasetVersion); } catch (XMLStreamException ex) { //Logger.getLogger("global").log(Level.SEVERE, null, ex); throw new EJBException("ERROR occurred while parsing XML fragment ("+xmlToParse.substring(0, 64)+"...); ", ex); @@ -352,7 +357,7 @@ private String getOtherIdFromDTO(DatasetVersionDTO datasetVersionDTO) { if (!otherIds.isEmpty()) { // We prefer doi or hdl identifiers like "doi:10.7910/DVN/1HE30F" for (String otherId : otherIds) { - if (otherId.startsWith(DOIServiceBean.DOI_PROTOCOL) || otherId.startsWith(HandlenetServiceBean.HDL_PROTOCOL) || otherId.startsWith(DOIServiceBean.DOI_RESOLVER_URL) || otherId.startsWith(HandlenetServiceBean.HDL_RESOLVER_URL) || otherId.startsWith(DOIServiceBean.HTTP_DOI_RESOLVER_URL) || otherId.startsWith(HandlenetServiceBean.HTTP_HDL_RESOLVER_URL) || otherId.startsWith(DOIServiceBean.DXDOI_RESOLVER_URL) || otherId.startsWith(DOIServiceBean.HTTP_DXDOI_RESOLVER_URL)) { + if (otherId.startsWith(AbstractDOIProvider.DOI_PROTOCOL) || otherId.startsWith(HandlePidProvider.HDL_PROTOCOL) || otherId.startsWith(AbstractDOIProvider.DOI_RESOLVER_URL) || otherId.startsWith(HandlePidProvider.HDL_RESOLVER_URL) || otherId.startsWith(AbstractDOIProvider.HTTP_DOI_RESOLVER_URL) || otherId.startsWith(HandlePidProvider.HTTP_HDL_RESOLVER_URL) || otherId.startsWith(AbstractDOIProvider.DXDOI_RESOLVER_URL) || otherId.startsWith(AbstractDOIProvider.HTTP_DXDOI_RESOLVER_URL)) { return otherId; } } @@ -361,7 +366,7 @@ private String getOtherIdFromDTO(DatasetVersionDTO datasetVersionDTO) { try { HandleResolver hr = new HandleResolver(); hr.resolveHandle(otherId); - return HandlenetServiceBean.HDL_PROTOCOL + ":" + otherId; + return HandlePidProvider.HDL_PROTOCOL + ":" + otherId; } catch (HandleException e) { logger.fine("Not a valid handle: " + e.toString()); } @@ -388,7 +393,7 @@ public String reassignIdentifierAsGlobalId(String identifierString, DatasetDTO d String protocol = identifierString.substring(0, index1); - if (DOIServiceBean.DOI_PROTOCOL.equals(protocol) || HandlenetServiceBean.HDL_PROTOCOL.equals(protocol) || PermaLinkPidProviderServiceBean.PERMA_PROTOCOL.equals(protocol)) { + if (AbstractDOIProvider.DOI_PROTOCOL.equals(protocol) || HandlePidProvider.HDL_PROTOCOL.equals(protocol) || PermaLinkPidProvider.PERMA_PROTOCOL.equals(protocol)) { logger.fine("Processing hdl:- or doi:- or perma:-style identifier : "+identifierString); } else if ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) { @@ -396,21 +401,21 @@ public String reassignIdentifierAsGlobalId(String identifierString, DatasetDTO d // We also recognize global identifiers formatted as global resolver URLs: //ToDo - refactor index1 always has -1 here so that we can use index1+1 later //ToDo - single map of protocol/url, are all three cases the same then? - if (identifierString.startsWith(HandlenetServiceBean.HDL_RESOLVER_URL) || identifierString.startsWith(HandlenetServiceBean.HTTP_HDL_RESOLVER_URL)) { + if (identifierString.startsWith(HandlePidProvider.HDL_RESOLVER_URL) || identifierString.startsWith(HandlePidProvider.HTTP_HDL_RESOLVER_URL)) { logger.fine("Processing Handle identifier formatted as a resolver URL: "+identifierString); - protocol = HandlenetServiceBean.HDL_PROTOCOL; - index1 = (identifierString.startsWith(HandlenetServiceBean.HDL_RESOLVER_URL)) ? HandlenetServiceBean.HDL_RESOLVER_URL.length() - 1 : HandlenetServiceBean.HTTP_HDL_RESOLVER_URL.length() - 1; + protocol = HandlePidProvider.HDL_PROTOCOL; + index1 = (identifierString.startsWith(HandlePidProvider.HDL_RESOLVER_URL)) ? HandlePidProvider.HDL_RESOLVER_URL.length() - 1 : HandlePidProvider.HTTP_HDL_RESOLVER_URL.length() - 1; index2 = identifierString.indexOf("/", index1 + 1); - } else if (identifierString.startsWith(DOIServiceBean.DOI_RESOLVER_URL) || identifierString.startsWith(DOIServiceBean.HTTP_DOI_RESOLVER_URL) || identifierString.startsWith(DOIServiceBean.DXDOI_RESOLVER_URL) || identifierString.startsWith(DOIServiceBean.HTTP_DXDOI_RESOLVER_URL)) { + } else if (identifierString.startsWith(AbstractDOIProvider.DOI_RESOLVER_URL) || identifierString.startsWith(AbstractDOIProvider.HTTP_DOI_RESOLVER_URL) || identifierString.startsWith(AbstractDOIProvider.DXDOI_RESOLVER_URL) || identifierString.startsWith(AbstractDOIProvider.HTTP_DXDOI_RESOLVER_URL)) { logger.fine("Processing DOI identifier formatted as a resolver URL: "+identifierString); - protocol = DOIServiceBean.DOI_PROTOCOL; - identifierString = identifierString.replace(DOIServiceBean.DXDOI_RESOLVER_URL, DOIServiceBean.DOI_RESOLVER_URL); - identifierString = identifierString.replace(DOIServiceBean.HTTP_DXDOI_RESOLVER_URL, DOIServiceBean.HTTP_DOI_RESOLVER_URL); - index1 = (identifierString.startsWith(DOIServiceBean.DOI_RESOLVER_URL)) ? DOIServiceBean.DOI_RESOLVER_URL.length() - 1 : DOIServiceBean.HTTP_DOI_RESOLVER_URL.length() - 1; + protocol = AbstractDOIProvider.DOI_PROTOCOL; + identifierString = identifierString.replace(AbstractDOIProvider.DXDOI_RESOLVER_URL, AbstractDOIProvider.DOI_RESOLVER_URL); + identifierString = identifierString.replace(AbstractDOIProvider.HTTP_DXDOI_RESOLVER_URL, AbstractDOIProvider.HTTP_DOI_RESOLVER_URL); + index1 = (identifierString.startsWith(AbstractDOIProvider.DOI_RESOLVER_URL)) ? AbstractDOIProvider.DOI_RESOLVER_URL.length() - 1 : AbstractDOIProvider.HTTP_DOI_RESOLVER_URL.length() - 1; index2 = identifierString.indexOf("/", index1 + 1); - } else if (identifierString.startsWith(PermaLinkPidProviderServiceBean.PERMA_RESOLVER_URL + Dataset.TARGET_URL)) { - protocol = PermaLinkPidProviderServiceBean.PERMA_PROTOCOL; - index1 = PermaLinkPidProviderServiceBean.PERMA_RESOLVER_URL.length() + + Dataset.TARGET_URL.length() - 1; + } else if (identifierString.startsWith(PermaLinkPidProvider.PERMA_RESOLVER_URL + Dataset.TARGET_URL)) { + protocol = PermaLinkPidProvider.PERMA_PROTOCOL; + index1 = PermaLinkPidProvider.PERMA_RESOLVER_URL.length() + + Dataset.TARGET_URL.length() - 1; index2 = identifierString.indexOf("/", index1 + 1); } else { logger.warning("HTTP Url in supplied as the identifier is neither a Handle nor DOI resolver: "+identifierString); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportServiceBean.java index c17ba909230..d2bba56f884 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportServiceBean.java @@ -23,6 +23,7 @@ import edu.harvard.iq.dataverse.MetadataBlockServiceBean; import edu.harvard.iq.dataverse.api.dto.DatasetDTO; import edu.harvard.iq.dataverse.api.imports.ImportUtil.ImportType; +import edu.harvard.iq.dataverse.dataset.DatasetTypeServiceBean; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.impl.CreateDatasetVersionCommand; @@ -38,6 +39,8 @@ import edu.harvard.iq.dataverse.util.json.JsonParser; import edu.harvard.iq.dataverse.util.json.JsonUtil; import edu.harvard.iq.dataverse.license.LicenseServiceBean; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; + import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -102,8 +105,13 @@ public class ImportServiceBean { @EJB IndexServiceBean indexService; + @EJB LicenseServiceBean licenseService; + + @EJB + DatasetTypeServiceBean datasetTypeService; + /** * This is just a convenience method, for testing migration. It creates * a dummy dataverse with the directory name as dataverse name & alias. @@ -262,7 +270,7 @@ public Dataset doImportHarvestedDataset(DataverseRequest dataverseRequest, Harve JsonObject obj = JsonUtil.getJsonObject(json); //and call parse Json to read it into a dataset try { - JsonParser parser = new JsonParser(datasetfieldService, metadataBlockService, settingsService, licenseService, harvestingClient); + JsonParser parser = new JsonParser(datasetfieldService, metadataBlockService, settingsService, licenseService, datasetTypeService, harvestingClient); parser.setLenient(true); Dataset ds = parser.parseDataset(obj); @@ -415,14 +423,15 @@ public JsonObjectBuilder doImport(DataverseRequest dataverseRequest, Dataverse o JsonObject obj = JsonUtil.getJsonObject(json); //and call parse Json to read it into a dataset try { - JsonParser parser = new JsonParser(datasetfieldService, metadataBlockService, settingsService, licenseService); + JsonParser parser = new JsonParser(datasetfieldService, metadataBlockService, settingsService, licenseService, datasetTypeService); parser.setLenient(!importType.equals(ImportType.NEW)); Dataset ds = parser.parseDataset(obj); // For ImportType.NEW, if the user supplies a global identifier, and it's not a protocol // we support, it will be rejected. + if (importType.equals(ImportType.NEW)) { - if (ds.getGlobalId().asString() != null && !ds.getProtocol().equals(settingsService.getValueForKey(SettingsServiceBean.Key.Protocol, ""))) { + if (ds.getGlobalId().asString() != null && !PidUtil.getPidProvider(ds.getGlobalId().getProviderId()).canManagePID()) { throw new ImportException("Could not register id " + ds.getGlobalId().asString() + ", protocol not supported"); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthFilter.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthFilter.java index a2cf3082ae7..c93a1496c17 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthFilter.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthFilter.java @@ -29,9 +29,7 @@ public void init(FilterConfig filterConfig) throws ServletException { logger.info(AuthFilter.class.getName() + "initialized. filterConfig.getServletContext().getServerInfo(): " + filterConfig.getServletContext().getServerInfo()); try { - String glassfishLogsDirectory = "logs"; - - FileHandler logFile = new FileHandler(".." + File.separator + glassfishLogsDirectory + File.separator + "authfilter.log"); + FileHandler logFile = new FileHandler( System.getProperty("com.sun.aas.instanceRoot") + File.separator + "logs" + File.separator + "authfilter.log"); SimpleFormatter formatterTxt = new SimpleFormatter(); logFile.setFormatter(formatterTxt); logger.addHandler(logFile); diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java index 1c0f5010059..4a8fb123fd4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java @@ -307,11 +307,9 @@ public AuthenticatedUser getUpdateAuthenticatedUser( String authenticationProvid if (user != null && !user.isDeactivated()) { user = userService.updateLastLogin(user); } - + if ( user == null ) { throw new IllegalStateException("Authenticated user does not exist. The functionality to support creating one at this point in authentication has been removed."); - //return createAuthenticatedUser( - // new UserRecordIdentifier(authenticationProviderId, resp.getUserId()), resp.getUserId(), resp.getUserDisplayInfo(), true ); } else { if (BuiltinAuthenticationProvider.PROVIDER_ID.equals(user.getAuthenticatedUserLookup().getAuthenticationProviderId())) { return user; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java index a0e3f899443..48afb2b830a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java @@ -528,6 +528,8 @@ public void displayNotification() { case GLOBUSUPLOADCOMPLETEDWITHERRORS: case GLOBUSDOWNLOADCOMPLETED: case GLOBUSDOWNLOADCOMPLETEDWITHERRORS: + case GLOBUSUPLOADREMOTEFAILURE: + case GLOBUSUPLOADLOCALFAILURE: userNotification.setTheObject(datasetService.find(userNotification.getObjectId())); break; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/PasswordEncryption.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/PasswordEncryption.java index 4446f68228d..aef8b375b63 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/PasswordEncryption.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/PasswordEncryption.java @@ -1,6 +1,6 @@ package edu.harvard.iq.dataverse.authorization.providers.builtin; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import org.apache.commons.lang3.RandomStringUtils; @@ -36,13 +36,13 @@ public interface Algorithm { public String encrypt(String plainText) { try { MessageDigest md = MessageDigest.getInstance("SHA"); - md.update(plainText.getBytes("UTF-8")); + md.update(plainText.getBytes(StandardCharsets.UTF_8)); byte[] raw = md.digest(); //String hash = Base64.encodeToString(raw, true); String hash = Base64.getEncoder().encodeToString(raw); return hash; - } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { + } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java index 0fd0852b4df..8f3dc07fdea 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java @@ -1,6 +1,7 @@ package edu.harvard.iq.dataverse.authorization.providers.oauth2; import edu.harvard.iq.dataverse.DataverseSession; +import edu.harvard.iq.dataverse.UserServiceBean; import edu.harvard.iq.dataverse.authorization.AuthenticationProvider; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; @@ -65,6 +66,9 @@ public class OAuth2LoginBackingBean implements Serializable { @EJB SystemConfig systemConfig; + @EJB + UserServiceBean userService; + @Inject DataverseSession session; @@ -128,6 +132,7 @@ public void exchangeCodeForToken() throws IOException { } else { // login the user and redirect to HOME of intended page (if any). // setUser checks for deactivated users. + dvUser = userService.updateLastLogin(dvUser); session.setUser(dvUser); final OAuth2TokenData tokenData = oauthUser.getTokenData(); if (tokenData != null) { diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java index b307c655798..d6d3e0317ed 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java @@ -16,6 +16,7 @@ import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.json.JsonPrinter; import static edu.harvard.iq.dataverse.util.StringUtil.nonEmpty; + import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; import java.io.Serializable; import java.sql.Timestamp; @@ -42,6 +43,7 @@ import jakarta.persistence.PostLoad; import jakarta.persistence.PrePersist; import jakarta.persistence.Transient; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -146,6 +148,10 @@ public class AuthenticatedUser implements User, Serializable { @Transient private Set mutedNotificationsSet = new HashSet<>(); + @Column(nullable=false) + @Min(value = 1, message = "Rate Limit Tier must be greater than 0.") + private int rateLimitTier = 1; + @PrePersist void prePersist() { mutedNotifications = Type.toStringValue(mutedNotificationsSet); @@ -397,6 +403,13 @@ public void setDeactivatedTime(Timestamp deactivatedTime) { this.deactivatedTime = deactivatedTime; } + public int getRateLimitTier() { + return rateLimitTier; + } + public void setRateLimitTier(int rateLimitTier) { + this.rateLimitTier = rateLimitTier; + } + @OneToOne(mappedBy = "authenticatedUser") private AuthenticatedUserLookup authenticatedUserLookup; @@ -435,7 +448,6 @@ public void setShibIdentityProvider(String shibIdentityProvider) { public JsonObjectBuilder toJson() { //JsonObjectBuilder authenicatedUserJson = Json.createObjectBuilder(); - NullSafeJsonBuilder authenicatedUserJson = NullSafeJsonBuilder.jsonObjectBuilder(); authenicatedUserJson.add("id", this.id); diff --git a/src/main/java/edu/harvard/iq/dataverse/batch/jobs/importer/filesystem/FileRecordWriter.java b/src/main/java/edu/harvard/iq/dataverse/batch/jobs/importer/filesystem/FileRecordWriter.java index ba34a3d1ed1..af1e9c6a294 100644 --- a/src/main/java/edu/harvard/iq/dataverse/batch/jobs/importer/filesystem/FileRecordWriter.java +++ b/src/main/java/edu/harvard/iq/dataverse/batch/jobs/importer/filesystem/FileRecordWriter.java @@ -33,6 +33,7 @@ import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.FileUtil; @@ -58,7 +59,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import jakarta.servlet.http.HttpServletRequest; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; @Named @Dependent @@ -360,31 +360,22 @@ private DataFile createPackageDataFile(List files) { if (commandEngine.getContext().systemConfig().isFilePIDsEnabledForCollection(dataset.getOwner())) { - GlobalIdServiceBean idServiceBean = GlobalIdServiceBean.getBean(packageFile.getProtocol(), commandEngine.getContext()); + PidProvider pidProvider = commandEngine.getContext().dvObjects().getEffectivePidGenerator(dataset); if (packageFile.getIdentifier() == null || packageFile.getIdentifier().isEmpty()) { - packageFile.setIdentifier(idServiceBean.generateDataFileIdentifier(packageFile)); - } - String nonNullDefaultIfKeyNotFound = ""; - String protocol = commandEngine.getContext().settings().getValueForKey(SettingsServiceBean.Key.Protocol, nonNullDefaultIfKeyNotFound); - String authority = commandEngine.getContext().settings().getValueForKey(SettingsServiceBean.Key.Authority, nonNullDefaultIfKeyNotFound); - if (packageFile.getProtocol() == null) { - packageFile.setProtocol(protocol); - } - if (packageFile.getAuthority() == null) { - packageFile.setAuthority(authority); + pidProvider.generatePid(packageFile); } if (!packageFile.isIdentifierRegistered()) { String doiRetString = ""; - idServiceBean = GlobalIdServiceBean.getBean(commandEngine.getContext()); + try { - doiRetString = idServiceBean.createIdentifier(packageFile); + doiRetString = pidProvider.createIdentifier(packageFile); } catch (Throwable e) { } // Check return value to make sure registration succeeded - if (!idServiceBean.registerWhenPublished() && doiRetString.contains(packageFile.getIdentifier())) { + if (!pidProvider.registerWhenPublished() && doiRetString.contains(packageFile.getIdentifier())) { packageFile.setIdentifierRegistered(true); packageFile.setGlobalIdCreateTime(new Date()); } diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/AbstractRemoteOverlayAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/AbstractRemoteOverlayAccessIO.java index 10ff68a56f3..18269f6970e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/AbstractRemoteOverlayAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/AbstractRemoteOverlayAccessIO.java @@ -334,11 +334,5 @@ protected String getStoragePath() throws IOException { logger.fine("fullStoragePath: " + fullStoragePath); return fullStoragePath; } - - public static boolean isNotDataverseAccessible(String storeId) { - return Boolean.parseBoolean(StorageIO.getConfigParamForDriver(storeId, FILES_NOT_ACCESSIBLE_BY_DATAVERSE)); - } - - } \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/DataAccess.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/DataAccess.java index a1bcbe49327..bc4c69390cf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/DataAccess.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/DataAccess.java @@ -270,6 +270,10 @@ public static StorageIO createNewStorageIO(T dvObject, S logger.warning("Could not find storage driver for: " + storageTag); throw new IOException("createDataAccessObject: Unsupported storage method " + storageDriverId); } + if (storageIO == null) { + logger.warning("Could not find storage driver for: " + storageTag); + throw new IOException("createDataAccessObject: Unsupported storage method " + storageDriverId); + } // Note: All storageIO classes must assure that dvObject instances' storageIdentifiers are prepended with // the :// + any additional storageIO type information required (e.g. the bucketname for s3/swift) // This currently happens when the storageIO is opened for write access diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java index f2a1312a150..26637ec5742 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java @@ -120,7 +120,8 @@ public void open (DataAccessOption... options) throws IOException { && dataFile.getContentType().equals("text/tab-separated-values") && dataFile.isTabularData() && dataFile.getDataTable() != null - && (!this.noVarHeader())) { + && (!this.noVarHeader()) + && (!dataFile.getDataTable().isStoredWithVariableHeader())) { List datavariables = dataFile.getDataTable().getDataVariables(); String varHeaderLine = generateVariableHeader(datavariables); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/GlobusOverlayAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/GlobusOverlayAccessIO.java index 7a6809cb2ff..3bf2107e52b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/GlobusOverlayAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/GlobusOverlayAccessIO.java @@ -171,24 +171,25 @@ private static String findMatchingEndpoint(String path, String[] allowedEndpoint } protected void validatePath(String relPath) throws IOException { - if (isManaged()) { - if (!usesStandardNamePattern(relPath)) { - throw new IOException("Unacceptable identifier pattern in submitted identifier: " + relPath); - } - } else { - try { - String endpoint = findMatchingEndpoint(relPath, allowedEndpoints); - logger.fine(endpoint + " " + relPath); - - if (endpoint == null || !Paths.get(endpoint, relPath).normalize().startsWith(endpoint)) { - throw new IOException( - "storageidentifier doesn't start with one of " + this.driverId + "'s allowed endpoints"); + if (dvObject != null && dvObject.isInstanceofDataFile()) { + if (isManaged()) { + if (!usesStandardNamePattern(relPath)) { + throw new IOException("Unacceptable identifier pattern in submitted identifier: " + relPath); + } + } else { + try { + String endpoint = findMatchingEndpoint(relPath, allowedEndpoints); + logger.fine(endpoint + " " + relPath); + + if (endpoint == null || !Paths.get(endpoint, relPath).normalize().startsWith(endpoint)) { + throw new IOException("storageidentifier doesn't start with one of " + this.driverId + + "'s allowed endpoints"); + } + } catch (InvalidPathException e) { + throw new IOException("Could not interpret storageidentifier in globus store " + this.driverId); } - } catch (InvalidPathException e) { - throw new IOException("Could not interpret storageidentifier in globus store " + this.driverId); } } - } // Call the Globus API to get the file size @@ -214,7 +215,7 @@ public long retrieveSizeFromMedia() { JsonArray dataArray = responseJson.getJsonArray("DATA"); if (dataArray != null && dataArray.size() != 0) { //File found - return (long) responseJson.getJsonArray("DATA").getJsonObject(0).getInt("size"); + return (long) responseJson.getJsonArray("DATA").getJsonObject(0).getJsonNumber("size").longValueExact(); } } else { logger.warning("Response from " + get.getURI().toString() + " was " @@ -239,7 +240,7 @@ public long retrieveSizeFromMedia() { public InputStream getInputStream() throws IOException { //Currently only supported when using an S3 store with the Globus S3Connector. //ToDo: Support when using a managed Globus endpoint that supports http access - if(!AbstractRemoteOverlayAccessIO.isNotDataverseAccessible(endpoint)) { + if(StorageIO.isDataverseAccessible(endpoint)) { return baseStore.getInputStream(); } else { throw new IOException("Not implemented"); @@ -450,8 +451,12 @@ public void open(DataAccessOption... options) throws IOException { this.setSize(retrieveSizeFromMedia()); } // Only applies for the S3 Connector case (where we could have run an ingest) - if (dataFile.getContentType() != null && dataFile.getContentType().equals("text/tab-separated-values") - && dataFile.isTabularData() && dataFile.getDataTable() != null && (!this.noVarHeader())) { + if (dataFile.getContentType() != null + && dataFile.getContentType().equals("text/tab-separated-values") + && dataFile.isTabularData() + && dataFile.getDataTable() != null + && (!this.noVarHeader()) + && (!dataFile.getDataTable().isStoredWithVariableHeader())) { List datavariables = dataFile.getDataTable().getDataVariables(); String varHeaderLine = generateVariableHeader(datavariables); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/ImageThumbConverter.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/ImageThumbConverter.java index 2de37174a3b..2435e3f778a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/ImageThumbConverter.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/ImageThumbConverter.java @@ -35,21 +35,21 @@ import javax.imageio.stream.ImageOutputStream; import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.DataFileServiceBean; import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.SystemConfig; import java.io.ByteArrayOutputStream; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.OutputStream; import java.nio.channels.Channel; import java.nio.channels.Channels; -import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.logging.Level; import java.util.logging.Logger; + +import jakarta.enterprise.inject.spi.CDI; import org.apache.commons.io.IOUtils; //import org.primefaces.util.Base64; import java.util.Base64; @@ -110,6 +110,12 @@ private static boolean isThumbnailAvailable(StorageIO storageIO, int s return false; } + // check if thumbnail generation failed: + if (file.isPreviewImageFail()) { + logger.fine("Thumbnail failed to be generated for "+ file.getId()); + return false; + } + if (isThumbnailCached(storageIO, size)) { logger.fine("Found cached thumbnail for " + file.getId()); return true; @@ -119,22 +125,23 @@ private static boolean isThumbnailAvailable(StorageIO storageIO, int s } private static boolean generateThumbnail(DataFile file, StorageIO storageIO, int size) { - logger.log(Level.FINE, (file.isPreviewImageFail() ? "Not trying" : "Trying") + " to generate thumbnail, file id: " + file.getId()); + logger.fine((file.isPreviewImageFail() ? "Not trying" : "Trying") + " to generate thumbnail, file id: " + file.getId()); + boolean thumbnailGenerated = false; // Don't try to generate if there have been failures: if (!file.isPreviewImageFail()) { - boolean thumbnailGenerated = false; if (file.getContentType().substring(0, 6).equalsIgnoreCase("image/")) { thumbnailGenerated = generateImageThumbnail(storageIO, size); } else if (file.getContentType().equalsIgnoreCase("application/pdf")) { thumbnailGenerated = generatePDFThumbnail(storageIO, size); } if (!thumbnailGenerated) { + file.setPreviewImageFail(true); + file.setPreviewImageAvailable(false); logger.fine("No thumbnail generated for " + file.getId()); } - return thumbnailGenerated; } - return false; + return thumbnailGenerated; } // Note that this method works on ALL file types for which thumbnail @@ -165,15 +172,30 @@ public static InputStreamIO getImageThumbnailAsInputStream(StorageIO s return null; } int cachedThumbnailSize = (int) storageIO.getAuxObjectSize(THUMBNAIL_SUFFIX + size); + InputStreamIO inputStreamIO = cachedThumbnailSize > 0 ? new InputStreamIO(cachedThumbnailInputStream, cachedThumbnailSize) : null; - InputStreamIO inputStreamIO = new InputStreamIO(cachedThumbnailInputStream, cachedThumbnailSize); + if (inputStreamIO != null) { + inputStreamIO.setMimeType(THUMBNAIL_MIME_TYPE); - inputStreamIO.setMimeType(THUMBNAIL_MIME_TYPE); - - String fileName = storageIO.getFileName(); - if (fileName != null) { - fileName = fileName.replaceAll("\\.[^\\.]*$", THUMBNAIL_FILE_EXTENSION); - inputStreamIO.setFileName(fileName); + String fileName = storageIO.getFileName(); + if (fileName != null) { + fileName = fileName.replaceAll("\\.[^\\.]*$", THUMBNAIL_FILE_EXTENSION); + inputStreamIO.setFileName(fileName); + } + } else { + if (storageIO.getDataFile() != null && cachedThumbnailSize == 0) { + // We found an older 0 length thumbnail. Newer image uploads will not have this issue. + // Once cleaned up, this thumbnail will no longer have this issue + logger.warning("Cleaning up zero sized thumbnail ID: "+ storageIO.getDataFile().getId()); + storageIO.getDataFile().setPreviewImageFail(true); + storageIO.getDataFile().setPreviewImageAvailable(false); + DataFileServiceBean datafileService = CDI.current().select(DataFileServiceBean.class).get(); + datafileService.save(storageIO.getDataFile()); + + // Now that we have marked this File as a thumbnail failure, + // no reason not to try and delete this 0-size cache here: + storageIO.deleteAuxObject(THUMBNAIL_SUFFIX + size); + } } return inputStreamIO; } catch (Exception ioex) { @@ -208,6 +230,7 @@ private static boolean generatePDFThumbnail(StorageIO storageIO, int s // will run the ImageMagick on it, and will save its output in another temp // file, and will save it as an "auxiliary" file via the driver. boolean tempFilesRequired = false; + File tempFile = null; try { Path pdfFilePath = storageIO.getFileSystemPath(); @@ -225,7 +248,7 @@ private static boolean generatePDFThumbnail(StorageIO storageIO, int s } if (tempFilesRequired) { - InputStream inputStream = null; + InputStream inputStream = null; try { storageIO.open(); inputStream = storageIO.getInputStream(); @@ -234,12 +257,11 @@ private static boolean generatePDFThumbnail(StorageIO storageIO, int s return false; } - File tempFile; OutputStream outputStream = null; try { tempFile = File.createTempFile("tempFileToRescale", ".tmp"); outputStream = new FileOutputStream(tempFile); - //Reads/transfers all bytes from the input stream to the output stream. + //Reads/transfers all bytes from the input stream to the output stream. inputStream.transferTo(outputStream); } catch (IOException ioex) { logger.warning("GenerateImageThumb: failed to save pdf bytes in a temporary file."); @@ -270,6 +292,12 @@ private static boolean generatePDFThumbnail(StorageIO storageIO, int s logger.warning("failed to save generated pdf thumbnail, as AUX file " + THUMBNAIL_SUFFIX + size + "!"); return false; } + finally { + try { + tempFile.delete(); + } + catch (Exception e) {} + } } return true; @@ -301,6 +329,7 @@ private static boolean generateImageThumbnail(StorageIO storageIO, int private static boolean generateImageThumbnailFromInputStream(StorageIO storageIO, int size, InputStream inputStream) { BufferedImage fullSizeImage; + boolean thumbnailGenerated = false; try { logger.fine("attempting to read the image file with ImageIO.read(InputStream), " + storageIO.getDataFile().getStorageIdentifier()); @@ -353,26 +382,35 @@ private static boolean generateImageThumbnailFromInputStream(StorageIO try { rescaleImage(fullSizeImage, width, height, size, outputStream); - /* - // while we are at it, let's make sure other size thumbnails are - // generated too: - for (int s : (new int[]{DEFAULT_PREVIEW_SIZE, DEFAULT_THUMBNAIL_SIZE, DEFAULT_CARDIMAGE_SIZE})) { - if (size != s && !thumbnailFileExists(fileLocation, s)) { - rescaleImage(fullSizeImage, width, height, s, fileLocation); - } - } - */ if (tempFileRequired) { storageIO.savePathAsAux(Paths.get(tempFile.getAbsolutePath()), THUMBNAIL_SUFFIX + size); } + thumbnailGenerated = true; } catch (Exception ioex) { logger.warning("Failed to rescale and/or save the image: " + ioex.getMessage()); - return false; + thumbnailGenerated = false; + } + finally { + if(tempFileRequired) { + try { + tempFile.delete(); + } + catch (Exception e) {} + } else if (!thumbnailGenerated) { + // if it was a local file - let's make sure we are not leaving + // behind a half-baked, broken image - such as a 0-size file - + // if this was a failure. + try { + storageIO.deleteAuxObject(THUMBNAIL_SUFFIX + size); + } catch (IOException ioex) { + logger.fine("Failed attempt to delete the result of a failed thumbnail rescaling; this is most likely ok - for ex., because it was never created in the first place."); + } + } } - return true; + return thumbnailGenerated; } @@ -530,12 +568,10 @@ private static String getImageAsBase64FromInputStream(InputStream inputStream) { public static String getImageAsBase64FromFile(File imageFile) { InputStream imageInputStream = null; try { - - int imageSize = (int) imageFile.length(); - - imageInputStream = new FileInputStream(imageFile); - - return getImageAsBase64FromInputStream(imageInputStream); //, imageSize); + if (imageFile.length() > 0) { + imageInputStream = new FileInputStream(imageFile); + return getImageAsBase64FromInputStream(imageInputStream); + } } catch (IOException ex) { // too bad - but not fatal logger.warning("getImageAsBase64FromFile: Failed to read data from thumbnail file"); @@ -595,16 +631,12 @@ public static String generateImageThumbnailFromFile(String fileLocation, int siz logger.fine("image dimensions: " + width + "x" + height); - thumbFileLocation = rescaleImage(fullSizeImage, width, height, size, fileLocation); + return rescaleImage(fullSizeImage, width, height, size, fileLocation); - if (thumbFileLocation != null) { - return thumbFileLocation; - } } catch (Exception e) { logger.warning("Failed to read in an image from " + fileLocation + ": " + e.getMessage()); } return null; - } /* @@ -643,10 +675,14 @@ public static String rescaleImage(BufferedImage fullSizeImage, int width, int he try { rescaleImage(fullSizeImage, width, height, size, outputFileStream); } catch (Exception ioex) { - logger.warning("caught Exceptiopn trying to create rescaled image " + outputLocation); - return null; + logger.warning("caught Exception trying to create rescaled image " + outputLocation); + outputLocation = null; } finally { IOUtils.closeQuietly(outputFileStream); + // delete the file if the rescaleImage failed + if (outputLocation == null) { + outputFile.delete(); + } } return outputLocation; @@ -702,13 +738,19 @@ private static void rescaleImage(BufferedImage fullSizeImage, int width, int hei if (iter.hasNext()) { writer = (ImageWriter) iter.next(); } else { - throw new IOException("Failed to locatie ImageWriter plugin for image type PNG"); + throw new IOException("Failed to locate ImageWriter plugin for image type PNG"); } - BufferedImage lowRes = new BufferedImage(thumbWidth, thumbHeight, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2 = lowRes.createGraphics(); - g2.drawImage(thumbImage, 0, 0, null); - g2.dispose(); + BufferedImage lowRes = null; + try { + lowRes = new BufferedImage(thumbWidth, thumbHeight, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = lowRes.createGraphics(); + g2.drawImage(thumbImage, 0, 0, null); + g2.dispose(); + } catch (Exception ex) { + logger.warning("Failed to create LoRes Image: " + ex.getMessage()); + throw new IOException("Caught exception trying to generate thumbnail: " + ex.getMessage()); + } try (ImageOutputStream ios = ImageIO.createImageOutputStream(outputStream);) { @@ -824,6 +866,7 @@ public static String generatePDFThumbnailFromFile(String fileLocation, int size) // generate the thumbnail for the requested size, *using the already scaled-down // 400x400 png version, above*: + // (the "exists()" check below appears to be unnecessary - we've already checked early on - ?) if (!((new File(thumbFileLocation)).exists())) { thumbFileLocation = runImageMagick(imageMagickExec, previewFileLocation, thumbFileLocation, size, "png"); } diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java index 1616bfabf96..bca70259cb7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java @@ -124,8 +124,12 @@ public void open(DataAccessOption... options) throws IOException { logger.fine("Setting size"); this.setSize(retrieveSizeFromMedia()); } - if (dataFile.getContentType() != null && dataFile.getContentType().equals("text/tab-separated-values") - && dataFile.isTabularData() && dataFile.getDataTable() != null && (!this.noVarHeader())) { + if (dataFile.getContentType() != null + && dataFile.getContentType().equals("text/tab-separated-values") + && dataFile.isTabularData() + && dataFile.getDataTable() != null + && (!this.noVarHeader()) + && (!dataFile.getDataTable().isStoredWithVariableHeader())) { List datavariables = dataFile.getDataTable().getDataVariables(); String varHeaderLine = generateVariableHeader(datavariables); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index 8afc365417e..d2fdec7b323 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -40,6 +40,7 @@ import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.DvObject; import edu.harvard.iq.dataverse.datavariable.DataVariable; +import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.util.FileUtil; import opennlp.tools.util.StringUtil; @@ -225,7 +226,8 @@ public void open(DataAccessOption... options) throws IOException { && dataFile.getContentType().equals("text/tab-separated-values") && dataFile.isTabularData() && dataFile.getDataTable() != null - && (!this.noVarHeader())) { + && (!this.noVarHeader()) + && (!dataFile.getDataTable().isStoredWithVariableHeader())) { List datavariables = dataFile.getDataTable().getDataVariables(); String varHeaderLine = generateVariableHeader(datavariables); @@ -990,7 +992,10 @@ private String generateTemporaryS3UploadUrl(String key, Date expiration) throws GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, key).withMethod(HttpMethod.PUT).withExpiration(expiration); //Require user to add this header to indicate a temporary file - generatePresignedUrlRequest.putCustomRequestHeader(Headers.S3_TAGGING, "dv-state=temp"); + final boolean taggingDisabled = JvmSettings.DISABLE_S3_TAGGING.lookupOptional(Boolean.class, this.driverId).orElse(false); + if (!taggingDisabled) { + generatePresignedUrlRequest.putCustomRequestHeader(Headers.S3_TAGGING, "dv-state=temp"); + } URL presignedUrl; try { @@ -1039,7 +1044,10 @@ public JsonObjectBuilder generateTemporaryS3UploadUrls(String globalId, String s } else { JsonObjectBuilder urls = Json.createObjectBuilder(); InitiateMultipartUploadRequest initiationRequest = new InitiateMultipartUploadRequest(bucketName, key); - initiationRequest.putCustomRequestHeader(Headers.S3_TAGGING, "dv-state=temp"); + final boolean taggingDisabled = JvmSettings.DISABLE_S3_TAGGING.lookupOptional(Boolean.class, this.driverId).orElse(false); + if (!taggingDisabled) { + initiationRequest.putCustomRequestHeader(Headers.S3_TAGGING, "dv-state=temp"); + } InitiateMultipartUploadResult initiationResponse = s3.initiateMultipartUpload(initiationRequest); String uploadId = initiationResponse.getUploadId(); for (int i = 1; i <= (fileSize / minPartSize) + (fileSize % minPartSize > 0 ? 1 : 0); i++) { diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java index 51cdecf64a0..8d3efa79b51 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java @@ -57,7 +57,6 @@ public abstract class StorageIO { static final String UPLOAD_REDIRECT = "upload-redirect"; static final String UPLOAD_OUT_OF_BAND = "upload-out-of-band"; protected static final String DOWNLOAD_REDIRECT = "download-redirect"; - protected static final String DATAVERSE_INACCESSIBLE = "dataverse-inaccessible"; public StorageIO() { @@ -624,7 +623,7 @@ public static boolean isDirectUploadEnabled(String driverId) { //True by default, Stores (e.g. RemoteOverlay, Globus) can set this false to stop attempts to read bytes public static boolean isDataverseAccessible(String driverId) { - return (true && !Boolean.parseBoolean(getConfigParamForDriver(driverId, DATAVERSE_INACCESSIBLE))); + return (true && !Boolean.parseBoolean(StorageIO.getConfigParamForDriver(driverId, AbstractRemoteOverlayAccessIO.FILES_NOT_ACCESSIBLE_BY_DATAVERSE))); } // Check that storageIdentifier is consistent with store's config diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java index 105a60ab418..717f46ffd60 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java @@ -142,7 +142,8 @@ public void open(DataAccessOption... options) throws IOException { && dataFile.getContentType().equals("text/tab-separated-values") && dataFile.isTabularData() && dataFile.getDataTable() != null - && (!this.noVarHeader())) { + && (!this.noVarHeader()) + && (!dataFile.getDataTable().isStoredWithVariableHeader())) { List datavariables = dataFile.getDataTable().getDataVariables(); String varHeaderLine = generateVariableHeader(datavariables); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/TabularSubsetGenerator.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/TabularSubsetGenerator.java index 782f7f3a52d..a42bb35615f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/TabularSubsetGenerator.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/TabularSubsetGenerator.java @@ -20,30 +20,16 @@ package edu.harvard.iq.dataverse.dataaccess; -import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.datavariable.DataVariable; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Scanner; -import java.util.Set; -import java.math.BigDecimal; -import java.math.MathContext; -import java.math.RoundingMode; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; import java.util.logging.Logger; import java.util.regex.Matcher; @@ -60,305 +46,26 @@ public class TabularSubsetGenerator implements SubsetGenerator { - private static Logger dbgLog = Logger.getLogger(TabularSubsetGenerator.class.getPackage().getName()); - - private static int COLUMN_TYPE_STRING = 1; - private static int COLUMN_TYPE_LONG = 2; - private static int COLUMN_TYPE_DOUBLE = 3; - private static int COLUMN_TYPE_FLOAT = 4; - - private static int MAX_COLUMN_BUFFER = 8192; - - private FileChannel fileChannel = null; - - private int varcount; - private int casecount; - private int subsetcount; - - private byte[][] columnEntries = null; - - - private ByteBuffer[] columnByteBuffers; - private int[] columnBufferSizes; - private int[] columnBufferOffsets; - - private long[] columnStartOffsets; - private long[] columnTotalOffsets; - private long[] columnTotalLengths; - - public TabularSubsetGenerator() { - - } - - public TabularSubsetGenerator (DataFile datafile, List variables) throws IOException { - if (!datafile.isTabularData()) { - throw new IOException("DataFile is not tabular data."); - } - - setVarCount(datafile.getDataTable().getVarQuantity().intValue()); - setCaseCount(datafile.getDataTable().getCaseQuantity().intValue()); - - - - StorageIO dataAccess = datafile.getStorageIO(); - if (!dataAccess.isLocalFile()) { - throw new IOException("Subsetting is supported on local files only!"); - } - - //File tabfile = datafile.getFileSystemLocation().toFile(); - File tabfile = dataAccess.getFileSystemPath().toFile(); - - File rotatedImageFile = getRotatedImage(tabfile, getVarCount(), getCaseCount()); - long[] columnEndOffsets = extractColumnOffsets(rotatedImageFile, getVarCount(), getCaseCount()); - - fileChannel = (FileChannel.open(Paths.get(rotatedImageFile.getAbsolutePath()), StandardOpenOption.READ)); - - if (variables == null || variables.size() < 1 || variables.size() > getVarCount()) { - throw new IOException("Illegal number of variables in the subset request"); - } - - subsetcount = variables.size(); - columnTotalOffsets = new long[subsetcount]; - columnTotalLengths = new long[subsetcount]; - columnByteBuffers = new ByteBuffer[subsetcount]; - + private static Logger logger = Logger.getLogger(TabularSubsetGenerator.class.getPackage().getName()); + //private static int MAX_COLUMN_BUFFER = 8192; - if (subsetcount == 1) { - if (!datafile.getDataTable().getId().equals(variables.get(0).getDataTable().getId())) { - throw new IOException("Variable in the subset request does not belong to the datafile."); - } - dbgLog.fine("single variable subset; setting fileChannel position to "+extractColumnOffset(columnEndOffsets, variables.get(0).getFileOrder())); - fileChannel.position(extractColumnOffset(columnEndOffsets, variables.get(0).getFileOrder())); - columnTotalLengths[0] = extractColumnLength(columnEndOffsets, variables.get(0).getFileOrder()); - columnTotalOffsets[0] = 0; - } else { - columnEntries = new byte[subsetcount][]; - - columnBufferSizes = new int[subsetcount]; - columnBufferOffsets = new int[subsetcount]; - columnStartOffsets = new long[subsetcount]; - - int i = 0; - for (DataVariable var : variables) { - if (!datafile.getDataTable().getId().equals(var.getDataTable().getId())) { - throw new IOException("Variable in the subset request does not belong to the datafile."); - } - columnByteBuffers[i] = ByteBuffer.allocate(MAX_COLUMN_BUFFER); - columnTotalLengths[i] = extractColumnLength(columnEndOffsets, var.getFileOrder()); - columnStartOffsets[i] = extractColumnOffset(columnEndOffsets, var.getFileOrder()); - if (columnTotalLengths[i] < MAX_COLUMN_BUFFER) { - columnByteBuffers[i].limit((int)columnTotalLengths[i]); - } - fileChannel.position(columnStartOffsets[i]); - columnBufferSizes[i] = fileChannel.read(columnByteBuffers[i]); - columnBufferOffsets[i] = 0; - columnTotalOffsets[i] = columnBufferSizes[i]; - i++; - } - } - } - - private int getVarCount() { - return varcount; - } - - private void setVarCount(int varcount) { - this.varcount = varcount; - } - - private int getCaseCount() { - return casecount; - } - - private void setCaseCount(int casecount) { - this.casecount = casecount; - } - - - /* - * Note that this method operates on the *absolute* column number, i.e. - * the number of the physical column in the tabular file. This is stored - * in DataVariable.FileOrder. - * This "column number" should not be confused with the number of column - * in the subset request; a user can request any number of variable - * columns, in an order that doesn't have to follow the physical order - * of the columns in the file. - */ - private long extractColumnOffset(long[] columnEndOffsets, int column) throws IOException { - if (columnEndOffsets == null || columnEndOffsets.length <= column) { - throw new IOException("Offsets table not initialized; or column out of bounds."); - } - long columnOffset; + public TabularSubsetGenerator() { - if (column > 0) { - columnOffset = columnEndOffsets[column - 1]; - } else { - columnOffset = getVarCount() * 8; - } - return columnOffset; } - /* - * See the comment for the method above. + /** + * This class used to be much more complex. There were methods for subsetting + * from fixed-width field files; including using the optimized, "90 deg. rotated" + * versions of such files (i.e. you create a *columns-wise* copy of your data + * file in which the columns are stored sequentially, and a table of byte + * offsets of each column. You can then read individual variable columns + * for cheap; at the expense of doubling the storage size of your tabular + * data files. These methods were not used, so they were deleted (in Jan. 2024 + * prior to 6.2. + * Please consult git history if you are interested in looking at that code. */ - private long extractColumnLength(long[] columnEndOffsets, int column) throws IOException { - if (columnEndOffsets == null || columnEndOffsets.length <= column) { - throw new IOException("Offsets table not initialized; or column out of bounds."); - } - long columnLength; - - if (column > 0) { - columnLength = columnEndOffsets[column] - columnEndOffsets[column - 1]; - } else { - columnLength = columnEndOffsets[0] - varcount * 8; - } - - return columnLength; - } - - - private void bufferMoreColumnBytes(int column) throws IOException { - if (columnTotalOffsets[column] >= columnTotalLengths[column]) { - throw new IOException("attempt to buffer bytes past the column boundary"); - } - fileChannel.position(columnStartOffsets[column] + columnTotalOffsets[column]); - - columnByteBuffers[column].clear(); - if (columnTotalLengths[column] < columnTotalOffsets[column] + MAX_COLUMN_BUFFER) { - dbgLog.fine("Limiting the buffer to "+(columnTotalLengths[column] - columnTotalOffsets[column])+" bytes"); - columnByteBuffers[column].limit((int) (columnTotalLengths[column] - columnTotalOffsets[column])); - } - columnBufferSizes[column] = fileChannel.read(columnByteBuffers[column]); - dbgLog.fine("Read "+columnBufferSizes[column]+" bytes for subset column "+column); - columnBufferOffsets[column] = 0; - columnTotalOffsets[column] += columnBufferSizes[column]; - } - - public byte[] readColumnEntryBytes(int column) { - return readColumnEntryBytes(column, true); - } - - - public byte[] readColumnEntryBytes(int column, boolean addTabs) { - byte[] leftover = null; - byte[] ret = null; - if (columnBufferOffsets[column] >= columnBufferSizes[column]) { - try { - bufferMoreColumnBytes(column); - if (columnBufferSizes[column] < 1) { - return null; - } - } catch (IOException ioe) { - return null; - } - } - - int byteindex = columnBufferOffsets[column]; - try { - while (columnByteBuffers[column].array()[byteindex] != '\n') { - byteindex++; - if (byteindex == columnBufferSizes[column]) { - // save the leftover: - if (leftover == null) { - leftover = new byte[columnBufferSizes[column] - columnBufferOffsets[column]]; - System.arraycopy(columnByteBuffers[column].array(), columnBufferOffsets[column], leftover, 0, columnBufferSizes[column] - columnBufferOffsets[column]); - } else { - byte[] merged = new byte[leftover.length + columnBufferSizes[column]]; - - System.arraycopy(leftover, 0, merged, 0, leftover.length); - System.arraycopy(columnByteBuffers[column].array(), 0, merged, leftover.length, columnBufferSizes[column]); - leftover = merged; - merged = null; - } - // read more bytes: - bufferMoreColumnBytes(column); - if (columnBufferSizes[column] < 1) { - return null; - } - byteindex = 0; - } - } - - // presumably, we have found our '\n': - if (leftover == null) { - ret = new byte[byteindex - columnBufferOffsets[column] + 1]; - System.arraycopy(columnByteBuffers[column].array(), columnBufferOffsets[column], ret, 0, byteindex - columnBufferOffsets[column] + 1); - } else { - ret = new byte[leftover.length + byteindex + 1]; - System.arraycopy(leftover, 0, ret, 0, leftover.length); - System.arraycopy(columnByteBuffers[column].array(), 0, ret, leftover.length, byteindex + 1); - } - - } catch (IOException ioe) { - return null; - } - - columnBufferOffsets[column] = (byteindex + 1); - - if (column < columnBufferOffsets.length - 1) { - ret[ret.length - 1] = '\t'; - } - return ret; - } - - public int readSingleColumnSubset(byte[] buffer) throws IOException { - if (columnTotalOffsets[0] == columnTotalLengths[0]) { - return -1; - } - - if (columnByteBuffers[0] == null) { - dbgLog.fine("allocating single column subset buffer."); - columnByteBuffers[0] = ByteBuffer.allocate(buffer.length); - } - - int bytesread = fileChannel.read(columnByteBuffers[0]); - dbgLog.fine("single column subset: read "+bytesread+" bytes."); - if (columnTotalOffsets[0] + bytesread > columnTotalLengths[0]) { - bytesread = (int)(columnTotalLengths[0] - columnTotalOffsets[0]); - } - System.arraycopy(columnByteBuffers[0].array(), 0, buffer, 0, bytesread); - - columnTotalOffsets[0] += bytesread; - columnByteBuffers[0].clear(); - return bytesread > 0 ? bytesread : -1; - } - - - public byte[] readSubsetLineBytes() throws IOException { - byte[] ret = null; - int total = 0; - - for (int i = 0; i < subsetcount; i++) { - columnEntries[i] = readColumnEntryBytes(i); - if (columnEntries[i] == null) { - throw new IOException("Failed to read subset line entry"); - } - total += columnEntries[i].length; - } - - ret = new byte[total]; - int offset = 0; - for (int i = 0; i < subsetcount; i++) { - System.arraycopy(columnEntries[i], 0, ret, offset, columnEntries[i].length); - offset += columnEntries[i].length; - } - dbgLog.fine("line: "+new String(ret)); - return ret; - } - - - public void close() { - if (fileChannel != null) { - try { - fileChannel.close(); - } catch (IOException ioe) { - // don't care. - } - } - } - public void subsetFile(String infile, String outfile, List columns, Long numCases) { subsetFile(infile, outfile, columns, numCases, "\t"); } @@ -411,11 +118,15 @@ public void subsetFile(InputStream in, String outfile, List columns, Lo * files, OK to use on small files: */ - public static Double[] subsetDoubleVector(InputStream in, int column, int numCases) { + public static Double[] subsetDoubleVector(InputStream in, int column, int numCases, boolean skipHeader) { Double[] retVector = new Double[numCases]; try (Scanner scanner = new Scanner(in)) { scanner.useDelimiter("\\n"); + if (skipHeader) { + skipFirstLine(scanner); + } + for (int caseIndex = 0; caseIndex < numCases; caseIndex++) { if (scanner.hasNext()) { String[] line = (scanner.next()).split("\t", -1); @@ -463,11 +174,15 @@ public static Double[] subsetDoubleVector(InputStream in, int column, int numCas * Same deal as with the method above - straightforward, but (potentially) slow. * Not a resource hog though - will only try to store one vector in memory. */ - public static Float[] subsetFloatVector(InputStream in, int column, int numCases) { + public static Float[] subsetFloatVector(InputStream in, int column, int numCases, boolean skipHeader) { Float[] retVector = new Float[numCases]; try (Scanner scanner = new Scanner(in)) { scanner.useDelimiter("\\n"); + if (skipHeader) { + skipFirstLine(scanner); + } + for (int caseIndex = 0; caseIndex < numCases; caseIndex++) { if (scanner.hasNext()) { String[] line = (scanner.next()).split("\t", -1); @@ -513,11 +228,15 @@ public static Float[] subsetFloatVector(InputStream in, int column, int numCases * Same deal as with the method above - straightforward, but (potentially) slow. * Not a resource hog though - will only try to store one vector in memory. */ - public static Long[] subsetLongVector(InputStream in, int column, int numCases) { + public static Long[] subsetLongVector(InputStream in, int column, int numCases, boolean skipHeader) { Long[] retVector = new Long[numCases]; try (Scanner scanner = new Scanner(in)) { scanner.useDelimiter("\\n"); + if (skipHeader) { + skipFirstLine(scanner); + } + for (int caseIndex = 0; caseIndex < numCases; caseIndex++) { if (scanner.hasNext()) { String[] line = (scanner.next()).split("\t", -1); @@ -549,11 +268,15 @@ public static Long[] subsetLongVector(InputStream in, int column, int numCases) * Same deal as with the method above - straightforward, but (potentially) slow. * Not a resource hog though - will only try to store one vector in memory. */ - public static String[] subsetStringVector(InputStream in, int column, int numCases) { + public static String[] subsetStringVector(InputStream in, int column, int numCases, boolean skipHeader) { String[] retVector = new String[numCases]; try (Scanner scanner = new Scanner(in)) { scanner.useDelimiter("\\n"); + if (skipHeader) { + skipFirstLine(scanner); + } + for (int caseIndex = 0; caseIndex < numCases; caseIndex++) { if (scanner.hasNext()) { String[] line = (scanner.next()).split("\t", -1); @@ -621,819 +344,10 @@ public static String[] subsetStringVector(InputStream in, int column, int numCas } - /* - * Straightforward method for subsetting a tab-delimited data file, extracting - * all the columns representing continuous variables and returning them as - * a 2-dimensional array of Doubles; - * Inefficient on large files, OK to use on small ones. - */ - public static Double[][] subsetDoubleVectors(InputStream in, Set columns, int numCases) throws IOException { - Double[][] retVector = new Double[columns.size()][numCases]; - try (Scanner scanner = new Scanner(in)) { - scanner.useDelimiter("\\n"); - - for (int caseIndex = 0; caseIndex < numCases; caseIndex++) { - if (scanner.hasNext()) { - String[] line = (scanner.next()).split("\t", -1); - int j = 0; - for (Integer i : columns) { - try { - // TODO: verify that NaN and +-Inf are going to be - // handled correctly here! -- L.A. - // NO, "+-Inf" is not handled correctly; see the - // comment further down below. - retVector[j][caseIndex] = new Double(line[i]); - } catch (NumberFormatException ex) { - retVector[j][caseIndex] = null; // missing value - } - j++; - } - } else { - throw new IOException("Tab file has fewer rows than the stored number of cases!"); - } - } - - int tailIndex = numCases; - while (scanner.hasNext()) { - String nextLine = scanner.next(); - if (!"".equals(nextLine)) { - throw new IOException("Tab file has more nonempty rows than the stored number of cases ("+numCases+")! current index: "+tailIndex+", line: "+nextLine); - } - tailIndex++; - } - - } - return retVector; - - } - - public String[] subsetStringVector(DataFile datafile, int column) throws IOException { - return (String[])subsetObjectVector(datafile, column, COLUMN_TYPE_STRING); - } - - public Double[] subsetDoubleVector(DataFile datafile, int column) throws IOException { - return (Double[])subsetObjectVector(datafile, column, COLUMN_TYPE_DOUBLE); - } - - public Long[] subsetLongVector(DataFile datafile, int column) throws IOException { - return (Long[])subsetObjectVector(datafile, column, COLUMN_TYPE_LONG); - } - - // Float methods are temporary; - // In normal operations we'll be treating all the floating point types as - // doubles. I need to be able to handle floats for some 4.0 vs 3.* ingest - // tests. -- L.A. - - public Float[] subsetFloatVector(DataFile datafile, int column) throws IOException { - return (Float[])subsetObjectVector(datafile, column, COLUMN_TYPE_FLOAT); - } - - public String[] subsetStringVector(File tabfile, int column, int varcount, int casecount) throws IOException { - return (String[])subsetObjectVector(tabfile, column, varcount, casecount, COLUMN_TYPE_STRING); - } - - public Double[] subsetDoubleVector(File tabfile, int column, int varcount, int casecount) throws IOException { - return (Double[])subsetObjectVector(tabfile, column, varcount, casecount, COLUMN_TYPE_DOUBLE); - } - - public Long[] subsetLongVector(File tabfile, int column, int varcount, int casecount) throws IOException { - return (Long[])subsetObjectVector(tabfile, column, varcount, casecount, COLUMN_TYPE_LONG); - } - - public Float[] subsetFloatVector(File tabfile, int column, int varcount, int casecount) throws IOException { - return (Float[])subsetObjectVector(tabfile, column, varcount, casecount, COLUMN_TYPE_FLOAT); - } - - public Object[] subsetObjectVector(DataFile dataFile, int column, int columntype) throws IOException { - if (!dataFile.isTabularData()) { - throw new IOException("DataFile is not tabular data."); - } - - int varcount = dataFile.getDataTable().getVarQuantity().intValue(); - int casecount = dataFile.getDataTable().getCaseQuantity().intValue(); - - if (column >= varcount) { - throw new IOException("Column "+column+" is out of bounds."); - } - - StorageIO dataAccess = dataFile.getStorageIO(); - if (!dataAccess.isLocalFile()) { - throw new IOException("Subsetting is supported on local files only!"); - } - - //File tabfile = datafile.getFileSystemLocation().toFile(); - File tabfile = dataAccess.getFileSystemPath().toFile(); - - if (columntype == COLUMN_TYPE_STRING) { - String filename = dataFile.getFileMetadata().getLabel(); - if (filename != null) { - filename = filename.replaceFirst("^_", ""); - Integer fnumvalue = null; - try { - fnumvalue = new Integer(filename); - } catch (Exception ex){ - fnumvalue = null; - } - if (fnumvalue != null) { - //if ((fnumvalue.intValue() < 112497)) { // && (fnumvalue.intValue() > 60015)) { - if ((fnumvalue.intValue() < 111931)) { // && (fnumvalue.intValue() > 60015)) { - if (!(fnumvalue.intValue() == 60007 - || fnumvalue.intValue() == 59997 - || fnumvalue.intValue() == 60015 - || fnumvalue.intValue() == 59948 - || fnumvalue.intValue() == 60012 - || fnumvalue.intValue() == 52585 - || fnumvalue.intValue() == 60005 - || fnumvalue.intValue() == 60002 - || fnumvalue.intValue() == 59954 - || fnumvalue.intValue() == 60008 - || fnumvalue.intValue() == 54972 - || fnumvalue.intValue() == 55010 - || fnumvalue.intValue() == 54996 - || fnumvalue.intValue() == 53527 - || fnumvalue.intValue() == 53546 - || fnumvalue.intValue() == 55002 - || fnumvalue.intValue() == 55006 - || fnumvalue.intValue() == 54998 - || fnumvalue.intValue() == 52552 - // SPSS/SAV cases with similar issue - compat mode must be disabled - //|| fnumvalue.intValue() == 101826 // temporary - tricky file with accents and v. 16... - || fnumvalue.intValue() == 54618 // another SAV file, with long strings... - || fnumvalue.intValue() == 54619 // [same] - || fnumvalue.intValue() == 57983 - || fnumvalue.intValue() == 58262 - || fnumvalue.intValue() == 58288 - || fnumvalue.intValue() == 58656 - || fnumvalue.intValue() == 59144 - // || fnumvalue.intValue() == 69626 [nope!] - )) { - dbgLog.info("\"Old\" file name detected; using \"compatibility mode\" for a character vector subset;"); - return subsetObjectVector(tabfile, column, varcount, casecount, columntype, true); - } - } - } - } - } - - return subsetObjectVector(tabfile, column, varcount, casecount, columntype); - } - - public Object[] subsetObjectVector(File tabfile, int column, int varcount, int casecount, int columntype) throws IOException { - return subsetObjectVector(tabfile, column, varcount, casecount, columntype, false); - } - - - - public Object[] subsetObjectVector(File tabfile, int column, int varcount, int casecount, int columntype, boolean compatmode) throws IOException { - - Object[] retVector = null; - - boolean isString = false; - boolean isDouble = false; - boolean isLong = false; - boolean isFloat = false; - - //Locale loc = new Locale("en", "US"); - - if (columntype == COLUMN_TYPE_STRING) { - isString = true; - retVector = new String[casecount]; - } else if (columntype == COLUMN_TYPE_DOUBLE) { - isDouble = true; - retVector = new Double[casecount]; - } else if (columntype == COLUMN_TYPE_LONG) { - isLong = true; - retVector = new Long[casecount]; - } else if (columntype == COLUMN_TYPE_FLOAT){ - isFloat = true; - retVector = new Float[casecount]; - } else { - throw new IOException("Unsupported column type: "+columntype); - } - - File rotatedImageFile = getRotatedImage(tabfile, varcount, casecount); - long[] columnEndOffsets = extractColumnOffsets(rotatedImageFile, varcount, casecount); - long columnOffset = 0; - long columnLength = 0; - - if (column > 0) { - columnOffset = columnEndOffsets[column - 1]; - columnLength = columnEndOffsets[column] - columnEndOffsets[column - 1]; - } else { - columnOffset = varcount * 8; - columnLength = columnEndOffsets[0] - varcount * 8; - } - int caseindex = 0; - - try (FileChannel fc = (FileChannel.open(Paths.get(rotatedImageFile.getAbsolutePath()), - StandardOpenOption.READ))) { - fc.position(columnOffset); - int MAX_COLUMN_BUFFER = 8192; - - ByteBuffer in = ByteBuffer.allocate(MAX_COLUMN_BUFFER); - - if (columnLength < MAX_COLUMN_BUFFER) { - in.limit((int) (columnLength)); - } - - long bytesRead = 0; - long bytesReadTotal = 0; - - int byteoffset = 0; - byte[] leftover = null; - - while (bytesReadTotal < columnLength) { - bytesRead = fc.read(in); - byte[] columnBytes = in.array(); - int bytecount = 0; - - while (bytecount < bytesRead) { - if (columnBytes[bytecount] == '\n') { - /* - String token = new String(columnBytes, byteoffset, bytecount-byteoffset, "UTF8"); - - if (leftover != null) { - String leftoverString = new String (leftover, "UTF8"); - token = leftoverString + token; - leftover = null; - } - */ - /* - * Note that the way I was doing it at first - above - - * was not quite the correct way - because I was creating UTF8 - * strings from the leftover bytes, and the bytes in the - * current buffer *separately*; which means, if a multi-byte - * UTF8 character got split in the middle between one buffer - * and the next, both chunks of it would become junk - * characters, on each side! - * The correct way of doing it, of course, is to create a - * merged byte buffer, and then turn it into a UTF8 string. - * -- L.A. 4.0 - */ - String token = null; - - if (leftover == null) { - token = new String(columnBytes, byteoffset, bytecount - byteoffset, "UTF8"); - } else { - byte[] merged = new byte[leftover.length + bytecount - byteoffset]; - - System.arraycopy(leftover, 0, merged, 0, leftover.length); - System.arraycopy(columnBytes, byteoffset, merged, leftover.length, bytecount - byteoffset); - token = new String(merged, "UTF8"); - leftover = null; - merged = null; - } - - if (isString) { - if ("".equals(token)) { - // An empty string is a string missing value! - // An empty string in quotes is an empty string! - retVector[caseindex] = null; - } else { - // Strip the outer quotes: - token = token.replaceFirst("^\\\"", ""); - token = token.replaceFirst("\\\"$", ""); - - // We need to restore the special characters that - // are stored in tab files escaped - quotes, new lines - // and tabs. Before we do that however, we need to - // take care of any escaped backslashes stored in - // the tab file. I.e., "foo\t" should be transformed - // to "foo"; but "foo\\t" should be transformed - // to "foo\t". This way new lines and tabs that were - // already escaped in the original data are not - // going to be transformed to unescaped tab and - // new line characters! - - String[] splitTokens = token.split(Matcher.quoteReplacement("\\\\"), -2); - - // (note that it's important to use the 2-argument version - // of String.split(), and set the limit argument to a - // negative value; otherwise any trailing backslashes - // are lost.) - - for (int i = 0; i < splitTokens.length; i++) { - splitTokens[i] = splitTokens[i].replaceAll(Matcher.quoteReplacement("\\\""), "\""); - splitTokens[i] = splitTokens[i].replaceAll(Matcher.quoteReplacement("\\t"), "\t"); - splitTokens[i] = splitTokens[i].replaceAll(Matcher.quoteReplacement("\\n"), "\n"); - splitTokens[i] = splitTokens[i].replaceAll(Matcher.quoteReplacement("\\r"), "\r"); - } - // TODO: - // Make (some of?) the above optional; for ex., we - // do need to restore the newlines when calculating UNFs; - // But if we are subsetting these vectors in order to - // create a new tab-delimited file, they will - // actually break things! -- L.A. Jul. 28 2014 - - token = StringUtils.join(splitTokens, '\\'); - - // "compatibility mode" - a hack, to be able to produce - // unfs identical to those produced by the "early" - // unf5 jar; will be removed in production 4.0. - // -- L.A. (TODO: ...) - if (compatmode && !"".equals(token)) { - if (token.length() > 128) { - if ("".equals(token.trim())) { - // don't ask... - token = token.substring(0, 129); - } else { - token = token.substring(0, 128); - // token = String.format(loc, "%.128s", token); - token = token.trim(); - // dbgLog.info("formatted and trimmed: "+token); - } - } else { - if ("".equals(token.trim())) { - // again, don't ask; - // - this replicates some bugginness - // that happens inside unf5; - token = "null"; - } else { - token = token.trim(); - } - } - } - - retVector[caseindex] = token; - } - } else if (isDouble) { - try { - // TODO: verify that NaN and +-Inf are - // handled correctly here! -- L.A. - // Verified: new Double("nan") works correctly, - // resulting in Double.NaN; - // Double("[+-]Inf") doesn't work however; - // (the constructor appears to be expecting it - // to be spelled as "Infinity", "-Infinity", etc. - if ("inf".equalsIgnoreCase(token) || "+inf".equalsIgnoreCase(token)) { - retVector[caseindex] = java.lang.Double.POSITIVE_INFINITY; - } else if ("-inf".equalsIgnoreCase(token)) { - retVector[caseindex] = java.lang.Double.NEGATIVE_INFINITY; - } else if (token == null || token.equals("")) { - // missing value: - retVector[caseindex] = null; - } else { - retVector[caseindex] = new Double(token); - } - } catch (NumberFormatException ex) { - dbgLog.warning("NumberFormatException thrown for " + token + " as Double"); - - retVector[caseindex] = null; // missing value - // TODO: ? - } - } else if (isLong) { - try { - retVector[caseindex] = new Long(token); - } catch (NumberFormatException ex) { - retVector[caseindex] = null; // assume missing value - } - } else if (isFloat) { - try { - if ("inf".equalsIgnoreCase(token) || "+inf".equalsIgnoreCase(token)) { - retVector[caseindex] = java.lang.Float.POSITIVE_INFINITY; - } else if ("-inf".equalsIgnoreCase(token)) { - retVector[caseindex] = java.lang.Float.NEGATIVE_INFINITY; - } else if (token == null || token.equals("")) { - // missing value: - retVector[caseindex] = null; - } else { - retVector[caseindex] = new Float(token); - } - } catch (NumberFormatException ex) { - dbgLog.warning("NumberFormatException thrown for " + token + " as Float"); - retVector[caseindex] = null; // assume missing value (TODO: ?) - } - } - caseindex++; - - if (bytecount == bytesRead - 1) { - byteoffset = 0; - } else { - byteoffset = bytecount + 1; - } - } else { - if (bytecount == bytesRead - 1) { - // We've reached the end of the buffer; - // This means we'll save whatever unused bytes left in - // it - i.e., the bytes between the last new line - // encountered and the end - in the leftover buffer. - - // *EXCEPT*, there may be a case of a very long String - // that is actually longer than MAX_COLUMN_BUFFER, in - // which case it is possible that we've read through - // an entire buffer of bytes without finding any - // new lines... in this case we may need to add this - // entire byte buffer to an already existing leftover - // buffer! - if (leftover == null) { - leftover = new byte[(int) bytesRead - byteoffset]; - System.arraycopy(columnBytes, byteoffset, leftover, 0, (int) bytesRead - byteoffset); - } else { - if (byteoffset != 0) { - throw new IOException("Reached the end of the byte buffer, with some leftover left from the last read; yet the offset is not zero!"); - } - byte[] merged = new byte[leftover.length + (int) bytesRead]; - - System.arraycopy(leftover, 0, merged, 0, leftover.length); - System.arraycopy(columnBytes, byteoffset, merged, leftover.length, (int) bytesRead); - // leftover = null; - leftover = merged; - merged = null; - } - byteoffset = 0; - - } - } - bytecount++; - } - - bytesReadTotal += bytesRead; - in.clear(); - if (columnLength - bytesReadTotal < MAX_COLUMN_BUFFER) { - in.limit((int) (columnLength - bytesReadTotal)); - } - } - - } - - if (caseindex != casecount) { - throw new IOException("Faile to read "+casecount+" tokens for column "+column); - //System.out.println("read "+caseindex+" tokens instead of expected "+casecount+"."); - } - - return retVector; - } - - private long[] extractColumnOffsets (File rotatedImageFile, int varcount, int casecount) throws IOException { - long[] byteOffsets = new long[varcount]; - - try (BufferedInputStream rotfileStream = new BufferedInputStream(new FileInputStream(rotatedImageFile))) { - - byte[] offsetHeader = new byte[varcount * 8]; - - int readlen = rotfileStream.read(offsetHeader); - - if (readlen != varcount * 8) { - throw new IOException("Could not read " + varcount * 8 + " header bytes from the rotated file."); - } - - for (int varindex = 0; varindex < varcount; varindex++) { - byte[] offsetBytes = new byte[8]; - System.arraycopy(offsetHeader, varindex * 8, offsetBytes, 0, 8); - - ByteBuffer offsetByteBuffer = ByteBuffer.wrap(offsetBytes); - byteOffsets[varindex] = offsetByteBuffer.getLong(); - - // System.out.println(byteOffsets[varindex]); - } - + private static void skipFirstLine(Scanner scanner) { + if (!scanner.hasNext()) { + throw new RuntimeException("Failed to read the variable name header line from the tab-delimited file!"); } - - return byteOffsets; - } - - private File getRotatedImage(File tabfile, int varcount, int casecount) throws IOException { - String fileName = tabfile.getAbsolutePath(); - String rotatedImageFileName = fileName + ".90d"; - File rotatedImageFile = new File(rotatedImageFileName); - if (rotatedImageFile.exists()) { - //System.out.println("Image already exists!"); - return rotatedImageFile; - } - - return generateRotatedImage(tabfile, varcount, casecount); - - } - - private File generateRotatedImage (File tabfile, int varcount, int casecount) throws IOException { - // TODO: throw exceptions if bad file, zero varcount, etc. ... - - String fileName = tabfile.getAbsolutePath(); - String rotatedImageFileName = fileName + ".90d"; - - int MAX_OUTPUT_STREAMS = 32; - int MAX_BUFFERED_BYTES = 10 * 1024 * 1024; // 10 MB - for now? - int MAX_COLUMN_BUFFER = 8 * 1024; - - // offsetHeader will contain the byte offsets of the individual column - // vectors in the final rotated image file - byte[] offsetHeader = new byte[varcount * 8]; - int[] bufferedSizes = new int[varcount]; - long[] cachedfileSizes = new long[varcount]; - File[] columnTempFiles = new File[varcount]; - - for (int i = 0; i < varcount; i++) { - bufferedSizes[i] = 0; - cachedfileSizes[i] = 0; - } - - // TODO: adjust MAX_COLUMN_BUFFER here, so that the total size is - // no more than MAX_BUFFERED_BYTES (but no less than 1024 maybe?) - - byte[][] bufferedColumns = new byte [varcount][MAX_COLUMN_BUFFER]; - - // read the tab-delimited file: - - try (FileInputStream tabfileStream = new FileInputStream(tabfile); - Scanner scanner = new Scanner(tabfileStream)) { - scanner.useDelimiter("\\n"); - - for (int caseindex = 0; caseindex < casecount; caseindex++) { - if (scanner.hasNext()) { - String[] line = (scanner.next()).split("\t", -1); - // TODO: throw an exception if there are fewer tab-delimited - // tokens than the number of variables specified. - String token = ""; - int tokensize = 0; - for (int varindex = 0; varindex < varcount; varindex++) { - // TODO: figure out the safest way to convert strings to - // bytes here. Is it going to be safer to use getBytes("UTF8")? - // we are already making the assumption that the values - // in the tab file are in UTF8. -- L.A. - token = line[varindex] + "\n"; - tokensize = token.getBytes().length; - if (bufferedSizes[varindex] + tokensize > MAX_COLUMN_BUFFER) { - // fill the buffer and dump its contents into the temp file: - // (do note that there may be *several* MAX_COLUMN_BUFFERs - // worth of bytes in the token!) - - int tokenoffset = 0; - - if (bufferedSizes[varindex] != MAX_COLUMN_BUFFER) { - tokenoffset = MAX_COLUMN_BUFFER - bufferedSizes[varindex]; - System.arraycopy(token.getBytes(), 0, bufferedColumns[varindex], bufferedSizes[varindex], tokenoffset); - } // (otherwise the buffer is already full, and we should - // simply dump it into the temp file, without adding any - // extra bytes to it) - - File bufferTempFile = columnTempFiles[varindex]; - if (bufferTempFile == null) { - bufferTempFile = File.createTempFile("columnBufferFile", "bytes"); - columnTempFiles[varindex] = bufferTempFile; - } - - // *append* the contents of the buffer to the end of the - // temp file, if already exists: - try (BufferedOutputStream outputStream = new BufferedOutputStream( - new FileOutputStream(bufferTempFile, true))) { - outputStream.write(bufferedColumns[varindex], 0, MAX_COLUMN_BUFFER); - cachedfileSizes[varindex] += MAX_COLUMN_BUFFER; - - // keep writing MAX_COLUMN_BUFFER-size chunks of bytes into - // the temp file, for as long as there's more than MAX_COLUMN_BUFFER - // bytes left in the token: - - while (tokensize - tokenoffset > MAX_COLUMN_BUFFER) { - outputStream.write(token.getBytes(), tokenoffset, MAX_COLUMN_BUFFER); - cachedfileSizes[varindex] += MAX_COLUMN_BUFFER; - tokenoffset += MAX_COLUMN_BUFFER; - } - - } - - // buffer the remaining bytes and reset the buffered - // byte counter: - - System.arraycopy(token.getBytes(), - tokenoffset, - bufferedColumns[varindex], - 0, - tokensize - tokenoffset); - - bufferedSizes[varindex] = tokensize - tokenoffset; - - } else { - // continue buffering - System.arraycopy(token.getBytes(), 0, bufferedColumns[varindex], bufferedSizes[varindex], tokensize); - bufferedSizes[varindex] += tokensize; - } - } - } else { - throw new IOException("Tab file has fewer rows than the stored number of cases!"); - } - } - } - - // OK, we've created the individual byte vectors of the tab file columns; - // they may be partially saved in temp files and/or in memory. - // We now need to go through all these buffers and create the final - // rotated image file. - - try (BufferedOutputStream finalOut = new BufferedOutputStream( - new FileOutputStream(new File(rotatedImageFileName)))) { - - // but first we should create the offset header and write it out into - // the final file; because it should be at the head, doh! - - long columnOffset = varcount * 8; - // (this is the offset of the first column vector; it is equal to the - // size of the offset header, i.e. varcount * 8 bytes) - - for (int varindex = 0; varindex < varcount; varindex++) { - long totalColumnBytes = cachedfileSizes[varindex] + bufferedSizes[varindex]; - columnOffset += totalColumnBytes; - // totalColumnBytes; - byte[] columnOffsetByteArray = ByteBuffer.allocate(8).putLong(columnOffset).array(); - System.arraycopy(columnOffsetByteArray, 0, offsetHeader, varindex * 8, 8); - } - - finalOut.write(offsetHeader, 0, varcount * 8); - - for (int varindex = 0; varindex < varcount; varindex++) { - long cachedBytesRead = 0; - - // check if there is a cached temp file: - - File cachedTempFile = columnTempFiles[varindex]; - if (cachedTempFile != null) { - byte[] cachedBytes = new byte[MAX_COLUMN_BUFFER]; - try (BufferedInputStream cachedIn = new BufferedInputStream(new FileInputStream(cachedTempFile))) { - int readlen = 0; - while ((readlen = cachedIn.read(cachedBytes)) > -1) { - finalOut.write(cachedBytes, 0, readlen); - cachedBytesRead += readlen; - } - } - - // delete the temp file: - cachedTempFile.delete(); - - } - - if (cachedBytesRead != cachedfileSizes[varindex]) { - throw new IOException("Could not read the correct number of bytes cached for column "+varindex+"; "+ - cachedfileSizes[varindex] + " bytes expected, "+cachedBytesRead+" read."); - } - - // then check if there are any bytes buffered for this column: - - if (bufferedSizes[varindex] > 0) { - finalOut.write(bufferedColumns[varindex], 0, bufferedSizes[varindex]); - } - - } - } - - return new File(rotatedImageFileName); - - } - - /* - * Test method for taking a "rotated" image, and reversing it, reassembling - * all the columns in the original order. Which should result in a file - * byte-for-byte identical file to the original tab-delimited version. - * - * (do note that this method is not efficiently implemented; it's only - * being used for experiments so far, to confirm the accuracy of the - * accuracy of generateRotatedImage(). It should not be used for any - * practical means in the application!) - */ - private void reverseRotatedImage (File rotfile, int varcount, int casecount) throws IOException { - // open the file, read in the offset header: - try (BufferedInputStream rotfileStream = new BufferedInputStream(new FileInputStream(rotfile))) { - byte[] offsetHeader = new byte[varcount * 8]; - long[] byteOffsets = new long[varcount]; - - int readlen = rotfileStream.read(offsetHeader); - - if (readlen != varcount * 8) { - throw new IOException ("Could not read "+varcount*8+" header bytes from the rotated file."); - } - - for (int varindex = 0; varindex < varcount; varindex++) { - byte[] offsetBytes = new byte[8]; - System.arraycopy(offsetHeader, varindex*8, offsetBytes, 0, 8); - - ByteBuffer offsetByteBuffer = ByteBuffer.wrap(offsetBytes); - byteOffsets[varindex] = offsetByteBuffer.getLong(); - - //System.out.println(byteOffsets[varindex]); - } - - String [][] reversedMatrix = new String[casecount][varcount]; - - long offset = varcount * 8; - byte[] columnBytes; - - for (int varindex = 0; varindex < varcount; varindex++) { - long columnLength = byteOffsets[varindex] - offset; - - - - columnBytes = new byte[(int)columnLength]; - readlen = rotfileStream.read(columnBytes); - - if (readlen != columnLength) { - throw new IOException ("Could not read "+columnBytes+" bytes for column "+varindex); - } - /* - String columnString = new String(columnBytes); - //System.out.print(columnString); - String[] values = columnString.split("\n", -1); - - if (values.length < casecount) { - throw new IOException("count mismatch: "+values.length+" tokens found for column "+varindex); - } - - for (int caseindex = 0; caseindex < casecount; caseindex++) { - reversedMatrix[caseindex][varindex] = values[caseindex]; - }*/ - - int bytecount = 0; - int byteoffset = 0; - int caseindex = 0; - //System.out.println("generating value vector for column "+varindex); - while (bytecount < columnLength) { - if (columnBytes[bytecount] == '\n') { - String token = new String(columnBytes, byteoffset, bytecount-byteoffset); - reversedMatrix[caseindex++][varindex] = token; - byteoffset = bytecount + 1; - } - bytecount++; - } - - if (caseindex != casecount) { - throw new IOException("count mismatch: "+caseindex+" tokens found for column "+varindex); - } - offset = byteOffsets[varindex]; - } - - for (int caseindex = 0; caseindex < casecount; caseindex++) { - for (int varindex = 0; varindex < varcount; varindex++) { - System.out.print(reversedMatrix[caseindex][varindex]); - if (varindex < varcount-1) { - System.out.print("\t"); - } else { - System.out.print("\n"); - } - } - } - - } - - - } - - /** - * main() method, for testing - * usage: java edu.harvard.iq.dataverse.dataaccess.TabularSubsetGenerator testfile.tab varcount casecount column type - * make sure the CLASSPATH contains ... - * - */ - - public static void main(String[] args) { - - String tabFileName = args[0]; - int varcount = new Integer(args[1]).intValue(); - int casecount = new Integer(args[2]).intValue(); - int column = new Integer(args[3]).intValue(); - String type = args[4]; - - File tabFile = new File(tabFileName); - File rotatedImageFile = null; - - TabularSubsetGenerator subsetGenerator = new TabularSubsetGenerator(); - - /* - try { - rotatedImageFile = subsetGenerator.getRotatedImage(tabFile, varcount, casecount); - } catch (IOException ex) { - System.out.println(ex.getMessage()); - } - */ - - //System.out.println("\nFinished generating \"rotated\" column image file."); - - //System.out.println("\nOffsets:"); - - MathContext doubleMathContext = new MathContext(15, RoundingMode.HALF_EVEN); - String FORMAT_IEEE754 = "%+#.15e"; - - try { - //subsetGenerator.reverseRotatedImage(rotatedImageFile, varcount, casecount); - //String[] columns = subsetGenerator.subsetStringVector(tabFile, column, varcount, casecount); - if ("string".equals(type)) { - String[] columns = subsetGenerator.subsetStringVector(tabFile, column, varcount, casecount); - for (int i = 0; i < casecount; i++) { - System.out.println(columns[i]); - } - } else { - - Double[] columns = subsetGenerator.subsetDoubleVector(tabFile, column, varcount, casecount); - for (int i = 0; i < casecount; i++) { - if (columns[i] != null) { - BigDecimal outBigDecimal = new BigDecimal(columns[i], doubleMathContext); - System.out.println(String.format(FORMAT_IEEE754, outBigDecimal)); - } else { - System.out.println("NA"); - } - //System.out.println(columns[i]); - } - } - } catch (IOException ex) { - System.out.println(ex.getMessage()); - } - } -} - - + scanner.next(); + } +} \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/TabularSubsetInputStream.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/TabularSubsetInputStream.java deleted file mode 100644 index 89e033353c1..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/TabularSubsetInputStream.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ - -package edu.harvard.iq.dataverse.dataaccess; - -import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.datavariable.DataVariable; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import java.util.logging.Logger; - -/** - * - * @author Leonid Andreev - */ -public class TabularSubsetInputStream extends InputStream { - private static final Logger logger = Logger.getLogger(TabularSubsetInputStream.class.getCanonicalName()); - - private TabularSubsetGenerator subsetGenerator = null; - private int numberOfSubsetVariables; - private int numberOfObservations; - private int numberOfObservationsRead = 0; - private byte[] leftoverBytes = null; - - public TabularSubsetInputStream(DataFile datafile, List variables) throws IOException { - if (datafile == null) { - throw new IOException("Null datafile in subset request"); - } - if (!datafile.isTabularData()) { - throw new IOException("Subset requested on a non-tabular data file"); - } - numberOfObservations = datafile.getDataTable().getCaseQuantity().intValue(); - - if (variables == null || variables.size() < 1) { - throw new IOException("Null or empty list of variables in subset request."); - } - numberOfSubsetVariables = variables.size(); - subsetGenerator = new TabularSubsetGenerator(datafile, variables); - - } - - //@Override - public int read() throws IOException { - throw new IOException("read() method not implemented; do not use."); - } - - //@Override - public int read(byte[] b) throws IOException { - // TODO: - // Move this code into TabularSubsetGenerator - logger.fine("subset input stream: read request, on a "+b.length+" byte buffer;"); - - if (numberOfSubsetVariables == 1) { - logger.fine("calling the single variable subset read method"); - return subsetGenerator.readSingleColumnSubset(b); - } - - int bytesread = 0; - byte [] linebuffer; - - // do we have a leftover? - if (leftoverBytes != null) { - if (leftoverBytes.length < b.length) { - System.arraycopy(leftoverBytes, 0, b, 0, leftoverBytes.length); - bytesread = leftoverBytes.length; - leftoverBytes = null; - - } else { - // shouldn't really happen... unless it's a very large subset, - // or a very long string, etc. - System.arraycopy(leftoverBytes, 0, b, 0, b.length); - byte[] tmp = new byte[leftoverBytes.length - b.length]; - System.arraycopy(leftoverBytes, b.length, tmp, 0, leftoverBytes.length - b.length); - leftoverBytes = tmp; - tmp = null; - return b.length; - } - } - - while (bytesread < b.length && numberOfObservationsRead < numberOfObservations) { - linebuffer = subsetGenerator.readSubsetLineBytes(); - numberOfObservationsRead++; - - if (bytesread + linebuffer.length < b.length) { - // copy linebuffer into the return buffer: - System.arraycopy(linebuffer, 0, b, bytesread, linebuffer.length); - bytesread += linebuffer.length; - } else { - System.arraycopy(linebuffer, 0, b, bytesread, b.length - bytesread); - // save the leftover; - if (bytesread + linebuffer.length > b.length) { - leftoverBytes = new byte[bytesread + linebuffer.length - b.length]; - System.arraycopy(linebuffer, b.length - bytesread, leftoverBytes, 0, bytesread + linebuffer.length - b.length); - } - return b.length; - } - } - - // and this means we've reached the end of the tab file! - - return bytesread > 0 ? bytesread : -1; - } - - //@Override - public void close() { - if (subsetGenerator != null) { - subsetGenerator.close(); - } - } -} diff --git a/src/main/java/edu/harvard/iq/dataverse/datacapturemodule/DataCaptureModuleException.java b/src/main/java/edu/harvard/iq/dataverse/datacapturemodule/DataCaptureModuleException.java index 3329d92b7a9..474674bda73 100644 --- a/src/main/java/edu/harvard/iq/dataverse/datacapturemodule/DataCaptureModuleException.java +++ b/src/main/java/edu/harvard/iq/dataverse/datacapturemodule/DataCaptureModuleException.java @@ -1,7 +1,9 @@ package edu.harvard.iq.dataverse.datacapturemodule; +@Deprecated(forRemoval = true, since = "2024-07-07") public class DataCaptureModuleException extends Exception { + @Deprecated(forRemoval = true, since = "2024-07-07") public DataCaptureModuleException(String message, Throwable cause) { super(message, cause); } diff --git a/src/main/java/edu/harvard/iq/dataverse/datacapturemodule/DataCaptureModuleUtil.java b/src/main/java/edu/harvard/iq/dataverse/datacapturemodule/DataCaptureModuleUtil.java index 460e4727afc..094d3976133 100644 --- a/src/main/java/edu/harvard/iq/dataverse/datacapturemodule/DataCaptureModuleUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/datacapturemodule/DataCaptureModuleUtil.java @@ -12,10 +12,12 @@ import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; +@Deprecated(forRemoval = true, since = "2024-07-07") public class DataCaptureModuleUtil { private static final Logger logger = Logger.getLogger(DataCaptureModuleUtil.class.getCanonicalName()); + @Deprecated(forRemoval = true, since = "2024-07-07") public static boolean rsyncSupportEnabled(String uploadMethodsSettings) { logger.fine("uploadMethodsSettings: " + uploadMethodsSettings);; if (uploadMethodsSettings==null){ @@ -28,6 +30,7 @@ public static boolean rsyncSupportEnabled(String uploadMethodsSettings) { /** * generate JSON to send to DCM */ + @Deprecated(forRemoval = true, since = "2024-07-07") public static JsonObject generateJsonForUploadRequest(AuthenticatedUser user, Dataset dataset) { JsonObjectBuilder jab = Json.createObjectBuilder(); // The general rule should be to always pass the user id and dataset identifier to the DCM. @@ -39,6 +42,7 @@ public static JsonObject generateJsonForUploadRequest(AuthenticatedUser user, Da /** * transfer script from DCM */ + @Deprecated(forRemoval = true, since = "2024-07-07") public static ScriptRequestResponse getScriptFromRequest(HttpResponse uploadRequest) { int status = uploadRequest.getStatus(); JsonNode body = uploadRequest.getBody(); @@ -54,6 +58,7 @@ public static ScriptRequestResponse getScriptFromRequest(HttpResponse return scriptRequestResponse; } + @Deprecated(forRemoval = true, since = "2024-07-07") static UploadRequestResponse makeUploadRequest(HttpResponse uploadRequest) { int status = uploadRequest.getStatus(); String body = uploadRequest.getBody(); @@ -61,6 +66,7 @@ static UploadRequestResponse makeUploadRequest(HttpResponse uploadReques return new UploadRequestResponse(uploadRequest.getStatus(), body); } + @Deprecated(forRemoval = true, since = "2024-07-07") public static String getMessageFromException(DataCaptureModuleException ex) { if (ex == null) { return "DataCaptureModuleException was null!"; @@ -76,6 +82,7 @@ public static String getMessageFromException(DataCaptureModuleException ex) { return message + " was caused by " + cause.getMessage(); } + @Deprecated(forRemoval = true, since = "2024-07-07") public static String getScriptName(DatasetVersion datasetVersion) { return "upload-" + datasetVersion.getDataset().getIdentifier().replace("/", "_") + ".bash"; } diff --git a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java new file mode 100644 index 00000000000..78bf232e1a6 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java @@ -0,0 +1,70 @@ +package edu.harvard.iq.dataverse.dataset; + +import jakarta.json.Json; +import jakarta.json.JsonObjectBuilder; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.NamedQueries; +import jakarta.persistence.NamedQuery; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import java.io.Serializable; + +@NamedQueries({ + @NamedQuery(name = "DatasetType.findAll", + query = "SELECT d FROM DatasetType d"), + @NamedQuery(name = "DatasetType.findById", + query = "SELECT d FROM DatasetType d WHERE d.id=:id"), + @NamedQuery(name = "DatasetType.findByName", + query = "SELECT d FROM DatasetType d WHERE d.name=:name"), + @NamedQuery(name = "DatasetType.deleteById", + query = "DELETE FROM DatasetType d WHERE d.id=:id"),}) +@Entity +@Table(uniqueConstraints = { + @UniqueConstraint(columnNames = "name"),} +) + +public class DatasetType implements Serializable { + + public static final String DATASET_TYPE_DATASET = "dataset"; + public static final String DATASET_TYPE_SOFTWARE = "software"; + public static final String DATASET_TYPE_WORKFLOW = "workflow"; + public static final String DEFAULT_DATASET_TYPE = DATASET_TYPE_DATASET; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + // Any constraints? @Pattern regexp? + @Column(nullable = false) + private String name; + + public DatasetType() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public JsonObjectBuilder toJson() { + return Json.createObjectBuilder() + .add("id", getId()) + .add("name", getName()); + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetTypeServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetTypeServiceBean.java new file mode 100644 index 00000000000..832182f2a4a --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetTypeServiceBean.java @@ -0,0 +1,79 @@ +package edu.harvard.iq.dataverse.dataset; + +import edu.harvard.iq.dataverse.actionlogging.ActionLogRecord; +import edu.harvard.iq.dataverse.api.AbstractApiBean; +import jakarta.ejb.Stateless; +import jakarta.inject.Named; +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.PersistenceException; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Stateless +@Named +public class DatasetTypeServiceBean { + + private static final Logger logger = Logger.getLogger(DatasetTypeServiceBean.class.getName()); + + @PersistenceContext + EntityManager em; + + public List listAll() { + return em.createNamedQuery("DatasetType.findAll", DatasetType.class).getResultList(); + } + + public DatasetType getById(long id) { + try { + return em.createNamedQuery("DatasetType.findById", DatasetType.class) + .setParameter("id", id) + .getSingleResult(); + } catch (NoResultException noResultException) { + logger.log(Level.WARNING, "Couldn't find a dataset type with id " + id); + return null; + } + } + + public DatasetType getByName(String name) { + try { + return em.createNamedQuery("DatasetType.findByName", DatasetType.class) + .setParameter("name", name) + .getSingleResult(); + } catch (NoResultException noResultException) { + logger.log(Level.WARNING, "Couldn't find a dataset type named " + name); + return null; + } + } + + public DatasetType save(DatasetType datasetType) throws AbstractApiBean.WrappedResponse { + if (datasetType.getId() != null) { + throw new AbstractApiBean.WrappedResponse(new IllegalArgumentException("There shouldn't be an ID in the request body"), null); + } + try { + em.persist(datasetType); + em.flush(); + } catch (PersistenceException p) { + if (p.getMessage().contains("duplicate key")) { + throw new AbstractApiBean.WrappedResponse(new IllegalStateException("A dataset type with the same name is already present.", p), null); + } else { + throw p; + } + } + return datasetType; + } + + public int deleteById(long id) throws AbstractApiBean.WrappedResponse { + try { + return em.createNamedQuery("DatasetType.deleteById").setParameter("id", id).executeUpdate(); + } catch (PersistenceException p) { + if (p.getMessage().contains("violates foreign key constraint")) { + throw new AbstractApiBean.WrappedResponse(new IllegalStateException("Dataset type with id " + id + " is referenced and cannot be deleted.", p), null); + } else { + throw p; + } + } + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java index 03a0044a987..cacd409b365 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java @@ -1,17 +1,14 @@ package edu.harvard.iq.dataverse.dataset; -import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.Dataset; -import edu.harvard.iq.dataverse.DatasetField; -import edu.harvard.iq.dataverse.DatasetVersion; -import edu.harvard.iq.dataverse.FileMetadata; -import edu.harvard.iq.dataverse.TermsOfUseAndAccess; +import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.dataaccess.DataAccess; import static edu.harvard.iq.dataverse.api.ApiConstants.DS_VERSION_DRAFT; import static edu.harvard.iq.dataverse.dataaccess.DataAccess.getStorageIO; + +import edu.harvard.iq.dataverse.dataaccess.InputStreamIO; import edu.harvard.iq.dataverse.dataaccess.StorageIO; import edu.harvard.iq.dataverse.dataaccess.ImageThumbConverter; import edu.harvard.iq.dataverse.util.BundleUtil; @@ -20,26 +17,24 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.UnsupportedEncodingException; import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.*; import java.util.logging.Logger; import javax.imageio.ImageIO; + +import jakarta.enterprise.inject.spi.CDI; import org.apache.commons.io.IOUtils; -import static edu.harvard.iq.dataverse.dataaccess.DataAccess.getStorageIO; import edu.harvard.iq.dataverse.datasetutility.FileSizeChecker; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.license.License; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.StringUtil; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; -import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.EnumUtils; @@ -118,13 +113,19 @@ public static List getThumbnailCandidates(Dataset dataset, boo * * @param dataset * @param datasetVersion - * @return + * @param size of the requested thumbnail + * @return DatasetThumbnail object, or null if not available */ public static DatasetThumbnail getThumbnail(Dataset dataset, DatasetVersion datasetVersion, int size) { if (dataset == null) { return null; } + if (size == 0) { + // Size 0 will fail (and set the failure flag) and should never be sent + logger.warning("getThumbnail called with size 0"); + return null; + } StorageIO dataAccess = null; try{ @@ -218,7 +219,8 @@ public static boolean deleteDatasetLogo(Dataset dataset) { storageIO.deleteAuxObject(datasetLogoThumbnail + thumbExtension + ImageThumbConverter.DEFAULT_CARDIMAGE_SIZE); } catch (IOException ex) { - logger.info("Failed to delete dataset logo: " + ex.getMessage()); + logger.fine("Failed to delete dataset logo: " + ex.getMessage() + + " (this is most likely harmless; this method is often called without checking if the custom dataset logo was in fact present)"); return false; } return true; @@ -284,7 +286,7 @@ public static Dataset persistDatasetLogoToStorageAndCreateThumbnails(Dataset dat try { tmpFile = FileUtil.inputStreamToFile(inputStream); } catch (IOException ex) { - logger.severe(ex.getMessage()); + logger.severe("FileUtil.inputStreamToFile failed for tmpFile: " + ex.getMessage()); } StorageIO dataAccess = null; @@ -293,7 +295,7 @@ public static Dataset persistDatasetLogoToStorageAndCreateThumbnails(Dataset dat dataAccess = DataAccess.getStorageIO(dataset); } catch(IOException ioex){ - //TODO: Add a suitable waing message + //TODO: Add a suitable warning message logger.warning("Failed to save the file, storage id " + dataset.getStorageIdentifier() + " (" + ioex.getMessage() + ")"); } @@ -310,7 +312,7 @@ public static Dataset persistDatasetLogoToStorageAndCreateThumbnails(Dataset dat fullSizeImage = ImageIO.read(tmpFile); } catch (IOException ex) { IOUtils.closeQuietly(inputStream); - logger.severe(ex.getMessage()); + logger.severe("ImageIO.read failed for tmpFile: " + ex.getMessage()); return null; } if (fullSizeImage == null) { @@ -321,25 +323,14 @@ public static Dataset persistDatasetLogoToStorageAndCreateThumbnails(Dataset dat int width = fullSizeImage.getWidth(); int height = fullSizeImage.getHeight(); FileChannel src = null; - try { - src = new FileInputStream(tmpFile).getChannel(); - } catch (FileNotFoundException ex) { - IOUtils.closeQuietly(inputStream); - logger.severe(ex.getMessage()); - return null; - } FileChannel dest = null; - try { - dest = new FileOutputStream(tmpFile).getChannel(); - } catch (FileNotFoundException ex) { - IOUtils.closeQuietly(inputStream); - logger.severe(ex.getMessage()); - return null; - } - try { + try (FileInputStream fis = new FileInputStream(tmpFile); FileOutputStream fos = new FileOutputStream(tmpFile)) { + src = fis.getChannel(); + dest = fos.getChannel(); dest.transferFrom(src, 0, src.size()); } catch (IOException ex) { - logger.severe(ex.getMessage()); + IOUtils.closeQuietly(inputStream); + logger.severe("Error occurred during transfer using FileChannels: " + ex.getMessage()); return null; } File tmpFileForResize = null; @@ -347,7 +338,7 @@ public static Dataset persistDatasetLogoToStorageAndCreateThumbnails(Dataset dat //The stream was used around line 274 above, so this creates an empty file (OK since all it is used for is getting a path, but not reusing it here would make it easier to close it above.) tmpFileForResize = FileUtil.inputStreamToFile(inputStream); } catch (IOException ex) { - logger.severe(ex.getMessage()); + logger.severe("FileUtil.inputStreamToFile failed for tmpFileForResize: " + ex.getMessage()); return null; } finally { IOUtils.closeQuietly(inputStream); @@ -355,30 +346,44 @@ public static Dataset persistDatasetLogoToStorageAndCreateThumbnails(Dataset dat // We'll try to pre-generate the rescaled versions in both the // DEFAULT_DATASET_LOGO (currently 140) and DEFAULT_CARDIMAGE_SIZE (48) String thumbFileLocation = ImageThumbConverter.rescaleImage(fullSizeImage, width, height, ImageThumbConverter.DEFAULT_DATASETLOGO_SIZE, tmpFileForResize.toPath().toString()); - logger.fine("thumbFileLocation = " + thumbFileLocation); - logger.fine("tmpFileLocation=" + tmpFileForResize.toPath().toString()); - //now we must save the updated thumbnail - try { - dataAccess.savePathAsAux(Paths.get(thumbFileLocation), datasetLogoThumbnail+thumbExtension+ImageThumbConverter.DEFAULT_DATASETLOGO_SIZE); - } catch (IOException ex) { - logger.severe("Failed to move updated thumbnail file from " + tmpFile.getAbsolutePath() + " to its DataAccess location" + ": " + ex); + if (thumbFileLocation == null) { + logger.warning("Rescale Thumbnail Image to logo failed"); + dataset.setPreviewImageAvailable(false); + dataset.setUseGenericThumbnail(true); + } else { + logger.fine("thumbFileLocation = " + thumbFileLocation); + logger.fine("tmpFileLocation=" + tmpFileForResize.toPath().toString()); + //now we must save the updated thumbnail + try { + dataAccess.savePathAsAux(Paths.get(thumbFileLocation), datasetLogoThumbnail + thumbExtension + ImageThumbConverter.DEFAULT_DATASETLOGO_SIZE); + } catch (IOException ex) { + logger.severe("Failed to move updated thumbnail file from " + tmpFile.getAbsolutePath() + " to its DataAccess location" + ": " + ex); + } } thumbFileLocation = ImageThumbConverter.rescaleImage(fullSizeImage, width, height, ImageThumbConverter.DEFAULT_CARDIMAGE_SIZE, tmpFileForResize.toPath().toString()); - logger.fine("thumbFileLocation = " + thumbFileLocation); - logger.fine("tmpFileLocation=" + tmpFileForResize.toPath().toString()); - //now we must save the updated thumbnail - try { - dataAccess.savePathAsAux(Paths.get(thumbFileLocation), datasetLogoThumbnail+thumbExtension+ImageThumbConverter.DEFAULT_CARDIMAGE_SIZE); - } catch (IOException ex) { - logger.severe("Failed to move updated thumbnail file from " + tmpFile.getAbsolutePath() + " to its DataAccess location" + ": " + ex); + if (thumbFileLocation == null) { + logger.warning("Rescale Thumbnail Image to card failed"); + dataset.setPreviewImageAvailable(false); + dataset.setUseGenericThumbnail(true); + } else { + logger.fine("thumbFileLocation = " + thumbFileLocation); + logger.fine("tmpFileLocation=" + tmpFileForResize.toPath().toString()); + //now we must save the updated thumbnail + try { + dataAccess.savePathAsAux(Paths.get(thumbFileLocation), datasetLogoThumbnail + thumbExtension + ImageThumbConverter.DEFAULT_CARDIMAGE_SIZE); + } catch (IOException ex) { + logger.severe("Failed to move updated thumbnail file from " + tmpFile.getAbsolutePath() + " to its DataAccess location" + ": " + ex); + } } //This deletes the tempfiles created for rescaling and encoding boolean tmpFileWasDeleted = tmpFile.delete(); boolean originalTempFileWasDeleted = tmpFileForResize.delete(); try { - Files.delete(Paths.get(thumbFileLocation)); + if (thumbFileLocation != null) { + Files.delete(Paths.get(thumbFileLocation)); + } } catch (IOException ioex) { logger.fine("Failed to delete temporary thumbnail file"); } @@ -398,14 +403,8 @@ public static InputStream getThumbnailAsInputStream(Dataset dataset, int size) { String base64Image = datasetThumbnail.getBase64image(); String leadingStringToRemove = FileUtil.DATA_URI_SCHEME; String encodedImg = base64Image.substring(leadingStringToRemove.length()); - byte[] decodedImg = null; - try { - decodedImg = Base64.getDecoder().decode(encodedImg.getBytes("UTF-8")); - logger.fine("returning this many bytes for " + "dataset id: " + dataset.getId() + ", persistentId: " + dataset.getIdentifier() + " :" + decodedImg.length); - } catch (UnsupportedEncodingException ex) { - logger.info("dataset thumbnail could not be decoded for dataset id " + dataset.getId() + ": " + ex); - return null; - } + byte[] decodedImg = Base64.getDecoder().decode(encodedImg.getBytes(StandardCharsets.UTF_8)); + logger.fine("returning this many bytes for " + "dataset id: " + dataset.getId() + ", persistentId: " + dataset.getIdentifier() + " :" + decodedImg.length); ByteArrayInputStream nonDefaultDatasetThumbnail = new ByteArrayInputStream(decodedImg); logger.fine("For dataset id " + dataset.getId() + " a thumbnail was found and is being returned."); return nonDefaultDatasetThumbnail; @@ -463,8 +462,19 @@ public static InputStream getLogoAsInputStream(Dataset dataset) { } try { - in = ImageThumbConverter.getImageThumbnailAsInputStream(thumbnailFile.getStorageIO(), - ImageThumbConverter.DEFAULT_DATASETLOGO_SIZE).getInputStream(); + + boolean origImageFailed = thumbnailFile.isPreviewImageFail(); + InputStreamIO isIO = ImageThumbConverter.getImageThumbnailAsInputStream(thumbnailFile.getStorageIO(), + ImageThumbConverter.DEFAULT_DATASETLOGO_SIZE); + if (!origImageFailed && thumbnailFile.isPreviewImageFail()) { + // We found an older 0 length thumbnail. Newer image uploads will not have this issue. + // Once cleaned up, this thumbnail will no longer have this issue + // ImageThumbConverter fixed the DataFile + // Now we need to update dataset since this is a bad logo + DatasetServiceBean datasetService = CDI.current().select(DatasetServiceBean.class).get(); + datasetService.clearDatasetLevelThumbnail(dataset); + } + in = isIO != null ? isIO.getInputStream() : null; } catch (IOException ioex) { logger.warning("getLogo(): Failed to get logo from DataFile for " + dataset.getStorageIdentifier() + " (" + ioex.getMessage() + ")"); @@ -605,7 +615,7 @@ public static boolean validateDatasetMetadataExternally(Dataset ds, String execu try { File tempFile = File.createTempFile("datasetMetadataCheck", ".tmp"); - FileUtils.writeStringToFile(tempFile, jsonMetadata); + FileUtils.writeStringToFile(tempFile, jsonMetadata, StandardCharsets.UTF_8); // run the external executable: String[] params = { executable, tempFile.getAbsolutePath() }; @@ -631,6 +641,15 @@ public static License getLicense(DatasetVersion dsv) { } public static String getLicenseName(DatasetVersion dsv) { + + DatasetVersionServiceBean datasetVersionService = CDI.current().select(DatasetVersionServiceBean.class).get(); + /* + Special case where there are default custom terms indicating that no actual choice has been made... + */ + if (datasetVersionService.isVersionDefaultCustomTerms(dsv)) { + return BundleUtil.getStringFromBundle("license.none.chosen"); + } + License license = DatasetUtil.getLicense(dsv); return getLocalizedLicenseName(license); } @@ -661,7 +680,16 @@ public static String getLicenseIcon(DatasetVersion dsv) { } public static String getLicenseDescription(DatasetVersion dsv) { + + DatasetVersionServiceBean datasetVersionService = CDI.current().select(DatasetVersionServiceBean.class).get(); + /* + Special case where there are default custom terms indicating that no actual choice has been made... + */ + if (datasetVersionService.isVersionDefaultCustomTerms(dsv)) { + return BundleUtil.getStringFromBundle("license.none.chosen.description"); + } License license = DatasetUtil.getLicense(dsv); + return license != null ? getLocalizedLicenseDetails(license,"DESCRIPTION") : BundleUtil.getStringFromBundle("license.custom.description"); } diff --git a/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java b/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java index 0143fced87c..a470f08f736 100644 --- a/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java +++ b/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java @@ -2139,9 +2139,9 @@ public Response addFiles(String jsonData, Dataset dataset, User authUser) { logger.log(Level.WARNING, "Dataset not locked for EditInProgress "); } else { datasetService.removeDatasetLocks(dataset, DatasetLock.Reason.EditInProgress); - logger.log(Level.INFO, "Removed EditInProgress lock "); + logger.log(Level.FINE, "Removed EditInProgress lock"); } - + try { Command cmd = new UpdateDatasetVersionCommand(dataset, dvRequest, clone); ((UpdateDatasetVersionCommand) cmd).setValidateLenient(true); @@ -2167,8 +2167,8 @@ public Response addFiles(String jsonData, Dataset dataset, User authUser) { } JsonObjectBuilder result = Json.createObjectBuilder() - .add("Total number of files", totalNumberofFiles) - .add("Number of files successfully added", successNumberofFiles); + .add(ApiConstants.API_ADD_FILES_COUNT_PROCESSED, totalNumberofFiles) + .add(ApiConstants.API_ADD_FILES_COUNT_SUCCESSFUL, successNumberofFiles); return Response.ok().entity(Json.createObjectBuilder() @@ -2306,7 +2306,7 @@ public Response replaceFiles(String jsonData, Dataset ds, User authUser) { logger.warning("Dataset not locked for EditInProgress "); } else { datasetService.removeDatasetLocks(dataset, DatasetLock.Reason.EditInProgress); - logger.info("Removed EditInProgress lock "); + logger.fine("Removed EditInProgress lock "); } try { diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/CommandContext.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/CommandContext.java index f74c1222bb0..282cbb88988 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/CommandContext.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/CommandContext.java @@ -1,9 +1,7 @@ package edu.harvard.iq.dataverse.engine.command; -import edu.harvard.iq.dataverse.DOIDataCiteServiceBean; -import edu.harvard.iq.dataverse.DOIEZIdServiceBean; -import edu.harvard.iq.dataverse.HandlenetServiceBean; import edu.harvard.iq.dataverse.DataFileServiceBean; +import edu.harvard.iq.dataverse.DatasetFieldServiceBean; import edu.harvard.iq.dataverse.DatasetLinkingServiceBean; import edu.harvard.iq.dataverse.DatasetServiceBean; import edu.harvard.iq.dataverse.DatasetVersionServiceBean; @@ -18,6 +16,7 @@ import edu.harvard.iq.dataverse.FileDownloadServiceBean; import edu.harvard.iq.dataverse.GuestbookResponseServiceBean; import edu.harvard.iq.dataverse.GuestbookServiceBean; +import edu.harvard.iq.dataverse.MetadataBlockServiceBean; import edu.harvard.iq.dataverse.search.IndexServiceBean; import edu.harvard.iq.dataverse.PermissionServiceBean; import edu.harvard.iq.dataverse.RoleAssigneeServiceBean; @@ -30,10 +29,10 @@ import edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroupServiceBean; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean; import edu.harvard.iq.dataverse.datacapturemodule.DataCaptureModuleServiceBean; +import edu.harvard.iq.dataverse.dataset.DatasetTypeServiceBean; import edu.harvard.iq.dataverse.engine.DataverseEngine; 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.pidproviders.PidProviderFactoryBean; import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; import edu.harvard.iq.dataverse.search.IndexBatchServiceBean; import edu.harvard.iq.dataverse.search.SolrIndexServiceBean; @@ -100,15 +99,7 @@ public interface CommandContext { public DataverseFieldTypeInputLevelServiceBean fieldTypeInputLevels(); - public DOIEZIdServiceBean doiEZId(); - - public DOIDataCiteServiceBean doiDataCite(); - - public FakePidProviderServiceBean fakePidProvider(); - - public HandlenetServiceBean handleNet(); - - public PermaLinkPidProviderServiceBean permaLinkProvider(); + public PidProviderFactoryBean pidProviderFactory(); public GuestbookServiceBean guestbooks(); @@ -145,7 +136,11 @@ public interface CommandContext { public ConfirmEmailServiceBean confirmEmail(); public ActionLogServiceBean actionLog(); - + + public MetadataBlockServiceBean metadataBlocks(); + + public DatasetTypeServiceBean datasetTypes(); + public void beginCommandSequence(); public boolean completeCommandSequence(Command command); @@ -155,4 +150,6 @@ public interface CommandContext { public Stack getCommandsCalled(); public void addCommand(Command command); + + public DatasetFieldServiceBean dsField(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/exception/RateLimitCommandException.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/exception/RateLimitCommandException.java new file mode 100644 index 00000000000..99a665b31ac --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/exception/RateLimitCommandException.java @@ -0,0 +1,16 @@ +package edu.harvard.iq.dataverse.engine.command.exception; + +import edu.harvard.iq.dataverse.engine.command.Command; + +/** + * An exception raised when a command cannot be executed, due to the + * issuing user being rate limited. + * + * @author + */ +public class RateLimitCommandException extends CommandException { + + public RateLimitCommandException(String message, Command aCommand) { + super(message, aCommand); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractCreateDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractCreateDatasetCommand.java index 303d8e1c25f..db9dc142506 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractCreateDatasetCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractCreateDatasetCommand.java @@ -1,24 +1,20 @@ package edu.harvard.iq.dataverse.engine.command.impl; -import edu.harvard.iq.dataverse.*; +import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; -import edu.harvard.iq.dataverse.batch.util.LoggingUtil; import edu.harvard.iq.dataverse.dataaccess.DataAccess; -import edu.harvard.iq.dataverse.datacapturemodule.DataCaptureModuleUtil; -import edu.harvard.iq.dataverse.datacapturemodule.ScriptRequestResponse; +import edu.harvard.iq.dataverse.dataset.DatasetType; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; -import edu.harvard.iq.dataverse.engine.command.exception.CommandExecutionException; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; import static edu.harvard.iq.dataverse.util.StringUtil.isEmpty; -import java.io.IOException; import java.util.Objects; -import java.util.logging.Level; import java.util.logging.Logger; -import org.apache.solr.client.solrj.SolrServerException; /**; * An abstract base class for commands that creates {@link Dataset}s. @@ -81,9 +77,10 @@ public Dataset execute(CommandContext ctxt) throws CommandException { additionalParameterTests(ctxt); Dataset theDataset = getDataset(); - GlobalIdServiceBean idServiceBean = GlobalIdServiceBean.getBean(ctxt); + PidProvider pidProvider = ctxt.dvObjects().getEffectivePidGenerator(theDataset); + if ( isEmpty(theDataset.getIdentifier()) ) { - theDataset.setIdentifier(idServiceBean.generateDatasetIdentifier(theDataset)); + pidProvider.generatePid(theDataset); } DatasetVersion dsv = getVersionToPersist(theDataset); @@ -94,6 +91,8 @@ public Dataset execute(CommandContext ctxt) throws CommandException { if(!harvested) { checkSystemMetadataKeyIfNeeded(dsv, null); } + + registerExternalVocabValuesIfAny(ctxt, dsv); theDataset.setCreator((AuthenticatedUser) getRequest().getUser()); @@ -105,24 +104,35 @@ public Dataset execute(CommandContext ctxt) throws CommandException { dataFile.setCreateDate(theDataset.getCreateDate()); } - String nonNullDefaultIfKeyNotFound = ""; if (theDataset.getProtocol()==null) { - theDataset.setProtocol(ctxt.settings().getValueForKey(SettingsServiceBean.Key.Protocol, nonNullDefaultIfKeyNotFound)); + theDataset.setProtocol(pidProvider.getProtocol()); } if (theDataset.getAuthority()==null) { - theDataset.setAuthority(ctxt.settings().getValueForKey(SettingsServiceBean.Key.Authority, nonNullDefaultIfKeyNotFound)); + theDataset.setAuthority(pidProvider.getAuthority()); } if (theDataset.getStorageIdentifier() == null) { String driverId = theDataset.getEffectiveStorageDriverId(); theDataset.setStorageIdentifier(driverId + DataAccess.SEPARATOR + theDataset.getAuthorityForFileStorage() + "/" + theDataset.getIdentifierForFileStorage()); } if (theDataset.getIdentifier()==null) { - theDataset.setIdentifier(idServiceBean.generateDatasetIdentifier(theDataset)); + pidProvider.generatePid(theDataset); + } + + DatasetType defaultDatasetType = ctxt.datasetTypes().getByName(DatasetType.DEFAULT_DATASET_TYPE); + DatasetType existingDatasetType = theDataset.getDatasetType(); + logger.fine("existing dataset type: " + existingDatasetType); + if (existingDatasetType != null) { + // A dataset type can be specified via API, for example. + theDataset.setDatasetType(existingDatasetType); + } else { + theDataset.setDatasetType(defaultDatasetType); } // Attempt the registration if importing dataset through the API, or the app (but not harvest) handlePid(theDataset, ctxt); - + + + ctxt.em().persist(theDataset); postPersist(theDataset, ctxt); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractDatasetCommand.java index 6061461306d..bd38245d334 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractDatasetCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractDatasetCommand.java @@ -1,11 +1,15 @@ package edu.harvard.iq.dataverse.engine.command.impl; +import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetField; +import edu.harvard.iq.dataverse.DatasetFieldServiceBean; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.DatasetVersionDifference; import edu.harvard.iq.dataverse.DatasetVersionUser; import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.MetadataBlock; +import edu.harvard.iq.dataverse.TermsOfUseAndAccess; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.engine.command.AbstractCommand; import edu.harvard.iq.dataverse.engine.command.CommandContext; @@ -13,19 +17,21 @@ import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.CommandExecutionException; import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; +import edu.harvard.iq.dataverse.pidproviders.doi.fake.FakeDOIProvider; import edu.harvard.iq.dataverse.util.BundleUtil; import java.sql.Timestamp; +import java.util.Arrays; import java.util.Date; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import static java.util.stream.Collectors.joining; +import jakarta.ejb.EJB; import jakarta.validation.ConstraintViolation; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; -import edu.harvard.iq.dataverse.MetadataBlock; -import edu.harvard.iq.dataverse.TermsOfUseAndAccess; import edu.harvard.iq.dataverse.settings.JvmSettings; /** @@ -152,32 +158,31 @@ protected void validateOrDie(DatasetVersion dsv, Boolean lenient) throws Command */ protected void registerExternalIdentifier(Dataset theDataset, CommandContext ctxt, boolean retry) throws CommandException { if (!theDataset.isIdentifierRegistered()) { - GlobalIdServiceBean globalIdServiceBean = GlobalIdServiceBean.getBean(theDataset.getProtocol(), ctxt); - if ( globalIdServiceBean != null ) { + PidProvider pidProvider = PidUtil.getPidProvider(theDataset.getGlobalId().getProviderId()); + if ( pidProvider != null ) { try { - if (globalIdServiceBean.alreadyRegistered(theDataset)) { + if (pidProvider.alreadyRegistered(theDataset)) { int attempts = 0; if(retry) { do { - theDataset.setIdentifier(globalIdServiceBean.generateDatasetIdentifier(theDataset)); + pidProvider.generatePid(theDataset); logger.log(Level.INFO, "Attempting to register external identifier for dataset {0} (trying: {1}).", new Object[]{theDataset.getId(), theDataset.getIdentifier()}); attempts++; - } while (globalIdServiceBean.alreadyRegistered(theDataset) && attempts <= FOOLPROOF_RETRIAL_ATTEMPTS_LIMIT); + } while (pidProvider.alreadyRegistered(theDataset) && attempts <= FOOLPROOF_RETRIAL_ATTEMPTS_LIMIT); } if(!retry) { - logger.warning("Reserving PID for: " + getDataset().getId() + " during publication failed."); - throw new IllegalCommandException(BundleUtil.getStringFromBundle("publishDatasetCommand.pidNotReserved"), this); + logger.warning("Reserving PID for: " + getDataset().getId() + " failed."); + throw new CommandExecutionException(BundleUtil.getStringFromBundle("abstractDatasetCommand.pidNotReserved", Arrays.asList(theDataset.getIdentifier())), this); } if(attempts > FOOLPROOF_RETRIAL_ATTEMPTS_LIMIT) { //Didn't work - we existed the loop with too many tries - throw new CommandExecutionException("This dataset may not be published because its identifier is already in use by another dataset; " - + "gave up after " + attempts + " attempts. Current (last requested) identifier: " + theDataset.getIdentifier(), this); + throw new CommandExecutionException(BundleUtil.getStringFromBundle("abstractDatasetCommand.pidReservationRetryExceeded", Arrays.asList(Integer.toString(attempts), theDataset.getIdentifier())), this); } } // Invariant: Dataset identifier does not exist in the remote registry try { - globalIdServiceBean.createIdentifier(theDataset); + pidProvider.createIdentifier(theDataset); theDataset.setGlobalIdCreateTime(getTimestamp()); theDataset.setIdentifierRegistered(true); } catch (Throwable ex) { @@ -185,7 +190,10 @@ protected void registerExternalIdentifier(Dataset theDataset, CommandContext ctx } } catch (Throwable e) { - throw new CommandException(BundleUtil.getStringFromBundle("dataset.publish.error", globalIdServiceBean.getProviderInformation()), this); + if (e instanceof CommandException) { + throw (CommandException) e; + } + throw new CommandException(BundleUtil.getStringFromBundle("dataset.publish.error", pidProvider.getProviderInformation()), this); } } else { throw new IllegalCommandException("This dataset may not be published because its id registry service is not supported.", this); @@ -214,6 +222,73 @@ protected Timestamp getTimestamp() { return timestamp; } + protected void registerFilePidsIfNeeded(Dataset theDataset, CommandContext ctxt, boolean b) throws CommandException { + // Register file PIDs if needed + PidProvider pidGenerator = ctxt.dvObjects().getEffectivePidGenerator(getDataset()); + boolean shouldRegister = !pidGenerator.registerWhenPublished() && + ctxt.systemConfig().isFilePIDsEnabledForCollection(getDataset().getOwner()) && + pidGenerator.canCreatePidsLike(getDataset().getGlobalId()); + if (shouldRegister) { + for (DataFile dataFile : theDataset.getFiles()) { + logger.fine(dataFile.getId() + " is registered?: " + dataFile.isIdentifierRegistered()); + if (!dataFile.isIdentifierRegistered()) { + // pre-register a persistent id + registerFileExternalIdentifier(dataFile, pidGenerator, ctxt, true); + } + } + } + } + + private void registerFileExternalIdentifier(DataFile dataFile, PidProvider pidProvider, CommandContext ctxt, boolean retry) throws CommandException { + + if (!dataFile.isIdentifierRegistered()) { + + if (pidProvider instanceof FakeDOIProvider) { + retry = false; // No reason to allow a retry with the FakeProvider (even if it allows + // pre-registration someday), so set false for efficiency + } + try { + if (pidProvider.alreadyRegistered(dataFile)) { + int attempts = 0; + if (retry) { + do { + pidProvider.generatePid(dataFile); + logger.log(Level.INFO, "Attempting to register external identifier for datafile {0} (trying: {1}).", + new Object[] { dataFile.getId(), dataFile.getIdentifier() }); + attempts++; + } while (pidProvider.alreadyRegistered(dataFile) && attempts <= FOOLPROOF_RETRIAL_ATTEMPTS_LIMIT); + } + if (!retry) { + logger.warning("Reserving File PID for: " + getDataset().getId() + ", fileId: " + dataFile.getId() + ", during publication failed."); + throw new CommandExecutionException(BundleUtil.getStringFromBundle("abstractDatasetCommand.filePidNotReserved", Arrays.asList(getDataset().getIdentifier())), this); + } + if (attempts > FOOLPROOF_RETRIAL_ATTEMPTS_LIMIT) { + // Didn't work - we existed the loop with too many tries + throw new CommandExecutionException("This dataset may not be published because its identifier is already in use by another dataset; " + + "gave up after " + attempts + " attempts. Current (last requested) identifier: " + dataFile.getIdentifier(), this); + } + } + // Invariant: DataFile identifier does not exist in the remote registry + try { + pidProvider.createIdentifier(dataFile); + dataFile.setGlobalIdCreateTime(getTimestamp()); + dataFile.setIdentifierRegistered(true); + } catch (Throwable ex) { + logger.info("Call to globalIdServiceBean.createIdentifier failed: " + ex); + } + + } catch (Throwable e) { + if (e instanceof CommandException) { + throw (CommandException) e; + } + throw new CommandException(BundleUtil.getStringFromBundle("file.register.error", pidProvider.getProviderInformation()), this); + } + } else { + throw new IllegalCommandException("This datafile may not have a PID because its id registry service is not supported.", this); + } + + } + protected void checkSystemMetadataKeyIfNeeded(DatasetVersion newVersion, DatasetVersion persistedVersion) throws IllegalCommandException { Set changedMDBs = DatasetVersionDifference.getBlocksWithChanges(newVersion, persistedVersion); for (MetadataBlock mdb : changedMDBs) { @@ -230,4 +305,13 @@ protected void checkSystemMetadataKeyIfNeeded(DatasetVersion newVersion, Dataset } } } + + protected void registerExternalVocabValuesIfAny(CommandContext ctxt, DatasetVersion newVersion) { + for (DatasetField df : newVersion.getFlatDatasetFields()) { + logger.fine("Found id: " + df.getDatasetFieldType().getId()); + if (ctxt.dsField().getCVocConf(true).containsKey(df.getDatasetFieldType().getId())) { + ctxt.dsField().registerExternalVocabValues(df); + } + } + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractGetPublishedFileMetadataCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractGetPublishedFileMetadataCommand.java new file mode 100644 index 00000000000..4fef2c27efb --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractGetPublishedFileMetadataCommand.java @@ -0,0 +1,39 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetVersion; +import edu.harvard.iq.dataverse.FileMetadata; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.engine.command.AbstractCommand; +import edu.harvard.iq.dataverse.engine.command.CommandContext; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; + +@RequiredPermissions({}) +abstract class AbstractGetPublishedFileMetadataCommand extends AbstractCommand { + protected final DataFile dataFile; + protected final boolean includeDeaccessioned; + + public AbstractGetPublishedFileMetadataCommand(DataverseRequest request, DataFile dataFile, boolean includeDeaccessioned) { + super(request, dataFile); + this.dataFile = dataFile; + this.includeDeaccessioned = includeDeaccessioned; + } + + protected FileMetadata getLatestPublishedFileMetadata(CommandContext ctxt) { + return dataFile.getFileMetadatas().stream().filter(fileMetadata -> { + DatasetVersion.VersionState versionState = fileMetadata.getDatasetVersion().getVersionState(); + return (!versionState.equals(DatasetVersion.VersionState.DRAFT) + && isDatasetVersionAccessible(fileMetadata.getDatasetVersion(), dataFile.getOwner(), ctxt)); + }).reduce(null, DataFile::getTheNewerFileMetadata); + } + + protected boolean isDatasetVersionAccessible(DatasetVersion datasetVersion, Dataset ownerDataset, CommandContext ctxt) { + return datasetVersion.isReleased() || isDatasetVersionDeaccessionedAndAccessible(datasetVersion, ownerDataset, ctxt); + } + + private boolean isDatasetVersionDeaccessionedAndAccessible(DatasetVersion datasetVersion, Dataset ownerDataset, CommandContext ctxt) { + return includeDeaccessioned && datasetVersion.isDeaccessioned() && ctxt.permissions().requestOn(getRequest(), ownerDataset).has(Permission.EditDataset); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractSubmitToArchiveCommand.java index b988fd05f03..29c27d0396d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractSubmitToArchiveCommand.java @@ -1,6 +1,5 @@ package edu.harvard.iq.dataverse.engine.command.impl; -import edu.harvard.iq.dataverse.DOIDataCiteRegisterService; import edu.harvard.iq.dataverse.DataCitation; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetVersion; @@ -14,6 +13,7 @@ import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import edu.harvard.iq.dataverse.pidproviders.doi.datacite.DOIDataCiteRegisterService; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.bagit.BagGenerator; import edu.harvard.iq.dataverse.util.bagit.OREMap; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AssignRoleCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AssignRoleCommand.java index 5577d541012..121af765737 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AssignRoleCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AssignRoleCommand.java @@ -3,7 +3,6 @@ */ package edu.harvard.iq.dataverse.engine.command.impl; -import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.authorization.DataverseRole; @@ -18,7 +17,10 @@ import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; +import edu.harvard.iq.dataverse.util.BundleUtil; + import java.util.Collections; +import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -67,17 +69,38 @@ public RoleAssignment execute(CommandContext ctxt) throws CommandException { throw new IllegalCommandException("User " + user.getUserIdentifier() + " is deactivated and cannot be given a role.", this); } } + if(isExistingRole(ctxt)){ + throw new IllegalCommandException(BundleUtil.getStringFromBundle("datasets.api.grant.role.assignee.has.role.error"), this); + } // TODO make sure the role is defined on the dataverse. RoleAssignment roleAssignment = new RoleAssignment(role, grantee, defPoint, privateUrlToken, anonymizedAccess); return ctxt.roles().save(roleAssignment); } + private boolean isExistingRole(CommandContext ctxt) { + return ctxt.roles() + .directRoleAssignments(grantee, defPoint) + .stream() + .map(RoleAssignment::getRole) + .anyMatch(it -> it.equals(role)); + } + @Override public Map> getRequiredPermissions() { // for data file check permission on owning dataset - return Collections.singletonMap("", - defPoint instanceof Dataverse ? Collections.singleton(Permission.ManageDataversePermissions) - : defPoint instanceof Dataset ? Collections.singleton(Permission.ManageDatasetPermissions) : Collections.singleton(Permission.ManageFilePermissions)); + Set requiredPermissions = new HashSet(); + + if (defPoint instanceof Dataverse) { + requiredPermissions.add(Permission.ManageDataversePermissions); + } else if (defPoint instanceof Dataset) { + requiredPermissions.add(Permission.ManageDatasetPermissions); + } else { + requiredPermissions.add(Permission.ManageFilePermissions); + } + + requiredPermissions.addAll(role.permissions()); + + return Collections.singletonMap("", requiredPermissions); } @Override diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForCollectionPageCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForCollectionPageCommand.java new file mode 100644 index 00000000000..b23e6034c9a --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForCollectionPageCommand.java @@ -0,0 +1,16 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.engine.command.AbstractVoidCommand; +import edu.harvard.iq.dataverse.engine.command.CommandContext; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; + +public class CheckRateLimitForCollectionPageCommand extends AbstractVoidCommand { + public CheckRateLimitForCollectionPageCommand(DataverseRequest aRequest, DvObject dvObject) { + super(aRequest, dvObject); + } + + @Override + protected void executeImpl(CommandContext ctxt) throws CommandException { } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForDatasetPageCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForDatasetPageCommand.java new file mode 100644 index 00000000000..da8c1e4d8e3 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForDatasetPageCommand.java @@ -0,0 +1,17 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.engine.command.AbstractVoidCommand; +import edu.harvard.iq.dataverse.engine.command.CommandContext; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; + +public class CheckRateLimitForDatasetPageCommand extends AbstractVoidCommand { + + public CheckRateLimitForDatasetPageCommand(DataverseRequest aRequest, DvObject dvObject) { + super(aRequest, dvObject); + } + + @Override + protected void executeImpl(CommandContext ctxt) throws CommandException { } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDatasetVersionCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDatasetVersionCommand.java index bcaece55fed..6539ac27ea2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDatasetVersionCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDatasetVersionCommand.java @@ -59,7 +59,8 @@ public DatasetVersion execute(CommandContext ctxt) throws CommandException { //Will throw an IllegalCommandException if a system metadatablock is changed and the appropriate key is not supplied. checkSystemMetadataKeyIfNeeded(newVersion, latest); - + registerExternalVocabValuesIfAny(ctxt, newVersion); + List newVersionMetadatum = new ArrayList<>(latest.getFileMetadatas().size()); for ( FileMetadata fmd : latest.getFileMetadatas() ) { FileMetadata fmdCopy = fmd.createCopy(); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommand.java index 3efefe90681..489b36e7cef 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommand.java @@ -1,17 +1,11 @@ package edu.harvard.iq.dataverse.engine.command.impl; -import edu.harvard.iq.dataverse.DatasetFieldType; -import edu.harvard.iq.dataverse.Dataverse; -import edu.harvard.iq.dataverse.DataverseFieldTypeInputLevel; +import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.authorization.DataverseRole; -import edu.harvard.iq.dataverse.RoleAssignment; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.groups.Group; -import edu.harvard.iq.dataverse.authorization.groups.GroupProvider; -import edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroupProvider; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; -import edu.harvard.iq.dataverse.batch.util.LoggingUtil; import edu.harvard.iq.dataverse.engine.command.AbstractCommand; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; @@ -19,15 +13,12 @@ import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; -import java.io.IOException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; -import java.util.logging.Logger; -import org.apache.solr.client.solrj.SolrServerException; /** * TODO make override the date and user more active, so prevent code errors. @@ -38,14 +29,23 @@ @RequiredPermissions(Permission.AddDataverse) public class CreateDataverseCommand extends AbstractCommand { - private static final Logger logger = Logger.getLogger(CreateDataverseCommand.class.getName()); - private final Dataverse created; private final List inputLevelList; private final List facetList; + private final List metadataBlocks; + + public CreateDataverseCommand(Dataverse created, + DataverseRequest aRequest, + List facetList, + List inputLevelList) { + this(created, aRequest, facetList, inputLevelList, null); + } - public CreateDataverseCommand(Dataverse created, DataverseRequest aRequest, List facetList, - List inputLevelList) { + public CreateDataverseCommand(Dataverse created, + DataverseRequest aRequest, + List facetList, + List inputLevelList, + List metadataBlocks) { super(aRequest, created.getOwner()); this.created = created; if (facetList != null) { @@ -58,6 +58,11 @@ public CreateDataverseCommand(Dataverse created, DataverseRequest aRequest, List } else { this.inputLevelList = null; } + if (metadataBlocks != null) { + this.metadataBlocks = new ArrayList<>(metadataBlocks); + } else { + this.metadataBlocks = null; + } } @Override @@ -70,6 +75,11 @@ public Dataverse execute(CommandContext ctxt) throws CommandException { } } + if (metadataBlocks != null && !metadataBlocks.isEmpty()) { + created.setMetadataBlockRoot(true); + created.setMetadataBlocks(metadataBlocks); + } + if (created.getCreateDate() == null) { created.setCreateDate(new Timestamp(new Date().getTime())); } @@ -97,8 +107,8 @@ public Dataverse execute(CommandContext ctxt) throws CommandException { if (ctxt.dataverses().findByAlias(created.getAlias()) != null) { throw new IllegalCommandException("A dataverse with alias " + created.getAlias() + " already exists", this); } - - if(created.getFilePIDsEnabled()!=null && !ctxt.settings().isTrueForKey(SettingsServiceBean.Key.AllowEnablingFilePIDsPerCollection, false)) { + + if (created.getFilePIDsEnabled() != null && !ctxt.settings().isTrueForKey(SettingsServiceBean.Key.AllowEnablingFilePIDsPerCollection, false)) { throw new IllegalCommandException("File PIDs cannot be enabled per collection", this); } @@ -109,7 +119,7 @@ public Dataverse execute(CommandContext ctxt) throws CommandException { DataverseRole adminRole = ctxt.roles().findBuiltinRoleByAlias(DataverseRole.ADMIN); String privateUrlToken = null; - ctxt.roles().save(new RoleAssignment(adminRole, getRequest().getUser(), managedDv, privateUrlToken),false); + ctxt.roles().save(new RoleAssignment(adminRole, getRequest().getUser(), managedDv, privateUrlToken), false); // Add additional role assignments if inheritance is set boolean inheritAllRoles = false; String rolesString = ctxt.settings().getValueForKey(SettingsServiceBean.Key.InheritParentRoleAssignments, ""); @@ -129,18 +139,18 @@ public Dataverse execute(CommandContext ctxt) throws CommandException { // above... if ((inheritAllRoles || rolesToInherit.contains(role.getRole().getAlias())) && !(role.getAssigneeIdentifier().equals(getRequest().getUser().getIdentifier()) - && role.getRole().equals(adminRole))) { + && role.getRole().equals(adminRole))) { String identifier = role.getAssigneeIdentifier(); if (identifier.startsWith(AuthenticatedUser.IDENTIFIER_PREFIX)) { identifier = identifier.substring(AuthenticatedUser.IDENTIFIER_PREFIX.length()); ctxt.roles().save(new RoleAssignment(role.getRole(), - ctxt.authentication().getAuthenticatedUser(identifier), managedDv, privateUrlToken),false); + ctxt.authentication().getAuthenticatedUser(identifier), managedDv, privateUrlToken), false); } else if (identifier.startsWith(Group.IDENTIFIER_PREFIX)) { identifier = identifier.substring(Group.IDENTIFIER_PREFIX.length()); Group roleGroup = ctxt.groups().getGroup(identifier); if (roleGroup != null) { ctxt.roles().save(new RoleAssignment(role.getRole(), - roleGroup, managedDv, privateUrlToken),false); + roleGroup, managedDv, privateUrlToken), false); } } } @@ -150,12 +160,14 @@ public Dataverse execute(CommandContext ctxt) throws CommandException { } managedDv.setPermissionModificationTime(new Timestamp(new Date().getTime())); - // TODO: save is called here and above; we likely don't need both - managedDv = ctxt.dataverses().save(managedDv); - // ctxt.index().indexDataverse(managedDv); if (facetList != null) { ctxt.facets().deleteFacetsFor(managedDv); + + if (!facetList.isEmpty()) { + managedDv.setFacetRoot(true); + } + int i = 0; for (DatasetFieldType df : facetList) { ctxt.facets().create(i++, df, managedDv); @@ -163,17 +175,23 @@ public Dataverse execute(CommandContext ctxt) throws CommandException { } if (inputLevelList != null) { + if (!inputLevelList.isEmpty()) { + managedDv.addInputLevelsMetadataBlocksIfNotPresent(inputLevelList); + } ctxt.fieldTypeInputLevels().deleteFacetsFor(managedDv); - for (DataverseFieldTypeInputLevel obj : inputLevelList) { - obj.setDataverse(managedDv); - ctxt.fieldTypeInputLevels().create(obj); + for (DataverseFieldTypeInputLevel inputLevel : inputLevelList) { + inputLevel.setDataverse(managedDv); + ctxt.fieldTypeInputLevels().create(inputLevel); } } + + // TODO: save is called here and above; we likely don't need both + managedDv = ctxt.dataverses().save(managedDv); return managedDv; } - + @Override - public boolean onSuccess(CommandContext ctxt, Object r) { + public boolean onSuccess(CommandContext ctxt, Object r) { return ctxt.dataverses().index((Dataverse) r); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateNewDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateNewDatasetCommand.java index c9ebe735e31..c22a2cdb4a2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateNewDatasetCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateNewDatasetCommand.java @@ -3,6 +3,7 @@ import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.GlobalId; import edu.harvard.iq.dataverse.RoleAssignment; import edu.harvard.iq.dataverse.Template; import edu.harvard.iq.dataverse.UserNotification; @@ -12,12 +13,13 @@ import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import static edu.harvard.iq.dataverse.util.StringUtil.nonEmpty; import java.util.logging.Logger; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import java.util.List; import java.sql.Timestamp; @@ -71,13 +73,18 @@ public CreateNewDatasetCommand(Dataset theDataset, DataverseRequest aRequest, Te */ @Override protected void additionalParameterTests(CommandContext ctxt) throws CommandException { - if ( nonEmpty(getDataset().getIdentifier()) ) { - GlobalIdServiceBean idServiceBean = GlobalIdServiceBean.getBean(getDataset().getProtocol(), ctxt); - if ( !idServiceBean.isGlobalIdUnique(getDataset().getGlobalId()) ) { - throw new IllegalCommandException(String.format("Dataset with identifier '%s', protocol '%s' and authority '%s' already exists", - getDataset().getIdentifier(), getDataset().getProtocol(), getDataset().getAuthority()), - this); - } + if (nonEmpty(getDataset().getIdentifier())) { + GlobalId pid = getDataset().getGlobalId(); + if (pid != null) { + PidProvider pidProvider = PidUtil.getPidProvider(pid.getProviderId()); + + if (!pidProvider.isGlobalIdUnique(pid)) { + throw new IllegalCommandException(String.format( + "Dataset with identifier '%s', protocol '%s' and authority '%s' already exists", + getDataset().getIdentifier(), getDataset().getProtocol(), getDataset().getAuthority()), + this); + } + } } } @@ -88,11 +95,11 @@ protected DatasetVersion getVersionToPersist( Dataset theDataset ) { @Override protected void handlePid(Dataset theDataset, CommandContext ctxt) throws CommandException { - GlobalIdServiceBean idServiceBean = GlobalIdServiceBean.getBean(ctxt); - if(!idServiceBean.isConfigured()) { - throw new IllegalCommandException("PID Provider " + idServiceBean.getProviderInformation().get(0) + " is not configured.", this); + PidProvider pidProvider = PidUtil.getPidProvider(theDataset.getGlobalId().getProviderId()); + if(!pidProvider.canManagePID()) { + throw new IllegalCommandException("PID Provider " + pidProvider.getId() + " is not configured.", this); } - if ( !idServiceBean.registerWhenPublished() ) { + if ( !pidProvider.registerWhenPublished() ) { // pre-register a persistent id registerExternalIdentifier(theDataset, ctxt, true); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CuratePublishedDatasetVersionCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CuratePublishedDatasetVersionCommand.java index f83041d87bd..e6e8279a314 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CuratePublishedDatasetVersionCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CuratePublishedDatasetVersionCommand.java @@ -1,7 +1,6 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.authorization.Permission; -import edu.harvard.iq.dataverse.datavariable.VarGroup; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; @@ -13,14 +12,17 @@ import edu.harvard.iq.dataverse.util.DatasetFieldUtil; import edu.harvard.iq.dataverse.workflows.WorkflowComment; import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetField; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.TermsOfUseAndAccess; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.FileMetadata; +import edu.harvard.iq.dataverse.RoleAssignment; import edu.harvard.iq.dataverse.DataFileCategory; import edu.harvard.iq.dataverse.DatasetVersionDifference; -import java.util.Collection; +import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -50,6 +52,9 @@ public Dataset execute(CommandContext ctxt) throws CommandException { if (!getUser().isSuperuser()) { throw new IllegalCommandException("Only superusers can curate published dataset versions", this); } + Dataset savedDataset = null; + // Merge the dataset into our JPA context + setDataset(ctxt.em().merge(getDataset())); ctxt.permissions().checkEditDatasetLock(getDataset(), getRequest(), this); // Invariant: Dataset has no locks preventing the update @@ -58,23 +63,23 @@ public Dataset execute(CommandContext ctxt) throws CommandException { DatasetVersion newVersion = getDataset().getOrCreateEditVersion(); // Copy metadata from draft version to latest published version updateVersion.setDatasetFields(newVersion.initDatasetFields()); - - + newVersion.setDatasetFields(new ArrayList()); // final DatasetVersion editVersion = getDataset().getEditVersion(); DatasetFieldUtil.tidyUpFields(updateVersion.getDatasetFields(), true); - // Merge the new version into our JPA context - ctxt.em().merge(updateVersion); - TermsOfUseAndAccess oldTerms = updateVersion.getTermsOfUseAndAccess(); TermsOfUseAndAccess newTerms = newVersion.getTermsOfUseAndAccess(); newTerms.setDatasetVersion(updateVersion); updateVersion.setTermsOfUseAndAccess(newTerms); - //Put old terms on version that will be deleted.... - newVersion.setTermsOfUseAndAccess(oldTerms); - - //Validate metadata and TofA conditions + // Clear unnecessary terms relationships .... + newVersion.setTermsOfUseAndAccess(null); + oldTerms.setDatasetVersion(null); + // Without this there's a db exception related to the oldTerms being referenced + // by the datasetversion table at the flush around line 212 + ctxt.em().flush(); + + // Validate metadata and TofA conditions validateOrDie(updateVersion, isValidateLenient()); //Also set the fileaccessrequest boolean on the dataset to match the new terms @@ -87,19 +92,20 @@ public Dataset execute(CommandContext ctxt) throws CommandException { updateVersion.getWorkflowComments().addAll(newComments); } - // we have to merge to update the database but not flush because // we don't want to create two draft versions! - Dataset tempDataset = ctxt.em().merge(getDataset()); - + Dataset tempDataset = getDataset(); updateVersion = tempDataset.getLatestVersionForCopy(); // Look for file metadata changes and update published metadata if needed List pubFmds = updateVersion.getFileMetadatas(); int pubFileCount = pubFmds.size(); int newFileCount = tempDataset.getOrCreateEditVersion().getFileMetadatas().size(); - /* The policy for this command is that it should only be used when the change is a 'minor update' with no file changes. - * Nominally we could call .isMinorUpdate() for that but we're making the same checks as we go through the update here. + /* + * The policy for this command is that it should only be used when the change is + * a 'minor update' with no file changes. Nominally we could call + * .isMinorUpdate() for that but we're making the same checks as we go through + * the update here. */ if (pubFileCount != newFileCount) { logger.severe("Draft version of dataset: " + tempDataset.getId() + " has: " + newFileCount + " while last published version has " + pubFileCount); @@ -108,7 +114,10 @@ public Dataset execute(CommandContext ctxt) throws CommandException { Long thumbId = null; if(tempDataset.getThumbnailFile()!=null) { thumbId = tempDataset.getThumbnailFile().getId(); - }; + } + + // Note - Curate allows file metadata changes but not adding/deleting files. If + // that ever changes, this command needs to be updated. for (FileMetadata publishedFmd : pubFmds) { DataFile dataFile = publishedFmd.getDataFile(); FileMetadata draftFmd = dataFile.getLatestFileMetadata(); @@ -155,45 +164,73 @@ public Dataset execute(CommandContext ctxt) throws CommandException { // Update modification time on the published version and the dataset updateVersion.setLastUpdateTime(getTimestamp()); tempDataset.setModificationTime(getTimestamp()); - ctxt.em().merge(updateVersion); - Dataset savedDataset = ctxt.em().merge(tempDataset); - - // Flush before calling DeleteDatasetVersion which calls - // PrivateUrlServiceBean.getPrivateUrlFromDatasetId() that will query the DB and - // fail if our changes aren't there - ctxt.em().flush(); + newVersion = ctxt.em().merge(newVersion); + savedDataset = ctxt.em().merge(tempDataset); // Now delete draft version - DeleteDatasetVersionCommand cmd; - cmd = new DeleteDatasetVersionCommand(getRequest(), savedDataset); - ctxt.engine().submit(cmd); - // Running the command above reindexes the dataset, so we don't need to do it - // again in here. + ctxt.em().remove(newVersion); + + Iterator dvIt = savedDataset.getVersions().iterator(); + while (dvIt.hasNext()) { + DatasetVersion dv = dvIt.next(); + if (dv.isDraft()) { + dvIt.remove(); + break; // We've removed the draft version, no need to continue iterating + } + } + + savedDataset = ctxt.em().merge(savedDataset); + ctxt.em().flush(); + + RoleAssignment ra = ctxt.privateUrl().getPrivateUrlRoleAssignmentFromDataset(savedDataset); + if (ra != null) { + ctxt.roles().revoke(ra); + } // And update metadata at PID provider - ctxt.engine().submit( - new UpdateDvObjectPIDMetadataCommand(savedDataset, getRequest())); - - //And the exported metadata files try { - ExportService instance = ExportService.getInstance(); - instance.exportAllFormats(getDataset()); - } catch (ExportException ex) { - // Just like with indexing, a failure to export is not a fatal condition. - logger.log(Level.WARNING, "Curate Published DatasetVersion: exception while exporting metadata files:{0}", ex.getMessage()); + ctxt.engine().submit( + new UpdateDvObjectPIDMetadataCommand(savedDataset, getRequest())); + } catch (CommandException ex) { + // The try/catch makes this non-fatal. Should it be non-fatal - it's different from what we do in publish? + // This can be corrected by running the update PID API later, but who will look in the log? + // With the change to not use the DeleteDatasetVersionCommand above and other + // fixes, this error may now cleanly restore the initial state + // with the draft and last published versions unchanged, but this has not yet bee tested. + // (Alternately this could move to onSuccess if we intend it to stay non-fatal.) + logger.log(Level.WARNING, "Curate Published DatasetVersion: exception while updating PID metadata:{0}", ex.getMessage()); } - - - // Update so that getDataset() in updateDatasetUser will get the up-to-date copy - // (with no draft version) + // Update so that getDataset() in updateDatasetUser() will get the up-to-date + // copy (with no draft version) setDataset(savedDataset); + updateDatasetUser(ctxt); - - + // ToDo - see if there are other DatasetVersionUser entries unique to the draft + // version that should be moved to the last published version + // As this command is intended for minor fixes, often done by the person pushing + // the update-current-version button, this is probably a minor issue. return savedDataset; } + @Override + public boolean onSuccess(CommandContext ctxt, Object r) { + boolean retVal = true; + Dataset d = (Dataset) r; + + ctxt.index().asyncIndexDataset(d, true); + + // And the exported metadata files + try { + ExportService instance = ExportService.getInstance(); + instance.exportAllFormats(d); + } catch (ExportException ex) { + // Just like with indexing, a failure to export is not a fatal condition. + retVal = false; + logger.log(Level.WARNING, "Curate Published DatasetVersion: exception while exporting metadata files:{0}", ex.getMessage()); + } + return retVal; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataFileCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataFileCommand.java index e2730ec06d3..0812c52a846 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataFileCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataFileCommand.java @@ -1,6 +1,7 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.GlobalId; import edu.harvard.iq.dataverse.search.IndexServiceBean; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; @@ -11,6 +12,8 @@ import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.CommandExecutionException; import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.StringUtil; import java.io.IOException; @@ -23,7 +26,6 @@ import java.util.Collections; import java.util.logging.Level; import java.util.logging.Logger; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; /** * Deletes a data file, both DB entity and filesystem object. @@ -202,15 +204,18 @@ public FileVisitResult postVisitDirectory(final Path dir, final IOException e) */ } } - GlobalIdServiceBean idServiceBean = GlobalIdServiceBean.getBean(ctxt); - try { - if (idServiceBean.alreadyRegistered(doomed)) { - idServiceBean.deleteIdentifier(doomed); + GlobalId pid = doomed.getGlobalId(); + if (pid != null) { + PidProvider pidProvider = PidUtil.getPidProvider(pid.getProviderId()); + + try { + if (pidProvider.alreadyRegistered(doomed)) { + pidProvider.deleteIdentifier(doomed); + } + } catch (Exception e) { + logger.log(Level.WARNING, "Identifier deletion was not successfull:", e.getMessage()); } - } catch (Exception e) { - logger.log(Level.WARNING, "Identifier deletion was not successfull:", e.getMessage()); } - DataFile doomedAndMerged = ctxt.em().merge(doomed); ctxt.em().remove(doomedAndMerged); /** diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDatasetLinkingDataverseCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDatasetLinkingDataverseCommand.java index f21a2782609..7f5672c0cd7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDatasetLinkingDataverseCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDatasetLinkingDataverseCommand.java @@ -8,17 +8,14 @@ import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetLinkingDataverse; import edu.harvard.iq.dataverse.authorization.Permission; -import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.batch.util.LoggingUtil; import edu.harvard.iq.dataverse.engine.command.AbstractCommand; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; -import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; import java.io.IOException; -import java.util.Collections; -import java.util.concurrent.Future; + import org.apache.solr.client.solrj.SolrServerException; /** @@ -26,7 +23,7 @@ * @author sarahferry */ -@RequiredPermissions( Permission.EditDataset ) +@RequiredPermissions( Permission.PublishDataset ) public class DeleteDatasetLinkingDataverseCommand extends AbstractCommand{ private final DatasetLinkingDataverse doomed; private final Dataset editedDs; @@ -41,10 +38,6 @@ public DeleteDatasetLinkingDataverseCommand(DataverseRequest aRequest, Dataset e @Override public Dataset execute(CommandContext ctxt) throws CommandException { - if ((!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser())) { - throw new PermissionException("Delete dataset linking dataverse can only be called by superusers.", - this, Collections.singleton(Permission.EditDataset), editedDs); - } Dataset merged = ctxt.em().merge(editedDs); DatasetLinkingDataverse doomedAndMerged = ctxt.em().merge(doomed); ctxt.em().remove(doomedAndMerged); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeletePidCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeletePidCommand.java index 274aeb3c3fd..c4910dd10c2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeletePidCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeletePidCommand.java @@ -1,7 +1,6 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.Dataset; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.engine.command.AbstractVoidCommand; @@ -11,7 +10,8 @@ import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.util.BundleUtil; import org.apache.commons.httpclient.HttpException; @@ -38,25 +38,26 @@ public DeletePidCommand(DataverseRequest request, Dataset dataset) { protected void executeImpl(CommandContext ctxt) throws CommandException { if (!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser()) { - throw new PermissionException(BundleUtil.getStringFromBundle("admin.api.auth.mustBeSuperUser"), - this, Collections.singleton(Permission.EditDataset), dataset); + throw new PermissionException(BundleUtil.getStringFromBundle("admin.api.auth.mustBeSuperUser"), this, + Collections.singleton(Permission.EditDataset), dataset); } - String nonNullDefaultIfKeyNotFound = ""; - String protocol = ctxt.settings().getValueForKey(SettingsServiceBean.Key.Protocol, nonNullDefaultIfKeyNotFound); - GlobalIdServiceBean idServiceBean = GlobalIdServiceBean.getBean(protocol, ctxt); + PidProvider pidProvider = PidUtil.getPidProvider(dataset.getGlobalId().getProviderId()); + try { - idServiceBean.deleteIdentifier(dataset); + pidProvider.deleteIdentifier(dataset); // Success! Clear the create time, etc. dataset.setGlobalIdCreateTime(null); dataset.setIdentifierRegistered(false); ctxt.datasets().merge(dataset); } catch (HttpException hex) { - String message = BundleUtil.getStringFromBundle("pids.deletePid.failureExpected", Arrays.asList(dataset.getGlobalId().asString(), Integer.toString(hex.getReasonCode()))); + String message = BundleUtil.getStringFromBundle("pids.deletePid.failureExpected", + Arrays.asList(dataset.getGlobalId().asString(), Integer.toString(hex.getReasonCode()))); logger.info(message); throw new IllegalCommandException(message, this); } catch (Exception ex) { - String message = BundleUtil.getStringFromBundle("pids.deletePid.failureOther", Arrays.asList(dataset.getGlobalId().asString(), ex.getLocalizedMessage())); + String message = BundleUtil.getStringFromBundle("pids.deletePid.failureOther", + Arrays.asList(dataset.getGlobalId().asString(), ex.getLocalizedMessage())); logger.info(message); throw new IllegalCommandException(message, this); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DestroyDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DestroyDatasetCommand.java index 41093444360..be3e28029e4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DestroyDatasetCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DestroyDatasetCommand.java @@ -2,7 +2,9 @@ import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.GlobalId; import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.search.IndexServiceBean; import edu.harvard.iq.dataverse.RoleAssignment; @@ -15,6 +17,8 @@ import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.search.IndexResponse; import java.util.ArrayList; import java.util.Collections; @@ -22,7 +26,7 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; + import edu.harvard.iq.dataverse.batch.util.LoggingUtil; import java.io.IOException; import java.util.concurrent.Future; @@ -61,17 +65,17 @@ protected void executeImpl(CommandContext ctxt) throws CommandException { throw new PermissionException("Destroy can only be called by superusers.", this, Collections.singleton(Permission.DeleteDatasetDraft), doomed); } + Dataset managedDoomed = ctxt.em().merge(doomed); // If there is a dedicated thumbnail DataFile, it needs to be reset // explicitly, or we'll get a constraint violation when deleting: - doomed.setThumbnailFile(null); - final Dataset managedDoomed = ctxt.em().merge(doomed); - + managedDoomed.setThumbnailFile(null); + // files need to iterate through and remove 'by hand' to avoid // optimistic lock issues... (plus the physical files need to be // deleted too!) - - Iterator dfIt = doomed.getFiles().iterator(); + DatasetVersion dv = managedDoomed.getLatestVersion(); + Iterator dfIt = managedDoomed.getFiles().iterator(); while (dfIt.hasNext()){ DataFile df = dfIt.next(); // Gather potential Solr IDs of files. As of this writing deaccessioned files are never indexed. @@ -82,50 +86,52 @@ protected void executeImpl(CommandContext ctxt) throws CommandException { ctxt.engine().submit(new DeleteDataFileCommand(df, getRequest(), true)); dfIt.remove(); } - - //also, lets delete the uploaded thumbnails! - if (!doomed.isHarvested()) { - deleteDatasetLogo(doomed); - } + dv.setFileMetadatas(null); // ASSIGNMENTS - for (RoleAssignment ra : ctxt.roles().directRoleAssignments(doomed)) { + for (RoleAssignment ra : ctxt.roles().directRoleAssignments(managedDoomed)) { ctxt.em().remove(ra); } // ROLES - for (DataverseRole ra : ctxt.roles().findByOwnerId(doomed.getId())) { + for (DataverseRole ra : ctxt.roles().findByOwnerId(managedDoomed.getId())) { ctxt.em().remove(ra); } - if (!doomed.isHarvested()) { - GlobalIdServiceBean idServiceBean = GlobalIdServiceBean.getBean(ctxt); - try { - if (idServiceBean.alreadyRegistered(doomed)) { - idServiceBean.deleteIdentifier(doomed); - for (DataFile df : doomed.getFiles()) { - idServiceBean.deleteIdentifier(df); + if (!managedDoomed.isHarvested()) { + //also, lets delete the uploaded thumbnails! + deleteDatasetLogo(managedDoomed); + // and remove the PID (perhaps should be after the remove in case that causes a roll-back?) + GlobalId pid = managedDoomed.getGlobalId(); + if (pid != null) { + PidProvider pidProvider = PidUtil.getPidProvider(pid.getProviderId()); + try { + if (pidProvider.alreadyRegistered(managedDoomed)) { + pidProvider.deleteIdentifier(managedDoomed); + //Files are handled in DeleteDataFileCommand } + } catch (Exception e) { + logger.log(Level.WARNING, "Identifier deletion was not successful:", e.getMessage()); } - } catch (Exception e) { - logger.log(Level.WARNING, "Identifier deletion was not successful:", e.getMessage()); } - } + } toReIndex = managedDoomed.getOwner(); - // dataset - ctxt.em().remove(managedDoomed); - // add potential Solr IDs of datasets to list for deletion - String solrIdOfPublishedDatasetVersion = IndexServiceBean.solrDocIdentifierDataset + doomed.getId(); + String solrIdOfPublishedDatasetVersion = IndexServiceBean.solrDocIdentifierDataset + managedDoomed.getId(); datasetAndFileSolrIdsToDelete.add(solrIdOfPublishedDatasetVersion); - String solrIdOfDraftDatasetVersion = IndexServiceBean.solrDocIdentifierDataset + doomed.getId() + IndexServiceBean.draftSuffix; + String solrIdOfDraftDatasetVersion = IndexServiceBean.solrDocIdentifierDataset + managedDoomed.getId() + IndexServiceBean.draftSuffix; datasetAndFileSolrIdsToDelete.add(solrIdOfDraftDatasetVersion); String solrIdOfDraftDatasetVersionPermission = solrIdOfDraftDatasetVersion + IndexServiceBean.discoverabilityPermissionSuffix; datasetAndFileSolrIdsToDelete.add(solrIdOfDraftDatasetVersionPermission); - String solrIdOfDeaccessionedDatasetVersion = IndexServiceBean.solrDocIdentifierDataset + doomed.getId() + IndexServiceBean.deaccessionedSuffix; + String solrIdOfDeaccessionedDatasetVersion = IndexServiceBean.solrDocIdentifierDataset + managedDoomed.getId() + IndexServiceBean.deaccessionedSuffix; datasetAndFileSolrIdsToDelete.add(solrIdOfDeaccessionedDatasetVersion); + + // dataset + ctxt.em().remove(managedDoomed); + + } @Override diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java index d6d7b49d172..94f983f0c13 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java @@ -5,7 +5,6 @@ import edu.harvard.iq.dataverse.DatasetLock.Reason; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.ApiToken; -import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.workflow.step.Failure; @@ -14,7 +13,7 @@ import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -32,7 +31,7 @@ import org.duracloud.error.ContentStoreException; @RequiredPermissions(Permission.PublishDataset) -public class DuraCloudSubmitToArchiveCommand extends AbstractSubmitToArchiveCommand implements Command { +public class DuraCloudSubmitToArchiveCommand extends AbstractSubmitToArchiveCommand { private static final Logger logger = Logger.getLogger(DuraCloudSubmitToArchiveCommand.class.getName()); private static final String DEFAULT_PORT = "443"; @@ -117,7 +116,7 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t public void run() { try (PipedOutputStream dataciteOut = new PipedOutputStream(dataciteIn)) { - dataciteOut.write(dataciteXml.getBytes(Charset.forName("utf-8"))); + dataciteOut.write(dataciteXml.getBytes(StandardCharsets.UTF_8)); dataciteOut.close(); success=true; } catch (Exception e) { diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java index 89cfc732455..fa8cfeb810a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java @@ -20,6 +20,8 @@ import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.export.ExportService; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.privateurl.PrivateUrl; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; @@ -30,7 +32,7 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; + import edu.harvard.iq.dataverse.batch.util.LoggingUtil; import edu.harvard.iq.dataverse.dataaccess.StorageIO; import edu.harvard.iq.dataverse.engine.command.Command; @@ -100,13 +102,13 @@ public Dataset execute(CommandContext ctxt) throws CommandException { try { // This can potentially throw a CommandException, so let's make // sure we exit cleanly: - - registerExternalIdentifier(theDataset, ctxt, false); + registerExternalIdentifier(theDataset, ctxt, false); + registerFilePidsIfNeeded(theDataset, ctxt, false); } catch (CommandException comEx) { - logger.warning("Failed to reserve the identifier "+theDataset.getGlobalId().asString()+"; notifying the user(s), unlocking the dataset"); - // Send failure notification to the user: + logger.warning("Failed to reserve the identifier " + theDataset.getGlobalId().asString() + "; notifying the user(s), unlocking the dataset"); + // Send failure notification to the user: notifyUsersDatasetPublishStatus(ctxt, theDataset, UserNotification.Type.PUBLISHFAILED_PIDREG); - // Remove the dataset lock: + // Remove the dataset lock: ctxt.datasets().removeDatasetLocks(theDataset, DatasetLock.Reason.finalizePublication); // re-throw the exception: throw comEx; @@ -209,7 +211,7 @@ public Dataset execute(CommandContext ctxt) throws CommandException { if (theDataset.getLatestVersion().getVersionState() != RELEASED) { // some imported datasets may already be released. - + theDataset.getLatestVersion().setVersionState(RELEASED); if (!datasetExternallyReleased) { publicizeExternalIdentifier(theDataset, ctxt); // Will throw a CommandException, unless successful. @@ -218,7 +220,6 @@ public Dataset execute(CommandContext ctxt) throws CommandException { // a failure - it will remove any locks, and it will send a // proper notification to the user(s). } - theDataset.getLatestVersion().setVersionState(RELEASED); } final Dataset ds = ctxt.em().merge(theDataset); @@ -265,7 +266,6 @@ public boolean onSuccess(CommandContext ctxt, Object r) { } catch (Exception e) { logger.warning("Failure to send dataset published messages for : " + dataset.getId() + " : " + e.getMessage()); } - ctxt.index().asyncIndexDataset(dataset, true); //re-indexing dataverses that have additional subjects if (!dataversesToIndex.isEmpty()){ @@ -295,7 +295,8 @@ public boolean onSuccess(CommandContext ctxt, Object r) { logger.log(Level.WARNING, "Finalization: exception caught while exporting: "+ex.getMessage(), ex); // ... but it is important to only update the export time stamp if the // export was indeed successful. - } + } + ctxt.index().asyncIndexDataset(dataset, true); return retVal; } @@ -349,7 +350,7 @@ private void validateDataFiles(Dataset dataset, CommandContext ctxt) throws Comm // major release; we can revisit the decision if there's any // indication that this makes publishing take significantly longer. String driverId = FileUtil.getStorageDriver(dataFile); - if(StorageIO.isDataverseAccessible(driverId) && maxFileSize == -1 || dataFile.getFilesize() < maxFileSize) { + if(StorageIO.isDataverseAccessible(driverId) && (maxFileSize == -1 || dataFile.getFilesize() < maxFileSize)) { FileUtil.validateDataFileChecksum(dataFile); } else { @@ -384,56 +385,51 @@ private void validateDataFiles(Dataset dataset, CommandContext ctxt) throws Comm } private void publicizeExternalIdentifier(Dataset dataset, CommandContext ctxt) throws CommandException { - String protocol = getDataset().getProtocol(); - String authority = getDataset().getAuthority(); - GlobalIdServiceBean idServiceBean = GlobalIdServiceBean.getBean(protocol, ctxt); - - if (idServiceBean != null) { - - try { - String currentGlobalIdProtocol = ctxt.settings().getValueForKey(SettingsServiceBean.Key.Protocol, ""); - String currentGlobalAuthority = ctxt.settings().getValueForKey(SettingsServiceBean.Key.Authority, ""); - String dataFilePIDFormat = ctxt.settings().getValueForKey(SettingsServiceBean.Key.DataFilePIDFormat, "DEPENDENT"); - boolean isFilePIDsEnabled = ctxt.systemConfig().isFilePIDsEnabledForCollection(getDataset().getOwner()); - // We will skip trying to register the global identifiers for datafiles - // if "dependent" file-level identifiers are requested, AND the naming - // protocol, or the authority of the dataset global id is different from - // what's currently configured for the Dataverse. In other words - // we can't get "dependent" DOIs assigned to files in a dataset - // with the registered id that is a handle; or even a DOI, but in - // an authority that's different from what's currently configured. - // Additionaly in 4.9.3 we have added a system variable to disable - // registering file PIDs on the installation level. - if (((currentGlobalIdProtocol.equals(protocol) && currentGlobalAuthority.equals(authority)) - || dataFilePIDFormat.equals("INDEPENDENT")) - && isFilePIDsEnabled - && dataset.getLatestVersion().getMinorVersionNumber() != null - && dataset.getLatestVersion().getMinorVersionNumber().equals((long) 0)) { - //A false return value indicates a failure in calling the service - for (DataFile df : dataset.getFiles()) { - logger.log(Level.FINE, "registering global id for file {0}", df.getId()); - //A false return value indicates a failure in calling the service - if (!idServiceBean.publicizeIdentifier(df)) { - throw new Exception(); - } - df.setGlobalIdCreateTime(getTimestamp()); - df.setIdentifierRegistered(true); + PidProvider pidProvider = ctxt.dvObjects().getEffectivePidGenerator(dataset); + try { + // We will skip trying to register the global identifiers for datafiles + // if "dependent" file-level identifiers are requested, AND the naming + // protocol, or the authority of the dataset global id is different from + // what's currently configured for the Dataverse. In other words + // we can't get "dependent" DOIs assigned to files in a dataset + // with the registered id that is a handle; or even a DOI, but in + // an authority that's different from what's currently configured. + // File PIDs may be enabled/disabled per collection. + boolean registerGlobalIdsForFiles = ctxt.systemConfig().isFilePIDsEnabledForCollection( + getDataset().getOwner()) + && pidProvider.canCreatePidsLike(dataset.getGlobalId()); + + if (registerGlobalIdsForFiles + && dataset.getLatestVersion().getMinorVersionNumber() != null + && dataset.getLatestVersion().getMinorVersionNumber().equals((long) 0)) { + // A false return value indicates a failure in calling the service + for (DataFile df : dataset.getFiles()) { + logger.log(Level.FINE, "registering global id for file {0}", df.getId()); + // A false return value indicates a failure in calling the service + if (!pidProvider.publicizeIdentifier(df)) { + throw new Exception(); } + df.setGlobalIdCreateTime(getTimestamp()); + df.setIdentifierRegistered(true); } - if (!idServiceBean.publicizeIdentifier(dataset)) { - throw new Exception(); - } - dataset.setGlobalIdCreateTime(new Date()); // TODO these two methods should be in the responsibility of the idServiceBean. - dataset.setIdentifierRegistered(true); - } catch (Throwable e) { - logger.warning("Failed to register the identifier "+dataset.getGlobalId().asString()+", or to register a file in the dataset; notifying the user(s), unlocking the dataset"); - - // Send failure notification to the user: - notifyUsersDatasetPublishStatus(ctxt, dataset, UserNotification.Type.PUBLISHFAILED_PIDREG); - - ctxt.datasets().removeDatasetLocks(dataset, DatasetLock.Reason.finalizePublication); - throw new CommandException(BundleUtil.getStringFromBundle("dataset.publish.error", idServiceBean.getProviderInformation()), this); } + if (!pidProvider.publicizeIdentifier(dataset)) { + throw new Exception(); + } + dataset.setGlobalIdCreateTime(new Date()); // TODO these two methods should be in the responsibility of the + // pidProvider. + dataset.setIdentifierRegistered(true); + } catch (Throwable e) { + logger.warning("Failed to publicize the identifier " + dataset.getGlobalId().asString() + + ", or to publicize a file in the dataset; notifying the user(s), unlocking the dataset"); + + // Send failure notification to the user: + notifyUsersDatasetPublishStatus(ctxt, dataset, UserNotification.Type.PUBLISHFAILED_PIDREG); + + ctxt.datasets().removeDatasetLocks(dataset, DatasetLock.Reason.finalizePublication); + throw new CommandException( + BundleUtil.getStringFromBundle("dataset.publish.error", pidProvider.getProviderInformation()), + this); } } @@ -442,8 +438,9 @@ private void updateFiles(Timestamp updateTime, CommandContext ctxt) throws Comma if (dataFile.getPublicationDate() == null) { // this is a new, previously unpublished file, so publish by setting date dataFile.setPublicationDate(updateTime); - - // check if any prexisting roleassignments have file download and send notifications + + // check if any pre-existing role assignments have file download and send + // notifications notifyUsersFileDownload(ctxt, dataFile); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDataFileCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDataFileCommand.java index fdf47bbd2dd..369f3cbfda6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDataFileCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDataFileCommand.java @@ -11,35 +11,34 @@ import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; + import java.util.Collections; import java.util.Map; import java.util.Set; /** - * * @author Matthew */ // no annotations here, since permissions are dynamically decided // based off GetDatasetCommand for similar permissions checking public class GetDataFileCommand extends AbstractCommand { - private final DataFile df; + private final DataFile dataFile; - public GetDataFileCommand(DataverseRequest aRequest, DataFile anAffectedDataset) { - super(aRequest, anAffectedDataset); - df = anAffectedDataset; + public GetDataFileCommand(DataverseRequest aRequest, DataFile dataFile) { + super(aRequest, dataFile); + this.dataFile = dataFile; } @Override public DataFile execute(CommandContext ctxt) throws CommandException { - return df; + return dataFile; } @Override public Map> getRequiredPermissions() { return Collections.singletonMap("", - df.isReleased() ? Collections.emptySet() - : Collections.singleton(Permission.ViewUnpublishedDataset)); + dataFile.isReleased() ? Collections.emptySet() + : Collections.singleton(Permission.ViewUnpublishedDataset)); } - } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDraftFileMetadataIfAvailableCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDraftFileMetadataIfAvailableCommand.java index 14999548b34..1d83f0dd1f4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDraftFileMetadataIfAvailableCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDraftFileMetadataIfAvailableCommand.java @@ -1,8 +1,6 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.Dataset; -import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.FileMetadata; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.engine.command.AbstractCommand; @@ -12,25 +10,19 @@ import edu.harvard.iq.dataverse.engine.command.exception.CommandException; /** - * * @author Matthew */ -@RequiredPermissions( Permission.ViewUnpublishedDataset ) -public class GetDraftFileMetadataIfAvailableCommand extends AbstractCommand{ - private final DataFile df; +@RequiredPermissions(Permission.ViewUnpublishedDataset) +public class GetDraftFileMetadataIfAvailableCommand extends AbstractCommand { + private final DataFile dataFile; - public GetDraftFileMetadataIfAvailableCommand(DataverseRequest aRequest, DataFile dataFile) { - super(aRequest, dataFile); - df = dataFile; + public GetDraftFileMetadataIfAvailableCommand(DataverseRequest request, DataFile dataFile) { + super(request, dataFile); + this.dataFile = dataFile; } @Override public FileMetadata execute(CommandContext ctxt) throws CommandException { - FileMetadata fm = df.getLatestFileMetadata(); - if(fm.getDatasetVersion().getVersionState().equals(DatasetVersion.VersionState.DRAFT)) { - return df.getLatestFileMetadata(); - } - return null; + return dataFile.getDraftFileMetadata(); } - } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleDatasetVersionCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleDatasetVersionCommand.java index 7bcc851bde2..431b3ff47c6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleDatasetVersionCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleDatasetVersionCommand.java @@ -25,24 +25,25 @@ public class GetLatestAccessibleDatasetVersionCommand extends AbstractCommand { private final Dataset ds; private final boolean includeDeaccessioned; - private boolean checkPerms; + private boolean checkPermsWhenDeaccessioned; public GetLatestAccessibleDatasetVersionCommand(DataverseRequest aRequest, Dataset anAffectedDataset) { - this(aRequest, anAffectedDataset, false, false); + this(aRequest, anAffectedDataset,false, false); } - public GetLatestAccessibleDatasetVersionCommand(DataverseRequest aRequest, Dataset anAffectedDataset, boolean includeDeaccessioned, boolean checkPerms) { + public GetLatestAccessibleDatasetVersionCommand(DataverseRequest aRequest, Dataset anAffectedDataset, boolean includeDeaccessioned, boolean checkPermsWhenDeaccessioned) { super(aRequest, anAffectedDataset); ds = anAffectedDataset; this.includeDeaccessioned = includeDeaccessioned; - this.checkPerms = checkPerms; + this.checkPermsWhenDeaccessioned = checkPermsWhenDeaccessioned; } @Override public DatasetVersion execute(CommandContext ctxt) throws CommandException { - if (ds.getLatestVersion().isDraft() && ctxt.permissions().requestOn(getRequest(), ds).has(Permission.ViewUnpublishedDataset)) { + if (ds.getLatestVersion().isDraft() && + ctxt.permissions().requestOn(getRequest(), ds).has(Permission.ViewUnpublishedDataset)) { return ctxt.engine().submit(new GetDraftDatasetVersionCommand(getRequest(), ds)); } - return ctxt.engine().submit(new GetLatestPublishedDatasetVersionCommand(getRequest(), ds, includeDeaccessioned, checkPerms)); + return ctxt.engine().submit(new GetLatestPublishedDatasetVersionCommand(getRequest(), ds, includeDeaccessioned, checkPermsWhenDeaccessioned)); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java new file mode 100644 index 00000000000..05f3c73bde0 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java @@ -0,0 +1,30 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.FileMetadata; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.engine.command.CommandContext; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; + +public class GetLatestAccessibleFileMetadataCommand extends AbstractGetPublishedFileMetadataCommand { + + public GetLatestAccessibleFileMetadataCommand(DataverseRequest request, DataFile dataFile, boolean includeDeaccessioned) { + super(request, dataFile, includeDeaccessioned); + } + + @Override + public FileMetadata execute(CommandContext ctxt) throws CommandException { + FileMetadata fileMetadata = null; + + if (ctxt.permissions().requestOn(getRequest(), dataFile.getOwner()).has(Permission.ViewUnpublishedDataset)) { + fileMetadata = dataFile.getDraftFileMetadata(); + } + + if (fileMetadata == null) { + fileMetadata = getLatestPublishedFileMetadata(ctxt); + } + + return fileMetadata; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedFileMetadataCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedFileMetadataCommand.java new file mode 100644 index 00000000000..fc13dba1a34 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedFileMetadataCommand.java @@ -0,0 +1,19 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.FileMetadata; +import edu.harvard.iq.dataverse.engine.command.CommandContext; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; + +public class GetLatestPublishedFileMetadataCommand extends AbstractGetPublishedFileMetadataCommand { + + public GetLatestPublishedFileMetadataCommand(DataverseRequest request, DataFile dataFile, boolean includeDeaccessioned) { + super(request, dataFile, includeDeaccessioned); + } + + @Override + public FileMetadata execute(CommandContext ctxt) throws CommandException { + return getLatestPublishedFileMetadata(ctxt); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetSpecificPublishedFileMetadataByDatasetVersionCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetSpecificPublishedFileMetadataByDatasetVersionCommand.java new file mode 100644 index 00000000000..deffbfb57ee --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetSpecificPublishedFileMetadataByDatasetVersionCommand.java @@ -0,0 +1,34 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.DatasetVersion; +import edu.harvard.iq.dataverse.FileMetadata; +import edu.harvard.iq.dataverse.engine.command.CommandContext; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; + +public class GetSpecificPublishedFileMetadataByDatasetVersionCommand extends AbstractGetPublishedFileMetadataCommand { + private final long majorVersion; + private final long minorVersion; + + public GetSpecificPublishedFileMetadataByDatasetVersionCommand(DataverseRequest request, DataFile dataFile, long majorVersion, long minorVersion, boolean includeDeaccessioned) { + super(request, dataFile, includeDeaccessioned); + this.majorVersion = majorVersion; + this.minorVersion = minorVersion; + } + + @Override + public FileMetadata execute(CommandContext ctxt) throws CommandException { + return dataFile.getFileMetadatas().stream() + .filter(fileMetadata -> isRequestedVersionFileMetadata(fileMetadata, ctxt)) + .findFirst() + .orElse(null); + } + + private boolean isRequestedVersionFileMetadata(FileMetadata fileMetadata, CommandContext ctxt) { + DatasetVersion datasetVersion = fileMetadata.getDatasetVersion(); + return isDatasetVersionAccessible(datasetVersion, dataFile.getOwner(), ctxt) + && datasetVersion.getVersionNumber().equals(majorVersion) + && datasetVersion.getMinorVersionNumber().equals(minorVersion); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java index 512987866d4..7d749262b87 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java @@ -11,7 +11,6 @@ import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.ApiToken; -import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.settings.JvmSettings; @@ -26,14 +25,14 @@ import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.security.DigestInputStream; import java.security.MessageDigest; import java.util.Map; import java.util.logging.Logger; @RequiredPermissions(Permission.PublishDataset) -public class GoogleCloudSubmitToArchiveCommand extends AbstractSubmitToArchiveCommand implements Command { +public class GoogleCloudSubmitToArchiveCommand extends AbstractSubmitToArchiveCommand { private static final Logger logger = Logger.getLogger(GoogleCloudSubmitToArchiveCommand.class.getName()); private static final String GOOGLECLOUD_BUCKET = ":GoogleCloudBucket"; @@ -82,7 +81,7 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t public void run() { try (PipedOutputStream dataciteOut = new PipedOutputStream(dataciteIn)) { - dataciteOut.write(dataciteXml.getBytes(Charset.forName("utf-8"))); + dataciteOut.write(dataciteXml.getBytes(StandardCharsets.UTF_8)); dataciteOut.close(); success = true; } catch (Exception e) { diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GrantSuperuserStatusCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GrantSuperuserStatusCommand.java index 42af43b7247..ec8c8976260 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GrantSuperuserStatusCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GrantSuperuserStatusCommand.java @@ -14,7 +14,7 @@ import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; /** * diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ImportDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ImportDatasetCommand.java index 478272950bd..772c989264c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ImportDatasetCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ImportDatasetCommand.java @@ -1,14 +1,14 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.Dataset; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.CommandExecutionException; import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; -import edu.harvard.iq.dataverse.pidproviders.FakePidProviderServiceBean; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; import java.io.IOException; import java.util.Collections; @@ -80,9 +80,9 @@ protected void additionalParameterTests(CommandContext ctxt) throws CommandExcep * Dataverse) but aren't findable to be used. That could be the case if, for * example, someone was importing a draft dataset from elsewhere. */ - GlobalIdServiceBean globalIdServiceBean = GlobalIdServiceBean.getBean(ds.getProtocol(), ctxt); - if (globalIdServiceBean != null) { - if (globalIdServiceBean.alreadyRegistered(ds.getGlobalId(), true)) { + PidProvider pidProvider = PidUtil.getPidProvider(ds.getGlobalId().getProviderId()); + if (pidProvider != null) { + if (pidProvider.alreadyRegistered(ds.getGlobalId(), true)) { return; } } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseInputLevelsCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseInputLevelsCommand.java new file mode 100644 index 00000000000..1727ac9698f --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseInputLevelsCommand.java @@ -0,0 +1,40 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.DataverseFacet; +import edu.harvard.iq.dataverse.DataverseFieldTypeInputLevel; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.engine.command.AbstractCommand; +import edu.harvard.iq.dataverse.engine.command.CommandContext; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * List the field type input levels {@link DataverseFieldTypeInputLevel} of a {@link Dataverse}. + */ +public class ListDataverseInputLevelsCommand extends AbstractCommand> { + + private final Dataverse dataverse; + + public ListDataverseInputLevelsCommand(DataverseRequest request, Dataverse dataverse) { + super(request, dataverse); + this.dataverse = dataverse; + } + + @Override + public List execute(CommandContext ctxt) throws CommandException { + return dataverse.getDataverseFieldTypeInputLevels(); + } + + @Override + public Map> getRequiredPermissions() { + return Collections.singletonMap("", + dataverse.isReleased() ? Collections.emptySet() + : Collections.singleton(Permission.ViewUnpublishedDataverse)); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListFacetsCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListFacetsCommand.java index cbab378ccac..36bd1ef4981 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListFacetsCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListFacetsCommand.java @@ -7,6 +7,7 @@ import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; + import java.util.Collections; import java.util.List; import java.util.Map; @@ -14,27 +15,34 @@ /** * List the search facets {@link DataverseFacet} of a {@link Dataverse}. + * * @author michaelsuo */ // no annotations here, since permissions are dynamically decided public class ListFacetsCommand extends AbstractCommand> { - private final Dataverse dv; + private final Dataverse dataverse; + private boolean rootFacets; + + public ListFacetsCommand(DataverseRequest request, Dataverse dataverse) { + this(request, dataverse, true); + } - public ListFacetsCommand(DataverseRequest aRequest, Dataverse aDataverse) { - super(aRequest, aDataverse); - dv = aDataverse; + public ListFacetsCommand(DataverseRequest request, Dataverse dataverse, boolean rootFacets) { + super(request, dataverse); + this.dataverse = dataverse; + this.rootFacets = rootFacets; } @Override public List execute(CommandContext ctxt) throws CommandException { - return dv.getDataverseFacets(); + return dataverse.getDataverseFacets(!rootFacets); } @Override public Map> getRequiredPermissions() { return Collections.singletonMap("", - dv.isReleased() ? Collections.emptySet() - : Collections.singleton(Permission.ViewUnpublishedDataverse)); + dataverse.isReleased() ? Collections.emptySet() + : Collections.singleton(Permission.ViewUnpublishedDataverse)); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListFeaturedCollectionsCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListFeaturedCollectionsCommand.java new file mode 100644 index 00000000000..4dca522e499 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListFeaturedCollectionsCommand.java @@ -0,0 +1,50 @@ + +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.DataverseFeaturedDataverse; +import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.engine.command.AbstractCommand; +import edu.harvard.iq.dataverse.engine.command.CommandContext; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * + * @author stephenkraffmiller + */ +public class ListFeaturedCollectionsCommand extends AbstractCommand> { + + private final Dataverse dv; + + public ListFeaturedCollectionsCommand(DataverseRequest aRequest, Dataverse aDataverse) { + super(aRequest, aDataverse); + dv = aDataverse; + } + + @Override + public List execute(CommandContext ctxt) throws CommandException { + List featuredTarget = new ArrayList<>(); + List featuredList = ctxt.featuredDataverses().findByDataverseId(dv.getId()); + for (DataverseFeaturedDataverse dfd : featuredList) { + Dataverse fd = dfd.getFeaturedDataverse(); + featuredTarget.add(fd); + } + return featuredTarget; + + } + + @Override + public Map> getRequiredPermissions() { + return Collections.singletonMap("", + dv.isReleased() ? Collections.emptySet() + : Collections.singleton(Permission.ViewUnpublishedDataverse)); + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListMetadataBlocksCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListMetadataBlocksCommand.java index 912318cf155..8275533ced2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListMetadataBlocksCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListMetadataBlocksCommand.java @@ -7,6 +7,7 @@ import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; + import java.util.Collections; import java.util.List; import java.util.Map; @@ -14,29 +15,40 @@ /** * Lists the metadata blocks of a {@link Dataverse}. - * + * * @author michael */ // no annotations here, since permissions are dynamically decided -public class ListMetadataBlocksCommand extends AbstractCommand>{ - - private final Dataverse dv; - - public ListMetadataBlocksCommand(DataverseRequest aRequest, Dataverse aDataverse) { - super(aRequest, aDataverse); - dv = aDataverse; +public class ListMetadataBlocksCommand extends AbstractCommand> { + + private final Dataverse dataverse; + private final boolean onlyDisplayedOnCreate; + + public ListMetadataBlocksCommand(DataverseRequest request, Dataverse dataverse, boolean onlyDisplayedOnCreate) { + super(request, dataverse); + this.dataverse = dataverse; + this.onlyDisplayedOnCreate = onlyDisplayedOnCreate; } @Override public List execute(CommandContext ctxt) throws CommandException { - return dv.getMetadataBlocks(); + if (onlyDisplayedOnCreate) { + return listMetadataBlocksDisplayedOnCreate(ctxt, dataverse); + } + return dataverse.getMetadataBlocks(); } - + + private List listMetadataBlocksDisplayedOnCreate(CommandContext ctxt, Dataverse dataverse) { + if (dataverse.isMetadataBlockRoot() || dataverse.getOwner() == null) { + return ctxt.metadataBlocks().listMetadataBlocksDisplayedOnCreate(dataverse); + } + return listMetadataBlocksDisplayedOnCreate(ctxt, dataverse.getOwner()); + } + @Override public Map> getRequiredPermissions() { return Collections.singletonMap("", - dv.isReleased() ? Collections.emptySet() - : Collections.singleton(Permission.ViewUnpublishedDataverse)); - } - + dataverse.isReleased() ? Collections.emptySet() + : Collections.singleton(Permission.ViewUnpublishedDataverse)); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MergeInAccountCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MergeInAccountCommand.java index 1ec51764d73..03f4dceef88 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MergeInAccountCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MergeInAccountCommand.java @@ -14,7 +14,6 @@ import edu.harvard.iq.dataverse.UserNotification; import edu.harvard.iq.dataverse.authorization.AuthenticatedUserLookup; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUser; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2TokenData; import edu.harvard.iq.dataverse.authorization.users.ApiToken; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.batch.util.LoggingUtil; @@ -25,7 +24,6 @@ import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; -import edu.harvard.iq.dataverse.passwordreset.PasswordResetData; import edu.harvard.iq.dataverse.search.IndexResponse; import edu.harvard.iq.dataverse.search.savedsearch.SavedSearch; import edu.harvard.iq.dataverse.workflows.WorkflowComment; @@ -177,6 +175,7 @@ protected void executeImpl(CommandContext ctxt) throws CommandException { ctxt.em().createNativeQuery("Delete from OAuth2TokenData where user_id ="+consumedAU.getId()).executeUpdate(); + ctxt.em().createNativeQuery("DELETE FROM explicitgroup_authenticateduser consumed USING explicitgroup_authenticateduser ongoing WHERE consumed.containedauthenticatedusers_id="+ongoingAU.getId()+" AND ongoing.containedauthenticatedusers_id="+consumedAU.getId()).executeUpdate(); ctxt.em().createNativeQuery("UPDATE explicitgroup_authenticateduser SET containedauthenticatedusers_id="+ongoingAU.getId()+" WHERE containedauthenticatedusers_id="+consumedAU.getId()).executeUpdate(); ctxt.actionLog().changeUserIdentifierInHistory(consumedAU.getIdentifier(), ongoingAU.getIdentifier()); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MoveDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MoveDatasetCommand.java index 94bcfa2f5b7..bee5dc648b9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MoveDatasetCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/MoveDatasetCommand.java @@ -7,8 +7,10 @@ import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetLinkingDataverse; +import edu.harvard.iq.dataverse.DatasetLock; import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.Guestbook; +import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.engine.command.AbstractVoidCommand; @@ -135,7 +137,14 @@ public void executeImpl(CommandContext ctxt) throws CommandException { } throw new UnforcedCommandException(errorString.toString(), this); } - + + // 6575 if dataset is submitted for review and the default contributor + // role includes dataset publish then remove the lock + + if (moved.isLockedFor(DatasetLock.Reason.InReview) + && destination.getDefaultContributorRole().permissions().contains(Permission.PublishDataset)) { + ctxt.datasets().removeDatasetLocks(moved, DatasetLock.Reason.InReview); + } // OK, move moved.setOwner(destination); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/PublishDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/PublishDatasetCommand.java index f5ef121dee2..1ac41105237 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/PublishDatasetCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/PublishDatasetCommand.java @@ -2,22 +2,15 @@ import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetLock; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; -import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; -import edu.harvard.iq.dataverse.privateurl.PrivateUrl; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; -import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.workflow.Workflow; import edu.harvard.iq.dataverse.workflow.WorkflowContext.TriggerType; -import java.util.Date; -import java.util.List; import java.util.Optional; import java.util.logging.Logger; import static java.util.stream.Collectors.joining; @@ -130,24 +123,15 @@ public PublishDatasetResult execute(CommandContext ctxt) throws CommandException // ... // Additionaly in 4.9.3 we have added a system variable to disable // registering file PIDs on the installation level. - String currentGlobalIdProtocol = ctxt.settings().getValueForKey(SettingsServiceBean.Key.Protocol, ""); - String currentGlobalAuthority= ctxt.settings().getValueForKey(SettingsServiceBean.Key.Authority, ""); - String dataFilePIDFormat = ctxt.settings().getValueForKey(SettingsServiceBean.Key.DataFilePIDFormat, "DEPENDENT"); boolean registerGlobalIdsForFiles = - (currentGlobalIdProtocol.equals(theDataset.getProtocol()) || dataFilePIDFormat.equals("INDEPENDENT")) - && ctxt.systemConfig().isFilePIDsEnabledForCollection(theDataset.getOwner()); - - if ( registerGlobalIdsForFiles ){ - registerGlobalIdsForFiles = currentGlobalAuthority.equals( theDataset.getAuthority() ); - } + ctxt.systemConfig().isFilePIDsEnabledForCollection(getDataset().getOwner()) && + ctxt.dvObjects().getEffectivePidGenerator(getDataset()).canCreatePidsLike(getDataset().getGlobalId()); boolean validatePhysicalFiles = ctxt.systemConfig().isDatafileValidationOnPublishEnabled(); // As of v5.0, publishing a dataset is always done asynchronously, // with the dataset locked for the duration of the operation. - //if ((registerGlobalIdsForFiles || validatePhysicalFiles) - // && theDataset.getFiles().size() > ctxt.systemConfig().getPIDAsynchRegFileCount()) { String info = "Publishing the dataset; "; info += registerGlobalIdsForFiles ? "Registering PIDs for Datafiles; " : ""; @@ -178,15 +162,6 @@ public PublishDatasetResult execute(CommandContext ctxt) throws CommandException // method: //ctxt.datasets().callFinalizePublishCommandAsynchronously(theDataset.getId(), ctxt, request, datasetExternallyReleased); return new PublishDatasetResult(theDataset, Status.Inprogress); - - /** - * Code for for "synchronous" (while-you-wait) publishing - * is preserved below, commented out: - } else { - // Synchronous publishing (no workflow involved) - theDataset = ctxt.engine().submit(new FinalizeDatasetPublicationCommand(theDataset, getRequest(),datasetExternallyReleased)); - return new PublishDatasetResult(theDataset, Status.Completed); - } */ } } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/RegisterDvObjectCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/RegisterDvObjectCommand.java index 779bc7fb7fe..7b80871a1e0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/RegisterDvObjectCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/RegisterDvObjectCommand.java @@ -4,20 +4,17 @@ import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DvObject; -import edu.harvard.iq.dataverse.GlobalId; +import edu.harvard.iq.dataverse.DvObjectContainer; import edu.harvard.iq.dataverse.engine.command.AbstractVoidCommand; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.handle.HandlePidProvider; + import java.sql.Timestamp; import java.util.Date; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; -import edu.harvard.iq.dataverse.HandlenetServiceBean; -import edu.harvard.iq.dataverse.batch.util.LoggingUtil; -import java.io.IOException; -import org.apache.solr.client.solrj.SolrServerException; /** * @@ -44,48 +41,37 @@ public RegisterDvObjectCommand(DataverseRequest aRequest, DvObject target, Boole @Override protected void executeImpl(CommandContext ctxt) throws CommandException { + DvObjectContainer container = (target instanceof DvObjectContainer) ? (DvObjectContainer) target : target.getOwner(); + // Get the pidProvider that is configured to mint new IDs + PidProvider pidProvider = ctxt.dvObjects().getEffectivePidGenerator(container); if(this.migrateHandle){ //Only continue if you can successfully migrate the handle - if (!processMigrateHandle(ctxt)) return; + if (HandlePidProvider.HDL_PROTOCOL.equals(pidProvider.getProtocol()) || !processMigrateHandle(ctxt)) return; } - String nonNullDefaultIfKeyNotFound = ""; - String protocol = ctxt.settings().getValueForKey(SettingsServiceBean.Key.Protocol, nonNullDefaultIfKeyNotFound); - String authority = ctxt.settings().getValueForKey(SettingsServiceBean.Key.Authority, nonNullDefaultIfKeyNotFound); - // Get the idServiceBean that is configured to mint new IDs - GlobalIdServiceBean idServiceBean = GlobalIdServiceBean.getBean(protocol, ctxt); + try { //Test to see if identifier already present //if so, leave. if (target.getIdentifier() == null || target.getIdentifier().isEmpty()) { - if (target.isInstanceofDataset()) { - target.setIdentifier(idServiceBean.generateDatasetIdentifier((Dataset) target)); - - } else { - target.setIdentifier(idServiceBean.generateDataFileIdentifier((DataFile) target)); - } - if (target.getProtocol() == null) { - target.setProtocol(protocol); - } - if (target.getAuthority() == null) { - target.setAuthority(authority); - } + pidProvider.generatePid(target); } - if (idServiceBean.alreadyRegistered(target)) { + + if (pidProvider.alreadyRegistered(target)) { return; } - String doiRetString = idServiceBean.createIdentifier(target); + String doiRetString = pidProvider.createIdentifier(target); if (doiRetString != null && doiRetString.contains(target.getIdentifier())) { - if (!idServiceBean.registerWhenPublished()) { + if (!pidProvider.registerWhenPublished()) { // Should register ID before publicize() is called - // For example, DOIEZIdServiceBean tries to recreate the id if the identifier isn't registered before + // For example, DOIEZIdProvider tries to recreate the id if the identifier isn't registered before // publicizeIdentifier is called target.setIdentifierRegistered(true); target.setGlobalIdCreateTime(new Timestamp(new Date().getTime())); } if (target.isReleased()) { - idServiceBean.publicizeIdentifier(target); + pidProvider.publicizeIdentifier(target); } - if (idServiceBean.registerWhenPublished() && target.isReleased()) { + if (pidProvider.registerWhenPublished() && target.isReleased()) { target.setGlobalIdCreateTime(new Timestamp(new Date().getTime())); target.setIdentifierRegistered(true); } @@ -95,27 +81,21 @@ protected void executeImpl(CommandContext ctxt) throws CommandException { Dataset dataset = (Dataset) target; for (DataFile df : dataset.getFiles()) { if (df.getIdentifier() == null || df.getIdentifier().isEmpty()) { - df.setIdentifier(idServiceBean.generateDataFileIdentifier(df)); - if (df.getProtocol() == null || df.getProtocol().isEmpty()) { - df.setProtocol(protocol); - } - if (df.getAuthority() == null || df.getAuthority().isEmpty()) { - df.setAuthority(authority); - } + pidProvider.generatePid(df); } - doiRetString = idServiceBean.createIdentifier(df); + doiRetString = pidProvider.createIdentifier(df); if (doiRetString != null && doiRetString.contains(df.getIdentifier())) { - if (!idServiceBean.registerWhenPublished()) { + if (!pidProvider.registerWhenPublished()) { // Should register ID before publicize() is called - // For example, DOIEZIdServiceBean tries to recreate the id if the identifier isn't registered before + // For example, DOIEZIdProvider tries to recreate the id if the identifier isn't registered before // publicizeIdentifier is called df.setIdentifierRegistered(true); df.setGlobalIdCreateTime(new Timestamp(new Date().getTime())); } if (df.isReleased()) { - idServiceBean.publicizeIdentifier(df); + pidProvider.publicizeIdentifier(df); } - if (idServiceBean.registerWhenPublished() && df.isReleased()) { + if (pidProvider.registerWhenPublished() && df.isReleased()) { df.setGlobalIdCreateTime(new Timestamp(new Date().getTime())); df.setIdentifierRegistered(true); } @@ -145,7 +125,7 @@ protected void executeImpl(CommandContext ctxt) throws CommandException { private Boolean processMigrateHandle (CommandContext ctxt){ boolean retval = true; if(!target.isInstanceofDataset()) return false; - if(!target.getProtocol().equals(HandlenetServiceBean.HDL_PROTOCOL)) return false; + if(!target.getProtocol().equals(HandlePidProvider.HDL_PROTOCOL)) return false; AlternativePersistentIdentifier api = new AlternativePersistentIdentifier(); api.setProtocol(target.getProtocol()); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/RequestRsyncScriptCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/RequestRsyncScriptCommand.java index a29e7fdd59c..6b7baa7d01b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/RequestRsyncScriptCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/RequestRsyncScriptCommand.java @@ -27,6 +27,7 @@ * "actiontype" in the actionlogrecord rather than "InternalError" if you throw * a CommandExecutionException. */ +@Deprecated(forRemoval = true, since = "2024-07-07") @RequiredPermissions(Permission.EditDataset) public class RequestRsyncScriptCommand extends AbstractCommand { diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ReservePidCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ReservePidCommand.java index 6b2872f3397..77b06e4e152 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ReservePidCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ReservePidCommand.java @@ -1,28 +1,23 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.Dataset; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; -import edu.harvard.iq.dataverse.engine.command.AbstractVoidCommand; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; -import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; -import java.util.Arrays; import java.util.Collections; -import java.util.Date; import java.util.logging.Logger; /** * No required permissions because we check for superuser status. + * @param */ @RequiredPermissions({}) -public class ReservePidCommand extends AbstractVoidCommand { +public class ReservePidCommand extends AbstractDatasetCommand { private static final Logger logger = Logger.getLogger(ReservePidCommand.class.getCanonicalName()); @@ -34,28 +29,15 @@ public ReservePidCommand(DataverseRequest request, Dataset dataset) { } @Override - protected void executeImpl(CommandContext ctxt) throws CommandException { + public Dataset execute(CommandContext ctxt) throws CommandException { if (!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser()) { throw new PermissionException(BundleUtil.getStringFromBundle("admin.api.auth.mustBeSuperUser"), this, Collections.singleton(Permission.EditDataset), dataset); } - - String nonNullDefaultIfKeyNotFound = ""; - String protocol = ctxt.settings().getValueForKey(SettingsServiceBean.Key.Protocol, nonNullDefaultIfKeyNotFound); - GlobalIdServiceBean idServiceBean = GlobalIdServiceBean.getBean(protocol, ctxt); - try { - String returnString = idServiceBean.createIdentifier(dataset); - logger.fine(returnString); - // No errors caught, so mark PID as reserved. - dataset.setGlobalIdCreateTime(new Date()); - // We don't setIdentifierRegistered(true) yet. - ctxt.datasets().merge(dataset); - } catch (Throwable ex) { - String message = BundleUtil.getStringFromBundle("pids.commands.reservePid.failure", Arrays.asList(dataset.getId().toString(), ex.getLocalizedMessage())); - logger.info(message); - throw new IllegalCommandException(message, this); - } + registerExternalIdentifier(getDataset(), ctxt, true); + registerFilePidsIfNeeded(getDataset(), ctxt, true); + return dataset; } } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ReturnDatasetToAuthorCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ReturnDatasetToAuthorCommand.java index caf37ad4de1..8d8fddeda6b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ReturnDatasetToAuthorCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ReturnDatasetToAuthorCommand.java @@ -11,6 +11,7 @@ import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; +import edu.harvard.iq.dataverse.settings.FeatureFlags; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.workflows.WorkflowComment; import java.io.IOException; @@ -25,6 +26,11 @@ public class ReturnDatasetToAuthorCommand extends AbstractDatasetCommand { +public class S3SubmitToArchiveCommand extends AbstractSubmitToArchiveCommand { private static final Logger logger = Logger.getLogger(S3SubmitToArchiveCommand.class.getName()); private static final String S3_CONFIG = ":S3ArchiverConfig"; @@ -86,7 +86,7 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t spaceName = getSpaceName(dataset); String dataciteXml = getDataCiteXml(dv); - try (ByteArrayInputStream dataciteIn = new ByteArrayInputStream(dataciteXml.getBytes("UTF-8"))) { + try (ByteArrayInputStream dataciteIn = new ByteArrayInputStream(dataciteXml.getBytes(StandardCharsets.UTF_8))) { // Add datacite.xml file ObjectMetadata om = new ObjectMetadata(); om.setContentLength(dataciteIn.available()); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UningestFileCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UningestFileCommand.java index 3e85630dd59..ba04c4d7931 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UningestFileCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UningestFileCommand.java @@ -47,10 +47,10 @@ public UningestFileCommand(DataverseRequest aRequest, DataFile uningest) { @Override protected void executeImpl(CommandContext ctxt) throws CommandException { - // first check if user is a superuser - if ( (!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser() ) ) { - throw new PermissionException("Uningest File can only be called by Superusers.", - this, Collections.singleton(Permission.EditDataset), uningest); + // first check if user is a superuser + if ((!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser())) { + throw new PermissionException("Uningest File can only be called by Superusers.", this, + Collections.singleton(Permission.EditDataset), uningest); } // is this actually a tabular data file? diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDatasetTargetURLCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDatasetTargetURLCommand.java index 1f5989c9e08..8cf2d0109d6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDatasetTargetURLCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDatasetTargetURLCommand.java @@ -10,10 +10,12 @@ import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; + import java.sql.Timestamp; import java.util.Collections; import java.util.Date; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; /** * @@ -36,19 +38,21 @@ protected void executeImpl(CommandContext ctxt) throws CommandException { throw new PermissionException("Update Target URL can only be called by superusers.", this, Collections.singleton(Permission.EditDataset), target); } - GlobalIdServiceBean idServiceBean = GlobalIdServiceBean.getBean(target.getProtocol(), ctxt); + PidProvider pidProvider = PidUtil.getPidProvider(target.getGlobalId().getProviderId()); try { - String doiRetString = idServiceBean.modifyIdentifierTargetURL(target); + String doiRetString = pidProvider.modifyIdentifierTargetURL(target); if (doiRetString != null && doiRetString.contains(target.getIdentifier())) { target.setGlobalIdCreateTime(new Timestamp(new Date().getTime())); ctxt.em().merge(target); ctxt.em().flush(); for (DataFile df : target.getFiles()) { - doiRetString = idServiceBean.modifyIdentifierTargetURL(df); - if (doiRetString != null && doiRetString.contains(df.getIdentifier())) { - df.setGlobalIdCreateTime(new Timestamp(new Date().getTime())); - ctxt.em().merge(df); - ctxt.em().flush(); + if (df.isReleased()) { + doiRetString = pidProvider.modifyIdentifierTargetURL(df); + if (doiRetString != null && doiRetString.contains(df.getIdentifier())) { + df.setGlobalIdCreateTime(new Timestamp(new Date().getTime())); + ctxt.em().merge(df); + ctxt.em().flush(); + } } } } else { diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDatasetThumbnailCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDatasetThumbnailCommand.java index 3f4b3c36b70..b8c70ec6c46 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDatasetThumbnailCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDatasetThumbnailCommand.java @@ -11,12 +11,14 @@ import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; +import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.FileUtil; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.IOUtils; @@ -59,17 +61,18 @@ public DatasetThumbnail execute(CommandContext ctxt) throws CommandException { // throw new CommandException("Just testing what an error would look like in the GUI.", this); // } if (userIntent == null) { - throw new IllegalCommandException("No changes to save.", this); + throw new IllegalCommandException(BundleUtil.getStringFromBundle("datasets.api.thumbnail.noChange"), this); } switch (userIntent) { case setDatasetFileAsThumbnail: if (dataFileIdSupplied == null) { - throw new CommandException("A file was not selected to be the new dataset thumbnail.", this); + throw new CommandException(BundleUtil.getStringFromBundle("datasets.api.thumbnail.fileNotSupplied"), this); } DataFile datasetFileThumbnailToSwitchTo = ctxt.files().find(dataFileIdSupplied); if (datasetFileThumbnailToSwitchTo == null) { - throw new CommandException("Could not find file based on id supplied: " + dataFileIdSupplied + ".", this); + throw new CommandException(BundleUtil.getStringFromBundle("datasets.api.thumbnail.fileNotFound", + List.of(dataFileIdSupplied.toString())), this); } Dataset ds1 = ctxt.datasets().setDatasetFileAsThumbnail(dataset, datasetFileThumbnailToSwitchTo); DatasetThumbnail datasetThumbnail = ds1.getDatasetThumbnail(ImageThumbConverter.DEFAULT_CARDIMAGE_SIZE); @@ -79,11 +82,12 @@ public DatasetThumbnail execute(CommandContext ctxt) throws CommandException { if (dataFile.getId().equals(dataFileIdSupplied)) { return datasetThumbnail; } else { - throw new CommandException("Dataset thumbnail is should be based on file id " + dataFile.getId() + " but instead it is " + dataFileIdSupplied + ".", this); + throw new CommandException(BundleUtil.getStringFromBundle("datasets.api.thumbnail.basedOnWrongFileId", + List.of(String.valueOf(dataFile.getId()),String.valueOf(dataFileIdSupplied))), this); } } } else { - throw new CommandException("Dataset thumbnail is unexpectedly absent.", this); + throw new CommandException(BundleUtil.getStringFromBundle("datasets.api.thumbnail.missing"), this); } case setNonDatasetFileAsThumbnail: @@ -91,14 +95,14 @@ public DatasetThumbnail execute(CommandContext ctxt) throws CommandException { try { uploadedFile = FileUtil.inputStreamToFile(inputStream); } catch (IOException ex) { - throw new CommandException("In setNonDatasetFileAsThumbnail caught exception calling inputStreamToFile: " + ex, this); + throw new CommandException(BundleUtil.getStringFromBundle("datasets.api.thumbnail.inputStreamToFile.exception", List.of(ex.getMessage())), this); } if (uploadedFile == null) { - throw new CommandException("In setNonDatasetFileAsThumbnail uploadedFile was null.", this); + throw new CommandException(BundleUtil.getStringFromBundle("datasets.api.thumbnail.nonDatasetsFileIsNull"), this); } long uploadLogoSizeLimit = ctxt.systemConfig().getUploadLogoSizeLimit(); if (uploadedFile.length() > uploadLogoSizeLimit) { - throw new IllegalCommandException("File is larger than maximum size: " + uploadLogoSizeLimit + ".", this); + throw new IllegalCommandException(BundleUtil.getStringFromBundle("datasets.api.thumbnail.fileToLarge", List.of(String.valueOf(uploadLogoSizeLimit))), this); } FileInputStream fileAsStream = null; try { @@ -107,23 +111,25 @@ public DatasetThumbnail execute(CommandContext ctxt) throws CommandException { Logger.getLogger(UpdateDatasetThumbnailCommand.class.getName()).log(Level.SEVERE, null, ex); } Dataset datasetWithNewThumbnail = ctxt.datasets().setNonDatasetFileAsThumbnail(dataset, fileAsStream); - IOUtils.closeQuietly(fileAsStream); + IOUtils.closeQuietly(fileAsStream); if (datasetWithNewThumbnail != null) { - return datasetWithNewThumbnail.getDatasetThumbnail(ImageThumbConverter.DEFAULT_CARDIMAGE_SIZE); - } else { - return null; + DatasetThumbnail thumbnail = datasetWithNewThumbnail.getDatasetThumbnail(ImageThumbConverter.DEFAULT_CARDIMAGE_SIZE); + if (thumbnail != null) { + return thumbnail; + } } + throw new IllegalCommandException(BundleUtil.getStringFromBundle("datasets.api.thumbnail.nonDatasetFailed"), this); case removeThumbnail: - Dataset ds2 = ctxt.datasets().removeDatasetThumbnail(dataset); + Dataset ds2 = ctxt.datasets().clearDatasetLevelThumbnail(dataset); DatasetThumbnail datasetThumbnail2 = ds2.getDatasetThumbnail(ImageThumbConverter.DEFAULT_CARDIMAGE_SIZE); if (datasetThumbnail2 == null) { return null; } else { - throw new CommandException("User wanted to remove the thumbnail it still has one!", this); + throw new CommandException(BundleUtil.getStringFromBundle("datasets.api.thumbnail.notDeleted"), this); } default: - throw new IllegalCommandException("Whatever you are trying to do to the dataset thumbnail is not supported.", this); + throw new IllegalCommandException(BundleUtil.getStringFromBundle("datasets.api.thumbnail.actionNotSupported"), this); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDatasetVersionCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDatasetVersionCommand.java index 7591bebe796..bb5f5a71e24 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDatasetVersionCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDatasetVersionCommand.java @@ -1,6 +1,12 @@ package edu.harvard.iq.dataverse.engine.command.impl; -import edu.harvard.iq.dataverse.*; +import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.DataFileCategory; +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetLock; +import edu.harvard.iq.dataverse.DatasetVersion; +import edu.harvard.iq.dataverse.DatasetVersionDifference; +import edu.harvard.iq.dataverse.FileMetadata; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.engine.command.CommandContext; @@ -8,7 +14,6 @@ import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.DatasetFieldUtil; import edu.harvard.iq.dataverse.util.FileMetadataUtil; @@ -115,8 +120,11 @@ public Dataset execute(CommandContext ctxt) throws CommandException { //Will throw an IllegalCommandException if a system metadatablock is changed and the appropriate key is not supplied. checkSystemMetadataKeyIfNeeded(getDataset().getOrCreateEditVersion(fmVarMet), persistedVersion); - - + + getDataset().getOrCreateEditVersion().setLastUpdateTime(getTimestamp()); + + registerExternalVocabValuesIfAny(ctxt, getDataset().getOrCreateEditVersion(fmVarMet)); + try { // Invariant: Dataset has no locks preventing the update String lockInfoMessage = "saving current edits"; @@ -146,7 +154,7 @@ public Dataset execute(CommandContext ctxt) throws CommandException { throw e; } } - + //Set creator and create date for files if needed for (DataFile dataFile : theDataset.getFiles()) { if (dataFile.getCreateDate() == null) { dataFile.setCreateDate(getTimestamp()); @@ -251,12 +259,12 @@ public Dataset execute(CommandContext ctxt) throws CommandException { for(FileMetadata fmd: theDataset.getOrCreateEditVersion().getFileMetadatas()) { logger.fine("FMD: " + fmd.getId() + " for file: " + fmd.getDataFile().getId() + "is in final draft version"); } + registerFilePidsIfNeeded(theDataset, ctxt, true); if (recalculateUNF) { ctxt.ingest().recalculateDatasetVersionUNF(theDataset.getOrCreateEditVersion()); } - theDataset.getOrCreateEditVersion().setLastUpdateTime(getTimestamp()); theDataset.setModificationTime(getTimestamp()); savedDataset = ctxt.em().merge(theDataset); @@ -290,5 +298,5 @@ public boolean onSuccess(CommandContext ctxt, Object r) { ctxt.index().asyncIndexDataset((Dataset) r, true); return true; } - + } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseCommand.java index fe9415f39f9..bdb69dc918f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseCommand.java @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; -import jakarta.persistence.TypedQuery; /** * Update an existing dataverse. @@ -30,10 +29,10 @@ public class UpdateDataverseCommand extends AbstractCommand { private final Dataverse editedDv; private final List facetList; - private final List featuredDataverseList; - private final List inputLevelList; - - private boolean datasetsReindexRequired = false; + private final List featuredDataverseList; + private final List inputLevelList; + + private boolean datasetsReindexRequired = false; public UpdateDataverseCommand(Dataverse editedDv, List facetList, List featuredDataverseList, DataverseRequest aRequest, List inputLevelList ) { diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseInputLevelsCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseInputLevelsCommand.java new file mode 100644 index 00000000000..b9b08992919 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseInputLevelsCommand.java @@ -0,0 +1,35 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.DataverseFieldTypeInputLevel; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.engine.command.AbstractCommand; +import edu.harvard.iq.dataverse.engine.command.CommandContext; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; + +import java.util.ArrayList; +import java.util.List; + +@RequiredPermissions(Permission.EditDataverse) +public class UpdateDataverseInputLevelsCommand extends AbstractCommand { + private final Dataverse dataverse; + private final List inputLevelList; + + public UpdateDataverseInputLevelsCommand(Dataverse dataverse, DataverseRequest request, List inputLevelList) { + super(request, dataverse); + this.dataverse = dataverse; + this.inputLevelList = new ArrayList<>(inputLevelList); + } + + @Override + public Dataverse execute(CommandContext ctxt) throws CommandException { + if (inputLevelList == null || inputLevelList.isEmpty()) { + throw new CommandException("Error while updating dataverse input levels: Input level list cannot be null or empty", this); + } + dataverse.addInputLevelsMetadataBlocksIfNotPresent(inputLevelList); + dataverse.setMetadataBlockRoot(true); + return ctxt.engine().submit(new UpdateDataverseCommand(dataverse, null, null, getRequest(), inputLevelList)); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseThemeCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseThemeCommand.java index 9ef9fed4b1b..b770b9febef 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseThemeCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseThemeCommand.java @@ -1,19 +1,12 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.Dataverse; -import edu.harvard.iq.dataverse.ThemeWidgetFragment; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.engine.command.AbstractCommand; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; /** * Update an existing dataverse. @@ -22,19 +15,14 @@ @RequiredPermissions( Permission.EditDataverse ) public class UpdateDataverseThemeCommand extends AbstractCommand { private final Dataverse editedDv; - private final File uploadedFile; - private String locate; - public UpdateDataverseThemeCommand(Dataverse editedDv, File uploadedFile, DataverseRequest aRequest, String location) { + public UpdateDataverseThemeCommand(Dataverse editedDv, DataverseRequest aRequest) { super(aRequest, editedDv); - this.uploadedFile = uploadedFile; this.editedDv = editedDv; - this.locate = location; - } + /** - * Update Theme and Widget related data for this dataverse, and - * do file management needed for theme images. + * Update Theme and Widget related data for this dataverse. * * @param ctxt * @return @@ -42,59 +30,6 @@ public UpdateDataverseThemeCommand(Dataverse editedDv, File uploadedFile, Datave */ @Override public Dataverse execute(CommandContext ctxt) throws CommandException { - // Get current dataverse, so we can delete current logo file if necessary - Dataverse currentDv = ctxt.dataverses().find(editedDv.getId()); - File logoFileDir = ThemeWidgetFragment.getLogoDir(editedDv.getId().toString()).toFile(); - File currentFile=null; - - if (locate.equals("FOOTER")){ - if (currentDv.getDataverseTheme()!=null && currentDv.getDataverseTheme().getLogoFooter()!=null) { - currentFile = new File(logoFileDir, currentDv.getDataverseTheme().getLogoFooter()); - } - try { - // If edited logo field is empty, and a logoFile currently exists, delete it - if (editedDv.getDataverseTheme()==null || editedDv.getDataverseTheme().getLogoFooter()==null ) { - if (currentFile!=null) { - currentFile.delete(); - } - } // If edited logo file isn't empty,and uploaded File exists, delete currentFile and copy uploaded file from temp dir to logos dir - else if (uploadedFile!=null) { - File newFile = new File(logoFileDir,editedDv.getDataverseTheme().getLogoFooter()); - if (currentFile!=null) { - currentFile.delete(); - } - logoFileDir.mkdirs(); - Files.copy(uploadedFile.toPath(), newFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - - } catch (IOException e) { - throw new CommandException("Error saving logo footer file", e,this); // improve error handling - - } - } else if (locate.equals("HEADER")){ - if (currentDv.getDataverseTheme()!=null && currentDv.getDataverseTheme().getLogo()!=null) { - currentFile = new File(logoFileDir, currentDv.getDataverseTheme().getLogo()); - } - try { - // If edited logo field is empty, and a logoFile currently exists, delete it - if (editedDv.getDataverseTheme()==null || editedDv.getDataverseTheme().getLogo()==null ) { - if (currentFile!=null) { - currentFile.delete(); - } - } // If edited logo file isn't empty,and uploaded File exists, delete currentFile and copy uploaded file from temp dir to logos dir - else if (uploadedFile!=null) { - File newFile = new File(logoFileDir,editedDv.getDataverseTheme().getLogo()); - if (currentFile!=null) { - currentFile.delete(); - } - logoFileDir.mkdirs(); - Files.copy(uploadedFile.toPath(), newFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - } catch (IOException e) { - throw new CommandException("Error saving logo file", e,this); // improve error handling - - } - } // save updated dataverse to db return ctxt.dataverses().save(editedDv); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDvObjectPIDMetadataCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDvObjectPIDMetadataCommand.java index 7230f9f9c0a..14d17dcd900 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDvObjectPIDMetadataCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDvObjectPIDMetadataCommand.java @@ -2,7 +2,6 @@ import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.Dataset; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.engine.command.AbstractVoidCommand; @@ -11,6 +10,8 @@ import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; import java.sql.Timestamp; @@ -46,31 +47,32 @@ protected void executeImpl(CommandContext ctxt) throws CommandException { //the single dataset update api checks for drafts before calling the command return; } - GlobalIdServiceBean idServiceBean = GlobalIdServiceBean.getBean(target.getProtocol(), ctxt); + PidProvider pidProvider = PidUtil.getPidProvider(target.getGlobalId().getProviderId()); + try { - Boolean doiRetString = idServiceBean.publicizeIdentifier(target); + Boolean doiRetString = pidProvider.updateIdentifier(target); if (doiRetString) { target.setGlobalIdCreateTime(new Timestamp(new Date().getTime())); ctxt.em().merge(target); ctxt.em().flush(); // When updating, we want to traverse through files even if the dataset itself // didn't need updating. - String currentGlobalIdProtocol = ctxt.settings().getValueForKey(SettingsServiceBean.Key.Protocol, ""); - String dataFilePIDFormat = ctxt.settings().getValueForKey(SettingsServiceBean.Key.DataFilePIDFormat, "DEPENDENT"); boolean isFilePIDsEnabled = ctxt.systemConfig().isFilePIDsEnabledForCollection(target.getOwner()); // We will skip trying to update the global identifiers for datafiles if they // aren't being used. // If they are, we need to assure that there's an existing PID or, as when - // creating PIDs, that the protocol matches that of the dataset DOI if - // we're going to create a DEPENDENT file PID. - String protocol = target.getProtocol(); + // creating PIDs, that it's possible. + + boolean canCreatePidsForFiles = + isFilePIDsEnabled && ctxt.dvObjects().getEffectivePidGenerator(target).canCreatePidsLike(target.getGlobalId()); + for (DataFile df : target.getFiles()) { if (isFilePIDsEnabled && // using file PIDs and (!(df.getIdentifier() == null || df.getIdentifier().isEmpty()) || // identifier exists, or - currentGlobalIdProtocol.equals(protocol) || // right protocol to create dependent DOIs, or - dataFilePIDFormat.equals("INDEPENDENT"))// or independent. TODO(pm) - check authority too + canCreatePidsForFiles) && // we can create PIDs for files and + df.isReleased() // the file is not a draft ) { - doiRetString = idServiceBean.publicizeIdentifier(df); + doiRetString = pidProvider.updateIdentifier(df); if (doiRetString) { df.setGlobalIdCreateTime(new Timestamp(new Date().getTime())); ctxt.em().merge(df); diff --git a/src/main/java/edu/harvard/iq/dataverse/export/DDIExportServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/export/DDIExportServiceBean.java index 5119b4b96c7..edd01ae98a3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/export/DDIExportServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/export/DDIExportServiceBean.java @@ -545,6 +545,16 @@ private void createDataFileDDI(XMLStreamWriter xmlw, Set excludedFieldSe List vars = variableService.findByDataTableId(dt.getId()); if (checkField("catgry", excludedFieldSet, includedFieldSet)) { if (checkIsWithoutFrequencies(vars)) { + // @todo: the method called here to calculate frequencies + // when they are missing from the database (for whatever + // reasons) subsets the physical tab-delimited file and + // calculates them in real time. this is very expensive operation + // potentially. let's make sure that, when we do this, we + // save the resulting frequencies in the database, so that + // we don't have to do this again. Also, let's double check + // whether the "checkIsWithoutFrequencies()" method is doing + // the right thing - as it appears to return true when there + // are no categorical variables in the DataTable (?) calculateFrequencies(df, vars); } } @@ -580,6 +590,7 @@ private boolean checkIsWithoutFrequencies(List vars) { private void calculateFrequencies(DataFile df, List vars) { + // @todo: see the comment in the part of the code that calls this method try { DataConverter dc = new DataConverter(); File tabFile = dc.downloadFromStorageIO(df.getStorageIO()); diff --git a/src/main/java/edu/harvard/iq/dataverse/export/DataCiteExporter.java b/src/main/java/edu/harvard/iq/dataverse/export/DataCiteExporter.java index 8caf32b2df0..c21d6b5cd1a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/export/DataCiteExporter.java +++ b/src/main/java/edu/harvard/iq/dataverse/export/DataCiteExporter.java @@ -7,6 +7,7 @@ import io.gdcc.spi.export.ExportException; import io.gdcc.spi.export.Exporter; import io.gdcc.spi.export.XMLExporter; +import edu.harvard.iq.dataverse.pidproviders.doi.XmlMetadataTemplate; import edu.harvard.iq.dataverse.util.BundleUtil; import java.io.IOException; import java.io.OutputStream; @@ -20,11 +21,7 @@ */ @AutoService(Exporter.class) public class DataCiteExporter implements XMLExporter { - - private static String DEFAULT_XML_NAMESPACE = "http://datacite.org/schema/kernel-3"; - private static String DEFAULT_XML_SCHEMALOCATION = "http://datacite.org/schema/kernel-3 http://schema.datacite.org/meta/kernel-3/metadata.xsd"; - private static String DEFAULT_XML_VERSION = "3.0"; - + public static final String NAME = "Datacite"; @Override @@ -60,17 +57,17 @@ public Boolean isAvailableToUsers() { @Override public String getXMLNameSpace() { - return DataCiteExporter.DEFAULT_XML_NAMESPACE; + return XmlMetadataTemplate.XML_NAMESPACE; } @Override public String getXMLSchemaLocation() { - return DataCiteExporter.DEFAULT_XML_SCHEMALOCATION; + return XmlMetadataTemplate.XML_SCHEMA_LOCATION; } @Override public String getXMLSchemaVersion() { - return DataCiteExporter.DEFAULT_XML_VERSION; + return XmlMetadataTemplate.XML_SCHEMA_VERSION; } } diff --git a/src/main/java/edu/harvard/iq/dataverse/export/InternalExportDataProvider.java b/src/main/java/edu/harvard/iq/dataverse/export/InternalExportDataProvider.java index a7967f6ccb6..f0d77eb8b52 100644 --- a/src/main/java/edu/harvard/iq/dataverse/export/InternalExportDataProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/export/InternalExportDataProvider.java @@ -8,12 +8,11 @@ import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; - -import edu.harvard.iq.dataverse.DOIDataCiteRegisterService; import edu.harvard.iq.dataverse.DataCitation; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.FileMetadata; +import edu.harvard.iq.dataverse.pidproviders.doi.datacite.DOIDataCiteRegisterService; import io.gdcc.spi.export.ExportDataProvider; import edu.harvard.iq.dataverse.util.bagit.OREMap; import edu.harvard.iq.dataverse.util.json.JsonPrinter; diff --git a/src/main/java/edu/harvard/iq/dataverse/export/JSONExporter.java b/src/main/java/edu/harvard/iq/dataverse/export/JSONExporter.java index a54e61c7c1e..cf3afd1a39a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/export/JSONExporter.java +++ b/src/main/java/edu/harvard/iq/dataverse/export/JSONExporter.java @@ -7,10 +7,10 @@ import io.gdcc.spi.export.Exporter; import edu.harvard.iq.dataverse.util.BundleUtil; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.util.Locale; import java.util.Optional; -import jakarta.json.JsonObject; import jakarta.ws.rs.core.MediaType; @@ -35,7 +35,7 @@ public String getDisplayName(Locale locale) { @Override public void exportDataset(ExportDataProvider dataProvider, OutputStream outputStream) throws ExportException { try{ - outputStream.write(dataProvider.getDatasetJson().toString().getBytes("UTF8")); + outputStream.write(dataProvider.getDatasetJson().toString().getBytes(StandardCharsets.UTF_8)); outputStream.flush(); } catch (Exception e){ throw new ExportException("Unknown exception caught during JSON export."); diff --git a/src/main/java/edu/harvard/iq/dataverse/export/OAI_OREExporter.java b/src/main/java/edu/harvard/iq/dataverse/export/OAI_OREExporter.java index feec4403570..86af45195d7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/export/OAI_OREExporter.java +++ b/src/main/java/edu/harvard/iq/dataverse/export/OAI_OREExporter.java @@ -7,11 +7,11 @@ import edu.harvard.iq.dataverse.util.BundleUtil; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.util.Locale; import java.util.Optional; import java.util.logging.Logger; -import jakarta.json.JsonObject; import jakarta.ws.rs.core.MediaType; @AutoService(Exporter.class) @@ -25,7 +25,7 @@ public class OAI_OREExporter implements Exporter { public void exportDataset(ExportDataProvider dataProvider, OutputStream outputStream) throws ExportException { try { - outputStream.write(dataProvider.getDatasetORE().toString().getBytes("UTF8")); + outputStream.write(dataProvider.getDatasetORE().toString().getBytes(StandardCharsets.UTF_8)); outputStream.flush(); } catch (Exception e) { logger.severe(e.getMessage()); diff --git a/src/main/java/edu/harvard/iq/dataverse/export/SchemaDotOrgExporter.java b/src/main/java/edu/harvard/iq/dataverse/export/SchemaDotOrgExporter.java index 5428715b905..0c4b39fd641 100644 --- a/src/main/java/edu/harvard/iq/dataverse/export/SchemaDotOrgExporter.java +++ b/src/main/java/edu/harvard/iq/dataverse/export/SchemaDotOrgExporter.java @@ -7,6 +7,7 @@ import edu.harvard.iq.dataverse.util.BundleUtil; import java.io.IOException; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.util.Locale; import java.util.logging.Logger; import jakarta.ws.rs.core.MediaType; @@ -75,7 +76,7 @@ public class SchemaDotOrgExporter implements Exporter { @Override public void exportDataset(ExportDataProvider dataProvider, OutputStream outputStream) throws ExportException { try { - outputStream.write(dataProvider.getDatasetSchemaDotOrg().toString().getBytes("UTF8")); + outputStream.write(dataProvider.getDatasetSchemaDotOrg().toString().getBytes(StandardCharsets.UTF_8)); } catch (IOException ex) { logger.info("IOException calling outputStream.write: " + ex); } diff --git a/src/main/java/edu/harvard/iq/dataverse/export/ddi/DdiExportUtil.java b/src/main/java/edu/harvard/iq/dataverse/export/ddi/DdiExportUtil.java index 9a689f7a4ed..f5efc448090 100644 --- a/src/main/java/edu/harvard/iq/dataverse/export/ddi/DdiExportUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/export/ddi/DdiExportUtil.java @@ -24,6 +24,8 @@ import edu.harvard.iq.dataverse.util.SystemConfig; import edu.harvard.iq.dataverse.util.json.JsonUtil; import edu.harvard.iq.dataverse.util.xml.XmlPrinter; +import edu.harvard.iq.dataverse.util.xml.XmlWriterUtil; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -111,9 +113,9 @@ private static void dtoddi(DatasetDTO datasetDto, OutputStream outputStream) thr xmlw.writeDefaultNamespace("ddi:codebook:2_5"); xmlw.writeAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); xmlw.writeAttribute("xsi:schemaLocation", DDIExporter.DEFAULT_XML_NAMESPACE + " " + DDIExporter.DEFAULT_XML_SCHEMALOCATION); - writeAttribute(xmlw, "version", DDIExporter.DEFAULT_XML_VERSION); + xmlw.writeAttribute("version", DDIExporter.DEFAULT_XML_VERSION); if(DvObjectContainer.isMetadataLanguageSet(datasetDto.getMetadataLanguage())) { - writeAttribute(xmlw, "xml:lang", datasetDto.getMetadataLanguage()); + xmlw.writeAttribute("xml:lang", datasetDto.getMetadataLanguage()); } createStdyDscr(xmlw, datasetDto); createOtherMats(xmlw, datasetDto.getDatasetVersion().getFiles()); @@ -133,9 +135,9 @@ public static void datasetJson2ddi(JsonObject datasetDtoAsJson, JsonArray fileDe xmlw.writeDefaultNamespace("ddi:codebook:2_5"); xmlw.writeAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); xmlw.writeAttribute("xsi:schemaLocation", DDIExporter.DEFAULT_XML_NAMESPACE + " " + DDIExporter.DEFAULT_XML_SCHEMALOCATION); - writeAttribute(xmlw, "version", DDIExporter.DEFAULT_XML_VERSION); + xmlw.writeAttribute("version", DDIExporter.DEFAULT_XML_VERSION); if(DvObjectContainer.isMetadataLanguageSet(datasetDto.getMetadataLanguage())) { - writeAttribute(xmlw, "xml:lang", datasetDto.getMetadataLanguage()); + xmlw.writeAttribute("xml:lang", datasetDto.getMetadataLanguage()); } createStdyDscr(xmlw, datasetDto); createFileDscr(xmlw, fileDetails); @@ -186,15 +188,15 @@ private static void createStdyDscr(XMLStreamWriter xmlw, DatasetDTO datasetDto) xmlw.writeStartElement("citation"); xmlw.writeStartElement("titlStmt"); - writeFullElement(xmlw, "titl", dto2Primitive(version, DatasetFieldConstant.title), datasetDto.getMetadataLanguage()); - writeFullElement(xmlw, "subTitl", dto2Primitive(version, DatasetFieldConstant.subTitle)); + XmlWriterUtil.writeFullElement(xmlw, "titl", XmlWriterUtil.dto2Primitive(version, DatasetFieldConstant.title), datasetDto.getMetadataLanguage()); + XmlWriterUtil.writeFullElement(xmlw, "subTitl", XmlWriterUtil.dto2Primitive(version, DatasetFieldConstant.subTitle)); FieldDTO altField = dto2FieldDTO( version, DatasetFieldConstant.alternativeTitle, "citation" ); if (altField != null) { writeMultipleElement(xmlw, "altTitl", altField, datasetDto.getMetadataLanguage()); } xmlw.writeStartElement("IDNo"); - writeAttribute(xmlw, "agency", persistentAgency); + XmlWriterUtil.writeAttribute(xmlw, "agency", persistentAgency); xmlw.writeCharacters(pid); @@ -218,23 +220,23 @@ private static void createStdyDscr(XMLStreamWriter xmlw, DatasetDTO datasetDto) boolean excludeRepository = settingsService.isTrueForKey(SettingsServiceBean.Key.ExportInstallationAsDistributorOnlyWhenNotSet, false); if (!StringUtils.isEmpty(datasetDto.getPublisher()) && !(excludeRepository && distributorSet)) { xmlw.writeStartElement("distrbtr"); - writeAttribute(xmlw, "source", "archive"); + xmlw.writeAttribute("source", "archive"); xmlw.writeCharacters(datasetDto.getPublisher()); xmlw.writeEndElement(); //distrbtr } writeDistributorsElement(xmlw, version, datasetDto.getMetadataLanguage()); writeContactsElement(xmlw, version); /* per SCHEMA, depositr comes before depDate! - L.A. */ - writeFullElement(xmlw, "depositr", dto2Primitive(version, DatasetFieldConstant.depositor)); + XmlWriterUtil.writeFullElement(xmlw, "depositr", XmlWriterUtil.dto2Primitive(version, DatasetFieldConstant.depositor)); /* ... and depDate comes before distDate - L.A. */ - writeFullElement(xmlw, "depDate", dto2Primitive(version, DatasetFieldConstant.dateOfDeposit)); - writeFullElement(xmlw, "distDate", dto2Primitive(version, DatasetFieldConstant.distributionDate)); + XmlWriterUtil.writeFullElement(xmlw, "depDate", XmlWriterUtil.dto2Primitive(version, DatasetFieldConstant.dateOfDeposit)); + XmlWriterUtil.writeFullElement(xmlw, "distDate", XmlWriterUtil.dto2Primitive(version, DatasetFieldConstant.distributionDate)); xmlw.writeEndElement(); // diststmt writeSeriesElement(xmlw, version); xmlw.writeStartElement("holdings"); - writeAttribute(xmlw, "URI", pidUri); + XmlWriterUtil.writeAttribute(xmlw, "URI", pidUri); xmlw.writeEndElement(); //holdings xmlw.writeEndElement(); // citation @@ -247,7 +249,7 @@ private static void createStdyDscr(XMLStreamWriter xmlw, DatasetDTO datasetDto) writeSubjectElement(xmlw, version, datasetDto.getMetadataLanguage()); //Subject and Keywords writeAbstractElement(xmlw, version, datasetDto.getMetadataLanguage()); // Description writeSummaryDescriptionElement(xmlw, version, datasetDto.getMetadataLanguage()); - writeFullElement(xmlw, "notes", dto2Primitive(version, DatasetFieldConstant.notesText)); + XmlWriterUtil.writeFullElement(xmlw, "notes", XmlWriterUtil.dto2Primitive(version, DatasetFieldConstant.notesText)); //////// xmlw.writeEndElement(); // stdyInfo @@ -255,7 +257,7 @@ private static void createStdyDscr(XMLStreamWriter xmlw, DatasetDTO datasetDto) writeDataAccess(xmlw , version); writeOtherStudyMaterial(xmlw , version); - writeFullElement(xmlw, "notes", dto2Primitive(version, DatasetFieldConstant.datasetLevelErrorNotes)); + XmlWriterUtil.writeFullElement(xmlw, "notes", XmlWriterUtil.dto2Primitive(version, DatasetFieldConstant.datasetLevelErrorNotes)); xmlw.writeEndElement(); // stdyDscr @@ -274,10 +276,10 @@ private static void writeOtherStudyMaterial(XMLStreamWriter xmlw , DatasetVersio return; } xmlw.writeStartElement("othrStdyMat"); - writeFullElementList(xmlw, "relMat", relMaterials); - writeFullElementList(xmlw, "relStdy", relDatasets); + XmlWriterUtil.writeFullElementList(xmlw, "relMat", relMaterials); + XmlWriterUtil.writeFullElementList(xmlw, "relStdy", relDatasets); writeRelPublElement(xmlw, version); - writeFullElementList(xmlw, "othRefs", relReferences); + XmlWriterUtil.writeFullElementList(xmlw, "othRefs", relReferences); xmlw.writeEndElement(); //othrStdyMat } @@ -292,29 +294,29 @@ private static void writeDataAccess(XMLStreamWriter xmlw , DatasetVersionDTO ver xmlw.writeStartElement("dataAccs"); xmlw.writeStartElement("setAvail"); - writeFullElement(xmlw, "accsPlac", version.getDataAccessPlace()); - writeFullElement(xmlw, "origArch", version.getOriginalArchive()); - writeFullElement(xmlw, "avlStatus", version.getAvailabilityStatus()); - writeFullElement(xmlw, "collSize", version.getSizeOfCollection()); - writeFullElement(xmlw, "complete", version.getStudyCompletion()); + XmlWriterUtil.writeFullElement(xmlw, "accsPlac", version.getDataAccessPlace()); + XmlWriterUtil.writeFullElement(xmlw, "origArch", version.getOriginalArchive()); + XmlWriterUtil.writeFullElement(xmlw, "avlStatus", version.getAvailabilityStatus()); + XmlWriterUtil.writeFullElement(xmlw, "collSize", version.getSizeOfCollection()); + XmlWriterUtil.writeFullElement(xmlw, "complete", version.getStudyCompletion()); xmlw.writeEndElement(); //setAvail xmlw.writeStartElement("useStmt"); - writeFullElement(xmlw, "confDec", version.getConfidentialityDeclaration()); - writeFullElement(xmlw, "specPerm", version.getSpecialPermissions()); - writeFullElement(xmlw, "restrctn", version.getRestrictions()); - writeFullElement(xmlw, "contact", version.getContactForAccess()); - writeFullElement(xmlw, "citReq", version.getCitationRequirements()); - writeFullElement(xmlw, "deposReq", version.getDepositorRequirements()); - writeFullElement(xmlw, "conditions", version.getConditions()); - writeFullElement(xmlw, "disclaimer", version.getDisclaimer()); + XmlWriterUtil.writeFullElement(xmlw, "confDec", version.getConfidentialityDeclaration()); + XmlWriterUtil.writeFullElement(xmlw, "specPerm", version.getSpecialPermissions()); + XmlWriterUtil.writeFullElement(xmlw, "restrctn", version.getRestrictions()); + XmlWriterUtil.writeFullElement(xmlw, "contact", version.getContactForAccess()); + XmlWriterUtil.writeFullElement(xmlw, "citReq", version.getCitationRequirements()); + XmlWriterUtil.writeFullElement(xmlw, "deposReq", version.getDepositorRequirements()); + XmlWriterUtil.writeFullElement(xmlw, "conditions", version.getConditions()); + XmlWriterUtil.writeFullElement(xmlw, "disclaimer", version.getDisclaimer()); xmlw.writeEndElement(); //useStmt /* any s: */ if (version.getTermsOfAccess() != null && !version.getTermsOfAccess().trim().equals("")) { xmlw.writeStartElement("notes"); - writeAttribute(xmlw, "type", NOTE_TYPE_TERMS_OF_ACCESS); - writeAttribute(xmlw, "level", LEVEL_DV); + xmlw.writeAttribute("type", NOTE_TYPE_TERMS_OF_ACCESS); + xmlw.writeAttribute("level", LEVEL_DV); xmlw.writeCharacters(version.getTermsOfAccess()); xmlw.writeEndElement(); //notes } @@ -341,9 +343,9 @@ private static void writeDocDescElement (XMLStreamWriter xmlw, DatasetDTO datase xmlw.writeStartElement("docDscr"); xmlw.writeStartElement("citation"); xmlw.writeStartElement("titlStmt"); - writeFullElement(xmlw, "titl", dto2Primitive(version, DatasetFieldConstant.title), datasetDto.getMetadataLanguage()); + XmlWriterUtil.writeFullElement(xmlw, "titl", XmlWriterUtil.dto2Primitive(version, DatasetFieldConstant.title), datasetDto.getMetadataLanguage()); xmlw.writeStartElement("IDNo"); - writeAttribute(xmlw, "agency", persistentAgency); + XmlWriterUtil.writeAttribute(xmlw, "agency", persistentAgency); xmlw.writeCharacters(persistentProtocol + ":" + persistentAuthority + "/" + persistentId); xmlw.writeEndElement(); // IDNo xmlw.writeEndElement(); // titlStmt @@ -351,11 +353,11 @@ private static void writeDocDescElement (XMLStreamWriter xmlw, DatasetDTO datase //The doc is always published by the Dataverse Repository if (!StringUtils.isEmpty(datasetDto.getPublisher())) { xmlw.writeStartElement("distrbtr"); - writeAttribute(xmlw, "source", "archive"); + xmlw.writeAttribute("source", "archive"); xmlw.writeCharacters(datasetDto.getPublisher()); xmlw.writeEndElement(); // distrbtr } - writeFullElement(xmlw, "distDate", datasetDto.getPublicationDate()); + XmlWriterUtil.writeFullElement(xmlw, "distDate", datasetDto.getPublicationDate()); xmlw.writeEndElement(); // diststmt writeVersionStatement(xmlw, version); @@ -369,10 +371,10 @@ private static void writeDocDescElement (XMLStreamWriter xmlw, DatasetDTO datase private static void writeVersionStatement(XMLStreamWriter xmlw, DatasetVersionDTO datasetVersionDTO) throws XMLStreamException{ xmlw.writeStartElement("verStmt"); - writeAttribute(xmlw,"source","archive"); + xmlw.writeAttribute("source","archive"); xmlw.writeStartElement("version"); - writeAttribute(xmlw,"date", datasetVersionDTO.getReleaseTime().substring(0, 10)); - writeAttribute(xmlw,"type", datasetVersionDTO.getVersionState().toString()); + XmlWriterUtil.writeAttribute(xmlw,"date", datasetVersionDTO.getReleaseTime().substring(0, 10)); + XmlWriterUtil.writeAttribute(xmlw,"type", datasetVersionDTO.getVersionState().toString()); xmlw.writeCharacters(datasetVersionDTO.getVersionNumber().toString()); xmlw.writeEndElement(); // version xmlw.writeEndElement(); // verStmt @@ -523,14 +525,14 @@ private static void writeSummaryDescriptionElement(XMLStreamWriter xmlw, Dataset * "" entries, then all the "" ones: */ for (String nationEntry : nationList) { - writeFullElement(xmlw, "nation", nationEntry); + XmlWriterUtil.writeFullElement(xmlw, "nation", nationEntry); } for (String geogCoverEntry : geogCoverList) { - writeFullElement(xmlw, "geogCover", geogCoverEntry); + XmlWriterUtil.writeFullElement(xmlw, "geogCover", geogCoverEntry); } } - writeFullElementList(xmlw, "geogUnit", dto2PrimitiveList(datasetVersionDTO, DatasetFieldConstant.geographicUnit)); + XmlWriterUtil.writeFullElementList(xmlw, "geogUnit", dto2PrimitiveList(datasetVersionDTO, DatasetFieldConstant.geographicUnit)); /* Only 1 geoBndBox is allowed in the DDI. So, I'm just going to arbitrarily use the first one, and ignore the rest! -L.A. */ @@ -563,16 +565,16 @@ private static void writeSummaryDescriptionElement(XMLStreamWriter xmlw, Dataset */ if (geoBndBoxMap.get("westBL") != null) { - writeFullElement(xmlw, "westBL", geoBndBoxMap.get("westBL")); + XmlWriterUtil.writeFullElement(xmlw, "westBL", geoBndBoxMap.get("westBL")); } if (geoBndBoxMap.get("eastBL") != null) { - writeFullElement(xmlw, "eastBL", geoBndBoxMap.get("eastBL")); + XmlWriterUtil.writeFullElement(xmlw, "eastBL", geoBndBoxMap.get("eastBL")); } if (geoBndBoxMap.get("southBL") != null) { - writeFullElement(xmlw, "southBL", geoBndBoxMap.get("southBL")); + XmlWriterUtil.writeFullElement(xmlw, "southBL", geoBndBoxMap.get("southBL")); } if (geoBndBoxMap.get("northBL") != null) { - writeFullElement(xmlw, "northBL", geoBndBoxMap.get("northBL")); + XmlWriterUtil.writeFullElement(xmlw, "northBL", geoBndBoxMap.get("northBL")); } xmlw.writeEndElement(); @@ -580,7 +582,7 @@ private static void writeSummaryDescriptionElement(XMLStreamWriter xmlw, Dataset /* analyUnit: */ if (unitOfAnalysisDTO != null) { - writeI18NElementList(xmlw, "anlyUnit", unitOfAnalysisDTO.getMultipleVocab(), "unitOfAnalysis", unitOfAnalysisDTO.getTypeClass(), "socialscience", lang); + XmlWriterUtil.writeI18NElementList(xmlw, "anlyUnit", unitOfAnalysisDTO.getMultipleVocab(), "unitOfAnalysis", unitOfAnalysisDTO.getTypeClass(), "socialscience", lang); } @@ -600,16 +602,16 @@ private static void writeSummaryDescriptionElement(XMLStreamWriter xmlw, Dataset private static void writeMultipleElement(XMLStreamWriter xmlw, String element, FieldDTO fieldDTO, String lang) throws XMLStreamException { for (String value : fieldDTO.getMultiplePrimitive()) { //Write multiple lang vals for controlled vocab, otherwise don't include any lang tag - writeFullElement(xmlw, element, value, fieldDTO.isControlledVocabularyField() ? lang : null); + XmlWriterUtil.writeFullElement(xmlw, element, value, fieldDTO.isControlledVocabularyField() ? lang : null); } } private static void writeDateElement(XMLStreamWriter xmlw, String element, String cycle, String event, String dateIn) throws XMLStreamException { xmlw.writeStartElement(element); - writeAttribute(xmlw, "cycle", cycle); - writeAttribute(xmlw, "event", event); - writeAttribute(xmlw, "date", dateIn); + XmlWriterUtil.writeAttribute(xmlw, "cycle", cycle); + XmlWriterUtil.writeAttribute(xmlw, "event", event); + XmlWriterUtil.writeAttribute(xmlw, "date", dateIn); xmlw.writeCharacters(dateIn); xmlw.writeEndElement(); @@ -641,15 +643,15 @@ private static void writeDateElement(XMLStreamWriter xmlw, String element, Strin private static void writeMethodElement(XMLStreamWriter xmlw , DatasetVersionDTO version, String lang) throws XMLStreamException{ xmlw.writeStartElement("method"); xmlw.writeStartElement("dataColl"); - writeI18NElement(xmlw, "timeMeth", version, DatasetFieldConstant.timeMethod,lang); - writeI18NElement(xmlw, "dataCollector", version, DatasetFieldConstant.dataCollector, lang); - writeI18NElement(xmlw, "collectorTraining", version, DatasetFieldConstant.collectorTraining, lang); - writeI18NElement(xmlw, "frequenc", version, DatasetFieldConstant.frequencyOfDataCollection, lang); - writeI18NElement(xmlw, "sampProc", version, DatasetFieldConstant.samplingProcedure, lang); + XmlWriterUtil.writeI18NElement(xmlw, "timeMeth", version, DatasetFieldConstant.timeMethod,lang); + XmlWriterUtil.writeI18NElement(xmlw, "dataCollector", version, DatasetFieldConstant.dataCollector, lang); + XmlWriterUtil.writeI18NElement(xmlw, "collectorTraining", version, DatasetFieldConstant.collectorTraining, lang); + XmlWriterUtil.writeI18NElement(xmlw, "frequenc", version, DatasetFieldConstant.frequencyOfDataCollection, lang); + XmlWriterUtil.writeI18NElement(xmlw, "sampProc", version, DatasetFieldConstant.samplingProcedure, lang); writeTargetSampleElement(xmlw, version); - writeI18NElement(xmlw, "deviat", version, DatasetFieldConstant.deviationsFromSampleDesign, lang); + XmlWriterUtil.writeI18NElement(xmlw, "deviat", version, DatasetFieldConstant.deviationsFromSampleDesign, lang); /* comes before : */ FieldDTO collModeFieldDTO = dto2FieldDTO(version, DatasetFieldConstant.collectionMode, "socialscience"); @@ -658,37 +660,37 @@ private static void writeMethodElement(XMLStreamWriter xmlw , DatasetVersionDTO // Below is a backward compatibility check allowing export to work in // an instance where the metadata block has not been updated yet. if (collModeFieldDTO.getMultiple()) { - writeI18NElementList(xmlw, "collMode", collModeFieldDTO.getMultipleVocab(), DatasetFieldConstant.collectionMode, collModeFieldDTO.getTypeClass(), "socialscience", lang); + XmlWriterUtil.writeI18NElementList(xmlw, "collMode", collModeFieldDTO.getMultipleVocab(), DatasetFieldConstant.collectionMode, collModeFieldDTO.getTypeClass(), "socialscience", lang); } else { - writeI18NElement(xmlw, "collMode", version, DatasetFieldConstant.collectionMode, lang); + XmlWriterUtil.writeI18NElement(xmlw, "collMode", version, DatasetFieldConstant.collectionMode, lang); } } /* and so does : */ - writeI18NElement(xmlw, "resInstru", version, DatasetFieldConstant.researchInstrument, lang); + XmlWriterUtil.writeI18NElement(xmlw, "resInstru", version, DatasetFieldConstant.researchInstrument, lang); xmlw.writeStartElement("sources"); - writeFullElementList(xmlw, "dataSrc", dto2PrimitiveList(version, DatasetFieldConstant.dataSources)); - writeI18NElement(xmlw, "srcOrig", version, DatasetFieldConstant.originOfSources, lang); - writeI18NElement(xmlw, "srcChar", version, DatasetFieldConstant.characteristicOfSources, lang); - writeI18NElement(xmlw, "srcDocu", version, DatasetFieldConstant.accessToSources, lang); + XmlWriterUtil.writeFullElementList(xmlw, "dataSrc", dto2PrimitiveList(version, DatasetFieldConstant.dataSources)); + XmlWriterUtil.writeI18NElement(xmlw, "srcOrig", version, DatasetFieldConstant.originOfSources, lang); + XmlWriterUtil.writeI18NElement(xmlw, "srcChar", version, DatasetFieldConstant.characteristicOfSources, lang); + XmlWriterUtil.writeI18NElement(xmlw, "srcDocu", version, DatasetFieldConstant.accessToSources, lang); xmlw.writeEndElement(); //sources - writeI18NElement(xmlw, "collSitu", version, DatasetFieldConstant.dataCollectionSituation, lang); - writeI18NElement(xmlw, "actMin", version, DatasetFieldConstant.actionsToMinimizeLoss, lang); + XmlWriterUtil.writeI18NElement(xmlw, "collSitu", version, DatasetFieldConstant.dataCollectionSituation, lang); + XmlWriterUtil.writeI18NElement(xmlw, "actMin", version, DatasetFieldConstant.actionsToMinimizeLoss, lang); /* "" has the uppercase C: */ - writeI18NElement(xmlw, "ConOps", version, DatasetFieldConstant.controlOperations, lang); - writeI18NElement(xmlw, "weight", version, DatasetFieldConstant.weighting, lang); - writeI18NElement(xmlw, "cleanOps", version, DatasetFieldConstant.cleaningOperations, lang); + XmlWriterUtil.writeI18NElement(xmlw, "ConOps", version, DatasetFieldConstant.controlOperations, lang); + XmlWriterUtil.writeI18NElement(xmlw, "weight", version, DatasetFieldConstant.weighting, lang); + XmlWriterUtil.writeI18NElement(xmlw, "cleanOps", version, DatasetFieldConstant.cleaningOperations, lang); xmlw.writeEndElement(); //dataColl /* before : */ writeNotesElement(xmlw, version); xmlw.writeStartElement("anlyInfo"); - //writeFullElement(xmlw, "anylInfo", dto2Primitive(version, DatasetFieldConstant.datasetLevelErrorNotes)); - writeI18NElement(xmlw, "respRate", version, DatasetFieldConstant.responseRate, lang); - writeI18NElement(xmlw, "EstSmpErr", version, DatasetFieldConstant.samplingErrorEstimates, lang); - writeI18NElement(xmlw, "dataAppr", version, DatasetFieldConstant.otherDataAppraisal, lang); + //XmlWriterUtil.writeFullElement(xmlw, "anylInfo", dto2Primitive(version, DatasetFieldConstant.datasetLevelErrorNotes)); + XmlWriterUtil.writeI18NElement(xmlw, "respRate", version, DatasetFieldConstant.responseRate, lang); + XmlWriterUtil.writeI18NElement(xmlw, "EstSmpErr", version, DatasetFieldConstant.samplingErrorEstimates, lang); + XmlWriterUtil.writeI18NElement(xmlw, "dataAppr", version, DatasetFieldConstant.otherDataAppraisal, lang); xmlw.writeEndElement(); //anlyInfo xmlw.writeEndElement();//method @@ -705,7 +707,7 @@ private static void writeSubjectElement(XMLStreamWriter xmlw, DatasetVersionDTO if (CITATION_BLOCK_NAME.equals(key)) { for (FieldDTO fieldDTO : value.getFields()) { if (DatasetFieldConstant.subject.equals(fieldDTO.getTypeName())) { - writeI18NElementList(xmlw, "keyword", fieldDTO.getMultipleVocab(), "subject", + XmlWriterUtil.writeI18NElementList(xmlw, "keyword", fieldDTO.getMultipleVocab(), "subject", fieldDTO.getTypeClass(), "citation", lang); } @@ -732,14 +734,10 @@ private static void writeSubjectElement(XMLStreamWriter xmlw, DatasetVersionDTO } if (!keywordValue.isEmpty()) { xmlw.writeStartElement("keyword"); - if (!keywordVocab.isEmpty()) { - writeAttribute(xmlw, "vocab", keywordVocab); - } - if (!keywordURI.isEmpty()) { - writeAttribute(xmlw, "vocabURI", keywordURI); - } + XmlWriterUtil.writeAttribute(xmlw, "vocab", keywordVocab); + XmlWriterUtil.writeAttribute(xmlw, "vocabURI", keywordURI); if (lang != null && isCVV) { - writeAttribute(xmlw, "xml:lang", defaultLocale.getLanguage()); + XmlWriterUtil.writeAttribute(xmlw, "xml:lang", defaultLocale.getLanguage()); xmlw.writeCharacters(ControlledVocabularyValue.getLocaleStrValue(keywordValue, DatasetFieldConstant.keywordValue, CITATION_BLOCK_NAME, defaultLocale, true)); @@ -753,13 +751,9 @@ DatasetFieldConstant.keywordValue, CITATION_BLOCK_NAME, new Locale(lang), false); if (translatedValue != null) { xmlw.writeStartElement("keyword"); - if (!keywordVocab.isEmpty()) { - writeAttribute(xmlw, "vocab", keywordVocab); - } - if (!keywordURI.isEmpty()) { - writeAttribute(xmlw, "vocabURI", keywordURI); - } - writeAttribute(xmlw, "xml:lang", lang); + XmlWriterUtil.writeAttribute(xmlw, "vocab", keywordVocab); + XmlWriterUtil.writeAttribute(xmlw, "vocabURI", keywordURI); + XmlWriterUtil.writeAttribute(xmlw, "xml:lang", lang); xmlw.writeCharacters(translatedValue); xmlw.writeEndElement(); // Keyword } @@ -792,14 +786,10 @@ DatasetFieldConstant.keywordValue, CITATION_BLOCK_NAME, new Locale(lang), } if (!topicClassificationValue.isEmpty()) { xmlw.writeStartElement("topcClas"); - if (!topicClassificationVocab.isEmpty()) { - writeAttribute(xmlw, "vocab", topicClassificationVocab); - } - if (!topicClassificationURI.isEmpty()) { - writeAttribute(xmlw, "vocabURI", topicClassificationURI); - } + XmlWriterUtil.writeAttribute(xmlw, "vocab", topicClassificationVocab); + XmlWriterUtil.writeAttribute(xmlw, "vocabURI", topicClassificationURI); if (lang != null && isCVV) { - writeAttribute(xmlw, "xml:lang", defaultLocale.getLanguage()); + XmlWriterUtil.writeAttribute(xmlw, "xml:lang", defaultLocale.getLanguage()); xmlw.writeCharacters(ControlledVocabularyValue.getLocaleStrValue( topicClassificationValue, DatasetFieldConstant.topicClassValue, CITATION_BLOCK_NAME, defaultLocale, true)); @@ -813,13 +803,9 @@ DatasetFieldConstant.keywordValue, CITATION_BLOCK_NAME, new Locale(lang), CITATION_BLOCK_NAME, new Locale(lang), false); if (translatedValue != null) { xmlw.writeStartElement("topcClas"); - if (!topicClassificationVocab.isEmpty()) { - writeAttribute(xmlw, "vocab", topicClassificationVocab); - } - if (!topicClassificationURI.isEmpty()) { - writeAttribute(xmlw, "vocabURI", topicClassificationURI); - } - writeAttribute(xmlw, "xml:lang", lang); + XmlWriterUtil.writeAttribute(xmlw, "vocab", topicClassificationVocab); + XmlWriterUtil.writeAttribute(xmlw, "vocabURI", topicClassificationURI); + XmlWriterUtil.writeAttribute(xmlw, "xml:lang", lang); xmlw.writeCharacters(translatedValue); xmlw.writeEndElement(); // topcClas } @@ -856,9 +842,7 @@ private static void writeAuthorsElement(XMLStreamWriter xmlw, DatasetVersionDTO } if (!authorName.isEmpty()){ xmlw.writeStartElement("AuthEnty"); - if(!authorAffiliation.isEmpty()){ - writeAttribute(xmlw,"affiliation",authorAffiliation); - } + XmlWriterUtil.writeAttribute(xmlw,"affiliation",authorAffiliation); xmlw.writeCharacters(authorName); xmlw.writeEndElement(); //AuthEnty } @@ -879,9 +863,7 @@ private static void writeAuthorsElement(XMLStreamWriter xmlw, DatasetVersionDTO } if (!contributorName.isEmpty()){ xmlw.writeStartElement("othId"); - if(!contributorType.isEmpty()){ - writeAttribute(xmlw,"role", contributorType); - } + XmlWriterUtil.writeAttribute(xmlw,"role", contributorType); xmlw.writeCharacters(contributorName); xmlw.writeEndElement(); //othId } @@ -921,12 +903,8 @@ private static void writeContactsElement(XMLStreamWriter xmlw, DatasetVersionDTO // TODO: Since datasetContactEmail is a required field but datasetContactName is not consider not checking if datasetContactName is empty so we can write out datasetContactEmail. if (!datasetContactName.isEmpty()){ xmlw.writeStartElement("contact"); - if(!datasetContactAffiliation.isEmpty()){ - writeAttribute(xmlw,"affiliation",datasetContactAffiliation); - } - if(!datasetContactEmail.isEmpty()){ - writeAttribute(xmlw,"email",datasetContactEmail); - } + XmlWriterUtil.writeAttribute(xmlw,"affiliation",datasetContactAffiliation); + XmlWriterUtil.writeAttribute(xmlw,"email",datasetContactEmail); xmlw.writeCharacters(datasetContactName); xmlw.writeEndElement(); //AuthEnty } @@ -969,15 +947,9 @@ private static void writeProducersElement(XMLStreamWriter xmlw, DatasetVersionDT } if (!producerName.isEmpty()) { xmlw.writeStartElement("producer"); - if (!producerAffiliation.isEmpty()) { - writeAttribute(xmlw, "affiliation", producerAffiliation); - } - if (!producerAbbreviation.isEmpty()) { - writeAttribute(xmlw, "abbr", producerAbbreviation); - } - /*if (!producerLogo.isEmpty()) { - writeAttribute(xmlw, "role", producerLogo); - }*/ + XmlWriterUtil.writeAttribute(xmlw, "affiliation", producerAffiliation); + XmlWriterUtil.writeAttribute(xmlw, "abbr", producerAbbreviation); + //XmlWriterUtil.writeAttribute(xmlw, "role", producerLogo); xmlw.writeCharacters(producerName); xmlw.writeEndElement(); //AuthEnty } @@ -987,7 +959,7 @@ private static void writeProducersElement(XMLStreamWriter xmlw, DatasetVersionDT } } } - writeFullElement(xmlw, "prodDate", dto2Primitive(version, DatasetFieldConstant.productionDate)); + XmlWriterUtil.writeFullElement(xmlw, "prodDate", XmlWriterUtil.dto2Primitive(version, DatasetFieldConstant.productionDate)); // productionPlace was made multiple as of 5.14: // (a quick backward compatibility check was added to dto2PrimitiveList(), // see the method for details) @@ -1033,17 +1005,11 @@ private static void writeDistributorsElement(XMLStreamWriter xmlw, DatasetVersio if (!distributorName.isEmpty()) { xmlw.writeStartElement("distrbtr"); if(DvObjectContainer.isMetadataLanguageSet(lang)) { - writeAttribute(xmlw, "xml:lang", lang); - } - if (!distributorAffiliation.isEmpty()) { - writeAttribute(xmlw, "affiliation", distributorAffiliation); - } - if (!distributorAbbreviation.isEmpty()) { - writeAttribute(xmlw, "abbr", distributorAbbreviation); - } - if (!distributorURL.isEmpty()) { - writeAttribute(xmlw, "URI", distributorURL); + xmlw.writeAttribute("xml:lang", lang); } + XmlWriterUtil.writeAttribute(xmlw, "affiliation", distributorAffiliation); + XmlWriterUtil.writeAttribute(xmlw, "abbr", distributorAbbreviation); + XmlWriterUtil.writeAttribute(xmlw, "URI", distributorURL); xmlw.writeCharacters(distributorName); xmlw.writeEndElement(); //AuthEnty } @@ -1102,7 +1068,7 @@ private static void writeRelPublElement(XMLStreamWriter xmlw, DatasetVersionDTO (In other words - titlStmt is mandatory! -L.A.) */ xmlw.writeStartElement("titlStmt"); - writeFullElement(xmlw, "titl", citation); + XmlWriterUtil.writeFullElement(xmlw, "titl", citation); if (IDNo != null && !IDNo.trim().equals("")) { xmlw.writeStartElement("IDNo"); @@ -1115,7 +1081,7 @@ private static void writeRelPublElement(XMLStreamWriter xmlw, DatasetVersionDTO xmlw.writeEndElement(); // titlStmt - writeFullElement(xmlw,"biblCit",citation); + XmlWriterUtil.writeFullElement(xmlw,"biblCit",citation); xmlw.writeEndElement(); //citation if (url != null && !url.trim().equals("") ) { xmlw.writeStartElement("ExtLink"); @@ -1163,11 +1129,9 @@ private static void writeAbstractElement(XMLStreamWriter xmlw, DatasetVersionDTO } if (!descriptionText.isEmpty()){ xmlw.writeStartElement("abstract"); - if(!descriptionDate.isEmpty()){ - writeAttribute(xmlw,"date",descriptionDate); - } + XmlWriterUtil.writeAttribute(xmlw,"date",descriptionDate); if(DvObjectContainer.isMetadataLanguageSet(lang)) { - writeAttribute(xmlw, "xml:lang", lang); + xmlw.writeAttribute("xml:lang", lang); } xmlw.writeCharacters(descriptionText); xmlw.writeEndElement(); //abstract @@ -1200,9 +1164,7 @@ private static void writeGrantElement(XMLStreamWriter xmlw, DatasetVersionDTO da } if (!grantNumber.isEmpty()){ xmlw.writeStartElement("grantNo"); - if(!grantAgency.isEmpty()){ - writeAttribute(xmlw,"agency",grantAgency); - } + XmlWriterUtil.writeAttribute(xmlw,"agency",grantAgency); xmlw.writeCharacters(grantNumber); xmlw.writeEndElement(); //grantno } @@ -1234,9 +1196,7 @@ private static void writeOtherIdElement(XMLStreamWriter xmlw, DatasetVersionDTO } if (!otherId.isEmpty()){ xmlw.writeStartElement("IDNo"); - if(!otherIdAgency.isEmpty()){ - writeAttribute(xmlw,"agency",otherIdAgency); - } + XmlWriterUtil.writeAttribute(xmlw,"agency",otherIdAgency); xmlw.writeCharacters(otherId); xmlw.writeEndElement(); //IDNo } @@ -1268,9 +1228,7 @@ private static void writeSoftwareElement(XMLStreamWriter xmlw, DatasetVersionDTO } if (!softwareName.isEmpty()){ xmlw.writeStartElement("software"); - if(!softwareVersion.isEmpty()){ - writeAttribute(xmlw,"version",softwareVersion); - } + XmlWriterUtil.writeAttribute(xmlw,"version",softwareVersion); xmlw.writeCharacters(softwareName); xmlw.writeEndElement(); //software } @@ -1383,12 +1341,8 @@ private static void writeNotesElement(XMLStreamWriter xmlw, DatasetVersionDTO da } if (!notesText.isEmpty()) { xmlw.writeStartElement("notes"); - if(!notesType.isEmpty()){ - writeAttribute(xmlw,"type",notesType); - } - if(!notesSubject.isEmpty()){ - writeAttribute(xmlw,"subject",notesSubject); - } + XmlWriterUtil.writeAttribute(xmlw,"type",notesType); + XmlWriterUtil.writeAttribute(xmlw,"subject",notesSubject); xmlw.writeCharacters(notesText); xmlw.writeEndElement(); } @@ -1412,14 +1366,14 @@ private static void createOtherMats(XMLStreamWriter xmlw, List fileDtos // and observations, etc.) if (fileDTo.getDataFile().getDataTables() == null || fileDTo.getDataFile().getDataTables().isEmpty()) { xmlw.writeStartElement("otherMat"); - writeAttribute(xmlw, "ID", "f" + fileDTo.getDataFile().getId()); + XmlWriterUtil.writeAttribute(xmlw, "ID", "f" + fileDTo.getDataFile().getId()); String pidURL = fileDTo.getDataFile().getPidURL(); if (pidURL != null && !pidURL.isEmpty()){ - writeAttribute(xmlw, "URI", pidURL); + xmlw.writeAttribute("URI", pidURL); } else { - writeAttribute(xmlw, "URI", dataverseUrl + "/api/access/datafile/" + fileDTo.getDataFile().getId()); + xmlw.writeAttribute("URI", dataverseUrl + "/api/access/datafile/" + fileDTo.getDataFile().getId()); } - writeAttribute(xmlw, "level", "datafile"); + xmlw.writeAttribute("level", "datafile"); xmlw.writeStartElement("labl"); xmlw.writeCharacters(fileDTo.getDataFile().getFilename()); xmlw.writeEndElement(); // labl @@ -1430,9 +1384,9 @@ private static void createOtherMats(XMLStreamWriter xmlw, List fileDtos String contentType = fileDTo.getDataFile().getContentType(); if (!StringUtilisEmpty(contentType)) { xmlw.writeStartElement("notes"); - writeAttribute(xmlw, "level", LEVEL_FILE); - writeAttribute(xmlw, "type", NOTE_TYPE_CONTENTTYPE); - writeAttribute(xmlw, "subject", NOTE_SUBJECT_CONTENTTYPE); + xmlw.writeAttribute("level", LEVEL_FILE); + xmlw.writeAttribute("type", NOTE_TYPE_CONTENTTYPE); + xmlw.writeAttribute("subject", NOTE_SUBJECT_CONTENTTYPE); xmlw.writeCharacters(contentType); xmlw.writeEndElement(); // notes } @@ -1460,14 +1414,14 @@ private static void createOtherMatsFromFileMetadatas(XMLStreamWriter xmlw, JsonA // and observations, etc.) if (!fileJson.containsKey("dataTables")) { xmlw.writeStartElement("otherMat"); - writeAttribute(xmlw, "ID", "f" + fileJson.getJsonNumber(("id").toString())); + xmlw.writeAttribute("ID", "f" + fileJson.getJsonNumber(("id").toString())); if (fileJson.containsKey("pidUrl")){ - writeAttribute(xmlw, "URI", fileJson.getString("pidUrl")); + XmlWriterUtil.writeAttribute(xmlw, "URI", fileJson.getString("pidUrl")); } else { - writeAttribute(xmlw, "URI", dataverseUrl + "/api/access/datafile/" + fileJson.getJsonNumber("id").toString()); + xmlw.writeAttribute("URI", dataverseUrl + "/api/access/datafile/" + fileJson.getJsonNumber("id").toString()); } - writeAttribute(xmlw, "level", "datafile"); + xmlw.writeAttribute("level", "datafile"); xmlw.writeStartElement("labl"); xmlw.writeCharacters(fileJson.getString("filename")); xmlw.writeEndElement(); // labl @@ -1482,9 +1436,9 @@ private static void createOtherMatsFromFileMetadatas(XMLStreamWriter xmlw, JsonA // specially formatted notes section: if (fileJson.containsKey("contentType")) { xmlw.writeStartElement("notes"); - writeAttribute(xmlw, "level", LEVEL_FILE); - writeAttribute(xmlw, "type", NOTE_TYPE_CONTENTTYPE); - writeAttribute(xmlw, "subject", NOTE_SUBJECT_CONTENTTYPE); + xmlw.writeAttribute("level", LEVEL_FILE); + xmlw.writeAttribute("type", NOTE_TYPE_CONTENTTYPE); + xmlw.writeAttribute("subject", NOTE_SUBJECT_CONTENTTYPE); xmlw.writeCharacters(fileJson.getString("contentType")); xmlw.writeEndElement(); // notes } @@ -1502,33 +1456,7 @@ private static void writeFileDescription(XMLStreamWriter xmlw, FileDTO fileDTo) xmlw.writeEndElement(); // txt } - private static String dto2Primitive(DatasetVersionDTO datasetVersionDTO, String datasetFieldTypeName) { - for (Map.Entry entry : datasetVersionDTO.getMetadataBlocks().entrySet()) { - MetadataBlockDTO value = entry.getValue(); - for (FieldDTO fieldDTO : value.getFields()) { - if (datasetFieldTypeName.equals(fieldDTO.getTypeName())) { - return fieldDTO.getSinglePrimitive(); - } - } - } - return null; - } - - private static String dto2Primitive(DatasetVersionDTO datasetVersionDTO, String datasetFieldTypeName, Locale locale) { - for (Map.Entry entry : datasetVersionDTO.getMetadataBlocks().entrySet()) { - MetadataBlockDTO value = entry.getValue(); - for (FieldDTO fieldDTO : value.getFields()) { - if (datasetFieldTypeName.equals(fieldDTO.getTypeName())) { - String rawVal = fieldDTO.getSinglePrimitive(); - if (fieldDTO.isControlledVocabularyField()) { - return ControlledVocabularyValue.getLocaleStrValue(rawVal, datasetFieldTypeName, value.getName(), - locale, false); - } - } - } - } - return null; - } + private static List dto2PrimitiveList(DatasetVersionDTO datasetVersionDTO, String datasetFieldTypeName) { for (Map.Entry entry : datasetVersionDTO.getMetadataBlocks().entrySet()) { @@ -1562,104 +1490,6 @@ private static FieldDTO dto2FieldDTO(DatasetVersionDTO datasetVersionDTO, String return null; } - private static void writeFullElementList(XMLStreamWriter xmlw, String name, List values) throws XMLStreamException { - //For the simplest Elements we can - if (values != null && !values.isEmpty()) { - for (String value : values) { - xmlw.writeStartElement(name); - xmlw.writeCharacters(value); - xmlw.writeEndElement(); // labl - } - } - } - - private static void writeI18NElementList(XMLStreamWriter xmlw, String name, List values, - String fieldTypeName, String fieldTypeClass, String metadataBlockName, String lang) - throws XMLStreamException { - - if (values != null && !values.isEmpty()) { - Locale defaultLocale = Locale.getDefault(); - for (String value : values) { - if (fieldTypeClass.equals("controlledVocabulary")) { - String localeVal = ControlledVocabularyValue.getLocaleStrValue(value, fieldTypeName, metadataBlockName, defaultLocale, false); - if (localeVal != null) { - - value = localeVal; - writeFullElement(xmlw, name, value, defaultLocale.getLanguage()); - } else { - writeFullElement(xmlw, name, value); - } - } else { - writeFullElement(xmlw, name, value); - } - } - if (lang != null && !defaultLocale.getLanguage().equals(lang)) { - // Get values in dataset metadata language - // Loop before testing fieldTypeClass to be ready for external CVV - for (String value : values) { - if (fieldTypeClass.equals("controlledVocabulary")) { - String localeVal = ControlledVocabularyValue.getLocaleStrValue(value, fieldTypeName, metadataBlockName, new Locale(lang), false); - if (localeVal != null) { - writeFullElement(xmlw, name, localeVal, lang); - } - } - } - } - } - } - - private static void writeI18NElement(XMLStreamWriter xmlw, String name, DatasetVersionDTO version, - String fieldTypeName, String lang) throws XMLStreamException { - // Get the default value - String val = dto2Primitive(version, fieldTypeName); - Locale defaultLocale = Locale.getDefault(); - // Get the language-specific value for the default language - // A null value is returned if this is not a CVV field - String localeVal = dto2Primitive(version, fieldTypeName, defaultLocale); - String requestedLocaleVal = null; - if (lang != null && localeVal != null && !defaultLocale.getLanguage().equals(lang)) { - // Also get the value in the requested locale/lang if that's not the default - // lang. - requestedLocaleVal = dto2Primitive(version, fieldTypeName, new Locale(lang)); - } - // FWIW locale-specific vals will only be non-null for CVV values (at present) - if (localeVal == null && requestedLocaleVal == null) { - // Not CVV/no translations so print without lang tag - writeFullElement(xmlw, name, val); - } else { - // Print in either/both languages if we have values - if (localeVal != null) { - // Print the value for the default locale with it's own lang tag - writeFullElement(xmlw, name, localeVal, defaultLocale.getLanguage()); - } - // Also print in the request lang (i.e. the metadata language for the dataset) if a value exists, print it with a lang tag - if (requestedLocaleVal != null) { - writeFullElement(xmlw, name, requestedLocaleVal, lang); - } - } - } - - private static void writeFullElement(XMLStreamWriter xmlw, String name, String value) throws XMLStreamException { - writeFullElement(xmlw, name, value, null); - } - - private static void writeFullElement (XMLStreamWriter xmlw, String name, String value, String lang) throws XMLStreamException { - //For the simplest Elements we can - if (!StringUtilisEmpty(value)) { - xmlw.writeStartElement(name); - if(DvObjectContainer.isMetadataLanguageSet(lang)) { - writeAttribute(xmlw, "xml:lang", lang); - } - xmlw.writeCharacters(value); - xmlw.writeEndElement(); // labl - } - } - - private static void writeAttribute(XMLStreamWriter xmlw, String name, String value) throws XMLStreamException { - if (!StringUtilisEmpty(value)) { - xmlw.writeAttribute(name, value); - } - } private static boolean StringUtilisEmpty(String str) { if (str == null || str.trim().equals("")) { @@ -1747,14 +1577,14 @@ public static void createDataDscr(XMLStreamWriter xmlw, JsonArray fileDetails) t } private static void createVarGroupDDI(XMLStreamWriter xmlw, JsonObject varGrp) throws XMLStreamException { xmlw.writeStartElement("varGrp"); - writeAttribute(xmlw, "ID", "VG" + varGrp.getJsonNumber("id").toString()); + xmlw.writeAttribute("ID", "VG" + varGrp.getJsonNumber("id").toString()); String vars = ""; JsonArray varsInGroup = varGrp.getJsonArray("dataVariableIds"); for (int j=0;j sumStat : dvar.getJsonObject("summaryStatistics").entrySet()) { xmlw.writeStartElement("sumStat"); - writeAttribute(xmlw, "type", sumStat.getKey()); + XmlWriterUtil.writeAttribute(xmlw, "type", sumStat.getKey()); xmlw.writeCharacters(((JsonString)sumStat.getValue()).getString()); xmlw.writeEndElement(); // sumStat } @@ -1917,7 +1747,7 @@ private static void createVarDDI(XMLStreamWriter xmlw, JsonObject dvar, String f JsonObject varCat = varCats.getJsonObject(i); xmlw.writeStartElement("catgry"); if (varCat.getBoolean("isMissing")) { - writeAttribute(xmlw, "missing", "Y"); + xmlw.writeAttribute("missing", "Y"); } // catValu @@ -1928,7 +1758,7 @@ private static void createVarDDI(XMLStreamWriter xmlw, JsonObject dvar, String f // label if (varCat.containsKey("label")) { xmlw.writeStartElement("labl"); - writeAttribute(xmlw, "level", "category"); + xmlw.writeAttribute("level", "category"); xmlw.writeCharacters(varCat.getString("label")); xmlw.writeEndElement(); // labl } @@ -1936,7 +1766,7 @@ private static void createVarDDI(XMLStreamWriter xmlw, JsonObject dvar, String f // catStat if (varCat.containsKey("frequency")) { xmlw.writeStartElement("catStat"); - writeAttribute(xmlw, "type", "freq"); + xmlw.writeAttribute("type", "freq"); Double freq = varCat.getJsonNumber("frequency").doubleValue(); // if frequency is actually a long value, we want to write "100" instead of // "100.0" @@ -1955,8 +1785,8 @@ private static void createVarDDI(XMLStreamWriter xmlw, JsonObject dvar, String f JsonObject cm = catMetas.getJsonObject(j); if (cm.getString("categoryValue").equals(varCat.getString("value"))) { xmlw.writeStartElement("catStat"); - writeAttribute(xmlw, "wgtd", "wgtd"); - writeAttribute(xmlw, "type", "freq"); + xmlw.writeAttribute("wgtd", "wgtd"); + xmlw.writeAttribute("type", "freq"); xmlw.writeCharacters(cm.getJsonNumber("wFreq").toString()); xmlw.writeEndElement(); // catStat break; @@ -1972,24 +1802,24 @@ private static void createVarDDI(XMLStreamWriter xmlw, JsonObject dvar, String f // varFormat xmlw.writeEmptyElement("varFormat"); if(dvar.containsKey("variableFormatType")) { - writeAttribute(xmlw, "type", dvar.getString("variableFormatType").toLowerCase()); + XmlWriterUtil.writeAttribute(xmlw, "type", dvar.getString("variableFormatType").toLowerCase()); } else { throw new XMLStreamException("Illegal Variable Format Type!"); } if(dvar.containsKey("format")) { - writeAttribute(xmlw, "formatname", dvar.getString("format")); + XmlWriterUtil.writeAttribute(xmlw, "formatname", dvar.getString("format")); } //experiment writeAttribute(xmlw, "schema", dv.getFormatSchema()); if(dvar.containsKey("formatCategory")) { - writeAttribute(xmlw, "category", dvar.getString("formatCategory")); + XmlWriterUtil.writeAttribute(xmlw, "category", dvar.getString("formatCategory")); } // notes if (dvar.containsKey("UNF") && !dvar.getString("UNF").isBlank()) { xmlw.writeStartElement("notes"); - writeAttribute(xmlw, "subject", "Universal Numeric Fingerprint"); - writeAttribute(xmlw, "level", "variable"); - writeAttribute(xmlw, "type", "Dataverse:UNF"); + xmlw.writeAttribute("subject", "Universal Numeric Fingerprint"); + xmlw.writeAttribute("level", "variable"); + xmlw.writeAttribute("type", "Dataverse:UNF"); xmlw.writeCharacters(dvar.getString("UNF")); xmlw.writeEndElement(); //notes } @@ -2020,8 +1850,8 @@ private static void createFileDscr(XMLStreamWriter xmlw, JsonArray fileDetails) } xmlw.writeStartElement("fileDscr"); String fileId = fileJson.getJsonNumber("id").toString(); - writeAttribute(xmlw, "ID", "f" + fileId); - writeAttribute(xmlw, "URI", dataverseUrl + "/api/access/datafile/" + fileId); + xmlw.writeAttribute("ID", "f" + fileId); + xmlw.writeAttribute("URI", dataverseUrl + "/api/access/datafile/" + fileId); xmlw.writeStartElement("fileTxt"); xmlw.writeStartElement("fileName"); @@ -2064,9 +1894,9 @@ private static void createFileDscr(XMLStreamWriter xmlw, JsonArray fileDetails) // (Universal Numeric Fingerprint) signature: if ((dt!=null) && (dt.containsKey("UNF") && !dt.getString("UNF").isBlank())) { xmlw.writeStartElement("notes"); - writeAttribute(xmlw, "level", LEVEL_FILE); - writeAttribute(xmlw, "type", NOTE_TYPE_UNF); - writeAttribute(xmlw, "subject", NOTE_SUBJECT_UNF); + xmlw.writeAttribute("level", LEVEL_FILE); + xmlw.writeAttribute("type", NOTE_TYPE_UNF); + xmlw.writeAttribute("subject", NOTE_SUBJECT_UNF); xmlw.writeCharacters(dt.getString("UNF")); xmlw.writeEndElement(); // notes } @@ -2075,9 +1905,9 @@ private static void createFileDscr(XMLStreamWriter xmlw, JsonArray fileDetails) JsonArray tags = fileJson.getJsonArray("tabularTags"); for (int j = 0; j < tags.size(); j++) { xmlw.writeStartElement("notes"); - writeAttribute(xmlw, "level", LEVEL_FILE); - writeAttribute(xmlw, "type", NOTE_TYPE_TAG); - writeAttribute(xmlw, "subject", NOTE_SUBJECT_TAG); + xmlw.writeAttribute("level", LEVEL_FILE); + xmlw.writeAttribute("type", NOTE_TYPE_TAG); + xmlw.writeAttribute("subject", NOTE_SUBJECT_TAG); xmlw.writeCharacters(tags.getString(j)); xmlw.writeEndElement(); // notes } @@ -2091,13 +1921,7 @@ private static void createFileDscr(XMLStreamWriter xmlw, JsonArray fileDetails) - private static boolean checkParentElement(XMLStreamWriter xmlw, String elementName, boolean elementAdded) throws XMLStreamException { - if (!elementAdded) { - xmlw.writeStartElement(elementName); - } - return true; - } public static void datasetHtmlDDI(InputStream datafile, OutputStream outputStream) throws XMLStreamException { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); diff --git a/src/main/java/edu/harvard/iq/dataverse/export/dublincore/DublinCoreExportUtil.java b/src/main/java/edu/harvard/iq/dataverse/export/dublincore/DublinCoreExportUtil.java index 6b7cb844f3e..d201801bc45 100644 --- a/src/main/java/edu/harvard/iq/dataverse/export/dublincore/DublinCoreExportUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/export/dublincore/DublinCoreExportUtil.java @@ -7,6 +7,8 @@ import com.google.gson.Gson; import edu.harvard.iq.dataverse.DatasetFieldConstant; +import edu.harvard.iq.dataverse.DatasetFieldType; +import edu.harvard.iq.dataverse.DatasetServiceBean; import edu.harvard.iq.dataverse.GlobalId; import edu.harvard.iq.dataverse.api.dto.DatasetDTO; import edu.harvard.iq.dataverse.api.dto.DatasetVersionDTO; @@ -28,6 +30,8 @@ import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; +import org.apache.commons.lang3.StringUtils; + /** * * @author skraffmi @@ -176,11 +180,24 @@ private static void createOAIDC(XMLStreamWriter xmlw, DatasetDTO datasetDto, Str writeFullElementList(xmlw, dcFlavor+":"+"language", dto2PrimitiveList(version, DatasetFieldConstant.language)); - String date = dto2Primitive(version, DatasetFieldConstant.productionDate); - if (date == null) { - date = datasetDto.getPublicationDate(); + /** + * dc:date. "I suggest changing the Dataverse / DC Element (oai_dc) + * mapping, so that dc:date is mapped with Publication Date. This is + * also in line with citation recommendations. The publication date is + * the preferred date when citing research data; see, e.g., page 12 in + * The Tromsø Recommendations for Citation of Research Data in + * Linguistics; https://doi.org/10.15497/rda00040 ." -- + * https://github.com/IQSS/dataverse/issues/8129 + * + * However, if the citation date field has been set, use that. + */ + String date = datasetDto.getPublicationDate(); + DatasetFieldType citationDataType = jakarta.enterprise.inject.spi.CDI.current().select(DatasetServiceBean.class).get().findByGlobalId(globalId.asString()).getCitationDateDatasetFieldType(); + if (citationDataType != null) { + date = dto2Primitive(version, citationDataType.getName()); } - writeFullElement(xmlw, dcFlavor+":"+"date", date); + + writeFullElement(xmlw, dcFlavor+":"+"date", date); writeFullElement(xmlw, dcFlavor+":"+"contributor", dto2Primitive(version, DatasetFieldConstant.depositor)); @@ -188,10 +205,16 @@ private static void createOAIDC(XMLStreamWriter xmlw, DatasetDTO datasetDto, Str writeFullElementList(xmlw, dcFlavor+":"+"relation", dto2PrimitiveList(version, DatasetFieldConstant.relatedDatasets)); - writeFullElementList(xmlw, dcFlavor+":"+"type", dto2PrimitiveList(version, DatasetFieldConstant.kindOfData)); + /** + * dc:type. "Dublin Core (see + * https://www.dublincore.org/specifications/dublin-core/dcmi-terms/#http://purl.org/dc/terms/type + * ) recommends “to use a controlled vocabulary such as the DCMI Type + * Vocabulary” for dc:type." So we hard-coded it to "Dataset". See + * https://github.com/IQSS/dataverse/issues/8129 + */ + writeFullElement(xmlw, dcFlavor+":"+"type", "Dataset"); writeFullElementList(xmlw, dcFlavor+":"+"source", dto2PrimitiveList(version, DatasetFieldConstant.dataSources)); - } @@ -301,26 +324,35 @@ private static void writeRelPublElement(XMLStreamWriter xmlw, DatasetVersionDTO String IDType = ""; String IDNo = ""; String url = ""; + String relationType = null; for (Iterator iterator = foo.iterator(); iterator.hasNext();) { FieldDTO next = iterator.next(); - if (DatasetFieldConstant.publicationCitation.equals(next.getTypeName())) { - citation = next.getSinglePrimitive(); - } - if (DatasetFieldConstant.publicationIDType.equals(next.getTypeName())) { - IDType = next.getSinglePrimitive(); - } - if (DatasetFieldConstant.publicationIDNumber.equals(next.getTypeName())) { - IDNo = next.getSinglePrimitive(); - } - if (DatasetFieldConstant.publicationURL.equals(next.getTypeName())) { - url = next.getSinglePrimitive(); + switch (next.getTypeName()) { + case DatasetFieldConstant.publicationCitation: + citation = next.getSinglePrimitive(); + break; + case DatasetFieldConstant.publicationIDType: + IDType = next.getSinglePrimitive(); + break; + case DatasetFieldConstant.publicationIDNumber: + IDNo = next.getSinglePrimitive(); + break; + case DatasetFieldConstant.publicationURL: + url = next.getSinglePrimitive(); + break; + case DatasetFieldConstant.publicationRelationType: + relationType = next.getSinglePrimitive(); + break; } } + if(StringUtils.isBlank(relationType)) { + relationType = "isReferencedBy"; + } pubString = appendCommaSeparatedValue(citation, IDType); pubString = appendCommaSeparatedValue(pubString, IDNo); pubString = appendCommaSeparatedValue(pubString, url); if (!pubString.isEmpty()){ - xmlw.writeStartElement(dcFlavor+":"+"isReferencedBy"); + xmlw.writeStartElement(dcFlavor+":" + relationType); xmlw.writeCharacters(pubString); xmlw.writeEndElement(); //relPubl } diff --git a/src/main/java/edu/harvard/iq/dataverse/export/openaire/OpenAireExportUtil.java b/src/main/java/edu/harvard/iq/dataverse/export/openaire/OpenAireExportUtil.java index 7b0a92a4372..dd01750942d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/export/openaire/OpenAireExportUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/export/openaire/OpenAireExportUtil.java @@ -13,16 +13,16 @@ import com.google.gson.Gson; -import edu.harvard.iq.dataverse.DOIServiceBean; import edu.harvard.iq.dataverse.DatasetFieldConstant; import edu.harvard.iq.dataverse.GlobalId; -import edu.harvard.iq.dataverse.HandlenetServiceBean; import edu.harvard.iq.dataverse.api.dto.DatasetDTO; import edu.harvard.iq.dataverse.api.dto.DatasetVersionDTO; import edu.harvard.iq.dataverse.api.dto.FieldDTO; import edu.harvard.iq.dataverse.api.dto.MetadataBlockDTO; import edu.harvard.iq.dataverse.util.PersonOrOrgUtil; import edu.harvard.iq.dataverse.pidproviders.PidUtil; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; +import edu.harvard.iq.dataverse.pidproviders.handle.HandlePidProvider; import edu.harvard.iq.dataverse.util.json.JsonUtil; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -189,10 +189,10 @@ public static void writeIdentifierElement(XMLStreamWriter xmlw, String identifie if (StringUtils.isNotBlank(identifier)) { Map identifier_map = new HashMap(); - if (StringUtils.containsIgnoreCase(identifier, DOIServiceBean.DOI_RESOLVER_URL)) { + if (StringUtils.containsIgnoreCase(identifier, AbstractDOIProvider.DOI_RESOLVER_URL)) { identifier_map.put("identifierType", "DOI"); identifier = StringUtils.substring(identifier, identifier.indexOf("10.")); - } else if (StringUtils.containsIgnoreCase(identifier, HandlenetServiceBean.HDL_RESOLVER_URL)) { + } else if (StringUtils.containsIgnoreCase(identifier, HandlePidProvider.HDL_RESOLVER_URL)) { identifier_map.put("identifierType", "Handle"); if (StringUtils.contains(identifier, "http")) { identifier = identifier.replace(identifier.substring(0, identifier.indexOf("/") + 2), ""); @@ -437,7 +437,7 @@ public static void writeSubjectsElement(XMLStreamWriter xmlw, DatasetVersionDTO for (String subject : fieldDTO.getMultipleVocab()) { if (StringUtils.isNotBlank(subject)) { subject_check = writeOpenTag(xmlw, "subjects", subject_check); - writeSubjectElement(xmlw, null, null, subject, language); + writeSubjectElement(xmlw, null, null, null, subject, language); } } } @@ -446,7 +446,8 @@ public static void writeSubjectsElement(XMLStreamWriter xmlw, DatasetVersionDTO for (HashSet fieldDTOs : fieldDTO.getMultipleCompound()) { String subject = null; String subjectScheme = null; - String schemeURI = null; + String keywordTermURI = null; + String keywordVocabURI = null; for (Iterator iterator = fieldDTOs.iterator(); iterator.hasNext();) { FieldDTO next = iterator.next(); @@ -454,18 +455,22 @@ public static void writeSubjectsElement(XMLStreamWriter xmlw, DatasetVersionDTO subject = next.getSinglePrimitive(); } + if (DatasetFieldConstant.keywordTermURI.equals(next.getTypeName())) { + keywordTermURI = next.getSinglePrimitive(); + } + if (DatasetFieldConstant.keywordVocab.equals(next.getTypeName())) { subjectScheme = next.getSinglePrimitive(); } - + if (DatasetFieldConstant.keywordVocabURI.equals(next.getTypeName())) { - schemeURI = next.getSinglePrimitive(); + keywordVocabURI = next.getSinglePrimitive(); } } if (StringUtils.isNotBlank(subject)) { subject_check = writeOpenTag(xmlw, "subjects", subject_check); - writeSubjectElement(xmlw, subjectScheme, schemeURI, subject, language); + writeSubjectElement(xmlw, subjectScheme, keywordTermURI, keywordVocabURI, subject, language); } } } @@ -493,7 +498,7 @@ public static void writeSubjectsElement(XMLStreamWriter xmlw, DatasetVersionDTO if (StringUtils.isNotBlank(subject)) { subject_check = writeOpenTag(xmlw, "subjects", subject_check); - writeSubjectElement(xmlw, subjectScheme, schemeURI, subject, language); + writeSubjectElement(xmlw, subjectScheme, null, schemeURI, subject, language); } } } @@ -513,7 +518,7 @@ public static void writeSubjectsElement(XMLStreamWriter xmlw, DatasetVersionDTO * @param language * @throws XMLStreamException */ - private static void writeSubjectElement(XMLStreamWriter xmlw, String subjectScheme, String schemeURI, String value, String language) throws XMLStreamException { + private static void writeSubjectElement(XMLStreamWriter xmlw, String subjectScheme, String valueURI, String schemeURI, String value, String language) throws XMLStreamException { // write a subject Map subject_map = new HashMap(); @@ -524,6 +529,9 @@ private static void writeSubjectElement(XMLStreamWriter xmlw, String subjectSche if (StringUtils.isNotBlank(subjectScheme)) { subject_map.put("subjectScheme", subjectScheme); } + if (StringUtils.isNotBlank(valueURI)) { + subject_map.put("valueURI", valueURI); + } if (StringUtils.isNotBlank(schemeURI)) { subject_map.put("schemeURI", schemeURI); } @@ -924,6 +932,7 @@ public static void writeRelatedIdentifierElement(XMLStreamWriter xmlw, DatasetVe String relatedIdentifierType = null; String relatedIdentifier = null; // is used when relatedIdentifierType variable is not URL String relatedURL = null; // is used when relatedIdentifierType variable is URL + String relationType = null; // is used when relatedIdentifierType variable is URL for (Iterator iterator = fieldDTOs.iterator(); iterator.hasNext();) { FieldDTO next = iterator.next(); @@ -936,6 +945,9 @@ public static void writeRelatedIdentifierElement(XMLStreamWriter xmlw, DatasetVe if (DatasetFieldConstant.publicationURL.equals(next.getTypeName())) { relatedURL = next.getSinglePrimitive(); } + if (DatasetFieldConstant.publicationRelationType.equals(next.getTypeName())) { + relationType = next.getSinglePrimitive(); + } } if (StringUtils.isNotBlank(relatedIdentifierType)) { @@ -948,7 +960,10 @@ public static void writeRelatedIdentifierElement(XMLStreamWriter xmlw, DatasetVe } relatedIdentifier_map.put("relatedIdentifierType", relatedIdentifierType); - relatedIdentifier_map.put("relationType", "IsCitedBy"); + if(relationType== null) { + relationType = "IsCitedBy"; + } + relatedIdentifier_map.put("relationType", relationType); if (StringUtils.containsIgnoreCase(relatedIdentifierType, "url")) { writeFullElement(xmlw, null, "relatedIdentifier", relatedIdentifier_map, relatedURL, language); @@ -1428,6 +1443,8 @@ public static void writeFundingReferencesElement(XMLStreamWriter xmlw, DatasetVe writeEndTag(xmlw, fundingReference_check); } + + //Duplicates XmlWriterUtil.dto2Primitive private static String dto2Primitive(DatasetVersionDTO datasetVersionDTO, String datasetFieldTypeName) { // give the single value of the given metadata for (Map.Entry entry : datasetVersionDTO.getMetadataBlocks().entrySet()) { diff --git a/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalTool.java b/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalTool.java index 7f1f46c06cb..c103f6981e1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalTool.java +++ b/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalTool.java @@ -40,6 +40,7 @@ public class ExternalTool implements Serializable { public static final String TOOL_NAME = "toolName"; public static final String ALLOWED_API_CALLS = "allowedApiCalls"; public static final String REQUIREMENTS = "requirements"; + public static final String AUX_FILES_EXIST = "auxFilesExist"; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -362,4 +363,9 @@ public void setRequirements(String requirements) { this.requirements = requirements; } + public boolean accessesAuxFiles() { + String reqs = getRequirements(); + return reqs!=null && reqs.contains(AUX_FILES_EXIST); + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolServiceBean.java index e13843eadfa..5ee0bf1355d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolServiceBean.java @@ -5,6 +5,8 @@ import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.DataFileServiceBean; import edu.harvard.iq.dataverse.authorization.users.ApiToken; +import edu.harvard.iq.dataverse.dataaccess.DataAccess; +import edu.harvard.iq.dataverse.dataaccess.StorageIO; import edu.harvard.iq.dataverse.externaltools.ExternalTool.Type; import edu.harvard.iq.dataverse.util.URLTokenUtil.ReservedWord; import edu.harvard.iq.dataverse.util.json.JsonUtil; @@ -141,9 +143,10 @@ public List findExternalToolsByFile(List allExternal List externalTools = new ArrayList<>(); //Map tabular data to it's mimetype (the isTabularData() check assures that this code works the same as before, but it may need to change if tabular data is split into subtypes with differing mimetypes) final String contentType = file.isTabularData() ? DataFileServiceBean.MIME_TYPE_TSV_ALT : file.getContentType(); + boolean isAccessible = StorageIO.isDataverseAccessible(DataAccess.getStorageDriverFromIdentifier(file.getStorageIdentifier())); allExternalTools.forEach((externalTool) -> { //Match tool and file type, then check requirements - if (contentType.equals(externalTool.getContentType()) && meetsRequirements(externalTool, file)) { + if (contentType.equals(externalTool.getContentType()) && meetsRequirements(externalTool, file) && (isAccessible || externalTool.accessesAuxFiles())) { externalTools.add(externalTool); } }); @@ -159,7 +162,7 @@ public boolean meetsRequirements(ExternalTool externalTool, DataFile dataFile) { } boolean meetsRequirements = true; JsonObject requirementsObj = JsonUtil.getJsonObject(requirements); - JsonArray auxFilesExist = requirementsObj.getJsonArray("auxFilesExist"); + JsonArray auxFilesExist = requirementsObj.getJsonArray(ExternalTool.AUX_FILES_EXIST); for (JsonValue jsonValue : auxFilesExist) { String formatTag = jsonValue.asJsonObject().getString("formatTag"); String formatVersion = jsonValue.asJsonObject().getString("formatVersion"); diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java index 3e60441850b..ac3c81622fc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -22,7 +22,6 @@ import jakarta.json.JsonString; import jakarta.json.JsonValue.ValueType; import jakarta.json.stream.JsonParsingException; -import jakarta.servlet.http.HttpServletRequest; import jakarta.ws.rs.HttpMethod; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; @@ -33,7 +32,6 @@ import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; -import java.net.URLEncoder; import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.time.Duration; @@ -53,6 +51,7 @@ import org.primefaces.PrimeFaces; import com.google.gson.Gson; +import edu.harvard.iq.dataverse.api.ApiConstants; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.users.ApiToken; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; @@ -61,15 +60,26 @@ import edu.harvard.iq.dataverse.dataaccess.DataAccess; import edu.harvard.iq.dataverse.dataaccess.GlobusAccessibleStore; import edu.harvard.iq.dataverse.dataaccess.StorageIO; +import edu.harvard.iq.dataverse.datasetutility.AddReplaceFileHelper; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.ingest.IngestServiceBean; import edu.harvard.iq.dataverse.privateurl.PrivateUrl; import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; +import edu.harvard.iq.dataverse.settings.FeatureFlags; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.FileUtil; +import edu.harvard.iq.dataverse.util.StringUtil; import edu.harvard.iq.dataverse.util.SystemConfig; import edu.harvard.iq.dataverse.util.URLTokenUtil; import edu.harvard.iq.dataverse.util.UrlSignerUtil; import edu.harvard.iq.dataverse.util.json.JsonUtil; +import jakarta.json.JsonReader; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.core.Response; +import org.apache.http.util.EntityUtils; @Stateless @Named("GlobusServiceBean") @@ -81,6 +91,8 @@ public class GlobusServiceBean implements java.io.Serializable { protected SettingsServiceBean settingsSvc; @Inject DataverseSession session; + @Inject + DataverseRequestServiceBean dataverseRequestSvc; @EJB protected AuthenticationServiceBean authSvc; @EJB @@ -92,7 +104,15 @@ public class GlobusServiceBean implements java.io.Serializable { @EJB FileDownloadServiceBean fileDownloadService; @EJB - DataFileServiceBean dataFileService; + DataFileServiceBean dataFileSvc; + @EJB + PermissionServiceBean permissionSvc; + @EJB + IngestServiceBean ingestSvc; + @EJB + SystemConfig systemConfig; + @PersistenceContext(unitName = "VDCNet-ejbPU") + private EntityManager em; private static final Logger logger = Logger.getLogger(GlobusServiceBean.class.getCanonicalName()); private static final SimpleDateFormat logFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss"); @@ -130,7 +150,7 @@ private String getRuleId(GlobusEndpoint endpoint, String principal, String permi * @param ruleId - Globus rule id - assumed to be associated with the * dataset's file path (should not be called with a user * specified rule id w/o further checking) - * @param datasetId - the id of the dataset associated with the rule + * @param dataset - the dataset associated with the rule * @param globusLogger - a separate logger instance, may be null */ public void deletePermission(String ruleId, Dataset dataset, Logger globusLogger) { @@ -379,19 +399,33 @@ private void monitorTemporaryPermissions(String ruleId, long datasetId) { * @return * @throws MalformedURLException */ - public GlobusTask getTask(String accessToken, String taskId, Logger globusLogger) throws MalformedURLException { + public GlobusTaskState getTask(String accessToken, String taskId, Logger globusLogger) { + + Logger myLogger = globusLogger != null ? globusLogger : logger; - URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint_manager/task/" + taskId); + URL url; + try { + url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint_manager/task/" + taskId); + } catch (MalformedURLException mue) { + myLogger.warning("Malformed URL exception when trying to contact Globus. Globus API url: " + + "https://transfer.api.globusonline.org/v0.10/endpoint_manager/task/" + + taskId); + return null; + } MakeRequestResponse result = makeRequest(url, "Bearer", accessToken, "GET", null); - GlobusTask task = null; + GlobusTaskState task = null; if (result.status == 200) { - task = parseJson(result.jsonResponse, GlobusTask.class, false); + task = parseJson(result.jsonResponse, GlobusTaskState.class, false); } if (result.status != 200) { - globusLogger.warning("Cannot find information for the task " + taskId + " : Reason : " + // @todo It should probably retry it 2-3 times before giving up; + // similarly, it should probably differentiate between a "no such task" + // response and something intermittent like a server/network error or + // an expired token... i.e. something that's recoverable (?) + myLogger.warning("Cannot find information for the task " + taskId + " : Reason : " + result.jsonResponse.toString()); } @@ -633,41 +667,50 @@ private String getGlobusDownloadScript(Dataset dataset, ApiToken apiToken, List< @Asynchronous @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) - public void globusUpload(JsonObject jsonData, ApiToken token, Dataset dataset, String httpRequestUrl, - AuthenticatedUser authUser) throws ExecutionException, InterruptedException, MalformedURLException { + public void globusUpload(JsonObject jsonData, Dataset dataset, String httpRequestUrl, + AuthenticatedUser authUser) throws IllegalArgumentException, ExecutionException, InterruptedException, MalformedURLException { - Integer countAll = 0; - Integer countSuccess = 0; - Integer countError = 0; - String logTimestamp = logFormatter.format(new Date()); + // Before we do anything else, let's do some basic validation of what + // we've been passed: + + JsonArray filesJsonArray = jsonData.getJsonArray("files"); + + if (filesJsonArray == null || filesJsonArray.size() < 1) { + throw new IllegalArgumentException("No valid json entries supplied for the files being uploaded"); + } + + Date startDate = new Date(); + + String logTimestamp = logFormatter.format(startDate); Logger globusLogger = Logger.getLogger( "edu.harvard.iq.dataverse.upload.client.DatasetServiceBean." + "GlobusUpload" + logTimestamp); - String logFileName = "../logs" + File.separator + "globusUpload_id_" + dataset.getId() + "_" + logTimestamp + String logFileName = System.getProperty("com.sun.aas.instanceRoot") + File.separator + "logs" + File.separator + "globusUpload_" + dataset.getId() + "_" + logTimestamp + ".log"; FileHandler fileHandler; - boolean fileHandlerSuceeded; + try { fileHandler = new FileHandler(logFileName); globusLogger.setUseParentHandlers(false); - fileHandlerSuceeded = true; } catch (IOException | SecurityException ex) { Logger.getLogger(DatasetServiceBean.class.getName()).log(Level.SEVERE, null, ex); - return; + fileHandler = null; } - if (fileHandlerSuceeded) { + if (fileHandler != null) { globusLogger.addHandler(fileHandler); } else { globusLogger = logger; } logger.fine("json: " + JsonUtil.prettyPrint(jsonData)); + + globusLogger.info("Globus upload initiated"); String taskIdentifier = jsonData.getString("taskIdentifier"); GlobusEndpoint endpoint = getGlobusEndpoint(dataset); - GlobusTask task = getTask(endpoint.getClientToken(), taskIdentifier, globusLogger); - String ruleId = getRuleId(endpoint, task.getOwner_id(), "rw"); + GlobusTaskState taskState = getTask(endpoint.getClientToken(), taskIdentifier, globusLogger); + String ruleId = getRuleId(endpoint, taskState.getOwner_id(), "rw"); logger.fine("Found rule: " + ruleId); if (ruleId != null) { Long datasetId = rulesCache.getIfPresent(ruleId); @@ -676,28 +719,109 @@ public void globusUpload(JsonObject jsonData, ApiToken token, Dataset dataset, S rulesCache.invalidate(ruleId); } } - + // Wait before first check Thread.sleep(5000); - // globus task status check - task = globusStatusCheck(endpoint, taskIdentifier, globusLogger); - String taskStatus = getTaskStatus(task); - - globusLogger.info("Starting a globusUpload "); + + if (FeatureFlags.GLOBUS_USE_EXPERIMENTAL_ASYNC_FRAMEWORK.enabled()) { + + // Save the task information in the database so that the Globus monitoring + // service can continue checking on its progress. + + GlobusTaskInProgress taskInProgress = new GlobusTaskInProgress(taskIdentifier, GlobusTaskInProgress.TaskType.UPLOAD, dataset, endpoint.getClientToken(), authUser, ruleId, new Timestamp(startDate.getTime())); + em.persist(taskInProgress); + + // Save the metadata entries that define the files that are being uploaded + // in the database. These entries will be used once/if the uploads + // completes successfully to add the files to the dataset. + + for (JsonObject fileJsonObject : filesJsonArray.getValuesAs(JsonObject.class)) { + ExternalFileUploadInProgress fileUploadRecord = new ExternalFileUploadInProgress(taskIdentifier, fileJsonObject.toString()); + + em.persist(fileUploadRecord); + } + + if (fileHandler != null) { + fileHandler.close(); + } + // return and forget + return; + } + + + // the old implementation that relies on looping continuosly, + // sleeping-then-checking the task status repeatedly: + + // globus task status check + // (the following method performs continuous looped checks of the remote + // Globus API, monitoring it for as long as it takes for the task to + // finish one way or another!) + taskState = globusStatusCheck(endpoint, taskIdentifier, globusLogger); + // @todo null check, or make sure it's never null + String taskStatus = GlobusUtil.getTaskStatus(taskState); + + boolean taskSuccess = GlobusUtil.isTaskCompleted(taskState); + + processCompletedUploadTask(dataset, filesJsonArray, authUser, ruleId, globusLogger, taskSuccess, taskStatus); + + if (fileHandler != null) { + fileHandler.close(); + } + } + /** + * As the name suggests, the method completes and finalizes an upload task, + * whether it completed successfully or failed. (In the latter case, it + * simply sends a failure notification and does some cleanup). + * The method is called in both task monitoring scenarios: the old method, + * that relies on continuous looping, and the new, implemented on the basis + * of timer-like monitoring from a dedicated monitoring Singleton service. + * @param dataset the dataset + * @param filesJsonArray JsonArray containing files metadata entries as passed to /addGlobusFiles + * @param authUser the user that should be be performing the addFiles call + * finalizing adding the files to the Dataset. Note that this + * user will need to be obtained from the saved api token, when this + * method is called via the TaskMonitoringService + * @param ruleId Globus rule/permission id associated with the task + * @param myLogger the Logger; if null, the main logger of the service bean will be used + * @param fileHandler FileHandler associated with the Logger, when not null + * @param taskSuccess boolean task status of the completed task + * @param taskState human-readable task status label as reported by the Globus API + * the method should not throw any exceptions; all the exceptions thrown + * by the methods within are expected to be intercepted. + */ + private void processCompletedUploadTask(Dataset dataset, + JsonArray filesJsonArray, + AuthenticatedUser authUser, + String ruleId, + Logger globusLogger, + boolean taskSuccess, + String taskStatus) { + + Logger myLogger = globusLogger == null ? logger : globusLogger; + if (ruleId != null) { // Transfer is complete, so delete rule - deletePermission(ruleId, dataset, globusLogger); - + deletePermission(ruleId, dataset, myLogger); } - + // If success, switch to an EditInProgress lock - do this before removing the // GlobusUpload lock // Keeping a lock through the add datafiles API call avoids a conflicting edit - // and keeps any open dataset page refreshing until the datafile appears - if (!(taskStatus.startsWith("FAILED") || taskStatus.startsWith("INACTIVE"))) { - datasetSvc.addDatasetLock(dataset, - new DatasetLock(DatasetLock.Reason.EditInProgress, authUser, "Completing Globus Upload")); + // and keeps any open dataset page refreshing until the datafile appears. + + if (taskSuccess) { + myLogger.info("Finished upload via Globus job."); + + DatasetLock editLock = datasetSvc.addDatasetLock(dataset.getId(), + DatasetLock.Reason.EditInProgress, + (authUser).getId(), + "Completing Globus Upload"); + if (editLock != null) { + dataset.addLock(editLock); + } else { + myLogger.log(Level.WARNING, "Failed to lock the dataset (dataset id={0})", dataset.getId()); + } } DatasetLock gLock = dataset.getLockFor(DatasetLock.Reason.GlobusUpload); @@ -714,205 +838,260 @@ public void globusUpload(JsonObject jsonData, ApiToken token, Dataset dataset, S * addFilesAsync method called within the globusUpload method. I.e. it appeared * that the lock removal was not committed/visible outside this method until * globusUpload itself ended. + * (from @landreev:) If I understand the comment above correctly - annotations + * like "@TransactionAttribute(REQUIRES_NEW) do NOT work when you call a method + * directly within the same service bean. Strictly speaking, it's not the + * "within the same bean" part that is the key, rather, these annotations + * only apply when calling a method via an @EJB-defined service. So it + * is generally possible to call another method within FooServiceBean + * with the REQUIRES_NEW transaction taking effect - but then it would need + * to define *itself* as an @EJB - + * @EJB FooServiceBean fooSvc; + * ... + * fooSvc.doSomethingInNewTransaction(...); + * etc. */ datasetSvc.removeDatasetLocks(dataset, DatasetLock.Reason.GlobusUpload); } - - if (taskStatus.startsWith("FAILED") || taskStatus.startsWith("INACTIVE")) { - String comment = "Reason : " + taskStatus.split("#")[1] + "
    Short Description : " - + taskStatus.split("#")[2]; + + if (!taskSuccess) { + String comment; + if (taskStatus != null) { + comment = "Reason : " + taskStatus.split("#")[1] + "
    Short Description : " + + taskStatus.split("#")[2]; + } else { + comment = "No further information available"; + } + + myLogger.info("Globus Upload task failed "); userNotificationService.sendNotification((AuthenticatedUser) authUser, new Timestamp(new Date().getTime()), - UserNotification.Type.GLOBUSUPLOADCOMPLETEDWITHERRORS, dataset.getId(), comment, true); - globusLogger.info("Globus task failed "); + UserNotification.Type.GLOBUSUPLOADREMOTEFAILURE, dataset.getId(), comment, true); } else { try { - // - - List inputList = new ArrayList(); - JsonArray filesJsonArray = jsonData.getJsonArray("files"); - - if (filesJsonArray != null) { - String datasetIdentifier = dataset.getAuthorityForFileStorage() + "/" - + dataset.getIdentifierForFileStorage(); - - for (JsonObject fileJsonObject : filesJsonArray.getValuesAs(JsonObject.class)) { - - // storageIdentifier s3://gcs5-bucket1:1781cfeb8a7-748c270a227c from - // externalTool - String storageIdentifier = fileJsonObject.getString("storageIdentifier"); - String[] parts = DataAccess.getDriverIdAndStorageLocation(storageIdentifier); - String storeId = parts[0]; - // If this is an S3 store, we need to split out the bucket name - String[] bits = parts[1].split(":"); - String bucketName = ""; - if (bits.length > 1) { - bucketName = bits[0]; - } - String fileId = bits[bits.length - 1]; - - // fullpath s3://gcs5-bucket1/10.5072/FK2/3S6G2E/1781cfeb8a7-4ad9418a5873 - // or globus:///10.5072/FK2/3S6G2E/1781cfeb8a7-4ad9418a5873 - String fullPath = storeId + "://" + bucketName + "/" + datasetIdentifier + "/" + fileId; - String fileName = fileJsonObject.getString("fileName"); - - inputList.add(fileId + "IDsplit" + fullPath + "IDsplit" + fileName); - } - - // calculateMissingMetadataFields: checksum, mimetype - JsonObject newfilesJsonObject = calculateMissingMetadataFields(inputList, globusLogger); - JsonArray newfilesJsonArray = newfilesJsonObject.getJsonArray("files"); - logger.fine("Size: " + newfilesJsonArray.size()); - logger.fine("Val: " + JsonUtil.prettyPrint(newfilesJsonArray.getJsonObject(0))); - JsonArrayBuilder jsonDataSecondAPI = Json.createArrayBuilder(); - - for (JsonObject fileJsonObject : filesJsonArray.getValuesAs(JsonObject.class)) { - - countAll++; - String storageIdentifier = fileJsonObject.getString("storageIdentifier"); - String fileName = fileJsonObject.getString("fileName"); - String[] parts = DataAccess.getDriverIdAndStorageLocation(storageIdentifier); - // If this is an S3 store, we need to split out the bucket name - String[] bits = parts[1].split(":"); - if (bits.length > 1) { - } - String fileId = bits[bits.length - 1]; - - List newfileJsonObject = IntStream.range(0, newfilesJsonArray.size()) - .mapToObj(index -> ((JsonObject) newfilesJsonArray.get(index)).getJsonObject(fileId)) - .filter(Objects::nonNull).collect(Collectors.toList()); - if (newfileJsonObject != null) { - logger.fine("List Size: " + newfileJsonObject.size()); - // if (!newfileJsonObject.get(0).getString("hash").equalsIgnoreCase("null")) { - JsonPatch path = Json.createPatchBuilder() - .add("/md5Hash", newfileJsonObject.get(0).getString("hash")).build(); - fileJsonObject = path.apply(fileJsonObject); - path = Json.createPatchBuilder() - .add("/mimeType", newfileJsonObject.get(0).getString("mime")).build(); - fileJsonObject = path.apply(fileJsonObject); - jsonDataSecondAPI.add(fileJsonObject); - countSuccess++; - // } else { - // globusLogger.info(fileName - // + " will be skipped from adding to dataset by second API due to missing - // values "); - // countError++; - // } - } else { - globusLogger.info(fileName - + " will be skipped from adding to dataset by second API due to missing values "); - countError++; - } - } - - String newjsonData = jsonDataSecondAPI.build().toString(); - - globusLogger.info("Successfully generated new JsonData for Second API call"); - - String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST " - + httpRequestUrl + "/api/datasets/:persistentId/addFiles?persistentId=doi:" - + datasetIdentifier + " -F jsonData='" + newjsonData + "'"; - System.out.println("*******====command ==== " + command); - - // ToDo - refactor to call AddReplaceFileHelper.addFiles directly instead of - // calling API - - String output = addFilesAsync(command, globusLogger); - if (output.equalsIgnoreCase("ok")) { - // if(!taskSkippedFiles) - if (countError == 0) { - userNotificationService.sendNotification((AuthenticatedUser) authUser, - new Timestamp(new Date().getTime()), UserNotification.Type.GLOBUSUPLOADCOMPLETED, - dataset.getId(), countSuccess + " files added out of " + countAll, true); - } else { - userNotificationService.sendNotification((AuthenticatedUser) authUser, - new Timestamp(new Date().getTime()), - UserNotification.Type.GLOBUSUPLOADCOMPLETEDWITHERRORS, dataset.getId(), - countSuccess + " files added out of " + countAll, true); - } - globusLogger.info("Successfully completed api/datasets/:persistentId/addFiles call "); - } else { - globusLogger.log(Level.SEVERE, - "******* Error while executing api/datasets/:persistentId/add call ", command); - } - - } - - globusLogger.info("Files processed: " + countAll.toString()); - globusLogger.info("Files added successfully: " + countSuccess.toString()); - globusLogger.info("Files failures: " + countError.toString()); - globusLogger.info("Finished upload via Globus job."); - + processUploadedFiles(filesJsonArray, dataset, authUser, myLogger); } catch (Exception e) { - logger.info("Exception from globusUpload call "); + logger.info("Exception from processUploadedFiles call "); e.printStackTrace(); - globusLogger.info("Exception from globusUpload call " + e.getMessage()); + myLogger.info("Exception from processUploadedFiles call " + e.getMessage()); datasetSvc.removeDatasetLocks(dataset, DatasetLock.Reason.EditInProgress); } } if (ruleId != null) { - deletePermission(ruleId, dataset, globusLogger); - globusLogger.info("Removed upload permission: " + ruleId); - } - if (fileHandlerSuceeded) { - fileHandler.close(); + deletePermission(ruleId, dataset, myLogger); + myLogger.info("Removed upload permission: " + ruleId); } + //if (fileHandler != null) { + // fileHandler.close(); + //} + } + + + /** + * The code in this method is copy-and-pasted from the previous Borealis + * implemenation. + * @todo see if it can be refactored and simplified a bit, the json manipulation + * specifically (?) + * @param filesJsonArray JsonArray containing files metadata entries as passed to /addGlobusFiles + * @param dataset the dataset + * @param authUser the user that should be be performing the addFiles call + * finalizing adding the files to the Dataset. Note that this + * user will need to be obtained from the saved api token, when this + * method is called via the TaskMonitoringService + * @param myLogger the Logger; if null, the main logger of the service bean will be used + * @throws IOException, InterruptedException, ExecutionException @todo may need to throw more exceptions (?) + */ + private void processUploadedFiles(JsonArray filesJsonArray, Dataset dataset, AuthenticatedUser authUser, Logger myLogger) throws IOException, InterruptedException, ExecutionException { + myLogger = myLogger != null ? myLogger : logger; + + Integer countAll = 0; + Integer countSuccess = 0; + Integer countError = 0; + Integer countAddFilesSuccess = 0; + String notificationErrorMessage = ""; + + List inputList = new ArrayList(); + + String datasetIdentifier = dataset.getAuthorityForFileStorage() + "/" + + dataset.getIdentifierForFileStorage(); + + for (JsonObject fileJsonObject : filesJsonArray.getValuesAs(JsonObject.class)) { + + // storageIdentifier s3://gcs5-bucket1:1781cfeb8a7-748c270a227c from + // externalTool + String storageIdentifier = fileJsonObject.getString("storageIdentifier"); + String[] parts = DataAccess.getDriverIdAndStorageLocation(storageIdentifier); + String storeId = parts[0]; + // If this is an S3 store, we need to split out the bucket name + String[] bits = parts[1].split(":"); + String bucketName = ""; + if (bits.length > 1) { + bucketName = bits[0]; + } + String fileId = bits[bits.length - 1]; - public String addFilesAsync(String curlCommand, Logger globusLogger) - throws ExecutionException, InterruptedException { - CompletableFuture addFilesFuture = CompletableFuture.supplyAsync(() -> { - try { - Thread.sleep(2000); - } catch (InterruptedException e) { - e.printStackTrace(); + // fullpath s3://gcs5-bucket1/10.5072/FK2/3S6G2E/1781cfeb8a7-4ad9418a5873 + // or globus:///10.5072/FK2/3S6G2E/1781cfeb8a7-4ad9418a5873 + String fullPath = storeId + "://" + bucketName + "/" + datasetIdentifier + "/" + fileId; + String fileName = fileJsonObject.getString("fileName"); + + inputList.add(fileId + "IDsplit" + fullPath + "IDsplit" + fileName); + } + + // calculateMissingMetadataFields: checksum, mimetype + JsonObject newfilesJsonObject = calculateMissingMetadataFields(inputList, myLogger); + JsonArray newfilesJsonArray = newfilesJsonObject.getJsonArray("files"); + logger.fine("Size: " + newfilesJsonArray.size()); + logger.fine("Val: " + JsonUtil.prettyPrint(newfilesJsonArray.getJsonObject(0))); + JsonArrayBuilder addFilesJsonData = Json.createArrayBuilder(); + + for (JsonObject fileJsonObject : filesJsonArray.getValuesAs(JsonObject.class)) { + + countAll++; + String storageIdentifier = fileJsonObject.getString("storageIdentifier"); + String fileName = fileJsonObject.getString("fileName"); + String[] parts = DataAccess.getDriverIdAndStorageLocation(storageIdentifier); + // If this is an S3 store, we need to split out the bucket name + String[] bits = parts[1].split(":"); + if (bits.length > 1) { } - return (addFiles(curlCommand, globusLogger)); - }, executor).exceptionally(ex -> { - globusLogger.fine("Something went wrong : " + ex.getLocalizedMessage()); - ex.printStackTrace(); - return null; - }); + String fileId = bits[bits.length - 1]; + + List newfileJsonObject = IntStream.range(0, newfilesJsonArray.size()) + .mapToObj(index -> ((JsonObject) newfilesJsonArray.get(index)).getJsonObject(fileId)) + .filter(Objects::nonNull).collect(Collectors.toList()); + if (newfileJsonObject != null) { + logger.fine("List Size: " + newfileJsonObject.size()); + // if (!newfileJsonObject.get(0).getString("hash").equalsIgnoreCase("null")) { + JsonPatch path = Json.createPatchBuilder() + .add("/md5Hash", newfileJsonObject.get(0).getString("hash")).build(); + fileJsonObject = path.apply(fileJsonObject); + path = Json.createPatchBuilder() + .add("/mimeType", newfileJsonObject.get(0).getString("mime")).build(); + fileJsonObject = path.apply(fileJsonObject); + addFilesJsonData.add(fileJsonObject); + countSuccess++; + // } else { + // globusLogger.info(fileName + // + " will be skipped from adding to dataset by second API due to missing + // values "); + // countError++; + // } + } else { + myLogger.info(fileName + + " will be skipped from adding to dataset in the final AddReplaceFileHelper.addFiles() call. "); + countError++; + } + } - String result = addFilesFuture.get(); + String newjsonData = addFilesJsonData.build().toString(); - return result; - } + myLogger.info("Successfully generated new JsonData for addFiles call"); - private String addFiles(String curlCommand, Logger globusLogger) { - ProcessBuilder processBuilder = new ProcessBuilder(); - Process process = null; - String line; - String status = ""; + myLogger.info("Files passed to /addGlobusFiles: " + countAll); + myLogger.info("Files processed successfully: " + countSuccess); + myLogger.info("Files failures to process: " + countError); + + if (countSuccess < 1) { + // We don't have any valid entries to call addFiles() for; so, no + // need to proceed. + notificationErrorMessage = "Failed to successfully process any of the file entries, " + + "out of the " + countAll + " total as submitted to Dataverse"; + userNotificationService.sendNotification((AuthenticatedUser) authUser, + new Timestamp(new Date().getTime()), UserNotification.Type.GLOBUSUPLOADREMOTEFAILURE, + dataset.getId(), notificationErrorMessage, true); + return; + } else if (countSuccess < countAll) { + notificationErrorMessage = "Out of the " + countAll + " file entries submitted to /addGlobusFiles " + + "only " + countSuccess + " could be successfully parsed and processed. "; + } + + // A new AddReplaceFileHelper implementation, replacing the old one that + // was relying on calling /addFiles api via curl: + + // Passing null for the HttpServletRequest to make a new DataverseRequest. + // The parent method is always executed asynchronously, so the real request + // that was associated with the original API call that triggered this upload + // cannot be obtained. + DataverseRequest dataverseRequest = new DataverseRequest(authUser, (HttpServletRequest)null); + + AddReplaceFileHelper addFileHelper = new AddReplaceFileHelper( + dataverseRequest, + this.ingestSvc, + this.datasetSvc, + this.dataFileSvc, + this.permissionSvc, + this.commandEngine, + this.systemConfig + ); + + // The old code had 2 sec. of sleep, so ... + Thread.sleep(2000); + + Response addFilesResponse = addFileHelper.addFiles(newjsonData, dataset, authUser); + + if (addFilesResponse == null) { + logger.info("null response from addFiles call"); + //@todo add this case to the user notification in case of error + return; + } + + JsonObject addFilesJsonObject = JsonUtil.getJsonObject(addFilesResponse.getEntity().toString()); + + // @todo null check? + String addFilesStatus = addFilesJsonObject.getString("status", null); + myLogger.info("addFilesResponse status: " + addFilesStatus); + + if (ApiConstants.STATUS_OK.equalsIgnoreCase(addFilesStatus)) { + if (addFilesJsonObject.containsKey("data") && addFilesJsonObject.getJsonObject("data").containsKey("Result")) { + + //Integer countAddFilesTotal = addFilesJsonObject.getJsonObject("data").getJsonObject("Result").getInt(ApiConstants.API_ADD_FILES_COUNT_PROCESSED, -1); + countAddFilesSuccess = addFilesJsonObject.getJsonObject("data").getJsonObject("Result").getInt(ApiConstants.API_ADD_FILES_COUNT_SUCCESSFUL, -1); + myLogger.info("Files successfully added by addFiles(): " + countAddFilesSuccess); - try { - globusLogger.info("Call to : " + curlCommand); - processBuilder.command("bash", "-c", curlCommand); - process = processBuilder.start(); - process.waitFor(); - - BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream())); - - StringBuilder sb = new StringBuilder(); - while ((line = br.readLine()) != null) - sb.append(line); - globusLogger.info(" API Output : " + sb.toString()); - JsonObject jsonObject = null; - jsonObject = JsonUtil.getJsonObject(sb.toString()); - - status = jsonObject.getString("status"); - } catch (Exception ex) { - if (ex instanceof JsonParsingException) { - globusLogger.log(Level.SEVERE, "Error parsing dataset json."); } else { - globusLogger.log(Level.SEVERE, - "******* Unexpected Exception while executing api/datasets/:persistentId/add call ", ex); + myLogger.warning("Malformed addFiles response json: " + addFilesJsonObject.toString()); + notificationErrorMessage = "Malformed response received when attempting to add the files to the dataset. "; } + + myLogger.info("Completed addFiles call "); + } else if (ApiConstants.STATUS_ERROR.equalsIgnoreCase(addFilesStatus)) { + String addFilesMessage = addFilesJsonObject.getString("message", null); + + myLogger.log(Level.SEVERE, + "******* Error while executing addFiles ", newjsonData); + myLogger.log(Level.SEVERE, "****** Output from addFiles: ", addFilesMessage); + notificationErrorMessage += "Error response received when attempting to add the files to the dataset: " + addFilesMessage + " "; + + } else { + myLogger.log(Level.SEVERE, + "******* Error while executing addFiles ", newjsonData); + notificationErrorMessage += "Unexpected error encountered when attemptingh to add the files to the dataset."; + } + + // if(!taskSkippedFiles) + if (countAddFilesSuccess == countAll) { + userNotificationService.sendNotification((AuthenticatedUser) authUser, + new Timestamp(new Date().getTime()), UserNotification.Type.GLOBUSUPLOADCOMPLETED, + dataset.getId(), countSuccess + " files added out of " + countAll, true); + } else if (countAddFilesSuccess > 0) { + // success, but partial: + userNotificationService.sendNotification((AuthenticatedUser) authUser, + new Timestamp(new Date().getTime()), + UserNotification.Type.GLOBUSUPLOADCOMPLETEDWITHERRORS, dataset.getId(), + countSuccess + " files added out of " + countAll + notificationErrorMessage, true); + } else { + notificationErrorMessage = "".equals(notificationErrorMessage) + ? " No additional information is available." : notificationErrorMessage; + userNotificationService.sendNotification((AuthenticatedUser) authUser, + new Timestamp(new Date().getTime()), + UserNotification.Type.GLOBUSUPLOADLOCALFAILURE, dataset.getId(), + notificationErrorMessage, true); } - return status; } - + @Asynchronous public void globusDownload(String jsonData, Dataset dataset, User authUser) throws MalformedURLException { @@ -920,7 +1099,7 @@ public void globusDownload(String jsonData, Dataset dataset, User authUser) thro Logger globusLogger = Logger.getLogger( "edu.harvard.iq.dataverse.upload.client.DatasetServiceBean." + "GlobusDownload" + logTimestamp); - String logFileName = "../logs" + File.separator + "globusDownload_id_" + dataset.getId() + "_" + logTimestamp + String logFileName = System.getProperty("com.sun.aas.instanceRoot") + File.separator + "logs" + File.separator + "globusDownload_id_" + dataset.getId() + "_" + logTimestamp + ".log"; FileHandler fileHandler; boolean fileHandlerSuceeded; @@ -958,7 +1137,7 @@ public void globusDownload(String jsonData, Dataset dataset, User authUser) thro // If the rules_cache times out, the permission will be deleted. Presumably that // doesn't affect a // globus task status check - GlobusTask task = getTask(endpoint.getClientToken(), taskIdentifier, globusLogger); + GlobusTaskState task = getTask(endpoint.getClientToken(), taskIdentifier, globusLogger); String ruleId = getRuleId(endpoint, task.getOwner_id(), "r"); if (ruleId != null) { logger.fine("Found rule: " + ruleId); @@ -974,7 +1153,8 @@ public void globusDownload(String jsonData, Dataset dataset, User authUser) thro logger.warning("ruleId not found for taskId: " + taskIdentifier); } task = globusStatusCheck(endpoint, taskIdentifier, globusLogger); - String taskStatus = getTaskStatus(task); + // @todo null check? + String taskStatus = GlobusUtil.getTaskStatus(task); // Transfer is done (success or failure) so delete the rule if (ruleId != null) { @@ -985,10 +1165,14 @@ public void globusDownload(String jsonData, Dataset dataset, User authUser) thro if (taskStatus.startsWith("FAILED") || taskStatus.startsWith("INACTIVE")) { String comment = "Reason : " + taskStatus.split("#")[1] + "
    Short Description : " + taskStatus.split("#")[2]; - userNotificationService.sendNotification((AuthenticatedUser) authUser, new Timestamp(new Date().getTime()), - UserNotification.Type.GLOBUSDOWNLOADCOMPLETEDWITHERRORS, dataset.getId(), comment, true); - globusLogger.info("Globus task failed during download process"); - } else { + if (authUser != null && authUser instanceof AuthenticatedUser) { + userNotificationService.sendNotification((AuthenticatedUser) authUser, new Timestamp(new Date().getTime()), + UserNotification.Type.GLOBUSDOWNLOADCOMPLETEDWITHERRORS, dataset.getId(), comment, true); + } + + globusLogger.info("Globus task failed during download process: "+comment); + } else if (authUser != null && authUser instanceof AuthenticatedUser) { + boolean taskSkippedFiles = (task.getSkip_source_errors() == null) ? false : task.getSkip_source_errors(); if (!taskSkippedFiles) { userNotificationService.sendNotification((AuthenticatedUser) authUser, @@ -1004,76 +1188,29 @@ public void globusDownload(String jsonData, Dataset dataset, User authUser) thro Executor executor = Executors.newFixedThreadPool(10); - private GlobusTask globusStatusCheck(GlobusEndpoint endpoint, String taskId, Logger globusLogger) + private GlobusTaskState globusStatusCheck(GlobusEndpoint endpoint, String taskId, Logger globusLogger) throws MalformedURLException { - boolean taskCompletion = false; - String status = ""; - GlobusTask task = null; + boolean taskCompleted = false; + GlobusTaskState task = null; int pollingInterval = SystemConfig.getIntLimitFromStringOrDefault( settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusPollingInterval), 50); do { try { globusLogger.info("checking globus transfer task " + taskId); Thread.sleep(pollingInterval * 1000); + // Call the (centralized) Globus API to check on the task state/status: task = getTask(endpoint.getClientToken(), taskId, globusLogger); - if (task != null) { - status = task.getStatus(); - if (status != null) { - // The task is in progress. - if (status.equalsIgnoreCase("ACTIVE")) { - if (task.getNice_status().equalsIgnoreCase("ok") - || task.getNice_status().equalsIgnoreCase("queued")) { - taskCompletion = false; - } else { - taskCompletion = true; - // status = "FAILED" + "#" + task.getNice_status() + "#" + - // task.getNice_status_short_description(); - } - } else { - // The task is either succeeded, failed or inactive. - taskCompletion = true; - // status = status + "#" + task.getNice_status() + "#" + - // task.getNice_status_short_description(); - } - } else { - // status = "FAILED"; - taskCompletion = true; - } - } else { - // status = "FAILED"; - taskCompletion = true; - } + taskCompleted = GlobusUtil.isTaskCompleted(task); } catch (Exception ex) { ex.printStackTrace(); } - } while (!taskCompletion); + } while (!taskCompleted); globusLogger.info("globus transfer task completed successfully"); return task; } - - private String getTaskStatus(GlobusTask task) { - String status = null; - if (task != null) { - status = task.getStatus(); - if (status != null) { - // The task is in progress but is not ok or queued - if (status.equalsIgnoreCase("ACTIVE")) { - status = "FAILED" + "#" + task.getNice_status() + "#" + task.getNice_status_short_description(); - } else { - // The task is either succeeded, failed or inactive. - status = status + "#" + task.getNice_status() + "#" + task.getNice_status_short_description(); - } - } else { - status = "FAILED"; - } - } else { - status = "FAILED"; - } - return status; - } - + public JsonObject calculateMissingMetadataFields(List inputList, Logger globusLogger) throws InterruptedException, ExecutionException, IOException { @@ -1129,27 +1266,39 @@ private FileDetailsHolder calculateDetails(String id, Logger globusLogger) String fileName = id.split("IDsplit")[2]; // ToDo: what if the file does not exist in s3 + // (L.A.) - good question. maybe it should call .open and .exists() here? + // otherwise, there doesn't seem to be any diagnostics as to which + // files uploaded successfully and which failed (?) + // ... however, any partially successful upload cases should be + // properly handled later, during the .addFiles() call - only + // the files that actually exists in storage remotely will be + // added to the dataset permanently then. // ToDo: what if checksum calculation failed + // (L.A.) - this appears to have been addressed - by using "Not available in Dataverse" + // in place of a checksum. - do { - try { - StorageIO dataFileStorageIO = DataAccess.getDirectStorageIO(fullPath); - in = dataFileStorageIO.getInputStream(); - checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); - count = 3; - } catch (IOException ioex) { - count = 3; - logger.fine(ioex.getMessage()); - globusLogger.info( - "DataFile (fullPath " + fullPath + ") does not appear to be accessible within Dataverse: "); - } catch (Exception ex) { - count = count + 1; - ex.printStackTrace(); - logger.info(ex.getMessage()); - Thread.sleep(5000); - } + String storageDriverId = DataAccess.getDriverIdAndStorageLocation(fullPath)[0]; - } while (count < 3); + if (StorageIO.isDataverseAccessible(storageDriverId)) { + do { + try { + StorageIO dataFileStorageIO = DataAccess.getDirectStorageIO(fullPath); + in = dataFileStorageIO.getInputStream(); + checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); + count = 3; + } catch (IOException ioex) { + count = 3; + logger.fine(ioex.getMessage()); + globusLogger.info( + "DataFile (fullPath " + fullPath + ") does not appear to be accessible within Dataverse: "); + } catch (Exception ex) { + count = count + 1; + ex.printStackTrace(); + logger.info(ex.getMessage()); + Thread.sleep(5000); + } + } while (count < 3); + } if (checksumVal.length() == 0) { checksumVal = "Not available in Dataverse"; @@ -1257,11 +1406,11 @@ public void writeGuestbookAndStartTransfer(GuestbookResponse guestbookResponse, Long fileId = Long.parseLong(idAsString); // If we need to create a GuestBookResponse record, we have to // look up the DataFile object for this file: + df = dataFileSvc.findCheapAndEasy(fileId); + selectedFiles.add(df); if (!doNotSaveGuestbookResponse) { - df = dataFileService.findCheapAndEasy(fileId); guestbookResponse.setDataFile(df); fileDownloadService.writeGuestbookResponseRecord(guestbookResponse); - selectedFiles.add(df); } } catch (NumberFormatException nfe) { logger.warning( @@ -1277,5 +1426,58 @@ public void writeGuestbookAndStartTransfer(GuestbookResponse guestbookResponse, } } } + + public List findAllOngoingTasks() { + return em.createQuery("select object(o) from GlobusTaskInProgress as o order by o.startTime", GlobusTaskInProgress.class).getResultList(); + } + + public void deleteTask(GlobusTaskInProgress task) { + GlobusTaskInProgress mergedTask = em.merge(task); + em.remove(mergedTask); + } + + public List findExternalUploadsByTaskId(String taskId) { + return em.createNamedQuery("ExternalFileUploadInProgress.findByTaskId").setParameter("taskId", taskId).getResultList(); + } + + public void processCompletedTask(GlobusTaskInProgress globusTask, boolean taskSuccess, String taskStatus, Logger taskLogger) { + String ruleId = globusTask.getRuleId(); + Dataset dataset = globusTask.getDataset(); + AuthenticatedUser authUser = globusTask.getLocalUser(); + if (authUser == null) { + // @todo log error message; do nothing + return; + } + + if (GlobusTaskInProgress.TaskType.UPLOAD.equals(globusTask.getTaskType())) { + List fileUploadsInProgress = findExternalUploadsByTaskId(globusTask.getTaskId()); + if (fileUploadsInProgress == null || fileUploadsInProgress.size() < 1) { + // @todo log error message; do nothing + // (will this ever happen though?) + return; + } + + JsonArrayBuilder filesJsonArrayBuilder = Json.createArrayBuilder(); + + for (ExternalFileUploadInProgress pendingFile : fileUploadsInProgress) { + String jsonInfoString = pendingFile.getFileInfo(); + JsonObject fileObject = JsonUtil.getJsonObject(jsonInfoString); + filesJsonArrayBuilder.add(fileObject); + } + + JsonArray filesJsonArray = filesJsonArrayBuilder.build(); + + processCompletedUploadTask(dataset, filesJsonArray, authUser, ruleId, taskLogger, taskSuccess, taskStatus); + } else { + // @todo eventually, extend this async. framework to handle Glonus downloads as well + } + + } + + public void deleteExternalUploadRecords(String taskId) { + em.createNamedQuery("ExternalFileUploadInProgress.deleteByTaskId") + .setParameter("taskId", taskId) + .executeUpdate(); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusTaskInProgress.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusTaskInProgress.java new file mode 100644 index 00000000000..8644bca6143 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusTaskInProgress.java @@ -0,0 +1,202 @@ +package edu.harvard.iq.dataverse.globus; + +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import jakarta.persistence.Column; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.Arrays; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; + +/** + * + * @author landreev + */ +@Entity +@Table(uniqueConstraints = {@UniqueConstraint(columnNames = "taskid")}) +public class GlobusTaskInProgress implements Serializable { + + private static final long serialVersionUID = 1L; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * Globus-side identifier of the task in progress, upload or download + */ + @Column(nullable=false, unique = true) + private String taskId; + + /** + * I was considering giving this enum type a more specific name "TransferType" + * - but maybe there will be another use case where we need to keep track of + * Globus tasks that are not data transfers (?) + */ + public enum TaskType { + + UPLOAD("UPLOAD"), + DOWNLOAD("DOWNLOAD"); + + private final String text; + + private TaskType(final String text) { + this.text = text; + } + + public static TaskType fromString(String text) { + if (text != null) { + for (TaskType taskType : TaskType.values()) { + if (text.equals(taskType.text)) { + return taskType; + } + } + } + throw new IllegalArgumentException("TaskType must be one of these values: " + Arrays.asList(TaskType.values()) + "."); + } + + @Override + public String toString() { + return text; + } + } + + @Column(nullable=false) + @Enumerated(EnumType.STRING) + private TaskType taskType; + + /** + * Globus API token that should be used to monitor the status of the task + */ + @Column(nullable=false) + private String globusToken; + + /** + * This is the the user who initiated the Globus task + */ + @ManyToOne + @JoinColumn + private AuthenticatedUser user; + + @Column(nullable=false) + private String ruleId; + + @JoinColumn(nullable = false) + @ManyToOne + private Dataset dataset; + + @Column + private Timestamp startTime; + + public GlobusTaskInProgress() { + } + + GlobusTaskInProgress(String taskId, TaskType taskType, Dataset dataset, String globusToken, AuthenticatedUser authUser, String ruleId, Timestamp startTime) { + this.taskId = taskId; + this.taskType = taskType; + this.dataset = dataset; + this.globusToken = globusToken; + this.user = authUser; + this.ruleId = ruleId; + this.startTime = startTime; + } + + + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + public TaskType getTaskType() { + return taskType; + } + + public void setTaskType(TaskType taskType) { + this.taskType = taskType; + } + + public String getGlobusToken() { + return globusToken; + } + + public void setGlobusToken(String clientToken) { + this.globusToken = clientToken; + } + + public AuthenticatedUser getLocalUser() { + return user; + } + + public void setLocalUser(AuthenticatedUser authUser) { + this.user = authUser; + } + + public String getRuleId() { + return ruleId; + } + + public void setRuleId(String ruleId) { + this.ruleId = ruleId; + } + public Dataset getDataset() { + return dataset; + } + + public void setDataset(Dataset dataset) { + this.dataset = dataset; + } + + public Timestamp getStartTime() { + return startTime; + } + + public void setStartTime(Timestamp startTime) { + this.startTime = startTime; + } + + @Override + public int hashCode() { + int hash = 0; + hash += (id != null ? id.hashCode() : 0); + return hash; + } + + @Override + public boolean equals(Object object) { + // TODO: Warning - this method won't work in the case the id fields are not set + if (!(object instanceof GlobusTaskInProgress)) { + return false; + } + GlobusTaskInProgress other = (GlobusTaskInProgress) object; + if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) { + return false; + } + return true; + } + + @Override + public String toString() { + return "edu.harvard.iq.dataverse.globus.GlobusTaskInProgress[ id=" + id + " ]"; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusTask.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusTaskState.java similarity index 93% rename from src/main/java/edu/harvard/iq/dataverse/globus/GlobusTask.java rename to src/main/java/edu/harvard/iq/dataverse/globus/GlobusTaskState.java index c2b01779f4a..b5db20d46c1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusTask.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusTaskState.java @@ -1,6 +1,10 @@ package edu.harvard.iq.dataverse.globus; -public class GlobusTask { +/** + * This class is used to store the state of an ongoing Globus task (transfer) + * as reported by the Globus task API. + */ +public class GlobusTaskState { private String DATA_TYPE; private String type; diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusUtil.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusUtil.java index 92cf8ac7704..652898591ac 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusUtil.java @@ -30,4 +30,64 @@ public static JsonObject getFilesMap(List dataFiles, Dataset d) { } return filesBuilder.build(); } + + public static boolean isTaskCompleted(GlobusTaskState task) { + if (task != null) { + String status = task.getStatus(); + if (status != null) { + if (status.equalsIgnoreCase("ACTIVE")) { + if (task.getNice_status().equalsIgnoreCase("ok") + || task.getNice_status().equalsIgnoreCase("queued")) { + return false; + } + } + } + } + return true; + } + + public static boolean isTaskSucceeded(GlobusTaskState task) { + String status = null; + if (task != null) { + status = task.getStatus(); + if (status != null) { + status = status.toUpperCase(); + if (status.equals("ACTIVE") || status.startsWith("FAILED") || status.startsWith("INACTIVE")) { + // There are cases where a failed task may still be showing + // as "ACTIVE". But it is definitely safe to assume that it + // has not completed *successfully*. + return false; + } + return true; + } + } + return false; + } + /** + * Produces a human-readable Status label of a completed task + * @param GlobusTaskState task - a looked-up state of a task as reported by Globus API + */ + public static String getTaskStatus(GlobusTaskState task) { + String status = null; + if (task != null) { + status = task.getStatus(); + if (status != null) { + // The task is in progress but is not ok or queued + // (L.A.) I think the assumption here is that this method is called + // exclusively on tasks that have already completed. So that's why + // it is safe to assume that "ACTIVE" means "FAILED". + if (status.equalsIgnoreCase("ACTIVE")) { + status = "FAILED" + "#" + task.getNice_status() + "#" + task.getNice_status_short_description(); + } else { + // The task is either succeeded, failed or inactive. + status = status + "#" + task.getNice_status() + "#" + task.getNice_status_short_description(); + } + } else { + status = "FAILED"; + } + } else { + status = "FAILED"; + } + return status; + } } \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/TaskMonitoringServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/TaskMonitoringServiceBean.java new file mode 100644 index 00000000000..fdb2b222804 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/TaskMonitoringServiceBean.java @@ -0,0 +1,131 @@ +package edu.harvard.iq.dataverse.globus; + +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.SystemConfig; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import jakarta.ejb.EJB; +import jakarta.ejb.Singleton; +import jakarta.ejb.Startup; +import jakarta.enterprise.concurrent.ManagedScheduledExecutorService; +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.FileHandler; +import java.util.logging.Logger; + +/** + * + * This Singleton monitors ongoing Globus tasks by checking with the centralized + * Globus API on the status of all the registered ongoing tasks. + * When a successful completion of a task is detected, the service triggers + * the execution of the associated tasks (for example, finalizing adding datafiles + * to the dataset on completion of a remote Globus upload). When a task fails or + * terminates abnormally, a message is logged and the task record is deleted + * from the database. + * + * @author landreev + */ +@Singleton +@Startup +public class TaskMonitoringServiceBean { + private static final Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.globus.TaskMonitoringServiceBean"); + + @Resource + ManagedScheduledExecutorService scheduler; + + @EJB + SystemConfig systemConfig; + @EJB + SettingsServiceBean settingsSvc; + @EJB + GlobusServiceBean globusService; + + private static final SimpleDateFormat logFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss"); + + @PostConstruct + public void init() { + if (JvmSettings.GLOBUS_TASK_MONITORING_SERVER.lookupOptional(Boolean.class).orElse(false)) { + logger.info("Starting Globus task monitoring service"); + int pollingInterval = SystemConfig.getIntLimitFromStringOrDefault( + settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusPollingInterval), 600); + this.scheduler.scheduleWithFixedDelay(this::checkOngoingTasks, + 0, pollingInterval, + TimeUnit.SECONDS); + } else { + logger.info("Skipping Globus task monitor initialization"); + } + } + + /** + * This method will be executed on a timer-like schedule, continuously + * monitoring all the ongoing external Globus tasks (transfers). + */ + public void checkOngoingTasks() { + logger.fine("Performing a scheduled external Globus task check"); + List tasks = globusService.findAllOngoingTasks(); + + tasks.forEach(t -> { + FileHandler taskLogHandler = getTaskLogHandler(t); + Logger taskLogger = getTaskLogger(t, taskLogHandler); + + GlobusTaskState retrieved = globusService.getTask(t.getGlobusToken(), t.getTaskId(), taskLogger); + if (GlobusUtil.isTaskCompleted(retrieved)) { + // Do our thing, finalize adding the files to the dataset + globusService.processCompletedTask(t, GlobusUtil.isTaskSucceeded(retrieved), GlobusUtil.getTaskStatus(retrieved), taskLogger); + // Whether it finished successfully, or failed in the process, + // there's no need to keep monitoring this task, so we can + // delete it. + //globusService.deleteExternalUploadRecords(t.getTaskId()); + globusService.deleteTask(t); + } + + if (taskLogHandler != null) { + // @todo it should be prudent to cache these loggers and handlers + // between monitoring runs (should be fairly easy to do) + taskLogHandler.close(); + } + }); + } + + private FileHandler getTaskLogHandler(GlobusTaskInProgress task) { + if (task == null) { + return null; + } + + Date startDate = new Date(task.getStartTime().getTime()); + String logTimeStamp = logFormatter.format(startDate); + + String logFileName = System.getProperty("com.sun.aas.instanceRoot") + File.separator + "logs" + File.separator + "globusUpload_" + task.getDataset().getId() + "_" + logTimeStamp + + ".log"; + FileHandler fileHandler; + try { + fileHandler = new FileHandler(logFileName); + } catch (IOException | SecurityException ex) { + // @todo log this error somehow? + fileHandler = null; + } + return fileHandler; + } + + private Logger getTaskLogger(GlobusTaskInProgress task, FileHandler logFileHandler) { + if (logFileHandler == null) { + return null; + } + Date startDate = new Date(task.getStartTime().getTime()); + String logTimeStamp = logFormatter.format(startDate); + + Logger taskLogger = Logger.getLogger( + "edu.harvard.iq.dataverse.upload.client.DatasetServiceBean." + "GlobusUpload" + logTimeStamp); + taskLogger.setUseParentHandlers(false); + + taskLogger.addHandler(logFileHandler); + + return taskLogger; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvesterServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvesterServiceBean.java index 20884e3360c..e0b5c2dfbfb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvesterServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvesterServiceBean.java @@ -88,7 +88,6 @@ public class HarvesterServiceBean { public static final String HARVEST_RESULT_FAILED="failed"; public static final String DATAVERSE_PROPRIETARY_METADATA_FORMAT="dataverse_json"; public static final String DATAVERSE_PROPRIETARY_METADATA_API="/api/datasets/export?exporter="+DATAVERSE_PROPRIETARY_METADATA_FORMAT+"&persistentId="; - public static final String DATAVERSE_HARVEST_STOP_FILE="../logs/stopharvest_"; public HarvesterServiceBean() { @@ -399,7 +398,7 @@ private void deleteHarvestedDatasetIfExists(String persistentIdentifier, Dataver private boolean checkIfStoppingJob(HarvestingClient harvestingClient) { Long pid = ProcessHandle.current().pid(); - String stopFileName = DATAVERSE_HARVEST_STOP_FILE + harvestingClient.getName() + "." + pid; + String stopFileName = System.getProperty("com.sun.aas.instanceRoot") + File.separator + "logs" + File.separator + "stopharvest_" + harvestingClient.getName() + "." + pid; Path stopFilePath = Paths.get(stopFileName); if (Files.exists(stopFilePath)) { diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvestingClient.java b/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvestingClient.java index 40db55f2a0c..0667f5594ce 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvestingClient.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvestingClient.java @@ -243,6 +243,14 @@ public String getCustomHttpHeaders() { public void setCustomHttpHeaders(String customHttpHeaders) { this.customHttpHeaders = customHttpHeaders; } + + private boolean allowHarvestingMissingCVV; + public boolean getAllowHarvestingMissingCVV() { + return allowHarvestingMissingCVV; + } + public void setAllowHarvestingMissingCVV(boolean allowHarvestingMissingCVV) { + this.allowHarvestingMissingCVV = allowHarvestingMissingCVV; + } // TODO: do we need "orphanRemoval=true"? -- L.A. 4.4 // TODO: should it be @OrderBy("startTime")? -- L.A. 4.4 diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java index 1b4a7bc7db0..cc15d4c978b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java @@ -40,10 +40,6 @@ @Stateless @Named public class OAIRecordServiceBean implements java.io.Serializable { - @EJB - OAISetServiceBean oaiSetService; - @EJB - IndexServiceBean indexService; @EJB DatasetServiceBean datasetService; @EJB @@ -55,13 +51,24 @@ public class OAIRecordServiceBean implements java.io.Serializable { EntityManager em; private static final Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.harvest.server.OAIRecordServiceBean"); - - public void updateOaiRecords(String setName, List datasetIds, Date updateTime, boolean doExport) { - updateOaiRecords(setName, datasetIds, updateTime, doExport, logger); - } - public void updateOaiRecords(String setName, List datasetIds, Date updateTime, boolean doExport, Logger setUpdateLogger) { - + /** + * Updates the OAI records for the set specified + * @param setName name of the OAI set + * @param datasetIds ids of the datasets that are candidates for this OAI set + * @param updateTime time stamp + * @param doExport attempt to export datasets that haven't been exported yet + * @param confirmed true if the datasetIds above were looked up in the database + * - as opposed to in the search engine. Meaning, that it is + * confirmed that any dataset not on this list that's currently + * in the set is no longer in the database and should be + * marked as deleted without any further checks. Otherwise + * we'll want to double-check if the dataset still exists + * as published. This is to prevent marking existing datasets + * as deleted during a full reindex etc. + * @param setUpdateLogger dedicated Logger + */ + public void updateOaiRecords(String setName, List datasetIds, Date updateTime, boolean doExport, boolean confirmed, Logger setUpdateLogger) { // create Map of OaiRecords List oaiRecords = findOaiRecordsBySetName(setName); Map recordMap = new HashMap<>(); @@ -101,9 +108,6 @@ public void updateOaiRecords(String setName, List datasetIds, Date updateT DatasetVersion releasedVersion = dataset.getReleasedVersion(); Date publicationDate = releasedVersion == null ? null : releasedVersion.getReleaseTime(); - //if (dataset.getPublicationDate() != null - // && (dataset.getLastExportTime() == null - // || dataset.getLastExportTime().before(dataset.getPublicationDate()))) { if (publicationDate != null && (dataset.getLastExportTime() == null || dataset.getLastExportTime().before(publicationDate))) { @@ -125,7 +129,9 @@ public void updateOaiRecords(String setName, List datasetIds, Date updateT } // anything left in the map should be marked as removed! - markOaiRecordsAsRemoved( recordMap.values(), updateTime, setUpdateLogger); + markOaiRecordsAsRemoved(recordMap.values(), updateTime, confirmed, setUpdateLogger); + + } @@ -162,7 +168,7 @@ record = new OAIRecord(setName, dataset.getGlobalId().asString(), new Date()); } } - + /* // Updates any existing OAI records for this dataset // Should be called whenever there's a change in the release status of the Dataset // (i.e., when it's published or deaccessioned), so that the timestamps and @@ -201,13 +207,31 @@ public void updateOaiRecordsForDataset(Dataset dataset) { logger.fine("Null returned - no records found."); } } +*/ - public void markOaiRecordsAsRemoved(Collection records, Date updateTime, Logger setUpdateLogger) { + public void markOaiRecordsAsRemoved(Collection records, Date updateTime, boolean confirmed, Logger setUpdateLogger) { for (OAIRecord oaiRecord : records) { if ( !oaiRecord.isRemoved() ) { - setUpdateLogger.fine("marking OAI record "+oaiRecord.getGlobalId()+" as removed"); - oaiRecord.setRemoved(true); - oaiRecord.setLastUpdateTime(updateTime); + boolean confirmedRemoved = confirmed; + if (!confirmedRemoved) { + Dataset lookedUp = datasetService.findByGlobalId(oaiRecord.getGlobalId()); + if (lookedUp == null) { + confirmedRemoved = true; + } else if (lookedUp.getLastExportTime() == null) { + confirmedRemoved = true; + } else { + boolean isReleased = lookedUp.getReleasedVersion() != null; + if (!isReleased) { + confirmedRemoved = true; + } + } + } + + if (confirmedRemoved) { + setUpdateLogger.fine("marking OAI record "+oaiRecord.getGlobalId()+" as removed"); + oaiRecord.setRemoved(true); + oaiRecord.setLastUpdateTime(updateTime); + } } else { setUpdateLogger.fine("OAI record "+oaiRecord.getGlobalId()+" is already marked as removed."); } diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAISetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAISetServiceBean.java index d5c78c36b98..242187db7f1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAISetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAISetServiceBean.java @@ -171,6 +171,8 @@ public void exportOaiSet(OAISet oaiSet, Logger exportLogger) { String query = managedSet.getDefinition(); List datasetIds; + boolean databaseLookup = false; // As opposed to a search engine lookup + try { if (!oaiSet.isDefaultSet()) { datasetIds = expandSetQuery(query); @@ -181,6 +183,7 @@ public void exportOaiSet(OAISet oaiSet, Logger exportLogger) { // including the unpublished drafts and deaccessioned ones. // Those will be filtered out further down the line. datasetIds = datasetService.findAllLocalDatasetIds(); + databaseLookup = true; } } catch (OaiSetException ose) { datasetIds = null; @@ -191,7 +194,7 @@ public void exportOaiSet(OAISet oaiSet, Logger exportLogger) { // they will be properly marked as "deleted"! -- L.A. 4.5 //if (datasetIds != null && !datasetIds.isEmpty()) { exportLogger.info("Calling OAI Record Service to re-export " + datasetIds.size() + " datasets."); - oaiRecordService.updateOaiRecords(managedSet.getSpec(), datasetIds, new Date(), true, exportLogger); + oaiRecordService.updateOaiRecords(managedSet.getSpec(), datasetIds, new Date(), true, databaseLookup, exportLogger); //} managedSet.setUpdateInProgress(false); @@ -200,7 +203,7 @@ public void exportOaiSet(OAISet oaiSet, Logger exportLogger) { public void exportAllSets() { String logTimestamp = logFormatter.format(new Date()); Logger exportLogger = Logger.getLogger("edu.harvard.iq.dataverse.harvest.client.OAISetServiceBean." + "UpdateAllSets." + logTimestamp); - String logFileName = "../logs" + File.separator + "oaiSetsUpdate_" + logTimestamp + ".log"; + String logFileName = System.getProperty("com.sun.aas.instanceRoot") + File.separator + "logs" + File.separator + "oaiSetsUpdate_" + logTimestamp + ".log"; FileHandler fileHandler = null; boolean fileHandlerSuceeded = false; try { diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java index 233ca94f5fc..f9047e3ee5f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java @@ -5,6 +5,7 @@ */ package edu.harvard.iq.dataverse.harvest.server.web.servlet; +import edu.harvard.iq.dataverse.MailServiceBean; import io.gdcc.xoai.dataprovider.DataProvider; import io.gdcc.xoai.dataprovider.repository.Repository; import io.gdcc.xoai.dataprovider.repository.RepositoryConfiguration; @@ -39,6 +40,7 @@ import java.io.IOException; +import java.util.Optional; import java.util.logging.Logger; import jakarta.ejb.EJB; import jakarta.inject.Inject; @@ -67,14 +69,14 @@ public class OAIServlet extends HttpServlet { @EJB OAIRecordServiceBean recordService; @EJB - SettingsServiceBean settingsService; - @EJB DataverseServiceBean dataverseService; @EJB DatasetServiceBean datasetService; @EJB SystemConfig systemConfig; + @EJB + MailServiceBean mailServiceBean; @Inject @ConfigProperty(name = "dataverse.oai.server.maxidentifiers", defaultValue="100") @@ -194,9 +196,13 @@ private RepositoryConfiguration createRepositoryConfiguration() { // (Note: if the setting does not exist, we are going to assume that they // have a reason not to want to configure their email address, if it is // a developer's instance, for example; or a reason not to want to - // advertise it to the world.) - InternetAddress systemEmailAddress = MailUtil.parseSystemAddress(settingsService.getValueForKey(SettingsServiceBean.Key.SystemEmail)); - String systemEmailLabel = systemEmailAddress != null ? systemEmailAddress.getAddress() : "donotreply@localhost"; + // advertise it to the world.) + String systemEmailLabel = "donotreply@localhost"; + // TODO: should we expose the support team's address if configured? + Optional systemAddress = mailServiceBean.getSystemAddress(); + if (systemAddress.isPresent()) { + systemEmailLabel = systemAddress.get().getAddress(); + } RepositoryConfiguration configuration = new RepositoryConfiguration.RepositoryConfigurationBuilder() .withAdminEmail(systemEmailLabel) diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java index 233f746fb17..b42fd950528 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java @@ -345,12 +345,13 @@ public List saveAndAddFilesToDataset(DatasetVersion version, StorageIO dataAccess = DataAccess.getStorageIO(dataFile); //Populate metadata dataAccess.open(DataAccessOption.READ_ACCESS); - + // (the .open() above makes a remote call to check if + // the file exists and obtains its size) confirmedFileSize = dataAccess.getSize(); // For directly-uploaded files, we will perform the file size // limit and quota checks here. Perform them *again*, in - // some cases: a directly uploaded files have already been + // some cases: files directly uploaded via the UI have already been // checked (for the sake of being able to reject the upload // before the user clicks "save"). But in case of direct // uploads via API, these checks haven't been performed yet, @@ -726,27 +727,17 @@ public void produceSummaryStatistics(DataFile dataFile, File generatedTabularFil } public void produceContinuousSummaryStatistics(DataFile dataFile, File generatedTabularFile) throws IOException { - - /* - // quick, but memory-inefficient way: - // - this method just loads the entire file-worth of continuous vectors - // into a Double[][] matrix. - //Double[][] variableVectors = subsetContinuousVectors(dataFile); - //calculateContinuousSummaryStatistics(dataFile, variableVectors); - - // A more sophisticated way: this subsets one column at a time, using - // the new optimized subsetting that does not have to read any extra - // bytes from the file to extract the column: - - TabularSubsetGenerator subsetGenerator = new TabularSubsetGenerator(); - */ for (int i = 0; i < dataFile.getDataTable().getVarQuantity(); i++) { if (dataFile.getDataTable().getDataVariables().get(i).isIntervalContinuous()) { logger.fine("subsetting continuous vector"); if ("float".equals(dataFile.getDataTable().getDataVariables().get(i).getFormat())) { - Float[] variableVector = TabularSubsetGenerator.subsetFloatVector(new FileInputStream(generatedTabularFile), i, dataFile.getDataTable().getCaseQuantity().intValue()); + Float[] variableVector = TabularSubsetGenerator.subsetFloatVector( + new FileInputStream(generatedTabularFile), + i, + dataFile.getDataTable().getCaseQuantity().intValue(), + dataFile.getDataTable().isStoredWithVariableHeader()); logger.fine("Calculating summary statistics on a Float vector;"); calculateContinuousSummaryStatistics(dataFile, i, variableVector); // calculate the UNF while we are at it: @@ -754,7 +745,11 @@ public void produceContinuousSummaryStatistics(DataFile dataFile, File generated calculateUNF(dataFile, i, variableVector); variableVector = null; } else { - Double[] variableVector = TabularSubsetGenerator.subsetDoubleVector(new FileInputStream(generatedTabularFile), i, dataFile.getDataTable().getCaseQuantity().intValue()); + Double[] variableVector = TabularSubsetGenerator.subsetDoubleVector( + new FileInputStream(generatedTabularFile), + i, + dataFile.getDataTable().getCaseQuantity().intValue(), + dataFile.getDataTable().isStoredWithVariableHeader()); logger.fine("Calculating summary statistics on a Double vector;"); calculateContinuousSummaryStatistics(dataFile, i, variableVector); // calculate the UNF while we are at it: @@ -776,7 +771,11 @@ public void produceDiscreteNumericSummaryStatistics(DataFile dataFile, File gene && dataFile.getDataTable().getDataVariables().get(i).isTypeNumeric()) { logger.fine("subsetting discrete-numeric vector"); - Long[] variableVector = TabularSubsetGenerator.subsetLongVector(new FileInputStream(generatedTabularFile), i, dataFile.getDataTable().getCaseQuantity().intValue()); + Long[] variableVector = TabularSubsetGenerator.subsetLongVector( + new FileInputStream(generatedTabularFile), + i, + dataFile.getDataTable().getCaseQuantity().intValue(), + dataFile.getDataTable().isStoredWithVariableHeader()); // We are discussing calculating the same summary stats for // all numerics (the same kind of sumstats that we've been calculating // for numeric continuous type) -- L.A. Jul. 2014 @@ -810,7 +809,11 @@ public void produceCharacterSummaryStatistics(DataFile dataFile, File generatedT if (dataFile.getDataTable().getDataVariables().get(i).isTypeCharacter()) { logger.fine("subsetting character vector"); - String[] variableVector = TabularSubsetGenerator.subsetStringVector(new FileInputStream(generatedTabularFile), i, dataFile.getDataTable().getCaseQuantity().intValue()); + String[] variableVector = TabularSubsetGenerator.subsetStringVector( + new FileInputStream(generatedTabularFile), + i, + dataFile.getDataTable().getCaseQuantity().intValue(), + dataFile.getDataTable().isStoredWithVariableHeader()); //calculateCharacterSummaryStatistics(dataFile, i, variableVector); // calculate the UNF while we are at it: logger.fine("Calculating UNF on a String vector"); @@ -828,20 +831,29 @@ public static void produceFrequencyStatistics(DataFile dataFile, File generatedT produceFrequencies(generatedTabularFile, vars); } - public static void produceFrequencies( File generatedTabularFile, List vars) throws IOException { + public static void produceFrequencies(File generatedTabularFile, List vars) throws IOException { for (int i = 0; i < vars.size(); i++) { Collection cats = vars.get(i).getCategories(); int caseQuantity = vars.get(i).getDataTable().getCaseQuantity().intValue(); boolean isNumeric = vars.get(i).isTypeNumeric(); + boolean skipVariableHeaderLine = vars.get(i).getDataTable().isStoredWithVariableHeader(); Object[] variableVector = null; if (cats.size() > 0) { if (isNumeric) { - variableVector = TabularSubsetGenerator.subsetFloatVector(new FileInputStream(generatedTabularFile), i, caseQuantity); + variableVector = TabularSubsetGenerator.subsetFloatVector( + new FileInputStream(generatedTabularFile), + i, + caseQuantity, + skipVariableHeaderLine); } else { - variableVector = TabularSubsetGenerator.subsetStringVector(new FileInputStream(generatedTabularFile), i, caseQuantity); + variableVector = TabularSubsetGenerator.subsetStringVector( + new FileInputStream(generatedTabularFile), + i, + caseQuantity, + skipVariableHeaderLine); } if (variableVector != null) { Hashtable freq = calculateFrequency(variableVector); @@ -923,6 +935,7 @@ public boolean ingestAsTabular(Long datafile_id) { DataFile dataFile = fileService.find(datafile_id); boolean ingestSuccessful = false; boolean forceTypeCheck = false; + boolean storingWithVariableHeader = systemConfig.isStoringIngestedFilesWithHeaders(); // Never attempt to ingest a file that's already ingested! if (dataFile.isTabularData()) { @@ -1024,11 +1037,7 @@ public boolean ingestAsTabular(Long datafile_id) { TabularDataIngest tabDataIngest = null; try { - if (additionalData != null) { - tabDataIngest = ingestPlugin.read(inputStream, additionalData); - } else { - tabDataIngest = ingestPlugin.read(inputStream, null); - } + tabDataIngest = ingestPlugin.read(inputStream, storingWithVariableHeader, additionalData); } catch (IOException ingestEx) { dataFile.SetIngestProblem(); FileUtil.createIngestFailureReport(dataFile, ingestEx.getMessage()); @@ -1081,6 +1090,7 @@ public boolean ingestAsTabular(Long datafile_id) { dataFile.setDataTable(tabDataIngest.getDataTable()); tabDataIngest.getDataTable().setDataFile(dataFile); tabDataIngest.getDataTable().setOriginalFileName(originalFileName); + dataFile.getDataTable().setStoredWithVariableHeader(storingWithVariableHeader); try { produceSummaryStatistics(dataFile, tabFile); @@ -1172,6 +1182,7 @@ public boolean ingestAsTabular(Long datafile_id) { // Replace contents of the file with the tab-delimited data produced: dataAccess.savePath(Paths.get(tabFile.getAbsolutePath())); + // Reset the file size: dataFile.setFilesize(dataAccess.getSize()); @@ -2297,7 +2308,7 @@ public static void main(String[] args) { TabularDataIngest tabDataIngest = null; try { - tabDataIngest = ingestPlugin.read(fileInputStream, null); + tabDataIngest = ingestPlugin.read(fileInputStream, false, null); } catch (IOException ingestEx) { System.err.println("Caught an exception trying to ingest file "+file+"."); System.exit(1); diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestableDataChecker.java b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestableDataChecker.java index 9b62b62fe61..fa83552a9ec 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestableDataChecker.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestableDataChecker.java @@ -24,6 +24,7 @@ import java.io.*; import java.nio.*; import java.nio.channels.*; +import java.nio.charset.StandardCharsets; import java.util.*; import java.lang.reflect.*; import java.util.regex.*; @@ -252,7 +253,7 @@ public String testDTAformat(MappedByteBuffer buff) { try { headerBuffer = new byte[STATA_13_HEADER.length()]; buff.get(headerBuffer, 0, STATA_13_HEADER.length()); - headerString = new String(headerBuffer, "US-ASCII"); + headerString = new String(headerBuffer, StandardCharsets.US_ASCII); } catch (Exception ex) { // probably a buffer underflow exception; // we don't have to do anything... null will @@ -273,7 +274,7 @@ public String testDTAformat(MappedByteBuffer buff) { try { headerBuffer = new byte[STATA_14_HEADER.length()]; buff.get(headerBuffer, 0, STATA_14_HEADER.length()); - headerString = new String(headerBuffer, "US-ASCII"); + headerString = new String(headerBuffer, StandardCharsets.US_ASCII); } catch (Exception ex) { // probably a buffer underflow exception; // we don't have to do anything... null will @@ -292,7 +293,7 @@ public String testDTAformat(MappedByteBuffer buff) { try { headerBuffer = new byte[STATA_15_HEADER.length()]; buff.get(headerBuffer, 0, STATA_15_HEADER.length()); - headerString = new String(headerBuffer, "US-ASCII"); + headerString = new String(headerBuffer, StandardCharsets.US_ASCII); } catch (Exception ex) { // probably a buffer underflow exception; // we don't have to do anything... null will diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/TabularDataFileReader.java b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/TabularDataFileReader.java index 223b171dfb5..0f23a3d9781 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/TabularDataFileReader.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/TabularDataFileReader.java @@ -20,10 +20,13 @@ package edu.harvard.iq.dataverse.ingest.tabulardata; +import edu.harvard.iq.dataverse.datavariable.DataVariable; import edu.harvard.iq.dataverse.ingest.tabulardata.spi.*; //import edu.harvard.iq.dataverse.ingest.plugin.metadata.*; import java.io.*; import static java.lang.System.*; +import java.util.Iterator; +import java.util.List; import java.util.regex.Matcher; /** @@ -98,7 +101,7 @@ public void setDataLanguageEncoding(String dataLanguageEncoding) { * * @throws java.io.IOException if a reading error occurs. */ - public abstract TabularDataIngest read(BufferedInputStream stream, File dataFile) + public abstract TabularDataIngest read(BufferedInputStream stream, boolean storeWithVariableHeader, File dataFile) throws IOException; @@ -176,5 +179,26 @@ protected String escapeCharacterString(String rawString) { return escapedString; } + + protected String generateVariableHeader(List dvs) { + String varHeader = null; + + if (dvs != null) { + Iterator iter = dvs.iterator(); + DataVariable dv; + + if (iter.hasNext()) { + dv = iter.next(); + varHeader = dv.getName(); + } + + while (iter.hasNext()) { + dv = iter.next(); + varHeader = varHeader + "\t" + dv.getName(); + } + } + + return varHeader; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/csv/CSVFileReader.java b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/csv/CSVFileReader.java index 57f76df3802..f8816ababb4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/csv/CSVFileReader.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/csv/CSVFileReader.java @@ -110,7 +110,7 @@ private void init() throws IOException { * @throws java.io.IOException if a reading error occurs. */ @Override - public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws IOException { + public TabularDataIngest read(BufferedInputStream stream, boolean saveWithVariableHeader, File dataFile) throws IOException { init(); if (stream == null) { @@ -124,7 +124,7 @@ public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws File tabFileDestination = File.createTempFile("data-", ".tab"); PrintWriter tabFileWriter = new PrintWriter(tabFileDestination.getAbsolutePath()); - int lineCount = readFile(localBufferedReader, dataTable, tabFileWriter); + int lineCount = readFile(localBufferedReader, dataTable, saveWithVariableHeader, tabFileWriter); logger.fine("Tab file produced: " + tabFileDestination.getAbsolutePath()); @@ -136,14 +136,17 @@ public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws } - public int readFile(BufferedReader csvReader, DataTable dataTable, PrintWriter finalOut) throws IOException { + public int readFile(BufferedReader csvReader, DataTable dataTable, boolean saveWithVariableHeader, PrintWriter finalOut) throws IOException { List variableList = new ArrayList<>(); CSVParser parser = new CSVParser(csvReader, inFormat.withHeader()); Map headers = parser.getHeaderMap(); int i = 0; + String variableNameHeader = null; + for (String varName : headers.keySet()) { + // @todo: is .keySet() guaranteed to return the names in the right order? if (varName == null || varName.isEmpty()) { // TODO: // Add a sensible variable name validation algorithm. @@ -158,6 +161,13 @@ public int readFile(BufferedReader csvReader, DataTable dataTable, PrintWriter f dv.setTypeCharacter(); dv.setIntervalDiscrete(); + + if (saveWithVariableHeader) { + variableNameHeader = variableNameHeader == null + ? varName + : variableNameHeader.concat("\t" + varName); + } + i++; } @@ -342,6 +352,14 @@ public int readFile(BufferedReader csvReader, DataTable dataTable, PrintWriter f try (BufferedReader secondPassReader = new BufferedReader(new FileReader(firstPassTempFile))) { parser = new CSVParser(secondPassReader, inFormat.withHeader()); String[] caseRow = new String[headers.size()]; + + // Save the variable name header, if requested + if (saveWithVariableHeader) { + if (variableNameHeader == null) { + throw new IOException("failed to generate the Variable Names header"); + } + finalOut.println(variableNameHeader); + } for (CSVRecord record : parser) { if (!record.isConsistent()) { diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/DTAFileReader.java b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/DTAFileReader.java index 2dec701592e..f0262af9e33 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/DTAFileReader.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/DTAFileReader.java @@ -29,6 +29,7 @@ import java.io.PrintWriter; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.ParseException; @@ -505,7 +506,7 @@ private void init() throws IOException { } @Override - public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws IOException { + public TabularDataIngest read(BufferedInputStream stream, boolean storeWithVariableHeader, File dataFile) throws IOException { dbgLog.info("***** DTAFileReader: read() start *****"); if (dataFile != null) { @@ -519,7 +520,7 @@ public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws if (releaseNumber!=104) { decodeExpansionFields(stream); } - decodeData(stream); + decodeData(stream, storeWithVariableHeader); decodeValueLabels(stream); ingesteddata.setDataTable(dataTable); @@ -685,7 +686,7 @@ private void decodeHeader(BufferedInputStream stream) throws IOException { } String data_label = new String(Arrays.copyOfRange(header, dl_offset, - (dl_offset + dataLabelLength)), "ISO-8859-1"); + (dl_offset + dataLabelLength)), StandardCharsets.ISO_8859_1); if (dbgLog.isLoggable(Level.FINE)) { dbgLog.fine("data_label_length=" + data_label.length()); @@ -710,7 +711,7 @@ private void decodeHeader(BufferedInputStream stream) throws IOException { if (releaseNumber > 104) { int ts_offset = dl_offset + dataLabelLength; String time_stamp = new String(Arrays.copyOfRange(header, ts_offset, - ts_offset + TIME_STAMP_LENGTH), "ISO-8859-1"); + ts_offset + TIME_STAMP_LENGTH), StandardCharsets.ISO_8859_1); if (dbgLog.isLoggable(Level.FINE)) { dbgLog.fine("time_stamp_length=" + time_stamp.length()); } @@ -912,7 +913,7 @@ private void decodeDescriptorVarNameList(BufferedInputStream stream, int nvar) t for (DataVariable dataVariable: dataTable.getDataVariables()) { offset_end += length_var_name; String vari = new String(Arrays.copyOfRange(variableNameBytes, offset_start, - offset_end), "ISO-8859-1"); + offset_end), StandardCharsets.ISO_8859_1); String varName = getNullStrippedString(vari); dataVariable.setName(varName); dbgLog.fine("next name=[" + varName + "]"); @@ -978,7 +979,7 @@ private void decodeDescriptorVariableFormat(BufferedInputStream stream, int nvar for (int i = 0; i < nvar; i++) { offset_end += length_var_format; String vari = new String(Arrays.copyOfRange(variableFormatList, offset_start, - offset_end), "ISO-8859-1"); + offset_end), StandardCharsets.ISO_8859_1); String variableFormat = getNullStrippedString(vari); if (dbgLog.isLoggable(Level.FINE)) dbgLog.fine(i + "-th format=[" + variableFormat + "]"); @@ -1045,7 +1046,7 @@ private void decodeDescriptorValueLabel(BufferedInputStream stream, int nvar) th for (int i = 0; i < nvar; i++) { offset_end += length_label_name; String vari = new String(Arrays.copyOfRange(labelNameList, offset_start, - offset_end), "ISO-8859-1"); + offset_end), StandardCharsets.ISO_8859_1); labelNames[i] = getNullStrippedString(vari); dbgLog.fine(i + "-th label=[" + labelNames[i] + "]"); offset_start = offset_end; @@ -1090,7 +1091,7 @@ private void decodeVariableLabels(BufferedInputStream stream) throws IOException for (int i = 0; i < nvar; i++) { offset_end += length_var_label; String vari = new String(Arrays.copyOfRange(variableLabelBytes, offset_start, - offset_end), "ISO-8859-1"); + offset_end), StandardCharsets.ISO_8859_1); String variableLabelParsed = getNullStrippedString(vari); if (dbgLog.isLoggable(Level.FINE)) { @@ -1272,7 +1273,7 @@ void parseValueLabelsRelease105(BufferedInputStream stream) throws IOException { valueLabelHeader, value_label_table_length, (value_label_table_length + length_label_name)), - "ISO-8859-1"); + StandardCharsets.ISO_8859_1); if (dbgLog.isLoggable(Level.FINE)) { dbgLog.fine("rawLabelName(length)=" + rawLabelName.length()); @@ -1335,7 +1336,7 @@ void parseValueLabelsRelease105(BufferedInputStream stream) throws IOException { for (int l = 0; l < no_value_label_pairs; l++) { String string_l = new String(Arrays.copyOfRange(valueLabelTable_i, offset_start, - offset_end), "ISO-8859-1"); + offset_end), StandardCharsets.ISO_8859_1); int null_position = string_l.indexOf(0); if (null_position != -1) { @@ -1485,7 +1486,7 @@ private void parseValueLabelsReleasel108(BufferedInputStream stream) throws IOEx valueLabelHeader, value_label_table_length, (value_label_table_length + length_label_name)), - "ISO-8859-1"); + StandardCharsets.ISO_8859_1); String labelName = getNullStrippedString(rawLabelName); if (dbgLog.isLoggable(Level.FINE)) { @@ -1581,7 +1582,7 @@ private void parseValueLabelsReleasel108(BufferedInputStream stream) throws IOEx String label_segment = new String( Arrays.copyOfRange(valueLabelTable_i, offset_value, - (length_label_segment + offset_value)), "ISO-8859-1"); + (length_label_segment + offset_value)), StandardCharsets.ISO_8859_1); // L.A. -- 2011.2.25: // This assumes that the labels are already stored in the right @@ -1665,7 +1666,7 @@ private void parseValueLabelsReleasel108(BufferedInputStream stream) throws IOEx dbgLog.fine("parseValueLabelsRelease108(): end"); } - private void decodeData(BufferedInputStream stream) throws IOException { + private void decodeData(BufferedInputStream stream, boolean saveWithVariableHeader) throws IOException { dbgLog.fine("\n***** decodeData(): start *****"); @@ -1701,7 +1702,7 @@ private void decodeData(BufferedInputStream stream) throws IOException { ingesteddata.setTabDelimitedFile(tabDelimitedDataFile); fileOutTab = new FileOutputStream(tabDelimitedDataFile); - pwout = new PrintWriter(new OutputStreamWriter(fileOutTab, "utf8"), true); + pwout = new PrintWriter(new OutputStreamWriter(fileOutTab, StandardCharsets.UTF_8), true); /* Should we lose this dateFormat thing in 4.0? * the UNF should be calculatable on the app side solely from the data @@ -1719,6 +1720,11 @@ private void decodeData(BufferedInputStream stream) throws IOException { BUT, this needs to be reviewed/confirmed etc! */ //String[][] dateFormat = new String[nvar][nobs]; + + // add the variable header here, if needed + if (saveWithVariableHeader) { + pwout.println(generateVariableHeader(dataTable.getDataVariables())); + } for (int i = 0; i < nobs; i++) { byte[] dataRowBytes = new byte[bytes_per_row]; @@ -1927,7 +1933,7 @@ private void decodeData(BufferedInputStream stream) throws IOException { // String case int strVarLength = StringLengthTable.get(columnCounter); String raw_datum = new String(Arrays.copyOfRange(dataRowBytes, byte_offset, - (byte_offset + strVarLength)), "ISO-8859-1"); + (byte_offset + strVarLength)), StandardCharsets.ISO_8859_1); // TODO: // is it the right thing to do, to default to "ISO-8859-1"? // (it may be; since there's no mechanism for specifying diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/DataReader.java b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/DataReader.java index 0822f6eed72..913c0ebeab2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/DataReader.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/DataReader.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.logging.Logger; @@ -273,7 +274,7 @@ public float readFloat() throws IOException { */ public String readString(int n) throws IOException { - String ret = new String(readBytes(n), "US-ASCII"); + String ret = new String(readBytes(n), StandardCharsets.US_ASCII); // Remove the terminating and/or padding zero bytes: if (ret != null && ret.indexOf(0) > -1) { @@ -287,7 +288,7 @@ public String readString(int n) throws IOException { */ public String readUtfString(int n) throws IOException { - String ret = new String(readBytes(n), "UTF8"); + String ret = new String(readBytes(n), StandardCharsets.UTF_8); // Remove the terminating and/or padding zero bytes: if (ret.indexOf(0) > -1) { @@ -314,11 +315,11 @@ public byte[] readPrimitiveSection(String tag, int length) throws IOException { } public String readPrimitiveStringSection(String tag) throws IOException { - return new String(readPrimitiveSection(tag), "US-ASCII"); + return new String(readPrimitiveSection(tag), StandardCharsets.US_ASCII); } public String readPrimitiveStringSection(String tag, int length) throws IOException { - return new String(readPrimitiveSection(tag, length), "US-ASCII"); + return new String(readPrimitiveSection(tag, length), StandardCharsets.US_ASCII); } public String readLabelSection(String tag, int limit) throws IOException { @@ -332,7 +333,7 @@ public String readLabelSection(String tag, int limit) throws IOException { logger.fine("length of label: " + lengthOfLabel); String label = null; if (lengthOfLabel > 0) { - label = new String(readBytes(lengthOfLabel), "US-ASCII"); + label = new String(readBytes(lengthOfLabel), StandardCharsets.US_ASCII); } logger.fine("ret: " + label); readClosingTag(tag); @@ -358,7 +359,7 @@ public String readDefinedStringSection(String tag, int limit) throws IOException } String ret = null; if (number > 0) { - ret = new String(readBytes(number), "US-ASCII"); + ret = new String(readBytes(number), StandardCharsets.US_ASCII); } logger.fine("ret: " + ret); readClosingTag(tag); @@ -400,7 +401,7 @@ public boolean checkTag(String tag) throws IOException { int n = tag.length(); if ((this.buffer_size - buffer_byte_offset) >= n) { - return (tag).equals(new String(Arrays.copyOfRange(buffer, buffer_byte_offset, buffer_byte_offset+n),"US-ASCII")); + return (tag).equals(new String(Arrays.copyOfRange(buffer, buffer_byte_offset, buffer_byte_offset+n),StandardCharsets.US_ASCII)); } else{ bufferMoreBytes(); @@ -414,7 +415,7 @@ public void readOpeningTag(String tag) throws IOException { throw new IOException("opening tag must be a non-empty string."); } - String openTagString = new String(readBytes(tag.length() + 2), "US-ASCII"); + String openTagString = new String(readBytes(tag.length() + 2), StandardCharsets.US_ASCII); if (openTagString == null || !openTagString.equals("<"+tag+">")) { throw new IOException("Could not read opening tag <"+tag+">"); } @@ -425,7 +426,7 @@ public void readClosingTag(String tag) throws IOException { throw new IOException("closing tag must be a non-empty string."); } - String closeTagString = new String(readBytes(tag.length() + 3), "US-ASCII"); + String closeTagString = new String(readBytes(tag.length() + 3), StandardCharsets.US_ASCII); logger.fine("closeTagString: " + closeTagString); if (closeTagString == null || !closeTagString.equals("")) { diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/NewDTAFileReader.java b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/NewDTAFileReader.java index 22581834676..b0f2c50c997 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/NewDTAFileReader.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/NewDTAFileReader.java @@ -7,6 +7,7 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.ParseException; @@ -339,7 +340,7 @@ private void init() throws IOException { } @Override - public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws IOException { + public TabularDataIngest read(BufferedInputStream stream, boolean storeWithVariableHeader, File dataFile) throws IOException { logger.fine("NewDTAFileReader: read() start"); // shit ton of diagnostics (still) needed here!! -- L.A. @@ -363,7 +364,13 @@ public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws // "characteristics" - STATA-proprietary information // (we are skipping it) readCharacteristics(dataReader); - readData(dataReader); + + String variableHeaderLine = null; + + if (storeWithVariableHeader) { + variableHeaderLine = generateVariableHeader(dataTable.getDataVariables()); + } + readData(dataReader, variableHeaderLine); // (potentially) large, (potentially) non-ASCII character strings // saved outside the section, and referenced @@ -707,7 +714,7 @@ private void readCharacteristics(DataReader reader) throws IOException { } - private void readData(DataReader reader) throws IOException { + private void readData(DataReader reader, String variableHeaderLine) throws IOException { logger.fine("Data section; at offset " + reader.getByteOffset() + "; dta map offset: " + dtaMap.getOffset_data()); logger.fine("readData(): start"); reader.readOpeningTag(TAG_DATA); @@ -729,8 +736,13 @@ private void readData(DataReader reader) throws IOException { ingesteddata.setTabDelimitedFile(tabDelimitedDataFile); FileOutputStream fileOutTab = new FileOutputStream(tabDelimitedDataFile); - PrintWriter pwout = new PrintWriter(new OutputStreamWriter(fileOutTab, "utf8"), true); + PrintWriter pwout = new PrintWriter(new OutputStreamWriter(fileOutTab, StandardCharsets.UTF_8), true); + // add the variable header here, if needed + if (variableHeaderLine != null) { + pwout.println(variableHeaderLine); + } + logger.fine("Beginning to read data stream."); for (int i = 0; i < nobs; i++) { @@ -990,7 +1002,7 @@ private void readSTRLs(DataReader reader) throws IOException { File finalTabFile = File.createTempFile("finalTabfile.", ".tab"); FileOutputStream fileOutTab = new FileOutputStream(finalTabFile); - PrintWriter pwout = new PrintWriter(new OutputStreamWriter(fileOutTab, "utf8"), true); + PrintWriter pwout = new PrintWriter(new OutputStreamWriter(fileOutTab, StandardCharsets.UTF_8), true); logger.fine("Setting the tab-delimited file to " + finalTabFile.getName()); ingesteddata.setTabDelimitedFile(finalTabFile); @@ -999,6 +1011,8 @@ private void readSTRLs(DataReader reader) throws IOException { int nobs = dataTable.getCaseQuantity().intValue(); String[] line; + + //@todo: adjust for the case of storing the file with the variable header for (int obsindex = 0; obsindex < nobs; obsindex++) { if (scanner.hasNext()) { @@ -1117,9 +1131,9 @@ private String readGSO(DataReader reader, long v, long o) throws IOException { String gsoString; if (binary) { - gsoString = new String(contents, "utf8"); + gsoString = new String(contents, StandardCharsets.UTF_8); } else { - gsoString = new String(contents, 0, (int) length - 1, "US-ASCII"); + gsoString = new String(contents, 0, (int) length - 1, StandardCharsets.US_ASCII); } logger.fine("GSO " + v + "," + o + ": " + gsoString); @@ -1213,7 +1227,7 @@ private void readValueLabels(DataReader reader) throws IOException { } label_length = (int)(label_end - label_offset); - category_value_labels[i] = new String(Arrays.copyOfRange(labelBytes, (int)label_offset, (int)label_end-1), "UTF8"); + category_value_labels[i] = new String(Arrays.copyOfRange(labelBytes, (int)label_offset, (int)label_end-1), StandardCharsets.UTF_8); total_label_bytes += label_length; } diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/por/PORFileReader.java b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/por/PORFileReader.java index c90b0ea6950..13325ca8f60 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/por/PORFileReader.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/por/PORFileReader.java @@ -31,7 +31,7 @@ import java.io.PrintWriter; import java.io.Writer; import java.nio.ByteBuffer; - +import java.nio.charset.StandardCharsets; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.SimpleDateFormat; @@ -180,7 +180,7 @@ private void init() throws IOException { } @Override - public TabularDataIngest read(BufferedInputStream stream, File additionalData) throws IOException{ + public TabularDataIngest read(BufferedInputStream stream, boolean storeWithVariableHeader, File additionalData) throws IOException{ dbgLog.fine("PORFileReader: read() start"); if (additionalData != null) { @@ -195,7 +195,7 @@ public TabularDataIngest read(BufferedInputStream stream, File additionalData) t BufferedReader bfReader = null; try { - bfReader = new BufferedReader(new InputStreamReader(new FileInputStream(tempPORfile.getAbsolutePath()), "US-ASCII")); + bfReader = new BufferedReader(new InputStreamReader(new FileInputStream(tempPORfile.getAbsolutePath()), StandardCharsets.US_ASCII)); if (bfReader == null){ dbgLog.fine("bfReader is null"); throw new IOException("bufferedReader is null"); @@ -226,7 +226,7 @@ public TabularDataIngest read(BufferedInputStream stream, File additionalData) t headerId = "8S"; } - decode(headerId, bfReader); + decode(headerId, bfReader, storeWithVariableHeader); // for last iteration @@ -382,7 +382,7 @@ public TabularDataIngest read(BufferedInputStream stream, File additionalData) t return ingesteddata; } - private void decode(String headerId, BufferedReader reader) throws IOException{ + private void decode(String headerId, BufferedReader reader, boolean storeWithVariableHeader) throws IOException{ if (headerId.equals("1")) decodeProductName(reader); else if (headerId.equals("2")) decodeLicensee(reader); else if (headerId.equals("3")) decodeFileLabel(reader); @@ -398,7 +398,7 @@ private void decode(String headerId, BufferedReader reader) throws IOException{ else if (headerId.equals("C")) decodeVariableLabel(reader); else if (headerId.equals("D")) decodeValueLabel(reader); else if (headerId.equals("E")) decodeDocument(reader); - else if (headerId.equals("F")) decodeData(reader); + else if (headerId.equals("F")) decodeData(reader, storeWithVariableHeader); } @@ -567,7 +567,7 @@ private File decodeHeader(BufferedInputStream stream) throws IOException { try { tempPORfile = File.createTempFile("tempPORfile.", ".por"); fileOutPOR = new FileOutputStream(tempPORfile); - fileWriter = new BufferedWriter(new OutputStreamWriter(fileOutPOR, "utf8")); + fileWriter = new BufferedWriter(new OutputStreamWriter(fileOutPOR, StandardCharsets.UTF_8)); porScanner = new Scanner(stream); // Because 64-bit and 32-bit machines decode POR's first 40-byte @@ -1099,7 +1099,7 @@ private void decodeDocument(BufferedReader reader) throws IOException { } - private void decodeData(BufferedReader reader) throws IOException { + private void decodeData(BufferedReader reader, boolean storeWithVariableHeader) throws IOException { dbgLog.fine("decodeData(): start"); // TODO: get rid of this "variableTypeFinal"; -- L.A. 4.0 beta int[] variableTypeFinal= new int[varQnty]; @@ -1115,7 +1115,7 @@ private void decodeData(BufferedReader reader) throws IOException { try { fileOutTab = new FileOutputStream(tabDelimitedDataFile); - pwout = new PrintWriter(new OutputStreamWriter(fileOutTab, "utf8"), true); + pwout = new PrintWriter(new OutputStreamWriter(fileOutTab, StandardCharsets.UTF_8), true); variableFormatTypeList = new String[varQnty]; for (int i = 0; i < varQnty; i++) { @@ -1126,6 +1126,9 @@ private void decodeData(BufferedReader reader) throws IOException { // contents (variable) checker concering decimals Arrays.fill(variableTypeFinal, 0); + if (storeWithVariableHeader) { + pwout.println(StringUtils.join(variableNameList, "\t")); + } // raw-case counter int j = 0; // case diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/rdata/RDATAFileReader.java b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/rdata/RDATAFileReader.java index eb1353fd792..215c7a5e6d2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/rdata/RDATAFileReader.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/rdata/RDATAFileReader.java @@ -22,12 +22,11 @@ import java.io.*; import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.text.*; import java.util.logging.*; import java.util.*; -import jakarta.inject.Inject; - // Rosuda Wrappers and Methods for R-calls to Rserve import edu.harvard.iq.dataverse.settings.JvmSettings; import org.rosuda.REngine.REXP; @@ -473,7 +472,7 @@ private void init() throws IOException { * @throws java.io.IOException if a reading error occurs. */ @Override - public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws IOException { + public TabularDataIngest read(BufferedInputStream stream, boolean saveWithVariableHeader, File dataFile) throws IOException { init(); @@ -504,12 +503,12 @@ public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws // created! // - L.A. RTabFileParser csvFileReader = new RTabFileParser('\t'); - BufferedReader localBufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(localCsvFile), "UTF-8")); + BufferedReader localBufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(localCsvFile), StandardCharsets.UTF_8)); File tabFileDestination = File.createTempFile("data-", ".tab"); - PrintWriter tabFileWriter = new PrintWriter(tabFileDestination.getAbsolutePath(), "UTF-8"); + PrintWriter tabFileWriter = new PrintWriter(tabFileDestination.getAbsolutePath(), StandardCharsets.UTF_8); - int lineCount = csvFileReader.read(localBufferedReader, dataTable, tabFileWriter); + int lineCount = csvFileReader.read(localBufferedReader, dataTable, saveWithVariableHeader, tabFileWriter); LOG.fine("RDATAFileReader: successfully read "+lineCount+" lines of tab-delimited data."); @@ -685,7 +684,7 @@ private static String readLocalResource(String path) { // Try opening a buffered reader stream try { - BufferedReader rd = new BufferedReader(new InputStreamReader(resourceStream, "UTF-8")); + BufferedReader rd = new BufferedReader(new InputStreamReader(resourceStream, StandardCharsets.UTF_8)); String line = null; while ((line = rd.readLine()) != null) { diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/rdata/RTabFileParser.java b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/rdata/RTabFileParser.java index f60b7733463..fbe7e401b57 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/rdata/RTabFileParser.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/rdata/RTabFileParser.java @@ -61,8 +61,8 @@ public RTabFileParser (char delimiterChar) { // should be used. - public int read(BufferedReader csvReader, DataTable dataTable, PrintWriter pwout) throws IOException { - dbgLog.warning("RTabFileParser: Inside R Tab file parser"); + public int read(BufferedReader csvReader, DataTable dataTable, boolean saveWithVariableHeader, PrintWriter pwout) throws IOException { + dbgLog.fine("RTabFileParser: Inside R Tab file parser"); int varQnty = 0; @@ -94,14 +94,17 @@ public int read(BufferedReader csvReader, DataTable dataTable, PrintWriter pwout boolean[] isTimeVariable = new boolean[varQnty]; boolean[] isBooleanVariable = new boolean[varQnty]; + String variableNameHeader = null; + if (dataTable.getDataVariables() != null) { for (int i = 0; i < varQnty; i++) { DataVariable var = dataTable.getDataVariables().get(i); if (var == null) { - // throw exception! + throw new IOException ("null dataVariable passed to the parser"); + } if (var.getType() == null) { - // throw exception! + throw new IOException ("null dataVariable type passed to the parser"); } if (var.isTypeCharacter()) { isCharacterVariable[i] = true; @@ -128,13 +131,24 @@ public int read(BufferedReader csvReader, DataTable dataTable, PrintWriter pwout } } } else { - // throw excepion "unknown variable format type" - ? + throw new IOException ("unknown dataVariable format passed to the parser"); } - + if (saveWithVariableHeader) { + variableNameHeader = variableNameHeader == null + ? var.getName() + : variableNameHeader.concat("\t" + var.getName()); + } } } else { - // throw exception! + throw new IOException ("null dataVariables list passed to the parser"); + } + + if (saveWithVariableHeader) { + if (variableNameHeader == null) { + throw new IOException ("failed to generate the Variable Names header"); + } + pwout.println(variableNameHeader); } while ((line = csvReader.readLine()) != null) { diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/sav/SAVFileReader.java b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/sav/SAVFileReader.java index 682b8f1166c..308ff352b2a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/sav/SAVFileReader.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/sav/SAVFileReader.java @@ -29,7 +29,7 @@ import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.ByteOrder; - +import java.nio.charset.StandardCharsets; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.SimpleDateFormat; @@ -58,10 +58,7 @@ import edu.harvard.iq.dataverse.DataTable; import edu.harvard.iq.dataverse.datavariable.DataVariable; -import edu.harvard.iq.dataverse.datavariable.SummaryStatistic; import edu.harvard.iq.dataverse.datavariable.VariableCategory; -import edu.harvard.iq.dataverse.datavariable.VariableRange; - import edu.harvard.iq.dataverse.ingest.tabulardata.TabularDataFileReader; import edu.harvard.iq.dataverse.ingest.tabulardata.spi.TabularDataFileReaderSpi; import edu.harvard.iq.dataverse.ingest.tabulardata.TabularDataIngest; @@ -338,7 +335,7 @@ private void init() throws IOException { } } - public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws IOException{ + public TabularDataIngest read(BufferedInputStream stream, boolean storeWithVariableHeader, File dataFile) throws IOException{ dbgLog.info("SAVFileReader: read() start"); if (dataFile != null) { @@ -422,7 +419,7 @@ public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws methodCurrentlyExecuted = "decodeRecordTypeData"; dbgLog.fine("***** SAVFileReader: executing method decodeRecordTypeData"); - decodeRecordTypeData(stream); + decodeRecordTypeData(stream, storeWithVariableHeader); } catch (IllegalArgumentException e) { @@ -633,7 +630,7 @@ void decodeRecordType1(BufferedInputStream stream) throws IOException { int offset_end = LENGTH_SPSS_PRODUCT_INFO; // 60 bytes String productInfo = new String(Arrays.copyOfRange(recordType1, offset_start, - offset_end),"US-ASCII"); + offset_end),StandardCharsets.US_ASCII); dbgLog.fine("productInfo:\n"+productInfo+"\n"); dataTable.setOriginalFormatVersion(productInfo); @@ -872,7 +869,7 @@ void decodeRecordType1(BufferedInputStream stream) throws IOException { offset_end += LENGTH_FILE_CREATION_INFO; // 84 bytes String fileCreationInfo = getNullStrippedString(new String(Arrays.copyOfRange(recordType1, offset_start, - offset_end),"US-ASCII")); + offset_end),StandardCharsets.US_ASCII)); dbgLog.fine("fileCreationInfo:\n"+fileCreationInfo+"\n"); @@ -1220,7 +1217,7 @@ void decodeRecordType2(BufferedInputStream stream) throws IOException { // borders. So we always read the bytes, but only use them for // the real variable entries. /*String variableLabel = new String(Arrays.copyOfRange(variable_label, - 0, rawVariableLabelLength),"US-ASCII");*/ + 0, rawVariableLabelLength),StandardCharsets.US_ASCII);*/ variableLabelMap.put(variableName, variableLabel); } @@ -2075,7 +2072,7 @@ void decodeRecordType7(BufferedInputStream stream) throws IOException { byte[] work = new byte[unitLength*numberOfUnits]; int nbtyes13 = stream.read(work); - String[] variableShortLongNamePairs = new String(work,"US-ASCII").split("\t"); + String[] variableShortLongNamePairs = new String(work,StandardCharsets.US_ASCII).split("\t"); for (int i=0; i 1) { + throw new EJBException("More than one MakeDataCount Process State record found for YearMonth " + yearMonth + "."); + } + if (resultList.size() == 1) { + mdcps = (MakeDataCountProcessState) resultList.get(0); + } + return mdcps; + } + + public MakeDataCountProcessState setMakeDataCountProcessState(String yearMonth, String state) { + MakeDataCountProcessState mdcps = getMakeDataCountProcessState(yearMonth); + if (mdcps == null) { + mdcps = new MakeDataCountProcessState(yearMonth, state); + } else { + mdcps.setState(state); + } + return em.merge(mdcps); + } + + public boolean deleteMakeDataCountProcessState(String yearMonth) { + MakeDataCountProcessState mdcps = getMakeDataCountProcessState(yearMonth); + if (mdcps == null) { + return false; + } else { + em.remove(mdcps); + em.flush(); + return true; + } + } + + private void validateYearMonth(String yearMonth) { + // Check yearMonth format. either yyyy-mm or yyyy-mm-dd + if (yearMonth == null || (!yearMonth.matches("\\d{4}-\\d{2}") && !yearMonth.matches("\\d{4}-\\d{2}-\\d{2}"))) { + throw new IllegalArgumentException("YEAR-MONTH date format must be either yyyy-mm or yyyy-mm-dd"); + } + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountUtil.java b/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountUtil.java index 8f32750f090..30cbed18337 100644 --- a/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountUtil.java @@ -27,7 +27,7 @@ * How to Make Your Data Count July 10th, 2018). * * The recommended starting point to implement Make Data Count is - * https://github.com/CDLUC3/Make-Data-Count/blob/master/getting-started.md + * https://github.com/gdcc/Make-Data-Count/blob/master/getting-started.md * which specifically recommends reading the "COUNTER Code of Practice for * Research Data" mentioned in the user facing docs. * @@ -35,7 +35,7 @@ * https://dash.ucmerced.edu/stash/dataset/doi:10.6071/M3RP49 * * For processing logs we could try DASH's - * https://github.com/CDLUC3/counter-processor + * https://github.com/gdcc/counter-processor * * Next, DataOne implemented it, and you can see an example dataset here: * https://search.dataone.org/view/doi:10.5063/F1Z899CZ diff --git a/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsServiceBean.java index 1b5619c53e0..a74474efa15 100644 --- a/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsServiceBean.java @@ -572,6 +572,54 @@ public JsonArray uniqueDatasetDownloads(String yyyymm, Dataverse d) { } + //Accounts + + /* + * + * @param yyyymm Month in YYYY-MM format. + */ + public long accountsToMonth(String yyyymm) throws ParseException { + Query query = em.createNativeQuery("" + + "select count(authenticateduser.id)\n" + + "from authenticateduser\n" + + "where authenticateduser.createdtime is not null\n" + + "and date_trunc('month', createdtime) <= to_date('" + yyyymm + "','YYYY-MM');" + ); + logger.log(Level.FINE, "Metric query: {0}", query); + + return (long) query.getSingleResult(); + } + + /* + * + * @param days interval since the current date to list + * the number of user accounts created + */ + public long accountsPastDays(int days) { + Query query = em.createNativeQuery("" + + "select count(id)\n" + + "from authenticateduser\n" + + "where authenticateduser.createdtime is not null\n" + + "and authenticateduser.createdtime > current_date - interval '" + days + "' day;" + ); + logger.log(Level.FINE, "Metric query: {0}", query); + + return (long) query.getSingleResult(); + } + + public JsonArray accountsTimeSeries() { + Query query = em.createNativeQuery("" + + "select distinct to_char(au.createdtime, 'YYYY-MM'), count(id)\n" + + "from authenticateduser as au\n" + + "where au.createdtime is not null\n" + + "group by to_char(au.createdtime, 'YYYY-MM')\n" + + "order by to_char(au.createdtime, 'YYYY-MM');"); + + logger.log(Level.FINE, "Metric query: {0}", query); + List results = query.getResultList(); + return MetricsUtil.timeSeriesToJson(results); + } + //MDC diff --git a/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsUtil.java b/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsUtil.java index 74bb53e1191..7d968e7e5c1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsUtil.java @@ -1,7 +1,8 @@ package edu.harvard.iq.dataverse.metrics; import edu.harvard.iq.dataverse.Dataverse; -import java.io.StringReader; +import edu.harvard.iq.dataverse.util.json.JsonUtil; + import java.math.BigDecimal; import java.time.LocalDate; import java.time.YearMonth; @@ -17,28 +18,30 @@ import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; -import jakarta.json.JsonReader; +import jakarta.json.JsonException; import jakarta.ws.rs.BadRequestException; public class MetricsUtil { private static final Logger logger = Logger.getLogger(MetricsUtil.class.getCanonicalName()); - public final static String CONTENTTYPE = "contenttype"; - public final static String COUNT = "count"; - public final static String CATEGORY = "category"; - public final static String ID = "id"; - public final static String PID = "pid"; - public final static String SUBJECT = "subject"; - public final static String DATE = "date"; - public final static String SIZE = "size"; + public static final String CONTENTTYPE = "contenttype"; + public static final String COUNT = "count"; + public static final String CATEGORY = "category"; + public static final String ID = "id"; + public static final String PID = "pid"; + public static final String SUBJECT = "subject"; + public static final String DATE = "date"; + public static final String SIZE = "size"; - public static String YEAR_AND_MONTH_PATTERN = "yyyy-MM"; + public static final String YEAR_AND_MONTH_PATTERN = "yyyy-MM"; public static final String DATA_LOCATION_LOCAL = "local"; public static final String DATA_LOCATION_REMOTE = "remote"; public static final String DATA_LOCATION_ALL = "all"; + private MetricsUtil() {} + public static JsonObjectBuilder countToJson(long count) { JsonObjectBuilder job = Json.createObjectBuilder(); job.add(COUNT, count); @@ -134,8 +137,8 @@ public static JsonArray timeSeriesToJson(List results, boolean isBigDe public static JsonArray timeSeriesByTypeToJson(List results) { JsonArrayBuilder jab = Json.createArrayBuilder(); - Map totals = new HashMap(); - Map sizes = new HashMap(); + Map totals = new HashMap<>(); + Map sizes = new HashMap<>(); String curDate = (String) results.get(0)[0]; // Get a list of all the monthly dates from the start until now List dates = getDatesFrom(curDate); @@ -169,7 +172,7 @@ public static JsonArray timeSeriesByTypeToJson(List results) { public static JsonArray timeSeriesByPIDToJson(List results) { JsonArrayBuilder jab = Json.createArrayBuilder(); - Map totals = new HashMap(); + Map totals = new HashMap<>(); String curDate = (String) results.get(0)[0]; // Get a list of all the monthly dates from the start until now List dates = getDatesFrom(curDate); @@ -200,8 +203,8 @@ public static JsonArray timeSeriesByPIDToJson(List results) { public static JsonArray timeSeriesByIDAndPIDToJson(List results) { JsonArrayBuilder jab = Json.createArrayBuilder(); - Map totals = new HashMap(); - Map pids = new HashMap(); + Map totals = new HashMap<>(); + Map pids = new HashMap<>(); String curDate = (String) results.get(0)[0]; // Get a list of all the monthly dates from the start until now List dates = getDatesFrom(curDate); @@ -238,11 +241,11 @@ public static JsonArray timeSeriesByIDAndPIDToJson(List results) { /** * - * @param userInput A year and month in YYYY-MM format. - * @return A year and month in YYYY-M * Note that along with sanitization, this checks that the requested month is * not after the current one. This will need to be made more robust if we - * start writing metrics for farther in the future (e.g. the current year) the current year) + * start writing metrics for farther in the future (e.g. the current year) + * @param userInput A year and month in YYYY-MM format. + * @return A year and month in YYYY-M */ public static String sanitizeYearMonthUserInput(String userInput) throws BadRequestException { logger.fine("string from user to sanitize (hopefully YYYY-MM format): " + userInput); @@ -260,8 +263,7 @@ public static String sanitizeYearMonthUserInput(String userInput) throws BadRequ throw new BadRequestException("The requested date is set past the current month."); } - String sanitized = inputLocalDate.format(dateTimeFormatter); - return sanitized; + return inputLocalDate.format(dateTimeFormatter); } public static String validateDataLocationStringType(String dataLocation) throws BadRequestException { @@ -279,30 +281,38 @@ public static String getCurrentMonth() { return LocalDate.now().format(DateTimeFormatter.ofPattern(MetricsUtil.YEAR_AND_MONTH_PATTERN)); } + /** + * Parse a String into a JSON object + * @param str serialized JSON + * @return {@code null} if {@code str} is {@code null}, or the parsed JSON object + * @throws JsonException + * @see JsonUtil#getJsonObject(String) + */ public static JsonObject stringToJsonObject(String str) { if (str == null) { return null; } - JsonReader jsonReader = Json.createReader(new StringReader(str)); - JsonObject jo = jsonReader.readObject(); - jsonReader.close(); - return jo; + return JsonUtil.getJsonObject(str); } + /** + * Parse a String into a JSON array + * @param str serialized JSON + * @return {@code null} if {@code str} is {@code null}, or the parsed JSON array + * @throws JsonException + * @see JsonUtil#getJsonArray(String) + */ public static JsonArray stringToJsonArray(String str) { if (str == null) { return null; } - JsonReader jsonReader = Json.createReader(new StringReader(str)); - JsonArray ja = jsonReader.readArray(); - jsonReader.close(); - return ja; + return JsonUtil.getJsonArray(str); } public static List getDatesFrom(String startMonth) { - List dates = new ArrayList(); + List dates = new ArrayList<>(); LocalDate next = LocalDate.parse(startMonth+ "-01").plusMonths(1); dates.add(startMonth); DateTimeFormatter monthFormat = DateTimeFormatter.ofPattern(YEAR_AND_MONTH_PATTERN); diff --git a/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java b/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java index 0a64f42d840..6c99155d8a4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java +++ b/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java @@ -3,6 +3,7 @@ */ package edu.harvard.iq.dataverse.mydata; +import edu.harvard.iq.dataverse.DatasetServiceBean; import edu.harvard.iq.dataverse.DataverseRoleServiceBean; import edu.harvard.iq.dataverse.DataverseServiceBean; import edu.harvard.iq.dataverse.DataverseSession; @@ -26,10 +27,10 @@ import edu.harvard.iq.dataverse.search.SearchFields; import edu.harvard.iq.dataverse.search.SortBy; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.logging.Logger; -import java.util.Locale; import jakarta.ejb.EJB; import jakarta.inject.Inject; import jakarta.json.Json; @@ -63,7 +64,7 @@ public class DataRetrieverAPI extends AbstractApiBean { private static final String retrieveDataPartialAPIPath = "retrieve"; @Inject - DataverseSession session; + DataverseSession session; @EJB DataverseRoleServiceBean dataverseRoleService; @@ -81,6 +82,8 @@ public class DataRetrieverAPI extends AbstractApiBean { //MyDataQueryHelperServiceBean myDataQueryHelperServiceBean; @EJB GroupServiceBean groupService; + @EJB + DatasetServiceBean datasetService; private List roleList; private DataverseRolePermissionHelper rolePermissionHelper; @@ -274,9 +277,7 @@ public String retrieveMyDataAsJsonString( @QueryParam("dataset_valid") List datasetValidities) { boolean OTHER_USER = false; - String localeCode = session.getLocaleCode(); - String noMsgResultsFound = BundleUtil.getStringFromPropertyFile("dataretrieverAPI.noMsgResultsFound", - "Bundle", new Locale(localeCode)); + String noMsgResultsFound = BundleUtil.getStringFromBundle("dataretrieverAPI.noMsgResultsFound"); if ((session.getUser() != null) && (session.getUser().isAuthenticated())) { authUser = (AuthenticatedUser) session.getUser(); @@ -284,7 +285,10 @@ public String retrieveMyDataAsJsonString( try { authUser = getRequestAuthenticatedUserOrDie(crc); } catch (WrappedResponse e) { - return this.getJSONErrorString("Requires authentication. Please login.", "retrieveMyDataAsJsonString. User not found! Shouldn't be using this anyway"); + return this.getJSONErrorString( + BundleUtil.getStringFromBundle("dataretrieverAPI.authentication.required"), + BundleUtil.getStringFromBundle("dataretrieverAPI.authentication.required.opt") + ); } } @@ -297,7 +301,9 @@ public String retrieveMyDataAsJsonString( authUser = searchUser; OTHER_USER = true; } else { - return this.getJSONErrorString("No user found for: \"" + userIdentifier + "\"", null); + return this.getJSONErrorString( + BundleUtil.getStringFromBundle("dataretrieverAPI.user.not.found", Arrays.asList(userIdentifier)), + null); } } @@ -337,8 +343,7 @@ public String retrieveMyDataAsJsonString( myDataFinder = new MyDataFinder(rolePermissionHelper, roleAssigneeService, dvObjectServiceBean, - groupService, - noMsgResultsFound); + groupService); this.myDataFinder.runFindDataSteps(filterParams); if (myDataFinder.hasError()){ return this.getJSONErrorString(myDataFinder.getErrorMessage(), myDataFinder.getErrorMessage()); @@ -393,11 +398,14 @@ public String retrieveMyDataAsJsonString( } catch (SearchException ex) { solrQueryResponse = null; - this.logger.severe("Solr SearchException: " + ex.getMessage()); + logger.severe("Solr SearchException: " + ex.getMessage()); } - if (solrQueryResponse==null){ - return this.getJSONErrorString("Sorry! There was an error with the search service.", "Sorry! There was a SOLR Error"); + if (solrQueryResponse == null) { + return this.getJSONErrorString( + BundleUtil.getStringFromBundle("dataretrieverAPI.solr.error"), + BundleUtil.getStringFromBundle("dataretrieverAPI.solr.error.opt") + ); } // --------------------------------- @@ -491,9 +499,10 @@ private JsonArrayBuilder formatSolrDocs(SolrQueryResponse solrResponse, RoleTagR // ------------------------------------------- // (a) Get core card data from solr // ------------------------------------------- - myDataCardInfo = doc.getJsonForMyData(); - if (!doc.getEntity().isInstanceofDataFile()){ + myDataCardInfo = doc.getJsonForMyData(isValid(doc)); + + if (doc.getEntity() != null && !doc.getEntity().isInstanceofDataFile()){ String parentAlias = dataverseService.getParentAliasString(doc); myDataCardInfo.add("parent_alias",parentAlias); } @@ -514,4 +523,8 @@ private JsonArrayBuilder formatSolrDocs(SolrQueryResponse solrResponse, RoleTagR return jsonSolrDocsArrayBuilder; } + + private boolean isValid(SolrSearchResult result) { + return result.isValid(x -> true); + } } \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/mydata/MyDataFilterParams.java b/src/main/java/edu/harvard/iq/dataverse/mydata/MyDataFilterParams.java index 2ab248fcc0b..2acb93d37f5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/mydata/MyDataFilterParams.java +++ b/src/main/java/edu/harvard/iq/dataverse/mydata/MyDataFilterParams.java @@ -12,6 +12,7 @@ import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.search.SearchConstants; import edu.harvard.iq.dataverse.search.SearchFields; +import edu.harvard.iq.dataverse.util.BundleUtil; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -178,26 +179,25 @@ public List getRoleIds(){ } - - private void checkParams(){ - - if ((this.userIdentifier == null)||(this.userIdentifier.isEmpty())){ - this.addError("Sorry! No user was found!"); + private void checkParams() { + if ((this.userIdentifier == null) || (this.userIdentifier.isEmpty())) { + this.addError(BundleUtil.getStringFromBundle("myDataFilterParams.error.no.user")); return; } - if ((this.roleIds == null)||(this.roleIds.isEmpty())){ - this.addError("No results. Please select at least one Role."); + if ((this.roleIds == null) || (this.roleIds.isEmpty())) { + this.addError(BundleUtil.getStringFromBundle("myDataFilterParams.error.result.no.role")); return; } - if ((this.dvObjectTypes == null)||(this.dvObjectTypes.isEmpty())){ - this.addError("No results. Please select one of Dataverses, Datasets, Files."); + if ((this.dvObjectTypes == null) || (this.dvObjectTypes.isEmpty())) { + this.addError(BundleUtil.getStringFromBundle("myDataFilterParams.error.result.no.dvobject")); return; } - - if ((this.publicationStatuses == null)||(this.publicationStatuses.isEmpty())){ - this.addError("No results. Please select one of " + StringUtils.join(MyDataFilterParams.defaultPublishedStates, ", ") + "."); + + if ((this.publicationStatuses == null) || (this.publicationStatuses.isEmpty())) { + this.addError(BundleUtil.getStringFromBundle("dataretrieverAPI.user.not.found", + Arrays.asList(StringUtils.join(MyDataFilterParams.defaultPublishedStates, ", ")))); return; } } @@ -292,7 +292,7 @@ public String getSolrFragmentForPublicationStatus(){ } public String getSolrFragmentForDatasetValidity(){ - if ((this.datasetValidities == null) || (this.datasetValidities.isEmpty())){ + if ((this.datasetValidities == null) || (this.datasetValidities.isEmpty()) || (this.datasetValidities.size() > 1)){ return ""; } diff --git a/src/main/java/edu/harvard/iq/dataverse/mydata/MyDataFinder.java b/src/main/java/edu/harvard/iq/dataverse/mydata/MyDataFinder.java index 917884f3549..5626a442762 100644 --- a/src/main/java/edu/harvard/iq/dataverse/mydata/MyDataFinder.java +++ b/src/main/java/edu/harvard/iq/dataverse/mydata/MyDataFinder.java @@ -11,7 +11,9 @@ import edu.harvard.iq.dataverse.authorization.DataverseRolePermissionHelper; import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean; import edu.harvard.iq.dataverse.search.SearchFields; +import edu.harvard.iq.dataverse.util.BundleUtil; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -47,7 +49,6 @@ public class MyDataFinder { private RoleAssigneeServiceBean roleAssigneeService; private DvObjectServiceBean dvObjectServiceBean; private GroupServiceBean groupService; - private String noMsgResultsFound; //private RoleAssigneeServiceBean roleService = new RoleAssigneeServiceBean(); //private MyDataQueryHelperServiceBean myDataQueryHelperService; // -------------------- @@ -86,12 +87,11 @@ public class MyDataFinder { private List fileGrandparentFileIds = new ArrayList<>(); // dataverse has file permissions - public MyDataFinder(DataverseRolePermissionHelper rolePermissionHelper, RoleAssigneeServiceBean roleAssigneeService, DvObjectServiceBean dvObjectServiceBean, GroupServiceBean groupService, String _noMsgResultsFound) { + public MyDataFinder(DataverseRolePermissionHelper rolePermissionHelper, RoleAssigneeServiceBean roleAssigneeService, DvObjectServiceBean dvObjectServiceBean, GroupServiceBean groupService) { this.rolePermissionHelper = rolePermissionHelper; this.roleAssigneeService = roleAssigneeService; this.dvObjectServiceBean = dvObjectServiceBean; this.groupService = groupService; - this.noMsgResultsFound = _noMsgResultsFound; this.loadHarvestedDataverseIds(); } @@ -213,7 +213,7 @@ private List getSolrFilterQueries(boolean totalCountsOnly){ // ----------------------------------------------------------------- String dvObjectFQ = this.getSolrDvObjectFilterQuery(); if (dvObjectFQ ==null){ - this.addErrorMessage(noMsgResultsFound); + this.addErrorMessage(BundleUtil.getStringFromBundle("myDataFinder.error.result.empty")); return null; } filterQueries.add(dvObjectFQ); @@ -239,7 +239,7 @@ private List getSolrFilterQueries(boolean totalCountsOnly){ //fq=publicationStatus:"Unpublished"&fq=publicationStatus:"Draft" // ----------------------------------------------------------------- - // (4) FQ by dataset metadata vlidity + // (4) FQ by dataset metadata validity // ----------------------------------------------------------------- filterQueries.add(this.filterParams.getSolrFragmentForDatasetValidity()); //fq=datasetValid:(true OR false) @@ -286,7 +286,7 @@ public String getSolrDvObjectFilterQuery(){ if ((distinctEntityIds.isEmpty()) && (distinctParentIds.isEmpty())) { - this.addErrorMessage(noMsgResultsFound); + this.addErrorMessage(BundleUtil.getStringFromBundle("myDataFinder.error.result.empty")); return null; } @@ -430,24 +430,25 @@ public JsonArrayBuilder getListofSelectedRoles(){ } - private boolean runStep1RoleAssignments(){ + private boolean runStep1RoleAssignments() { List results = this.roleAssigneeService.getAssigneeAndRoleIdListFor(filterParams); //logger.info("runStep1RoleAssignments results: " + results.toString()); - if (results == null){ - this.addErrorMessage("Sorry, the EntityManager isn't working (still)."); + if (results == null) { + this.addErrorMessage(BundleUtil.getStringFromBundle("myDataFinder.error.result.null")); return false; - }else if (results.isEmpty()){ + } else if (results.isEmpty()) { List roleNames = this.rolePermissionHelper.getRoleNamesByIdList(this.filterParams.getRoleIds()); - if ((roleNames == null)||(roleNames.isEmpty())){ - this.addErrorMessage("Sorry, you have no assigned roles."); - }else{ - if (roleNames.size()==1){ - this.addErrorMessage("Sorry, nothing was found for this role: " + StringUtils.join(roleNames, ", ")); - }else{ - this.addErrorMessage("Sorry, nothing was found for these roles: " + StringUtils.join(roleNames, ", ")); + if ((roleNames == null) || (roleNames.isEmpty())) { + this.addErrorMessage(BundleUtil.getStringFromBundle("myDataFinder.error.result.no.role")); + } else { + final List args = Arrays.asList(StringUtils.join(roleNames, ", ")); + if (roleNames.size() == 1) { + this.addErrorMessage(BundleUtil.getStringFromBundle("myDataFinder.error.result.role.empty", args)); + } else { + this.addErrorMessage(BundleUtil.getStringFromBundle("myDataFinder.error.result.roles.empty", args)); } } return false; @@ -497,7 +498,7 @@ private boolean runStep2DirectAssignments(){ List results = this.dvObjectServiceBean.getDvObjectInfoForMyData(directDvObjectIds); //List results = this.roleAssigneeService.getAssignmentsFor(this.userIdentifier); if (results.isEmpty()){ - this.addErrorMessage("Sorry, you have no assigned Dataverses, Datasets, or Files."); + this.addErrorMessage(BundleUtil.getStringFromBundle("myDataFinder.error.result.no.dvobject")); return false; } diff --git a/src/main/java/edu/harvard/iq/dataverse/openapi/OpenApi.java b/src/main/java/edu/harvard/iq/dataverse/openapi/OpenApi.java new file mode 100644 index 00000000000..6bd54916e0d --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/openapi/OpenApi.java @@ -0,0 +1,101 @@ +package edu.harvard.iq.dataverse.openapi; + +import java.io.*; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.logging.*; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.*; +import jakarta.ws.rs.core.*; +import org.apache.commons.io.IOUtils; +import edu.harvard.iq.dataverse.api.Info; +import edu.harvard.iq.dataverse.util.BundleUtil; + +@WebServlet("/openapi") +public class OpenApi extends HttpServlet { + + private static final Logger logger = Logger.getLogger(Info.class.getCanonicalName()); + + private static final String YAML_FORMAT = "yaml"; + private static final String JSON_FORMAT = "json"; + + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + + String format = req.getParameter("format"); + String accept = req.getHeader("Accept"); + + /* + * We first check for the headers, if the request accepts application/json + * have to check for the format parameter, if it is different from json + * return BAD_REQUEST (400) + */ + if (MediaType.APPLICATION_JSON.equals(accept)){ + if (format != null && !JSON_FORMAT.equals(format)){ + List args = Arrays.asList(accept, format); + String bundleResponse = BundleUtil.getStringFromBundle("openapi.exception.unaligned", args); + resp.sendError(Response.Status.BAD_REQUEST.getStatusCode(), + bundleResponse); + return; + } else { + format = JSON_FORMAT; + } + } + + /* + * We currently support only JSON or YAML being the second the default + * if no format is specified, if a different format is specified we return + * UNSUPPORTED_MEDIA_TYPE (415) specifying that the format is not supported + */ + + format = format == null ? YAML_FORMAT : format.toLowerCase(); + + if (JSON_FORMAT.equals(format)) { + resp.setContentType(MediaType.APPLICATION_JSON_TYPE.toString()); + } else if (YAML_FORMAT.equals(format)){ + resp.setContentType(MediaType.TEXT_PLAIN_TYPE.toString()); + } else { + + List args = Arrays.asList(format); + String bundleResponse = BundleUtil.getStringFromBundle("openapi.exception.invalid.format", args); + + JsonObject errorResponse = Json.createObjectBuilder() + .add("status", "ERROR") + .add("code", HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE) + .add("message", bundleResponse) + .build(); + + resp.setContentType(MediaType.APPLICATION_JSON_TYPE.toString()); + resp.setStatus(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); + + PrintWriter responseWriter = resp.getWriter(); + responseWriter.println(errorResponse.toString()); + responseWriter.flush(); + return; + } + + try { + String baseFileName = "/META-INF/openapi." + format; + ClassLoader classLoader = this.getClass().getClassLoader(); + URL aliasesResource = classLoader.getResource(baseFileName); + InputStream openapiDefinitionStream = aliasesResource.openStream(); + String content = IOUtils.toString(openapiDefinitionStream, StandardCharsets.UTF_8); + resp.getWriter().write(content); + } catch (Exception e) { + logger.log(Level.SEVERE, "OpenAPI Definition format not found " + format + ":" + e.getMessage(), e); + String bundleResponse = BundleUtil.getStringFromBundle("openapi.exception"); + resp.sendError(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), + bundleResponse); + } + + + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/AbstractPidProvider.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/AbstractPidProvider.java new file mode 100644 index 00000000000..f6d142aac96 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/AbstractPidProvider.java @@ -0,0 +1,556 @@ +package edu.harvard.iq.dataverse.pidproviders; + +import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetField; +import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.GlobalId; +import edu.harvard.iq.dataverse.util.SystemConfig; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; + +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.lang3.RandomStringUtils; +import com.beust.jcommander.Strings; + +public abstract class AbstractPidProvider implements PidProvider { + + private static final Logger logger = Logger.getLogger(AbstractPidProvider.class.getCanonicalName()); + + public static String UNAVAILABLE = ":unav"; + public static final String SEPARATOR = "/"; + + protected PidProviderFactoryBean pidProviderService; + + private String protocol; + + private String authority = null; + + private String shoulder = null; + + private String identifierGenerationStyle = null; + + private String datafilePidFormat = null; + + private HashSet managedSet; + + private HashSet excludedSet; + + private String id; + private String label; + + protected AbstractPidProvider(String id, String label, String protocol) { + this.id = id; + this.label = label; + this.protocol = protocol; + this.managedSet = new HashSet(); + this.excludedSet = new HashSet(); + } + + protected AbstractPidProvider(String id, String label, String protocol, String authority, String shoulder, + String identifierGenerationStyle, String datafilePidFormat, String managedList, String excludedList) { + this.id = id; + this.label = label; + this.protocol = protocol; + this.authority = authority; + this.shoulder = shoulder; + this.identifierGenerationStyle = identifierGenerationStyle; + this.datafilePidFormat = datafilePidFormat; + this.managedSet = new HashSet(Arrays.asList(managedList.split(",\\s"))); + this.excludedSet = new HashSet(Arrays.asList(excludedList.split(",\\s"))); + if (logger.isLoggable(Level.FINE)) { + Iterator iter = managedSet.iterator(); + while (iter.hasNext()) { + logger.fine("managedSet in " + getId() + ": " + iter.next()); + } + iter = excludedSet.iterator(); + while (iter.hasNext()) { + logger.fine("excludedSet in " + getId() + ": " + iter.next()); + } + } + } + + @Override + public Map getMetadataForCreateIndicator(DvObject dvObjectIn) { + logger.log(Level.FINE, "getMetadataForCreateIndicator(DvObject)"); + Map metadata = new HashMap<>(); + metadata = addBasicMetadata(dvObjectIn, metadata); + metadata.put("datacite.publicationyear", generateYear(dvObjectIn)); + metadata.put("_target", getTargetUrl(dvObjectIn)); + return metadata; + } + + protected Map getUpdateMetadata(DvObject dvObjectIn) { + logger.log(Level.FINE, "getUpdateMetadataFromDataset"); + Map metadata = new HashMap<>(); + metadata = addBasicMetadata(dvObjectIn, metadata); + return metadata; + } + + protected Map addBasicMetadata(DvObject dvObjectIn, Map metadata) { + + String authorString = dvObjectIn.getAuthorString(); + if (authorString.isEmpty() || authorString.contains(DatasetField.NA_VALUE)) { + authorString = UNAVAILABLE; + } + + String producerString = pidProviderService.getProducer(); + + if (producerString.isEmpty() || producerString.equals(DatasetField.NA_VALUE)) { + producerString = UNAVAILABLE; + } + + String titleString = dvObjectIn.getCurrentName(); + + if (titleString.isEmpty() || titleString.equals(DatasetField.NA_VALUE)) { + titleString = UNAVAILABLE; + } + + metadata.put("datacite.creator", authorString); + metadata.put("datacite.title", titleString); + metadata.put("datacite.publisher", producerString); + metadata.put("datacite.publicationyear", generateYear(dvObjectIn)); + return metadata; + } + + protected Map addDOIMetadataForDestroyedDataset(DvObject dvObjectIn) { + Map metadata = new HashMap<>(); + String authorString = UNAVAILABLE; + String producerString = UNAVAILABLE; + String titleString = "This item has been removed from publication"; + + metadata.put("datacite.creator", authorString); + metadata.put("datacite.title", titleString); + metadata.put("datacite.publisher", producerString); + metadata.put("datacite.publicationyear", "9999"); + return metadata; + } + + protected String getTargetUrl(DvObject dvObjectIn) { + logger.log(Level.FINE, "getTargetUrl"); + return SystemConfig.getDataverseSiteUrlStatic() + dvObjectIn.getTargetUrl() + + dvObjectIn.getGlobalId().asString(); + } + + @Override + public String getIdentifier(DvObject dvObject) { + GlobalId gid = dvObject.getGlobalId(); + return gid != null ? gid.asString() : null; + } + + protected String generateYear(DvObject dvObjectIn) { + return dvObjectIn.getYearPublishedCreated(); + } + + public Map getMetadataForTargetURL(DvObject dvObject) { + logger.log(Level.FINE, "getMetadataForTargetURL"); + HashMap metadata = new HashMap<>(); + metadata.put("_target", getTargetUrl(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 generatePid(DvObject dvObject) { + + if (dvObject.getProtocol() == null) { + dvObject.setProtocol(getProtocol()); + } else { + if (!dvObject.getProtocol().equals(getProtocol())) { + logger.warning("The protocol of the DvObject (" + dvObject.getProtocol() + + ") does not match the configured protocol (" + getProtocol() + ")"); + throw new IllegalArgumentException("The protocol of the DvObject (" + dvObject.getProtocol() + + ") doesn't match that of the provider, id: " + getId()); + } + } + if (dvObject.getAuthority() == null) { + dvObject.setAuthority(getAuthority()); + } else { + if (!dvObject.getAuthority().equals(getAuthority())) { + logger.warning("The authority of the DvObject (" + dvObject.getAuthority() + + ") does not match the configured authority (" + getAuthority() + ")"); + throw new IllegalArgumentException("The authority of the DvObject (" + dvObject.getAuthority() + + ") doesn't match that of the provider, id: " + getId()); + } + } + if (dvObject.isInstanceofDataset()) { + dvObject.setIdentifier(generateDatasetIdentifier((Dataset) dvObject)); + } else { + dvObject.setIdentifier(generateDataFileIdentifier((DataFile) dvObject)); + } + return dvObject; + } + + private String generateDatasetIdentifier(Dataset dataset) { + String shoulder = getShoulder(); + + switch (getIdentifierGenerationStyle()) { + 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 (!pidProviderService.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) { + // Occasionally, the protocol separator character ':' comes in still + // URL-encoded as %3A (usually as a result of the URL having been + // encoded twice): + fullIdentifierString = fullIdentifierString.replace("%3A", ":"); + + 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) { + String authority; + String identifier; + if (identifierString == null) { + return null; + } + int index = identifierString.indexOf(getSeparator()); + 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 = PidProvider.formatIdentifierString(identifierString.substring(0, index)); + + if (PidProvider.testforNullTerminator(authority)) { + return null; + } + identifier = PidProvider.formatIdentifierString(identifierString.substring(index + 1)); + if (PidProvider.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) { + logger.fine("Parsing: " + protocol + ":" + authority + getSeparator() + identifier + " in " + getId()); + if (!PidProvider.isValidGlobalId(protocol, authority, identifier)) { + return null; + } + // Check authority/identifier if this is a provider that manages specific + // identifiers + // /is not one of the unmanaged providers that has null authority + if (getAuthority() != null) { + + String cleanIdentifier = protocol + ":" + authority + getSeparator() + identifier; + /* + * Test if this provider manages this identifier - return null if it does not. + * It does match if ((the identifier's authority and shoulder match the + * provider's), or the identifier is in the managed set), and, in either case, + * the identifier is not in the excluded set. + */ + logger.fine("clean pid in " + getId() + ": " + cleanIdentifier); + logger.fine("managed in " + getId() + ": " + getManagedSet().contains(cleanIdentifier)); + logger.fine("excluded from " + getId() + ": " + getExcludedSet().contains(cleanIdentifier)); + + if (!(((authority.equals(getAuthority()) && identifier.startsWith(getShoulder())) + || getManagedSet().contains(cleanIdentifier)) && !getExcludedSet().contains(cleanIdentifier))) { + return null; + } + } + return new GlobalId(protocol, authority, identifier, getSeparator(), getUrlPrefix(), getId()); + } + + public String getSeparator() { + // The standard default + return SEPARATOR; + } + + private String generateDataFileIdentifier(DataFile datafile) { + String doiDataFileFormat = getDatafilePidFormat(); + + String prepend = ""; + if (doiDataFileFormat.equals(SystemConfig.DataFilePIDFormat.DEPENDENT.toString())) { + // If format is dependent then pre-pend the dataset identifier + prepend = datafile.getOwner().getIdentifier() + SEPARATOR; + datafile.setProtocol(datafile.getOwner().getProtocol()); + datafile.setAuthority(datafile.getOwner().getAuthority()); + } else { + // If there's a shoulder prepend independent identifiers with it + prepend = getShoulder(); + datafile.setProtocol(getProtocol()); + datafile.setAuthority(getAuthority()); + } + + switch (getIdentifierGenerationStyle()) { + 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.getId()))); + + 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 = pidProviderService.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.getId()))); + + 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.getId()))); + + return identifier; + } + + + @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 void setPidProviderServiceBean(PidProviderFactoryBean pidProviderServiceBean) { + this.pidProviderService = pidProviderServiceBean; + } + + @Override + public String getProtocol() { + return protocol; + } + + @Override + public String getAuthority() { + return authority; + } + + @Override + public String getShoulder() { + return shoulder; + } + + @Override + public String getIdentifierGenerationStyle() { + return identifierGenerationStyle; + } + + @Override + public String getDatafilePidFormat() { + return datafilePidFormat; + } + + @Override + public Set getManagedSet() { + return managedSet; + } + + @Override + public Set getExcludedSet() { + return excludedSet; + } + + @Override + public String getId() { + return id; + } + + @Override + public String getLabel() { + return label; + } + + @Override + /** + * True if this provider can manage PIDs in general, this pid is not in the + * managedSet (meaning it is managed but the provider does not generally manage + * it's protocol/authority/separator/shoulder) and either this provider is the + * same as the pid's or we're allowed to create INDEPENDENT pids. The latter + * clause covers the potential case where the effective pid provider/generator + * for the dataset is set to a different one that handles the dataset's pid + * itself. In this case, we can create file PIDs if they are independent. + * + * @param pid - the related pid to check + * @return true if this provider can manage PIDs like the one supplied + */ + public boolean canCreatePidsLike(GlobalId pid) { + return canManagePID() && !managedSet.contains(pid.asString()) + && (getIdentifierGenerationStyle().equals("INDEPENDENT") || getId().equals(pid.getProviderId())); + } + + @Override + public JsonObject getProviderSpecification() { + JsonObjectBuilder providerSpecification = Json.createObjectBuilder(); + providerSpecification.add("id", id); + providerSpecification.add("label", label); + providerSpecification.add("protocol", protocol); + providerSpecification.add("authority", authority); + providerSpecification.add("separator", getSeparator()); + providerSpecification.add("shoulder", shoulder); + providerSpecification.add("identifierGenerationStyle", identifierGenerationStyle); + providerSpecification.add("datafilePidFormat", datafilePidFormat); + providerSpecification.add("managedSet", Strings.join(",", managedSet.toArray())); + providerSpecification.add("excludedSet", Strings.join(",", excludedSet.toArray())); + return providerSpecification.build(); + } + + @Override + public boolean updateIdentifier(DvObject dvObject) { + //By default, these are the same + return publicizeIdentifier(dvObject); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/PermaLinkPidProviderServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/PermaLinkPidProviderServiceBean.java deleted file mode 100644 index d145a7ec106..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/pidproviders/PermaLinkPidProviderServiceBean.java +++ /dev/null @@ -1,160 +0,0 @@ -package edu.harvard.iq.dataverse.pidproviders; - -import edu.harvard.iq.dataverse.AbstractGlobalIdServiceBean; -import edu.harvard.iq.dataverse.DvObject; -import edu.harvard.iq.dataverse.GlobalId; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; -import edu.harvard.iq.dataverse.settings.JvmSettings; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean.Key; -import edu.harvard.iq.dataverse.util.SystemConfig; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.logging.Logger; - -import jakarta.annotation.PostConstruct; -import jakarta.ejb.Stateless; - -/** - * PermaLink provider - * This is a minimalist permanent ID provider intended for use with 'real' datasets/files where the use case none-the-less doesn't lend itself to the use of DOIs or Handles, e.g. - * * due to cost - * * for a catalog/archive where Dataverse has a dataset representing a dataset with DOI/handle stored elsewhere - * - * The initial implementation will mint identifiers locally and will provide the existing page URLs (using the ?persistentID= format). - * This will be overridable by a configurable parameter to support use of an external resolver. - * - */ -@Stateless -public class PermaLinkPidProviderServiceBean extends AbstractGlobalIdServiceBean { - - private static final Logger logger = Logger.getLogger(PermaLinkPidProviderServiceBean.class.getCanonicalName()); - - public static final String PERMA_PROTOCOL = "perma"; - public static final String PERMA_PROVIDER_NAME = "PERMA"; - - //ToDo - handle dataset/file defaults for local system - public static final String PERMA_RESOLVER_URL = JvmSettings.PERMALINK_BASEURL - .lookupOptional() - .orElse(SystemConfig.getDataverseSiteUrlStatic()); - - String authority = null; - private String separator = ""; - - @PostConstruct - private void init() { - if(PERMA_PROTOCOL.equals(settingsService.getValueForKey(Key.Protocol))){ - authority = settingsService.getValueForKey(Key.Authority); - configured=true; - }; - - } - - - //Only used in PidUtilTest - haven't figured out how to mock a PostConstruct call directly - // ToDo - remove after work to allow more than one Pid Provider which is expected to not use stateless beans - public void reInit() { - init(); - } - - @Override - public String getSeparator() { - //The perma default - return separator; - } - - @Override - public boolean alreadyRegistered(GlobalId globalId, boolean noProviderDefault) { - // Perma doesn't manage registration, so we assume all local PIDs can be treated - // as registered - boolean existsLocally = !dvObjectService.isGlobalIdLocallyUnique(globalId); - return existsLocally ? existsLocally : noProviderDefault; - } - - @Override - public boolean registerWhenPublished() { - return false; - } - - @Override - public List getProviderInformation() { - return List.of(PERMA_PROVIDER_NAME, PERMA_RESOLVER_URL); - } - - @Override - public String createIdentifier(DvObject dvo) throws Throwable { - //Call external resolver and send landing URL? - //FWIW: Return value appears to only be used in RegisterDvObjectCommand where success requires finding the dvo identifier in this string. (Also logged a couple places). - return(dvo.getGlobalId().asString()); - } - - @Override - public Map getIdentifierMetadata(DvObject dvo) { - Map map = new HashMap<>(); - return map; - } - - @Override - public String modifyIdentifierTargetURL(DvObject dvo) throws Exception { - return getTargetUrl(dvo); - } - - @Override - public void deleteIdentifier(DvObject dvo) throws Exception { - // no-op - } - - @Override - public boolean publicizeIdentifier(DvObject dvObject) { - //Generate if needed (i.e. datafile case where we don't create/register early (even with reigsterWhenPublished == false)) - if(dvObject.getIdentifier() == null || dvObject.getIdentifier().isEmpty() ){ - dvObject = generateIdentifier(dvObject); - } - //Call external resolver and send landing URL? - return true; - } - - @Override - public GlobalId parsePersistentId(String pidString) { - //ToDo - handle local PID resolver for dataset/file - if (pidString.startsWith(getUrlPrefix())) { - pidString = pidString.replace(getUrlPrefix(), - (PERMA_PROTOCOL + ":")); - } - return super.parsePersistentId(pidString); - } - - @Override - public GlobalId parsePersistentId(String protocol, String identifierString) { - logger.fine("Checking Perma: " + identifierString); - if (!PERMA_PROTOCOL.equals(protocol)) { - return null; - } - String identifier = null; - if (authority != null) { - if (identifierString.startsWith(authority)) { - identifier = identifierString.substring(authority.length()); - } - } - identifier = GlobalIdServiceBean.formatIdentifierString(identifier); - if (GlobalIdServiceBean.testforNullTerminator(identifier)) { - return null; - } - return new GlobalId(PERMA_PROTOCOL, authority, identifier, separator, getUrlPrefix(), PERMA_PROVIDER_NAME); - } - - @Override - public GlobalId parsePersistentId(String protocol, String authority, String identifier) { - if (!PERMA_PROTOCOL.equals(protocol)) { - return null; - } - return super.parsePersistentId(protocol, authority, identifier); - } - - @Override - public String getUrlPrefix() { - - return PERMA_RESOLVER_URL + "/citation?persistentId=" + PERMA_PROTOCOL + ":"; - } -} diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/PidHelper.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/PidHelper.java deleted file mode 100644 index 5bc855a9593..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/pidproviders/PidHelper.java +++ /dev/null @@ -1,43 +0,0 @@ -package edu.harvard.iq.dataverse.pidproviders; - -import java.util.Arrays; -import jakarta.annotation.PostConstruct; -import jakarta.ejb.EJB; -import jakarta.ejb.Singleton; -import jakarta.ejb.Startup; - -import edu.harvard.iq.dataverse.DOIDataCiteServiceBean; -import edu.harvard.iq.dataverse.DOIEZIdServiceBean; -import edu.harvard.iq.dataverse.HandlenetServiceBean; - - /** - * This is a small helper bean - * As it is a singleton and built at application start (=deployment), it will inject the (stateless) - * dataverse service into the BrandingUtil once it's ready. - */ - @Startup - @Singleton - public class PidHelper { - - @EJB - DOIDataCiteServiceBean datacitePidSvc; - @EJB - DOIEZIdServiceBean ezidPidSvc; - @EJB - HandlenetServiceBean handlePidSvc; - @EJB - FakePidProviderServiceBean fakePidSvc; - @EJB - PermaLinkPidProviderServiceBean permaPidSvc; - @EJB - UnmanagedDOIServiceBean unmanagedDOISvc; - @EJB - UnmanagedHandlenetServiceBean unmanagedHandleSvc; - - @PostConstruct - public void listServices() { - PidUtil.addAllToProviderList(Arrays.asList(datacitePidSvc, ezidPidSvc, handlePidSvc, permaPidSvc, fakePidSvc)); - PidUtil.addAllToUnmanagedProviderList(Arrays.asList(unmanagedDOISvc, unmanagedHandleSvc)); - } - - } \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/GlobalIdServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/PidProvider.java similarity index 56% rename from src/main/java/edu/harvard/iq/dataverse/GlobalIdServiceBean.java rename to src/main/java/edu/harvard/iq/dataverse/pidproviders/PidProvider.java index aebf13778c3..194a51eeae0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/GlobalIdServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/PidProvider.java @@ -1,19 +1,16 @@ -package edu.harvard.iq.dataverse; +package edu.harvard.iq.dataverse.pidproviders; -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 edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.GlobalId; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; import java.util.*; -import java.util.function.Function; -import java.util.logging.Level; import java.util.logging.Logger; -public interface GlobalIdServiceBean { +public interface PidProvider { - static final Logger logger = Logger.getLogger(GlobalIdServiceBean.class.getCanonicalName()); + static final Logger logger = Logger.getLogger(PidProvider.class.getCanonicalName()); boolean alreadyRegistered(DvObject dvo) throws Exception; @@ -36,7 +33,6 @@ public interface GlobalIdServiceBean { boolean registerWhenPublished(); boolean canManagePID(); - boolean isConfigured(); List getProviderInformation(); @@ -52,36 +48,26 @@ public interface GlobalIdServiceBean { Map getMetadataForTargetURL(DvObject dvObject); - DvObject generateIdentifier(DvObject dvObject); + DvObject generatePid(DvObject dvObject); String getIdentifier(DvObject dvObject); boolean publicizeIdentifier(DvObject studyIn); - String generateDatasetIdentifier(Dataset dataset); - String generateDataFileIdentifier(DataFile datafile); + boolean updateIdentifier(DvObject dvObject); + 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 ) { - 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; - } - } - - static GlobalIdServiceBean getBean(CommandContext ctxt) { - return getBean(ctxt.settings().getValueForKey(Key.Protocol, ""), ctxt); - } + String getProtocol(); + String getProviderType(); + String getId(); + String getLabel(); + String getAuthority(); + String getShoulder(); + String getIdentifierGenerationStyle(); public static Optional parse(String identifierString) { try { @@ -111,6 +97,7 @@ public static Optional parse(String identifierString) { * {@code null} if parsing failed. */ public GlobalId parsePersistentId(String identifierString); + public GlobalId parsePersistentId(String protocol, String authority, String identifier); @@ -119,16 +106,16 @@ public static boolean isValidGlobalId(String protocol, String authority, String if (protocol == null || authority == null || identifier == null) { return false; } - if(!authority.equals(GlobalIdServiceBean.formatIdentifierString(authority))) { + if(!authority.equals(PidProvider.formatIdentifierString(authority))) { return false; } - if (GlobalIdServiceBean.testforNullTerminator(authority)) { + if (PidProvider.testforNullTerminator(authority)) { return false; } - if(!identifier.equals(GlobalIdServiceBean.formatIdentifierString(identifier))) { + if(!identifier.equals(PidProvider.formatIdentifierString(identifier))) { return false; } - if (GlobalIdServiceBean.testforNullTerminator(identifier)) { + if (PidProvider.testforNullTerminator(identifier)) { return false; } return true; @@ -177,40 +164,28 @@ static boolean checkDOIAuthority(String doiAuthority){ 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 - */ -class BeanDispatcher { - static final Map> DISPATCHER = new HashMap<>(); - - static { - DISPATCHER.put("hdl", ctxt->ctxt.handleNet() ); - DISPATCHER.put("doi", ctxt->{ - String doiProvider = ctxt.settings().getValueForKey(Key.DoiProvider, ""); - switch ( doiProvider ) { - case "EZID": return ctxt.doiEZId(); - case "DataCite": return ctxt.doiDataCite(); - case "FAKE": return ctxt.fakePidProvider(); - default: - logger.log(Level.SEVERE, "Unknown doiProvider: {0}", doiProvider); - return null; - } - }); - - DISPATCHER.put(PermaLinkPidProviderServiceBean.PERMA_PROTOCOL, ctxt->ctxt.permaLinkProvider() ); - } + + public void setPidProviderServiceBean(PidProviderFactoryBean pidProviderFactoryBean); + + String getDatafilePidFormat(); + + Set getManagedSet(); + + Set getExcludedSet(); + + /** + * Whether related pids can be created by this pid provider + * @see edu.harvard.iq.dataverse.pidproviders.AbstractPidProvider#canCreatePidsLike(GlobalId) more details in the abstract implementation + * + * @param pid + * @return - whether related pids can be created by this pid provider. + */ + boolean canCreatePidsLike(GlobalId pid); + + /** + * Returns a JSON representation of this pid provider including it's id, label, protocol, authority, separator, and identifier. + * @return + */ + public JsonObject getProviderSpecification(); + } \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/PidProviderFactory.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/PidProviderFactory.java new file mode 100644 index 00000000000..f7c1a4b9174 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/PidProviderFactory.java @@ -0,0 +1,8 @@ +package edu.harvard.iq.dataverse.pidproviders; + +public interface PidProviderFactory { + + String getType(); + + PidProvider createPidProvider(String id); +} diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/PidProviderFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/PidProviderFactoryBean.java new file mode 100644 index 00000000000..b01fb5e7eba --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/PidProviderFactoryBean.java @@ -0,0 +1,253 @@ +package edu.harvard.iq.dataverse.pidproviders; + +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.logging.Level; +import java.util.logging.Logger; + +import jakarta.annotation.PostConstruct; +import jakarta.ejb.EJB; +import jakarta.ejb.Singleton; +import jakarta.ejb.Startup; +import jakarta.inject.Inject; +import jakarta.json.JsonObject; +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.SystemConfig; +import edu.harvard.iq.dataverse.DatasetFieldServiceBean; +import edu.harvard.iq.dataverse.DataverseServiceBean; +import edu.harvard.iq.dataverse.DvObjectServiceBean; +import edu.harvard.iq.dataverse.GlobalId; +import edu.harvard.iq.dataverse.pidproviders.doi.UnmanagedDOIProvider; +import edu.harvard.iq.dataverse.pidproviders.doi.datacite.DataCiteDOIProvider; +import edu.harvard.iq.dataverse.pidproviders.doi.ezid.EZIdDOIProvider; +import edu.harvard.iq.dataverse.pidproviders.doi.fake.FakeDOIProvider; +import edu.harvard.iq.dataverse.pidproviders.handle.HandlePidProvider; +import edu.harvard.iq.dataverse.pidproviders.handle.UnmanagedHandlePidProvider; +import edu.harvard.iq.dataverse.pidproviders.perma.PermaLinkPidProvider; +import edu.harvard.iq.dataverse.pidproviders.perma.UnmanagedPermaLinkPidProvider; + +/** + * This Bean loads all of the PidProviderFactory types available (e.g. EZID, + * DataCite, Handle, PermaLink) and then reads the configuration to load + * particular PidProviders (e.g. a DataCite provider with a specific + * authority/shoulder, username/password, etc.) + */ +@Startup +@Singleton +public class PidProviderFactoryBean { + + private static final Logger logger = Logger.getLogger(PidProviderFactoryBean.class.getCanonicalName()); + + @Inject + DataverseServiceBean dataverseService; + @EJB + protected SettingsServiceBean settingsService; + @Inject + protected DvObjectServiceBean dvObjectService; + @Inject + SystemConfig systemConfig; + + private ServiceLoader loader; + private Map pidProviderFactoryMap = new HashMap<>(); + + @PostConstruct + public void init() { + loadProviderFactories(); + loadProviders(); + } + + private void loadProviderFactories() { + /* + * Step 1 - find the PROVIDERS dir and add all jar files there to a class loader + */ + List jarUrls = new ArrayList<>(); + Optional providerPathSetting = JvmSettings.PIDPROVIDERS_DIRECTORY.lookupOptional(String.class); + if (providerPathSetting.isPresent()) { + Path providersDir = Paths.get(providerPathSetting.get()); + // Get all JAR files from the configured directory + try (DirectoryStream stream = Files.newDirectoryStream(providersDir, "*.jar")) { + // Using the foreach loop here to enable catching the URI/URL exceptions + for (Path path : stream) { + logger.log(Level.FINE, "Adding {0}", path.toUri().toURL()); + // This is the syntax required to indicate a jar file from which classes should + // be loaded (versus a class file). + jarUrls.add(new URL("jar:" + path.toUri().toURL() + "!/")); + } + } catch (IOException e) { + logger.warning("Problem accessing external Providers: " + e.getLocalizedMessage()); + } + } + URLClassLoader cl = URLClassLoader.newInstance(jarUrls.toArray(new URL[0]), this.getClass().getClassLoader()); + + /* + * Step 2 - load all PidProcviderFactories that can be found, using the jars as + * additional sources + */ + loader = ServiceLoader.load(PidProviderFactory.class, cl); + /* + * Step 3 - Fill pidProviderFactoryMap with type as the key, allow external + * factories to replace internal ones for the same type. FWIW: From the logging + * it appears that ServiceLoader returns classes in ~ alphabetical order rather + * than by class loader, so internal classes handling a given providerName may + * be processed before or after external ones. + */ + loader.forEach(providerFactory -> { + String type = providerFactory.getType(); + logger.fine("Loaded PidProviderFactory of type: " + type); + // If no entry for this providerName yet or if it is an external provider + if (!pidProviderFactoryMap.containsKey(type) || providerFactory.getClass().getClassLoader().equals(cl)) { + logger.fine("Adding PidProviderFactory of type: " + type + " to the map"); + pidProviderFactoryMap.put(type, providerFactory); + } + logger.log(Level.FINE, + "Loaded PidProviderFactory of type: " + type + " from " + + providerFactory.getClass().getCanonicalName() + " and classloader: " + + providerFactory.getClass().getClassLoader().getClass().getCanonicalName()); + }); + } + + private void loadProviders() { + Optional providers = JvmSettings.PID_PROVIDERS.lookupOptional(String[].class); + if (!providers.isPresent()) { + logger.warning( + "No PidProviders configured via dataverse.pid.providers. Please consider updating as older PIDProvider configuration mechanisms will be removed in a future version of Dataverse."); + } else { + for (String id : providers.get()) { + //Allows spaces in PID_PROVIDERS setting + id=id.trim(); + Optional type = JvmSettings.PID_PROVIDER_TYPE.lookupOptional(id); + if (!type.isPresent()) { + logger.warning("PidProvider " + id + + " listed in dataverse.pid.providers is not properly configured and will not be used."); + } else { + String typeString = type.get(); + if (pidProviderFactoryMap.containsKey(typeString)) { + PidProvider provider = pidProviderFactoryMap.get(typeString).createPidProvider(id); + provider.setPidProviderServiceBean(this); + PidUtil.addToProviderList(provider); + } + } + } + } + String protocol = settingsService.getValueForKey(SettingsServiceBean.Key.Protocol); + String authority = settingsService.getValueForKey(SettingsServiceBean.Key.Authority); + String shoulder = settingsService.getValueForKey(SettingsServiceBean.Key.Shoulder); + String provider = settingsService.getValueForKey(SettingsServiceBean.Key.DoiProvider); + + if (protocol != null && authority != null && shoulder != null && provider != null) { + logger.warning("Found legacy settings: " + protocol + " " + authority + " " + shoulder + " " + provider + + "Please consider updating as this PIDProvider configuration mechanism will be removed in a future version of Dataverse"); + if (PidUtil.getPidProvider(protocol, authority, shoulder) != null) { + logger.warning( + "Legacy PID provider settings found - ignored since a provider for the same protocol, authority, shoulder has been registered"); + } else { + PidProvider legacy = null; + // Try to add a legacy provider + String identifierGenerationStyle = settingsService + .getValueForKey(SettingsServiceBean.Key.IdentifierGenerationStyle, "random"); + String dataFilePidFormat = settingsService.getValueForKey(SettingsServiceBean.Key.DataFilePIDFormat, + "DEPENDENT"); + switch (protocol) { + case "doi": + switch (provider) { + case "EZID": + + String baseUrl = JvmSettings.LEGACY_EZID_API_URL.lookup(); + String username = JvmSettings.LEGACY_EZID_USERNAME.lookup(); + String password = JvmSettings.LEGACY_EZID_PASSWORD.lookup(); + PidUtil.addToProviderList(new EZIdDOIProvider("legacy", "legacy", authority, shoulder, + identifierGenerationStyle, dataFilePidFormat, "", "", baseUrl, username, password)); + + break; + case "DataCite": + String mdsUrl = JvmSettings.LEGACY_DATACITE_MDS_API_URL.lookup(); + String restUrl = JvmSettings.LEGACY_DATACITE_REST_API_URL.lookup(); + // Defaults for testing where no account is set up + String dcUsername = JvmSettings.LEGACY_DATACITE_USERNAME.lookup(); + String dcPassword = JvmSettings.LEGACY_DATACITE_PASSWORD.lookup(); + if (mdsUrl != null && restUrl != null && dcUsername != null && dcPassword != null) { + legacy = new DataCiteDOIProvider("legacy", "legacy", authority, shoulder, + identifierGenerationStyle, dataFilePidFormat, "", "", mdsUrl, restUrl, dcUsername, + dcPassword); + } + break; + case "FAKE": + logger.warning("Adding FAKE provider"); + legacy = new FakeDOIProvider("legacy", "legacy", authority, shoulder, identifierGenerationStyle, + dataFilePidFormat, "", ""); + break; + } + break; + case "hdl": + int index = JvmSettings.LEGACY_HANDLENET_INDEX.lookup(Integer.class); + String path = JvmSettings.LEGACY_HANDLENET_KEY_PATH.lookup(); + String passphrase = JvmSettings.LEGACY_HANDLENET_KEY_PASSPHRASE.lookup(); + boolean independentHandleService = settingsService + .isTrueForKey(SettingsServiceBean.Key.IndependentHandleService, false); + String handleAuthHandle = settingsService.getValueForKey(SettingsServiceBean.Key.HandleAuthHandle); + + legacy = new HandlePidProvider("legacy", "legacy", authority, shoulder, identifierGenerationStyle, + dataFilePidFormat, "", "", index, independentHandleService, handleAuthHandle, path, + passphrase); + break; + case "perma": + String baseUrl = JvmSettings.LEGACY_PERMALINK_BASEURL.lookup(); + legacy = new PermaLinkPidProvider("legacy", "legacy", authority, shoulder, + identifierGenerationStyle, dataFilePidFormat, "", "", baseUrl, + PermaLinkPidProvider.SEPARATOR); + } + if (legacy != null) { + legacy.setPidProviderServiceBean(this); + PidUtil.addToProviderList(legacy); + } + } + logger.info("Have " + PidUtil.getManagedProviderIds().size() + " managed PID providers"); + } + PidUtil.addAllToUnmanagedProviderList(Arrays.asList(new UnmanagedDOIProvider(), + new UnmanagedHandlePidProvider(), new UnmanagedPermaLinkPidProvider())); + } + + public String getProducer() { + return dataverseService.getRootDataverseName(); + } + + public boolean isGlobalIdLocallyUnique(GlobalId globalId) { + return dvObjectService.isGlobalIdLocallyUnique(globalId); + } + + String generateNewIdentifierByStoredProcedure() { + return dvObjectService.generateNewIdentifierByStoredProcedure(); + } + + public PidProvider getDefaultPidGenerator() { + Optional pidProviderDefaultId = JvmSettings.PID_DEFAULT_PROVIDER.lookupOptional(String.class); + if (pidProviderDefaultId.isPresent()) { + return PidUtil.getPidProvider(pidProviderDefaultId.get()); + } else { + String nonNullDefaultIfKeyNotFound = ""; + String protocol = settingsService.getValueForKey(SettingsServiceBean.Key.Protocol, + nonNullDefaultIfKeyNotFound); + String authority = settingsService.getValueForKey(SettingsServiceBean.Key.Authority, + nonNullDefaultIfKeyNotFound); + String shoulder = settingsService.getValueForKey(SettingsServiceBean.Key.Shoulder, + nonNullDefaultIfKeyNotFound); + + return PidUtil.getPidProvider(protocol, authority, shoulder); + } + } + +} \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/PidUtil.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/PidUtil.java index 78305648f67..279f18dcd0e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/pidproviders/PidUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/PidUtil.java @@ -1,9 +1,8 @@ package edu.harvard.iq.dataverse.pidproviders; -import edu.harvard.iq.dataverse.DOIServiceBean; import edu.harvard.iq.dataverse.GlobalId; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; -import edu.harvard.iq.dataverse.HandlenetServiceBean; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; +import edu.harvard.iq.dataverse.pidproviders.handle.HandlePidProvider; import edu.harvard.iq.dataverse.util.BundleUtil; import java.io.IOException; import java.io.InputStream; @@ -14,6 +13,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.logging.Logger; import jakarta.json.Json; @@ -113,24 +113,22 @@ public static JsonObjectBuilder queryDoi(GlobalId globalId, String baseUrl, Stri * @return DOI in the form 10.7910/DVN/TJCLKP (no "doi:") */ private static String acceptOnlyDoi(GlobalId globalId) { - if (!DOIServiceBean.DOI_PROTOCOL.equals(globalId.getProtocol())) { + if (!AbstractDOIProvider.DOI_PROTOCOL.equals(globalId.getProtocol())) { throw new IllegalArgumentException(BundleUtil.getStringFromBundle("pids.datacite.errors.DoiOnly")); } return globalId.getAuthority() + "/" + globalId.getIdentifier(); } - static Map providerMap = new HashMap(); - static Map unmanagedProviderMap = new HashMap(); + static Map providerMap = new HashMap(); + static Map unmanagedProviderMap = new HashMap(); - public static void addAllToProviderList(List list) { - for (GlobalIdServiceBean pidProvider : list) { - providerMap.put(pidProvider.getProviderInformation().get(0), pidProvider); - } + public static void addToProviderList(PidProvider pidProvider) { + providerMap.put(pidProvider.getId(), pidProvider); } - public static void addAllToUnmanagedProviderList(List list) { - for (GlobalIdServiceBean pidProvider : list) { - unmanagedProviderMap.put(pidProvider.getProviderInformation().get(0), pidProvider); + public static void addAllToUnmanagedProviderList(List list) { + for (PidProvider pidProvider : list) { + unmanagedProviderMap.put(pidProvider.getId(), pidProvider); } } @@ -141,7 +139,7 @@ public static void addAllToUnmanagedProviderList(List list) */ public static GlobalId parseAsGlobalID(String identifier) { logger.fine("In parseAsGlobalId: " + providerMap.size()); - for (GlobalIdServiceBean pidProvider : providerMap.values()) { + for (PidProvider pidProvider : providerMap.values()) { logger.fine(" Checking " + String.join(",", pidProvider.getProviderInformation())); GlobalId globalId = pidProvider.parsePersistentId(identifier); if (globalId != null) { @@ -149,7 +147,7 @@ public static GlobalId parseAsGlobalID(String identifier) { } } // If no providers can managed this PID, at least allow it to be recognized - for (GlobalIdServiceBean pidProvider : unmanagedProviderMap.values()) { + for (PidProvider pidProvider : unmanagedProviderMap.values()) { logger.fine(" Checking " + String.join(",", pidProvider.getProviderInformation())); GlobalId globalId = pidProvider.parsePersistentId(identifier); if (globalId != null) { @@ -167,14 +165,14 @@ public static GlobalId parseAsGlobalID(String identifier) { public static GlobalId parseAsGlobalID(String protocol, String authority, String identifier) { logger.fine("Looking for " + protocol + " " + authority + " " + identifier); logger.fine("In parseAsGlobalId: " + providerMap.size()); - for (GlobalIdServiceBean pidProvider : providerMap.values()) { + for (PidProvider pidProvider : providerMap.values()) { logger.fine(" Checking " + String.join(",", pidProvider.getProviderInformation())); GlobalId globalId = pidProvider.parsePersistentId(protocol, authority, identifier); if (globalId != null) { return globalId; } } - for (GlobalIdServiceBean pidProvider : unmanagedProviderMap.values()) { + for (PidProvider pidProvider : unmanagedProviderMap.values()) { logger.fine(" Checking " + String.join(",", pidProvider.getProviderInformation())); GlobalId globalId = pidProvider.parsePersistentId(protocol, authority, identifier); if (globalId != null) { @@ -191,28 +189,96 @@ public static GlobalId parseAsGlobalID(String protocol, String authority, String * This method should be deprecated/removed when further refactoring to support * multiple PID providers is done. At that point, when the providers aren't * beans, this code can be moved into other classes that go in the providerMap. - * If this method is not kept in sync with the DOIServiceBean and - * HandlenetServiceBean implementations, the tests using it won't be valid tests - * of the production code. + * If this method is not kept in sync with the AbstractDOIProvider and HandlePidProvider + * implementations, the tests using it won't be valid tests of the production + * code. */ private static GlobalId parseUnmanagedDoiOrHandle(String protocol, String authority, String identifier) { // Default recognition - could be moved to new classes in the future. - if (!GlobalIdServiceBean.isValidGlobalId(protocol, authority, identifier)) { + if (!PidProvider.isValidGlobalId(protocol, authority, identifier)) { return null; } String urlPrefix = null; switch (protocol) { - case DOIServiceBean.DOI_PROTOCOL: - if (!GlobalIdServiceBean.checkDOIAuthority(authority)) { + case AbstractDOIProvider.DOI_PROTOCOL: + if (!PidProvider.checkDOIAuthority(authority)) { return null; } - urlPrefix = DOIServiceBean.DOI_RESOLVER_URL; + urlPrefix = AbstractDOIProvider.DOI_RESOLVER_URL; break; - case HandlenetServiceBean.HDL_PROTOCOL: - urlPrefix = HandlenetServiceBean.HDL_RESOLVER_URL; + case HandlePidProvider.HDL_PROTOCOL: + urlPrefix = HandlePidProvider.HDL_RESOLVER_URL; break; } return new GlobalId(protocol, authority, identifier, "/", urlPrefix, null); } + + /** + * Get a PidProvider by name. GlobalIds have a getProviderName() method so this + * method is often used as + * getPidProvider(dvObject.getGlobalId().getProviderName(); (which will fail if + * the GlobalId is null - use PidProviderFactoryBean.getPidProvider(DvObject) if + * you aren't sure. + * + */ + + public static PidProvider getPidProvider(String name) { + for (PidProvider pidProvider : providerMap.values()) { + if (name.equals(pidProvider.getId())) { + return pidProvider; + } + } + for (PidProvider pidProvider : unmanagedProviderMap.values()) { + if (name.equals(pidProvider.getId())) { + return pidProvider; + } + } + return null; + } + + + + /** + * Method to clear all managed/unmanaged PidProviders. Only for testing as these + * lists are only loaded once by the @Stateless PidProviderFactoryBean in Dataverse. + */ + public static void clearPidProviders() { + providerMap.clear(); + unmanagedProviderMap.clear(); + } + + /** + * Get a PidProvider by protocol/authority/shoulder. + */ + public static PidProvider getPidProvider(String protocol, String authority, String shoulder) { + return getPidProvider(protocol, authority, shoulder, AbstractPidProvider.SEPARATOR); + } + + public static PidProvider getPidProvider(String protocol, String authority, String shoulder, String separator) { + for (PidProvider pidProvider : providerMap.values()) { + if (protocol.equals(pidProvider.getProtocol()) && authority.equals(pidProvider.getAuthority()) + && shoulder.equals(pidProvider.getShoulder()) && separator.equals(pidProvider.getSeparator())) { + return pidProvider; + } + } + for (PidProvider pidProvider : unmanagedProviderMap.values()) { + if (protocol.equals(pidProvider.getProtocol())) { + return pidProvider; + } + } + return null; + } + + public static Set getManagedProviderIds() { + return providerMap.keySet(); + } + + public static JsonObject getProviders() { + JsonObjectBuilder builder = Json.createObjectBuilder(); + for (PidProvider pidProvider : providerMap.values()) { + builder.add(pidProvider.getId(), pidProvider.getProviderSpecification()); + } + return builder.build(); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/AbstractDOIProvider.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/AbstractDOIProvider.java new file mode 100644 index 00000000000..02a7dedce47 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/AbstractDOIProvider.java @@ -0,0 +1,122 @@ +package edu.harvard.iq.dataverse.pidproviders.doi; + +import java.util.Arrays; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetField; +import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.GlobalId; +import edu.harvard.iq.dataverse.pidproviders.AbstractPidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; + + +public abstract class AbstractDOIProvider extends AbstractPidProvider { + + private static final Logger logger = Logger.getLogger(AbstractDOIProvider.class.getCanonicalName()); + + 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 AbstractDOIProvider(String id, String label, String providerAuthority, String providerShoulder, String identifierGenerationStyle, String datafilePidFormat, String managedList, String excludedList) { + super(id, label, DOI_PROTOCOL, providerAuthority, providerShoulder, identifierGenerationStyle, datafilePidFormat, managedList, excludedList); + } + + //For Unmanged provider + public AbstractDOIProvider(String name, String label) { + super(name, label, DOI_PROTOCOL); + } + + @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 && !PidProvider.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; + } + + protected String getProviderKeyName() { + return null; + } + + public String getProtocol() { + return DOI_PROTOCOL; + } + + public String getMetadataFromDvObject(String identifier, Map metadata, DvObject dvObject) { + + Dataset dataset = null; + + if (dvObject instanceof Dataset) { + dataset = (Dataset) dvObject; + } else { + dataset = (Dataset) dvObject.getOwner(); + } + DoiMetadata doiMetadata = new DoiMetadata(); + doiMetadata.setIdentifier(identifier.substring(identifier.indexOf(':') + 1)); + doiMetadata.setCreators(Arrays.asList(metadata.get("datacite.creator").split("; "))); + doiMetadata.setAuthors(dataset.getLatestVersion().getDatasetAuthors()); + if (dvObject.isInstanceofDataset()) { + doiMetadata.setDescription(dataset.getLatestVersion().getDescriptionPlainText()); + } + if (dvObject.isInstanceofDataFile()) { + DataFile df = (DataFile) dvObject; + String fileDescription = df.getDescription(); + doiMetadata.setDescription(fileDescription == null ? "" : fileDescription); + } + + doiMetadata.setContacts(dataset.getLatestVersion().getDatasetContacts()); + doiMetadata.setProducers(dataset.getLatestVersion().getDatasetProducers()); + doiMetadata.setTitle(dvObject.getCurrentName()); + String producerString = pidProviderService.getProducer(); + if (producerString.isEmpty() || producerString.equals(DatasetField.NA_VALUE)) { + producerString = UNAVAILABLE; + } + doiMetadata.setPublisher(producerString); + doiMetadata.setPublisherYear(metadata.get("datacite.publicationyear")); + + String xmlMetadata = new XmlMetadataTemplate(doiMetadata).generateXML(dvObject); + logger.log(Level.FINE, "XML to send to DataCite: {0}", xmlMetadata); + return xmlMetadata; + } + +} \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/DoiMetadata.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/DoiMetadata.java new file mode 100644 index 00000000000..ffd24747bc2 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/DoiMetadata.java @@ -0,0 +1,138 @@ +package edu.harvard.iq.dataverse.pidproviders.doi; + +import java.util.ArrayList; +import java.util.List; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import edu.harvard.iq.dataverse.DatasetAuthor; + + +//Parses some specific parts of a DataCite XML metadata file +public class DoiMetadata { + + private String identifier; + private List creators; + private String title; + private String publisher; + private String publisherYear; + private List datafileIdentifiers; + private List authors; + private String description; + private List contacts; + private List producers; + + + public DoiMetadata() { + } + + public void parseDataCiteXML(String xmlMetaData) { + Document doc = Jsoup.parseBodyFragment(xmlMetaData); + Elements identifierElements = doc.select("identifier"); + if (identifierElements.size() > 0) { + identifier = identifierElements.get(0).html(); + } + Elements creatorElements = doc.select("creatorName"); + creators = new ArrayList<>(); + for (Element creatorElement : creatorElements) { + creators.add(creatorElement.html()); + } + Elements titleElements = doc.select("title"); + if (titleElements.size() > 0) { + title = titleElements.get(0).html(); + } + Elements publisherElements = doc.select("publisher"); + if (publisherElements.size() > 0) { + publisher = publisherElements.get(0).html(); + } + Elements publisherYearElements = doc.select("publicationYear"); + if (publisherYearElements.size() > 0) { + publisherYear = publisherYearElements.get(0).html(); + } + } + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public List getCreators() { + return creators; + } + + public void setCreators(List creators) { + this.creators = creators; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getPublisher() { + return publisher; + } + + public void setPublisher(String publisher) { + this.publisher = publisher; + } + + public String getPublisherYear() { + return publisherYear; + } + + public void setPublisherYear(String publisherYear) { + this.publisherYear = publisherYear; + } + + + public List getProducers() { + return producers; + } + + public void setProducers(List producers) { + this.producers = producers; + } + + public List getContacts() { + return contacts; + } + + public void setContacts(List contacts) { + this.contacts = contacts; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getAuthors() { + return authors; + } + + public void setAuthors(List authors) { + this.authors = authors; + } + + + public List getDatafileIdentifiers() { + return datafileIdentifiers; + } + + public void setDatafileIdentifiers(List datafileIdentifiers) { + this.datafileIdentifiers = datafileIdentifiers; + } + +} \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/UnmanagedDOIServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/UnmanagedDOIProvider.java similarity index 73% rename from src/main/java/edu/harvard/iq/dataverse/pidproviders/UnmanagedDOIServiceBean.java rename to src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/UnmanagedDOIProvider.java index f7e9372cc9b..d4e674f8396 100644 --- a/src/main/java/edu/harvard/iq/dataverse/pidproviders/UnmanagedDOIServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/UnmanagedDOIProvider.java @@ -1,17 +1,11 @@ -package edu.harvard.iq.dataverse.pidproviders; +package edu.harvard.iq.dataverse.pidproviders.doi; import java.io.IOException; import java.util.List; import java.util.Map; -import java.util.logging.Logger; - -import jakarta.annotation.PostConstruct; -import jakarta.ejb.Stateless; - import org.apache.commons.httpclient.HttpException; import org.apache.commons.lang3.NotImplementedException; -import edu.harvard.iq.dataverse.DOIServiceBean; import edu.harvard.iq.dataverse.DvObject; import edu.harvard.iq.dataverse.GlobalId; @@ -20,15 +14,13 @@ * */ -@Stateless -public class UnmanagedDOIServiceBean extends DOIServiceBean { +public class UnmanagedDOIProvider extends AbstractDOIProvider { - private static final Logger logger = Logger.getLogger(UnmanagedDOIServiceBean.class.getCanonicalName()); + public static final String ID = "UnmanagedDOIProvider"; - @PostConstruct - private void init() { - // Always on - configured = true; + public UnmanagedDOIProvider() { + //Also using ID as label + super(ID, ID); } @Override @@ -73,11 +65,15 @@ public boolean publicizeIdentifier(DvObject dvObject) { @Override public List getProviderInformation() { - return List.of("UnmanagedDOIProvider", ""); + return List.of(getId(), ""); } + @Override + public String getProviderType() { + return "unamagedDOI"; + } // PID recognition - // Done by DOIServiceBean + // Done by AbstractDOIProvider } diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/XmlMetadataTemplate.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/XmlMetadataTemplate.java new file mode 100644 index 00000000000..a74a9f34bc9 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/XmlMetadataTemplate.java @@ -0,0 +1,1565 @@ +package edu.harvard.iq.dataverse.pidproviders.doi; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Logger; + +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.StringEscapeUtils; +import org.ocpsoft.common.util.Strings; + +import edu.harvard.iq.dataverse.AlternativePersistentIdentifier; +import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetAuthor; +import edu.harvard.iq.dataverse.DatasetField; +import edu.harvard.iq.dataverse.DatasetFieldCompoundValue; +import edu.harvard.iq.dataverse.DatasetFieldConstant; +import edu.harvard.iq.dataverse.DatasetFieldServiceBean; +import edu.harvard.iq.dataverse.DatasetRelPublication; +import edu.harvard.iq.dataverse.DatasetVersion; +import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.ExternalIdentifier; +import edu.harvard.iq.dataverse.FileMetadata; +import edu.harvard.iq.dataverse.GlobalId; +import edu.harvard.iq.dataverse.TermsOfUseAndAccess; +import edu.harvard.iq.dataverse.api.Util; +import edu.harvard.iq.dataverse.dataset.DatasetType; +import edu.harvard.iq.dataverse.dataset.DatasetUtil; +import edu.harvard.iq.dataverse.license.License; +import edu.harvard.iq.dataverse.pidproviders.AbstractPidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; +import edu.harvard.iq.dataverse.pidproviders.handle.HandlePidProvider; +import edu.harvard.iq.dataverse.pidproviders.perma.PermaLinkPidProvider; +import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.PersonOrOrgUtil; +import edu.harvard.iq.dataverse.util.xml.XmlPrinter; +import edu.harvard.iq.dataverse.util.xml.XmlWriterUtil; +import jakarta.enterprise.inject.spi.CDI; +import jakarta.json.JsonObject; + +public class XmlMetadataTemplate { + + private static final Logger logger = Logger.getLogger(XmlMetadataTemplate.class.getName()); + + public static final String XML_NAMESPACE = "http://datacite.org/schema/kernel-4"; + public static final String XML_SCHEMA_LOCATION = "http://datacite.org/schema/kernel-4 http://schema.datacite.org/meta/kernel-4.5/metadata.xsd"; + public static final String XML_XSI = "http://www.w3.org/2001/XMLSchema-instance"; + public static final String XML_SCHEMA_VERSION = "4.5"; + + private DoiMetadata doiMetadata; + //QDR - used to get ROR name from ExternalVocabularyValue via pidProvider.get + private PidProvider pidProvider = null; + + public XmlMetadataTemplate() { + } + + public XmlMetadataTemplate(DoiMetadata doiMetadata) { + this.doiMetadata = doiMetadata; + } + + public String generateXML(DvObject dvObject) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + generateXML(dvObject, outputStream); + + String xml = outputStream.toString(); + logger.fine(xml); + return XmlPrinter.prettyPrintXml(xml); + } catch (XMLStreamException | IOException e) { + logger.severe("Unable to generate DataCite XML for DOI: " + dvObject.getGlobalId().asString() + " : " + e.getMessage()); + e.printStackTrace(); + } + return null; + } + + private void generateXML(DvObject dvObject, OutputStream outputStream) throws XMLStreamException { + // Could/should use dataset metadata language for metadata from DvObject itself? + String language = null; // machine locale? e.g. for Publisher which is global + String metadataLanguage = null; // when set, otherwise = language? + + //QDR - used to get ROR name from ExternalVocabularyValue via pidProvider.get + GlobalId pid = null; + pid = dvObject.getGlobalId(); + if ((pid == null) && (dvObject instanceof DataFile df)) { + pid = df.getOwner().getGlobalId(); + } + pidProvider = PidUtil.getPidProvider(pid.getProviderId()); + XMLStreamWriter xmlw = XMLOutputFactory.newInstance().createXMLStreamWriter(outputStream); + xmlw.writeStartElement("resource"); + boolean deaccessioned=false; + if(dvObject instanceof Dataset d) { + deaccessioned=d.isDeaccessioned(); + } else if (dvObject instanceof DataFile df) { + deaccessioned = df.isDeaccessioned(); + } + xmlw.writeDefaultNamespace(XML_NAMESPACE); + xmlw.writeAttribute("xmlns:xsi", XML_XSI); + xmlw.writeAttribute("xsi:schemaLocation", XML_SCHEMA_LOCATION); + + writeIdentifier(xmlw, dvObject); + writeCreators(xmlw, doiMetadata.getAuthors(), deaccessioned); + writeTitles(xmlw, dvObject, language, deaccessioned); + writePublisher(xmlw, dvObject, deaccessioned); + writePublicationYear(xmlw, dvObject, deaccessioned); + if (!deaccessioned) { + writeSubjects(xmlw, dvObject); + writeContributors(xmlw, dvObject); + writeDates(xmlw, dvObject); + writeLanguage(xmlw, dvObject); + } + writeResourceType(xmlw, dvObject); + if (!deaccessioned) { + writeAlternateIdentifiers(xmlw, dvObject); + writeRelatedIdentifiers(xmlw, dvObject); + writeSize(xmlw, dvObject); + writeFormats(xmlw, dvObject); + writeVersion(xmlw, dvObject); + writeAccessRights(xmlw, dvObject); + } + writeDescriptions(xmlw, dvObject, deaccessioned); + if (!deaccessioned) { + writeGeoLocations(xmlw, dvObject); + writeFundingReferences(xmlw, dvObject); + } + xmlw.writeEndElement(); + xmlw.flush(); + } + + /** + * 3, Title(s) (with optional type sub-properties) (M) + * + * @param xmlw + * The Stream writer + * @param dvObject + * The dataset/file + * @param language + * the metadata language + * @return + * @throws XMLStreamException + */ + private void writeTitles(XMLStreamWriter xmlw, DvObject dvObject, String language, boolean deaccessioned) throws XMLStreamException { + String title = null; + String subTitle = null; + List altTitles = new ArrayList<>(); + + if (!deaccessioned) { + title = doiMetadata.getTitle(); + + // Only Datasets can have a subtitle or alternative titles + if (dvObject instanceof Dataset d) { + DatasetVersion dv = d.getLatestVersionForCopy(); + Optional subTitleField = dv.getDatasetFields().stream().filter(f -> f.getDatasetFieldType().getName().equals(DatasetFieldConstant.subTitle)).findFirst(); + if (subTitleField.isPresent()) { + subTitle = subTitleField.get().getValue(); + } + Optional altTitleField = dv.getDatasetFields().stream().filter(f -> f.getDatasetFieldType().getName().equals(DatasetFieldConstant.alternativeTitle)).findFirst(); + if (altTitleField.isPresent()) { + altTitles = altTitleField.get().getValues(); + } + } + } else { + title = AbstractDOIProvider.UNAVAILABLE; + } + if (StringUtils.isNotBlank(title) || StringUtils.isNotBlank(subTitle) || (altTitles != null && !String.join("", altTitles).isBlank())) { + xmlw.writeStartElement("titles"); + if (StringUtils.isNotBlank(title)) { + XmlWriterUtil.writeFullElement(xmlw, "title", title, language); + } + Map attributes = new HashMap(); + + if (StringUtils.isNotBlank(subTitle)) { + attributes.put("titleType", "Subtitle"); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "title", attributes, subTitle); + } + if ((altTitles != null && !String.join("", altTitles).isBlank())) { + attributes.clear(); + attributes.put("titleType", "AlternativeTitle"); + for (String altTitle : altTitles) { + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "title", attributes, altTitle); + } + } + xmlw.writeEndElement(); + } + } + + /** + * 1, Identifier (with mandatory type sub-property) (M) Note DataCite expects + * identifierType="DOI" but OpenAire allows several others (see + * https://guidelines.readthedocs.io/en/latest/data/field_identifier.html#d-identifiertype) + * Dataverse is currently only capable of creating DOI, Handle, or URL types + * from the OpenAire list (the last from PermaLinks) ToDo - If we add,e.g., an + * ARK or PURL provider, this code has to change or we'll need to refactor so + * that the identifiertype and id value can be sent via the JSON/ORE + * + * @param xmlw + * The Steam writer + * @param dvObject + * The dataset or file with the PID + * @throws XMLStreamException + */ + private void writeIdentifier(XMLStreamWriter xmlw, DvObject dvObject) throws XMLStreamException { + GlobalId pid = dvObject.getGlobalId(); + String identifierType = null; + String identifier = null; + switch (pid.getProtocol()) { + case AbstractDOIProvider.DOI_PROTOCOL: + identifierType = AbstractDOIProvider.DOI_PROTOCOL.toUpperCase(); + identifier = pid.asRawIdentifier(); + break; + case HandlePidProvider.HDL_PROTOCOL: + identifierType = "Handle"; + identifier = pid.asRawIdentifier(); + break; + case PermaLinkPidProvider.PERMA_PROTOCOL: + identifierType = "URL"; + identifier = pid.asURL(); + break; + } + Map attributeMap = new HashMap(); + attributeMap.put("identifierType", identifierType); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "identifier", attributeMap, identifier); + } + + /** + * 2, Creator (with optional given name, family name, name identifier and + * affiliation sub-properties) (M) + * + * @param xmlw + * The stream writer + * @param authorList + * - the list of authors + * @throws XMLStreamException + */ + public void writeCreators(XMLStreamWriter xmlw, List authorList, boolean deaccessioned) throws XMLStreamException { + // creators -> creator -> creatorName with nameType attribute, givenName, + // familyName, nameIdentifier + // write all creators + xmlw.writeStartElement("creators"); // + if(deaccessioned) { + //skip the loop below + authorList = null; + } + boolean nothingWritten = true; + if (authorList != null && !authorList.isEmpty()) { + for (DatasetAuthor author : authorList) { + String creatorName = author.getName().getDisplayValue(); + String affiliation = null; + if (author.getAffiliation() != null && !author.getAffiliation().getValue().isEmpty()) { + affiliation = author.getAffiliation().getValue(); + } + String nameIdentifier = null; + String nameIdentifierScheme = null; + if (StringUtils.isNotBlank(author.getIdValue()) && StringUtils.isNotBlank(author.getIdType())) { + nameIdentifier = author.getIdValue(); + if (nameIdentifier != null) { + // Normalizes to the URL form of the identifier, returns null if the identifier + // is not valid given the type + nameIdentifier = author.getIdentifierAsUrl(); + } + nameIdentifierScheme = author.getIdType(); + } + + if (StringUtils.isNotBlank(creatorName)) { + JsonObject creatorObj = PersonOrOrgUtil.getPersonOrOrganization(creatorName, false, + StringUtils.containsIgnoreCase(nameIdentifierScheme, "orcid")); + nothingWritten = false; + writeEntityElements(xmlw, "creator", null, creatorObj, affiliation, nameIdentifier, nameIdentifierScheme); + } + + + } + } + if (nothingWritten) { + // Authors unavailable + xmlw.writeStartElement("creator"); + XmlWriterUtil.writeFullElement(xmlw, "creatorName", AbstractPidProvider.UNAVAILABLE); + xmlw.writeEndElement(); + } + xmlw.writeEndElement(); // + } + + private void writePublisher(XMLStreamWriter xmlw, DvObject dvObject, boolean deaccessioned) throws XMLStreamException { + // publisher should already be non null - :unav if it wasn't available + if(deaccessioned) { + doiMetadata.setPublisher(AbstractPidProvider.UNAVAILABLE); + } + XmlWriterUtil.writeFullElement(xmlw, "publisher", doiMetadata.getPublisher()); + } + + private void writePublicationYear(XMLStreamWriter xmlw, DvObject dvObject, boolean deaccessioned) throws XMLStreamException { + // Can't use "UNKNOWN" here because DataCite will respond with "[facet + // 'pattern'] the value 'unknown' is not accepted by the pattern '[\d]{4}'" + String pubYear = "9999"; + // FIXME: Investigate why this.publisherYear is sometimes null now that pull + // request #4606 has been merged. + if (! deaccessioned && (doiMetadata.getPublisherYear() != null)) { + // Added to prevent a NullPointerException when trying to destroy datasets when + // using DataCite rather than EZID. + pubYear = doiMetadata.getPublisherYear(); + } + XmlWriterUtil.writeFullElement(xmlw, "publicationYear", String.valueOf(pubYear)); + } + + /** + * 6, Subject (with scheme sub-property) R + * + * @param xmlw + * The Steam writer + * @param dvObject + * The Dataset/DataFile + * @throws XMLStreamException + */ + private void writeSubjects(XMLStreamWriter xmlw, DvObject dvObject) throws XMLStreamException { + // subjects -> subject with subjectScheme and schemeURI attributes when + // available + boolean subjectsCreated = false; + List subjects = new ArrayList(); + List compoundKeywords = new ArrayList(); + List compoundTopics = new ArrayList(); + // Dataset Subject= Dataverse subject, keyword, and/or topic classification + // fields + if (dvObject instanceof Dataset d) { + DatasetVersion dv = d.getLatestVersionForCopy(); + for (DatasetField dsf : dv.getDatasetFields()) { + if (dsf.getDatasetFieldType().getName().equals(DatasetFieldConstant.subject)) { + subjects.addAll(dsf.getValues()); + } + if (dsf.getDatasetFieldType().getName().equals(DatasetFieldConstant.keyword)) { + compoundKeywords = dsf.getDatasetFieldCompoundValues(); + } else if (dsf.getDatasetFieldType().getName().equals(DatasetFieldConstant.topicClassification)) { + compoundTopics = dsf.getDatasetFieldCompoundValues(); + } + } + + } else if (dvObject instanceof DataFile df) { + subjects = df.getTagLabels(); + } + for (String subject : subjects) { + if (StringUtils.isNotBlank(subject)) { + subjectsCreated = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "subjects", subjectsCreated); + XmlWriterUtil.writeFullElement(xmlw, "subject", StringEscapeUtils.escapeXml10(subject)); + } + } + for (DatasetFieldCompoundValue keywordFieldValue : compoundKeywords) { + String keyword = null; + String scheme = null; + String schemeUri = null; + + for (DatasetField subField : keywordFieldValue.getChildDatasetFields()) { + switch (subField.getDatasetFieldType().getName()) { + case DatasetFieldConstant.keywordValue: + keyword = subField.getValue(); + break; + case DatasetFieldConstant.keywordVocab: + scheme = subField.getValue(); + break; + case DatasetFieldConstant.keywordVocabURI: + schemeUri = subField.getValue(); + break; + } + } + if (StringUtils.isNotBlank(keyword)) { + Map attributesMap = new HashMap(); + if (StringUtils.isNotBlank(scheme)) { + attributesMap.put("subjectScheme", scheme); + } + if (StringUtils.isNotBlank(schemeUri)) { + attributesMap.put("schemeURI", schemeUri); + } + subjectsCreated = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "subjects", subjectsCreated); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "subject", attributesMap, StringEscapeUtils.escapeXml10(keyword)); + } + } + for (DatasetFieldCompoundValue topicFieldValue : compoundTopics) { + String topic = null; + String scheme = null; + String schemeUri = null; + + for (DatasetField subField : topicFieldValue.getChildDatasetFields()) { + + switch (subField.getDatasetFieldType().getName()) { + case DatasetFieldConstant.topicClassValue: + topic = subField.getValue(); + break; + case DatasetFieldConstant.topicClassVocab: + scheme = subField.getValue(); + break; + case DatasetFieldConstant.topicClassVocabURI: + schemeUri = subField.getValue(); + break; + } + } + if (StringUtils.isNotBlank(topic)) { + Map attributesMap = new HashMap(); + if (StringUtils.isNotBlank(scheme)) { + attributesMap.put("subjectScheme", scheme); + } + if (StringUtils.isNotBlank(schemeUri)) { + attributesMap.put("schemeURI", schemeUri); + } + subjectsCreated = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "subjects", subjectsCreated); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "subject", attributesMap, StringEscapeUtils.escapeXml10(topic)); + } + } + if (subjectsCreated) { + xmlw.writeEndElement(); + } + } + + /** + * 7, Contributor (with optional given name, family name, name identifier and + * affiliation sub-properties) + * + * @see #writeContributorElement(javax.xml.stream.XMLStreamWriter, + * java.lang.String, java.lang.String, java.lang.String) + * + * @param xmlw + * The stream writer + * @param dvObject + * The Dataset/DataFile + * @throws XMLStreamException + */ + private void writeContributors(XMLStreamWriter xmlw, DvObject dvObject) throws XMLStreamException { + boolean contributorsCreated = false; + List compoundProducers = new ArrayList(); + List compoundDistributors = new ArrayList(); + List compoundContacts = new ArrayList(); + List compoundContributors = new ArrayList(); + // Dataset Subject= Dataverse subject, keyword, and/or topic classification + // fields + // ToDo Include for files? + /* + * if(dvObject instanceof DataFile df) { dvObject = df.getOwner(); } + */ + + if (dvObject instanceof Dataset d) { + DatasetVersion dv = d.getLatestVersionForCopy(); + for (DatasetField dsf : dv.getDatasetFields()) { + switch (dsf.getDatasetFieldType().getName()) { + case DatasetFieldConstant.producer: + compoundProducers = dsf.getDatasetFieldCompoundValues(); + break; + case DatasetFieldConstant.distributor: + compoundDistributors = dsf.getDatasetFieldCompoundValues(); + break; + case DatasetFieldConstant.datasetContact: + compoundContacts = dsf.getDatasetFieldCompoundValues(); + break; + case DatasetFieldConstant.contributor: + compoundContributors = dsf.getDatasetFieldCompoundValues(); + } + } + } + + for (DatasetFieldCompoundValue producerFieldValue : compoundProducers) { + String producer = null; + String affiliation = null; + + for (DatasetField subField : producerFieldValue.getChildDatasetFields()) { + + switch (subField.getDatasetFieldType().getName()) { + case DatasetFieldConstant.producerName: + producer = subField.getValue(); + break; + case DatasetFieldConstant.producerAffiliation: + affiliation = subField.getValue(); + break; + } + } + if (StringUtils.isNotBlank(producer)) { + contributorsCreated = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "contributors", contributorsCreated); + JsonObject entityObject = PersonOrOrgUtil.getPersonOrOrganization(producer, false, false); + writeEntityElements(xmlw, "contributor", "Producer", entityObject, affiliation, null, null); + } + + } + + for (DatasetFieldCompoundValue distributorFieldValue : compoundDistributors) { + String distributor = null; + String affiliation = null; + + for (DatasetField subField : distributorFieldValue.getChildDatasetFields()) { + + switch (subField.getDatasetFieldType().getName()) { + case DatasetFieldConstant.distributorName: + distributor = subField.getValue(); + break; + case DatasetFieldConstant.distributorAffiliation: + affiliation = subField.getValue(); + break; + } + } + if (StringUtils.isNotBlank(distributor)) { + contributorsCreated = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "contributors", contributorsCreated); + JsonObject entityObject = PersonOrOrgUtil.getPersonOrOrganization(distributor, false, false); + writeEntityElements(xmlw, "contributor", "Distributor", entityObject, affiliation, null, null); + } + + } + for (DatasetFieldCompoundValue contactFieldValue : compoundContacts) { + String contact = null; + String affiliation = null; + + for (DatasetField subField : contactFieldValue.getChildDatasetFields()) { + + switch (subField.getDatasetFieldType().getName()) { + case DatasetFieldConstant.datasetContactName: + contact = subField.getValue(); + break; + case DatasetFieldConstant.datasetContactAffiliation: + affiliation = subField.getValue(); + break; + } + } + if (StringUtils.isNotBlank(contact)) { + contributorsCreated = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "contributors", contributorsCreated); + JsonObject entityObject = PersonOrOrgUtil.getPersonOrOrganization(contact, false, false); + writeEntityElements(xmlw, "contributor", "ContactPerson", entityObject, affiliation, null, null); + } + + } + for (DatasetFieldCompoundValue contributorFieldValue : compoundContributors) { + String contributor = null; + String contributorType = null; + + for (DatasetField subField : contributorFieldValue.getChildDatasetFields()) { + + switch (subField.getDatasetFieldType().getName()) { + case DatasetFieldConstant.contributorName: + contributor = subField.getValue(); + break; + case DatasetFieldConstant.contributorType: + contributorType = subField.getValue(); + if (contributorType != null) { + contributorType = contributorType.replace(" ", ""); + } + break; + } + } + // QDR - doesn't have Funder in the contributor type list. + // Using a string isn't i18n + if (StringUtils.isNotBlank(contributor) && !StringUtils.equalsIgnoreCase("Funder", contributorType)) { + contributorType = getCanonicalContributorType(contributorType); + contributorsCreated = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "contributors", contributorsCreated); + JsonObject entityObject = PersonOrOrgUtil.getPersonOrOrganization(contributor, false, false); + writeEntityElements(xmlw, "contributor", contributorType, entityObject, null, null, null); + } + + } + + if (contributorsCreated) { + xmlw.writeEndElement(); + } + } + + //List from https://schema.datacite.org/meta/kernel-4/include/datacite-contributorType-v4.xsd + private Set contributorTypes = new HashSet<>(Arrays.asList("ContactPerson", "DataCollector", "DataCurator", "DataManager", "Distributor", "Editor", + "HostingInstitution", "Other", "Producer", "ProjectLeader", "ProjectManager", "ProjectMember", "RegistrationAgency", "RegistrationAuthority", + "RelatedPerson", "ResearchGroup", "RightsHolder", "Researcher", "Sponsor", "Supervisor", "WorkPackageLeader")); + + private String getCanonicalContributorType(String contributorType) { + if(StringUtils.isBlank(contributorType) || !contributorTypes.contains(contributorType)) { + return "Other"; + } + return contributorType; + } + + private void writeEntityElements(XMLStreamWriter xmlw, String elementName, String type, JsonObject entityObject, String affiliation, String nameIdentifier, String nameIdentifierScheme) throws XMLStreamException { + xmlw.writeStartElement(elementName); + Map attributeMap = new HashMap(); + if (StringUtils.isNotBlank(type)) { + xmlw.writeAttribute("contributorType", type); + } + // person name=, + if (entityObject.getBoolean("isPerson")) { + attributeMap.put("nameType", "Personal"); + } else { + attributeMap.put("nameType", "Organizational"); + } + XmlWriterUtil.writeFullElementWithAttributes(xmlw, elementName + "Name", attributeMap, + StringEscapeUtils.escapeXml10(entityObject.getString("fullName"))); + if (entityObject.containsKey("givenName")) { + XmlWriterUtil.writeFullElement(xmlw, "givenName", StringEscapeUtils.escapeXml10(entityObject.getString("givenName"))); + } + if (entityObject.containsKey("familyName")) { + XmlWriterUtil.writeFullElement(xmlw, "familyName", StringEscapeUtils.escapeXml10(entityObject.getString("familyName"))); + } + + if (nameIdentifier != null) { + attributeMap.clear(); + URL url; + try { + url = new URL(nameIdentifier); + String protocol = url.getProtocol(); + String authority = url.getAuthority(); + String site = String.format("%s://%s", protocol, authority); + attributeMap.put("schemeURI", site); + attributeMap.put("nameIdentifierScheme", nameIdentifierScheme); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "nameIdentifier", attributeMap, nameIdentifier); + } catch (MalformedURLException e) { + logger.warning("DatasetAuthor.getIdentifierAsUrl returned a Malformed URL: " + nameIdentifier); + } + } + + if (StringUtils.isNotBlank(affiliation)) { + attributeMap.clear(); + boolean isROR=false; + String orgName = affiliation; + ExternalIdentifier externalIdentifier = ExternalIdentifier.ROR; + if (externalIdentifier.isValidIdentifier(orgName)) { + isROR = true; + JsonObject jo = getExternalVocabularyValue(orgName); + if (jo != null) { + orgName = jo.getString("termName"); + } + } + + if (isROR) { + + attributeMap.put("schemeURI", "https://ror.org"); + attributeMap.put("affiliationIdentifierScheme", "ROR"); + attributeMap.put("affiliationIdentifier", orgName); + } + + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "affiliation", attributeMap, StringEscapeUtils.escapeXml10(orgName)); + } + xmlw.writeEndElement(); + } + + private JsonObject getExternalVocabularyValue(String id) { + return CDI.current().select(DatasetFieldServiceBean.class).get().getExternalVocabularyValue(id); + } + + /** + * 8, Date (with type sub-property) (R) + * + * @param xmlw + * The Steam writer + * @param dvObject + * The dataset/datafile + * @throws XMLStreamException + */ + private void writeDates(XMLStreamWriter xmlw, DvObject dvObject) throws XMLStreamException { + boolean datesWritten = false; + String dateOfDistribution = null; + String dateOfProduction = null; + String dateOfDeposit = null; + Date releaseDate = null; + String publicationDate = null; + boolean isAnUpdate=false; + List datesOfCollection = new ArrayList(); + List timePeriods = new ArrayList(); + + if (dvObject instanceof DataFile df) { + // Find the first released version the file is in to give a published date + List fmds = df.getFileMetadatas(); + DatasetVersion initialVersion = null; + for (FileMetadata fmd : fmds) { + DatasetVersion dv = fmd.getDatasetVersion(); + if (dv.isReleased()) { + initialVersion = dv; + publicationDate = Util.getDateFormat().format(dv.getReleaseTime()); + break; + } + } + // And the last update is the most recent + for (int i = fmds.size() - 1; i >= 0; i--) { + DatasetVersion dv = fmds.get(i).getDatasetVersion(); + if (dv.isReleased() && !dv.equals(initialVersion)) { + releaseDate = dv.getReleaseTime(); + isAnUpdate=true; + break; + } + } + } else if (dvObject instanceof Dataset d) { + DatasetVersion dv = d.getLatestVersionForCopy(); + Long versionNumber = dv.getVersionNumber(); + if (versionNumber != null && !(versionNumber.equals(1) && dv.getMinorVersionNumber().equals(0))) { + isAnUpdate = true; + } + releaseDate = dv.getReleaseTime(); + publicationDate = d.getPublicationDateFormattedYYYYMMDD(); + for (DatasetField dsf : dv.getDatasetFields()) { + switch (dsf.getDatasetFieldType().getName()) { + case DatasetFieldConstant.distributionDate: + dateOfDistribution = dsf.getValue(); + break; + case DatasetFieldConstant.productionDate: + dateOfProduction = dsf.getValue(); + break; + case DatasetFieldConstant.dateOfDeposit: + dateOfDeposit = dsf.getValue(); + break; + case DatasetFieldConstant.dateOfCollection: + datesOfCollection = dsf.getDatasetFieldCompoundValues(); + break; + case DatasetFieldConstant.timePeriodCovered: + timePeriods = dsf.getDatasetFieldCompoundValues(); + break; + } + } + } + Map attributes = new HashMap(); + if (StringUtils.isNotBlank(dateOfDistribution)) { + datesWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "dates", datesWritten); + attributes.put("dateType", "Issued"); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "date", attributes, dateOfDistribution); + } + // dates -> date with dateType attribute + + if (StringUtils.isNotBlank(dateOfProduction)) { + datesWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "dates", datesWritten); + attributes.put("dateType", "Created"); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "date", attributes, dateOfProduction); + } + if (StringUtils.isNotBlank(dateOfDeposit)) { + datesWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "dates", datesWritten); + attributes.put("dateType", "Submitted"); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "date", attributes, dateOfDeposit); + } + + if (publicationDate != null) { + datesWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "dates", datesWritten); + + attributes.put("dateType", "Available"); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "date", attributes, publicationDate); + } + if (isAnUpdate) { + String date = Util.getDateFormat().format(releaseDate); + datesWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "dates", datesWritten); + + attributes.put("dateType", "Updated"); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "date", attributes, date); + } + if (datesOfCollection != null) { + for (DatasetFieldCompoundValue collectionDateFieldValue : datesOfCollection) { + String startDate = null; + String endDate = null; + + for (DatasetField subField : collectionDateFieldValue.getChildDatasetFields()) { + switch (subField.getDatasetFieldType().getName()) { + case DatasetFieldConstant.dateOfCollectionStart: + startDate = subField.getValue(); + break; + case DatasetFieldConstant.dateOfCollectionEnd: + endDate = subField.getValue(); + break; + } + } + // Minimal clean-up - useful? Parse/format would remove unused chars, and an + // exception would clear the date so we don't send nonsense + startDate = cleanUpDate(startDate); + endDate = cleanUpDate(endDate); + if (StringUtils.isNotBlank(startDate) || StringUtils.isNotBlank(endDate)) { + datesWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "dates", datesWritten); + attributes.put("dateType", "Collected"); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "date", attributes, (startDate + "/" + endDate).trim()); + } + } + } + if (timePeriods != null) { + for (DatasetFieldCompoundValue timePeriodFieldValue : timePeriods) { + String startDate = null; + String endDate = null; + + for (DatasetField subField : timePeriodFieldValue.getChildDatasetFields()) { + switch (subField.getDatasetFieldType().getName()) { + case DatasetFieldConstant.timePeriodCoveredStart: + startDate = subField.getValue(); + break; + case DatasetFieldConstant.timePeriodCoveredEnd: + endDate = subField.getValue(); + break; + } + } + // Minimal clean-up - useful? Parse/format would remove unused chars, and an + // exception would clear the date so we don't send nonsense + startDate = cleanUpDate(startDate); + endDate = cleanUpDate(endDate); + if (StringUtils.isNotBlank(startDate) || StringUtils.isNotBlank(endDate)) { + datesWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "dates", datesWritten); + attributes.put("dateType", "Other"); + attributes.put("dateInformation", "Time period covered by the data"); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "date", attributes, (startDate + "/" + endDate).trim()); + } + } + } + if (datesWritten) { + xmlw.writeEndElement(); + } + } + + private String cleanUpDate(String date) { + String newDate = null; + if (!StringUtils.isBlank(date)) { + try { + SimpleDateFormat sdf = Util.getDateFormat(); + Date start = sdf.parse(date); + newDate = sdf.format(start); + } catch (ParseException e) { + logger.warning("Could not parse date: " + date); + } + } + return newDate; + } + + // 9, Language (MA), language + private void writeLanguage(XMLStreamWriter xmlw, DvObject dvObject) throws XMLStreamException { + // Currently not supported. Spec indicates one 'primary' language. Could send + // the first entry in DatasetFieldConstant.language or send iff there is only + // one entry, and/or default to the machine's default lang, or the dataverse metadatalang? + return; + } + + // 10, ResourceType (with mandatory general type + // description sub- property) (M) + private void writeResourceType(XMLStreamWriter xmlw, DvObject dvObject) throws XMLStreamException { + List kindOfDataValues = new ArrayList(); + Map attributes = new HashMap(); + String resourceType = "Dataset"; + if (dvObject instanceof Dataset dataset) { + String datasetTypeName = dataset.getDatasetType().getName(); + resourceType = switch (datasetTypeName) { + case DatasetType.DATASET_TYPE_DATASET -> "Dataset"; + case DatasetType.DATASET_TYPE_SOFTWARE -> "Software"; + case DatasetType.DATASET_TYPE_WORKFLOW -> "Workflow"; + default -> "Dataset"; + }; + } + attributes.put("resourceTypeGeneral", resourceType); + if (dvObject instanceof Dataset d) { + DatasetVersion dv = d.getLatestVersionForCopy(); + for (DatasetField dsf : dv.getDatasetFields()) { + switch (dsf.getDatasetFieldType().getName()) { + case DatasetFieldConstant.kindOfData: + List vals = dsf.getValues(); + for(String val: vals) { + if(StringUtils.isNotBlank(val)) { + kindOfDataValues.add(val); + } + } + break; + } + } + } + if (!kindOfDataValues.isEmpty()) { + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "resourceType", attributes, String.join(";", kindOfDataValues)); + + } else { + // Write an attribute only element if there are no kindOfData values. + xmlw.writeStartElement("resourceType"); + xmlw.writeAttribute("resourceTypeGeneral", attributes.get("resourceTypeGeneral")); + xmlw.writeEndElement(); + } + + } + + /** + * 11 AlternateIdentifier (with type sub-property) (O) + * + * @param xmlw + * The Steam writer + * @param dvObject + * The dataset/datafile + * @throws XMLStreamException + */ + private void writeAlternateIdentifiers(XMLStreamWriter xmlw, DvObject dvObject) throws XMLStreamException { + List otherIdentifiers = new ArrayList(); + Set altPids = dvObject.getAlternativePersistentIndentifiers(); + + boolean alternatesWritten = false; + + Map attributes = new HashMap(); + if (dvObject instanceof Dataset d) { + DatasetVersion dv = d.getLatestVersionForCopy(); + for (DatasetField dsf : dv.getDatasetFields()) { + if (DatasetFieldConstant.otherId.equals(dsf.getDatasetFieldType().getName())) { + otherIdentifiers = dsf.getDatasetFieldCompoundValues(); + break; + } + } + } + + if (altPids != null && !altPids.isEmpty()) { + alternatesWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "alternateIdentifiers", alternatesWritten); + for (AlternativePersistentIdentifier altPid : altPids) { + String identifierType = null; + String identifier = null; + switch (altPid.getProtocol()) { + case AbstractDOIProvider.DOI_PROTOCOL: + identifierType = AbstractDOIProvider.DOI_PROTOCOL.toUpperCase(); + identifier = altPid.getAuthority() + "/" + altPid.getIdentifier(); + break; + case HandlePidProvider.HDL_PROTOCOL: + identifierType = "Handle"; + identifier = altPid.getAuthority() + "/" + altPid.getIdentifier(); + break; + default: + // The AlternativePersistentIdentifier class isn't really ready for anything but + // doi or handle pids, but will add this as a default. + identifierType = ":unav"; + identifier = altPid.getAuthority() + altPid.getIdentifier(); + break; + } + attributes.put("alternateIdentifierType", identifierType); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "alternateIdentifier", attributes, identifier); + + } + } + + for (DatasetFieldCompoundValue otherIdentifier : otherIdentifiers) { + String identifierType = null; + String identifier = null; + for (DatasetField subField : otherIdentifier.getChildDatasetFields()) { + identifierType = ":unav"; + switch (subField.getDatasetFieldType().getName()) { + case DatasetFieldConstant.otherIdAgency: + identifierType = subField.getValue(); + break; + case DatasetFieldConstant.otherIdValue: + identifier = subField.getValue(); + break; + } + } + attributes.put("alternateIdentifierType", identifierType); + if (!StringUtils.isBlank(identifier)) { + alternatesWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "alternateIdentifiers", alternatesWritten); + + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "alternateIdentifier", attributes, identifier); + } + } + if (alternatesWritten) { + xmlw.writeEndElement(); + } + } + + /** + * 12, RelatedIdentifier (with type and relation type sub-properties) (R) + * + * @param xmlw + * The Steam writer + * @param dvObject + * the dataset/datafile + * @throws XMLStreamException + */ + private void writeRelatedIdentifiers(XMLStreamWriter xmlw, DvObject dvObject) throws XMLStreamException { + + boolean relatedIdentifiersWritten = false; + + Map attributes = new HashMap(); + + if (dvObject instanceof Dataset dataset) { + List relatedPublications = dataset.getLatestVersionForCopy().getRelatedPublications(); + if (!relatedPublications.isEmpty()) { + for (DatasetRelPublication relatedPub : relatedPublications) { + attributes.clear(); + + String pubIdType = relatedPub.getIdType(); + String identifier = relatedPub.getIdNumber(); + String url = relatedPub.getUrl(); + String relationType = relatedPub.getRelationType(); + if(StringUtils.isBlank(relationType)) { + relationType = "IsSupplementTo"; + } + /* + * Note - with identifier and url fields, it's not clear that there's a single + * way those two fields are used for all identifier types. The code here is + * ~best effort to interpret those fields. + */ + logger.fine("Found relpub: " + pubIdType + " " + identifier + " " + url); + + pubIdType = getCanonicalPublicationType(pubIdType); + logger.fine("Canonical type: " + pubIdType); + // Prefer identifier if set, otherwise check url + String relatedIdentifier = identifier; + if (StringUtils.isBlank(relatedIdentifier)) { + relatedIdentifier = url; + } + logger.fine("Related identifier: " + relatedIdentifier); + // For types where we understand the protocol, get the canonical form + if (StringUtils.isNotBlank(relatedIdentifier)) { + switch (pubIdType != null ? pubIdType : "none") { + case "DOI": + if (!(relatedIdentifier.startsWith("doi:") || relatedIdentifier.startsWith("http"))) { + relatedIdentifier = "doi:" + relatedIdentifier; + } + logger.fine("Intermediate Related identifier: " + relatedIdentifier); + try { + GlobalId pid = PidUtil.parseAsGlobalID(relatedIdentifier); + relatedIdentifier = pid.asRawIdentifier(); + } catch (IllegalArgumentException e) { + logger.warning("Invalid DOI: " + e.getLocalizedMessage()); + relatedIdentifier = null; + } + logger.fine("Final Related identifier: " + relatedIdentifier); + break; + case "Handle": + if (!relatedIdentifier.startsWith("hdl:") || !relatedIdentifier.startsWith("http")) { + relatedIdentifier = "hdl:" + relatedIdentifier; + } + try { + GlobalId pid = PidUtil.parseAsGlobalID(relatedIdentifier); + relatedIdentifier = pid.asRawIdentifier(); + } catch (IllegalArgumentException e) { + relatedIdentifier = null; + } + break; + case "URL": + // If a URL is given, split the string to get a schemeUri + try { + URL relatedUrl = new URI(relatedIdentifier).toURL(); + String protocol = relatedUrl.getProtocol(); + String authority = relatedUrl.getAuthority(); + String site = String.format("%s://%s", protocol, authority); + relatedIdentifier = relatedIdentifier.substring(site.length()); + attributes.put("schemeURI", site); + } catch (URISyntaxException | MalformedURLException | IllegalArgumentException e) { + // Just an identifier but without a pubIdType we won't include it + logger.warning("Invalid Identifier of type URL: " + relatedIdentifier); + relatedIdentifier = null; + } + break; + case "none": + //Try to identify PIDs and URLs and send them as related identifiers + if (relatedIdentifier != null) { + // See if it is a GlobalID we know + try { + GlobalId pid = PidUtil.parseAsGlobalID(relatedIdentifier); + relatedIdentifier = pid.asRawIdentifier(); + pubIdType = getCanonicalPublicationType(pid.getProtocol()); + } catch (IllegalArgumentException e) { + } + // For non-URL types, if a URL is given, split the string to get a schemeUri + try { + URL relatedUrl = new URI(relatedIdentifier).toURL(); + String protocol = relatedUrl.getProtocol(); + String authority = relatedUrl.getAuthority(); + String site = String.format("%s://%s", protocol, authority); + relatedIdentifier = relatedIdentifier.substring(site.length()); + attributes.put("schemeURI", site); + pubIdType = "URL"; + } catch (URISyntaxException | MalformedURLException | IllegalArgumentException e) { + // Just an identifier but without a pubIdType we won't include it + logger.warning("Related Identifier found without type: " + relatedIdentifier); + //Won't be sent since pubIdType is null - could also set relatedIdentifier to null + } + } + break; + default: + //Some other valid type - we just send the identifier w/o optional attributes + //To Do - validation for other types? + break; + } + } + if (StringUtils.isNotBlank(relatedIdentifier) && StringUtils.isNotBlank(pubIdType)) { + // Still have a valid entry + attributes.put("relatedIdentifierType", pubIdType); + attributes.put("relationType", relationType); + relatedIdentifiersWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "relatedIdentifiers", relatedIdentifiersWritten); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "relatedIdentifier", attributes, relatedIdentifier); + } + } + } + List fmds = dataset.getLatestVersionForCopy().getFileMetadatas(); + if (!((fmds==null) && fmds.isEmpty())) { + attributes.clear(); + attributes.put("relationType", "HasPart"); + for (FileMetadata fmd : fmds) { + DataFile dataFile = fmd.getDataFile(); + GlobalId pid = dataFile.getGlobalId(); + if (pid != null) { + String pubIdType = getCanonicalPublicationType(pid.getProtocol()); + if (pubIdType != null) { + attributes.put("relatedIdentifierType", pubIdType); + relatedIdentifiersWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "relatedIdentifiers", relatedIdentifiersWritten); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "relatedIdentifier", attributes, pid.asRawIdentifier()); + } + } + } + } + } else if (dvObject instanceof DataFile df) { + GlobalId pid = df.getOwner().getGlobalId(); + if (pid != null) { + String pubIdType = getCanonicalPublicationType(pid.getProtocol()); + if (pubIdType != null) { + + attributes.clear(); + attributes.put("relationType", "IsPartOf"); + attributes.put("relatedIdentifierType", pubIdType); + relatedIdentifiersWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "relatedIdentifiers", relatedIdentifiersWritten); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "relatedIdentifier", attributes, pid.asRawIdentifier()); + } + } + } + if (relatedIdentifiersWritten) { + xmlw.writeEndElement(); + } + } + + static HashMap relatedIdentifierTypeMap = new HashMap(); + + private static String getCanonicalPublicationType(String pubIdType) { + if (relatedIdentifierTypeMap.isEmpty()) { + relatedIdentifierTypeMap.put("ARK".toLowerCase(), "ARK"); + relatedIdentifierTypeMap.put("arXiv", "arXiv"); + relatedIdentifierTypeMap.put("bibcode".toLowerCase(), "bibcode"); + relatedIdentifierTypeMap.put("DOI".toLowerCase(), "DOI"); + relatedIdentifierTypeMap.put("EAN13".toLowerCase(), "EAN13"); + relatedIdentifierTypeMap.put("EISSN".toLowerCase(), "EISSN"); + relatedIdentifierTypeMap.put("Handle".toLowerCase(), "Handle"); + relatedIdentifierTypeMap.put("IGSN".toLowerCase(), "IGSN"); + relatedIdentifierTypeMap.put("ISBN".toLowerCase(), "ISBN"); + relatedIdentifierTypeMap.put("ISSN".toLowerCase(), "ISSN"); + relatedIdentifierTypeMap.put("ISTC".toLowerCase(), "ISTC"); + relatedIdentifierTypeMap.put("LISSN".toLowerCase(), "LISSN"); + relatedIdentifierTypeMap.put("LSID".toLowerCase(), "LSID"); + relatedIdentifierTypeMap.put("PISSN".toLowerCase(), "PISSN"); + relatedIdentifierTypeMap.put("PMID".toLowerCase(), "PMID"); + relatedIdentifierTypeMap.put("PURL".toLowerCase(), "PURL"); + relatedIdentifierTypeMap.put("UPC".toLowerCase(), "UPC"); + relatedIdentifierTypeMap.put("URL".toLowerCase(), "URL"); + relatedIdentifierTypeMap.put("URN".toLowerCase(), "URN"); + relatedIdentifierTypeMap.put("WOS".toLowerCase(), "WOS"); + // Add entry for Handle,Perma protocols so this can be used with GlobalId/getProtocol() + relatedIdentifierTypeMap.put("hdl".toLowerCase(), "Handle"); + relatedIdentifierTypeMap.put("perma".toLowerCase(), "URL"); + + } + return relatedIdentifierTypeMap.get(pubIdType); + } + + private void writeSize(XMLStreamWriter xmlw, DvObject dvObject) throws XMLStreamException { + // sizes -> size + boolean sizesWritten = false; + List dataFiles = new ArrayList(); + + if (dvObject instanceof Dataset dataset) { + dataFiles = dataset.getFiles(); + } else if (dvObject instanceof DataFile df) { + dataFiles.add(df); + } + if (dataFiles != null && !dataFiles.isEmpty()) { + for (DataFile dataFile : dataFiles) { + Long size = dataFile.getFilesize(); + if (size != -1) { + sizesWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "sizes", sizesWritten); + XmlWriterUtil.writeFullElement(xmlw, "size", size.toString()); + } + } + } + if (sizesWritten) { + xmlw.writeEndElement(); + } + + } + + private void writeFormats(XMLStreamWriter xmlw, DvObject dvObject) throws XMLStreamException { + + boolean formatsWritten = false; + List dataFiles = new ArrayList(); + + if (dvObject instanceof Dataset dataset) { + dataFiles = dataset.getFiles(); + } else if (dvObject instanceof DataFile df) { + dataFiles.add(df); + } + if (dataFiles != null && !dataFiles.isEmpty()) { + for (DataFile dataFile : dataFiles) { + String format = dataFile.getContentType(); + if (StringUtils.isNotBlank(format)) { + formatsWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "formats", formatsWritten); + XmlWriterUtil.writeFullElement(xmlw, "format", format); + } + /* + * Should original formats be sent? What about original sizes above? + * if(dataFile.isTabularData()) { String originalFormat = + * dataFile.getOriginalFileFormat(); if(StringUtils.isNotBlank(originalFormat)) + * { XmlWriterUtil.writeFullElement(xmlw, "format", format); } } + */ + } + } + if (formatsWritten) { + xmlw.writeEndElement(); + } + + } + + private void writeVersion(XMLStreamWriter xmlw, DvObject dvObject) throws XMLStreamException { + Dataset d = null; + if (dvObject instanceof Dataset) { + d = (Dataset) dvObject; + } else if (dvObject instanceof DataFile) { + d = ((DataFile) dvObject).getOwner(); + } + if (d != null) { + DatasetVersion dv = d.getLatestVersionForCopy(); + String version = dv.getFriendlyVersionNumber(); + if (StringUtils.isNotBlank(version)) { + XmlWriterUtil.writeFullElement(xmlw, "version", version); + } + } + + } + + private void writeAccessRights(XMLStreamWriter xmlw, DvObject dvObject) throws XMLStreamException { + // rightsList -> rights with rightsURI attribute + xmlw.writeStartElement("rightsList"); // + + // set terms from the info:eu-repo-Access-Terms vocabulary + xmlw.writeStartElement("rights"); // + DatasetVersion dv = null; + boolean closed = false; + if (dvObject instanceof Dataset d) { + dv = d.getLatestVersionForCopy(); + closed = dv.isHasRestrictedFile(); + } else if (dvObject instanceof DataFile df) { + dv = df.getOwner().getLatestVersionForCopy(); + + closed = df.isRestricted(); + } + TermsOfUseAndAccess terms = dv.getTermsOfUseAndAccess(); + boolean requestsAllowed = terms.isFileAccessRequest(); + License license = terms.getLicense(); + + if (requestsAllowed && closed) { + xmlw.writeAttribute("rightsURI", "info:eu-repo/semantics/restrictedAccess"); + } else if (!requestsAllowed && closed) { + xmlw.writeAttribute("rightsURI", "info:eu-repo/semantics/closedAccess"); + } else { + xmlw.writeAttribute("rightsURI", "info:eu-repo/semantics/openAccess"); + } + xmlw.writeEndElement(); // + xmlw.writeStartElement("rights"); // + + if (license != null) { + xmlw.writeAttribute("rightsURI", license.getUri().toString()); + xmlw.writeCharacters(license.getName()); + } else { + xmlw.writeAttribute("rightsURI", DatasetUtil.getLicenseURI(dv)); + xmlw.writeCharacters(BundleUtil.getStringFromBundle("license.custom.description")); + ; + } + xmlw.writeEndElement(); // + xmlw.writeEndElement(); // + } + + private void writeDescriptions(XMLStreamWriter xmlw, DvObject dvObject, boolean deaccessioned) throws XMLStreamException { + // descriptions -> description with descriptionType attribute + boolean descriptionsWritten = false; + List descriptions = null; + DatasetVersion dv = null; + if(deaccessioned) { + descriptions = new ArrayList(); + descriptions.add(AbstractDOIProvider.UNAVAILABLE); + } else { + if (dvObject instanceof Dataset d) { + dv = d.getLatestVersionForCopy(); + descriptions = dv.getDescriptions(); + } else if (dvObject instanceof DataFile df) { + String description = df.getDescription(); + if (description != null) { + descriptions = new ArrayList(); + descriptions.add(description); + } + } + } + Map attributes = new HashMap(); + attributes.put("descriptionType", "Abstract"); + if (descriptions != null) { + for (String description : descriptions) { + descriptionsWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "descriptions", descriptionsWritten); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "description", attributes, StringEscapeUtils.escapeXml10(description)); + } + } + + if (dv != null) { + List dsfs = dv.getDatasetFields(); + + for (DatasetField dsf : dsfs) { + + switch (dsf.getDatasetFieldType().getName()) { + case DatasetFieldConstant.software: + attributes.clear(); + attributes.put("descriptionType", "TechnicalInfo"); + List dsfcvs = dsf.getDatasetFieldCompoundValues(); + for (DatasetFieldCompoundValue dsfcv : dsfcvs) { + + String softwareName = null; + String softwareVersion = null; + List childDsfs = dsfcv.getChildDatasetFields(); + for (DatasetField childDsf : childDsfs) { + if (DatasetFieldConstant.softwareName.equals(childDsf.getDatasetFieldType().getName())) { + softwareName = childDsf.getValue(); + } else if (DatasetFieldConstant.softwareVersion.equals(childDsf.getDatasetFieldType().getName())) { + softwareVersion = childDsf.getValue(); + } + } + if (StringUtils.isNotBlank(softwareName)) { + if (StringUtils.isNotBlank(softwareVersion)) { + } + softwareName = softwareName + ", " + softwareVersion; + descriptionsWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "descriptions", descriptionsWritten); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "description", attributes, softwareName); + } + } + break; + case DatasetFieldConstant.originOfSources: + case DatasetFieldConstant.characteristicOfSources: + case DatasetFieldConstant.accessToSources: + attributes.clear(); + attributes.put("descriptionType", "Methods"); + String method = dsf.getValue(); + if (StringUtils.isNotBlank(method)) { + descriptionsWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "descriptions", descriptionsWritten); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "description", attributes, method); + + } + break; + case DatasetFieldConstant.series: + attributes.clear(); + attributes.put("descriptionType", "SeriesInformation"); + dsfcvs = dsf.getDatasetFieldCompoundValues(); + for (DatasetFieldCompoundValue dsfcv : dsfcvs) { + List childDsfs = dsfcv.getChildDatasetFields(); + for (DatasetField childDsf : childDsfs) { + + if (DatasetFieldConstant.seriesName.equals(childDsf.getDatasetFieldType().getName())) { + String seriesInformation = childDsf.getValue(); + if (StringUtils.isNotBlank(seriesInformation)) { + descriptionsWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "descriptions", descriptionsWritten); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "description", attributes, seriesInformation); + } + break; + } + } + } + break; + case DatasetFieldConstant.notesText: + attributes.clear(); + attributes.put("descriptionType", "Other"); + String notesText = dsf.getValue(); + if (StringUtils.isNotBlank(notesText)) { + descriptionsWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "descriptions", descriptionsWritten); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "description", attributes, notesText); + } + break; + + } + } + + } + + if (descriptionsWritten) { + xmlw.writeEndElement(); // + } + } + + private void writeGeoLocations(XMLStreamWriter xmlw, DvObject dvObject) throws XMLStreamException { + if (dvObject instanceof Dataset d) { + boolean geoLocationsWritten = false; + DatasetVersion dv = d.getLatestVersionForCopy(); + + List places = dv.getGeographicCoverage(); + if (places != null && !places.isEmpty()) { + // geoLocationPlace + geoLocationsWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "geoLocations", geoLocationsWritten); + for (String[] place : places) { + xmlw.writeStartElement("geoLocation"); // + + ArrayList placeList = new ArrayList(); + for (String placePart : place) { + if (!StringUtils.isBlank(placePart)) { + placeList.add(placePart); + } + } + XmlWriterUtil.writeFullElement(xmlw, "geoLocationPlace", Strings.join(placeList, ", ")); + xmlw.writeEndElement(); // + } + + } + boolean boundingBoxFound = false; + boolean productionPlaceFound = false; + for (DatasetField dsf : dv.getDatasetFields()) { + switch (dsf.getDatasetFieldType().getName()) { + case DatasetFieldConstant.geographicBoundingBox: + boundingBoxFound = true; + for (DatasetFieldCompoundValue dsfcv : dsf.getDatasetFieldCompoundValues()) { + List childDsfs = dsfcv.getChildDatasetFields(); + String nLatitude = null; + String sLatitude = null; + String eLongitude = null; + String wLongitude = null; + for (DatasetField childDsf : childDsfs) { + switch (childDsf.getDatasetFieldType().getName()) { + case DatasetFieldConstant.northLatitude: + nLatitude = childDsf.getValue(); + break; + case DatasetFieldConstant.southLatitude: + sLatitude = childDsf.getValue(); + break; + case DatasetFieldConstant.eastLongitude: + eLongitude = childDsf.getValue(); + break; + case DatasetFieldConstant.westLongitude: + wLongitude = childDsf.getValue(); + + } + } + if (StringUtils.isNoneBlank(wLongitude, eLongitude, nLatitude, sLatitude)) { + geoLocationsWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "geoLocations", geoLocationsWritten); + xmlw.writeStartElement("geoLocation"); // + if (wLongitude.equals(eLongitude) && nLatitude.equals(sLatitude)) { + // A point + xmlw.writeStartElement("geoLocationPoint"); + XmlWriterUtil.writeFullElement(xmlw, "pointLongitude", eLongitude); + XmlWriterUtil.writeFullElement(xmlw, "pointLatitude", sLatitude); + xmlw.writeEndElement(); + } else { + // A box + xmlw.writeStartElement("geoLocationBox"); + XmlWriterUtil.writeFullElement(xmlw, "westBoundLongitude", wLongitude); + XmlWriterUtil.writeFullElement(xmlw, "eastBoundLongitude", eLongitude); + XmlWriterUtil.writeFullElement(xmlw, "southBoundLatitude", sLatitude); + XmlWriterUtil.writeFullElement(xmlw, "northBoundLatitude", nLatitude); + xmlw.writeEndElement(); + + } + xmlw.writeEndElement(); // + } + } + case DatasetFieldConstant.productionPlace: + productionPlaceFound = true; + // geoLocationPlace + geoLocationsWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "geoLocations", geoLocationsWritten); + List prodPlaces = dsf.getValues(); + for (String prodPlace : prodPlaces) { + xmlw.writeStartElement("geoLocation"); // + XmlWriterUtil.writeFullElement(xmlw, "geoLocationPlace", prodPlace); + xmlw.writeEndElement(); // + } + break; + } + if (boundingBoxFound && productionPlaceFound) { + break; + } + } + if (geoLocationsWritten) { + xmlw.writeEndElement(); // + } + } + + } + + private void writeFundingReferences(XMLStreamWriter xmlw, DvObject dvObject) throws XMLStreamException { + // fundingReferences -> fundingReference -> funderName, awardNumber + boolean fundingReferenceWritten = false; + DatasetVersion dv = null; + if (dvObject instanceof Dataset d) { + dv = d.getLatestVersionForCopy(); + } else if (dvObject instanceof DataFile df) { + dv = df.getOwner().getLatestVersionForCopy(); + } + if (dv != null) { + List retList = new ArrayList<>(); + for (DatasetField dsf : dv.getDatasetFields()) { + if (dsf.getDatasetFieldType().getName().equals(DatasetFieldConstant.contributor)) { + boolean addFunder = false; + for (DatasetFieldCompoundValue contributorValue : dsf.getDatasetFieldCompoundValues()) { + String contributorName = null; + String contributorType = null; + for (DatasetField subField : contributorValue.getChildDatasetFields()) { + if (subField.getDatasetFieldType().getName().equals(DatasetFieldConstant.contributorName)) { + contributorName = subField.getDisplayValue(); + } + if (subField.getDatasetFieldType().getName().equals(DatasetFieldConstant.contributorType)) { + contributorType = subField.getRawValue(); + } + } + // SEK 02/12/2019 move outside loop to prevent contrib type to carry over to + // next contributor + // TODO: Consider how this will work in French, Chinese, etc. + if ("Funder".equals(contributorType)) { + if (!StringUtils.isBlank(contributorName)) { + fundingReferenceWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "fundingReferences", fundingReferenceWritten); + xmlw.writeStartElement("fundingReference"); // + XmlWriterUtil.writeFullElement(xmlw, "funderName", StringEscapeUtils.escapeXml10(contributorName)); + xmlw.writeEndElement(); // + } + } + } + } + if (dsf.getDatasetFieldType().getName().equals(DatasetFieldConstant.grantNumber)) { + for (DatasetFieldCompoundValue grantObject : dsf.getDatasetFieldCompoundValues()) { + String funder = null; + String awardNumber = null; + for (DatasetField subField : grantObject.getChildDatasetFields()) { + // It would be nice to do something with grantNumberValue (the actual number) + // but schema.org doesn't support it. + if (subField.getDatasetFieldType().getName().equals(DatasetFieldConstant.grantNumberAgency)) { + String grantAgency = subField.getDisplayValue(); + funder = grantAgency; + } else if (subField.getDatasetFieldType().getName().equals(DatasetFieldConstant.grantNumberValue)) { + String grantNumberValue = subField.getDisplayValue(); + awardNumber = grantNumberValue; + } + } + if (!StringUtils.isBlank(funder)) { + fundingReferenceWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "fundingReferences", fundingReferenceWritten); + boolean isROR=false; + String funderIdentifier = null; + ExternalIdentifier externalIdentifier = ExternalIdentifier.ROR; + if (externalIdentifier.isValidIdentifier(funder)) { + isROR = true; + JsonObject jo = getExternalVocabularyValue(funder); + if (jo != null) { + funderIdentifier = funder; + funder = jo.getString("termName"); + } + } + + xmlw.writeStartElement("fundingReference"); // + XmlWriterUtil.writeFullElement(xmlw, "funderName", StringEscapeUtils.escapeXml10(funder)); + if (isROR) { + Map attributeMap = new HashMap<>(); + attributeMap.put("schemeURI", "https://ror.org"); + attributeMap.put("funderIdentifierType", "ROR"); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "funderIdentifier", attributeMap, StringEscapeUtils.escapeXml10(funderIdentifier)); + } + if (StringUtils.isNotBlank(awardNumber)) { + XmlWriterUtil.writeFullElement(xmlw, "awardNumber", StringEscapeUtils.escapeXml10(awardNumber)); + } + xmlw.writeEndElement(); // + } + + } + } + } + + if (fundingReferenceWritten) { + xmlw.writeEndElement(); // + } + + } + } +} \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/crossref/CrossRefDOIProvider.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/crossref/CrossRefDOIProvider.java new file mode 100644 index 00000000000..98a4f806836 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/crossref/CrossRefDOIProvider.java @@ -0,0 +1,125 @@ +package edu.harvard.iq.dataverse.pidproviders.doi.crossref; + +import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.GlobalId; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class CrossRefDOIProvider extends AbstractDOIProvider { + private static final Logger logger = Logger.getLogger(CrossRefDOIProvider.class.getCanonicalName()); + + public static final String TYPE = "crossref"; + + CrossRefDOIRegisterService crossRefDOIRegisterService; + + public CrossRefDOIProvider(String id, String label, String providerAuthority, String providerShoulder, String identifierGenerationStyle, String datafilePidFormat, String managedList, String excludedList, + String url, String apiUrl, String username, String password, String depositor, String depositorEmail) { + super(id, label, providerAuthority, providerShoulder, identifierGenerationStyle, datafilePidFormat, + managedList, excludedList); + + crossRefDOIRegisterService = new CrossRefDOIRegisterService(url, apiUrl, username, password, depositor, depositorEmail); + } + + @Override + public boolean alreadyRegistered(GlobalId pid, boolean noProviderDefault) throws Exception { + logger.info("CrossRef alreadyRegistered"); + if (pid == null || pid.asString().isEmpty()) { + logger.fine("No identifier sent."); + return false; + } + boolean alreadyExists; + String identifier = pid.asString(); + try { + alreadyExists = crossRefDOIRegisterService.testDOIExists(identifier); + } catch (Exception e) { + logger.log(Level.WARNING, "alreadyExists failed"); + return false; + } + return alreadyExists; + } + + @Override + public boolean registerWhenPublished() { + return true; + } + + @Override + public List getProviderInformation() { + return List.of("CrossRef", "https://status.crossref.org/"); + } + + @Override + public String createIdentifier(DvObject dvObject) throws Throwable { + logger.info("CrossRef createIdentifier"); + if (dvObject.getIdentifier() == null || dvObject.getIdentifier().isEmpty()) { + dvObject = generatePid(dvObject); + } + String identifier = getIdentifier(dvObject); + try { + String retString = crossRefDOIRegisterService.reserveIdentifier(identifier, dvObject); + logger.log(Level.FINE, "CrossRef create DOI identifier retString : " + retString); + return retString; + } catch (Exception e) { + logger.log(Level.WARNING, "CrossRef Identifier not created: create failed", e); + throw e; + } + } + + @Override + public Map getIdentifierMetadata(DvObject dvObject) { + logger.info("CrossRef getIdentifierMetadata"); + String identifier = getIdentifier(dvObject); + Map metadata = new HashMap<>(); + try { + metadata = crossRefDOIRegisterService.getMetadata(identifier); + } catch (Exception e) { + logger.log(Level.WARNING, "getIdentifierMetadata failed", e); + } + return metadata; + } + + @Override + public String modifyIdentifierTargetURL(DvObject dvObject) throws Exception { + logger.info("CrossRef modifyIdentifier"); + String identifier = getIdentifier(dvObject); + try { + crossRefDOIRegisterService.modifyIdentifier(identifier, dvObject); + } catch (Exception e) { + logger.log(Level.WARNING, "modifyMetadata failed", e); + throw e; + } + return identifier; + } + + @Override + public void deleteIdentifier(DvObject dvo) throws Exception { + logger.info("CrossRef deleteIdentifier"); + } + + @Override + public boolean publicizeIdentifier(DvObject dvObject) { + logger.info("CrossRef updateIdentifierStatus"); + if (dvObject.getIdentifier() == null || dvObject.getIdentifier().isEmpty()) { + dvObject = generatePid(dvObject); + } + String identifier = getIdentifier(dvObject); + + try { + crossRefDOIRegisterService.reserveIdentifier(identifier, dvObject); + return true; + } catch (Exception e) { + logger.log(Level.WARNING, "modifyMetadata failed: " + e.getMessage(), e); + return false; + } + } + + @Override + public String getProviderType() { + return TYPE; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/crossref/CrossRefDOIProviderFactory.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/crossref/CrossRefDOIProviderFactory.java new file mode 100644 index 00000000000..545212cc43c --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/crossref/CrossRefDOIProviderFactory.java @@ -0,0 +1,43 @@ +package edu.harvard.iq.dataverse.pidproviders.doi.crossref; + +import com.google.auto.service.AutoService; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidProviderFactory; +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.util.SystemConfig; + +@AutoService(PidProviderFactory.class) +public class CrossRefDOIProviderFactory implements PidProviderFactory { + + @Override + public PidProvider createPidProvider(String providerId) { + String providerType = JvmSettings.PID_PROVIDER_TYPE.lookup(providerId); + if (!providerType.equals(CrossRefDOIProvider.TYPE)) { + // Being asked to create a non-CrossRef provider + return null; + } + String providerLabel = JvmSettings.PID_PROVIDER_LABEL.lookup(providerId); + String providerAuthority = JvmSettings.PID_PROVIDER_AUTHORITY.lookup(providerId); + String providerShoulder = JvmSettings.PID_PROVIDER_SHOULDER.lookupOptional(providerId).orElse(""); + String identifierGenerationStyle = JvmSettings.PID_PROVIDER_IDENTIFIER_GENERATION_STYLE + .lookupOptional(providerId).orElse("randomString"); + String datafilePidFormat = JvmSettings.PID_PROVIDER_DATAFILE_PID_FORMAT.lookupOptional(providerId) + .orElse(SystemConfig.DataFilePIDFormat.DEPENDENT.toString()); + String managedList = JvmSettings.PID_PROVIDER_MANAGED_LIST.lookupOptional(providerId).orElse(""); + String excludedList = JvmSettings.PID_PROVIDER_EXCLUDED_LIST.lookupOptional(providerId).orElse(""); + + String baseUrl = JvmSettings.CROSSREF_URL.lookup(providerId); + String apiUrl = JvmSettings.CROSSREF_REST_API_URL.lookup(providerId); + String username = JvmSettings.CROSSREF_USERNAME.lookup(providerId); + String password = JvmSettings.CROSSREF_PASSWORD.lookup(providerId); + String depositor = JvmSettings.CROSSREF_DEPOSITOR.lookup(providerId); + String depositorEmail = JvmSettings.CROSSREF_DEPOSITOR_EMAIL.lookup(providerId); + + return new CrossRefDOIProvider(providerId, providerLabel, providerAuthority, providerShoulder, identifierGenerationStyle, + datafilePidFormat, managedList, excludedList, baseUrl, apiUrl, username, password, depositor, depositorEmail); + } + + public String getType() { + return CrossRefDOIProvider.TYPE; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/crossref/CrossRefDOIRegisterService.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/crossref/CrossRefDOIRegisterService.java new file mode 100644 index 00000000000..825b18e7f4b --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/crossref/CrossRefDOIRegisterService.java @@ -0,0 +1,319 @@ +package edu.harvard.iq.dataverse.pidproviders.doi.crossref; + +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.harvard.iq.dataverse.*; +import edu.harvard.iq.dataverse.pidproviders.AbstractPidProvider; +import edu.harvard.iq.dataverse.pidproviders.doi.XmlMetadataTemplate; +import jakarta.enterprise.inject.spi.CDI; +import org.apache.commons.text.StringEscapeUtils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static edu.harvard.iq.dataverse.util.SystemConfig.getDataverseSiteUrlStatic; + +public class CrossRefDOIRegisterService implements java.io.Serializable { + private static final Logger logger = Logger.getLogger(CrossRefDOIRegisterService.class.getCanonicalName()); + + private final String url; + private final String apiUrl; + private final String username; + private final String password; + private final String depositor; + private final String depositorEmail; + + + private static DataverseServiceBean dataverseService = null; + + private CrossRefRESTfullClient client = null; + + public CrossRefDOIRegisterService(String url, String apiUrl, String username, String password, String depositor, String depositorEmail) { + this.url = url; + this.apiUrl = apiUrl; + this.username = username; + this.password = password; + this.depositor = depositor; + this.depositorEmail = depositorEmail; + } + + private CrossRefRESTfullClient getClient() { + if (client == null) { + client = new CrossRefRESTfullClient(url, apiUrl, username, password); + } + return client; + } + + public boolean testDOIExists(String identifier) { + boolean doiExists; + try { + CrossRefRESTfullClient client = getClient(); + doiExists = client.testDOIExists(identifier.substring(identifier.indexOf(":") + 1)); + } catch (Exception e) { + logger.log(Level.INFO, identifier, e); + return false; + } + return doiExists; + } + + public HashMap getMetadata(String identifier) throws IOException { + HashMap metadata = new HashMap<>(); + try { + CrossRefRESTfullClient client = getClient(); + String jsonMetadata = client.getMetadata(identifier.substring(identifier.indexOf(":") + 1)); + Map mappedJson = new ObjectMapper().readValue(jsonMetadata, HashMap.class); + logger.log(Level.FINE, jsonMetadata); + metadata.put("_status", mappedJson.get("status").toString()); + } catch (RuntimeException e) { + logger.log(Level.INFO, identifier, e); + } + return metadata; + } + + public String reserveIdentifier(String identifier, DvObject dvObject) throws IOException { + logger.fine("Crossref reserveIdentifier"); + String xmlMetadata = getMetadataFromDvObject(identifier, dvObject); + + CrossRefRESTfullClient client = getClient(); + return client.postMetadata(xmlMetadata); + } + + public void modifyIdentifier(String identifier, DvObject dvObject) throws IOException { + logger.fine("Crossref modifyIdentifier"); + String xmlMetadata = getMetadataFromDvObject(identifier, dvObject); + + CrossRefRESTfullClient client = getClient(); + client.postMetadata(xmlMetadata); + } + + public String getMetadataFromDvObject(String identifier, DvObject dvObject) { + Dataset dataset; + + if (dvObject instanceof Dataset) { + dataset = (Dataset) dvObject; + } else { + dataset = (Dataset) dvObject.getOwner(); + } + if (dataverseService == null) { + dataverseService = CDI.current().select(DataverseServiceBean.class).get(); + } + + CrossRefMetadataTemplate metadataTemplate = new CrossRefMetadataTemplate(); + metadataTemplate.setIdentifier(identifier.substring(identifier.indexOf(':') + 1)); + metadataTemplate.setAuthors(dataset.getLatestVersion().getDatasetAuthors()); + metadataTemplate.setDepositor(depositor); + metadataTemplate.setDepositorEmail(depositorEmail); + metadataTemplate.setInstitution(dataverseService.getRootDataverseName()); + + String title = dvObject.getCurrentName(); + if (dvObject.isInstanceofDataFile()) { + //Note file title is not currently escaped the way the dataset title is, so adding it here. + title = StringEscapeUtils.escapeXml10(title); + } + + if (title.isEmpty() || title.equals(DatasetField.NA_VALUE)) { + title = AbstractPidProvider.UNAVAILABLE; + } + + metadataTemplate.setTitle(title); + + return metadataTemplate.generateXML(); + } +} + +class CrossRefMetadataTemplate { + + private static final Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.edu.harvard.iq.dataverse.CrossRefMetadataTemplate"); + private static String template; + + static { + try (InputStream in = XmlMetadataTemplate.class.getResourceAsStream("crossref_metadata_template.xml")) { + template = CrossRefFileUtil.readAndClose(in, "utf-8"); + } catch (Exception e) { + logger.log(Level.SEVERE, "crossref metadata template load error"); + logger.log(Level.SEVERE, "String " + e); + logger.log(Level.SEVERE, "localized message " + e.getLocalizedMessage()); + logger.log(Level.SEVERE, "cause " + e.getCause()); + logger.log(Level.SEVERE, "message " + e.getMessage()); + } + } + + private final String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); + private String institution; + private String depositor; + private String depositorEmail; + private String databaseTitle; + private String identifier; + private String title; + private final String baseUrl = getDataverseSiteUrlStatic(); + private List authors; + + public List getAuthors() { + return authors; + } + + public void setAuthors(List authors) { + this.authors = authors; + } + + public CrossRefMetadataTemplate() { + } + + public String generateXML() { + String xmlMetadata = template.replace("${depositor}", depositor) + .replace("${depositorEmail}", depositorEmail) + .replace("${title}", title) + .replace("${institution}", institution) + .replace("${batchId}", identifier + " " + timestamp) + .replace("${timestamp}", timestamp); + + StringBuilder datasetElement = new StringBuilder(); + datasetElement.append(""); + + StringBuilder contributorsElement = new StringBuilder(); + if (authors != null && !authors.isEmpty()) { + contributorsElement.append(""); + for (DatasetAuthor author : authors) { + contributorsElement.append(""); + contributorsElement.append(author.getName().getDisplayValue()); + contributorsElement.append(""); + contributorsElement.append(author.getName().getDisplayValue()); + contributorsElement.append(""); + + if (author.getAffiliation() != null && !author.getAffiliation().getDisplayValue().isEmpty()) { + contributorsElement.append("") + .append(author.getAffiliation().getDisplayValue()) + .append(""); + } + + if (author.getIdType() != null && + author.getIdValue() != null && + !author.getIdType().isEmpty() && + !author.getIdValue().isEmpty() && + author.getAffiliation() != null && + !author.getAffiliation().getDisplayValue().isEmpty()) { + if (author.getIdType().equals("ORCID")) { + contributorsElement.append("").append("https://orcid.org/").append(author.getIdValue()).append(""); + } + if (author.getIdType().equals("ISNI")) { + contributorsElement.append("").append(author.getIdValue()).append(""); + } + if (author.getIdType().equals("LCNA")) { + contributorsElement.append("").append(author.getIdValue()).append(""); + } + } + + contributorsElement.append(""); + } + contributorsElement.append(""); + + } else { + contributorsElement.append("") + .append(AbstractPidProvider.UNAVAILABLE) + .append(""); + } + + datasetElement.append(contributorsElement); + + datasetElement.append("") + .append(this.title) + .append(""); + + datasetElement.append("") + .append(this.identifier) + .append("") + .append(this.baseUrl).append("/dataset.xhtml?persistentId=doi:").append(this.identifier) + .append(""); + + datasetElement.append(""); + xmlMetadata = xmlMetadata.replace("${datasets}", datasetElement.toString()); + return xmlMetadata; + } + + public static String getTemplate() { + return template; + } + + public static void setTemplate(String template) { + CrossRefMetadataTemplate.template = template; + } + + public String getIdentifier() { + return identifier; + } + + public String getDepositor() { + return depositor; + } + + public void setDepositor(String depositor) { + this.depositor = depositor; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getInstitution() { + return institution; + } + + public void setInstitution(String institution) { + this.institution = institution; + } + + public String getDepositorEmail() { + return depositorEmail; + } + + public void setDepositorEmail(String depositorEmail) { + this.depositorEmail = depositorEmail; + } + + public String getDatabaseTitle() { + return databaseTitle; + } + + public void setDatabaseTitle(String databaseTitle) { + this.databaseTitle = databaseTitle; + } +} + +class CrossRefFileUtil { + + public static void close(InputStream in) { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + throw new RuntimeException("Fail to close InputStream"); + } + } + } + + public static String readAndClose(InputStream inStream, String encoding) throws IOException { + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + byte[] buf = new byte[128]; + int cnt; + while ((cnt = inStream.read(buf)) >= 0) { + outStream.write(buf, 0, cnt); + } + return outStream.toString(encoding); + } +} + diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/crossref/CrossRefRESTfullClient.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/crossref/CrossRefRESTfullClient.java new file mode 100644 index 00000000000..ebdf875e664 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/crossref/CrossRefRESTfullClient.java @@ -0,0 +1,119 @@ +package edu.harvard.iq.dataverse.pidproviders.doi.crossref; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class CrossRefRESTfullClient implements Closeable { + + private static final Logger logger = Logger.getLogger(CrossRefRESTfullClient.class.getCanonicalName()); + + private final String url; + private final String apiUrl; + private final String username; + private final String password; + private final CloseableHttpClient httpClient; + private final HttpClientContext context; + private final String encoding = "utf-8"; + + public CrossRefRESTfullClient(String url, String apiUrl, String username, String password) { + this.url = url; + this.apiUrl = apiUrl; + this.username = username; + this.password = password; + try { + context = HttpClientContext.create(); + CredentialsProvider credsProvider = new BasicCredentialsProvider(); + credsProvider.setCredentials(new AuthScope(null, -1), + new UsernamePasswordCredentials(username, password)); + context.setCredentialsProvider(credsProvider); + + httpClient = HttpClients.createDefault(); + } catch (Exception ioe) { + close(); + logger.log(Level.SEVERE,"Fail to init Client",ioe); + throw new RuntimeException("Fail to init Client", ioe); + } + } + + public void close() { + if (this.httpClient != null) { + try { + httpClient.close(); + } catch (IOException io) { + logger.warning("IOException closing httpClient: " + io.getMessage()); + } + } + } + + public String getMetadata(String doi) { + HttpGet httpGet = new HttpGet(this.apiUrl + "/works/" + doi); + httpGet.setHeader("Accept", "application/json"); + try { + HttpResponse response = httpClient.execute(httpGet); + String data = EntityUtils.toString(response.getEntity(), encoding); + if (response.getStatusLine().getStatusCode() != 200) { + String errMsg = "Response from getMetadata: " + response.getStatusLine().getStatusCode() + ", " + data; + logger.warning(errMsg); + throw new RuntimeException(errMsg); + } + return data; + } catch (IOException ioe) { + logger.warning("IOException when get metadata"); + throw new RuntimeException("IOException when get metadata", ioe); + } + } + + public String postMetadata(String xml) throws IOException { + HttpEntity entity = MultipartEntityBuilder.create() + .addTextBody("operation", "doMDUpload") + .addTextBody("login_id", username) + .addTextBody("login_passwd", password) + .addBinaryBody("fname", xml.getBytes(StandardCharsets.UTF_8), ContentType.APPLICATION_XML, "metadata.xml") + .setMode(HttpMultipartMode.BROWSER_COMPATIBLE) + .build(); + + HttpPost httpPost = new HttpPost(url + "/servlet/deposit"); + httpPost.setHeader("Accept", "*/*"); + httpPost.setEntity(entity); + HttpResponse response = httpClient.execute(httpPost); + + String data = EntityUtils.toString(response.getEntity(), encoding); + if (response.getStatusLine().getStatusCode() != 200) { + String errMsg = "Response from postMetadata: " + response.getStatusLine().getStatusCode() + ", " + data; + logger.warning(errMsg); + throw new IOException(errMsg); + } + return data; + } + + public boolean testDOIExists(String doi) throws IOException { + HttpGet httpGet = new HttpGet(this.apiUrl + "/works/" + doi); + httpGet.setHeader("Accept", "application/json"); + HttpResponse response = httpClient.execute(httpGet); + if (response.getStatusLine().getStatusCode() != 200) { + EntityUtils.consumeQuietly(response.getEntity()); + return false; + } + EntityUtils.consumeQuietly(response.getEntity()); + return true; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/datacite/DOIDataCiteRegisterService.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/datacite/DOIDataCiteRegisterService.java new file mode 100644 index 00000000000..a4d788de4df --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/datacite/DOIDataCiteRegisterService.java @@ -0,0 +1,260 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package edu.harvard.iq.dataverse.pidproviders.doi.datacite; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.text.StringEscapeUtils; + +import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetField; +import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.branding.BrandingUtil; +import edu.harvard.iq.dataverse.pidproviders.AbstractPidProvider; +import edu.harvard.iq.dataverse.pidproviders.doi.DoiMetadata; +import edu.harvard.iq.dataverse.pidproviders.doi.XmlMetadataTemplate; + +import org.xmlunit.builder.DiffBuilder; +import org.xmlunit.builder.Input; +import org.xmlunit.builder.Input.Builder; +import org.xmlunit.diff.Diff; +import org.xmlunit.diff.Difference; + +/** + * + * @author luopc + */ +public class DOIDataCiteRegisterService { + + private static final Logger logger = Logger.getLogger(DOIDataCiteRegisterService.class.getCanonicalName()); + + + //A singleton since it, and the httpClient in it can be reused. + private DataCiteRESTfullClient client=null; + + public DOIDataCiteRegisterService(String url, String username, String password) { + client = new DataCiteRESTfullClient(url, username, password); + } + + /** + * This "reserveIdentifier" method is heavily based on the + * "registerIdentifier" method below but doesn't, this one doesn't doesn't + * register a URL, which causes the "state" of DOI to transition from + * "draft" to "findable". Here are some DataCite docs on the matter: + * + * "DOIs can exist in three states: draft, registered, and findable. DOIs + * are in the draft state when metadata have been registered, and will + * transition to the findable state when registering a URL." -- + * https://support.datacite.org/docs/mds-api-guide#doi-states + */ + public String reserveIdentifier(String identifier, Map metadata, DvObject dvObject) throws IOException { + String retString = ""; + String xmlMetadata = getMetadataFromDvObject(identifier, metadata, dvObject); + + retString = client.postMetadata(xmlMetadata); + + return retString; + } + + public String registerIdentifier(String identifier, Map metadata, DvObject dvObject) throws IOException { + String retString = ""; + String xmlMetadata = getMetadataFromDvObject(identifier, metadata, dvObject); + String target = metadata.get("_target"); + + retString = client.postMetadata(xmlMetadata); + client.postUrl(identifier.substring(identifier.indexOf(":") + 1), target); + + return retString; + } + + + public String reRegisterIdentifier(String identifier, Map metadata, DvObject dvObject) throws IOException { + String retString = ""; + String numericIdentifier = identifier.substring(identifier.indexOf(":") + 1); + String xmlMetadata = getMetadataFromDvObject(identifier, metadata, dvObject); + String target = metadata.get("_target"); + String currentMetadata = client.getMetadata(numericIdentifier); + Diff myDiff = DiffBuilder.compare(xmlMetadata) + .withTest(currentMetadata).ignoreWhitespace().checkForSimilar() + .build(); + + if (myDiff.hasDifferences()) { + for(Difference d : myDiff.getDifferences()) { + + logger.fine(d.toString()); + } + retString = "metadata:\\r" + client.postMetadata(xmlMetadata) + "\\r"; + } + if (!target.equals(client.getUrl(numericIdentifier))) { + logger.info("Updating target URL to " + target); + client.postUrl(numericIdentifier, target); + retString = retString + "url:\\r" + target; + + } + + return retString; + } + + + public String deactivateIdentifier(String identifier, Map metadata, DvObject dvObject) throws IOException { + String retString = ""; + + String metadataString = getMetadataForDeactivateIdentifier(identifier, metadata, dvObject); + retString = client.postMetadata(metadataString); + retString = client.inactiveDataset(identifier.substring(identifier.indexOf(":") + 1)); + + return retString; + } + + public static String getMetadataFromDvObject(String identifier, Map metadata, DvObject dvObject) { + + Dataset dataset = null; + + if (dvObject instanceof Dataset) { + dataset = (Dataset) dvObject; + } else { + dataset = (Dataset) dvObject.getOwner(); + } + + DoiMetadata doiMetadata = new DoiMetadata(); + doiMetadata.setIdentifier(identifier.substring(identifier.indexOf(':') + 1)); + doiMetadata.setCreators(Arrays.asList(metadata.get("datacite.creator").split("; "))); + doiMetadata.setAuthors(dataset.getLatestVersion().getDatasetAuthors()); + if (dvObject.isInstanceofDataset()) { + //While getDescriptionPlainText strips < and > from HTML, it leaves '&' (at least so we need to xml escape as well + String description = StringEscapeUtils.escapeXml10(dataset.getLatestVersion().getDescriptionPlainText()); + if (description.isEmpty() || description.equals(DatasetField.NA_VALUE)) { + description = AbstractPidProvider.UNAVAILABLE; + } + doiMetadata.setDescription(description); + } + if (dvObject.isInstanceofDataFile()) { + DataFile df = (DataFile) dvObject; + //Note: File metadata is not escaped like dataset metadata is, so adding an xml escape here. + //This could/should be removed if the datafile methods add escaping + String fileDescription = StringEscapeUtils.escapeXml10(df.getDescription()); + doiMetadata.setDescription(fileDescription == null ? AbstractPidProvider.UNAVAILABLE : fileDescription); + } + + doiMetadata.setContacts(dataset.getLatestVersion().getDatasetContacts()); + doiMetadata.setProducers(dataset.getLatestVersion().getDatasetProducers()); + String title = dvObject.getCurrentName(); + if(dvObject.isInstanceofDataFile()) { + //Note file title is not currently escaped the way the dataset title is, so adding it here. + title = StringEscapeUtils.escapeXml10(title); + } + + if (title.isEmpty() || title.equals(DatasetField.NA_VALUE)) { + title = AbstractPidProvider.UNAVAILABLE; + } + + doiMetadata.setTitle(title); + String producerString = BrandingUtil.getInstallationBrandName(); + if (producerString.isEmpty() || producerString.equals(DatasetField.NA_VALUE)) { + producerString = AbstractPidProvider.UNAVAILABLE; + } + doiMetadata.setPublisher(producerString); + doiMetadata.setPublisherYear(metadata.get("datacite.publicationyear")); + + String xmlMetadata = new XmlMetadataTemplate(doiMetadata).generateXML(dvObject); + logger.log(Level.FINE, "XML to send to DataCite: {0}", xmlMetadata); + return xmlMetadata; + } + + public static String getMetadataForDeactivateIdentifier(String identifier, Map metadata, DvObject dvObject) { + + DoiMetadata doiMetadata = new DoiMetadata(); + + doiMetadata.setIdentifier(identifier.substring(identifier.indexOf(':') + 1)); + doiMetadata.setCreators(Arrays.asList(metadata.get("datacite.creator").split("; "))); + + doiMetadata.setDescription(AbstractPidProvider.UNAVAILABLE); + + String title =metadata.get("datacite.title"); + + System.out.print("Map metadata title: "+ metadata.get("datacite.title")); + + doiMetadata.setAuthors(null); + + doiMetadata.setTitle(title); + String producerString = AbstractPidProvider.UNAVAILABLE; + + doiMetadata.setPublisher(producerString); + doiMetadata.setPublisherYear(metadata.get("datacite.publicationyear")); + + String xmlMetadata = new XmlMetadataTemplate(doiMetadata).generateXML(dvObject); + logger.log(Level.FINE, "XML to send to DataCite: {0}", xmlMetadata); + return xmlMetadata; + } + + public String modifyIdentifier(String identifier, Map metadata, DvObject dvObject) + throws IOException { + + String xmlMetadata = getMetadataFromDvObject(identifier, metadata, dvObject); + + logger.fine("XML to send to DataCite: " + xmlMetadata); + + String status = metadata.get("_status").trim(); + String target = metadata.get("_target"); + String retString = ""; + switch (status) { + case DataCiteDOIProvider.DRAFT: + // draft DOIs aren't currently being updated after every edit - ToDo - should + // this be changed or made optional? + retString = "success to reserved " + identifier; + break; + case DataCiteDOIProvider.FINDABLE: + try { + retString = client.postMetadata(xmlMetadata); + client.postUrl(identifier.substring(identifier.indexOf(":") + 1), target); + } catch (UnsupportedEncodingException ex) { + logger.log(Level.SEVERE, null, ex); + } catch (RuntimeException rte) { + logger.log(Level.SEVERE, "Error creating DOI at DataCite: {0}", rte.getMessage()); + logger.log(Level.SEVERE, "Exception", rte); + } + break; + case DataCiteDOIProvider.REGISTERED: + retString = client.inactiveDataset(identifier.substring(identifier.indexOf(":") + 1)); + break; + } + return retString; + } + + public boolean testDOIExists(String identifier) { + boolean doiExists; + try { + doiExists = client.testDOIExists(identifier.substring(identifier.indexOf(":") + 1)); + } catch (Exception e) { + logger.log(Level.INFO, identifier, e); + return false; + } + return doiExists; + } + + Map getMetadata(String identifier) throws IOException { + Map metadata = new HashMap<>(); + try { + String xmlMetadata = client.getMetadata(identifier.substring(identifier.indexOf(":") + 1)); + DoiMetadata doiMetadata = new DoiMetadata(); + doiMetadata.parseDataCiteXML(xmlMetadata); + metadata.put("datacite.creator", String.join("; ", doiMetadata.getCreators())); + metadata.put("datacite.title", doiMetadata.getTitle()); + metadata.put("datacite.publisher", doiMetadata.getPublisher()); + metadata.put("datacite.publicationyear", doiMetadata.getPublisherYear()); + } catch (RuntimeException e) { + logger.log(Level.INFO, identifier, e); + } + return metadata; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/datacite/DataCiteDOIProvider.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/datacite/DataCiteDOIProvider.java new file mode 100644 index 00000000000..5630844fb32 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/datacite/DataCiteDOIProvider.java @@ -0,0 +1,346 @@ +package edu.harvard.iq.dataverse.pidproviders.doi.datacite; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetVersion; +import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.FileMetadata; +import edu.harvard.iq.dataverse.GlobalId; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; +import jakarta.json.JsonObject; + +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.HttpStatus; + +/** + * + * @author luopc + */ +public class DataCiteDOIProvider extends AbstractDOIProvider { + + private static final Logger logger = Logger.getLogger(DataCiteDOIProvider.class.getCanonicalName()); + + static final String FINDABLE = "findable"; //public - published dataset versions + static final String DRAFT = "draft"; //reserved but not findable yet - draft/unpublished datasets + static final String REGISTERED = "registered"; //was findable once, not anymore - deaccessioned datasets + static final String NONE = "none"; //no record - draft/unpublished datasets where the initial request to reserve has failed + + public static final String TYPE = "datacite"; + + + private String mdsUrl; + private String apiUrl; + private String username; + private String password; + + private DOIDataCiteRegisterService doiDataCiteRegisterService; + + public DataCiteDOIProvider(String id, String label, String providerAuthority, String providerShoulder, + String identifierGenerationStyle, String datafilePidFormat, String managedList, String excludedList, + String mdsUrl, String apiUrl, String username, String password) { + super(id, label, providerAuthority, providerShoulder, identifierGenerationStyle, datafilePidFormat, managedList, + excludedList); + this.mdsUrl = mdsUrl; + this.apiUrl = apiUrl; + this.username = username; + this.password = password; + doiDataCiteRegisterService = new DOIDataCiteRegisterService(mdsUrl, username, password); + } + + @Override + public boolean registerWhenPublished() { + return false; + } + + @Override + 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 alreadyRegistered; + String identifier = pid.asString(); + try { + alreadyRegistered = doiDataCiteRegisterService.testDOIExists(identifier); + } catch (Exception e) { + logger.log(Level.WARNING, "alreadyRegistered failed"); + return false; + } + return alreadyRegistered; + } + + @Override + public String createIdentifier(DvObject dvObject) throws Exception { + logger.log(Level.FINE, "createIdentifier"); + if (dvObject.getIdentifier() == null || dvObject.getIdentifier().isEmpty()) { + dvObject = generatePid(dvObject); + } + String identifier = getIdentifier(dvObject); + Map metadata = getMetadataForCreateIndicator(dvObject); + metadata.put("_status", DRAFT); + try { + String retString = doiDataCiteRegisterService.reserveIdentifier(identifier, metadata, dvObject); + logger.log(Level.FINE, "create DOI identifier retString : " + retString); + return retString; + } catch (Exception e) { + logger.log(Level.WARNING, "Identifier not created: create failed", e); + throw e; + } + } + + @Override + public Map getIdentifierMetadata(DvObject dvObject) { + logger.log(Level.FINE, "getIdentifierMetadata"); + String identifier = getIdentifier(dvObject); + Map metadata = new HashMap<>(); + try { + metadata = doiDataCiteRegisterService.getMetadata(identifier); + metadata.put("_status", getPidStatus(dvObject)); + } catch (Exception e) { + logger.log(Level.WARNING, "getIdentifierMetadata failed", e); + } + return metadata; + } + + /** + * Modifies the DOI metadata for a Dataset + * + * @param dvObject the dvObject whose metadata needs to be modified + * @return the Dataset identifier, or null if the modification failed + * @throws java.lang.Exception + */ + @Override + public String modifyIdentifierTargetURL(DvObject dvObject) throws Exception { + logger.log(Level.FINE, "modifyIdentifier"); + String identifier = getIdentifier(dvObject); + try { + Map metadata = getIdentifierMetadata(dvObject); + metadata.put("_target", getTargetUrl(dvObject)); + doiDataCiteRegisterService.modifyIdentifier(identifier, metadata, dvObject); + } catch (Exception e) { + logger.log(Level.WARNING, "modifyMetadata failed", e); + throw e; + } + return identifier; + } + + /* + * Deletes a DOI if it is in DRAFT/DRAFT state or removes metadata and + * changes it from PUBLIC/FINDABLE to REGISTERED. + */ + @Override + public void deleteIdentifier(DvObject dvObject) throws IOException, HttpException { + logger.log(Level.FINE, "deleteIdentifier"); + String identifier = getIdentifier(dvObject); + String idStatus = getPidStatus(dvObject); + switch (idStatus) { + case DRAFT: + logger.log(Level.FINE, "Delete status is reserved.."); + deleteDraftIdentifier(dvObject); + break; + case FINDABLE: + // if public then it has been released set to REGISTERED/unavailable and reset + // target to n2t url + Map metadata = addDOIMetadataForDestroyedDataset(dvObject); + metadata.put("_status", "registered"); + metadata.put("_target", getTargetUrl(dvObject)); + doiDataCiteRegisterService.deactivateIdentifier(identifier, metadata, dvObject); + break; + + case REGISTERED: + case NONE: + // Nothing to do + } + } + + /** + * Deletes DOI from the DataCite side, if possible. Only "draft" DOIs can be + * deleted. + */ + private void deleteDraftIdentifier(DvObject dvObject) throws IOException { + + GlobalId doi = dvObject.getGlobalId(); + /** + * Deletes the DOI from DataCite if it can. Returns 204 if PID was deleted (only + * possible for "draft" DOIs), 405 (method not allowed) if the DOI wasn't + * deleted (because it's in "findable" state, for example, 404 if the DOI wasn't + * found, and possibly other status codes such as 500 if DataCite is down. + */ + + URL url = new URL(getApiUrl() + "/dois/" + doi.getAuthority() + "/" + doi.getIdentifier()); + HttpURLConnection connection = null; + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("DELETE"); + String userpass = getUsername() + ":" + getPassword(); + String basicAuth = "Basic " + new String(Base64.getEncoder().encode(userpass.getBytes())); + connection.setRequestProperty("Authorization", basicAuth); + int status = connection.getResponseCode(); + if (status != HttpStatus.SC_NO_CONTENT) { + logger.warning( + "Incorrect Response Status from DataCite: " + status + " : " + connection.getResponseMessage()); + throw new HttpException("Status: " + status); + } + logger.fine("deleteDoi status for " + doi.asString() + ": " + status); + } + + @Override + public boolean publicizeIdentifier(DvObject dvObject) { + logger.log(Level.FINE, "updateIdentifierStatus"); + if (dvObject.getIdentifier() == null || dvObject.getIdentifier().isEmpty()) { + dvObject = generatePid(dvObject); + } + String identifier = getIdentifier(dvObject); + Map metadata = getUpdateMetadata(dvObject); + metadata.put("_status", FINDABLE); + metadata.put("datacite.publicationyear", generateYear(dvObject)); + metadata.put("_target", getTargetUrl(dvObject)); + try { + doiDataCiteRegisterService.registerIdentifier(identifier, metadata, dvObject); + return true; + } catch (Exception e) { + logger.log(Level.WARNING, "modifyMetadata failed: " + e.getMessage(), e); + return false; + } + } + + @Override + public List getProviderInformation() { + return List.of(getId(), "https://status.datacite.org"); + } + + @Override + protected String getProviderKeyName() { + return "DataCite"; + } + + @Override + public String getProviderType() { + // TODO Auto-generated method stub + return null; + } + + public String getMdsUrl() { + return mdsUrl; + } + + public String getApiUrl() { + return apiUrl; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + /** + * Method to determine the status of a dvObject's PID. It replaces keeping a + * separate DOIDataCiteRegisterCache. We could also try to get this info from + * DataCite directly, but it appears to not be in the xml metadata return, so it + * would require another/different api call (possible ToDo). + * + * @param dvObject - Dataset or DataFile + * @return PID status - NONE, DRAFT, FINDABLE, or REGISTERED + */ + String getPidStatus(DvObject dvObject) { + String status = NONE; + if (dvObject instanceof Dataset) { + Dataset dataset = (Dataset) dvObject; + // return true, if all published versions were deaccessioned + boolean hasDeaccessionedVersions = false; + for (DatasetVersion testDsv : dataset.getVersions()) { + if (testDsv.isReleased()) { + // With any released version, we're done + return FINDABLE; + } + // Also check for draft version + if (testDsv.isDraft()) { + if (dataset.isIdentifierRegistered()) { + status = DRAFT; + // Keep interating to see if there's a released version + } + } + if (testDsv.isDeaccessioned()) { + hasDeaccessionedVersions = true; + // Keep interating to see if there's a released version + } + } + if (hasDeaccessionedVersions) { + if (dataset.isIdentifierRegistered()) { + return REGISTERED; + } + } + return status; + } else if (dvObject instanceof DataFile) { + DataFile df = (DataFile) dvObject; + // return true, if all published versions were deaccessioned + boolean isInDeaccessionedVersions = false; + for (FileMetadata fmd : df.getFileMetadatas()) { + DatasetVersion testDsv = fmd.getDatasetVersion(); + if (testDsv.isReleased()) { + // With any released version, we're done + return FINDABLE; + } + // Also check for draft version + if (testDsv.isDraft()) { + if (df.isIdentifierRegistered()) { + status = DRAFT; + // Keep interating to see if there's a released/deaccessioned version + } + } + if (testDsv.isDeaccessioned()) { + isInDeaccessionedVersions = true; + // Keep interating to see if there's a released version + } + } + if (isInDeaccessionedVersions) { + if (df.isIdentifierRegistered()) { + return REGISTERED; + } + } + + } + return status; + } + + + @Override + public boolean updateIdentifier(DvObject dvObject) { + logger.log(Level.FINE,"updateIdentifierStatus"); + if(dvObject.getIdentifier() == null || dvObject.getIdentifier().isEmpty() ){ + dvObject = generatePid(dvObject); + } + String identifier = getIdentifier(dvObject); + Map metadata = getUpdateMetadata(dvObject); + metadata.put("_status", "public"); + metadata.put("datacite.publicationyear", generateYear(dvObject)); + metadata.put("_target", getTargetUrl(dvObject)); + try { + String updated = doiDataCiteRegisterService.reRegisterIdentifier(identifier, metadata, dvObject); + if(updated.length()!=0) { + logger.info(identifier + "updated: " + updated ); + return true; + } else { + logger.info("No updated needed for " + identifier); + return false; //No update needed + } + } catch (Exception e) { + logger.log(Level.WARNING, "updateIdentifier failed: " + e.getMessage(), e); + return false; + } + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/datacite/DataCiteProviderFactory.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/datacite/DataCiteProviderFactory.java new file mode 100644 index 00000000000..99d13b2647c --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/datacite/DataCiteProviderFactory.java @@ -0,0 +1,43 @@ +package edu.harvard.iq.dataverse.pidproviders.doi.datacite; + +import com.google.auto.service.AutoService; + +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidProviderFactory; +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.util.SystemConfig; + +@AutoService(PidProviderFactory.class) +public class DataCiteProviderFactory implements PidProviderFactory { + + @Override + public PidProvider createPidProvider(String providerId) { + String providerType = JvmSettings.PID_PROVIDER_TYPE.lookup(providerId); + if (!providerType.equals(DataCiteDOIProvider.TYPE)) { + // Being asked to create a non-DataCite provider + return null; + } + String providerLabel = JvmSettings.PID_PROVIDER_LABEL.lookup(providerId); + String providerAuthority = JvmSettings.PID_PROVIDER_AUTHORITY.lookup(providerId); + String providerShoulder = JvmSettings.PID_PROVIDER_SHOULDER.lookupOptional(providerId).orElse(""); + String identifierGenerationStyle = JvmSettings.PID_PROVIDER_IDENTIFIER_GENERATION_STYLE + .lookupOptional(providerId).orElse("randomString"); + String datafilePidFormat = JvmSettings.PID_PROVIDER_DATAFILE_PID_FORMAT.lookupOptional(providerId) + .orElse(SystemConfig.DataFilePIDFormat.DEPENDENT.toString()); + String managedList = JvmSettings.PID_PROVIDER_MANAGED_LIST.lookupOptional(providerId).orElse(""); + String excludedList = JvmSettings.PID_PROVIDER_EXCLUDED_LIST.lookupOptional(providerId).orElse(""); + + String mdsUrl = JvmSettings.DATACITE_MDS_API_URL.lookupOptional(providerId).orElse("https://mds.test.datacite.org"); + String apiUrl = JvmSettings.DATACITE_REST_API_URL.lookupOptional(providerId).orElse("https://api.test.datacite.org"); + String username = JvmSettings.DATACITE_USERNAME.lookup(providerId); + String password = JvmSettings.DATACITE_PASSWORD.lookup(providerId); + + return new DataCiteDOIProvider(providerId, providerLabel, providerAuthority, providerShoulder, identifierGenerationStyle, + datafilePidFormat, managedList, excludedList, mdsUrl, apiUrl, username, password); + } + + public String getType() { + return DataCiteDOIProvider.TYPE; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/DataCiteRESTfullClient.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/datacite/DataCiteRESTfullClient.java similarity index 91% rename from src/main/java/edu/harvard/iq/dataverse/DataCiteRESTfullClient.java rename to src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/datacite/DataCiteRESTfullClient.java index 491f19ab36c..465b10ee407 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataCiteRESTfullClient.java +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/datacite/DataCiteRESTfullClient.java @@ -3,7 +3,7 @@ * To change this template file, choose Tools | Templates * and open the template in the editor. */ -package edu.harvard.iq.dataverse; +package edu.harvard.iq.dataverse.pidproviders.doi.datacite; import java.io.Closeable; @@ -21,6 +21,7 @@ import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.HttpEntity; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; @@ -45,21 +46,14 @@ public class DataCiteRESTfullClient implements Closeable { private HttpClientContext context; private String encoding = "utf-8"; - public DataCiteRESTfullClient(String url, String username, String password) throws IOException { + public DataCiteRESTfullClient(String url, String username, String password) { this.url = url; - try { - context = HttpClientContext.create(); - CredentialsProvider credsProvider = new BasicCredentialsProvider(); - credsProvider.setCredentials(new AuthScope(null, -1), - new UsernamePasswordCredentials(username, password)); - context.setCredentialsProvider(credsProvider); - - httpClient = HttpClients.createDefault(); - } catch (Exception ioe) { - close(); - logger.log(Level.SEVERE,"Fail to init Client",ioe); - throw new RuntimeException("Fail to init Client", ioe); - } + context = HttpClientContext.create(); + CredentialsProvider credsProvider = new BasicCredentialsProvider(); + credsProvider.setCredentials(new AuthScope(null, -1), new UsernamePasswordCredentials(username, password)); + context.setCredentialsProvider(credsProvider); + + httpClient = HttpClients.createDefault(); } public void close() { @@ -82,7 +76,12 @@ public String getUrl(String doi) { HttpGet httpGet = new HttpGet(this.url + "/doi/" + doi); try { HttpResponse response = httpClient.execute(httpGet,context); - String data = EntityUtils.toString(response.getEntity(), encoding); + HttpEntity entity = response.getEntity(); + String data = null; + + if(entity != null) { + data = EntityUtils.toString(entity, encoding); + } if (response.getStatusLine().getStatusCode() != 200) { throw new RuntimeException("Response code: " + response.getStatusLine().getStatusCode() + ", " + data); } diff --git a/src/main/java/edu/harvard/iq/dataverse/DOIEZIdServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/ezid/EZIdDOIProvider.java similarity index 89% rename from src/main/java/edu/harvard/iq/dataverse/DOIEZIdServiceBean.java rename to src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/ezid/EZIdDOIProvider.java index 86b74b72f30..fe8f1ec9c70 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DOIEZIdServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/ezid/EZIdDOIProvider.java @@ -1,5 +1,10 @@ -package edu.harvard.iq.dataverse; +package edu.harvard.iq.dataverse.pidproviders.doi.ezid; +import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.GlobalId; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.ucsb.nceas.ezid.EZIDException; import edu.ucsb.nceas.ezid.EZIDService; @@ -13,40 +18,40 @@ * * @author skraffmiller */ -@Stateless -public class DOIEZIdServiceBean extends DOIServiceBean { +public class EZIdDOIProvider extends AbstractDOIProvider { - private static final Logger logger = Logger.getLogger(DOIEZIdServiceBean.class.getCanonicalName()); + + + private static final Logger logger = Logger.getLogger(EZIdDOIProvider.class.getCanonicalName()); EZIDService ezidService; - // This has a sane default in microprofile-config.properties - private final String baseUrl = JvmSettings.EZID_API_URL.lookup(); + public static final String TYPE = "ezid"; + + private String baseUrl; - public DOIEZIdServiceBean() { + + public EZIdDOIProvider(String id, String label, String providerAuthority, String providerShoulder, String identifierGenerationStyle, + String datafilePidFormat, String managedList, String excludedList, String baseUrl, String username, String password) { + super(id, label, providerAuthority, providerShoulder, identifierGenerationStyle, datafilePidFormat, managedList, excludedList); // 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); + this.baseUrl = baseUrl; + this.ezidService = new EZIDService(baseUrl); try { - // 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; + } else { + logger.log(Level.WARNING, "You must give both username and password. Will not try to login."); } } catch (EZIDException e) { // 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, "Login failed to {0}", 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()); @@ -227,14 +232,14 @@ private boolean updateIdentifierStatus(DvObject dvObject, String statusIn) { @Override public List getProviderInformation(){ - return List.of("EZID", this.baseUrl); + return List.of(getId(), this.baseUrl); } @Override public String createIdentifier(DvObject dvObject) throws Throwable { logger.log(Level.FINE, "createIdentifier"); if(dvObject.getIdentifier() == null || dvObject.getIdentifier().isEmpty() ){ - dvObject = generateIdentifier(dvObject); + dvObject = generatePid(dvObject); } String identifier = getIdentifier(dvObject); Map metadata = getMetadataForCreateIndicator(dvObject); @@ -271,7 +276,7 @@ public String createIdentifier(DvObject dvObject) throws Throwable { * @return A HashMap with the same values as {@code map} */ private HashMap asHashMap(Map map) { - return (map instanceof HashMap) ? (HashMap)map : new HashMap<>(map); + return (map instanceof HashMap) ? (HashMap)map : new HashMap<>(map); } @Override @@ -279,5 +284,9 @@ protected String getProviderKeyName() { return "EZID"; } + @Override + public String getProviderType() { + return TYPE; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/ezid/EZIdProviderFactory.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/ezid/EZIdProviderFactory.java new file mode 100644 index 00000000000..95ad9bdeff0 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/ezid/EZIdProviderFactory.java @@ -0,0 +1,42 @@ +package edu.harvard.iq.dataverse.pidproviders.doi.ezid; + +import com.google.auto.service.AutoService; + +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidProviderFactory; +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.util.SystemConfig; + +@AutoService(PidProviderFactory.class) +public class EZIdProviderFactory implements PidProviderFactory { + + @Override + public PidProvider createPidProvider(String providerId) { + String providerType = JvmSettings.PID_PROVIDER_TYPE.lookup(providerId); + if (!providerType.equals(EZIdDOIProvider.TYPE)) { + // Being asked to create a non-EZId provider + return null; + } + String providerLabel = JvmSettings.PID_PROVIDER_LABEL.lookup(providerId); + String providerAuthority = JvmSettings.PID_PROVIDER_AUTHORITY.lookup(providerId); + String providerShoulder = JvmSettings.PID_PROVIDER_SHOULDER.lookupOptional(providerId).orElse(""); + String identifierGenerationStyle = JvmSettings.PID_PROVIDER_IDENTIFIER_GENERATION_STYLE + .lookupOptional(providerId).orElse("randomString"); + String datafilePidFormat = JvmSettings.PID_PROVIDER_DATAFILE_PID_FORMAT.lookupOptional(providerId) + .orElse(SystemConfig.DataFilePIDFormat.DEPENDENT.toString()); + String managedList = JvmSettings.PID_PROVIDER_MANAGED_LIST.lookupOptional(providerId).orElse(""); + String excludedList = JvmSettings.PID_PROVIDER_EXCLUDED_LIST.lookupOptional(providerId).orElse(""); + + String baseUrl = JvmSettings.EZID_API_URL.lookupOptional(providerId).orElse("https://ezid.cdlib.org"); + String username = JvmSettings.EZID_USERNAME.lookup(providerId); + String password = JvmSettings.EZID_PASSWORD.lookup(providerId); + + return new EZIdDOIProvider(providerId, providerLabel, providerAuthority, providerShoulder, identifierGenerationStyle, datafilePidFormat, + managedList, excludedList, baseUrl, username, password); + } + + public String getType() { + return EZIdDOIProvider.TYPE; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/FakePidProviderServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/fake/FakeDOIProvider.java similarity index 59% rename from src/main/java/edu/harvard/iq/dataverse/pidproviders/FakePidProviderServiceBean.java rename to src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/fake/FakeDOIProvider.java index 3bd9d9dd022..a967fb40620 100644 --- a/src/main/java/edu/harvard/iq/dataverse/pidproviders/FakePidProviderServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/fake/FakeDOIProvider.java @@ -1,22 +1,22 @@ -package edu.harvard.iq.dataverse.pidproviders; +package edu.harvard.iq.dataverse.pidproviders.doi.fake; -import edu.harvard.iq.dataverse.DOIServiceBean; import edu.harvard.iq.dataverse.DvObject; import edu.harvard.iq.dataverse.GlobalId; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.logging.Logger; -import jakarta.ejb.Stateless; +public class FakeDOIProvider extends AbstractDOIProvider { -@Stateless -public class FakePidProviderServiceBean extends DOIServiceBean { + public static final String TYPE = "FAKE"; - private static final Logger logger = Logger.getLogger(FakePidProviderServiceBean.class.getCanonicalName()); + public FakeDOIProvider(String id, String label, String providerAuthority, String providerShoulder, String identifierGenerationStyle, + String datafilePidFormat, String managedList, String excludedList) { + super(id, label, providerAuthority, providerShoulder, identifierGenerationStyle, datafilePidFormat, managedList, excludedList); + } - //Only need to check locally public boolean isGlobalIdUnique(GlobalId globalId) { try { @@ -29,7 +29,7 @@ public boolean isGlobalIdUnique(GlobalId globalId) { @Override public boolean alreadyRegistered(GlobalId globalId, boolean noProviderDefault) { - boolean existsLocally = !dvObjectService.isGlobalIdLocallyUnique(globalId); + boolean existsLocally = !pidProviderService.isGlobalIdLocallyUnique(globalId); return existsLocally ? existsLocally : noProviderDefault; } @@ -40,7 +40,7 @@ public boolean registerWhenPublished() { @Override public List getProviderInformation() { - return List.of("FAKE", "https://dataverse.org"); + return List.of(getId(), "https://dataverse.org"); } @Override @@ -65,7 +65,10 @@ public void deleteIdentifier(DvObject dvo) throws Exception { } @Override - public boolean publicizeIdentifier(DvObject studyIn) { + public boolean publicizeIdentifier(DvObject dvObject) { + if(dvObject.isInstanceofDataFile() && dvObject.getGlobalId()==null) { + generatePid(dvObject); + } return true; } @@ -74,4 +77,9 @@ protected String getProviderKeyName() { return "FAKE"; } + @Override + public String getProviderType() { + return TYPE; + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/fake/FakeProviderFactory.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/fake/FakeProviderFactory.java new file mode 100644 index 00000000000..292c39d4383 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/fake/FakeProviderFactory.java @@ -0,0 +1,38 @@ +package edu.harvard.iq.dataverse.pidproviders.doi.fake; + +import com.google.auto.service.AutoService; + +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidProviderFactory; +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.util.SystemConfig; + +@AutoService(PidProviderFactory.class) +public class FakeProviderFactory implements PidProviderFactory { + + @Override + public PidProvider createPidProvider(String providerId) { + String providerType = JvmSettings.PID_PROVIDER_TYPE.lookup(providerId); + if (!providerType.equals(FakeDOIProvider.TYPE)) { + // Being asked to create a non-EZId provider + return null; + } + String providerLabel = JvmSettings.PID_PROVIDER_LABEL.lookup(providerId); + String providerAuthority = JvmSettings.PID_PROVIDER_AUTHORITY.lookup(providerId); + String providerShoulder = JvmSettings.PID_PROVIDER_SHOULDER.lookupOptional(providerId).orElse(""); + String identifierGenerationStyle = JvmSettings.PID_PROVIDER_IDENTIFIER_GENERATION_STYLE + .lookupOptional(providerId).orElse("randomString"); + String datafilePidFormat = JvmSettings.PID_PROVIDER_DATAFILE_PID_FORMAT.lookupOptional(providerId) + .orElse(SystemConfig.DataFilePIDFormat.DEPENDENT.toString()); + String managedList = JvmSettings.PID_PROVIDER_MANAGED_LIST.lookupOptional(providerId).orElse("");; + String excludedList = JvmSettings.PID_PROVIDER_EXCLUDED_LIST.lookupOptional(providerId).orElse("");; + + return new FakeDOIProvider(providerId, providerLabel, providerAuthority, providerShoulder, identifierGenerationStyle, + datafilePidFormat, managedList, excludedList); + } + + public String getType() { + return FakeDOIProvider.TYPE; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/HandlenetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/handle/HandlePidProvider.java similarity index 84% rename from src/main/java/edu/harvard/iq/dataverse/HandlenetServiceBean.java rename to src/main/java/edu/harvard/iq/dataverse/pidproviders/handle/HandlePidProvider.java index 4942db9e7ec..9d61663d034 100644 --- a/src/main/java/edu/harvard/iq/dataverse/HandlenetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/handle/HandlePidProvider.java @@ -18,10 +18,12 @@ Version 3.0. */ -package edu.harvard.iq.dataverse; +package edu.harvard.iq.dataverse.pidproviders.handle; -import edu.harvard.iq.dataverse.settings.JvmSettings; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.GlobalId; +import edu.harvard.iq.dataverse.pidproviders.AbstractPidProvider; import java.io.File; import java.io.FileInputStream; @@ -29,8 +31,6 @@ import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; -import jakarta.ejb.EJB; -import jakarta.ejb.Stateless; import java.security.PrivateKey; /* Handlenet imports: */ @@ -60,23 +60,32 @@ * As of now, it only does the registration updates, to accommodate * the modifyRegistration datasets API sub-command. */ -@Stateless -public class HandlenetServiceBean extends AbstractGlobalIdServiceBean { - - @EJB - DataverseServiceBean dataverseService; - @EJB - SettingsServiceBean settingsService; - private static final Logger logger = Logger.getLogger(HandlenetServiceBean.class.getCanonicalName()); +public class HandlePidProvider extends AbstractPidProvider { + + private static final Logger logger = Logger.getLogger(HandlePidProvider.class.getCanonicalName()); public static final String HDL_PROTOCOL = "hdl"; - int handlenetIndex = JvmSettings.HANDLENET_INDEX.lookup(Integer.class); + public static final String TYPE = "hdl"; 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; + int handlenetIndex; + private boolean isIndependentHandleService; + private String authHandle; + private String keyPath; + private String keyPassphrase; + + public HandlePidProvider(String id, String label, String authority, String shoulder, String identifierGenerationStyle, + String datafilePidFormat, String managedList, String excludedList, int index, boolean isIndependentService, String authHandle, String path, String passphrase) { + super(id, label, HDL_PROTOCOL, authority, shoulder, identifierGenerationStyle, datafilePidFormat, managedList, excludedList); + this.handlenetIndex = index; + this.isIndependentHandleService = isIndependentService; + this.authHandle = authHandle; + this.keyPath = path; + this.keyPassphrase = passphrase; + } @Override @@ -111,21 +120,21 @@ public void reRegisterHandle(DvObject dvObject) { try { - AdminRecord admin = new AdminRecord(authHandle.getBytes("UTF8"), handlenetIndex, + AdminRecord admin = new AdminRecord(authHandle.getBytes(StandardCharsets.UTF_8), handlenetIndex, true, true, true, true, true, true, true, true, true, true, true, true); int timestamp = (int) (System.currentTimeMillis() / 1000); - HandleValue[] val = {new HandleValue(100, "HS_ADMIN".getBytes("UTF8"), + HandleValue[] val = {new HandleValue(100, "HS_ADMIN".getBytes(StandardCharsets.UTF_8), Encoder.encodeAdminRecord(admin), HandleValue.TTL_TYPE_RELATIVE, 86400, - timestamp, null, true, true, true, false), new HandleValue(1, "URL".getBytes("UTF8"), + timestamp, null, true, true, true, false), new HandleValue(1, "URL".getBytes(StandardCharsets.UTF_8), datasetUrl.getBytes(), HandleValue.TTL_TYPE_RELATIVE, 86400, timestamp, null, true, true, true, false)}; - ModifyValueRequest req = new ModifyValueRequest(handle.getBytes("UTF8"), val, auth); + ModifyValueRequest req = new ModifyValueRequest(handle.getBytes(StandardCharsets.UTF_8), val, auth); resolver.traceMessages = true; AbstractResponse response = resolver.processRequest(req); @@ -159,22 +168,22 @@ public Throwable registerNewHandle(DvObject dvObject) { try { - AdminRecord admin = new AdminRecord(authHandle.getBytes("UTF8"), handlenetIndex, + AdminRecord admin = new AdminRecord(authHandle.getBytes(StandardCharsets.UTF_8), handlenetIndex, true, true, true, true, true, true, true, true, true, true, true, true); int timestamp = (int) (System.currentTimeMillis() / 1000); - HandleValue[] val = {new HandleValue(100, "HS_ADMIN".getBytes("UTF8"), + HandleValue[] val = {new HandleValue(100, "HS_ADMIN".getBytes(StandardCharsets.UTF_8), Encoder.encodeAdminRecord(admin), HandleValue.TTL_TYPE_RELATIVE, 86400, - timestamp, null, true, true, true, false), new HandleValue(1, "URL".getBytes("UTF8"), + timestamp, null, true, true, true, false), new HandleValue(1, "URL".getBytes(StandardCharsets.UTF_8), datasetUrl.getBytes(), HandleValue.TTL_TYPE_RELATIVE, 86400, timestamp, null, true, true, true, false)}; CreateHandleRequest req - = new CreateHandleRequest(handle.getBytes("UTF8"), val, auth); + = new CreateHandleRequest(handle.getBytes(StandardCharsets.UTF_8), val, auth); resolver.traceMessages = true; AbstractResponse response = resolver.processRequest(req); @@ -231,10 +240,9 @@ private ResolutionRequest buildResolutionRequest(final String handle) { private PublicKeyAuthenticationInfo getAuthInfo(String handlePrefix) { logger.log(Level.FINE,"getAuthInfo"); byte[] key = null; - String adminCredFile = JvmSettings.HANDLENET_KEY_PATH.lookup(); - int handlenetIndex = JvmSettings.HANDLENET_INDEX.lookup(Integer.class); + String adminCredFile = getKeyPath(); - key = readKey(adminCredFile); + key = readKey(adminCredFile); PrivateKey privkey = null; privkey = readPrivKey(key, adminCredFile); String authHandle = getAuthenticationHandle(handlePrefix); @@ -244,8 +252,8 @@ private PublicKeyAuthenticationInfo getAuthInfo(String handlePrefix) { } private String getRegistrationUrl(DvObject dvObject) { logger.log(Level.FINE,"getRegistrationUrl"); - String siteUrl = systemConfig.getDataverseSiteUrl(); - String targetUrl = siteUrl + dvObject.getTargetUrl() + "hdl:" + dvObject.getAuthority() + String siteUrl = SystemConfig.getDataverseSiteUrlStatic(); + String targetUrl = siteUrl + dvObject.getTargetUrl() + "hdl:" + dvObject.getAuthority() + "/" + dvObject.getIdentifier(); return targetUrl; } @@ -278,8 +286,7 @@ private PrivateKey readPrivKey(byte[] key, final String file) { try { byte[] secKey = null; if ( Util.requiresSecretKey(key) ) { - String secret = JvmSettings.HANDLENET_KEY_PASSPHRASE.lookup(); - secKey = secret.getBytes(StandardCharsets.UTF_8); + secKey = getKeyPassphrase().getBytes(StandardCharsets.UTF_8); } key = Util.decrypt(key, secKey); privkey = Util.getPrivateKeyFromBytes(key, 0); @@ -304,9 +311,9 @@ private String getAuthenticationHandle(DvObject dvObject){ private String getAuthenticationHandle(String handlePrefix) { logger.log(Level.FINE,"getAuthenticationHandle"); - if (systemConfig.getHandleAuthHandle()!=null) { - return systemConfig.getHandleAuthHandle(); - } else if (systemConfig.isIndependentHandleService()) { + if (getHandleAuthHandle()!=null) { + return getHandleAuthHandle(); + } else if (isIndependentHandleService()) { return handlePrefix + "/ADMIN"; } else { return "0.NA/" + handlePrefix; @@ -348,9 +355,8 @@ public void deleteIdentifier(DvObject dvObject) throws Exception { String handle = getDvObjectHandle(dvObject); String authHandle = getAuthenticationHandle(dvObject); - String adminCredFile = JvmSettings.HANDLENET_KEY_PATH.lookup(); - int handlenetIndex = JvmSettings.HANDLENET_INDEX.lookup(Integer.class); - + String adminCredFile = getKeyPath(); + byte[] key = readKey(adminCredFile); PrivateKey privkey = readPrivKey(key, adminCredFile); @@ -383,7 +389,7 @@ private boolean updateIdentifierStatus(DvObject dvObject, String statusIn) { @Override public List getProviderInformation(){ - return List.of("Handle", "https://hdl.handle.net"); + return List.of(getId(), HDL_RESOLVER_URL); } @@ -401,7 +407,7 @@ public String createIdentifier(DvObject dvObject) throws Throwable { @Override public boolean publicizeIdentifier(DvObject dvObject) { if (dvObject.getIdentifier() == null || dvObject.getIdentifier().isEmpty()){ - generateIdentifier(dvObject); + generatePid(dvObject); } return updateIdentifierStatus(dvObject, "public"); @@ -438,6 +444,32 @@ public GlobalId parsePersistentId(String protocol, String authority, String iden public String getUrlPrefix() { return HDL_RESOLVER_URL; } + + @Override + public String getProtocol() { + return HDL_PROTOCOL; + } + + @Override + public String getProviderType() { + return TYPE; + } + + public String getKeyPath() { + return keyPath; + } + + public String getKeyPassphrase() { + return keyPassphrase; + } + + public boolean isIndependentHandleService() { + return isIndependentHandleService; + } + + public String getHandleAuthHandle() { + return authHandle; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/handle/HandleProviderFactory.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/handle/HandleProviderFactory.java new file mode 100644 index 00000000000..eef5bed8432 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/handle/HandleProviderFactory.java @@ -0,0 +1,45 @@ +package edu.harvard.iq.dataverse.pidproviders.handle; + +import com.google.auto.service.AutoService; + +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidProviderFactory; +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.util.SystemConfig; + +@AutoService(PidProviderFactory.class) +public class HandleProviderFactory implements PidProviderFactory { + + @Override + public PidProvider createPidProvider(String providerId) { + String providerType = JvmSettings.PID_PROVIDER_TYPE.lookup(providerId); + if (!providerType.equals(HandlePidProvider.TYPE)) { + // Being asked to create a non-EZId provider + return null; + } + String providerLabel = JvmSettings.PID_PROVIDER_LABEL.lookup(providerId); + String providerAuthority = JvmSettings.PID_PROVIDER_AUTHORITY.lookup(providerId); + String providerShoulder = JvmSettings.PID_PROVIDER_SHOULDER.lookupOptional(providerId).orElse(""); + String identifierGenerationStyle = JvmSettings.PID_PROVIDER_IDENTIFIER_GENERATION_STYLE + .lookupOptional(providerId).orElse("randomString"); + String datafilePidFormat = JvmSettings.PID_PROVIDER_DATAFILE_PID_FORMAT.lookupOptional(providerId) + .orElse(SystemConfig.DataFilePIDFormat.DEPENDENT.toString()); + String managedList = JvmSettings.PID_PROVIDER_MANAGED_LIST.lookupOptional(providerId).orElse(""); + String excludedList = JvmSettings.PID_PROVIDER_EXCLUDED_LIST.lookupOptional(providerId).orElse(""); + + int index = JvmSettings.HANDLENET_INDEX.lookupOptional(Integer.class, providerId).orElse(300); + boolean independentHandleService = JvmSettings.HANDLENET_INDEPENDENT_SERVICE + .lookupOptional(Boolean.class, providerId).orElse(false); + String handleAuthHandle = JvmSettings.HANDLENET_AUTH_HANDLE.lookup(providerId); + String path = JvmSettings.HANDLENET_KEY_PATH.lookup(providerId); + String passphrase = JvmSettings.HANDLENET_KEY_PASSPHRASE.lookup(providerId); + return new HandlePidProvider(providerId, providerLabel, providerAuthority, providerShoulder, identifierGenerationStyle, + datafilePidFormat, managedList, excludedList, index, independentHandleService, handleAuthHandle, path, + passphrase); + } + + public String getType() { + return HandlePidProvider.TYPE; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/UnmanagedHandlenetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/handle/UnmanagedHandlePidProvider.java similarity index 61% rename from src/main/java/edu/harvard/iq/dataverse/pidproviders/UnmanagedHandlenetServiceBean.java rename to src/main/java/edu/harvard/iq/dataverse/pidproviders/handle/UnmanagedHandlePidProvider.java index c856c5363e0..075e10d8164 100644 --- a/src/main/java/edu/harvard/iq/dataverse/pidproviders/UnmanagedHandlenetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/handle/UnmanagedHandlePidProvider.java @@ -1,27 +1,32 @@ -package edu.harvard.iq.dataverse.pidproviders; +package edu.harvard.iq.dataverse.pidproviders.handle; -import edu.harvard.iq.dataverse.AbstractGlobalIdServiceBean; import edu.harvard.iq.dataverse.DvObject; import edu.harvard.iq.dataverse.GlobalId; -import edu.harvard.iq.dataverse.HandlenetServiceBean; +import edu.harvard.iq.dataverse.pidproviders.AbstractPidProvider; + import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import jakarta.ejb.Stateless; import org.apache.commons.lang3.NotImplementedException; -/** This class is just used to parse Handles that are not managed by any account configured in Dataverse - * It does not implement any of the methods related to PID CRUD +/** + * This class is just used to parse Handles that are not managed by any account + * configured in Dataverse It does not implement any of the methods related to + * PID CRUD * */ -@Stateless -public class UnmanagedHandlenetServiceBean extends AbstractGlobalIdServiceBean { - private static final Logger logger = Logger.getLogger(UnmanagedHandlenetServiceBean.class.getCanonicalName()); +public class UnmanagedHandlePidProvider extends AbstractPidProvider { + + private static final Logger logger = Logger.getLogger(UnmanagedHandlePidProvider.class.getCanonicalName()); + public static final String ID = "UnmanagedHandleProvider"; - public UnmanagedHandlenetServiceBean() { + public UnmanagedHandlePidProvider() { + // Also using ID as label + super(ID, ID, HandlePidProvider.HDL_PROTOCOL); logger.log(Level.FINE, "Constructor"); - configured = true; + } @Override @@ -56,7 +61,7 @@ public void deleteIdentifier(DvObject dvObject) throws Exception { @Override public List getProviderInformation() { - return List.of("UnmanagedHandle", ""); + return List.of(getId(), ""); } @Override @@ -71,19 +76,18 @@ public boolean publicizeIdentifier(DvObject dvObject) { @Override public GlobalId parsePersistentId(String pidString) { - if (pidString.startsWith(HandlenetServiceBean.HDL_RESOLVER_URL)) { - pidString = pidString.replace(HandlenetServiceBean.HDL_RESOLVER_URL, - (HandlenetServiceBean.HDL_PROTOCOL + ":")); - } else if (pidString.startsWith(HandlenetServiceBean.HTTP_HDL_RESOLVER_URL)) { - pidString = pidString.replace(HandlenetServiceBean.HTTP_HDL_RESOLVER_URL, - (HandlenetServiceBean.HDL_PROTOCOL + ":")); + if (pidString.startsWith(HandlePidProvider.HDL_RESOLVER_URL)) { + pidString = pidString.replace(HandlePidProvider.HDL_RESOLVER_URL, (HandlePidProvider.HDL_PROTOCOL + ":")); + } else if (pidString.startsWith(HandlePidProvider.HTTP_HDL_RESOLVER_URL)) { + pidString = pidString.replace(HandlePidProvider.HTTP_HDL_RESOLVER_URL, + (HandlePidProvider.HDL_PROTOCOL + ":")); } return super.parsePersistentId(pidString); } @Override public GlobalId parsePersistentId(String protocol, String identifierString) { - if (!HandlenetServiceBean.HDL_PROTOCOL.equals(protocol)) { + if (!HandlePidProvider.HDL_PROTOCOL.equals(protocol)) { return null; } GlobalId globalId = super.parsePersistentId(protocol, identifierString); @@ -92,7 +96,7 @@ public GlobalId parsePersistentId(String protocol, String identifierString) { @Override public GlobalId parsePersistentId(String protocol, String authority, String identifier) { - if (!HandlenetServiceBean.HDL_PROTOCOL.equals(protocol)) { + if (!HandlePidProvider.HDL_PROTOCOL.equals(protocol)) { return null; } return super.parsePersistentId(protocol, authority, identifier); @@ -100,6 +104,11 @@ public GlobalId parsePersistentId(String protocol, String authority, String iden @Override public String getUrlPrefix() { - return HandlenetServiceBean.HDL_RESOLVER_URL; + return HandlePidProvider.HDL_RESOLVER_URL; + } + + @Override + public String getProviderType() { + return "unamagedHandle"; } } diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/perma/PermaLinkPidProvider.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/perma/PermaLinkPidProvider.java new file mode 100644 index 00000000000..7b55292350f --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/perma/PermaLinkPidProvider.java @@ -0,0 +1,201 @@ +package edu.harvard.iq.dataverse.pidproviders.perma; + +import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.GlobalId; +import edu.harvard.iq.dataverse.pidproviders.AbstractPidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.util.SystemConfig; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +/** + * PermaLink provider This is a minimalist permanent ID provider intended for + * use with 'real' datasets/files where the use case none-the-less doesn't lend + * itself to the use of DOIs or Handles, e.g. * due to cost * for a + * catalog/archive where Dataverse has a dataset representing a dataset with + * DOI/handle stored elsewhere + * + * The initial implementation will mint identifiers locally and will provide the + * existing page URLs (using the ?persistentID= format). This will be + * overridable by a configurable parameter to support use of an external + * resolver. + * + */ +public class PermaLinkPidProvider extends AbstractPidProvider { + + private static final Logger logger = Logger.getLogger(PermaLinkPidProvider.class.getCanonicalName()); + + public static final String PERMA_PROTOCOL = "perma"; + public static final String TYPE = "perma"; + public static final String SEPARATOR = ""; + + // ToDo - remove + @Deprecated + public static final String PERMA_RESOLVER_URL = JvmSettings.PERMALINK_BASE_URL.lookupOptional("permalink") + .orElse(SystemConfig.getDataverseSiteUrlStatic()); + + + private String separator = SEPARATOR; + + private String baseUrl; + + public PermaLinkPidProvider(String id, String label, String providerAuthority, String providerShoulder, String identifierGenerationStyle, + String datafilePidFormat, String managedList, String excludedList, String baseUrl, String separator) { + super(id, label, PERMA_PROTOCOL, providerAuthority, providerShoulder, identifierGenerationStyle, datafilePidFormat, + managedList, excludedList); + this.baseUrl = baseUrl; + this.separator = separator; + } + + @Override + public String getSeparator() { + return separator; + } + + @Override + public boolean alreadyRegistered(GlobalId globalId, boolean noProviderDefault) { + // Perma doesn't manage registration, so we assume all local PIDs can be treated + // as registered + boolean existsLocally = !pidProviderService.isGlobalIdLocallyUnique(globalId); + return existsLocally ? existsLocally : noProviderDefault; + } + + @Override + public boolean registerWhenPublished() { + return false; + } + + @Override + public List getProviderInformation() { + return List.of(getId(), getBaseUrl()); + } + + @Override + public String createIdentifier(DvObject dvo) throws Throwable { + // Call external resolver and send landing URL? + // FWIW: Return value appears to only be used in RegisterDvObjectCommand where + // success requires finding the dvo identifier in this string. (Also logged a + // couple places). + return (dvo.getGlobalId().asString()); + } + + @Override + public Map getIdentifierMetadata(DvObject dvo) { + Map map = new HashMap<>(); + return map; + } + + @Override + public String modifyIdentifierTargetURL(DvObject dvo) throws Exception { + return getTargetUrl(dvo); + } + + @Override + public void deleteIdentifier(DvObject dvo) throws Exception { + // no-op + } + + @Override + public boolean publicizeIdentifier(DvObject dvObject) { + // Generate if needed (i.e. datafile case where we don't create/register early + // (even with registerWhenPublished == false)) + if (dvObject.getIdentifier() == null || dvObject.getIdentifier().isEmpty()) { + dvObject = generatePid(dvObject); + } + // Call external resolver and send landing URL? + return true; + } + + @Override + public GlobalId parsePersistentId(String pidString) { + // ToDo - handle local PID resolver for dataset/file + logger.info("Parsing in Perma: " + pidString); + if (pidString.startsWith(getUrlPrefix())) { + pidString = pidString.replace(getUrlPrefix(), (PERMA_PROTOCOL + ":")); + } + return super.parsePersistentId(pidString); + } + + @Override + public GlobalId parsePersistentId(String protocol, String identifierString) { + logger.info("Checking Perma: " + identifierString); + if (!PERMA_PROTOCOL.equals(protocol)) { + return null; + } + String cleanIdentifier = PERMA_PROTOCOL + ":" + identifierString; + // With permalinks, we have to check the sets before parsing since the permalinks in these sets could have different authority, spearator, and shoulders + if (getExcludedSet().contains(cleanIdentifier)) { + return null; + } + if(getManagedSet().contains(cleanIdentifier)) { + /** With a variable separator that could also be empty, there is no way to determine the authority and shoulder for an unknown permalink. + * Since knowing this split isn't relevant for permalinks except for minting, the code below just assumes the authority + * is the first 4 characters and that the separator and the shoulder are empty. + * If this is found to cause issues, users should be able to use a managed permalink provider as a work-around. The code here could + * be changed to allow default lengths for the authority, separator, and shoulder and/or to add a list of known (but unmanaged) authority, separator, shoulder combos. + */ + if(identifierString.length() < 4) { + return new GlobalId(protocol, "", identifierString, SEPARATOR, getUrlPrefix(), + getId()); + } + return new GlobalId(protocol, identifierString.substring(0,4), identifierString.substring(4), SEPARATOR, getUrlPrefix(), + getId()); + } + String identifier = null; + if (getAuthority() != null) { + if (identifierString.startsWith(getAuthority())) { + identifier = identifierString.substring(getAuthority().length()); + } else { + //Doesn't match authority + return null; + } + if (identifier.startsWith(separator)) { + identifier = identifier.substring(separator.length()); + } else { + //Doesn't match separator + return null; + } + } + identifier = PidProvider.formatIdentifierString(identifier); + if (PidProvider.testforNullTerminator(identifier)) { + return null; + } + if(!identifier.startsWith(getShoulder())) { + //Doesn't match shoulder + return null; + } + return new GlobalId(PERMA_PROTOCOL, getAuthority(), identifier, separator, getUrlPrefix(), getId()); + } + + @Override + public GlobalId parsePersistentId(String protocol, String authority, String identifier) { + if (!PERMA_PROTOCOL.equals(protocol)) { + return null; + } + return super.parsePersistentId(protocol, authority, identifier); + } + + @Override + public String getUrlPrefix() { + + return getBaseUrl(); + } + + @Override + public String getProtocol() { + return PERMA_PROTOCOL; + } + + @Override + public String getProviderType() { + return PERMA_PROTOCOL; + } + + public String getBaseUrl() { + return baseUrl; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/perma/PermaLinkProviderFactory.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/perma/PermaLinkProviderFactory.java new file mode 100644 index 00000000000..ca75fbff3a8 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/perma/PermaLinkProviderFactory.java @@ -0,0 +1,43 @@ +package edu.harvard.iq.dataverse.pidproviders.perma; + +import com.google.auto.service.AutoService; + +import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.PidProviderFactory; +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.util.SystemConfig; + +@AutoService(PidProviderFactory.class) +public class PermaLinkProviderFactory implements PidProviderFactory { + + @Override + public PidProvider createPidProvider(String providerId) { + String providerType = JvmSettings.PID_PROVIDER_TYPE.lookup(providerId); + if (!providerType.equals(PermaLinkPidProvider.TYPE)) { + // Being asked to create a non-EZId provider + return null; + } + String providerLabel = JvmSettings.PID_PROVIDER_LABEL.lookup(providerId); + String providerAuthority = JvmSettings.PID_PROVIDER_AUTHORITY.lookup(providerId); + String providerShoulder = JvmSettings.PID_PROVIDER_SHOULDER.lookupOptional(providerId).orElse(""); + String identifierGenerationStyle = JvmSettings.PID_PROVIDER_IDENTIFIER_GENERATION_STYLE + .lookupOptional(providerId).orElse("randomString"); + String datafilePidFormat = JvmSettings.PID_PROVIDER_DATAFILE_PID_FORMAT.lookupOptional(providerId) + .orElse(SystemConfig.DataFilePIDFormat.DEPENDENT.toString()); + String managedList = JvmSettings.PID_PROVIDER_MANAGED_LIST.lookupOptional(providerId).orElse(""); + String excludedList = JvmSettings.PID_PROVIDER_EXCLUDED_LIST.lookupOptional(providerId).orElse(""); + + String baseUrl = JvmSettings.PERMALINK_BASE_URL.lookupOptional(providerId) + .orElse(SystemConfig.getDataverseSiteUrlStatic() + "/citation?persistentId=" + PermaLinkPidProvider.PERMA_PROTOCOL + ":"); + ; + String separator = JvmSettings.PERMALINK_SEPARATOR.lookupOptional(providerId).orElse(""); + + return new PermaLinkPidProvider(providerId, providerLabel, providerAuthority, providerShoulder, identifierGenerationStyle, + datafilePidFormat, managedList, excludedList, baseUrl, separator); + } + + public String getType() { + return PermaLinkPidProvider.TYPE; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/perma/UnmanagedPermaLinkPidProvider.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/perma/UnmanagedPermaLinkPidProvider.java new file mode 100644 index 00000000000..b7961a41c50 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/perma/UnmanagedPermaLinkPidProvider.java @@ -0,0 +1,114 @@ +package edu.harvard.iq.dataverse.pidproviders.perma; + +import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.GlobalId; +import edu.harvard.iq.dataverse.pidproviders.AbstractPidProvider; +import edu.harvard.iq.dataverse.util.SystemConfig; + +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; +import jakarta.ejb.Stateless; +import org.apache.commons.lang3.NotImplementedException; + +/** This class is just used to parse Handles that are not managed by any account configured in Dataverse + * It does not implement any of the methods related to PID CRUD + * + */ +public class UnmanagedPermaLinkPidProvider extends AbstractPidProvider { + + private static final Logger logger = Logger.getLogger(UnmanagedPermaLinkPidProvider.class.getCanonicalName()); + public static final String ID = "UnmanagedPermaLinkProvider"; + + public UnmanagedPermaLinkPidProvider() { + // Also using ID as label + super(ID, ID, PermaLinkPidProvider.PERMA_PROTOCOL); + logger.log(Level.FINE, "Constructor"); + } + + @Override + public boolean canManagePID() { + return false; + } + + @Override + public boolean registerWhenPublished() { + throw new NotImplementedException(); + } + + @Override + public boolean alreadyRegistered(GlobalId pid, boolean noProviderDefault) throws Exception { + throw new NotImplementedException(); + } + + @Override + public Map getIdentifierMetadata(DvObject dvObject) { + throw new NotImplementedException(); + } + + @Override + public String modifyIdentifierTargetURL(DvObject dvObject) throws Exception { + throw new NotImplementedException(); + } + + @Override + public void deleteIdentifier(DvObject dvObject) throws Exception { + throw new NotImplementedException(); + } + + @Override + public List getProviderInformation() { + return List.of(getId(), ""); + } + + @Override + public String createIdentifier(DvObject dvObject) throws Throwable { + throw new NotImplementedException(); + } + + @Override + public boolean publicizeIdentifier(DvObject dvObject) { + throw new NotImplementedException(); + } + + @Override + public GlobalId parsePersistentId(String protocol, String identifierString) { + if (!PermaLinkPidProvider.PERMA_PROTOCOL.equals(protocol)) { + return null; + } + /** With a variable separator that could also be empty, there is no way to determine the authority and shoulder for an unknown/unmanaged permalink. + * Since knowing this split isn't relevant for unmanaged permalinks, the code below just assumes the authority + * is the first 4 characters and that the separator and the shoulder are empty. + * If this is found to cause issues, users should be able to use a managed permalink provider as a work-around. The code here could + * be changed to allow default lengths for the authority, separator, and shoulder and/or to add a list of known (but unmanaged) authority, separator, shoulder combos. + */ + if(identifierString.length() < 4) { + logger.warning("A short unmanaged permalink was found - assuming the authority is empty: " + identifierString); + return super.parsePersistentId(protocol, "", identifierString); + } + return super.parsePersistentId(protocol, identifierString.substring(0, 4), identifierString.substring(4)); + } + + @Override + public GlobalId parsePersistentId(String protocol, String authority, String identifier) { + if (!PermaLinkPidProvider.PERMA_PROTOCOL.equals(protocol)) { + return null; + } + return super.parsePersistentId(protocol, authority, identifier); + } + + @Override + public String getUrlPrefix() { + return SystemConfig.getDataverseSiteUrlStatic()+ "/citation?persistentId=" + PermaLinkPidProvider.PERMA_PROTOCOL + ":"; + } + + @Override + public String getProviderType() { + return PermaLinkPidProvider.TYPE; + } + + @Override + public String getSeparator() { + return PermaLinkPidProvider.SEPARATOR; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/privateurl/PrivateUrlServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/privateurl/PrivateUrlServiceBean.java index 9e5879106e4..01710e06f8f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/privateurl/PrivateUrlServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/privateurl/PrivateUrlServiceBean.java @@ -96,7 +96,7 @@ private RoleAssignment getRoleAssignmentFromPrivateUrlToken(String privateUrlTok * * @todo This might be a good place for Optional. */ - private RoleAssignment getPrivateUrlRoleAssignmentFromDataset(Dataset dataset) { + public RoleAssignment getPrivateUrlRoleAssignmentFromDataset(Dataset dataset) { if (dataset == null) { return null; } diff --git a/src/main/java/edu/harvard/iq/dataverse/provenance/ProvPopupFragmentBean.java b/src/main/java/edu/harvard/iq/dataverse/provenance/ProvPopupFragmentBean.java index 6e8a512902a..a8b28d2d79d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/provenance/ProvPopupFragmentBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/provenance/ProvPopupFragmentBean.java @@ -21,6 +21,7 @@ import static edu.harvard.iq.dataverse.util.JsfHelper.JH; import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.logging.Level; @@ -499,7 +500,7 @@ public void showJsonPreviewNewWindow() throws IOException, WrappedResponse { OutputStream output = ec.getResponseOutputStream(); - OutputStreamWriter osw = new OutputStreamWriter(output, "UTF-8"); + OutputStreamWriter osw = new OutputStreamWriter(output, StandardCharsets.UTF_8); osw.write(provJsonState); //the button calling this will only be rendered if provJsonState exists (e.g. a file is uploaded) osw.close(); fc.responseComplete(); diff --git a/src/main/java/edu/harvard/iq/dataverse/repositorystorageabstractionlayer/RepositoryStorageAbstractionLayerPage.java b/src/main/java/edu/harvard/iq/dataverse/repositorystorageabstractionlayer/RepositoryStorageAbstractionLayerPage.java index c252d2e3330..9edb536eda2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/repositorystorageabstractionlayer/RepositoryStorageAbstractionLayerPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/repositorystorageabstractionlayer/RepositoryStorageAbstractionLayerPage.java @@ -11,6 +11,7 @@ import jakarta.inject.Named; import jakarta.json.JsonArray; +@Deprecated(forRemoval = true, since = "2024-07-07") @Stateless @Named public class RepositoryStorageAbstractionLayerPage { @@ -22,17 +23,20 @@ public class RepositoryStorageAbstractionLayerPage { @EJB StorageSiteServiceBean storageSiteServiceBean; + @Deprecated(forRemoval = true, since = "2024-07-07") public String getLocalDataAccessDirectory(DatasetVersion datasetVersion) { String localDataAccessParentDir = settingsService.getValueForKey(SettingsServiceBean.Key.LocalDataAccessPath); return RepositoryStorageAbstractionLayerUtil.getLocalDataAccessDirectory(localDataAccessParentDir, datasetVersion.getDataset()); } + @Deprecated(forRemoval = true, since = "2024-07-07") public List getRsyncSites(DatasetVersion datasetVersion) { List storageSites = storageSiteServiceBean.findAll(); JsonArray storageSitesAsJson = RepositoryStorageAbstractionLayerUtil.getStorageSitesAsJson(storageSites); return RepositoryStorageAbstractionLayerUtil.getRsyncSites(datasetVersion.getDataset(), storageSitesAsJson); } + @Deprecated(forRemoval = true, since = "2024-07-07") public String getVerifyDataCommand(DatasetVersion datasetVersion) { return RepositoryStorageAbstractionLayerUtil.getVerifyDataCommand(datasetVersion.getDataset()); } diff --git a/src/main/java/edu/harvard/iq/dataverse/repositorystorageabstractionlayer/RepositoryStorageAbstractionLayerUtil.java b/src/main/java/edu/harvard/iq/dataverse/repositorystorageabstractionlayer/RepositoryStorageAbstractionLayerUtil.java index 8501fba3ce0..0d547402676 100644 --- a/src/main/java/edu/harvard/iq/dataverse/repositorystorageabstractionlayer/RepositoryStorageAbstractionLayerUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/repositorystorageabstractionlayer/RepositoryStorageAbstractionLayerUtil.java @@ -13,10 +13,12 @@ import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonObject; +@Deprecated(forRemoval = true, since = "2024-07-07") public class RepositoryStorageAbstractionLayerUtil { private static final Logger logger = Logger.getLogger(RepositoryStorageAbstractionLayerUtil.class.getCanonicalName()); + @Deprecated(forRemoval = true, since = "2024-07-07") public static List getRsyncSites(Dataset dataset, JsonArray rsalSitesAsJson) { List rsalSites = new ArrayList<>(); boolean leafDirectoryOnly = false; @@ -30,6 +32,7 @@ public static List getRsyncSites(Dataset dataset, JsonArray rsalSites return rsalSites; } + @Deprecated(forRemoval = true, since = "2024-07-07") static String getLocalDataAccessDirectory(String localDataAccessParentDir, Dataset dataset) { if (localDataAccessParentDir == null) { localDataAccessParentDir = File.separator + "UNCONFIGURED ( " + SettingsServiceBean.Key.LocalDataAccessPath + " )"; @@ -38,6 +41,7 @@ static String getLocalDataAccessDirectory(String localDataAccessParentDir, Datas return localDataAccessParentDir + File.separator + getDirectoryContainingTheData(dataset, leafDirectoryOnly); } + @Deprecated(forRemoval = true, since = "2024-07-07") static String getVerifyDataCommand(Dataset dataset) { boolean leafDirectoryOnly = true; // TODO: if "files.sha" is defined somewhere, use it. @@ -51,6 +55,7 @@ static String getVerifyDataCommand(Dataset dataset) { * leafDirectoryOnly. See also * http://www.gnu.org/software/coreutils/manual/html_node/basename-invocation.html */ + @Deprecated(forRemoval = true, since = "2024-07-07") public static String getDirectoryContainingTheData(Dataset dataset, boolean leafDirectoryOnly) { /** * FIXME: What if there is more than one package in the dataset? @@ -81,6 +86,7 @@ public static String getDirectoryContainingTheData(Dataset dataset, boolean leaf * RSAL or some other "big data" component live for a list of remotes sites * to which a particular dataset is replicated to. */ + @Deprecated(forRemoval = true, since = "2024-07-07") static JsonArray getStorageSitesAsJson(List storageSites) { JsonArrayBuilder arraybuilder = Json.createArrayBuilder(); if (storageSites == null || storageSites.isEmpty()) { diff --git a/src/main/java/edu/harvard/iq/dataverse/rserve/RemoteDataFrameService.java b/src/main/java/edu/harvard/iq/dataverse/rserve/RemoteDataFrameService.java index df2e44ecb27..dbcfc039fa1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/rserve/RemoteDataFrameService.java +++ b/src/main/java/edu/harvard/iq/dataverse/rserve/RemoteDataFrameService.java @@ -33,6 +33,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -758,7 +759,7 @@ private static String readLocalResource(String path) { // Try opening a buffered reader stream try { - resourceAsString = IOUtils.toString(resourceStream, "UTF-8"); + resourceAsString = IOUtils.toString(resourceStream, StandardCharsets.UTF_8); resourceStream.close(); } catch (IOException ex) { logger.warning(String.format("RDATAFileReader: (readLocalResource) resource stream from path \"%s\" was invalid", path)); diff --git a/src/main/java/edu/harvard/iq/dataverse/search/IndexBatchServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/IndexBatchServiceBean.java index 0eeb681514c..3f7a7bb3363 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/IndexBatchServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/IndexBatchServiceBean.java @@ -50,35 +50,31 @@ public class IndexBatchServiceBean { public Future indexStatus() { JsonObjectBuilder response = Json.createObjectBuilder(); logger.info("Beginning indexStatus()"); - JsonObject contentInDatabaseButStaleInOrMissingFromSolr = getContentInDatabaseButStaleInOrMissingFromSolr().build(); - JsonObject contentInSolrButNotDatabase = null; - JsonObject permissionsInSolrButNotDatabase = null; try { - contentInSolrButNotDatabase = getContentInSolrButNotDatabase().build(); - permissionsInSolrButNotDatabase = getPermissionsInSolrButNotDatabase().build(); - - } catch (SearchException ex) { + JsonObject contentInDatabaseButStaleInOrMissingFromSolr = getContentInDatabaseButStaleInOrMissingFromSolr().build(); + JsonObject contentInSolrButNotDatabase = getContentInSolrButNotDatabase().build(); + JsonObject permissionsInSolrButNotDatabase = getPermissionsInSolrButNotDatabase().build(); + JsonObject permissionsInDatabaseButStaleInOrMissingFromSolr = getPermissionsInDatabaseButStaleInOrMissingFromSolr().build(); + + response + .add("contentInDatabaseButStaleInOrMissingFromIndex", contentInDatabaseButStaleInOrMissingFromSolr) + .add("contentInIndexButNotDatabase", contentInSolrButNotDatabase) + .add("permissionsInDatabaseButStaleInOrMissingFromIndex", permissionsInDatabaseButStaleInOrMissingFromSolr) + .add("permissionsInIndexButNotDatabase", permissionsInSolrButNotDatabase); + + logger.log(Level.INFO, "contentInDatabaseButStaleInOrMissingFromIndex: {0}", contentInDatabaseButStaleInOrMissingFromSolr); + logger.log(Level.INFO, "contentInIndexButNotDatabase: {0}", contentInSolrButNotDatabase); + logger.log(Level.INFO, "permissionsInDatabaseButStaleInOrMissingFromIndex: {0}", permissionsInDatabaseButStaleInOrMissingFromSolr); + logger.log(Level.INFO, "permissionsInIndexButNotDatabase: {0}", permissionsInSolrButNotDatabase); + } catch (Exception ex) { String msg = "Can not determine index status. " + ex.getLocalizedMessage() + ". Is Solr down? Exception: " + ex.getCause().getLocalizedMessage(); logger.info(msg); + ex.printStackTrace(); response.add("SearchException ", msg); - return new AsyncResult<>(response); } - - JsonObject permissionsInDatabaseButStaleInOrMissingFromSolr = getPermissionsInDatabaseButStaleInOrMissingFromSolr().build(); - - JsonObjectBuilder data = Json.createObjectBuilder() - .add("contentInDatabaseButStaleInOrMissingFromIndex", contentInDatabaseButStaleInOrMissingFromSolr) - .add("contentInIndexButNotDatabase", contentInSolrButNotDatabase) - .add("permissionsInDatabaseButStaleInOrMissingFromIndex", permissionsInDatabaseButStaleInOrMissingFromSolr) - .add("permissionsInIndexButNotDatabase", permissionsInSolrButNotDatabase); - - logger.log(Level.INFO, "contentInDatabaseButStaleInOrMissingFromIndex: {0}", contentInDatabaseButStaleInOrMissingFromSolr); - logger.log(Level.INFO, "contentInIndexButNotDatabase: {0}", contentInSolrButNotDatabase); - logger.log(Level.INFO, "permissionsInDatabaseButStaleInOrMissingFromIndex: {0}", permissionsInDatabaseButStaleInOrMissingFromSolr); - logger.log(Level.INFO, "permissionsInIndexButNotDatabase: {0}", permissionsInSolrButNotDatabase); - - return new AsyncResult<>(data); + return new AsyncResult<>(response); } + @Asynchronous public Future clearOrphans() { JsonObjectBuilder response = Json.createObjectBuilder(); diff --git a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java index d631e2829c1..a8cf9ed519b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java @@ -1,20 +1,24 @@ package edu.harvard.iq.dataverse.search; import edu.harvard.iq.dataverse.*; +import edu.harvard.iq.dataverse.DatasetVersion.VersionState; +import edu.harvard.iq.dataverse.DvObject.DType; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUserServiceBean; import edu.harvard.iq.dataverse.batch.util.LoggingUtil; import edu.harvard.iq.dataverse.dataaccess.DataAccess; import edu.harvard.iq.dataverse.dataaccess.DataAccessRequest; import edu.harvard.iq.dataverse.dataaccess.StorageIO; +import edu.harvard.iq.dataverse.dataset.DatasetType; import edu.harvard.iq.dataverse.datavariable.DataVariable; import edu.harvard.iq.dataverse.datavariable.VariableMetadata; import edu.harvard.iq.dataverse.datavariable.VariableMetadataUtil; import edu.harvard.iq.dataverse.datavariable.VariableServiceBean; import edu.harvard.iq.dataverse.harvest.client.HarvestingClient; +import edu.harvard.iq.dataverse.search.IndexableDataset.DatasetState; +import edu.harvard.iq.dataverse.settings.FeatureFlags; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; -import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.StringUtil; import edu.harvard.iq.dataverse.util.SystemConfig; @@ -36,7 +40,9 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; +import java.util.concurrent.Semaphore; import java.util.function.Function; +import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import jakarta.annotation.PostConstruct; @@ -48,6 +54,8 @@ import jakarta.ejb.Stateless; import jakarta.ejb.TransactionAttribute; import static jakarta.ejb.TransactionAttributeType.REQUIRES_NEW; + +import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.json.JsonObject; import jakarta.persistence.EntityManager; @@ -72,6 +80,9 @@ import org.apache.tika.sax.BodyContentHandler; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.metrics.MetricUnits; +import org.eclipse.microprofile.metrics.Timer; +import org.eclipse.microprofile.metrics.annotation.Metric; import org.xml.sax.ContentHandler; @Stateless @@ -91,6 +102,8 @@ public class IndexServiceBean { @EJB DatasetServiceBean datasetService; @EJB + DatasetVersionServiceBean datasetVersionService; + @EJB BuiltinUserServiceBean dataverseUserServiceBean; @EJB PermissionServiceBean permissionService; @@ -209,6 +222,9 @@ public Future indexDataverse(Dataverse dataverse, boolean processPaths) solrInputDocument.addField(SearchFields.DATAVERSE_CATEGORY, dataverse.getIndexableCategoryName()); if (dataverse.isReleased()) { solrInputDocument.addField(SearchFields.PUBLICATION_STATUS, PUBLISHED_STRING); + if (FeatureFlags.ADD_PUBLICOBJECT_SOLR_FIELD.enabled()) { + solrInputDocument.addField(SearchFields.PUBLIC_OBJECT, true); + } solrInputDocument.addField(SearchFields.RELEASE_OR_CREATE_DATE, dataverse.getPublicationDate()); } else { solrInputDocument.addField(SearchFields.PUBLICATION_STATUS, UNPUBLISHED_STRING); @@ -263,6 +279,7 @@ public Future indexDataverse(Dataverse dataverse, boolean processPaths) if (dataverse.getOwner() != null) { solrInputDocument.addField(SearchFields.PARENT_ID, dataverse.getOwner().getId()); solrInputDocument.addField(SearchFields.PARENT_NAME, dataverse.getOwner().getName()); + solrInputDocument.addField(SearchFields.DATAVERSE_PARENT_ALIAS, dataverse.getOwner().getAlias()); } } List dataversePathSegmentsAccumulator = new ArrayList<>(); @@ -312,14 +329,6 @@ public Future indexDataverse(Dataverse dataverse, boolean processPaths) logger.info(status); return new AsyncResult<>(status); } - try { - solrClientService.getSolrClient().commit(); - } catch (SolrServerException | IOException ex) { - status = ex.toString(); - logger.info(status); - return new AsyncResult<>(status); - } - dvObjectService.updateContentIndexTime(dataverse); IndexResponse indexResponse = solrIndexService.indexPermissionsForOneDvObject(dataverse); String msg = "indexed dataverse " + dataverse.getId() + ":" + dataverse.getAlias() + ". Response from permission indexing: " + indexResponse.getMessage(); @@ -342,6 +351,29 @@ public void indexDatasetInNewTransaction(Long datasetId) { //Dataset dataset) { private static final Map NEXT_TO_INDEX = new ConcurrentHashMap<>(); // indexingNow is a set of dataset ids of datasets being indexed asynchronously right now private static final Map INDEXING_NOW = new ConcurrentHashMap<>(); + // semaphore for async indexing + private static final Semaphore ASYNC_INDEX_SEMAPHORE = new Semaphore(JvmSettings.MAX_ASYNC_INDEXES.lookupOptional(Integer.class).orElse(4), true); + + @Inject + @Metric(name = "index_permit_wait_time", absolute = true, unit = MetricUnits.NANOSECONDS, + description = "Displays how long does it take to receive a permit to index a dataset") + Timer indexPermitWaitTimer; + + @Inject + @Metric(name = "index_time", absolute = true, unit = MetricUnits.NANOSECONDS, + description = "Displays how long does it take to index a dataset") + Timer indexTimer; + + /** + * Try to acquire a permit from the semaphore avoiding too many parallel indexes, potentially overwhelming Solr. + * This method will time the duration waiting for the permit, allowing indexing performance to be measured. + * @throws InterruptedException + */ + private void acquirePermitFromSemaphore() throws InterruptedException { + try (var timeContext = indexPermitWaitTimer.time()) { + ASYNC_INDEX_SEMAPHORE.acquire(); + } + } // When you pass null as Dataset parameter to this method, it indicates that the indexing of the dataset with "id" has finished // Pass non-null Dataset to schedule it for indexing @@ -386,10 +418,24 @@ synchronized private static Dataset getNextToIndex(Long id, Dataset d) { */ @Asynchronous public void asyncIndexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) { + try { + acquirePermitFromSemaphore(); + doAyncIndexDataset(dataset, doNormalSolrDocCleanUp); + } catch (InterruptedException e) { + String failureLogText = "Indexing failed: interrupted. You can kickoff a re-index of this dataset with: \r\n curl http://localhost:8080/api/admin/index/datasets/" + dataset.getId().toString(); + failureLogText += "\r\n" + e.getLocalizedMessage(); + LoggingUtil.writeOnSuccessFailureLog(null, failureLogText, dataset); + } finally { + ASYNC_INDEX_SEMAPHORE.release(); + } + } + + private void doAyncIndexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) { Long id = dataset.getId(); Dataset next = getNextToIndex(id, dataset); // if there is an ongoing index job for this dataset, next is null (ongoing index job will reindex the newest version after current indexing finishes) while (next != null) { - try { + // Time context will automatically start on creation and stop when leaving the try block + try (var timeContext = indexTimer.time()) { indexDataset(next, doNormalSolrDocCleanUp); } catch (Exception e) { // catch all possible exceptions; otherwise when something unexpected happes the dataset wold remain locked and impossible to reindex String failureLogText = "Indexing failed. You can kickoff a re-index of this dataset with: \r\n curl http://localhost:8080/api/admin/index/datasets/" + dataset.getId().toString(); @@ -403,7 +449,16 @@ public void asyncIndexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) { @Asynchronous public void asyncIndexDatasetList(List datasets, boolean doNormalSolrDocCleanUp) { for(Dataset dataset : datasets) { - asyncIndexDataset(dataset, true); + try { + acquirePermitFromSemaphore(); + doAyncIndexDataset(dataset, true); + } catch (InterruptedException e) { + String failureLogText = "Indexing failed: interrupted. You can kickoff a re-index of this dataset with: \r\n curl http://localhost:8080/api/admin/index/datasets/" + dataset.getId().toString(); + failureLogText += "\r\n" + e.getLocalizedMessage(); + LoggingUtil.writeOnSuccessFailureLog(null, failureLogText, dataset); + } finally { + ASYNC_INDEX_SEMAPHORE.release(); + } } } @@ -415,7 +470,7 @@ public void indexDvObject(DvObject objectIn) throws SolrServerException, IOExce } } - private void indexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) throws SolrServerException, IOException { + public void indexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) throws SolrServerException, IOException { doIndexDataset(dataset, doNormalSolrDocCleanUp); updateLastIndexedTime(dataset.getId()); } @@ -426,94 +481,160 @@ private void doIndexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) thr * @todo should we use solrDocIdentifierDataset or * IndexableObject.IndexableTypes.DATASET.getName() + "_" ? */ - // String solrIdPublished = solrDocIdentifierDataset + dataset.getId(); String solrIdPublished = determinePublishedDatasetSolrDocId(dataset); String solrIdDraftDataset = IndexableObject.IndexableTypes.DATASET.getName() + "_" + dataset.getId() + IndexableDataset.DatasetState.WORKING_COPY.getSuffix(); - // String solrIdDeaccessioned = IndexableObject.IndexableTypes.DATASET.getName() - // + "_" + dataset.getId() + - // IndexableDataset.DatasetState.DEACCESSIONED.getSuffix(); String solrIdDeaccessioned = determineDeaccessionedDatasetId(dataset); StringBuilder debug = new StringBuilder(); debug.append("\ndebug:\n"); - int numPublishedVersions = 0; - List versions = dataset.getVersions(); - List solrIdsOfFilesToDelete = new ArrayList<>(); - for (DatasetVersion datasetVersion : versions) { - Long versionDatabaseId = datasetVersion.getId(); - String versionTitle = datasetVersion.getTitle(); - String semanticVersion = datasetVersion.getSemanticVersion(); - DatasetVersion.VersionState versionState = datasetVersion.getVersionState(); - if (versionState.equals(DatasetVersion.VersionState.RELEASED)) { - numPublishedVersions += 1; - } - debug.append("version found with database id " + versionDatabaseId + "\n"); - debug.append("- title: " + versionTitle + "\n"); - debug.append("- semanticVersion-VersionState: " + semanticVersion + "-" + versionState + "\n"); - List fileMetadatas = datasetVersion.getFileMetadatas(); - List fileInfo = new ArrayList<>(); - for (FileMetadata fileMetadata : fileMetadatas) { - String solrIdOfPublishedFile = solrDocIdentifierFile + fileMetadata.getDataFile().getId(); - /** - * It sounds weird but the first thing we'll do is preemptively - * delete the Solr documents of all published files. Don't - * worry, published files will be re-indexed later along with - * the dataset. We do this so users can delete files from - * published versions of datasets and then re-publish a new - * version without fear that their old published files (now - * deleted from the latest published version) will be - * searchable. See also - * https://github.com/IQSS/dataverse/issues/762 - */ - solrIdsOfFilesToDelete.add(solrIdOfPublishedFile); - fileInfo.add(fileMetadata.getDataFile().getId() + ":" + fileMetadata.getLabel()); - } - try { - /** - * Preemptively delete *all* Solr documents for files associated - * with the dataset based on a Solr query. - * - * We must query Solr for this information because the file has - * been deleted from the database ( perhaps when Solr was down, - * as reported in https://github.com/IQSS/dataverse/issues/2086 - * ) so the database doesn't even know about the file. It's an - * orphan. - * - * @todo This Solr query should make the iteration above based - * on the database unnecessary because it the Solr query should - * find all files for the dataset. We can probably remove the - * iteration above after an "index all" has been performed. - * Without an "index all" we won't be able to find files based - * on parentId because that field wasn't searchable in 4.0. - * - * @todo We should also delete the corresponding Solr - * "permission" documents for the files. - */ - List allFilesForDataset = findFilesOfParentDataset(dataset.getId()); - solrIdsOfFilesToDelete.addAll(allFilesForDataset); - } catch (SearchException | NullPointerException ex) { - logger.fine("could not run search of files to delete: " + ex); + boolean reduceSolrDeletes = FeatureFlags.REDUCE_SOLR_DELETES.enabled(); + if (!reduceSolrDeletes) { + int numPublishedVersions = 0; + List versions = dataset.getVersions(); + List solrIdsOfFilesToDelete = new ArrayList<>(); + for (DatasetVersion datasetVersion : versions) { + Long versionDatabaseId = datasetVersion.getId(); + String versionTitle = datasetVersion.getTitle(); + String semanticVersion = datasetVersion.getSemanticVersion(); + DatasetVersion.VersionState versionState = datasetVersion.getVersionState(); + if (versionState.equals(DatasetVersion.VersionState.RELEASED)) { + numPublishedVersions += 1; + } + debug.append("version found with database id " + versionDatabaseId + "\n"); + debug.append("- title: " + versionTitle + "\n"); + debug.append("- semanticVersion-VersionState: " + semanticVersion + "-" + versionState + "\n"); + List fileMetadatas = datasetVersion.getFileMetadatas(); + List fileInfo = new ArrayList<>(); + for (FileMetadata fileMetadata : fileMetadatas) { + String solrIdOfPublishedFile = solrDocIdentifierFile + fileMetadata.getDataFile().getId(); + /** + * It sounds weird but the first thing we'll do is preemptively + * delete the Solr documents of all published files. Don't + * worry, published files will be re-indexed later along with + * the dataset. We do this so users can delete files from + * published versions of datasets and then re-publish a new + * version without fear that their old published files (now + * deleted from the latest published version) will be + * searchable. See also + * https://github.com/IQSS/dataverse/issues/762 + */ + solrIdsOfFilesToDelete.add(solrIdOfPublishedFile); + fileInfo.add(fileMetadata.getDataFile().getId() + ":" + fileMetadata.getLabel()); + } + try { + /** + * Preemptively delete *all* Solr documents for files associated + * with the dataset based on a Solr query. + * + * We must query Solr for this information because the file has + * been deleted from the database ( perhaps when Solr was down, + * as reported in https://github.com/IQSS/dataverse/issues/2086 + * ) so the database doesn't even know about the file. It's an + * orphan. + * + * @todo This Solr query should make the iteration above based + * on the database unnecessary because it the Solr query should + * find all files for the dataset. We can probably remove the + * iteration above after an "index all" has been performed. + * Without an "index all" we won't be able to find files based + * on parentId because that field wasn't searchable in 4.0. + * + * @todo We should also delete the corresponding Solr + * "permission" documents for the files. + */ + List allFilesForDataset = findFilesOfParentDataset(dataset.getId()); + solrIdsOfFilesToDelete.addAll(allFilesForDataset); + } catch (SearchException | NullPointerException ex) { + logger.fine("could not run search of files to delete: " + ex); + } + int numFiles = 0; + if (fileMetadatas != null) { + numFiles = fileMetadatas.size(); + } + debug.append("- files: " + numFiles + " " + fileInfo.toString() + "\n"); } - int numFiles = 0; - if (fileMetadatas != null) { - numFiles = fileMetadatas.size(); + debug.append("numPublishedVersions: " + numPublishedVersions + "\n"); + if (doNormalSolrDocCleanUp) { + IndexResponse resultOfAttemptToPremptivelyDeletePublishedFiles = solrIndexService.deleteMultipleSolrIds(solrIdsOfFilesToDelete); + debug.append("result of attempt to premptively deleted published files before reindexing: " + resultOfAttemptToPremptivelyDeletePublishedFiles + "\n"); } - debug.append("- files: " + numFiles + " " + fileInfo.toString() + "\n"); - } - debug.append("numPublishedVersions: " + numPublishedVersions + "\n"); - if (doNormalSolrDocCleanUp) { - IndexResponse resultOfAttemptToPremptivelyDeletePublishedFiles = solrIndexService.deleteMultipleSolrIds(solrIdsOfFilesToDelete); - debug.append("result of attempt to premptively deleted published files before reindexing: " + resultOfAttemptToPremptivelyDeletePublishedFiles + "\n"); } DatasetVersion latestVersion = dataset.getLatestVersion(); - String latestVersionStateString = latestVersion.getVersionState().name(); DatasetVersion.VersionState latestVersionState = latestVersion.getVersionState(); + String latestVersionStateString = latestVersionState.name(); DatasetVersion releasedVersion = dataset.getReleasedVersion(); boolean atLeastOnePublishedVersion = false; if (releasedVersion != null) { atLeastOnePublishedVersion = true; - } else { - atLeastOnePublishedVersion = false; } + if (reduceSolrDeletes) { + List solrIdsOfDocsToDelete = null; + if (logger.isLoggable(Level.FINE)) { + writeDebugInfo(debug, dataset); + } + if (doNormalSolrDocCleanUp) { + try { + solrIdsOfDocsToDelete = findFilesOfParentDataset(dataset.getId()); + logger.fine("Existing file docs: " + String.join(", ", solrIdsOfDocsToDelete)); + if (!solrIdsOfDocsToDelete.isEmpty()) { + // We keep the latest version's docs unless it is deaccessioned and there is no + // published/released version + // So skip the loop removing those docs from the delete list except in that case + if ((!latestVersion.isDeaccessioned() || atLeastOnePublishedVersion)) { + List latestFileMetadatas = latestVersion.getFileMetadatas(); + String suffix = (new IndexableDataset(latestVersion)).getDatasetState().getSuffix(); + for (FileMetadata fileMetadata : latestFileMetadatas) { + String solrIdOfPublishedFile = solrDocIdentifierFile + + fileMetadata.getDataFile().getId() + suffix; + solrIdsOfDocsToDelete.remove(solrIdOfPublishedFile); + } + } + if (releasedVersion != null && !releasedVersion.equals(latestVersion)) { + List releasedFileMetadatas = releasedVersion.getFileMetadatas(); + for (FileMetadata fileMetadata : releasedFileMetadatas) { + String solrIdOfPublishedFile = solrDocIdentifierFile + + fileMetadata.getDataFile().getId(); + solrIdsOfDocsToDelete.remove(solrIdOfPublishedFile); + } + } + } + // Clear any unused dataset docs + if (!latestVersion.isDraft()) { + // The latest version is released, so should delete any draft docs for the + // dataset + solrIdsOfDocsToDelete.add(solrIdDraftDataset); + } + if (!atLeastOnePublishedVersion) { + // There's no released version, so should delete any normal state docs for the + // dataset + solrIdsOfDocsToDelete.add(solrIdPublished); + } + if (atLeastOnePublishedVersion || !latestVersion.isDeaccessioned()) { + // There's a released version or a draft, so should delete any deaccessioned + // state docs for the dataset + solrIdsOfDocsToDelete.add(solrIdDeaccessioned); + } + } catch (SearchException | NullPointerException ex) { + logger.fine("could not run search of files to delete: " + ex); + } + logger.fine("Solr docs to delete: " + String.join(", ", solrIdsOfDocsToDelete)); + + if (!solrIdsOfDocsToDelete.isEmpty()) { + List solrIdsOfPermissionDocsToDelete = new ArrayList<>(); + for (String file : solrIdsOfDocsToDelete) { + // Also remove associated permission docs + solrIdsOfPermissionDocsToDelete.add(file + discoverabilityPermissionSuffix); + } + solrIdsOfDocsToDelete.addAll(solrIdsOfPermissionDocsToDelete); + logger.fine("Solr docs and perm docs to delete: " + String.join(", ", solrIdsOfDocsToDelete)); + + IndexResponse resultOfAttemptToPremptivelyDeletePublishedFiles = solrIndexService + .deleteMultipleSolrIds(solrIdsOfDocsToDelete); + debug.append("result of attempt to premptively deleted published files before reindexing: " + + resultOfAttemptToPremptivelyDeletePublishedFiles + "\n"); + } + } + } + Map desiredCards = new LinkedHashMap<>(); /** * @todo refactor all of this below and have a single method that takes @@ -536,7 +657,7 @@ private void doIndexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) thr .append(indexDraftResult).append("\n"); desiredCards.put(DatasetVersion.VersionState.DEACCESSIONED, false); - if (doNormalSolrDocCleanUp) { + if (!reduceSolrDeletes && doNormalSolrDocCleanUp) { String deleteDeaccessionedResult = removeDeaccessioned(dataset); results.append("Draft exists, no need for deaccessioned version. Deletion attempted for ") .append(solrIdDeaccessioned).append(" (and files). Result: ") @@ -544,7 +665,7 @@ private void doIndexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) thr } desiredCards.put(DatasetVersion.VersionState.RELEASED, false); - if (doNormalSolrDocCleanUp) { + if (!reduceSolrDeletes && doNormalSolrDocCleanUp) { String deletePublishedResults = removePublished(dataset); results.append("No published version. Attempting to delete traces of published version from index. Result: ") .append(deletePublishedResults).append("\n"); @@ -587,13 +708,13 @@ private void doIndexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) thr results.append("No draft version. Attempting to index as deaccessioned. Result: ").append(indexDeaccessionedVersionResult).append("\n"); desiredCards.put(DatasetVersion.VersionState.RELEASED, false); - if (doNormalSolrDocCleanUp) { + if (!reduceSolrDeletes && doNormalSolrDocCleanUp) { String deletePublishedResults = removePublished(dataset); results.append("No published version. Attempting to delete traces of published version from index. Result: ").append(deletePublishedResults).append("\n"); } desiredCards.put(DatasetVersion.VersionState.DRAFT, false); - if (doNormalSolrDocCleanUp) { + if (!reduceSolrDeletes && doNormalSolrDocCleanUp) { List solrDocIdsForDraftFilesToDelete = findSolrDocIdsForDraftFilesToDelete(dataset); String deleteDraftDatasetVersionResult = removeSolrDocFromIndex(solrIdDraftDataset); String deleteDraftFilesResults = deleteDraftFiles(solrDocIdsForDraftFilesToDelete); @@ -641,7 +762,7 @@ private void doIndexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) thr results.append("Attempted to index " + solrIdPublished).append(". Result: ").append(indexReleasedVersionResult).append("\n"); desiredCards.put(DatasetVersion.VersionState.DRAFT, false); - if (doNormalSolrDocCleanUp) { + if (!reduceSolrDeletes && doNormalSolrDocCleanUp) { List solrDocIdsForDraftFilesToDelete = findSolrDocIdsForDraftFilesToDelete(dataset); String deleteDraftDatasetVersionResult = removeSolrDocFromIndex(solrIdDraftDataset); String deleteDraftFilesResults = deleteDraftFiles(solrDocIdsForDraftFilesToDelete); @@ -650,7 +771,7 @@ private void doIndexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) thr } desiredCards.put(DatasetVersion.VersionState.DEACCESSIONED, false); - if (doNormalSolrDocCleanUp) { + if (!reduceSolrDeletes && doNormalSolrDocCleanUp) { String deleteDeaccessionedResult = removeDeaccessioned(dataset); results.append("No need for deaccessioned version. Deletion attempted for ") .append(solrIdDeaccessioned).append(". Result: ").append(deleteDeaccessionedResult); @@ -701,7 +822,7 @@ private void doIndexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) thr .append(solrIdDraftDataset).append(" (limited visibility). Result: ").append(indexDraftResult).append("\n"); desiredCards.put(DatasetVersion.VersionState.DEACCESSIONED, false); - if (doNormalSolrDocCleanUp) { + if (!reduceSolrDeletes && doNormalSolrDocCleanUp) { String deleteDeaccessionedResult = removeDeaccessioned(dataset); results.append("No need for deaccessioned version. Deletion attempted for ") .append(solrIdDeaccessioned).append(". Result: ").append(deleteDeaccessionedResult); @@ -743,11 +864,42 @@ private void doIndexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) thr } } - private String deleteDraftFiles(List solrDocIdsForDraftFilesToDelete) { - String deleteDraftFilesResults = ""; - IndexResponse indexResponse = solrIndexService.deleteMultipleSolrIds(solrDocIdsForDraftFilesToDelete); - deleteDraftFilesResults = indexResponse.toString(); - return deleteDraftFilesResults; + private void writeDebugInfo(StringBuilder debug, Dataset dataset) { + List versions = dataset.getVersions(); + int numPublishedVersions = 0; + for (DatasetVersion datasetVersion : versions) { + Long versionDatabaseId = datasetVersion.getId(); + String versionTitle = datasetVersion.getTitle(); + String semanticVersion = datasetVersion.getSemanticVersion(); + DatasetVersion.VersionState versionState = datasetVersion.getVersionState(); + if (versionState.equals(DatasetVersion.VersionState.RELEASED)) { + numPublishedVersions += 1; + } + debug.append("version found with database id " + versionDatabaseId + "\n"); + debug.append("- title: " + versionTitle + "\n"); + debug.append("- semanticVersion-VersionState: " + semanticVersion + "-" + versionState + "\n"); + List fileInfo = new ArrayList<>(); + List fileMetadatas = datasetVersion.getFileMetadatas(); + + for (FileMetadata fileMetadata : fileMetadatas) { + /** + * It sounds weird but the first thing we'll do is preemptively delete the Solr + * documents of all published files. Don't worry, published files will be + * re-indexed later along with the dataset. We do this so users can delete files + * from published versions of datasets and then re-publish a new version without + * fear that their old published files (now deleted from the latest published + * version) will be searchable. See also + * https://github.com/IQSS/dataverse/issues/762 + */ + fileInfo.add(fileMetadata.getDataFile().getId() + ":" + fileMetadata.getLabel()); + } + int numFiles = 0; + if (fileMetadatas != null) { + numFiles = fileMetadatas.size(); + } + debug.append("- files: " + numFiles + " " + fileInfo.toString() + "\n"); + } + debug.append("numPublishedVersions: " + numPublishedVersions + "\n"); } private IndexResponse indexDatasetPermissions(Dataset dataset) { @@ -784,15 +936,7 @@ public SolrInputDocuments toSolrDocs(IndexableDataset indexableDataset, Set langs = settingsService.getConfiguredLanguages(); Map cvocMap = datasetFieldService.getCVocConf(true); + Map> cvocManagedFieldMap = new HashMap<>(); + for (Map.Entry cvocEntry : cvocMap.entrySet()) { + if(cvocEntry.getValue().containsKey("managed-fields")) { + JsonObject managedFields = cvocEntry.getValue().getJsonObject("managed-fields"); + Set managedFieldValues = new HashSet<>(); + for (String s : managedFields.keySet()) { + managedFieldValues.add(managedFields.getString(s)); + } + cvocManagedFieldMap.put(cvocEntry.getKey(), managedFieldValues); + } + } + + + Set metadataBlocksWithValue = new HashSet<>(); for (DatasetField dsf : datasetVersion.getFlatDatasetFields()) { @@ -942,36 +1115,73 @@ public SolrInputDocuments toSolrDocs(IndexableDataset indexableDataset, Set vals = dsf.getValues_nondisplay(); - Set searchStrings = new HashSet(); + Set searchStrings = new HashSet<>(); for (String val: vals) { searchStrings.add(val); - searchStrings.addAll(datasetFieldService.getStringsFor(val)); + // Try to get string values from externalvocabularyvalue using val as termUri + searchStrings.addAll(datasetFieldService.getIndexableStringsByTermUri(val, cvocMap.get(dsfType.getId()), dsfType.getName())); + + if(dsfType.getParentDatasetFieldType()!=null) { + List childDatasetFields = dsf.getParentDatasetFieldCompoundValue().getChildDatasetFields(); + for (DatasetField df : childDatasetFields) { + if(cvocManagedFieldMap.containsKey(dsfType.getId()) && cvocManagedFieldMap.get(dsfType.getId()).contains(df.getDatasetFieldType().getName())) { + String solrManagedFieldSearchable = df.getDatasetFieldType().getSolrField().getNameSearchable(); + // Try to get string values from externalvocabularyvalue but for a managed fields of the CVOCConf + Set stringsForManagedField = datasetFieldService.getIndexableStringsByTermUri(val, cvocMap.get(dsfType.getId()), df.getDatasetFieldType().getName()); + logger.fine(solrManagedFieldSearchable + " filled with externalvocabularyvalue : " + stringsForManagedField); + //.addField works as addition of value not a replace of value + // it allows to add mapped values by CVOCConf before or after indexing real DatasetField value(s) of solrManagedFieldSearchable + solrInputDocument.addField(solrManagedFieldSearchable, stringsForManagedField); + } + } + } } + logger.fine(solrFieldSearchable + " filled with externalvocabularyvalue : " + searchStrings); solrInputDocument.addField(solrFieldSearchable, searchStrings); if (dsfType.getSolrField().isFacetable()) { + logger.fine(solrFieldFacetable + " gets " + vals); solrInputDocument.addField(solrFieldFacetable, vals); } } + if (dsfType.isControlledVocabulary()) { - for (ControlledVocabularyValue controlledVocabularyValue : dsf.getControlledVocabularyValues()) { - if (controlledVocabularyValue.getStrValue().equals(DatasetField.NA_VALUE)) { - continue; - } + /** If the cvv list is empty but the dfv list is not then it is assumed this was harvested + * from an installation that had controlled vocabulary entries that don't exist in our this db + * @see Feature Request/Idea: Harvest metadata values that aren't from a list of controlled values #9992 + */ + if (dsf.getControlledVocabularyValues().isEmpty()) { + for (DatasetFieldValue dfv : dsf.getDatasetFieldValues()) { + if (dfv.getValue() == null || dfv.getValue().equals(DatasetField.NA_VALUE)) { + continue; + } + solrInputDocument.addField(solrFieldSearchable, dfv.getValue()); - // Index in all used languages (display and metadata languages - if (!dsfType.isAllowMultiples() || langs.isEmpty()) { - solrInputDocument.addField(solrFieldSearchable, controlledVocabularyValue.getStrValue()); - } else { - for(String locale: langs) { - solrInputDocument.addField(solrFieldSearchable, controlledVocabularyValue.getLocaleStrValue(locale)); + if (dsfType.getSolrField().isFacetable()) { + solrInputDocument.addField(solrFieldFacetable, dfv.getValue()); } } + } else { + for (ControlledVocabularyValue controlledVocabularyValue : dsf.getControlledVocabularyValues()) { + if (controlledVocabularyValue.getStrValue().equals(DatasetField.NA_VALUE)) { + continue; + } - if (dsfType.getSolrField().isFacetable()) { - solrInputDocument.addField(solrFieldFacetable, controlledVocabularyValue.getStrValue()); + // Index in all used languages (display and metadata languages + if (!dsfType.isAllowMultiples() || langs.isEmpty()) { + solrInputDocument.addField(solrFieldSearchable, controlledVocabularyValue.getStrValue()); + } else { + for(String locale: langs) { + solrInputDocument.addField(solrFieldSearchable, controlledVocabularyValue.getLocaleStrValue(locale)); + } + } + + if (dsfType.getSolrField().isFacetable()) { + solrInputDocument.addField(solrFieldFacetable, controlledVocabularyValue.getStrValue()); + } } } } else if (dsfType.getFieldType().equals(DatasetFieldType.FieldType.TEXTBOX)) { @@ -1080,7 +1290,7 @@ public SolrInputDocuments toSolrDocs(IndexableDataset indexableDataset, Set accessObject = null; InputStream instream = null; ContentHandler textHandler = null; @@ -1242,6 +1463,7 @@ public SolrInputDocuments toSolrDocs(IndexableDataset indexableDataset, Set d try { solrClientService.getSolrClient().add(docs.getDocuments()); - solrClientService.getSolrClient().commit(); } catch (SolrServerException | IOException ex) { if (ex.getCause() instanceof SolrServerException) { throw new SolrServerException(ex); @@ -1475,6 +1709,7 @@ private void updateLastIndexedTimeInNewTransaction(Long id) { DvObject dvObjectToModify = em.find(DvObject.class, id); dvObjectToModify.setIndexTime(new Timestamp(new Date().getTime())); dvObjectToModify = em.merge(dvObjectToModify); + em.flush(); } /** @@ -1611,6 +1846,26 @@ private List getDataversePathsFromSegments(List dataversePathSeg return subtrees; } + private void addLicenseToSolrDoc(SolrInputDocument solrInputDocument, DatasetVersion datasetVersion) { + if (datasetVersion != null && datasetVersion.getTermsOfUseAndAccess() != null) { + //test to see if the terms of use are the default set in 5.10 - if so and there's no license then don't add license to solr doc. + //fixes 10513 + if (datasetVersionService.isVersionDefaultCustomTerms(datasetVersion)){ + return; + } + + String licenseName = "Custom Terms"; + if (datasetVersion.getTermsOfUseAndAccess().getLicense() != null) { + licenseName = datasetVersion.getTermsOfUseAndAccess().getLicense().getName(); + } else if (datasetVersion.getTermsOfUseAndAccess().getTermsOfUse() == null) { + // this fixes #10513 for datasets harvested in oai_dc - these + // have neither the license id, nor any actual custom terms + return; + } + solrInputDocument.addField(SearchFields.DATASET_LICENSE, licenseName); + } + } + private void addDataverseReleaseDateToSolrDoc(SolrInputDocument solrInputDocument, Dataverse dataverse) { if (dataverse.getPublicationDate() != null) { Calendar calendar = Calendar.getInstance(); @@ -1688,7 +1943,6 @@ private void updatePathForExistingSolrDocs(DvObject object) throws SolrServerExc sid.removeField(SearchFields.SUBTREE); sid.addField(SearchFields.SUBTREE, paths); UpdateResponse addResponse = solrClientService.getSolrClient().add(sid); - UpdateResponse commitResponse = solrClientService.getSolrClient().commit(); if (object.isInstanceofDataset()) { for (DataFile df : dataset.getFiles()) { solrQuery.setQuery(SearchUtil.constructQuery(SearchFields.ENTITY_ID, df.getId().toString())); @@ -1702,7 +1956,6 @@ private void updatePathForExistingSolrDocs(DvObject object) throws SolrServerExc sid.removeField(SearchFields.SUBTREE); sid.addField(SearchFields.SUBTREE, paths); addResponse = solrClientService.getSolrClient().add(sid); - commitResponse = solrClientService.getSolrClient().commit(); } } } @@ -1748,11 +2001,6 @@ public String delete(Dataverse doomed) { } catch (SolrServerException | IOException ex) { return ex.toString(); } - try { - solrClientService.getSolrClient().commit(); - } catch (SolrServerException | IOException ex) { - return ex.toString(); - } String response = "Successfully deleted dataverse " + doomed.getId() + " from Solr index. updateReponse was: " + updateResponse.toString(); logger.fine(response); return response; @@ -1773,11 +2021,6 @@ public String removeSolrDocFromIndex(String doomed) { } catch (SolrServerException | IOException ex) { return ex.toString(); } - try { - solrClientService.getSolrClient().commit(); - } catch (SolrServerException | IOException ex) { - return ex.toString(); - } String response = "Attempted to delete " + doomed + " from Solr index. updateReponse was: " + updateResponse.toString(); logger.fine(response); return response; @@ -1819,6 +2062,7 @@ private String determineDeaccessionedDatasetId(Dataset dataset) { return IndexableObject.IndexableTypes.DATASET.getName() + "_" + dataset.getId() + IndexableDataset.DatasetState.DEACCESSIONED.getSuffix(); } + //Only used when FeatureFlags.REDUCE_SOLR_DELETES is disabled private String removeDeaccessioned(Dataset dataset) { StringBuilder result = new StringBuilder(); String deleteDeaccessionedResult = removeSolrDocFromIndex(determineDeaccessionedDatasetId(dataset)); @@ -1829,6 +2073,7 @@ private String removeDeaccessioned(Dataset dataset) { return result.toString(); } + //Only used when FeatureFlags.REDUCE_SOLR_DELETES is disabled private String removePublished(Dataset dataset) { StringBuilder result = new StringBuilder(); String deletePublishedResult = removeSolrDocFromIndex(determinePublishedDatasetSolrDocId(dataset)); @@ -1838,6 +2083,14 @@ private String removePublished(Dataset dataset) { result.append(deleteFilesResult); return result.toString(); } + + // Only used when FeatureFlags.REDUCE_SOLR_DELETES is disabled + private String deleteDraftFiles(List solrDocIdsForDraftFilesToDelete) { + String deleteDraftFilesResults = ""; + IndexResponse indexResponse = solrIndexService.deleteMultipleSolrIds(solrDocIdsForDraftFilesToDelete); + deleteDraftFilesResults = indexResponse.toString(); + return deleteDraftFilesResults; + } private Dataverse findRootDataverseCached() { if (true) { @@ -1958,9 +2211,10 @@ public List findFilesInSolrOnly() throws SearchException { * @throws SearchException */ public List findPermissionsInSolrOnly() throws SearchException { + logger.info("Checking for solr-only permissions"); List permissionInSolrOnly = new ArrayList<>(); try { - int rows = 100; + int rows = 1000; SolrQuery q = (new SolrQuery(SearchFields.DEFINITION_POINT_DVOBJECT_ID+":*")).setRows(rows).setSort(SortClause.asc(SearchFields.ID)); String cursorMark = CursorMarkParams.CURSOR_MARK_START; boolean done = false; @@ -1968,11 +2222,56 @@ public List findPermissionsInSolrOnly() throws SearchException { q.set(CursorMarkParams.CURSOR_MARK_PARAM, cursorMark); QueryResponse rsp = solrServer.query(q); String nextCursorMark = rsp.getNextCursorMark(); + logger.fine("Next cursor mark (1K entries): " + nextCursorMark); SolrDocumentList list = rsp.getResults(); for (SolrDocument doc: list) { long id = Long.parseLong((String) doc.getFieldValue(SearchFields.DEFINITION_POINT_DVOBJECT_ID)); - if(!dvObjectService.checkExists(id)) { - permissionInSolrOnly.add((String)doc.getFieldValue(SearchFields.ID)); + String docId = (String) doc.getFieldValue(SearchFields.ID); + String dtype = dvObjectService.getDtype(id); + if (dtype == null) { + permissionInSolrOnly.add(docId); + } + if (dtype.equals(DType.Dataset.getDType())) { + List states = datasetService.getVersionStates(id); + if (states != null) { + String latestState = states.get(states.size() - 1); + if (docId.endsWith("draft_permission")) { + if (!latestState.equals(VersionState.DRAFT.toString())) { + permissionInSolrOnly.add(docId); + } + } else if (docId.endsWith("deaccessioned_permission")) { + if (!latestState.equals(VersionState.DEACCESSIONED.toString())) { + permissionInSolrOnly.add(docId); + } + } else { + if (!states.contains(VersionState.RELEASED.toString())) { + permissionInSolrOnly.add(docId); + } + } + } + } else if (dtype.equals(DType.DataFile.getDType())) { + List states = dataFileService.findVersionStates(id); + Set strings = states.stream().map(VersionState::toString).collect(Collectors.toSet()); + logger.fine("States for " + docId + ": " + String.join(", ", strings)); + if (docId.endsWith("draft_permission")) { + if (!states.contains(VersionState.DRAFT)) { + permissionInSolrOnly.add(docId); + } + } else if (docId.endsWith("deaccessioned_permission")) { + if (!states.contains(VersionState.DEACCESSIONED) && states.size() == 1) { + permissionInSolrOnly.add(docId); + } + } else { + if (!states.contains(VersionState.RELEASED)) { + permissionInSolrOnly.add(docId); + } else { + if (!dataFileService.isInReleasedVersion(id)) { + logger.fine("Adding doc " + docId + " to list of permissions in Solr only"); + permissionInSolrOnly.add(docId); + } + } + + } } } if (cursorMark.equals(nextCursorMark)) { @@ -1983,6 +2282,9 @@ public List findPermissionsInSolrOnly() throws SearchException { } catch (SolrServerException | IOException ex) { throw new SearchException("Error searching Solr for permissions" , ex); + } catch (Exception e) { + logger.warning(e.getLocalizedMessage()); + e.printStackTrace(); } return permissionInSolrOnly; } @@ -2013,7 +2315,7 @@ private List findDvObjectInSolrOnly(String type) throws SearchException if (idObject != null) { try { long id = (Long) idObject; - if (!dvObjectService.checkExists(id)) { + if (dvObjectService.getDtype(id) == null) { dvObjectInSolrOnly.add((String)doc.getFieldValue(SearchFields.ID)); } } catch (ClassCastException ex) { diff --git a/src/main/java/edu/harvard/iq/dataverse/search/SearchConstants.java b/src/main/java/edu/harvard/iq/dataverse/search/SearchConstants.java index 73b39332013..2d6632760fb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SearchConstants.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SearchConstants.java @@ -40,5 +40,5 @@ public class SearchConstants { public static final String RESTRICTED = "Restricted"; public static final String EMBARGOEDTHENRESTRICTED = "EmbargoedThenRestricted"; public static final String EMBARGOEDTHENPUBLIC = "EmbargoedThenPublic"; - + public static final String RETENTIONEXPIRED = "RetentionPeriodExpired"; } diff --git a/src/main/java/edu/harvard/iq/dataverse/search/SearchFields.java b/src/main/java/edu/harvard/iq/dataverse/search/SearchFields.java index 8fb7c161517..ef27a5eefaf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SearchFields.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SearchFields.java @@ -94,6 +94,7 @@ public class SearchFields { public static final String UNF = "unf"; public static final String DATAVERSE_NAME = "dvName"; public static final String DATAVERSE_ALIAS = "dvAlias"; + public static final String DATAVERSE_PARENT_ALIAS = "dvParentAlias"; public static final String DATAVERSE_AFFILIATION = "dvAffiliation"; public static final String DATAVERSE_DESCRIPTION = "dvDescription"; public static final String DATAVERSE_CATEGORY = "dvCategory"; @@ -217,6 +218,15 @@ public class SearchFields { public static final String DEFINITION_POINT_DVOBJECT_ID = "definitionPointDvObjectId"; public static final String DISCOVERABLE_BY = "discoverableBy"; + /** + * publicObject_b is an experimental field tied to the + * avoid-expensive-solr-join feature flag. Rather than discoverableBy which + * is a field on permission documents, publicObject_b is a field on content + * documents (dvObjects). By indexing publicObject_b=true, we can let guests + * search on it, avoiding an expensive join for those (common) users. + */ + public static final String PUBLIC_OBJECT = "publicObject_b"; + /** * i.e. "Unpublished", "Draft" (multivalued) */ @@ -255,6 +265,10 @@ more targeted results for just datasets. The format is YYYY (i.e. public static final String DATASET_PUBLICATION_DATE = "dsPublicationDate"; public static final String DATASET_PERSISTENT_ID = "dsPersistentId"; public static final String DATASET_VERSION_ID = "datasetVersionId"; + /** + * Datasets can be software, workflow, etc. See the DatasetType object. + */ + public static final String DATASET_TYPE = "datasetType"; public static final String VARIABLE_NAME = "variableName"; public static final String VARIABLE_LABEL = "variableLabel"; @@ -267,7 +281,7 @@ more targeted results for just datasets. The format is YYYY (i.e. public static final String FULL_TEXT = "_text_"; public static final String EMBARGO_END_DATE = "embargoEndDate"; - + public static final String RETENTION_END_DATE = "retentionEndDate"; // SpatialRecursivePrefixTreeFieldType: https://solr.apache.org/guide/8_11/spatial-search.html#rpt public static final String GEOLOCATION = "geolocation"; @@ -276,4 +290,6 @@ more targeted results for just datasets. The format is YYYY (i.e. public static final String DATASET_VALID = "datasetValid"; + public static final String DATASET_LICENSE = "license"; + } diff --git a/src/main/java/edu/harvard/iq/dataverse/search/SearchIncludeFragment.java b/src/main/java/edu/harvard/iq/dataverse/search/SearchIncludeFragment.java index 5a5d8781726..4f3f6e46e48 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SearchIncludeFragment.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SearchIncludeFragment.java @@ -21,6 +21,7 @@ import edu.harvard.iq.dataverse.SettingsWrapper; import edu.harvard.iq.dataverse.ThumbnailServiceWrapper; import edu.harvard.iq.dataverse.WidgetWrapper; +import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; @@ -355,8 +356,7 @@ The real issue here (https://github.com/IQSS/dataverse/issues/7304) is caused * https://github.com/IQSS/dataverse/issues/84 */ int numRows = 10; - HttpServletRequest httpServletRequest = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); - DataverseRequest dataverseRequest = new DataverseRequest(session.getUser(), httpServletRequest); + DataverseRequest dataverseRequest = getDataverseRequest(); List dataverses = new ArrayList<>(); dataverses.add(dataverse); solrQueryResponse = searchService.search(dataverseRequest, dataverses, queryToPassToSolr, filterQueriesFinal, sortField, sortOrder.toString(), paginationStart, onlyDataRelatedToMe, numRows, false, null, null, !isFacetsDisabled(), true); @@ -396,7 +396,7 @@ The real issue here (https://github.com/IQSS/dataverse/issues/7304) is caused } } - if (!wasSolrErrorEncountered() && selectedTypesList.size() < 3 && !isSolrTemporarilyUnavailable() && !isFacetsDisabled()) { + if (!wasSolrErrorEncountered() && selectedTypesList.size() < 3 && !isSolrTemporarilyUnavailable() && !isFacetsDisabled() && !isUncheckedTypesFacetDisabled()) { // If some types are NOT currently selected, we will need to // run a second search to obtain the numbers of the unselected types: @@ -1087,20 +1087,59 @@ public void setSolrTemporarilyUnavailable(boolean solrIsTemporarilyUnavailable) this.solrIsTemporarilyUnavailable = solrIsTemporarilyUnavailable; } + Boolean solrFacetsDisabled = null; /** * Indicates that the fragment should not be requesting facets in Solr * searches and rendering them on the page. * @return true if disabled; false by default */ public boolean isFacetsDisabled() { - // The method is used in rendered="..." logic. So we are using - // SettingsWrapper to make sure we are not looking it up repeatedly - // (settings are not expensive to look up, but - // still). + if (this.solrFacetsDisabled != null) { + return this.solrFacetsDisabled; + } + + if (settingsWrapper.isTrueForKey(SettingsServiceBean.Key.DisableSolrFacets, false)) { + return this.solrFacetsDisabled = true; + } + + // We also have mechanisms for disabling the facets selectively, just for + // the guests, or anonymous users: + if (session.getUser() instanceof GuestUser) { + if (settingsWrapper.isTrueForKey(SettingsServiceBean.Key.DisableSolrFacetsForGuestUsers, false)) { + return this.solrFacetsDisabled = true; + } + + // An even lower grade of user than Guest is a truly anonymous user - + // a guest user who came without the session cookie: + Map cookies = FacesContext.getCurrentInstance().getExternalContext().getRequestCookieMap(); + if (!(cookies != null && cookies.containsKey("JSESSIONID"))) { + if (settingsWrapper.isTrueForKey(SettingsServiceBean.Key.DisableSolrFacetsWithoutJsession, false)) { + return this.solrFacetsDisabled = true; + } + } + } - return settingsWrapper.isTrueForKey(SettingsServiceBean.Key.DisableSolrFacets, false); + return this.solrFacetsDisabled = false; + } + + Boolean disableSecondPassSearch = null; + + /** + * Indicates that we do not need to run the second search query to populate + * the counts for *unchecked* type facets. + * @return true if disabled; false by default + */ + public boolean isUncheckedTypesFacetDisabled() { + if (this.disableSecondPassSearch != null) { + return this.disableSecondPassSearch; + } + if (settingsWrapper.isTrueForKey(SettingsServiceBean.Key.DisableUncheckedTypesFacet, false)) { + return this.disableSecondPassSearch = true; + } + return this.disableSecondPassSearch = false; } + public boolean isRootDv() { return rootDv; } @@ -1243,6 +1282,12 @@ public List getFriendlyNamesFromFilterQuery(String filterQuery) { friendlyNames.add(friendlyName.get()); return friendlyNames; } + } else if (key.equals(SearchFields.DATASET_LICENSE)) { + try { + friendlyNames.add(BundleUtil.getStringFromPropertyFile("license." + valueWithoutQuotes.toLowerCase().replace(" ","_") + ".name", "License")); + } catch (Exception e) { + logger.fine(String.format("action=getFriendlyNamesFromFilterQuery cannot find friendlyName for key=%s value=%s", key, value)); + } } friendlyNames.add(valueWithoutQuotes); @@ -1474,9 +1519,31 @@ public boolean isActivelyEmbargoed(SolrSearchResult result) { return false; } } + + public boolean isRetentionExpired(SolrSearchResult result) { + Long retentionEndDate = result.getRetentionEndDate(); + if(retentionEndDate != null) { + return LocalDate.now().toEpochDay() > retentionEndDate; + } else { + return false; + } + } + private DataverseRequest getDataverseRequest() { + final HttpServletRequest httpServletRequest = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); + return new DataverseRequest(session.getUser(), httpServletRequest); + } + public boolean isValid(SolrSearchResult result) { - return result.isValid(); + return result.isValid(x -> { + Long id = x.getEntityId(); + DvObject obj = dvObjectService.findDvObject(id); + if(obj != null && obj instanceof Dataset) { + return permissionsWrapper.canUpdateDataset(getDataverseRequest(), (Dataset) obj); + } + logger.fine("isValid called for dvObject that is null (or not a dataset), id: " + id + "This can occur if a dataset is deleted while a search is in progress"); + return true; + }); } public enum SortOrder { diff --git a/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java index 51bf3bee30b..ee93c49ad34 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java @@ -1,14 +1,6 @@ package edu.harvard.iq.dataverse.search; -import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.DatasetFieldConstant; -import edu.harvard.iq.dataverse.DatasetFieldServiceBean; -import edu.harvard.iq.dataverse.DatasetFieldType; -import edu.harvard.iq.dataverse.DatasetVersionServiceBean; -import edu.harvard.iq.dataverse.Dataverse; -import edu.harvard.iq.dataverse.DataverseFacet; -import edu.harvard.iq.dataverse.DataverseMetadataBlockFacet; -import edu.harvard.iq.dataverse.DvObjectServiceBean; +import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.authorization.groups.Group; import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; @@ -16,6 +8,7 @@ import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.settings.FeatureFlags; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.SystemConfig; import java.io.IOException; @@ -39,6 +32,7 @@ import jakarta.ejb.EJBTransactionRolledbackException; import jakarta.ejb.Stateless; import jakarta.ejb.TransactionRolledbackLocalException; +import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.persistence.NoResultException; import org.apache.solr.client.solrj.SolrQuery; @@ -77,6 +71,8 @@ public class SearchServiceBean { SystemConfig systemConfig; @EJB SolrClientService solrClientService; + @Inject + ThumbnailServiceWrapper thumbnailServiceWrapper; /** * Import note: "onlyDatatRelatedToMe" relies on filterQueries for providing @@ -187,19 +183,11 @@ public SolrQueryResponse search( SolrQuery solrQuery = new SolrQuery(); query = SearchUtil.sanitizeQuery(query); solrQuery.setQuery(query); -// SortClause foo = new SortClause("name", SolrQuery.ORDER.desc); -// if (query.equals("*") || query.equals("*:*")) { -// solrQuery.setSort(new SortClause(SearchFields.NAME_SORT, SolrQuery.ORDER.asc)); if (sortField != null) { // is it ok not to specify any sort? - there are cases where we // don't care, and it must cost some extra cycles -- L.A. solrQuery.setSort(new SortClause(sortField, sortOrder)); } -// } else { -// solrQuery.setSort(sortClause); -// } -// solrQuery.setSort(sortClause); - solrQuery.setParam("fl", "*,score"); solrQuery.setParam("qt", "/select"); @@ -224,13 +212,25 @@ public SolrQueryResponse search( List metadataBlockFacets = new LinkedList<>(); if (addFacets) { + + + // ----------------------------------- // Facets to Retrieve // ----------------------------------- solrQuery.addFacetField(SearchFields.METADATA_TYPES); + solrQuery.addFacetField(SearchFields.DATASET_TYPE); solrQuery.addFacetField(SearchFields.DATAVERSE_CATEGORY); solrQuery.addFacetField(SearchFields.METADATA_SOURCE); solrQuery.addFacetField(SearchFields.PUBLICATION_YEAR); + /* + * We talked about this in slack on 2021-09-14, Users can see objects on draft/unpublished + * if the owner gives permissions to all users so it makes sense to expose this facet + * to all users. The request of this change started because the order of the facets were + * changed with the PR #9635 and this was unintended. + */ + solrQuery.addFacetField(SearchFields.PUBLICATION_STATUS); + solrQuery.addFacetField(SearchFields.DATASET_LICENSE); /** * @todo when a new method on datasetFieldService is available * (retrieveFacetsByDataverse?) only show the facets that the @@ -250,6 +250,7 @@ public SolrQueryResponse search( DatasetFieldType datasetField = dataverseFacet.getDatasetFieldType(); solrQuery.addFacetField(datasetField.getSolrField().getNameFacetable()); } + // Get all metadata block facets configured to be displayed metadataBlockFacets.addAll(dataverse.getMetadataBlockFacets()); } @@ -479,6 +480,7 @@ public SolrQueryResponse search( String identifier = (String) solrDocument.getFieldValue(SearchFields.IDENTIFIER); String citation = (String) solrDocument.getFieldValue(SearchFields.DATASET_CITATION); String citationPlainHtml = (String) solrDocument.getFieldValue(SearchFields.DATASET_CITATION_HTML); + String datasetType = (String) solrDocument.getFieldValue(SearchFields.DATASET_TYPE); String persistentUrl = (String) solrDocument.getFieldValue(SearchFields.PERSISTENT_URL); String name = (String) solrDocument.getFieldValue(SearchFields.NAME); String nameSort = (String) solrDocument.getFieldValue(SearchFields.NAME_SORT); @@ -494,9 +496,14 @@ public SolrQueryResponse search( String dvTree = (String) solrDocument.getFirstValue(SearchFields.SUBTREE); String identifierOfDataverse = (String) solrDocument.getFieldValue(SearchFields.IDENTIFIER_OF_DATAVERSE); String nameOfDataverse = (String) solrDocument.getFieldValue(SearchFields.DATAVERSE_NAME); + String dataverseAffiliation = (String) solrDocument.getFieldValue(SearchFields.DATAVERSE_AFFILIATION); + String dataverseParentAlias = (String) solrDocument.getFieldValue(SearchFields.DATAVERSE_PARENT_ALIAS); + String dataverseParentName = (String) solrDocument.getFieldValue(SearchFields.PARENT_NAME); Long embargoEndDate = (Long) solrDocument.getFieldValue(SearchFields.EMBARGO_END_DATE); + Long retentionEndDate = (Long) solrDocument.getFieldValue(SearchFields.RETENTION_END_DATE); + // Boolean datasetValid = (Boolean) solrDocument.getFieldValue(SearchFields.DATASET_VALID); - + List matchedFields = new ArrayList<>(); SolrSearchResult solrSearchResult = new SolrSearchResult(query, name); @@ -570,23 +577,23 @@ public SolrQueryResponse search( solrSearchResult.setDvTree(dvTree); solrSearchResult.setDatasetValid(datasetValid); - String originSource = (String) solrDocument.getFieldValue(SearchFields.METADATA_SOURCE); - if (IndexServiceBean.HARVESTED.equals(originSource)) { + if (Boolean.TRUE.equals((Boolean) solrDocument.getFieldValue(SearchFields.IS_HARVESTED))) { solrSearchResult.setHarvested(true); } solrSearchResult.setEmbargoEndDate(embargoEndDate); - + solrSearchResult.setRetentionEndDate(retentionEndDate); + /** * @todo start using SearchConstants class here */ if (type.equals("dataverses")) { solrSearchResult.setName(name); solrSearchResult.setHtmlUrl(baseUrl + SystemConfig.DATAVERSE_PATH + identifier); - // Do not set the ImageUrl, let the search include fragment fill in - // the thumbnail, similarly to how the dataset and datafile cards - // are handled. - //solrSearchResult.setImageUrl(baseUrl + "/api/access/dvCardImage/" + entityid); + solrSearchResult.setDataverseAffiliation(dataverseAffiliation); + solrSearchResult.setDataverseParentAlias(dataverseParentAlias); + solrSearchResult.setDataverseParentName(dataverseParentName); + solrSearchResult.setImageUrl(thumbnailServiceWrapper.getDataverseCardImageAsUrl(solrSearchResult)); /** * @todo Expose this API URL after "dvs" is changed to * "dataverses". Also, is an API token required for published @@ -596,6 +603,7 @@ public SolrQueryResponse search( } else if (type.equals("datasets")) { solrSearchResult.setHtmlUrl(baseUrl + "/dataset.xhtml?globalId=" + identifier); solrSearchResult.setApiUrl(baseUrl + "/api/datasets/" + entityid); + solrSearchResult.setImageUrl(thumbnailServiceWrapper.getDatasetCardImageAsUrl(solrSearchResult)); //Image url now set via thumbnail api //solrSearchResult.setImageUrl(baseUrl + "/api/access/dsCardImage/" + datasetVersionId); // No, we don't want to set the base64 thumbnails here. @@ -634,6 +642,7 @@ public SolrQueryResponse search( if (authors != null) { solrSearchResult.setDatasetAuthors(authors); } + solrSearchResult.setDatasetType(datasetType); } else if (type.equals("files")) { String parentGlobalId = null; Object parentGlobalIdObject = solrDocument.getFieldValue(SearchFields.PARENT_IDENTIFIER); @@ -643,6 +652,7 @@ public SolrQueryResponse search( } solrSearchResult.setHtmlUrl(baseUrl + "/dataset.xhtml?persistentId=" + parentGlobalId); solrSearchResult.setDownloadUrl(baseUrl + "/api/access/datafile/" + entityid); + solrSearchResult.setImageUrl(thumbnailServiceWrapper.getFileCardImageAsUrl(solrSearchResult)); /** * @todo We are not yet setting the API URL for files because * not all files have metadata. Only subsettable files (those @@ -712,10 +722,14 @@ public SolrQueryResponse search( boolean unpublishedAvailable = false; boolean deaccessionedAvailable = false; boolean hideMetadataSourceFacet = true; + boolean hideLicenseFacet = true; + boolean hideDatasetTypeFacet = true; for (FacetField facetField : queryResponse.getFacetFields()) { FacetCategory facetCategory = new FacetCategory(); List facetLabelList = new ArrayList<>(); int numMetadataSources = 0; + int numLicenses = 0; + int numDatasetTypes = 0; String metadataBlockName = ""; String datasetFieldName = ""; /** @@ -741,23 +755,33 @@ public SolrQueryResponse search( // logger.info("field: " + facetField.getName() + " " + facetFieldCount.getName() + " (" + facetFieldCount.getCount() + ")"); String localefriendlyName = null; if (facetFieldCount.getCount() > 0) { - if(metadataBlockName.length() > 0 ) { - localefriendlyName = getLocaleTitle(datasetFieldName,facetFieldCount.getName(), metadataBlockName); + if(metadataBlockName.length() > 0 ) { + localefriendlyName = getLocaleTitle(datasetFieldName,facetFieldCount.getName(), metadataBlockName); } else if (facetField.getName().equals(SearchFields.METADATA_TYPES)) { - Optional metadataBlockFacet = metadataBlockFacets.stream().filter(blockFacet -> blockFacet.getMetadataBlock().getName().equals(facetFieldCount.getName())).findFirst(); - if (metadataBlockFacet.isEmpty()) { + Optional metadataBlockFacet = metadataBlockFacets.stream().filter(blockFacet -> blockFacet.getMetadataBlock().getName().equals(facetFieldCount.getName())).findFirst(); + if (metadataBlockFacet.isEmpty()) { // metadata block facet is not configured to be displayed => ignore continue; - } + } - localefriendlyName = metadataBlockFacet.get().getMetadataBlock().getLocaleDisplayFacet(); - } else { - try { + localefriendlyName = metadataBlockFacet.get().getMetadataBlock().getLocaleDisplayFacet(); + } else if (facetField.getName().equals(SearchFields.DATASET_LICENSE)) { + try { + localefriendlyName = BundleUtil.getStringFromPropertyFile("license." + facetFieldCount.getName().toLowerCase().replace(" ","_") + ".name", "License"); + } catch (Exception e) { + localefriendlyName = facetFieldCount.getName(); + } + } else { + try { + // This is where facets are capitalized. + // This will be a problem for the API clients because they get back a string like this from the Search API... + // {"datasetType":{"friendly":"Dataset Type","labels":[{"Dataset":1},{"Software":1}]} + // ... but they will need to use the lower case version (e.g. "software") to narrow results. localefriendlyName = BundleUtil.getStringFromPropertyFile(facetFieldCount.getName(), "Bundle"); - } catch (Exception e) { + } catch (Exception e) { localefriendlyName = facetFieldCount.getName(); - } - } + } + } FacetLabel facetLabel = new FacetLabel(localefriendlyName, facetFieldCount.getCount()); // quote field facets facetLabel.setFilterQuery(facetField.getName() + ":\"" + facetFieldCount.getName() + "\""); @@ -770,15 +794,24 @@ public SolrQueryResponse search( } else if (facetFieldCount.getName().equals(IndexServiceBean.getDEACCESSIONED_STRING())) { deaccessionedAvailable = true; } - } - if (facetField.getName().equals(SearchFields.METADATA_SOURCE)) { + } else if (facetField.getName().equals(SearchFields.METADATA_SOURCE)) { numMetadataSources++; + } else if (facetField.getName().equals(SearchFields.DATASET_LICENSE)) { + numLicenses++; + } else if (facetField.getName().equals(SearchFields.DATASET_TYPE)) { + numDatasetTypes++; } } } if (numMetadataSources > 1) { hideMetadataSourceFacet = false; } + if (numLicenses > 1) { + hideLicenseFacet = false; + } + if (numDatasetTypes > 1 ) { + hideDatasetTypeFacet = false; + } facetCategory.setName(facetField.getName()); // hopefully people will never see the raw facetField.getName() because it may well have an _s at the end facetCategory.setFriendlyName(facetField.getName()); @@ -855,6 +888,14 @@ public SolrQueryResponse search( if (!hideMetadataSourceFacet) { facetCategoryList.add(facetCategory); } + } else if (facetCategory.getName().equals(SearchFields.DATASET_LICENSE)) { + if (!hideLicenseFacet) { + facetCategoryList.add(facetCategory); + } + } else if (facetCategory.getName().equals(SearchFields.DATASET_TYPE)) { + if (!hideDatasetTypeFacet) { + facetCategoryList.add(facetCategory); + } } else { facetCategoryList.add(facetCategory); } @@ -978,14 +1019,132 @@ private String getPermissionFilterQuery(DataverseRequest dataverseRequest, SolrQ user = GuestUser.get(); } + AuthenticatedUser au = null; + Set groups; + + if (user instanceof GuestUser) { + // Yes, GuestUser may be part of one or more groups; such as IP Groups. + groups = groupService.collectAncestors(groupService.groupsFor(dataverseRequest)); + } else { + if (!(user instanceof AuthenticatedUser)) { + logger.severe("Should never reach here. A User must be an AuthenticatedUser or a Guest"); + throw new IllegalStateException("A User must be an AuthenticatedUser or a Guest"); + } + + au = (AuthenticatedUser) user; + + // ---------------------------------------------------- + // (3) Is this a Super User? + // If so, they can see everything + // ---------------------------------------------------- + if (au.isSuperuser()) { + // Somewhat dangerous because this user (a superuser) will be able + // to see everything in Solr with no regard to permissions. But it's + // been this way since Dataverse 4.0. So relax. :) + + return dangerZoneNoSolrJoin; + } + + // ---------------------------------------------------- + // (4) User is logged in AND onlyDatatRelatedToMe == true + // Yes, give back everything -> the settings will be in + // the filterqueries given to search + // ---------------------------------------------------- + if (onlyDatatRelatedToMe == true) { + if (systemConfig.myDataDoesNotUsePermissionDocs()) { + logger.fine("old 4.2 behavior: MyData is not using Solr permission docs"); + return dangerZoneNoSolrJoin; + } else { + // fall-through + logger.fine("new post-4.2 behavior: MyData is using Solr permission docs"); + } + } + + // ---------------------------------------------------- + // (5) Work with Authenticated User who is not a Superuser + // ---------------------------------------------------- + + groups = groupService.collectAncestors(groupService.groupsFor(dataverseRequest)); + } + + if (FeatureFlags.AVOID_EXPENSIVE_SOLR_JOIN.enabled()) { + /** + * Instead of doing a super expensive join, we will rely on the + * new boolean field PublicObject:true for public objects. This field + * is indexed on the content document itself, rather than a permission + * document. An additional join will be added only for any extra, + * more restricted groups that the user may be part of. + * **Note the experimental nature of this optimization**. + */ + StringBuilder sb = new StringBuilder(); + StringBuilder sbgroups = new StringBuilder(); + + // All users, guests and authenticated, should see all the + // documents marked as publicObject_b:true, at least: + sb.append(SearchFields.PUBLIC_OBJECT + ":" + true); + + // One or more groups *may* also be available for this user. Once again, + // do note that Guest users may be part of some groups, such as + // IP groups. + + int groupCounter = 0; + + // An AuthenticatedUser should also be able to see all the content + // on which they have direct permissions: + if (au != null) { + groupCounter++; + sbgroups.append(IndexServiceBean.getGroupPerUserPrefix() + au.getId()); + } + + // In addition to the user referenced directly, we will also + // add joins on all the non-public groups that may exist for the + // user: + for (Group group : groups) { + String groupAlias = group.getAlias(); + if (groupAlias != null && !groupAlias.isEmpty() && !groupAlias.startsWith("builtIn")) { + groupCounter++; + if (groupCounter > 1) { + sbgroups.append(" OR "); + } + sbgroups.append(IndexServiceBean.getGroupPrefix() + groupAlias); + } + } + + if (groupCounter > 1) { + // If there is more than one group, the parentheses must be added: + sbgroups.insert(0, "("); + sbgroups.append(")"); + } + + if (groupCounter > 0) { + // If there are any groups for this user, an extra join must be + // added to the query, and the extra sub-query must be added to + // the combined Solr query: + sb.append(" OR {!join from=" + SearchFields.DEFINITION_POINT + " to=id v=$q1}"); + // Add the subquery to the combined Solr query: + solrQuery.setParam("q1", SearchFields.DISCOVERABLE_BY + ":" + sbgroups.toString()); + logger.info("The sub-query q1 set to " + SearchFields.DISCOVERABLE_BY + ":" + sbgroups.toString()); + } + + String ret = sb.toString(); + logger.fine("Returning experimental query: " + ret); + return ret; + } + + // END OF EXPERIMENTAL OPTIMIZATION + + // Old, un-optimized way of handling permissions. + // Largely left intact, minus the lookups that have already been performed + // above. + // ---------------------------------------------------- // (1) Is this a GuestUser? - // Yes, see if GuestUser is part of any groups such as IP Groups. // ---------------------------------------------------- if (user instanceof GuestUser) { - String groupsFromProviders = ""; - Set groups = groupService.collectAncestors(groupService.groupsFor(dataverseRequest)); + StringBuilder sb = new StringBuilder(); + + String groupsFromProviders = ""; for (Group group : groups) { logger.fine("found group " + group.getIdentifier() + " with alias " + group.getAlias()); String groupAlias = group.getAlias(); @@ -1002,51 +1161,11 @@ private String getPermissionFilterQuery(DataverseRequest dataverseRequest, SolrQ return guestWithGroups; } - // ---------------------------------------------------- - // (2) Retrieve Authenticated User - // ---------------------------------------------------- - if (!(user instanceof AuthenticatedUser)) { - logger.severe("Should never reach here. A User must be an AuthenticatedUser or a Guest"); - throw new IllegalStateException("A User must be an AuthenticatedUser or a Guest"); - } - - AuthenticatedUser au = (AuthenticatedUser) user; - - if (addFacets) { - // Logged in user, has publication status facet - // - solrQuery.addFacetField(SearchFields.PUBLICATION_STATUS); - } - - // ---------------------------------------------------- - // (3) Is this a Super User? - // Yes, give back everything - // ---------------------------------------------------- - if (au.isSuperuser()) { - // Somewhat dangerous because this user (a superuser) will be able - // to see everything in Solr with no regard to permissions. But it's - // been this way since Dataverse 4.0. So relax. :) - - return dangerZoneNoSolrJoin; - } - - // ---------------------------------------------------- - // (4) User is logged in AND onlyDatatRelatedToMe == true - // Yes, give back everything -> the settings will be in - // the filterqueries given to search - // ---------------------------------------------------- - if (onlyDatatRelatedToMe == true) { - if (systemConfig.myDataDoesNotUsePermissionDocs()) { - logger.fine("old 4.2 behavior: MyData is not using Solr permission docs"); - return dangerZoneNoSolrJoin; - } else { - logger.fine("new post-4.2 behavior: MyData is using Solr permission docs"); - } - } - // ---------------------------------------------------- // (5) Work with Authenticated User who is not a Superuser - // ---------------------------------------------------- + // ---------------------------------------------------- + // It was already confirmed, that if the user is not GuestUser, we + // have an AuthenticatedUser au which is not null. /** * @todo all this code needs cleanup and clarification. */ @@ -1077,7 +1196,6 @@ private String getPermissionFilterQuery(DataverseRequest dataverseRequest, SolrQ * a given "content document" (dataset version, etc) in Solr. */ String groupsFromProviders = ""; - Set groups = groupService.collectAncestors(groupService.groupsFor(dataverseRequest)); StringBuilder sb = new StringBuilder(); for (Group group : groups) { logger.fine("found group " + group.getIdentifier() + " with alias " + group.getAlias()); diff --git a/src/main/java/edu/harvard/iq/dataverse/search/SolrIndexServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/SolrIndexServiceBean.java index 04021eb75b6..cfe29ea08c7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SolrIndexServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SolrIndexServiceBean.java @@ -357,7 +357,6 @@ private void persistToSolr(Collection docs) throws SolrServer * @todo Do something with these responses from Solr. */ UpdateResponse addResponse = solrClientService.getSolrClient().add(docs); - UpdateResponse commitResponse = solrClientService.getSolrClient().commit(); } public IndexResponse indexPermissionsOnSelfAndChildren(long definitionPointId) { @@ -504,11 +503,6 @@ public IndexResponse deleteMultipleSolrIds(List solrIdsToDelete) { */ return new IndexResponse("problem deleting the following documents from Solr: " + solrIdsToDelete); } - try { - solrClientService.getSolrClient().commit(); - } catch (SolrServerException | IOException ex) { - return new IndexResponse("problem committing deletion of the following documents from Solr: " + solrIdsToDelete); - } return new IndexResponse("no known problem deleting the following documents from Solr:" + solrIdsToDelete); } @@ -516,7 +510,6 @@ public JsonObjectBuilder deleteAllFromSolrAndResetIndexTimes() throws SolrServer JsonObjectBuilder response = Json.createObjectBuilder(); logger.info("attempting to delete all Solr documents before a complete re-index"); solrClientService.getSolrClient().deleteByQuery("*:*"); - solrClientService.getSolrClient().commit(); int numRowsAffected = dvObjectService.clearAllIndexTimes(); response.add(numRowsClearedByClearAllIndexTimes, numRowsAffected); response.add(messageString, "Solr index and database index timestamps cleared."); diff --git a/src/main/java/edu/harvard/iq/dataverse/search/SolrSearchResult.java b/src/main/java/edu/harvard/iq/dataverse/search/SolrSearchResult.java index 6ad7f9dbbf6..27900bac63f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SolrSearchResult.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SolrSearchResult.java @@ -7,8 +7,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Predicate; import java.util.logging.Logger; +import edu.harvard.iq.dataverse.*; import jakarta.json.Json; import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonObject; @@ -16,149 +18,152 @@ import org.apache.commons.collections4.CollectionUtils; -import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.Dataset; -import edu.harvard.iq.dataverse.DatasetField; -import edu.harvard.iq.dataverse.DatasetRelPublication; -import edu.harvard.iq.dataverse.DatasetVersion; -import edu.harvard.iq.dataverse.DvObject; -import edu.harvard.iq.dataverse.GlobalId; -import edu.harvard.iq.dataverse.MetadataBlock; import edu.harvard.iq.dataverse.api.Util; import edu.harvard.iq.dataverse.dataset.DatasetThumbnail; +import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.util.DateUtil; import edu.harvard.iq.dataverse.util.json.JsonPrinter; import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; public class SolrSearchResult { - private static final Logger logger = Logger.getLogger(SolrSearchResult.class.getCanonicalName()); - - private String id; - private Long entityId; - private DvObject entity; - private String identifier; - private String type; - private String htmlUrl; - private String persistentUrl; - private String downloadUrl; - private String apiUrl; - /** - * This is called "imageUrl" because it used to really be a URL. While performance improvements were being made in the 4.2 timeframe, we started - * putting base64 representations of images in this String instead, which broke the Search API and probably things built on top of it such as MyData. - * See "`image_url` from Search API results no longer yields a downloadable image" at https://github.com/IQSS/dataverse/issues/3616 - */ - private String imageUrl; - private DatasetThumbnail datasetThumbnail; - private String query; - private String name; - private String nameSort; - private String status; - private Date releaseOrCreateDate; - private String dateToDisplayOnCard; - private List publicationStatuses = new ArrayList<>(); - - /** - * @todo: how important is it to differentiate between name and title? - */ - private String title; - private String descriptionNoSnippet; - private List datasetAuthors = new ArrayList<>(); - private String deaccessionReason; - private List highlightsAsList = new ArrayList<>(); - private Map highlightsMap; - private Map highlightsAsMap; - - // parent can be dataverse or dataset, store the name and id - /** - * The "identifier" of a file's parent (a dataset) is a globalId (often a doi). - */ - public static String PARENT_IDENTIFIER = "identifier"; - private Map parent; - private String dataverseAffiliation; - private String citation; - private String citationHtml; - /** - * Files and datasets might have a UNF. Dataverses don't. - */ - private String unf; - private String filetype; - private String fileContentType; - private Long fileSizeInBytes; - /** - * fileMD5 is here for legacy and backward-compatibility reasons. It might be deprecated some day in favor of "fileChecksumType" and - * "fileChecksumValue" - */ - private String fileMd5; - private DataFile.ChecksumType fileChecksumType; - private String fileChecksumValue; - private String dataverseAlias; - private String dataverseParentAlias; + private static final Logger logger = Logger.getLogger(SolrSearchResult.class.getCanonicalName()); + + private String id; + private Long entityId; + private DvObject entity; + private String identifier; + private String type; + private String htmlUrl; + private String persistentUrl; + private String downloadUrl; + private String apiUrl; + /** + * This is called "imageUrl" because it used to really be a URL. While + * performance improvements were being made in the 4.2 timeframe, we started + * putting base64 representations of images in this String instead, which + * broke the Search API and probably things built on top of it such as + * MyData. See "`image_url` from Search API results no longer yields a + * downloadable image" at https://github.com/IQSS/dataverse/issues/3616 + */ + private String imageUrl; + private DatasetThumbnail datasetThumbnail; + private String query; + private String name; + private String nameSort; + private String status; + private Date releaseOrCreateDate; + private String dateToDisplayOnCard; + private List publicationStatuses = new ArrayList<>(); + + /** + * @todo: how important is it to differentiate between name and title? + */ + private String title; + private String descriptionNoSnippet; + private List datasetAuthors = new ArrayList<>(); + private String deaccessionReason; + private List highlightsAsList = new ArrayList<>(); + private Map highlightsMap; + private Map highlightsAsMap; + + // parent can be dataverse or dataset, store the name and id + /** + * The "identifier" of a file's parent (a dataset) is a globalId (often a + * doi). + */ + public static String PARENT_IDENTIFIER = "identifier"; + private Map parent; + private String dataverseAffiliation; + private String citation; + private String citationHtml; + private String datasetType; + /** + * Files and datasets might have a UNF. Dataverses don't. + */ + private String unf; + private String filetype; + private String fileContentType; + private Long fileSizeInBytes; + /** + * fileMD5 is here for legacy and backward-compatibility reasons. It might + * be deprecated some day in favor of "fileChecksumType" and + * "fileChecksumValue" + */ + private String fileMd5; + private DataFile.ChecksumType fileChecksumType; + private String fileChecksumValue; + private String dataverseAlias; + private String dataverseParentAlias; + private String dataverseParentName; // private boolean statePublished; - /** - * @todo Investigate/remove this "unpublishedState" variable. For files that have been published along with a dataset it says "true", which makes no - * sense. - */ - private boolean publishedState = false; - private boolean unpublishedState = false; - private boolean draftState = false; - private boolean inReviewState = false; - private boolean deaccessionedState = false; - private long datasetVersionId; - private String versionNumberFriendly; - // Determine if the search result is owned by any of the dvs in the tree of the DV displayed - private boolean isInTree; - private float score; - private List userRole; - private boolean harvested = false; - private String dvTree; - private String harvestingDescription = null; - private List fileCategories = null; - private List tabularDataTags = null; - - private String identifierOfDataverse = null; - private String nameOfDataverse = null; - - private String filePersistentId = null; - - private Long embargoEndDate; - - private boolean datasetValid; - - public String getDvTree() { - return dvTree; - } - - public void setDvTree(String dvTree) { - this.dvTree = dvTree; - } - - public boolean isIsInTree() { - return isInTree; - } - - public void setIsInTree(boolean isInTree) { - this.isInTree = isInTree; - } - - public boolean isHarvested() { - return harvested; - } - - public void setHarvested(boolean harvested) { - this.harvested = harvested; - } - - public String getHarvestingDescription() { - // if (this.isHarvested()) { - return harvestingDescription; - // } - // return null; - } - - public void setHarvestingDescription(String harvestingDescription) { - this.harvestingDescription = harvestingDescription; - } + /** + * @todo Investigate/remove this "unpublishedState" variable. For files that + * have been published along with a dataset it says "true", which makes no + * sense. + */ + private boolean publishedState = false; + private boolean unpublishedState = false; + private boolean draftState = false; + private boolean inReviewState = false; + private boolean deaccessionedState = false; + private long datasetVersionId; + private String versionNumberFriendly; + // Determine if the search result is owned by any of the dvs in the tree of the DV displayed + private boolean isInTree; + private float score; + private List userRole; + private boolean harvested = false; + private String dvTree; + private String harvestingDescription = null; + private List fileCategories = null; + private List tabularDataTags = null; + + private String identifierOfDataverse = null; + private String nameOfDataverse = null; + + private String filePersistentId = null; + + private Long embargoEndDate; + + private Long retentionEndDate; + + private boolean datasetValid; + + public String getDvTree() { + return dvTree; + } + + public void setDvTree(String dvTree) { + this.dvTree = dvTree; + } + + public boolean isIsInTree() { + return isInTree; + } + + public void setIsInTree(boolean isInTree) { + this.isInTree = isInTree; + } + + public boolean isHarvested() { + return harvested; + } + + public void setHarvested(boolean harvested) { + this.harvested = harvested; + } + + public String getHarvestingDescription() { + // if (this.isHarvested()) { + return harvestingDescription; + // } + // return null; + } + + public void setHarvestingDescription(String harvestingDescription) { + this.harvestingDescription = harvestingDescription; + } // public boolean isStatePublished() { // return statePublished; // } @@ -166,1106 +171,1181 @@ public void setHarvestingDescription(String harvestingDescription) { // this.statePublished = statePublished; // } - public boolean isPublishedState() { - return publishedState; - } - - public void setPublishedState(boolean publishedState) { - this.publishedState = publishedState; - } - - public boolean isUnpublishedState() { - return unpublishedState; - } - - public void setUnpublishedState(boolean unpublishedState) { - this.unpublishedState = unpublishedState; - } - - public void setPublicationStatuses(List statuses) { - - if (statuses == null) { - this.publicationStatuses = new ArrayList<>(); - return; - } - this.publicationStatuses = statuses; - - // set booleans for individual statuses - // - for (String status : this.publicationStatuses) { - - if (status.equals(IndexServiceBean.getUNPUBLISHED_STRING())) { - this.setUnpublishedState(true); - - } else if (status.equals(IndexServiceBean.getPUBLISHED_STRING())) { - this.setPublishedState(true); - - } else if (status.equals(IndexServiceBean.getDRAFT_STRING())) { - this.setDraftState(true); - - } else if (status.equals(IndexServiceBean.getIN_REVIEW_STRING())) { - this.setInReviewState(true); - - } else if (status.equals(IndexServiceBean.getDEACCESSIONED_STRING())) { - this.setDeaccessionedState(true); - } - } - } // setPublicationStatuses - - /** - * Never return null, return an empty list instead - * - * @return - */ - public List getPublicationStatuses() { - - if (this.publicationStatuses == null) { - this.publicationStatuses = new ArrayList<>(); - } - return this.publicationStatuses; - } - - public JsonArrayBuilder getPublicationStatusesAsJSON() { - - JsonArrayBuilder statuses = Json.createArrayBuilder(); - for (String status : this.getPublicationStatuses()) { - statuses.add(status); - } - return statuses; - } - - public boolean isDraftState() { - return draftState; - } - - public void setDraftState(boolean draftState) { - this.draftState = draftState; - } - - public boolean isInReviewState() { - return inReviewState; - } - - public void setInReviewState(boolean inReviewState) { - this.inReviewState = inReviewState; - } - - public boolean isDeaccessionedState() { - return deaccessionedState; - } - - public void setDeaccessionedState(boolean deaccessionedState) { - this.deaccessionedState = deaccessionedState; - } - - /** - * @todo: used? remove - */ - private List matchedFields; - - // External Status Label (enabled via AllowedCurationLabels setting) - private String externalStatus; - - /** - * @todo: remove name? - */ - SolrSearchResult(String queryFromUser, String name) { - this.query = queryFromUser; + public boolean isPublishedState() { + return publishedState; + } + + public void setPublishedState(boolean publishedState) { + this.publishedState = publishedState; + } + + public boolean isUnpublishedState() { + return unpublishedState; + } + + public void setUnpublishedState(boolean unpublishedState) { + this.unpublishedState = unpublishedState; + } + + public void setPublicationStatuses(List statuses) { + + if (statuses == null) { + this.publicationStatuses = new ArrayList<>(); + return; + } + this.publicationStatuses = statuses; + + // set booleans for individual statuses + // + for (String status : this.publicationStatuses) { + + if (status.equals(IndexServiceBean.getUNPUBLISHED_STRING())) { + this.setUnpublishedState(true); + + } else if (status.equals(IndexServiceBean.getPUBLISHED_STRING())) { + this.setPublishedState(true); + + } else if (status.equals(IndexServiceBean.getDRAFT_STRING())) { + this.setDraftState(true); + + } else if (status.equals(IndexServiceBean.getIN_REVIEW_STRING())) { + this.setInReviewState(true); + + } else if (status.equals(IndexServiceBean.getDEACCESSIONED_STRING())) { + this.setDeaccessionedState(true); + } + } + } // setPublicationStatuses + + /** + * Never return null, return an empty list instead + * + * @return + */ + public List getPublicationStatuses() { + + if (this.publicationStatuses == null) { + this.publicationStatuses = new ArrayList<>(); + } + return this.publicationStatuses; + } + + public JsonArrayBuilder getPublicationStatusesAsJSON() { + + JsonArrayBuilder statuses = Json.createArrayBuilder(); + for (String status : this.getPublicationStatuses()) { + statuses.add(status); + } + return statuses; + } + + public boolean isDraftState() { + return draftState; + } + + public void setDraftState(boolean draftState) { + this.draftState = draftState; + } + + public boolean isInReviewState() { + return inReviewState; + } + + public void setInReviewState(boolean inReviewState) { + this.inReviewState = inReviewState; + } + + public boolean isDeaccessionedState() { + return deaccessionedState; + } + + public void setDeaccessionedState(boolean deaccessionedState) { + this.deaccessionedState = deaccessionedState; + } + + /** + * @todo: used? remove + */ + private List matchedFields; + + // External Status Label (enabled via AllowedCurationLabels setting) + private String externalStatus; + + /** + * @todo: remove name? + */ + SolrSearchResult(String queryFromUser, String name) { + this.query = queryFromUser; // this.name = name; - } - - public Map getHighlightsAsMap() { - return highlightsAsMap; - } - - public void setHighlightsAsMap(Map highlightsAsMap) { - this.highlightsAsMap = highlightsAsMap; - } - - public String getNameHighlightSnippet() { - Highlight highlight = highlightsAsMap.get(SearchFields.NAME); - if (highlight != null) { - String firstSnippet = highlight.getSnippets().get(0); - if (firstSnippet != null) { - return firstSnippet; - } - } - return null; - } - - public String getDataverseAffiliationHighlightSnippet() { - Highlight highlight = highlightsAsMap.get(SearchFields.AFFILIATION); - if (highlight != null) { - String firstSnippet = highlight.getSnippets().get(0); - if (firstSnippet != null) { - return firstSnippet; - } - } - return null; - } - - public String getFileTypeHighlightSnippet() { - Highlight highlight = highlightsAsMap.get(SearchFields.FILE_TYPE_FRIENDLY); - if (highlight != null) { - String firstSnippet = highlight.getSnippets().get(0); - if (firstSnippet != null) { - return firstSnippet; - } - } - return null; - } - - public String getTitleHighlightSnippet() { - /** - * @todo: don't hard-code title, look it up properly... or start indexing titles as names: https://redmine.hmdc.harvard.edu/issues/3798#note-2 - */ - Highlight highlight = highlightsAsMap.get("title"); - if (highlight != null) { - String firstSnippet = highlight.getSnippets().get(0); - if (firstSnippet != null) { - return firstSnippet; - } - } - return null; - } - - public List getDescriptionSnippets() { - for (Map.Entry entry : highlightsMap.entrySet()) { - SolrField solrField = entry.getKey(); - Highlight highlight = entry.getValue(); - logger.fine("SolrSearchResult class: " + solrField.getNameSearchable() + ":" + highlight.getSnippets()); - } - - Highlight highlight = highlightsAsMap.get(SearchFields.DESCRIPTION); - if (type.equals("datasets")) { - highlight = highlightsAsMap.get(SearchFields.DATASET_DESCRIPTION); - } - if (highlight != null) { - return highlight.getSnippets(); - } else { - return new ArrayList<>(); - } - } - - public Map getHighlightsMap() { - return highlightsMap; - } - - public void setHighlightsMap(Map highlightsMap) { - this.highlightsMap = highlightsMap; - } - - public List getMatchedFields() { - return matchedFields; - } - - public void setMatchedFields(List matchedFields) { - this.matchedFields = matchedFields; - } - - @Override - public String toString() { - if (this.name != null) { - return this.id + ":" + this.name + ":" + this.entityId; - } else { - return this.id + ":" + this.title + ":" + this.entityId; - } - } - - public JsonArrayBuilder getRelevance() { - JsonArrayBuilder matchedFieldsArray = Json.createArrayBuilder(); - JsonObjectBuilder matchedFieldObject = Json.createObjectBuilder(); - for (Map.Entry entry : highlightsMap.entrySet()) { - SolrField solrField = entry.getKey(); - Highlight snippets = entry.getValue(); - JsonArrayBuilder snippetArrayBuilder = Json.createArrayBuilder(); - JsonObjectBuilder matchedFieldDetails = Json.createObjectBuilder(); - for (String highlight : snippets.getSnippets()) { - snippetArrayBuilder.add(highlight); - } - /** - * @todo for the Search API, it might be nice to return offset numbers rather than html snippets surrounded by span tags or whatever. - * - * That's what the GitHub Search API does: "Requests can opt to receive those text fragments in the response, and every fragment is accompanied - * by numeric offsets identifying the exact location of each matching search term." https://developer.github.com/v3/search/#text-match-metadata - * - * It's not clear if getting the offset values is possible with Solr, however: - * stackoverflow.com/questions/13863118/can-solr-highlighting-also-indicate-the-position-or-offset-of-the-returned-fragments-within-the-original-field - */ - matchedFieldDetails.add("snippets", snippetArrayBuilder); - /** - * @todo In addition to the name of the field used by Solr , it would be nice to show the "friendly" name of the field we show in the GUI. - */ + } + + public Map getHighlightsAsMap() { + return highlightsAsMap; + } + + public void setHighlightsAsMap(Map highlightsAsMap) { + this.highlightsAsMap = highlightsAsMap; + } + + public String getNameHighlightSnippet() { + Highlight highlight = highlightsAsMap.get(SearchFields.NAME); + if (highlight != null) { + String firstSnippet = highlight.getSnippets().get(0); + if (firstSnippet != null) { + return firstSnippet; + } + } + return null; + } + + public String getDataverseAffiliationHighlightSnippet() { + Highlight highlight = highlightsAsMap.get(SearchFields.AFFILIATION); + if (highlight != null) { + String firstSnippet = highlight.getSnippets().get(0); + if (firstSnippet != null) { + return firstSnippet; + } + } + return null; + } + + public String getFileTypeHighlightSnippet() { + Highlight highlight = highlightsAsMap.get(SearchFields.FILE_TYPE_FRIENDLY); + if (highlight != null) { + String firstSnippet = highlight.getSnippets().get(0); + if (firstSnippet != null) { + return firstSnippet; + } + } + return null; + } + + public String getTitleHighlightSnippet() { + /** + * @todo: don't hard-code title, look it up properly... or start + * indexing titles as names: + * https://redmine.hmdc.harvard.edu/issues/3798#note-2 + */ + Highlight highlight = highlightsAsMap.get("title"); + if (highlight != null) { + String firstSnippet = highlight.getSnippets().get(0); + if (firstSnippet != null) { + return firstSnippet; + } + } + return null; + } + + public List getDescriptionSnippets() { + for (Map.Entry entry : highlightsMap.entrySet()) { + SolrField solrField = entry.getKey(); + Highlight highlight = entry.getValue(); + logger.fine("SolrSearchResult class: " + solrField.getNameSearchable() + ":" + highlight.getSnippets()); + } + + Highlight highlight = highlightsAsMap.get(SearchFields.DESCRIPTION); + if (type.equals("datasets")) { + highlight = highlightsAsMap.get(SearchFields.DATASET_DESCRIPTION); + } + if (highlight != null) { + return highlight.getSnippets(); + } else { + return new ArrayList<>(); + } + } + + public Map getHighlightsMap() { + return highlightsMap; + } + + public void setHighlightsMap(Map highlightsMap) { + this.highlightsMap = highlightsMap; + } + + public List getMatchedFields() { + return matchedFields; + } + + public void setMatchedFields(List matchedFields) { + this.matchedFields = matchedFields; + } + + @Override + public String toString() { + if (this.name != null) { + return this.id + ":" + this.name + ":" + this.entityId; + } else { + return this.id + ":" + this.title + ":" + this.entityId; + } + } + + public JsonArrayBuilder getRelevance() { + JsonArrayBuilder matchedFieldsArray = Json.createArrayBuilder(); + JsonObjectBuilder matchedFieldObject = Json.createObjectBuilder(); + for (Map.Entry entry : highlightsMap.entrySet()) { + SolrField solrField = entry.getKey(); + Highlight snippets = entry.getValue(); + JsonArrayBuilder snippetArrayBuilder = Json.createArrayBuilder(); + JsonObjectBuilder matchedFieldDetails = Json.createObjectBuilder(); + for (String highlight : snippets.getSnippets()) { + snippetArrayBuilder.add(highlight); + } + /** + * @todo for the Search API, it might be nice to return offset + * numbers rather than html snippets surrounded by span tags or + * whatever. + * + * That's what the GitHub Search API does: "Requests can opt to + * receive those text fragments in the response, and every fragment + * is accompanied by numeric offsets identifying the exact location + * of each matching search term." + * https://developer.github.com/v3/search/#text-match-metadata + * + * It's not clear if getting the offset values is possible with + * Solr, however: + * stackoverflow.com/questions/13863118/can-solr-highlighting-also-indicate-the-position-or-offset-of-the-returned-fragments-within-the-original-field + */ + matchedFieldDetails.add("snippets", snippetArrayBuilder); + /** + * @todo In addition to the name of the field used by Solr , it + * would be nice to show the "friendly" name of the field we show in + * the GUI. + */ // matchedFieldDetails.add("friendly", "FIXME"); - matchedFieldObject.add(solrField.getNameSearchable(), matchedFieldDetails); - matchedFieldsArray.add(matchedFieldObject); - } - return matchedFieldsArray; - } - - public JsonObject toJsonObject(boolean showRelevance, boolean showEntityIds, boolean showApiUrls) { - return toJsonObject(showRelevance, showEntityIds, showApiUrls, null); - } - - public JsonObject toJsonObject(boolean showRelevance, boolean showEntityIds, boolean showApiUrls, - List metadataFields) { - return json(showRelevance, showEntityIds, showApiUrls, metadataFields).build(); - } - - /** - * Add additional fields for the MyData page - * - * @return - */ - public JsonObjectBuilder getJsonForMyData() { - - JsonObjectBuilder myDataJson = json(true, true, true);// boolean showRelevance, boolean showEntityIds, boolean showApiUrls) - - myDataJson.add("publication_statuses", this.getPublicationStatusesAsJSON()) - .add("is_draft_state", this.isDraftState()).add("is_in_review_state", this.isInReviewState()) - .add("is_unpublished_state", this.isUnpublishedState()).add("is_published", this.isPublishedState()) - .add("is_deaccesioned", this.isDeaccessionedState()) - .add("is_valid", this.isValid()) - .add("date_to_display_on_card", getDateToDisplayOnCard()); - - // Add is_deaccessioned attribute, even though MyData currently screens any deaccessioned info out - // - if ((this.isDeaccessionedState()) && (this.getPublicationStatuses().size() == 1)) { - myDataJson.add("deaccesioned_is_only_pubstatus", true); - } - - if ((this.getParent() != null) && (!this.getParent().isEmpty())) { - // System.out.println("keys:" + parent.keySet().toString()); - if (this.entity.isInstanceofDataFile()) { - myDataJson.add("parentIdentifier", this.getParent().get(SolrSearchResult.PARENT_IDENTIFIER)) - .add("parentName", this.getParent().get("name")); - - } else { - // for Dataverse and Dataset, get parent which is a Dataverse - myDataJson.add("parentId", this.getParent().get("id")).add("parentName", this.getParent().get("name")); - } - } - - return myDataJson; - } // getJsonForMydata - - public JsonObjectBuilder json(boolean showRelevance, boolean showEntityIds, boolean showApiUrls) { - return json(showRelevance, showEntityIds, showApiUrls, null); - } - - public JsonObjectBuilder json(boolean showRelevance, boolean showEntityIds, boolean showApiUrls, - List metadataFields) { - - if (this.type == null) { - return jsonObjectBuilder(); - } - - String displayName = null; - - String identifierLabel = null; - String datasetCitation = null; - String datasetName = null; - String datasetId = null; - String datasetPersistentId = null; - String filePersistentId = null; - String preferredUrl = null; - String apiUrl = null; - String publisherName = null; - - if (this.type.equals(SearchConstants.DATAVERSES)) { - displayName = this.name; - identifierLabel = "identifier"; - preferredUrl = getHtmlUrl(); - } else if (this.type.equals(SearchConstants.DATASETS)) { - displayName = this.title; - identifierLabel = "global_id"; - preferredUrl = getPersistentUrl(); - publisherName = this.parent.get("name"); - // if - /** - * @todo Should we show the name of the parent dataverse? - */ - } else if (this.type.equals(SearchConstants.FILES)) { - displayName = this.name; - identifierLabel = "file_id"; - preferredUrl = getDownloadUrl(); - /** - * @todo show more information for a file's parent, such as the title of the dataset it belongs to. - */ - datasetCitation = parent.get("citation"); - datasetName = parent.get("name"); - datasetId = parent.get("id"); - datasetPersistentId = parent.get(SolrSearchResult.PARENT_IDENTIFIER); - } - - // displayName = null; // testing NullSafeJsonBuilder - // because we are using NullSafeJsonBuilder key/value pairs will be dropped if the value is null - NullSafeJsonBuilder nullSafeJsonBuilder = jsonObjectBuilder().add("name", displayName) - .add("type", getDisplayType(getType())).add("url", preferredUrl).add("image_url", getImageUrl()) - // .add("persistent_url", this.persistentUrl) - // .add("download_url", this.downloadUrl) - /** - * @todo How much value is there in exposing the identifier for dataverses? For - */ - .add(identifierLabel, this.identifier) - /** - * @todo Get dataset description from dsDescriptionValue. Also, is descriptionNoSnippet the right field to use generally? - * - * @todo What about the fact that datasets can now have multiple descriptions? Should we create an array called "additional_descriptions" that gets - * populated if there is more than one dataset description? - * - * @todo Why aren't file descriptions ever null? They always have an empty string at least. - */ - .add("description", this.descriptionNoSnippet) - /** - * @todo In the future we'd like to support non-public datasets per https://github.com/IQSS/dataverse/issues/1299 but for now we are only supporting - * non-public searches. - */ - .add("published_at", getDateTimePublished()) - /** - * @todo Expose MIME Type: https://github.com/IQSS/dataverse/issues/1595 - */ - .add("file_type", this.filetype).add("file_content_type", this.fileContentType) - .add("size_in_bytes", getFileSizeInBytes()) - /** - * "md5" was the only possible value so it's hard-coded here but we might want to deprecate it someday since we now put the MD5 or SHA-1 in - * "checksum". - */ - .add("md5", getFileMd5()) - .add("checksum", JsonPrinter.getChecksumTypeAndValue(getFileChecksumType(), getFileChecksumValue())) - .add("unf", getUnf()).add("file_persistent_id", this.filePersistentId).add("dataset_name", datasetName) - .add("dataset_id", datasetId).add("publisher", publisherName) - .add("dataset_persistent_id", datasetPersistentId).add("dataset_citation", datasetCitation) - .add("deaccession_reason", this.deaccessionReason).add("citationHtml", this.citationHtml) - .add("identifier_of_dataverse", this.identifierOfDataverse) - .add("name_of_dataverse", this.nameOfDataverse).add("citation", this.citation); - // Now that nullSafeJsonBuilder has been instatiated, check for null before adding to it! - if (showRelevance) { - nullSafeJsonBuilder.add("matches", getRelevance()); - nullSafeJsonBuilder.add("score", getScore()); - } - if (showEntityIds) { - if (this.entityId != null) { - nullSafeJsonBuilder.add("entity_id", this.entityId); - } - } - - if (this.entity == null) { - - } else { - if (this.entity.isInstanceofDataset()) { - nullSafeJsonBuilder.add("storageIdentifier", this.entity.getStorageIdentifier()); - Dataset ds = (Dataset) this.entity; - DatasetVersion dv = ds.getVersionFromId(this.datasetVersionId); - - if (!dv.getKeywords().isEmpty()) { - JsonArrayBuilder keyWords = Json.createArrayBuilder(); - for (String keyword : dv.getKeywords()) { - keyWords.add(keyword); - } - nullSafeJsonBuilder.add("keywords", keyWords); - } - - JsonArrayBuilder subjects = Json.createArrayBuilder(); - for (String subject : dv.getDatasetSubjects()) { - subjects.add(subject); - } - nullSafeJsonBuilder.add("subjects", subjects); - nullSafeJsonBuilder.add("fileCount", dv.getFileMetadatas().size()); - nullSafeJsonBuilder.add("versionId", dv.getId()); - nullSafeJsonBuilder.add("versionState", dv.getVersionState().toString()); - if (this.isPublishedState()) { - nullSafeJsonBuilder.add("majorVersion", dv.getVersionNumber()); - nullSafeJsonBuilder.add("minorVersion", dv.getMinorVersionNumber()); - } - - nullSafeJsonBuilder.add("createdAt", ds.getCreateDate()); - nullSafeJsonBuilder.add("updatedAt", ds.getModificationTime()); - - if (!dv.getDatasetContacts().isEmpty()) { - JsonArrayBuilder contacts = Json.createArrayBuilder(); - NullSafeJsonBuilder nullSafeJsonBuilderInner = jsonObjectBuilder(); - for (String contact[] : dv.getDatasetContacts(false)) { - nullSafeJsonBuilderInner.add("name", contact[0]); - nullSafeJsonBuilderInner.add("affiliation", contact[1]); - contacts.add(nullSafeJsonBuilderInner); - } - nullSafeJsonBuilder.add("contacts", contacts); - } - if (!dv.getRelatedPublications().isEmpty()) { - JsonArrayBuilder relPub = Json.createArrayBuilder(); - NullSafeJsonBuilder inner = jsonObjectBuilder(); - for (DatasetRelPublication dsRelPub : dv.getRelatedPublications()) { - inner.add("title", dsRelPub.getTitle()); - inner.add("citation", dsRelPub.getText()); - inner.add("url", dsRelPub.getUrl()); - relPub.add(inner); - } - nullSafeJsonBuilder.add("publications", relPub); - } - - if (!dv.getDatasetProducers().isEmpty()) { - JsonArrayBuilder producers = Json.createArrayBuilder(); - for (String[] producer : dv.getDatasetProducers()) { - producers.add(producer[0]); - } - nullSafeJsonBuilder.add("producers", producers); - } - if (!dv.getRelatedMaterial().isEmpty()) { - JsonArrayBuilder relatedMaterials = Json.createArrayBuilder(); - for (String relatedMaterial : dv.getRelatedMaterial()) { - relatedMaterials.add(relatedMaterial); - } - nullSafeJsonBuilder.add("relatedMaterial", relatedMaterials); - } - - if (!dv.getGeographicCoverage().isEmpty()) { - JsonArrayBuilder geoCov = Json.createArrayBuilder(); - NullSafeJsonBuilder inner = jsonObjectBuilder(); - for (String ind[] : dv.getGeographicCoverage()) { - inner.add("country", ind[0]); - inner.add("state", ind[1]); - inner.add("city", ind[2]); - inner.add("other", ind[3]); - geoCov.add(inner); - } - nullSafeJsonBuilder.add("geographicCoverage", geoCov); - } - if (!dv.getDataSource().isEmpty()) { - JsonArrayBuilder dataSources = Json.createArrayBuilder(); - for (String dsource : dv.getDataSource()) { - dataSources.add(dsource); - } - nullSafeJsonBuilder.add("dataSources", dataSources); - } - - if (CollectionUtils.isNotEmpty(metadataFields)) { - // create metadata fields map names - Map> metadataFieldMapNames = computeRequestedMetadataFieldMapNames( - metadataFields); - - // add metadatafields objet to wrap all requeested fields - NullSafeJsonBuilder metadataFieldBuilder = jsonObjectBuilder(); - - Map> groupedFields = DatasetField - .groupByBlock(dv.getFlatDatasetFields()); - json(metadataFieldMapNames, groupedFields, metadataFieldBuilder); - - nullSafeJsonBuilder.add("metadataBlocks", metadataFieldBuilder); - } - } - } - - if (showApiUrls) { - /** - * @todo We should probably have a metadata_url or api_url concept enabled by default, not hidden behind an undocumented boolean. For datasets, this - * would be http://example.com/api/datasets/10 or whatever (to get more detailed JSON), but right now this requires an API token. Discuss at - * https://docs.google.com/document/d/1d8sT2GLSavgiAuMTVX8KzTCX0lROEET1edhvHHRDZOs/edit?usp=sharing"; - */ - if (getApiUrl() != null) { - nullSafeJsonBuilder.add("api_url", getApiUrl()); - } - } - // NullSafeJsonBuilder is awesome but can't build null safe arrays. :( - if (!datasetAuthors.isEmpty()) { - JsonArrayBuilder authors = Json.createArrayBuilder(); - for (String datasetAuthor : datasetAuthors) { - authors.add(datasetAuthor); - } - nullSafeJsonBuilder.add("authors", authors); - } - return nullSafeJsonBuilder; - } - - private void json(Map> metadataFieldMapNames, - Map> groupedFields, NullSafeJsonBuilder metadataFieldBuilder) { - for (Map.Entry> metadataFieldNamesEntry : metadataFieldMapNames.entrySet()) { - String metadataBlockName = metadataFieldNamesEntry.getKey(); - List metadataBlockFieldNames = metadataFieldNamesEntry.getValue(); - for (MetadataBlock metadataBlock : groupedFields.keySet()) { - if (metadataBlockName.equals(metadataBlock.getName())) { - // create metadataBlock object - NullSafeJsonBuilder metadataBlockBuilder = jsonObjectBuilder(); - metadataBlockBuilder.add("displayName", metadataBlock.getDisplayName()); - JsonArrayBuilder fieldsArray = Json.createArrayBuilder(); - - List datasetFields = groupedFields.get(metadataBlock); - for (DatasetField datasetField : datasetFields) { - if (metadataBlockFieldNames.contains("*") - || metadataBlockFieldNames.contains(datasetField.getDatasetFieldType().getName())) { - if (datasetField.getDatasetFieldType().isCompound() || !datasetField.getDatasetFieldType().isHasParent()) { - JsonObject item = JsonPrinter.json(datasetField); - if (item != null) { - fieldsArray.add(item); - } - } - } - } - // with a fields to hold all requested properties - metadataBlockBuilder.add("fields", fieldsArray); - - metadataFieldBuilder.add(metadataBlock.getName(), metadataBlockBuilder); - } - } - } - } - - private Map> computeRequestedMetadataFieldMapNames(List metadataFields) { - Map> metadataFieldMapNames = new HashMap<>(); - for (String metadataField : metadataFields) { - String parts[] = metadataField.split(":"); - if (parts.length == 2) { - List metadataFieldNames = metadataFieldMapNames.get(parts[0]); - if (metadataFieldNames == null) { - metadataFieldNames = new ArrayList<>(); - metadataFieldMapNames.put(parts[0], metadataFieldNames); - } - metadataFieldNames.add(parts[1]); - } - } - return metadataFieldMapNames; - } - - private String getDateTimePublished() { - String datePublished = null; - if (draftState == false) { - datePublished = releaseOrCreateDate == null ? null : Util.getDateTimeFormat().format(releaseOrCreateDate); - } - return datePublished; - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public Long getEntityId() { - return entityId; - } - - public void setEntityId(Long entityId) { - this.entityId = entityId; - } - - public DvObject getEntity() { - return entity; - } - - public void setEntity(DvObject entity) { - this.entity = entity; - } - - public String getIdentifier() { - return identifier; - } - - public void setIdentifier(String identifier) { - this.identifier = identifier; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getHtmlUrl() { - return htmlUrl; - } - - public void setHtmlUrl(String htmlUrl) { - this.htmlUrl = htmlUrl; - } - - public String getPersistentUrl() { - return persistentUrl; - } - - public void setPersistentUrl(String persistentUrl) { - this.persistentUrl = persistentUrl; - } - - public String getDownloadUrl() { - return downloadUrl; - } - - public void setDownloadUrl(String downloadUrl) { - this.downloadUrl = downloadUrl; - } - - public String getApiUrl() { - return apiUrl; - } - - public void setApiUrl(String apiUrl) { - this.apiUrl = apiUrl; - } - - public String getImageUrl() { - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } - - public DatasetThumbnail getDatasetThumbnail() { - return datasetThumbnail; - } - - public void setDatasetThumbnail(DatasetThumbnail datasetThumbnail) { - this.datasetThumbnail = datasetThumbnail; - } - - public String getQuery() { - return query; - } - - public void setQuery(String query) { - this.query = query; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getDescriptionNoSnippet() { - return descriptionNoSnippet; - } - - public void setDescriptionNoSnippet(String descriptionNoSnippet) { - this.descriptionNoSnippet = descriptionNoSnippet; - } - - public List getDatasetAuthors() { - return datasetAuthors; - } - - public void setDatasetAuthors(List datasetAuthors) { - this.datasetAuthors = datasetAuthors; - } - - public String getDeaccessionReason() { - return deaccessionReason; - } - - public void setDeaccessionReason(String deaccessionReason) { - this.deaccessionReason = deaccessionReason; - } - - public List getHighlightsAsListOrig() { - return highlightsAsList; - } - - public List getHighlightsAsList() { - List filtered = new ArrayList<>(); - for (Highlight highlight : highlightsAsList) { - String field = highlight.getSolrField().getNameSearchable(); - /** - * @todo don't hard code "title" here. And should we collapse name and title together anyway? - */ - if (!field.equals(SearchFields.NAME) && !field.equals(SearchFields.DESCRIPTION) - && !field.equals(SearchFields.DATASET_DESCRIPTION) && !field.equals(SearchFields.AFFILIATION) - && !field.equals("title")) { - filtered.add(highlight); - } - } - return filtered; - } - - public void setHighlightsAsList(List highlightsAsList) { - this.highlightsAsList = highlightsAsList; - } - - public List getFileCategories() { - return fileCategories; - } - - public void setFileCategories(List fileCategories) { - this.fileCategories = fileCategories; - } - - public List getTabularDataTags() { - return tabularDataTags; - } - - public void setTabularDataTags(List tabularDataTags) { - this.tabularDataTags = tabularDataTags; - } - - public Map getParent() { - return parent; - } - - public Long getParentIdAsLong() { - - if (this.getParent() == null) { - return null; - } - if (!this.getParent().containsKey("id")) { - return null; - } - - String parentIdString = getParent().get("id"); - if (parentIdString == null) { - return null; - } - - try { - return Long.parseLong(parentIdString); - } catch (NumberFormatException ex) { - return null; - } - } - - public void setParent(Map parent) { - this.parent = parent; - } - - public String getDataverseAffiliation() { - return dataverseAffiliation; - } - - public void setDataverseAffiliation(String dataverseAffiliation) { - this.dataverseAffiliation = dataverseAffiliation; - } - - public String getCitation() { - return citation; - } - - public void setCitation(String citation) { - this.citation = citation; - } - - public String getCitationHtml() { - return citationHtml; - } - - public void setCitationHtml(String citationHtml) { - this.citationHtml = citationHtml; - } - - public String getFiletype() { - return filetype; - } - - public void setFiletype(String filetype) { - this.filetype = filetype; - } - - public String getFileContentType() { - return fileContentType; - } - - public void setFileContentType(String fileContentType) { - this.fileContentType = fileContentType; - } - - public String getUnf() { - return unf; - } - - public void setUnf(String unf) { - this.unf = unf; - } - - public Long getFileSizeInBytes() { - return fileSizeInBytes; - } - - public void setFileSizeInBytes(Long fileSizeInBytes) { - this.fileSizeInBytes = fileSizeInBytes; - } - - public String getFileMd5() { - if (DataFile.ChecksumType.MD5.equals(getFileChecksumType())) { - return fileMd5; - } else { - return null; - } - } - - public void setFileMd5(String fileMd5) { - this.fileMd5 = fileMd5; - } - - public DataFile.ChecksumType getFileChecksumType() { - return fileChecksumType; - } - - public void setFileChecksumType(DataFile.ChecksumType fileChecksumType) { - this.fileChecksumType = fileChecksumType; - } - - public String getFileChecksumValue() { - return fileChecksumValue; - } - - public void setFileChecksumValue(String fileChecksumValue) { - this.fileChecksumValue = fileChecksumValue; - } - - public String getNameSort() { - return nameSort; - } - - public void setNameSort(String nameSort) { - this.nameSort = nameSort; - } - - public String getStatus() { - return status; - } - - void setStatus(String status) { - this.status = status; - } - - public Date getReleaseOrCreateDate() { - return releaseOrCreateDate; - } - - public void setReleaseOrCreateDate(Date releaseOrCreateDate) { - this.releaseOrCreateDate = releaseOrCreateDate; - } - - public String getDateToDisplayOnCard() { - return DateUtil.formatDate(this.releaseOrCreateDate); - } - - public long getDatasetVersionId() { - return datasetVersionId; - } - - public void setDatasetVersionId(long datasetVersionId) { - this.datasetVersionId = datasetVersionId; - } - - public String getVersionNumberFriendly() { - return versionNumberFriendly; - } - - public void setVersionNumberFriendly(String versionNumberFriendly) { - this.versionNumberFriendly = versionNumberFriendly; - } - - public String getDatasetUrl() { - String failSafeUrl = "/dataset.xhtml?id=" + entityId + "&versionId=" + datasetVersionId; - if (identifier != null) { - /** - * Unfortunately, colons in the globalId (doi:10...) are converted to %3A (doi%3A10...). To prevent this we switched many JSF tags to a plain "a" tag - * with an href as suggested at http://stackoverflow.com/questions/24733959/houtputlink-value-escaped - */ - String badString = "null"; - if (!identifier.contains(badString)) { - if (entity != null && entity instanceof Dataset) { - if (this.isHarvested() && ((Dataset) entity).getHarvestedFrom() != null) { - String remoteArchiveUrl = ((Dataset) entity).getRemoteArchiveURL(); - if (remoteArchiveUrl != null) { - return remoteArchiveUrl; - } - return null; - } - } - if (isDraftState()) { - return "/dataset.xhtml?persistentId=" + identifier + "&version=DRAFT"; - } - return "/dataset.xhtml?persistentId=" + identifier; - } else { - logger.info("Dataset identifier/globalId contains \"" + badString - + "\" perhaps due to https://github.com/IQSS/dataverse/issues/1147 . Fix data in database and reindex. Returning failsafe URL: " - + failSafeUrl); - return failSafeUrl; - } - } else { - logger.info("Dataset identifier/globalId was null. Returning failsafe URL: " + failSafeUrl); - return failSafeUrl; - } - } - - public String getFileParentIdentifier() { - if (entity == null) { - return null; - } - if (entity instanceof DataFile) { - return parent.get(PARENT_IDENTIFIER); // Dataset globalID - } - - return null; - // if (entity) - } - - public String getFilePersistentId() { - return filePersistentId; - } - - public void setFilePersistentId(String pid) { - filePersistentId = pid; - } - - public String getFileUrl() { - // Nothing special needs to be done for harvested file URLs: - // simply directing these to the local dataset.xhtml for this dataset - // will take care of it - because DatasetPage will issue a redirect - // to the remote archive URL. - // This is true AS OF 4.2.4, FEB. 2016! - We'll probably want to make - // .getRemoteArchiveURL() methods, both in DataFile and Dataset objects, - // work again at some point in the future. - /* + matchedFieldObject.add(solrField.getNameSearchable(), matchedFieldDetails); + matchedFieldsArray.add(matchedFieldObject); + } + return matchedFieldsArray; + } + + /** + * Add additional fields for the MyData page + * + * @return + */ + public JsonObjectBuilder getJsonForMyData(boolean isValid) { + + JsonObjectBuilder myDataJson = json(true, true, true);// boolean showRelevance, boolean showEntityIds, boolean showApiUrls) + + myDataJson.add("publication_statuses", this.getPublicationStatusesAsJSON()) + .add("is_draft_state", this.isDraftState()).add("is_in_review_state", this.isInReviewState()) + .add("is_unpublished_state", this.isUnpublishedState()).add("is_published", this.isPublishedState()) + .add("is_deaccesioned", this.isDeaccessionedState()) + .add("is_valid", isValid) + .add("date_to_display_on_card", getDateToDisplayOnCard()); + + // Add is_deaccessioned attribute, even though MyData currently screens any deaccessioned info out + // + if ((this.isDeaccessionedState()) && (this.getPublicationStatuses().size() == 1)) { + myDataJson.add("deaccesioned_is_only_pubstatus", true); + } + + if ((this.getParent() != null) && (!this.getParent().isEmpty())) { + // System.out.println("keys:" + parent.keySet().toString()); + if (this.entity != null && this.entity.isInstanceofDataFile()) { + myDataJson.add("parentIdentifier", this.getParent().get(SolrSearchResult.PARENT_IDENTIFIER)) + .add("parentName", this.getParent().get("name")); + + } else { + // for Dataverse and Dataset, get parent which is a Dataverse + myDataJson.add("parentId", this.getParent().get("id")).add("parentName", this.getParent().get("name")); + } + } + + return myDataJson; + } // getJsonForMydata + + public JsonObjectBuilder json(boolean showRelevance, boolean showEntityIds, boolean showApiUrls) { + return json(showRelevance, showEntityIds, showApiUrls, null, null); + } + + public JsonObjectBuilder json(boolean showRelevance, boolean showEntityIds, boolean showApiUrls, List metadataFields, Long datasetFileCount) { + if (this.type == null) { + return jsonObjectBuilder(); + } + + String displayName = null; + + String identifierLabel = null; + String datasetCitation = null; + String datasetName = null; + String datasetId = null; + String datasetPersistentId = null; + String filePersistentId = null; + String preferredUrl = null; + String apiUrl = null; + String publisherName = null; + + if (this.type.equals(SearchConstants.DATAVERSES)) { + displayName = this.name; + identifierLabel = "identifier"; + preferredUrl = getHtmlUrl(); + } else if (this.type.equals(SearchConstants.DATASETS)) { + displayName = this.title; + identifierLabel = "global_id"; + preferredUrl = getPersistentUrl(); + publisherName = this.parent.get("name"); + // if + /** + * @todo Should we show the name of the parent dataverse? + */ + } else if (this.type.equals(SearchConstants.FILES)) { + displayName = this.name; + identifierLabel = "file_id"; + preferredUrl = getDownloadUrl(); + /** + * @todo show more information for a file's parent, such as the + * title of the dataset it belongs to. + */ + datasetCitation = parent.get("citation"); + datasetName = parent.get("name"); + datasetId = parent.get("id"); + datasetPersistentId = parent.get(SolrSearchResult.PARENT_IDENTIFIER); + } + + // displayName = null; // testing NullSafeJsonBuilder + // because we are using NullSafeJsonBuilder key/value pairs will be dropped if the value is null + NullSafeJsonBuilder nullSafeJsonBuilder = jsonObjectBuilder() + .add("name", displayName) + .add("type", getDisplayType(getType())) + .add("url", preferredUrl) + .add("image_url", getImageUrl()) + // .add("persistent_url", this.persistentUrl) + // .add("download_url", this.downloadUrl) + /** + * @todo How much value is there in exposing the identifier for + * dataverses? For + */ + .add(identifierLabel, this.identifier) + /** + * @todo Get dataset description from dsDescriptionValue. Also, + * is descriptionNoSnippet the right field to use generally? + * + * @todo What about the fact that datasets can now have multiple + * descriptions? Should we create an array called + * "additional_descriptions" that gets populated if there is + * more than one dataset description? + * + * @todo Why aren't file descriptions ever null? They always + * have an empty string at least. + */ + .add("description", this.descriptionNoSnippet) + /** + * @todo In the future we'd like to support non-public datasets + * per https://github.com/IQSS/dataverse/issues/1299 but for now + * we are only supporting non-public searches. + */ + .add("published_at", getDateTimePublished()) + /** + * @todo Expose MIME Type: + * https://github.com/IQSS/dataverse/issues/1595 + */ + .add("file_type", this.filetype) + .add("file_content_type", this.fileContentType) + .add("size_in_bytes", getFileSizeInBytes()) + /** + * "md5" was the only possible value so it's hard-coded here but + * we might want to deprecate it someday since we now put the + * MD5 or SHA-1 in "checksum". + */ + .add("md5", getFileMd5()) + .add("checksum", JsonPrinter.getChecksumTypeAndValue(getFileChecksumType(), getFileChecksumValue())) + .add("unf", getUnf()) + .add("file_persistent_id", this.filePersistentId) + .add("dataset_name", datasetName) + .add("dataset_id", datasetId) + .add("publisher", publisherName) + .add("dataset_persistent_id", datasetPersistentId) + .add("dataset_citation", datasetCitation) + .add("deaccession_reason", this.deaccessionReason) + .add("citationHtml", this.citationHtml) + .add("identifier_of_dataverse", this.identifierOfDataverse) + .add("name_of_dataverse", this.nameOfDataverse) + .add("citation", this.citation); + // Now that nullSafeJsonBuilder has been instatiated, check for null before adding to it! + if (showRelevance) { + nullSafeJsonBuilder.add("matches", getRelevance()); + nullSafeJsonBuilder.add("score", getScore()); + } + if (showEntityIds) { + if (this.entityId != null) { + nullSafeJsonBuilder.add("entity_id", this.entityId); + } + } + if (!getPublicationStatuses().isEmpty()) { + nullSafeJsonBuilder.add("publicationStatuses", getPublicationStatusesAsJSON()); + } + + if (this.entity == null) { + + } else { + if (this.entity.isInstanceofDataset()) { + nullSafeJsonBuilder.add("storageIdentifier", this.entity.getStorageIdentifier()); + Dataset ds = (Dataset) this.entity; + DatasetVersion dv = ds.getVersionFromId(this.datasetVersionId); + + if (!dv.getKeywords().isEmpty()) { + JsonArrayBuilder keyWords = Json.createArrayBuilder(); + for (String keyword : dv.getKeywords()) { + keyWords.add(keyword); + } + nullSafeJsonBuilder.add("keywords", keyWords); + } + + JsonArrayBuilder subjects = Json.createArrayBuilder(); + for (String subject : dv.getDatasetSubjects()) { + subjects.add(subject); + } + nullSafeJsonBuilder.add("subjects", subjects); + nullSafeJsonBuilder.add("fileCount", datasetFileCount); + nullSafeJsonBuilder.add("versionId", dv.getId()); + nullSafeJsonBuilder.add("versionState", dv.getVersionState().toString()); + if (this.isPublishedState()) { + nullSafeJsonBuilder.add("majorVersion", dv.getVersionNumber()); + nullSafeJsonBuilder.add("minorVersion", dv.getMinorVersionNumber()); + } + + nullSafeJsonBuilder.add("createdAt", ds.getCreateDate()); + nullSafeJsonBuilder.add("updatedAt", ds.getModificationTime()); + + if (!dv.getDatasetContacts().isEmpty()) { + JsonArrayBuilder contacts = Json.createArrayBuilder(); + NullSafeJsonBuilder nullSafeJsonBuilderInner = jsonObjectBuilder(); + for (String contact[] : dv.getDatasetContacts(false)) { + nullSafeJsonBuilderInner.add("name", contact[0]); + nullSafeJsonBuilderInner.add("affiliation", contact[1]); + contacts.add(nullSafeJsonBuilderInner); + } + nullSafeJsonBuilder.add("contacts", contacts); + } + if (!dv.getRelatedPublications().isEmpty()) { + JsonArrayBuilder relPub = Json.createArrayBuilder(); + NullSafeJsonBuilder inner = jsonObjectBuilder(); + for (DatasetRelPublication dsRelPub : dv.getRelatedPublications()) { + inner.add("title", dsRelPub.getTitle()); + inner.add("citation", dsRelPub.getText()); + inner.add("url", dsRelPub.getUrl()); + relPub.add(inner); + } + nullSafeJsonBuilder.add("publications", relPub); + } + + if (!dv.getDatasetProducers().isEmpty()) { + JsonArrayBuilder producers = Json.createArrayBuilder(); + for (String[] producer : dv.getDatasetProducers()) { + producers.add(producer[0]); + } + nullSafeJsonBuilder.add("producers", producers); + } + if (!dv.getRelatedMaterial().isEmpty()) { + JsonArrayBuilder relatedMaterials = Json.createArrayBuilder(); + for (String relatedMaterial : dv.getRelatedMaterial()) { + relatedMaterials.add(relatedMaterial); + } + nullSafeJsonBuilder.add("relatedMaterial", relatedMaterials); + } + + if (!dv.getGeographicCoverage().isEmpty()) { + JsonArrayBuilder geoCov = Json.createArrayBuilder(); + NullSafeJsonBuilder inner = jsonObjectBuilder(); + for (String ind[] : dv.getGeographicCoverage()) { + inner.add("country", ind[0]); + inner.add("state", ind[1]); + inner.add("city", ind[2]); + inner.add("other", ind[3]); + geoCov.add(inner); + } + nullSafeJsonBuilder.add("geographicCoverage", geoCov); + } + if (!dv.getDataSource().isEmpty()) { + JsonArrayBuilder dataSources = Json.createArrayBuilder(); + for (String dsource : dv.getDataSource()) { + dataSources.add(dsource); + } + nullSafeJsonBuilder.add("dataSources", dataSources); + } + + if (CollectionUtils.isNotEmpty(metadataFields)) { + // create metadata fields map names + Map> metadataFieldMapNames = computeRequestedMetadataFieldMapNames( + metadataFields); + + // add metadatafields objet to wrap all requeested fields + NullSafeJsonBuilder metadataFieldBuilder = jsonObjectBuilder(); + + Map> groupedFields = DatasetField + .groupByBlock(dv.getFlatDatasetFields()); + json(metadataFieldMapNames, groupedFields, metadataFieldBuilder); + + nullSafeJsonBuilder.add("metadataBlocks", metadataFieldBuilder); + } + } else if (this.entity.isInstanceofDataverse()) { + nullSafeJsonBuilder.add("affiliation", dataverseAffiliation); + nullSafeJsonBuilder.add("parentDataverseName", dataverseParentName); + nullSafeJsonBuilder.add("parentDataverseIdentifier", dataverseParentAlias); + } else if (this.entity.isInstanceofDataFile()) { + // "published_at" field is only set when the version state is not draft. + // On the contrary, this field also takes into account DataFiles in draft version, + // returning the creation date if the DataFile is not published, or the publication date otherwise. + nullSafeJsonBuilder.add("releaseOrCreateDate", getFormattedReleaseOrCreateDate()); + } + } + + if (showApiUrls) { + /** + * @todo We should probably have a metadata_url or api_url concept + * enabled by default, not hidden behind an undocumented boolean. + * For datasets, this would be http://example.com/api/datasets/10 or + * whatever (to get more detailed JSON), but right now this requires + * an API token. Discuss at + * https://docs.google.com/document/d/1d8sT2GLSavgiAuMTVX8KzTCX0lROEET1edhvHHRDZOs/edit?usp=sharing"; + */ + if (getApiUrl() != null) { + nullSafeJsonBuilder.add("api_url", getApiUrl()); + } + } + // NullSafeJsonBuilder is awesome but can't build null safe arrays. :( + if (!datasetAuthors.isEmpty()) { + JsonArrayBuilder authors = Json.createArrayBuilder(); + for (String datasetAuthor : datasetAuthors) { + authors.add(datasetAuthor); + } + nullSafeJsonBuilder.add("authors", authors); + } + return nullSafeJsonBuilder; + } + + private void json(Map> metadataFieldMapNames, + Map> groupedFields, NullSafeJsonBuilder metadataFieldBuilder) { + for (Map.Entry> metadataFieldNamesEntry : metadataFieldMapNames.entrySet()) { + String metadataBlockName = metadataFieldNamesEntry.getKey(); + List metadataBlockFieldNames = metadataFieldNamesEntry.getValue(); + for (MetadataBlock metadataBlock : groupedFields.keySet()) { + if (metadataBlockName.equals(metadataBlock.getName())) { + // create metadataBlock object + NullSafeJsonBuilder metadataBlockBuilder = jsonObjectBuilder(); + metadataBlockBuilder.add("displayName", metadataBlock.getDisplayName()); + JsonArrayBuilder fieldsArray = Json.createArrayBuilder(); + + List datasetFields = groupedFields.get(metadataBlock); + for (DatasetField datasetField : datasetFields) { + if (metadataBlockFieldNames.contains("*") + || metadataBlockFieldNames.contains(datasetField.getDatasetFieldType().getName())) { + if (datasetField.getDatasetFieldType().isCompound() || !datasetField.getDatasetFieldType().isHasParent()) { + JsonObject item = JsonPrinter.json(datasetField); + if (item != null) { + fieldsArray.add(item); + } + } + } + } + // with a fields to hold all requested properties + metadataBlockBuilder.add("fields", fieldsArray); + + metadataFieldBuilder.add(metadataBlock.getName(), metadataBlockBuilder); + } + } + } + } + + private Map> computeRequestedMetadataFieldMapNames(List metadataFields) { + Map> metadataFieldMapNames = new HashMap<>(); + for (String metadataField : metadataFields) { + String parts[] = metadataField.split(":"); + if (parts.length == 2) { + List metadataFieldNames = metadataFieldMapNames.get(parts[0]); + if (metadataFieldNames == null) { + metadataFieldNames = new ArrayList<>(); + metadataFieldMapNames.put(parts[0], metadataFieldNames); + } + metadataFieldNames.add(parts[1]); + } + } + return metadataFieldMapNames; + } + + private String getDateTimePublished() { + String datePublished = null; + if (draftState == false) { + datePublished = getFormattedReleaseOrCreateDate(); + } + return datePublished; + } + + private String getFormattedReleaseOrCreateDate() { + return releaseOrCreateDate == null ? null : Util.getDateTimeFormat().format(releaseOrCreateDate); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Long getEntityId() { + return entityId; + } + + public void setEntityId(Long entityId) { + this.entityId = entityId; + } + + public DvObject getEntity() { + return entity; + } + + public void setEntity(DvObject entity) { + this.entity = entity; + } + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getHtmlUrl() { + return htmlUrl; + } + + public void setHtmlUrl(String htmlUrl) { + this.htmlUrl = htmlUrl; + } + + public String getPersistentUrl() { + return persistentUrl; + } + + public void setPersistentUrl(String persistentUrl) { + this.persistentUrl = persistentUrl; + } + + public String getDownloadUrl() { + return downloadUrl; + } + + public void setDownloadUrl(String downloadUrl) { + this.downloadUrl = downloadUrl; + } + + public String getApiUrl() { + return apiUrl; + } + + public void setApiUrl(String apiUrl) { + this.apiUrl = apiUrl; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public DatasetThumbnail getDatasetThumbnail() { + return datasetThumbnail; + } + + public void setDatasetThumbnail(DatasetThumbnail datasetThumbnail) { + this.datasetThumbnail = datasetThumbnail; + } + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescriptionNoSnippet() { + return descriptionNoSnippet; + } + + public void setDescriptionNoSnippet(String descriptionNoSnippet) { + this.descriptionNoSnippet = descriptionNoSnippet; + } + + public List getDatasetAuthors() { + return datasetAuthors; + } + + public void setDatasetAuthors(List datasetAuthors) { + this.datasetAuthors = datasetAuthors; + } + + public String getDeaccessionReason() { + return deaccessionReason; + } + + public void setDeaccessionReason(String deaccessionReason) { + this.deaccessionReason = deaccessionReason; + } + + public List getHighlightsAsListOrig() { + return highlightsAsList; + } + + public List getHighlightsAsList() { + List filtered = new ArrayList<>(); + for (Highlight highlight : highlightsAsList) { + String field = highlight.getSolrField().getNameSearchable(); + /** + * @todo don't hard code "title" here. And should we collapse name + * and title together anyway? + */ + if (!field.equals(SearchFields.NAME) && !field.equals(SearchFields.DESCRIPTION) + && !field.equals(SearchFields.DATASET_DESCRIPTION) && !field.equals(SearchFields.AFFILIATION) + && !field.equals("title")) { + filtered.add(highlight); + } + } + return filtered; + } + + public void setHighlightsAsList(List highlightsAsList) { + this.highlightsAsList = highlightsAsList; + } + + public List getFileCategories() { + return fileCategories; + } + + public void setFileCategories(List fileCategories) { + this.fileCategories = fileCategories; + } + + public List getTabularDataTags() { + return tabularDataTags; + } + + public void setTabularDataTags(List tabularDataTags) { + this.tabularDataTags = tabularDataTags; + } + + public Map getParent() { + return parent; + } + + public Long getParentIdAsLong() { + + if (this.getParent() == null) { + return null; + } + if (!this.getParent().containsKey("id")) { + return null; + } + + String parentIdString = getParent().get("id"); + if (parentIdString == null) { + return null; + } + + try { + return Long.parseLong(parentIdString); + } catch (NumberFormatException ex) { + return null; + } + } + + public void setParent(Map parent) { + this.parent = parent; + } + + public String getDataverseAffiliation() { + return dataverseAffiliation; + } + + public void setDataverseAffiliation(String dataverseAffiliation) { + this.dataverseAffiliation = dataverseAffiliation; + } + + public String getCitation() { + return citation; + } + + public void setCitation(String citation) { + this.citation = citation; + } + + public String getCitationHtml() { + return citationHtml; + } + + public void setCitationHtml(String citationHtml) { + this.citationHtml = citationHtml; + } + + public String getDatasetType() { + return datasetType; + } + + public void setDatasetType(String datasetType) { + this.datasetType = datasetType; + } + + public String getFiletype() { + return filetype; + } + + public void setFiletype(String filetype) { + this.filetype = filetype; + } + + public String getFileContentType() { + return fileContentType; + } + + public void setFileContentType(String fileContentType) { + this.fileContentType = fileContentType; + } + + public String getUnf() { + return unf; + } + + public void setUnf(String unf) { + this.unf = unf; + } + + public Long getFileSizeInBytes() { + return fileSizeInBytes; + } + + public void setFileSizeInBytes(Long fileSizeInBytes) { + this.fileSizeInBytes = fileSizeInBytes; + } + + public String getFileMd5() { + if (DataFile.ChecksumType.MD5.equals(getFileChecksumType())) { + return fileMd5; + } else { + return null; + } + } + + public void setFileMd5(String fileMd5) { + this.fileMd5 = fileMd5; + } + + public DataFile.ChecksumType getFileChecksumType() { + return fileChecksumType; + } + + public void setFileChecksumType(DataFile.ChecksumType fileChecksumType) { + this.fileChecksumType = fileChecksumType; + } + + public String getFileChecksumValue() { + return fileChecksumValue; + } + + public void setFileChecksumValue(String fileChecksumValue) { + this.fileChecksumValue = fileChecksumValue; + } + + public String getNameSort() { + return nameSort; + } + + public void setNameSort(String nameSort) { + this.nameSort = nameSort; + } + + public String getStatus() { + return status; + } + + void setStatus(String status) { + this.status = status; + } + + public Date getReleaseOrCreateDate() { + return releaseOrCreateDate; + } + + public void setReleaseOrCreateDate(Date releaseOrCreateDate) { + this.releaseOrCreateDate = releaseOrCreateDate; + } + + public String getDateToDisplayOnCard() { + return DateUtil.formatDate(this.releaseOrCreateDate); + } + + public long getDatasetVersionId() { + return datasetVersionId; + } + + public void setDatasetVersionId(long datasetVersionId) { + this.datasetVersionId = datasetVersionId; + } + + public String getVersionNumberFriendly() { + return versionNumberFriendly; + } + + public void setVersionNumberFriendly(String versionNumberFriendly) { + this.versionNumberFriendly = versionNumberFriendly; + } + + public String getDatasetUrl() { + String failSafeUrl = "/dataset.xhtml?id=" + entityId + "&versionId=" + datasetVersionId; + if (identifier != null) { + /** + * Unfortunately, colons in the globalId (doi:10...) are converted + * to %3A (doi%3A10...). To prevent this we switched many JSF tags + * to a plain "a" tag with an href as suggested at + * http://stackoverflow.com/questions/24733959/houtputlink-value-escaped + */ + String badString = "null"; + if (!identifier.contains(badString)) { + if (entity != null && entity instanceof Dataset) { + if (this.isHarvested() && ((Dataset) entity).getHarvestedFrom() != null) { + String remoteArchiveUrl = ((Dataset) entity).getRemoteArchiveURL(); + if (remoteArchiveUrl != null) { + return remoteArchiveUrl; + } + return null; + } + } + if (isDraftState()) { + return "/dataset.xhtml?persistentId=" + identifier + "&version=DRAFT"; + } + return "/dataset.xhtml?persistentId=" + identifier; + } else { + logger.info("Dataset identifier/globalId contains \"" + badString + + "\" perhaps due to https://github.com/IQSS/dataverse/issues/1147 . Fix data in database and reindex. Returning failsafe URL: " + + failSafeUrl); + return failSafeUrl; + } + } else { + logger.info("Dataset identifier/globalId was null. Returning failsafe URL: " + failSafeUrl); + return failSafeUrl; + } + } + + public String getFileParentIdentifier() { + if (entity == null) { + return null; + } + if (entity instanceof DataFile) { + return parent.get(PARENT_IDENTIFIER); // Dataset globalID + } + + return null; + // if (entity) + } + + public String getFilePersistentId() { + return filePersistentId; + } + + public void setFilePersistentId(String pid) { + filePersistentId = pid; + } + + public String getFileUrl() { + // Nothing special needs to be done for harvested file URLs: + // simply directing these to the local dataset.xhtml for this dataset + // will take care of it - because DatasetPage will issue a redirect + // to the remote archive URL. + // This is true AS OF 4.2.4, FEB. 2016! - We'll probably want to make + // .getRemoteArchiveURL() methods, both in DataFile and Dataset objects, + // work again at some point in the future. + /* * if (entity != null && entity instanceof DataFile && this.isHarvested()) { String remoteArchiveUrl = ((DataFile) entity).getRemoteArchiveURL(); if * (remoteArchiveUrl != null) { return remoteArchiveUrl; } return null; } - */ + */ if (entity.getIdentifier() != null) { GlobalId entityPid = entity.getGlobalId(); return "/file.xhtml?persistentId=" + ((entityPid != null) ? entityPid.asString() : null); } - return "/file.xhtml?fileId=" + entity.getId() + "&datasetVersionId=" + datasetVersionId; + return "/file.xhtml?fileId=" + entity.getId() + "&datasetVersionId=" + datasetVersionId; - /* + /* * if (parentDatasetGlobalId != null) { return "/dataset.xhtml?persistentId=" + parentDatasetGlobalId; } else { return "/dataset.xhtml?id=" + * parent.get(SearchFields.ID) + "&versionId=" + datasetVersionId; } - */ - } + */ + } - public String getFileDatasetUrl() { - // See the comment in the getFileUrl() method above. -- L.A. 4.2.4 - /* + public String getFileDatasetUrl() { + // See the comment in the getFileUrl() method above. -- L.A. 4.2.4 + /* * if (entity != null && entity instanceof DataFile && this.isHarvested()) { String remoteArchiveUrl = ((DataFile) entity).getRemoteArchiveURL(); if * (remoteArchiveUrl != null) { return remoteArchiveUrl; } return null; } - */ - - String parentDatasetGlobalId = parent.get(PARENT_IDENTIFIER); - - if (parentDatasetGlobalId != null) { - if (isDraftState()) { - return "/dataset.xhtml?persistentId=" + parentDatasetGlobalId + "&version=DRAFT"; - } else { - return "/dataset.xhtml?persistentId=" + parentDatasetGlobalId; - } - } else { - return "/dataset.xhtml?id=" + parent.get(SearchFields.ID) + "&versionId=" + datasetVersionId; - } - } - - /** - * @return the dataverseAlias - */ - public String getDataverseAlias() { - return dataverseAlias; - } - - /** - * @param dataverseAlias the dataverseAlias to set - */ - public void setDataverseAlias(String dataverseAlias) { - this.dataverseAlias = dataverseAlias; - } - - /** - * @return the dataverseParentAlias - */ - public String getDataverseParentAlias() { - return dataverseParentAlias; - } - - /** - * @param dataverseParentAlias the dataverseParentAlias to set - */ - public void setDataverseParentAlias(String dataverseParentAlias) { - this.dataverseParentAlias = dataverseParentAlias; - } - - public float getScore() { - return score; - } - - public void setScore(float score) { - this.score = score; - } - - private String getDisplayType(String type) { - if (type.equals(SearchConstants.DATAVERSES)) { - return SearchConstants.DATAVERSE; - } else if (type.equals(SearchConstants.DATASETS)) { - return SearchConstants.DATASET; - } else if (type.equals(SearchConstants.FILES)) { - return SearchConstants.FILE; - } else { - return null; - } - } - - /* + */ + + String parentDatasetGlobalId = parent.get(PARENT_IDENTIFIER); + + if (parentDatasetGlobalId != null) { + if (isDraftState()) { + return "/dataset.xhtml?persistentId=" + parentDatasetGlobalId + "&version=DRAFT"; + } else { + return "/dataset.xhtml?persistentId=" + parentDatasetGlobalId; + } + } else { + return "/dataset.xhtml?id=" + parent.get(SearchFields.ID) + "&versionId=" + datasetVersionId; + } + } + + /** + * @return the dataverseAlias + */ + public String getDataverseAlias() { + return dataverseAlias; + } + + /** + * @param dataverseAlias the dataverseAlias to set + */ + public void setDataverseAlias(String dataverseAlias) { + this.dataverseAlias = dataverseAlias; + } + + /** + * @return the dataverseParentAlias + */ + public String getDataverseParentAlias() { + return dataverseParentAlias; + } + + /** + * @param dataverseParentAlias the dataverseParentAlias to set + */ + public void setDataverseParentAlias(String dataverseParentAlias) { + this.dataverseParentAlias = dataverseParentAlias; + } + + /** + * @param dataverseParentName the dataverseParentName to set + */ + public void setDataverseParentName(String dataverseParentName) { + this.dataverseParentName = dataverseParentName; + } + + public float getScore() { + return score; + } + + public void setScore(float score) { + this.score = score; + } + + private String getDisplayType(String type) { + if (type.equals(SearchConstants.DATAVERSES)) { + return SearchConstants.DATAVERSE; + } else if (type.equals(SearchConstants.DATASETS)) { + return SearchConstants.DATASET; + } else if (type.equals(SearchConstants.FILES)) { + return SearchConstants.FILE; + } else { + return null; + } + } + + /* * public JsonArrayBuilder getUserRolesAsJson() { * * JsonArrayBuilder jsonRoleStrings = Json.createArrayBuilder(); for (String role : this.getUserRole()) { jsonRoleStrings.add(role); } return * jsonRoleStrings; } - */ - public List getUserRole() { - return userRole; - } + */ + public List getUserRole() { + return userRole; + } - public void setUserRole(List userRole) { - this.userRole = userRole; - } + public void setUserRole(List userRole) { + this.userRole = userRole; + } - public String getIdentifierOfDataverse() { - return identifierOfDataverse; - } + public String getIdentifierOfDataverse() { + return identifierOfDataverse; + } - public void setIdentifierOfDataverse(String id) { - this.identifierOfDataverse = id; - } + public void setIdentifierOfDataverse(String id) { + this.identifierOfDataverse = id; + } - public String getNameOfDataverse() { - return nameOfDataverse; - } + public String getNameOfDataverse() { + return nameOfDataverse; + } - public void setNameOfDataverse(String id) { - this.nameOfDataverse = id; - } + public void setNameOfDataverse(String id) { + this.nameOfDataverse = id; + } - public String getExternalStatus() { - return externalStatus; - } + public String getExternalStatus() { + return externalStatus; + } - public void setExternalStatus(String externalStatus) { - this.externalStatus = externalStatus; + public void setExternalStatus(String externalStatus) { + this.externalStatus = externalStatus; - } + } - public Long getEmbargoEndDate() { - return embargoEndDate; - } + public Long getEmbargoEndDate() { + return embargoEndDate; + } - public void setEmbargoEndDate(Long embargoEndDate) { - this.embargoEndDate = embargoEndDate; - } + public void setEmbargoEndDate(Long embargoEndDate) { + this.embargoEndDate = embargoEndDate; + } + + public Long getRetentionEndDate() { + return retentionEndDate; + } + + public void setRetentionEndDate(Long retentionEndDate) { + this.retentionEndDate = retentionEndDate; + } - public void setDatasetValid(Boolean datasetValid) { - this.datasetValid = datasetValid == null || Boolean.valueOf(datasetValid); - } + public void setDatasetValid(Boolean datasetValid) { + this.datasetValid = datasetValid == null || Boolean.valueOf(datasetValid); + } - public boolean isValid() { - return datasetValid; + public boolean isValid(Predicate canUpdateDataset) { + if (this.datasetValid) { + return true; + } + if (!this.getType().equals("datasets")) { + return true; + } + if (this.isDraftState()) { + return false; + } + if (!JvmSettings.UI_SHOW_VALIDITY_LABEL_WHEN_PUBLISHED.lookupOptional(Boolean.class).orElse(true)) { + return true; + } + return !canUpdateDataset.test(this); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/search/savedsearch/SavedSearchServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/savedsearch/SavedSearchServiceBean.java index 7fc2bdf79a3..1dd89f75a26 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/savedsearch/SavedSearchServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/savedsearch/SavedSearchServiceBean.java @@ -2,29 +2,28 @@ import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetLinkingDataverse; +import edu.harvard.iq.dataverse.DatasetLinkingServiceBean; import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.DataverseLinkingDataverse; +import edu.harvard.iq.dataverse.DataverseLinkingServiceBean; import edu.harvard.iq.dataverse.DvObject; import edu.harvard.iq.dataverse.DvObjectServiceBean; import edu.harvard.iq.dataverse.EjbDataverseEngine; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; -import edu.harvard.iq.dataverse.search.SearchServiceBean; -import edu.harvard.iq.dataverse.search.SolrQueryResponse; -import edu.harvard.iq.dataverse.search.SolrSearchResult; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import edu.harvard.iq.dataverse.engine.command.impl.DeleteDatasetLinkingDataverseCommand; +import edu.harvard.iq.dataverse.engine.command.impl.DeleteDataverseLinkingDataverseCommand; import edu.harvard.iq.dataverse.engine.command.impl.LinkDatasetCommand; import edu.harvard.iq.dataverse.engine.command.impl.LinkDataverseCommand; import edu.harvard.iq.dataverse.search.SearchException; import edu.harvard.iq.dataverse.search.SearchFields; +import edu.harvard.iq.dataverse.search.SearchServiceBean; +import edu.harvard.iq.dataverse.search.SolrQueryResponse; +import edu.harvard.iq.dataverse.search.SolrSearchResult; import edu.harvard.iq.dataverse.search.SortBy; import edu.harvard.iq.dataverse.util.SystemConfig; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; import jakarta.ejb.EJB; import jakarta.ejb.Schedule; import jakarta.ejb.Stateless; @@ -39,6 +38,12 @@ import jakarta.persistence.TypedQuery; import jakarta.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + @Stateless @Named public class SavedSearchServiceBean { @@ -50,6 +55,10 @@ public class SavedSearchServiceBean { @EJB DvObjectServiceBean dvObjectService; @EJB + protected DatasetLinkingServiceBean dsLinkingService; + @EJB + protected DataverseLinkingServiceBean dvLinkingService; + @EJB EjbDataverseEngine commandEngine; @EJB SystemConfig systemConfig; @@ -96,21 +105,25 @@ public SavedSearch add(SavedSearch toPersist) { try { persisted = em.merge(toPersist); } catch (Exception ex) { - System.out.println("exeption: " + ex); + logger.fine("Failed to add SavedSearch" + ex); } return persisted; } - public boolean delete(long id) { + public boolean delete(long id, boolean unlink) throws SearchException, CommandException { SavedSearch doomed = find(id); boolean wasDeleted = false; if (doomed != null) { - System.out.println("deleting saved search id " + doomed.getId()); + logger.info("Deleting saved search id " + doomed.getId()); + if(unlink) { + DataverseRequest dataverseRequest = new DataverseRequest(doomed.getCreator(), getHttpServletRequest()); + removeLinks(dataverseRequest, doomed); + } em.remove(doomed); em.flush(); wasDeleted = true; } else { - System.out.println("problem deleting saved search id " + id); + logger.info("Problem deleting saved search id " + id); } return wasDeleted; } @@ -240,6 +253,45 @@ public JsonObjectBuilder makeLinksForSingleSavedSearch(DataverseRequest dvReq, S return response; } + /** + * This method to the reverse of a makeLinksForSingleSavedSearch method. + * It removes all Dataset and Dataverse links that match savedSearch's query. + * @param dvReq + * @param savedSearch + * @throws SearchException + * @throws CommandException + */ + public void removeLinks(DataverseRequest dvReq, SavedSearch savedSearch) throws SearchException, CommandException { + logger.fine("UNLINK SAVED SEARCH (" + savedSearch.getId() + ") START search and unlink process"); + Date start = new Date(); + Dataverse linkingDataverse = savedSearch.getDefinitionPoint(); + + SolrQueryResponse queryResponse = findHits(savedSearch); + for (SolrSearchResult solrSearchResult : queryResponse.getSolrSearchResults()) { + + DvObject dvObjectThatDefinitionPointWillLinkTo = dvObjectService.findDvObject(solrSearchResult.getEntityId()); + if (dvObjectThatDefinitionPointWillLinkTo == null) { + continue; + } + + if (dvObjectThatDefinitionPointWillLinkTo.isInstanceofDataverse()) { + Dataverse linkedDataverse = (Dataverse) dvObjectThatDefinitionPointWillLinkTo; + DataverseLinkingDataverse dvld = dvLinkingService.findDataverseLinkingDataverse(linkedDataverse.getId(), linkingDataverse.getId()); + if(dvld != null) { + Dataverse dv = commandEngine.submitInNewTransaction(new DeleteDataverseLinkingDataverseCommand(dvReq, linkingDataverse, dvld, true)); + } + } else if (dvObjectThatDefinitionPointWillLinkTo.isInstanceofDataset()) { + Dataset linkedDataset = (Dataset) dvObjectThatDefinitionPointWillLinkTo; + DatasetLinkingDataverse dsld = dsLinkingService.findDatasetLinkingDataverse(linkedDataset.getId(), linkingDataverse.getId()); + if(dsld != null) { + Dataset ds = commandEngine.submitInNewTransaction(new DeleteDatasetLinkingDataverseCommand(dvReq, linkedDataset, dsld, true)); + } + } + } + + logger.fine("UNLINK SAVED SEARCH (" + savedSearch.getId() + ") total time in ms: " + (new Date().getTime() - start.getTime())); + } + private SolrQueryResponse findHits(SavedSearch savedSearch) throws SearchException { String sortField = SearchFields.TYPE; // first return dataverses, then datasets String sortOrder = SortBy.DESCENDING; diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/ConfigCheckService.java b/src/main/java/edu/harvard/iq/dataverse/settings/ConfigCheckService.java index a2c3f53d59d..96222f40daf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/ConfigCheckService.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/ConfigCheckService.java @@ -1,25 +1,40 @@ package edu.harvard.iq.dataverse.settings; +import edu.harvard.iq.dataverse.MailServiceBean; +import edu.harvard.iq.dataverse.pidproviders.PidProviderFactoryBean; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean.Key; import edu.harvard.iq.dataverse.util.FileUtil; - +import edu.harvard.iq.dataverse.util.MailSessionProducer; import jakarta.annotation.PostConstruct; import jakarta.ejb.DependsOn; import jakarta.ejb.Singleton; import jakarta.ejb.Startup; +import jakarta.inject.Inject; +import jakarta.mail.internet.InternetAddress; + import java.io.IOException; import java.nio.file.FileSystemException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; +import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; @Startup @Singleton -@DependsOn("StartupFlywayMigrator") +@DependsOn({"StartupFlywayMigrator", "PidProviderFactoryBean"}) public class ConfigCheckService { private static final Logger logger = Logger.getLogger(ConfigCheckService.class.getCanonicalName()); + + @Inject + MailSessionProducer mailSessionProducer; + @Inject + MailServiceBean mailService; + @Inject + PidProviderFactoryBean pidProviderFactoryBean; public static class ConfigurationError extends RuntimeException { public ConfigurationError(String message) { @@ -29,11 +44,14 @@ public ConfigurationError(String message) { @PostConstruct public void startup() { - if (!checkSystemDirectories()) { + if (!checkSystemDirectories() || !checkPidProviders()) { throw new ConfigurationError("Not all configuration checks passed successfully. See logs above."); } + + // Only checks resulting in warnings, nothing critical that needs to stop deployment + checkSystemMailSetup(); } - + /** * In this method, we check the existence and write-ability of all important directories we use during * normal operations. It does not include checks for the storage system. If directories are not available, @@ -77,5 +95,56 @@ public boolean checkSystemDirectories() { } return success; } + + /** + * This method is not expected to make a deployment fail, but send out clear warning messages about missing or + * wrong configuration settings. + */ + public void checkSystemMailSetup() { + // Check if a system mail setting has been provided or issue warning about disabled mail notifications + Optional mailAddress = mailService.getSystemAddress(); + + // Not present -> warning + if (mailAddress.isEmpty()) { + logger.warning("Could not find a system mail setting in database (key :" + Key.SystemEmail + ", deprecated) or JVM option '" + JvmSettings.SYSTEM_EMAIL.getScopedKey() + "'"); + logger.warning("Mail notifications and system messages are deactivated until you provide a configuration"); + } + + // If there is an app server provided mail config, let's determine if the setup is matching + // TODO: when support for appserver provided mail session goes away, this code can be deleted + if (mailSessionProducer.hasSessionFromAppServer()) { + if (mailAddress.isEmpty()) { + logger.warning("Found a mail session provided by app server, but no system mail address (see logs above)"); + // Check if the "from" in the session is the same as the system mail address (see issue 4210) + } else { + String sessionFrom = mailSessionProducer.getSession().getProperty("mail.from"); + if (! mailAddress.get().toString().equals(sessionFrom)) { + logger.warning(() -> String.format( + "Found app server mail session provided 'from' (%s) does not match system mail setting (%s)", + sessionFrom, mailAddress.get())); + } + } + } + } + /** + * Verifies that at least one PidProvider capable of editing/minting PIDs is + * configured. Requires the @DependsOn("PidProviderFactoryBean") annotation above + * since it is the @PostCOnstruct init() method of that class that loads the PidProviders + * + * @return True if all checks successful, false otherwise. + */ + private boolean checkPidProviders() { + // Check if at least one PidProvider capable of editing/minting PIDs is configured. + boolean valid=true; + if(!(PidUtil.getManagedProviderIds().size() > 0)) { + valid = false; + logger.warning("No PID providers configured"); + } + if (pidProviderFactoryBean.getDefaultPidGenerator()==null){ + valid=false; + logger.warning("No default PID provider configured"); + } + return valid; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java index afa5a1c986a..33e828e619d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java @@ -36,6 +36,75 @@ public enum FeatureFlags { * @since Dataverse @TODO: */ API_BEARER_AUTH("api-bearer-auth"), + /** + * For published (public) objects, don't use a join when searching Solr. + * Experimental! Requires a reindex with the following feature flag enabled, + * in order to add the boolean publicObject_b:true field to all the public + * Solr documents. + * + * @apiNote Raise flag by setting + * "dataverse.feature.avoid-expensive-solr-join" + * @since Dataverse 6.3 + */ + AVOID_EXPENSIVE_SOLR_JOIN("avoid-expensive-solr-join"), + /** + * With this flag enabled, the boolean field publicObject_b:true will be + * added to all the indexed Solr documents for publicly-available collections, + * datasets and files. This flag makes it possible to rely on it in searches, + * instead of the very expensive join (the feature flag above). + * + * @apiNote Raise flag by setting + * "dataverse.feature.add-publicobject-solr-field" + * @since Dataverse 6.3 + */ + ADD_PUBLICOBJECT_SOLR_FIELD("add-publicobject-solr-field"), + /** + * With this flag set, Dataverse will index the actual origin of harvested + * metadata records, instead of the "Harvested" string in all cases. + * + * @apiNote Raise flag by setting + * "dataverse.feature.index-harvested-metadata-source" + * @since Dataverse 6.3 + */ + INDEX_HARVESTED_METADATA_SOURCE("index-harvested-metadata-source"), + + /** + * Dataverse normally deletes all solr documents related to a dataset's files + * when the dataset is reindexed. With this flag enabled, additional logic is + * added to the reindex process to delete only the solr documents that are no + * longer needed. (Required docs will be updated rather than deleted and + * replaced.) Enabling this feature flag should make the reindex process + * faster without impacting the search results. + * + * @apiNote Raise flag by setting + * "dataverse.feature.reduce-solr-deletes" + * @since Dataverse 6.3 + */ + REDUCE_SOLR_DELETES("reduce-solr-deletes"), + /** + * With this flag enabled, the Return To Author pop-up will not have a required + * "Reason" field, and a reason will not be required in the + * /api/datasets/{id}/returnToAuthor api call. + * + * @apiNote Raise flag by setting + * "dataverse.feature.disable-return-to-author-reason" + * @since Dataverse 6.3 + */ + DISABLE_RETURN_TO_AUTHOR_REASON("disable-return-to-author-reason"), + /** + * This flag disables the feature that automatically selects one of the + * DataFile thumbnails in the dataset/version as the dedicated thumbnail + * for the dataset. + * + * @apiNote Raise flag by setting + * "dataverse.feature.enable-dataset-thumbnail-autoselect" + * @since Dataverse 6.4 + */ + DISABLE_DATASET_THUMBNAIL_AUTOSELECT("disable-dataset-thumbnail-autoselect"), + /** + * Feature flag for the new Globus upload framework. + */ + GLOBUS_USE_EXPERIMENTAL_ASYNC_FRAMEWORK("globus-use-experimental-async-framework"), ; final String flag; diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java index 3bc06738a7e..d7eea970b8a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java @@ -51,6 +51,11 @@ public enum JvmSettings { DOCROOT_DIRECTORY(SCOPE_FILES, "docroot"), GUESTBOOK_AT_REQUEST(SCOPE_FILES, "guestbook-at-request"), GLOBUS_CACHE_MAXAGE(SCOPE_FILES, "globus-cache-maxage"), + GLOBUS_TASK_MONITORING_SERVER(SCOPE_FILES, "globus-monitoring-server"), + + //STORAGE DRIVER SETTINGS + SCOPE_DRIVER(SCOPE_FILES), + DISABLE_S3_TAGGING(SCOPE_DRIVER, "disable-tagging"), // SOLR INDEX SETTINGS SCOPE_SOLR(PREFIX, "solr"), @@ -60,6 +65,10 @@ public enum JvmSettings { SOLR_CORE(SCOPE_SOLR, "core"), SOLR_PATH(SCOPE_SOLR, "path"), + // INDEX CONCURENCY + SCOPE_SOLR_CONCURENCY(SCOPE_SOLR, "concurrency"), + MAX_ASYNC_INDEXES(SCOPE_SOLR_CONCURENCY, "max-async-indexes"), + // RSERVE CONNECTION SCOPE_RSERVE(PREFIX, "rserve"), RSERVE_HOST(SCOPE_RSERVE, "host"), @@ -91,40 +100,125 @@ public enum JvmSettings { // PERSISTENT IDENTIFIER SETTINGS SCOPE_PID(PREFIX, "pid"), - - // PROVIDER EZID (legacy) - these settings were formerly kept together with DataCite ones - SCOPE_PID_EZID(SCOPE_PID, "ezid"), - EZID_API_URL(SCOPE_PID_EZID, "api-url", "doi.baseurlstring"), - EZID_USERNAME(SCOPE_PID_EZID, "username", "doi.username"), - EZID_PASSWORD(SCOPE_PID_EZID, "password", "doi.password"), + PID_PROVIDERS(SCOPE_PID, "providers"), + PID_DEFAULT_PROVIDER(SCOPE_PID, "default-provider"), + SCOPE_PID_PROVIDER(SCOPE_PID), + PID_PROVIDER_TYPE(SCOPE_PID_PROVIDER, "type"), + PID_PROVIDER_LABEL(SCOPE_PID_PROVIDER, "label"), + PID_PROVIDER_AUTHORITY(SCOPE_PID_PROVIDER, "authority"), + PID_PROVIDER_SHOULDER(SCOPE_PID_PROVIDER, "shoulder"), + PID_PROVIDER_IDENTIFIER_GENERATION_STYLE(SCOPE_PID_PROVIDER, "identifier-generation-style"), + PID_PROVIDER_DATAFILE_PID_FORMAT(SCOPE_PID_PROVIDER, "datafile-pid-format"), + PID_PROVIDER_MANAGED_LIST(SCOPE_PID_PROVIDER, "managed-list"), + PID_PROVIDER_EXCLUDED_LIST(SCOPE_PID_PROVIDER, "excluded-list"), + + + // PROVIDER EZID - these settings were formerly kept together with DataCite ones + SCOPE_PID_EZID(SCOPE_PID_PROVIDER, "ezid"), + EZID_API_URL(SCOPE_PID_EZID, "api-url"), + EZID_USERNAME(SCOPE_PID_EZID, "username"), + EZID_PASSWORD(SCOPE_PID_EZID, "password"), // PROVIDER DATACITE - SCOPE_PID_DATACITE(SCOPE_PID, "datacite"), - DATACITE_MDS_API_URL(SCOPE_PID_DATACITE, "mds-api-url", "doi.baseurlstring"), - DATACITE_REST_API_URL(SCOPE_PID_DATACITE, "rest-api-url", "doi.dataciterestapiurlstring", "doi.mdcbaseurlstring"), - DATACITE_USERNAME(SCOPE_PID_DATACITE, "username", "doi.username"), - DATACITE_PASSWORD(SCOPE_PID_DATACITE, "password", "doi.password"), - + SCOPE_PID_DATACITE(SCOPE_PID_PROVIDER, "datacite"), + DATACITE_MDS_API_URL(SCOPE_PID_DATACITE, "mds-api-url"), + DATACITE_REST_API_URL(SCOPE_PID_DATACITE, "rest-api-url"), + DATACITE_USERNAME(SCOPE_PID_DATACITE, "username"), + DATACITE_PASSWORD(SCOPE_PID_DATACITE, "password"), + + // PROVIDER CROSSREF + SCOPE_PID_CROSSREF(SCOPE_PID_PROVIDER, "crossref"), + CROSSREF_URL(SCOPE_PID_CROSSREF, "url"), + CROSSREF_REST_API_URL(SCOPE_PID_CROSSREF, "rest-api-url"), + CROSSREF_USERNAME(SCOPE_PID_CROSSREF, "username"), + CROSSREF_PASSWORD(SCOPE_PID_CROSSREF, "password"), + CROSSREF_DEPOSITOR(SCOPE_PID_CROSSREF, "depositor"), + CROSSREF_DEPOSITOR_EMAIL(SCOPE_PID_CROSSREF, "depositor-email"), + // PROVIDER PERMALINK - SCOPE_PID_PERMALINK(SCOPE_PID, "permalink"), - PERMALINK_BASEURL(SCOPE_PID_PERMALINK, "base-url", "perma.baseurlstring"), + SCOPE_PID_PERMALINK(SCOPE_PID_PROVIDER, "permalink"), + PERMALINK_BASE_URL(SCOPE_PID_PERMALINK, "base-url"), + PERMALINK_SEPARATOR(SCOPE_PID_PERMALINK, "separator"), // PROVIDER HANDLE - SCOPE_PID_HANDLENET(SCOPE_PID, "handlenet"), - HANDLENET_INDEX(SCOPE_PID_HANDLENET, "index", "dataverse.handlenet.index"), + SCOPE_PID_HANDLENET(SCOPE_PID_PROVIDER, "handlenet"), + HANDLENET_INDEX(SCOPE_PID_HANDLENET, "index"), + HANDLENET_INDEPENDENT_SERVICE(SCOPE_PID_HANDLENET, "independent-service"), + HANDLENET_AUTH_HANDLE(SCOPE_PID_HANDLENET, "auth-handle"), SCOPE_PID_HANDLENET_KEY(SCOPE_PID_HANDLENET, "key"), - HANDLENET_KEY_PATH(SCOPE_PID_HANDLENET_KEY, "path", "dataverse.handlenet.admcredfile"), - HANDLENET_KEY_PASSPHRASE(SCOPE_PID_HANDLENET_KEY, "passphrase", "dataverse.handlenet.admprivphrase"), + HANDLENET_KEY_PATH(SCOPE_PID_HANDLENET_KEY, "path"), + HANDLENET_KEY_PASSPHRASE(SCOPE_PID_HANDLENET_KEY, "passphrase"), + + /* + * The deprecated legacy settings below are from when you could only have a + * single PIDProvider. They mirror the settings above, but are global,not within + * the SCOPE_PID_PROVIDER of an individual provider. + */ + /** + * DEPRECATED PROVIDER DATACITE + * + * @deprecated - legacy single provider setting providing backward compatibility + */ + @Deprecated(forRemoval = true, since = "2024-02-13") + SCOPE_LEGACY_PID_DATACITE(SCOPE_PID, "datacite"), + LEGACY_DATACITE_MDS_API_URL(SCOPE_LEGACY_PID_DATACITE, "mds-api-url", "doi.baseurlstring"), + LEGACY_DATACITE_REST_API_URL(SCOPE_LEGACY_PID_DATACITE, "rest-api-url", "doi.dataciterestapiurlstring", + "doi.mdcbaseurlstring"), + LEGACY_DATACITE_USERNAME(SCOPE_LEGACY_PID_DATACITE, "username", "doi.username"), + LEGACY_DATACITE_PASSWORD(SCOPE_LEGACY_PID_DATACITE, "password", "doi.password"), + + /** + * DEPRECATED PROVIDER EZID + * + * @deprecated - legacy single provider setting providing backward compatibility + */ + @Deprecated(forRemoval = true, since = "2024-02-13") + SCOPE_LEGACY_PID_EZID(SCOPE_PID, "ezid"), LEGACY_EZID_API_URL(SCOPE_LEGACY_PID_EZID, "api-url"), + LEGACY_EZID_USERNAME(SCOPE_LEGACY_PID_EZID, "username"), LEGACY_EZID_PASSWORD(SCOPE_LEGACY_PID_EZID, "password"), + + /** + * DEPRECATED PROVIDER PERMALINK + * + * @deprecated - legacy single provider setting providing backward compatibility + */ + @Deprecated(forRemoval = true, since = "2024-02-13") + SCOPE_LEGACY_PID_PERMALINK(SCOPE_PID, "permalink"), + LEGACY_PERMALINK_BASEURL(SCOPE_LEGACY_PID_PERMALINK, "base-url", "perma.baseurlstring"), + + /** + * DEPRECATED PROVIDER HANDLE + * + * @deprecated - legacy single provider setting providing backward compatibility + */ + @Deprecated(forRemoval = true, since = "2024-02-13") + SCOPE_LEGACY_PID_HANDLENET(SCOPE_PID, "handlenet"), + LEGACY_HANDLENET_INDEX(SCOPE_LEGACY_PID_HANDLENET, "index", "dataverse.handlenet.index"), + @Deprecated(forRemoval = true, since = "2024-02-13") + SCOPE_LEGACY_PID_HANDLENET_KEY(SCOPE_LEGACY_PID_HANDLENET, "key"), + LEGACY_HANDLENET_KEY_PATH(SCOPE_LEGACY_PID_HANDLENET_KEY, "path", "dataverse.handlenet.admcredfile"), + LEGACY_HANDLENET_KEY_PASSPHRASE(SCOPE_LEGACY_PID_HANDLENET_KEY, "passphrase", "dataverse.handlenet.admprivphrase"), // SPI SETTINGS SCOPE_SPI(PREFIX, "spi"), SCOPE_EXPORTERS(SCOPE_SPI, "exporters"), EXPORTERS_DIRECTORY(SCOPE_EXPORTERS, "directory"), + SCOPE_PIDPROVIDERS(SCOPE_SPI, "pidproviders"), + PIDPROVIDERS_DIRECTORY(SCOPE_PIDPROVIDERS, "directory"), // MAIL SETTINGS SCOPE_MAIL(PREFIX, "mail"), + SYSTEM_EMAIL(SCOPE_MAIL, "system-email"), SUPPORT_EMAIL(SCOPE_MAIL, "support-email"), CC_SUPPORT_ON_CONTACT_EMAIL(SCOPE_MAIL, "cc-support-on-contact-email"), + MAIL_DEBUG(SCOPE_MAIL, "debug"), + // Mail Transfer Agent settings + SCOPE_MAIL_MTA(SCOPE_MAIL, "mta"), + MAIL_MTA_AUTH(SCOPE_MAIL_MTA, "auth"), + MAIL_MTA_USER(SCOPE_MAIL_MTA, "user"), + MAIL_MTA_PASSWORD(SCOPE_MAIL_MTA, "password"), + MAIL_MTA_SUPPORT_UTF8(SCOPE_MAIL_MTA, "allow-utf8-addresses"), + // Placeholder setting for a large list of extra settings + MAIL_MTA_SETTING(SCOPE_MAIL_MTA), // AUTH SETTINGS SCOPE_AUTH(PREFIX, "auth"), @@ -146,6 +240,7 @@ public enum JvmSettings { SCOPE_UI(PREFIX, "ui"), UI_ALLOW_REVIEW_INCOMPLETE(SCOPE_UI, "allow-review-for-incomplete"), UI_SHOW_VALIDITY_FILTER(SCOPE_UI, "show-validity-filter"), + UI_SHOW_VALIDITY_LABEL_WHEN_PUBLISHED(SCOPE_UI, "show-validity-label-when-published"), // NetCDF SETTINGS SCOPE_NETCDF(PREFIX, "netcdf"), diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java index 627cef08d8b..8ed96690e84 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java @@ -45,6 +45,7 @@ public class SettingsServiceBean { * over your shoulder when typing strings in various places of a large app. * So there. */ + @SuppressWarnings("java:S115") public enum Key { AllowApiTokenLookupViaApi, /** @@ -54,6 +55,10 @@ public enum Key { CustomDatasetSummaryFields, /** * Defines a public installation -- all datafiles are unrestricted + * + * This was added along with CloudEnvironmentName and ComputeBaseUrl. + * See https://github.com/IQSS/dataverse/issues/3776 and + * https://github.com/IQSS/dataverse/pull/3967 */ PublicInstall, /** @@ -74,9 +79,12 @@ public enum Key { /** * For example, https://datacapture.example.org */ + @Deprecated(forRemoval = true, since = "2024-07-07") DataCaptureModuleUrl, + @Deprecated(forRemoval = true, since = "2024-07-07") RepositoryStorageAbstractionLayerUrl, UploadMethods, + @Deprecated(forRemoval = true, since = "2024-07-07") DownloadMethods, /** * If the data replicated around the world using RSAL (Repository @@ -86,7 +94,17 @@ public enum Key { * TODO: Think about if it makes sense to make this a column in the * StorageSite database table. */ + @Deprecated(forRemoval = true, since = "2024-07-07") LocalDataAccessPath, + /** + * The algorithm used to generate PIDs, randomString (default) or + * storedProcedure + * + * @deprecated New installations should not use this database setting, but use + * the settings within {@link JvmSettings#SCOPE_PID}. + * + */ + @Deprecated(forRemoval = true, since = "2024-02-13") IdentifierGenerationStyle, OAuth2CallbackUrl, DefaultAuthProvider, @@ -189,24 +207,51 @@ public enum Key { SignUpUrl, /** Key for whether we allow users to sign up */ AllowSignUp, - /** protocol for global id */ + /** + * protocol for global id + * + * @deprecated New installations should not use this database setting, but use + * the settings within {@link JvmSettings#SCOPE_PID}. + * + */ + @Deprecated(forRemoval = true, since = "2024-02-13") Protocol, - /** authority for global id */ + /** + * authority for global id + * + * @deprecated New installations should not use this database setting, but use + * the settings within {@link JvmSettings#SCOPE_PID}. + * + */ + @Deprecated(forRemoval = true, since = "2024-02-13") Authority, - /** DoiProvider for global id */ + /** + * DoiProvider for global id + * + * @deprecated New installations should not use this database setting, but use + * the settings within {@link JvmSettings#SCOPE_PID}. + * + */ + @Deprecated(forRemoval = true, since = "2024-02-13") DoiProvider, - /** Shoulder for global id - used to create a common prefix on identifiers */ + /** + * Shoulder for global id - used to create a common prefix on identifiers + * + * @deprecated New installations should not use this database setting, but use + * the settings within {@link JvmSettings#SCOPE_PID}. + * + */ + @Deprecated(forRemoval = true, since = "2024-02-13") Shoulder, - /* Removed for now - tried to add here but DOI Service Bean didn't like it at start-up - DoiUsername, - DoiPassword, - DoiBaseurlstring, - */ /** Optionally override http://guides.dataverse.org . */ GuidesBaseUrl, CVocConf, + // Default calls per hour for each tier. csv format (30,60,...) + RateLimitingDefaultCapacityTiers, + // json defined list of capacities by tier and action list. See RateLimitSetting.java + RateLimitingCapacityByTierAndAction, /** * A link to an installation of https://github.com/IQSS/miniverse or * some other metrics app. @@ -227,7 +272,12 @@ public enum Key { /* the number of files the GUI user is allowed to upload in one batch, via drag-and-drop, or through the file select dialog */ MultipleUploadFilesLimit, - /* return email address for system emails such as notifications */ + /** + * Return email address for system emails such as notifications + * @deprecated Please replace usages with {@link edu.harvard.iq.dataverse.MailServiceBean#getSystemAddress}, + * which is backward compatible with this setting. + */ + @Deprecated(since = "6.2", forRemoval = true) SystemEmail, /* size limit for Tabular data file ingests */ /* (can be set separately for specific ingestable formats; in which @@ -347,10 +397,16 @@ Whether Harvesting (OAI) service is enabled */ PVCustomPasswordResetAlertMessage, /* - String to describe DOI format for data files. Default is DEPENDENT. - 'DEPENEDENT' means the DOI will be the Dataset DOI plus a file DOI with a slash in between. - 'INDEPENDENT' means a new global id, completely independent from the dataset-level global id. - */ + * String to describe DOI format for data files. Default is DEPENDENT. + * 'DEPENDENT' means the DOI will be the Dataset DOI plus a file DOI with a + * slash in between. 'INDEPENDENT' means a new global id, completely independent + * from the dataset-level global id. + * + * @deprecated New installations should not use this database setting, but use + * the settings within {@link JvmSettings#SCOPE_PID}. + * + */ + @Deprecated(forRemoval = true, since = "2024-02-13") DataFilePIDFormat, /* Json array of supported languages */ @@ -358,7 +414,7 @@ Whether Harvesting (OAI) service is enabled /* Number for the minimum number of files to send PID registration to asynchronous workflow */ - PIDAsynchRegFileCount, + //PIDAsynchRegFileCount, /** * */ @@ -366,12 +422,22 @@ Whether Harvesting (OAI) service is enabled /** * Indicates if the Handle service is setup to work 'independently' (No communication with the Global Handle Registry) + * + * @deprecated New installations should not use this database setting, but use + * the settings within {@link JvmSettings#SCOPE_PID}. + * */ + @Deprecated(forRemoval = true, since = "2024-02-13") IndependentHandleService, /** Handle to use for authentication if the default is not being used - */ + * + * @deprecated New installations should not use this database setting, but use + * the settings within {@link JvmSettings#SCOPE_PID}. + * + */ + @Deprecated(forRemoval = true, since = "2024-02-13") HandleAuthHandle, /** @@ -528,6 +594,12 @@ Whether Harvesting (OAI) service is enabled * n: embargo enabled with n months the maximum allowed duration */ MaxEmbargoDurationInMonths, + /** This setting enables Retention capabilities in Dataverse and sets the minimum Retention duration allowed. + * 0 or not set: new retentions disabled + * -1: retention enabled, no time limit + * n: retention enabled with n months the minimum allowed duration + */ + MinRetentionDurationInMonths, /* * Include "Custom Terms" as an item in the license drop-down or not. */ @@ -598,7 +670,15 @@ Whether Harvesting (OAI) service is enabled * Allows an instance admin to disable Solr search facets on the collection * and dataset pages instantly */ - DisableSolrFacets + DisableSolrFacets, + DisableSolrFacetsForGuestUsers, + DisableSolrFacetsWithoutJsession, + DisableUncheckedTypesFacet, + /** + * When ingesting tabular data files, store the generated tab-delimited + * files *with* the variable names line up top. + */ + StoreIngestedTabularFilesWithVarHeaders ; @Override diff --git a/src/main/java/edu/harvard/iq/dataverse/sitemap/SiteMapUtil.java b/src/main/java/edu/harvard/iq/dataverse/sitemap/SiteMapUtil.java index 86ae697f771..8408e7d91f2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/sitemap/SiteMapUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/sitemap/SiteMapUtil.java @@ -1,194 +1,140 @@ package edu.harvard.iq.dataverse.sitemap; -import edu.harvard.iq.dataverse.Dataset; -import edu.harvard.iq.dataverse.Dataverse; -import edu.harvard.iq.dataverse.DvObjectContainer; -import edu.harvard.iq.dataverse.settings.ConfigCheckService; -import edu.harvard.iq.dataverse.settings.JvmSettings; -import edu.harvard.iq.dataverse.util.SystemConfig; -import edu.harvard.iq.dataverse.util.xml.XmlValidator; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; -import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.text.SimpleDateFormat; +import java.text.ParseException; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.logging.Logger; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.OutputKeys; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerConfigurationException; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.xml.sax.SAXException; + +import com.redfin.sitemapgenerator.W3CDateFormat; +import com.redfin.sitemapgenerator.W3CDateFormat.Pattern; +import com.redfin.sitemapgenerator.WebSitemapGenerator; +import com.redfin.sitemapgenerator.WebSitemapUrl; + +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.DvObjectContainer; +import edu.harvard.iq.dataverse.settings.ConfigCheckService; +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.util.SystemConfig; public class SiteMapUtil { + static final String DATE_PATTERN = "yyyy-MM-dd"; + static final String SITEMAP_FILENAME_STAGED = "sitemap.xml.staged"; + /** @see https://www.sitemaps.org/protocol.html#index */ + static final int SITEMAP_LIMIT = 50000; + private static final Logger logger = Logger.getLogger(SiteMapUtil.class.getCanonicalName()); + private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_PATTERN); - static final String SITEMAP_FILENAME_FINAL = "sitemap.xml"; - static final String SITEMAP_FILENAME_STAGED = "sitemap.xml.staged"; - /** - * TODO: Handle more than 50,000 entries in the sitemap. - * - * (As of this writing Harvard Dataverse only has ~3000 dataverses and - * ~30,000 datasets.) - * - * "each Sitemap file that you provide must have no more than 50,000 URLs" - * https://www.sitemaps.org/protocol.html - * - * Consider using a third party library: "One sitemap can contain a maximum - * of 50,000 URLs. (Some sitemaps, like Google News sitemaps, can contain - * only 1,000 URLs.) If you need to put more URLs than that in a sitemap, - * you'll have to use a sitemap index file. Fortunately, WebSitemapGenerator - * can manage the whole thing for you." - * https://github.com/dfabulich/sitemapgen4j - */ public static void updateSiteMap(List dataverses, List datasets) { logger.info("BEGIN updateSiteMap"); - String sitemapPathString = getSitemapPathString(); - String stagedSitemapPathAndFileString = sitemapPathString + File.separator + SITEMAP_FILENAME_STAGED; - String finalSitemapPathAndFileString = sitemapPathString + File.separator + SITEMAP_FILENAME_FINAL; - - Path stagedPath = Paths.get(stagedSitemapPathAndFileString); - if (Files.exists(stagedPath)) { - logger.warning("Unable to update sitemap! The staged file from a previous run already existed. Delete " + stagedSitemapPathAndFileString + " and try again."); + final String dataverseSiteUrl = SystemConfig.getDataverseSiteUrlStatic(); + final String msgErrorFormat = "Problem with %s : %s. The exception is %s"; + final String msgErrorW3CFormat = "%s isn't a valid W3C date time for %s. The exception is %s"; + final String sitemapPathString = getSitemapPathString(); + final String stagedSitemapPathAndFileString = sitemapPathString + File.separator + SITEMAP_FILENAME_STAGED; + final Path stagedSitemapPath = Paths.get(stagedSitemapPathAndFileString); + + if (Files.exists(stagedSitemapPath)) { + logger.warning(String.format( + "Unable to update sitemap! The staged file from a previous run already existed. Delete %s and try again.", + stagedSitemapPathAndFileString)); return; } - DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder documentBuilder = null; + final File directory = new File(sitemapPathString); + if (!directory.exists()) { + directory.mkdir(); + } + + // Use DAY pattern (YYYY-MM-DD), local machine timezone + final W3CDateFormat dateFormat = new W3CDateFormat(Pattern.DAY); + WebSitemapGenerator wsg = null; try { - documentBuilder = documentBuilderFactory.newDocumentBuilder(); - } catch (ParserConfigurationException ex) { - logger.warning("Unable to update sitemap! ParserConfigurationException: " + ex.getLocalizedMessage()); + // All sitemap files are in "sitemap" folder, see "getSitemapPathString" method. + // But with pretty-faces configuration, "sitemap.xml" and "sitemap_index.xml" are accessible directly, + // like "https://demo.dataverse.org/sitemap.xml". So "/sitemap/" need to be added on "WebSitemapGenerator" + // in order to have valid URL for sitemap location. + wsg = WebSitemapGenerator.builder(dataverseSiteUrl + "/sitemap/", directory).autoValidate(true).dateFormat(dateFormat) + .build(); + } catch (MalformedURLException e) { + logger.warning(String.format(msgErrorFormat, "Dataverse site URL", dataverseSiteUrl, e.getLocalizedMessage())); return; } - Document document = documentBuilder.newDocument(); - - Element urlSet = document.createElement("urlset"); - urlSet.setAttribute("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9"); - urlSet.setAttribute("xmlns:xhtml", "http://www.w3.org/1999/xhtml"); - document.appendChild(urlSet); for (Dataverse dataverse : dataverses) { if (!dataverse.isReleased()) { continue; } - Element url = document.createElement("url"); - urlSet.appendChild(url); - - Element loc = document.createElement("loc"); - String dataverseAlias = dataverse.getAlias(); - loc.appendChild(document.createTextNode(SystemConfig.getDataverseSiteUrlStatic() + "/dataverse/" + dataverseAlias)); - url.appendChild(loc); - - Element lastmod = document.createElement("lastmod"); - lastmod.appendChild(document.createTextNode(getLastModDate(dataverse))); - url.appendChild(lastmod); + final String dvAlias = dataverse.getAlias(); + final String dataverseUrl = dataverseSiteUrl + "/dataverse/" + dvAlias; + final String lastModDate = getLastModDate(dataverse); + try { + final WebSitemapUrl url = new WebSitemapUrl.Options(dataverseUrl).lastMod(lastModDate).build(); + wsg.addUrl(url); + } catch (MalformedURLException e) { + logger.fine(String.format(msgErrorFormat, "dataverse URL", dataverseUrl, e.getLocalizedMessage())); + } catch (ParseException e) { + logger.fine(String.format(msgErrorW3CFormat, lastModDate, "dataverse alias " + dvAlias, e.getLocalizedMessage())); + } } for (Dataset dataset : datasets) { - if (!dataset.isReleased()) { - continue; - } - if (dataset.isHarvested()) { - continue; - } // The deaccessioned check is last because it has to iterate through dataset versions. - if (dataset.isDeaccessioned()) { + if (!dataset.isReleased() || dataset.isHarvested() || dataset.isDeaccessioned()) { continue; } - Element url = document.createElement("url"); - urlSet.appendChild(url); - - Element loc = document.createElement("loc"); - String datasetPid = dataset.getGlobalId().asString(); - loc.appendChild(document.createTextNode(SystemConfig.getDataverseSiteUrlStatic() + "/dataset.xhtml?persistentId=" + datasetPid)); - url.appendChild(loc); - - Element lastmod = document.createElement("lastmod"); - lastmod.appendChild(document.createTextNode(getLastModDate(dataset))); - url.appendChild(lastmod); - } - - TransformerFactory transformerFactory = TransformerFactory.newInstance(); - Transformer transformer = null; - try { - transformer = transformerFactory.newTransformer(); - } catch (TransformerConfigurationException ex) { - logger.warning("Unable to update sitemap! TransformerConfigurationException: " + ex.getLocalizedMessage()); - return; - } - transformer.setOutputProperty(OutputKeys.INDENT, "yes"); - transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); - DOMSource source = new DOMSource(document); - File directory = new File(sitemapPathString); - if (!directory.exists()) { - directory.mkdir(); - } - - boolean debug = false; - if (debug) { - logger.info("Writing sitemap to console/logs"); - StreamResult consoleResult = new StreamResult(System.out); + final String datasetPid = dataset.getGlobalId().asString(); + final String datasetUrl = dataverseSiteUrl + "/dataset.xhtml?persistentId=" + datasetPid; + final String lastModDate = getLastModDate(dataset); try { - transformer.transform(source, consoleResult); - } catch (TransformerException ex) { - logger.warning("Unable to print sitemap to the console: " + ex.getLocalizedMessage()); + final WebSitemapUrl url = new WebSitemapUrl.Options(datasetUrl).lastMod(lastModDate).build(); + wsg.addUrl(url); + } catch (MalformedURLException e) { + logger.fine(String.format(msgErrorFormat, "dataset URL", datasetUrl, e.getLocalizedMessage())); + } catch (ParseException e) { + logger.fine(String.format(msgErrorW3CFormat, lastModDate, "dataset " + datasetPid, e.getLocalizedMessage())); } } - logger.info("Writing staged sitemap to " + stagedSitemapPathAndFileString); - StreamResult result = new StreamResult(new File(stagedSitemapPathAndFileString)); - try { - transformer.transform(source, result); - } catch (TransformerException ex) { - logger.warning("Unable to update sitemap! Unable to write staged sitemap to " + stagedSitemapPathAndFileString + ". TransformerException: " + ex.getLocalizedMessage()); - return; - } - - logger.info("Checking staged sitemap for well-formedness. The staged file is " + stagedSitemapPathAndFileString); + logger.info(String.format("Writing and checking sitemap file into %s", sitemapPathString)); try { - XmlValidator.validateXmlWellFormed(stagedSitemapPathAndFileString); + wsg.write(); + if (dataverses.size() + datasets.size() > SITEMAP_LIMIT) { + wsg.writeSitemapsWithIndex(); + } } catch (Exception ex) { - logger.warning("Unable to update sitemap! Staged sitemap file is not well-formed XML! The exception for " + stagedSitemapPathAndFileString + " is " + ex.getLocalizedMessage()); - return; - } - - logger.info("Checking staged sitemap against XML schema. The staged file is " + stagedSitemapPathAndFileString); - URL schemaUrl = null; - try { - schemaUrl = new URL("https://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"); - } catch (MalformedURLException ex) { - // This URL is hard coded and it's fine. We should never get MalformedURLException so we just swallow the exception and carry on. - } - try { - XmlValidator.validateXmlSchema(stagedSitemapPathAndFileString, schemaUrl); - } catch (SAXException | IOException ex) { - logger.warning("Unable to update sitemap! Exception caught while checking XML staged file (" + stagedSitemapPathAndFileString + " ) against XML schema: " + ex.getLocalizedMessage()); + final StringBuffer errorMsg = new StringBuffer("Unable to write or validate sitemap ! The exception is "); + errorMsg.append(ex.getLocalizedMessage()); + // Add causes messages exception + Throwable cause = ex.getCause(); + // Fix limit to 5 causes + final int causeLimit = 5; + int cpt = 0; + while (cause != null && cpt < causeLimit) { + errorMsg.append(" with cause ").append(cause.getLocalizedMessage()); + cause = ex.getCause(); + cpt = cpt + 1; + } + logger.warning(errorMsg.toString()); return; } - Path finalPath = Paths.get(finalSitemapPathAndFileString); - logger.info("Copying staged sitemap from " + stagedSitemapPathAndFileString + " to " + finalSitemapPathAndFileString); + logger.info(String.format("Remove staged sitemap %s", stagedSitemapPathAndFileString)); try { - Files.move(stagedPath, finalPath, StandardCopyOption.REPLACE_EXISTING); + Files.deleteIfExists(stagedSitemapPath); } catch (IOException ex) { - logger.warning("Unable to update sitemap! Unable to copy staged sitemap from " + stagedSitemapPathAndFileString + " to " + finalSitemapPathAndFileString + ". IOException: " + ex.getLocalizedMessage()); + logger.warning("Unable to delete sitemap staged file! IOException: " + ex.getLocalizedMessage()); return; } @@ -199,12 +145,11 @@ private static String getLastModDate(DvObjectContainer dvObjectContainer) { // TODO: Decide if YYYY-MM-DD is enough. https://www.sitemaps.org/protocol.html // says "The date of last modification of the file. This date should be in W3C Datetime format. // This format allows you to omit the time portion, if desired, and use YYYY-MM-DD." - return new SimpleDateFormat("yyyy-MM-dd").format(dvObjectContainer.getModificationTime()); + return dvObjectContainer.getModificationTime().toLocalDateTime().format(formatter); } public static boolean stageFileExists() { - String sitemapPathString = getSitemapPathString(); - String stagedSitemapPathAndFileString = sitemapPathString + File.separator + SITEMAP_FILENAME_STAGED; + String stagedSitemapPathAndFileString = getSitemapPathString() + File.separator + SITEMAP_FILENAME_STAGED; Path stagedPath = Paths.get(stagedSitemapPathAndFileString); if (Files.exists(stagedPath)) { logger.warning("Unable to update sitemap! The staged file from a previous run already existed. Delete " + stagedSitemapPathAndFileString + " and try again."); @@ -212,7 +157,7 @@ public static boolean stageFileExists() { } return false; } - + /** * Lookup the location where to generate the sitemap. * @@ -223,6 +168,6 @@ public static boolean stageFileExists() { */ private static String getSitemapPathString() { return JvmSettings.DOCROOT_DIRECTORY.lookup() + File.separator + "sitemap"; - } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/timer/DataverseTimerServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/timer/DataverseTimerServiceBean.java index 6eb3a8df0bc..a783b211b36 100644 --- a/src/main/java/edu/harvard/iq/dataverse/timer/DataverseTimerServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/timer/DataverseTimerServiceBean.java @@ -120,13 +120,13 @@ public void handleTimeout(jakarta.ejb.Timer timer) { } try { - logger.log(Level.INFO,"Handling timeout on " + InetAddress.getLocalHost().getCanonicalHostName()); + logger.log(Level.FINE,"Handling timeout on " + InetAddress.getLocalHost().getCanonicalHostName()); } catch (UnknownHostException ex) { Logger.getLogger(DataverseTimerServiceBean.class.getName()).log(Level.SEVERE, null, ex); } if (timer.getInfo() instanceof MotherTimerInfo) { - logger.info("Behold! I am the Master Timer, king of all timers! I'm here to create all the lesser timers!"); + logger.fine("Behold! I am the Master Timer, king of all timers! I'm here to create all the lesser timers!"); removeHarvestTimers(); for (HarvestingClient client : harvestingClientService.getAllHarvestingClients()) { createHarvestTimer(client); diff --git a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java index 8decf74fe13..a0c32d5c8ce 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java @@ -21,14 +21,8 @@ package edu.harvard.iq.dataverse.util; -import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.DataFile.ChecksumType; -import edu.harvard.iq.dataverse.DataFileServiceBean; -import edu.harvard.iq.dataverse.Dataset; -import edu.harvard.iq.dataverse.DatasetVersion; -import edu.harvard.iq.dataverse.Embargo; -import edu.harvard.iq.dataverse.FileMetadata; -import edu.harvard.iq.dataverse.TermsOfUseAndAccess; import edu.harvard.iq.dataverse.dataaccess.DataAccess; import edu.harvard.iq.dataverse.dataaccess.ImageThumbConverter; import edu.harvard.iq.dataverse.dataaccess.S3AccessIO; @@ -86,6 +80,7 @@ import java.util.HashMap; import java.util.List; import java.util.Optional; +import java.util.ResourceBundle; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; @@ -182,6 +177,7 @@ public class FileUtil implements java.io.Serializable { public static final String MIME_TYPE_NETCDF = "application/netcdf"; public static final String MIME_TYPE_XNETCDF = "application/x-netcdf"; public static final String MIME_TYPE_HDF5 = "application/x-hdf5"; + public static final String MIME_TYPE_RO_CRATE = "application/ld+json; profile=\"http://www.w3.org/ns/json-ld#flattened http://www.w3.org/ns/json-ld#compacted https://w3id.org/ro/crate\""; // File type "thumbnail classes" tags: @@ -278,6 +274,11 @@ public static String getUserFriendlyFileType(DataFile dataFile) { if (fileType.equalsIgnoreCase(ShapefileHandler.SHAPEFILE_FILE_TYPE)){ return ShapefileHandler.SHAPEFILE_FILE_TYPE_FRIENDLY_NAME; } + try { + return BundleUtil.getStringFromPropertyFile(fileType,"MimeTypeDisplay" ); + } catch (MissingResourceException e) { + //NOOP: we will try again after trimming ";" + } if (fileType.contains(";")) { fileType = fileType.substring(0, fileType.indexOf(";")); } @@ -292,6 +293,11 @@ public static String getUserFriendlyFileType(DataFile dataFile) { } public static String getIndexableFacetFileType(DataFile dataFile) { + try { + return BundleUtil.getStringFromDefaultPropertyFile(dataFile.getContentType(),"MimeTypeFacets" ); + } catch (MissingResourceException e) { + //NOOP: we will try again after trimming ";" + } String fileType = getFileType(dataFile); try { return BundleUtil.getStringFromDefaultPropertyFile(fileType,"MimeTypeFacets" ); @@ -421,7 +427,10 @@ public static String retestIngestableFileType(File file, String fileType) { } public static String determineFileType(File f, String fileName) throws IOException{ - String fileType = null; + String fileType = lookupFileTypeByFileName(fileName); + if (fileType != null) { + return fileType; + } String fileExtension = getFileExtension(fileName); @@ -480,17 +489,17 @@ public static String determineFileType(File f, String fileName) throws IOExcepti if (fileType != null && fileType.startsWith("text/plain") && STATISTICAL_FILE_EXTENSION.containsKey(fileExtension)) { fileType = STATISTICAL_FILE_EXTENSION.get(fileExtension); } else { - fileType = determineFileTypeByNameAndExtension(fileName); + fileType = lookupFileTypeByExtension(fileName); } logger.fine("mime type recognized by extension: "+fileType); } } else { logger.fine("fileExtension is null"); - String fileTypeByName = lookupFileTypeFromPropertiesFile(fileName); - if(!StringUtil.isEmpty(fileTypeByName)) { - logger.fine(String.format("mime type: %s recognized by filename: %s", fileTypeByName, fileName)); - fileType = fileTypeByName; + final String fileTypeByExtension = lookupFileTypeByExtensionFromPropertiesFile(fileName); + if(!StringUtil.isEmpty(fileTypeByExtension)) { + logger.fine(String.format("mime type: %s recognized by extension: %s", fileTypeByExtension, fileName)); + fileType = fileTypeByExtension; } } @@ -501,24 +510,15 @@ public static String determineFileType(File f, String fileName) throws IOExcepti if ("application/x-gzip".equals(fileType)) { logger.fine("we'll run additional checks on this gzipped file."); - // We want to be able to support gzipped FITS files, same way as - // if they were just regular FITS files: - FileInputStream gzippedIn = new FileInputStream(f); - // (new FileInputStream() can throw a "filen not found" exception; - // however, if we've made it this far, it really means that the - // file does exist and can be opened) - InputStream uncompressedIn = null; - try { - uncompressedIn = new GZIPInputStream(gzippedIn); + try (FileInputStream gzippedIn = new FileInputStream(f); + InputStream uncompressedIn = new GZIPInputStream(gzippedIn)) { if (isFITSFile(uncompressedIn)) { fileType = "application/fits-gzipped"; } } catch (IOException ioex) { - if (uncompressedIn != null) { - try {uncompressedIn.close();} catch (IOException e) {} - } + logger.warning("IOException while processing gzipped FITS file: " + ioex.getMessage()); } - } + } if ("application/zip".equals(fileType)) { // Is this a zipped Shapefile? @@ -544,33 +544,41 @@ public static String determineFileType(File f, String fileName) throws IOExcepti return fileType; } - public static String determineFileTypeByNameAndExtension(String fileName) { - String mimetypesFileTypeMapResult = MIME_TYPE_MAP.getContentType(fileName); + public static String determineFileTypeByNameAndExtension(final String fileName) { + final String fileType = lookupFileTypeByFileName(fileName); + if (fileType != null) { + return fileType; + } + return lookupFileTypeByExtension(fileName); + } + + private static String lookupFileTypeByExtension(final String fileName) { + final String mimetypesFileTypeMapResult = MIME_TYPE_MAP.getContentType(fileName); logger.fine("MimetypesFileTypeMap type by extension, for " + fileName + ": " + mimetypesFileTypeMapResult); - if (mimetypesFileTypeMapResult != null) { - if ("application/octet-stream".equals(mimetypesFileTypeMapResult)) { - return lookupFileTypeFromPropertiesFile(fileName); - } else { - return mimetypesFileTypeMapResult; - } - } else { + if (mimetypesFileTypeMapResult == null) { return null; } + if ("application/octet-stream".equals(mimetypesFileTypeMapResult)) { + return lookupFileTypeByExtensionFromPropertiesFile(fileName); + } + return mimetypesFileTypeMapResult; } - public static String lookupFileTypeFromPropertiesFile(String fileName) { - String fileKey = FilenameUtils.getExtension(fileName); - String propertyFileName = "MimeTypeDetectionByFileExtension"; - if(fileKey == null || fileKey.isEmpty()) { - fileKey = fileName; - propertyFileName = "MimeTypeDetectionByFileName"; + private static String lookupFileTypeByFileName(final String fileName) { + return lookupFileTypeFromPropertiesFile("MimeTypeDetectionByFileName", fileName); + } - } - String propertyFileNameOnDisk = propertyFileName + ".properties"; + private static String lookupFileTypeByExtensionFromPropertiesFile(final String fileName) { + final String fileKey = FilenameUtils.getExtension(fileName); + return lookupFileTypeFromPropertiesFile("MimeTypeDetectionByFileExtension", fileKey); + } + + private static String lookupFileTypeFromPropertiesFile(final String propertyFileName, final String fileKey) { + final String propertyFileNameOnDisk = propertyFileName + ".properties"; try { logger.fine("checking " + propertyFileNameOnDisk + " for file key " + fileKey); return BundleUtil.getStringFromPropertyFile(fileKey, propertyFileName); - } catch (MissingResourceException ex) { + } catch (final MissingResourceException ex) { logger.info(fileKey + " is a filename/extension Dataverse doesn't know about. Consider adding it to the " + propertyFileNameOnDisk + " file."); return null; } @@ -825,7 +833,8 @@ public static boolean useRecognizedType(String suppliedContentType, String recog || canIngestAsTabular(recognizedType) || recognizedType.equals("application/fits-gzipped") || recognizedType.equalsIgnoreCase(ShapefileHandler.SHAPEFILE_FILE_TYPE) || recognizedType.equalsIgnoreCase(BagItFileHandler.FILE_TYPE) - || recognizedType.equals(MIME_TYPE_ZIP)) { + || recognizedType.equals(MIME_TYPE_ZIP) + || recognizedType.equals(MIME_TYPE_RO_CRATE)) { return true; } return false; @@ -1223,6 +1232,9 @@ public static boolean isPubliclyDownloadable(FileMetadata fileMetadata) { if (isActivelyEmbargoed(fileMetadata)) { return false; } + if (isRetentionExpired(fileMetadata)) { + return false; + } boolean popupReasons = isDownloadPopupRequired(fileMetadata.getDatasetVersion()); if (popupReasons == true) { /** @@ -1776,10 +1788,41 @@ public static boolean isActivelyEmbargoed(List fmdList) { return false; } + public static boolean isRetentionExpired(DataFile df) { + Retention e = df.getRetention(); + if (e != null) { + LocalDate endDate = e.getDateUnavailable(); + if (endDate != null && endDate.isBefore(LocalDate.now())) { + return true; + } + } + return false; + } + + public static boolean isRetentionExpired(FileMetadata fileMetadata) { + return isRetentionExpired(fileMetadata.getDataFile()); + } + + public static boolean isRetentionExpired(List fmdList) { + for (FileMetadata fmd : fmdList) { + if (isRetentionExpired(fmd)) { + return true; + } + } + return false; + } public static String getStorageDriver(DataFile dataFile) { String storageIdentifier = dataFile.getStorageIdentifier(); return storageIdentifier.substring(0, storageIdentifier.indexOf(DataAccess.SEPARATOR)); } - + + /** + * Replace spaces with "_" and remove invalid chars + * @param fileNameIn - Name before sanitization NOTE: not full path since this method removes '/' and '\' + * @return filename without spaces or invalid chars + */ + public static String sanitizeFileName(String fileNameIn) { + return fileNameIn == null ? null : fileNameIn.replace(' ', '_').replaceAll("[\\\\/:*?\"<>|,;]", ""); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java new file mode 100644 index 00000000000..149f92761d2 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java @@ -0,0 +1,150 @@ +package edu.harvard.iq.dataverse.util; + +import edu.harvard.iq.dataverse.settings.JvmSettings; +import jakarta.annotation.Resource; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Named; +import jakarta.mail.Authenticator; +import jakarta.mail.PasswordAuthentication; +import jakarta.mail.Session; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import java.util.List; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +@ApplicationScoped +public class MailSessionProducer { + + // NOTE: We do not allow "from" here, as we want the transport to get it from the message being sent, enabling + // matching addresses. If "from" in transport and "from" in the message differ, some MTAs may reject or + // classify as spam. + // NOTE: Complete list including descriptions at https://eclipse-ee4j.github.io/angus-mail/docs/api/org.eclipse.angus.mail/org/eclipse/angus/mail/smtp/package-summary.html + static final List smtpStringProps = List.of( + "host", "localhost", "localaddress", "auth.mechanisms", "auth.ntlm.domain", "submitter", "dsn.notify", "dsn.ret", + "sasl.mechanisms", "sasl.authorizationid", "sasl.realm", "ssl.trust", "ssl.protocols", "ssl.ciphersuites", + "proxy.host", "proxy.port", "proxy.user", "proxy.password", "socks.host", "socks.port", "mailextension" + ); + static final List smtpIntProps = List.of( + "port", "connectiontimeout", "timeout", "writetimeout", "localport", "auth.ntlm.flag" + ); + static final List smtpBoolProps = List.of( + "auth", "ehlo", "auth.login.disable", "auth.plain.disable", "auth.digest-md5.disable", "auth.ntlm.disable", + "auth.xoauth2.disable", "allow8bitmime", "sendpartial", "sasl.enable", "sasl.usecanonicalhostname", + "quitwait", "quitonsessionreject", "ssl.enable", "ssl.checkserveridentity", "starttls.enable", + "starttls.required", "userset", "noop.strict" + ); + + private static final String PREFIX = "mail.smtp."; + private static final Logger logger = Logger.getLogger(MailSessionProducer.class.getCanonicalName()); + + static { + if (Boolean.TRUE.equals(JvmSettings.MAIL_DEBUG.lookup(Boolean.class))) { + logger.setLevel(Level.FINE); + } + } + + Session systemMailSession; + + /** + * Cache the application server provided (user defined) javamail resource to enable backwards compatibility. + * No direct JNDI lookup on the field to avoid deployment failures when not present. + * @deprecated This should be removed with the next major release of Dataverse, as it would be a breaking change. + */ + @Deprecated(forRemoval = true, since = "6.2") + Session appserverProvidedSession; + + public MailSessionProducer() { + try { + // Do JNDI lookup of legacy mail session programmatically to avoid deployment errors when not found. + Context initialContext = new InitialContext(); + this.appserverProvidedSession = (Session)initialContext.lookup("mail/notifyMailSession"); + } catch (NamingException e) { + // This exception simply means the appserver did not provide the legacy mail session. + // Debug level output is just fine. + logger.log(Level.FINER, "Error during legacy appserver-level mail resource lookup", e); + } + } + + @Produces + @Named("mail/systemSession") + public Session getSession() { + // For backward compatibility, prefer to return the mail resource configured on the appserver. + if (appserverProvidedSession != null) { + logger.warning("The configuration of mail transfer agents using asadmin create-javamail-resource is" + + " deprecated. Please migrate to using JVM options, see Dataverse guides for details"); + return appserverProvidedSession; + } + + if (systemMailSession == null) { + logger.fine("Setting up new mail session"); + + // Initialize with null (= no authenticator) is a valid argument for the session factory method. + Authenticator authenticator = null; + + // In case we want auth, create an authenticator (default = false from microprofile-config.properties) + if (Boolean.TRUE.equals(JvmSettings.MAIL_MTA_AUTH.lookup(Boolean.class))) { + logger.fine("Mail Authentication is enabled, building authenticator"); + authenticator = new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + logger.fine(() -> + String.format("Returning PasswordAuthenticator with username='%s', password='%s'", + JvmSettings.MAIL_MTA_USER.lookup(), + "*".repeat(JvmSettings.MAIL_MTA_PASSWORD.lookup().length()))); + return new PasswordAuthentication(JvmSettings.MAIL_MTA_USER.lookup(), JvmSettings.MAIL_MTA_PASSWORD.lookup()); + } + }; + } + + this.systemMailSession = Session.getInstance(getMailProperties(), authenticator); + } + return systemMailSession; + } + + Properties getMailProperties() { + Properties configuration = new Properties(); + + // See https://jakarta.ee/specifications/mail/2.1/apidocs/jakarta.mail/jakarta/mail/package-summary + configuration.put("mail.transport.protocol", "smtp"); + configuration.put("mail.debug", JvmSettings.MAIL_DEBUG.lookupOptional(Boolean.class).orElse(false).toString()); + // Only enable if your MTA properly supports UTF-8 mail addresses following RFC 6530/6531/6532. + // Before, we used a hack to put the raw UTF-8 mail address into the system. + // Now, make it proper, but make it possible to disable it - see also EMailValidator. + // Default = true from microprofile-config.properties as most MTAs these days support SMTPUTF8 extension + configuration.put("mail.mime.allowutf8", JvmSettings.MAIL_MTA_SUPPORT_UTF8.lookup(Boolean.class).toString()); + + // Map properties 1:1 to mail.smtp properties for the mail session. + smtpStringProps.forEach( + prop -> JvmSettings.MAIL_MTA_SETTING.lookupOptional(prop).ifPresent( + string -> configuration.put(PREFIX + prop, string))); + smtpBoolProps.forEach( + prop -> JvmSettings.MAIL_MTA_SETTING.lookupOptional(Boolean.class, prop).ifPresent( + bool -> configuration.put(PREFIX + prop, bool.toString()))); + smtpIntProps.forEach( + prop -> JvmSettings.MAIL_MTA_SETTING.lookupOptional(Integer.class, prop).ifPresent( + number -> configuration.put(PREFIX + prop, number.toString()))); + + logger.fine(() -> "Compiled properties:" + configuration.entrySet().stream() + .map(entry -> "\"" + entry.getKey() + "\": \"" + entry.getValue() + "\"") + .collect(Collectors.joining(",\n"))); + + return configuration; + } + + /** + * Determine if the session returned by {@link #getSession()} has been provided by the application server + * @return True if injected as resource from app server, false otherwise + * @deprecated This is supposed to be removed when {@link #appserverProvidedSession} is removed. + */ + @Deprecated(forRemoval = true, since = "6.2") + public boolean hasSessionFromAppServer() { + return this.appserverProvidedSession != null; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java index 0724e53700b..f81ce093815 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java @@ -5,32 +5,14 @@ import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.UserNotification; import edu.harvard.iq.dataverse.branding.BrandingUtil; -import static edu.harvard.iq.dataverse.settings.SettingsServiceBean.Key.SystemEmail; import java.util.Arrays; import java.util.List; import java.util.logging.Logger; -import jakarta.mail.internet.AddressException; -import jakarta.mail.internet.InternetAddress; public class MailUtil { private static final Logger logger = Logger.getLogger(MailUtil.class.getCanonicalName()); - public static InternetAddress parseSystemAddress(String systemEmail) { - if (systemEmail != null) { - try { - InternetAddress parsedSystemEmail = new InternetAddress(systemEmail); - logger.fine("parsed system email: " + parsedSystemEmail); - return parsedSystemEmail; - } catch (AddressException ex) { - logger.info("Email will not be sent due to invalid value in " + SystemEmail + " setting: " + ex); - return null; - } - } - logger.fine("Email will not be sent because the " + SystemEmail + " setting is null."); - return null; - } - public static String getSubjectTextBasedOnNotification(UserNotification userNotification, Object objectOfNotification) { List rootDvNameAsList = Arrays.asList(BrandingUtil.getInstallationBrandName()); String datasetDisplayName = ""; @@ -53,7 +35,10 @@ public static String getSubjectTextBasedOnNotification(UserNotification userNoti case CREATEDV: return BundleUtil.getStringFromBundle("notification.email.create.dataverse.subject", rootDvNameAsList); case REQUESTFILEACCESS: - return BundleUtil.getStringFromBundle("notification.email.request.file.access.subject", Arrays.asList(rootDvNameAsList.get(0), datasetDisplayName)); + String userNameFirst = userNotification.getRequestor().getFirstName(); + String userNameLast = userNotification.getRequestor().getLastName(); + String userIdentifier = userNotification.getRequestor().getIdentifier(); + return BundleUtil.getStringFromBundle("notification.email.request.file.access.subject", Arrays.asList(rootDvNameAsList.get(0), userNameFirst, userNameLast, userIdentifier, datasetDisplayName)); case REQUESTEDFILEACCESS: return BundleUtil.getStringFromBundle("notification.email.requested.file.access.subject", Arrays.asList(rootDvNameAsList.get(0), datasetDisplayName)); case GRANTFILEACCESS: @@ -114,6 +99,23 @@ public static String getSubjectTextBasedOnNotification(UserNotification userNoti } catch (Exception e) { return BundleUtil.getStringFromBundle("notification.email.globus.uploadCompletedWithErrors.subject", rootDvNameAsList); } + case GLOBUSUPLOADREMOTEFAILURE: + try { + DatasetVersion version = (DatasetVersion)objectOfNotification; + List dsNameAsList = Arrays.asList(version.getDataset().getDisplayName()); + return BundleUtil.getStringFromBundle("notification.email.globus.uploadFailedRemotely.subject", dsNameAsList); + + } catch (Exception e) { + return BundleUtil.getStringFromBundle("notification.email.globus.uploadFailedRemotely.subject", rootDvNameAsList); + } + case GLOBUSUPLOADLOCALFAILURE: + try { + DatasetVersion version = (DatasetVersion)objectOfNotification; + List dsNameAsList = Arrays.asList(version.getDataset().getDisplayName()); + return BundleUtil.getStringFromBundle("notification.email.globus.uploadFailedLocally.subject", dsNameAsList); + } catch (Exception e) { + return BundleUtil.getStringFromBundle("notification.email.globus.uploadFailedLocally.subject", rootDvNameAsList); + } case GLOBUSDOWNLOADCOMPLETEDWITHERRORS: try { DatasetVersion version = (DatasetVersion)objectOfNotification; diff --git a/src/main/java/edu/harvard/iq/dataverse/util/PersonOrOrgUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/PersonOrOrgUtil.java index f68957ad060..80e32184731 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/PersonOrOrgUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/PersonOrOrgUtil.java @@ -123,7 +123,7 @@ public static JsonObject getPersonOrOrganization(String name, boolean organizati if (!name.replaceFirst(",", "").contains(",")) { // contributorName=, String[] fullName = name.split(", "); - givenName = fullName[1]; + givenName = fullName.length > 1 ? fullName[1] : null; familyName = fullName[0]; } } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/ShapefileHandler.java b/src/main/java/edu/harvard/iq/dataverse/util/ShapefileHandler.java index 3af562882f3..f1440cc3c02 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/ShapefileHandler.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/ShapefileHandler.java @@ -15,6 +15,7 @@ import java.util.*; import java.nio.file.Files; +import java.nio.file.Paths; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import java.util.logging.Level; import java.util.logging.Logger; @@ -72,7 +73,7 @@ public class ShapefileHandler{ public final static List SHAPEFILE_MANDATORY_EXTENSIONS = Arrays.asList("shp", "shx", "dbf", "prj"); public final static String SHP_XML_EXTENSION = "shp.xml"; public final static String BLANK_EXTENSION = "__PLACEHOLDER-FOR-BLANK-EXTENSION__"; - public final static List SHAPEFILE_ALL_EXTENSIONS = Arrays.asList("shp", "shx", "dbf", "prj", "sbn", "sbx", "fbn", "fbx", "ain", "aih", "ixs", "mxs", "atx", "cpg", SHP_XML_EXTENSION); + public final static List SHAPEFILE_ALL_EXTENSIONS = Arrays.asList("shp", "shx", "dbf", "prj", "sbn", "sbx", "fbn", "fbx", "ain", "aih", "ixs", "mxs", "atx", "cpg", "qpj", "qmd", SHP_XML_EXTENSION); public boolean DEBUG = false; @@ -695,33 +696,42 @@ private boolean examineZipfile(FileInputStream zip_file_stream){ this.filesListInDir.clear(); this.filesizeHash.clear(); this.fileGroups.clear(); - - try{ + + try{ ZipInputStream zipStream = new ZipInputStream(zip_file_stream); ZipEntry entry; - + List hiddenDirectories = new ArrayList<>(); while((entry = zipStream.getNextEntry())!=null){ + String zentryFileName = entry.getName(); + boolean isDirectory = entry.isDirectory(); + + Boolean skip = isDirectory || this.isFileToSkip(zentryFileName); + + // check if path is hidden + if (isDirectory && Files.isHidden(Paths.get(zentryFileName))) { + hiddenDirectories.add(zentryFileName); + logger.fine("Ignoring files under hidden directory: " + zentryFileName); + } else { + // check if the path was already found to be hidden + for (String hidden : hiddenDirectories) { + if (zentryFileName.startsWith(hidden)) { + skip = true; + break; + } + } + } - String zentryFileName = entry.getName(); - //msg("zip entry: " + entry.getName()); - // Skip files or folders starting with __ - if (this.isFileToSkip(zentryFileName)){ - continue; - } - - if (entry.isDirectory()) { - //String dirpath = outputFolder + "/" + zentryFileName; - //createDirectory(dirpath); - continue; + if (skip) { + continue; } - + String unzipFileName = this.getFileBasename(zentryFileName); if (unzipFileName==null){ logger.warning("Zip Entry Basename is an empty string: " + zentryFileName); continue; } String unzipFolderName = this.getFolderName(zentryFileName); - + String unzipFilePath = unzipFileName; if (unzipFolderName != null) { unzipFilePath = unzipFolderName + "/" + unzipFileName; diff --git a/src/main/java/edu/harvard/iq/dataverse/util/StringUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/StringUtil.java index 33c87563104..56f85436773 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/StringUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/StringUtil.java @@ -1,10 +1,14 @@ package edu.harvard.iq.dataverse.util; import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2LoginBackingBean; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; @@ -19,6 +23,7 @@ import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.lang3.StringUtils; @@ -117,6 +122,10 @@ public static List htmlArray2textArray(List htmlArray) { return cleanTextArray; } + private final static SecureRandom secureRandom = new SecureRandom(); + // 12 bytes is recommended by GCM spec + private final static int GCM_IV_LENGTH = 12; + /** * Generates an AES-encrypted version of the string. Resultant string is URL safe. * @param value The value to encrypt. @@ -124,19 +133,26 @@ public static List htmlArray2textArray(List htmlArray) { * @return encrypted string, URL-safe. */ public static String encrypt(String value, String password ) { + byte[] baseBytes = value.getBytes(); try { - Cipher aes = Cipher.getInstance("AES"); + byte[] iv = new byte[GCM_IV_LENGTH]; //NEVER REUSE THIS IV WITH SAME KEY + secureRandom.nextBytes(iv); + Cipher aes = Cipher.getInstance("AES/GCM/NoPadding"); final SecretKeySpec secretKeySpec = generateKeyFromString(password); - aes.init(Cipher.ENCRYPT_MODE, secretKeySpec); + GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv); + aes.init(Cipher.ENCRYPT_MODE, secretKeySpec, parameterSpec); byte[] encrypted = aes.doFinal(baseBytes); - String base64ed = new String(Base64.getEncoder().encode(encrypted)); + ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + encrypted.length); + byteBuffer.put(iv); + byteBuffer.put(encrypted); + String base64ed = new String(Base64.getEncoder().encode(byteBuffer.array())); return base64ed.replaceAll("\\+", ".") .replaceAll("=", "-") .replaceAll("/", "_"); } catch ( InvalidKeyException | NoSuchAlgorithmException | BadPaddingException - | IllegalBlockSizeException | NoSuchPaddingException | UnsupportedEncodingException ex) { + | IllegalBlockSizeException | NoSuchPaddingException | InvalidAlgorithmParameterException ex) { Logger.getLogger(OAuth2LoginBackingBean.class.getName()).log(Level.SEVERE, null, ex); throw new RuntimeException(ex); } @@ -149,13 +165,15 @@ public static String decrypt(String value, String password ) { byte[] baseBytes = Base64.getDecoder().decode(base64); try { - Cipher aes = Cipher.getInstance("AES"); - aes.init( Cipher.DECRYPT_MODE, generateKeyFromString(password)); - byte[] decrypted = aes.doFinal(baseBytes); + Cipher aes = Cipher.getInstance("AES/GCM/NoPadding"); + //use first 12 bytes for iv + AlgorithmParameterSpec gcmIv = new GCMParameterSpec(128, baseBytes, 0, GCM_IV_LENGTH); + aes.init( Cipher.DECRYPT_MODE, generateKeyFromString(password),gcmIv); + byte[] decrypted = aes.doFinal(baseBytes,GCM_IV_LENGTH, baseBytes.length - GCM_IV_LENGTH); return new String(decrypted); } catch ( InvalidKeyException | NoSuchAlgorithmException | BadPaddingException - | IllegalBlockSizeException | NoSuchPaddingException | UnsupportedEncodingException ex) { + | IllegalBlockSizeException | NoSuchPaddingException | InvalidAlgorithmParameterException ex) { Logger.getLogger(OAuth2LoginBackingBean.class.getName()).log(Level.SEVERE, null, ex); throw new RuntimeException(ex); } @@ -191,8 +209,8 @@ public static String sanitizeFileDirectory(String value, boolean aggressively){ } - private static SecretKeySpec generateKeyFromString(final String secKey) throws UnsupportedEncodingException, NoSuchAlgorithmException { - byte[] key = (secKey).getBytes("UTF-8"); + private static SecretKeySpec generateKeyFromString(final String secKey) throws NoSuchAlgorithmException { + byte[] key = (secKey).getBytes(StandardCharsets.UTF_8); MessageDigest sha = MessageDigest.getInstance("SHA-1"); key = sha.digest(key); key = Arrays.copyOf(key, 16); // use only first 128 bits diff --git a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java index 3c6992f8ec3..60967b13131 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java @@ -545,7 +545,7 @@ public boolean isTimerServer() { } return false; } - + public String getFooterCopyrightAndYear() { return BundleUtil.getStringFromBundle("footer.copyright", Arrays.asList(Year.now().getValue() + "")); } @@ -752,6 +752,7 @@ public enum FileUploadMethods { * DCM stands for Data Capture Module. Right now it supports upload over * rsync+ssh but DCM may support additional methods in the future. */ + @Deprecated(forRemoval = true, since = "2024-07-07") RSYNC("dcm/rsync+ssh"), /** * Traditional Dataverse file handling, which tends to involve users @@ -809,6 +810,7 @@ public enum FileDownloadMethods { * RSAL stands for Repository Storage Abstraction Layer. Downloads don't * go through Glassfish. */ + @Deprecated(forRemoval = true, since = "2024-07-07") RSYNC("rsal/rsync"), NATIVE("native/http"), GLOBUS("globus") @@ -862,6 +864,7 @@ public String toString() { */ public enum TransferProtocols { + @Deprecated(forRemoval = true, since = "2024-07-07") RSYNC("rsync"), /** * POSIX includes NFS. This is related to Key.LocalDataAccessPath in @@ -898,7 +901,8 @@ public boolean isPublicInstall(){ boolean saneDefault = false; return settingsService.isTrueForKey(SettingsServiceBean.Key.PublicInstall, saneDefault); } - + + @Deprecated(forRemoval = true, since = "2024-07-07") public boolean isRsyncUpload(){ return getMethodAvailable(SystemConfig.FileUploadMethods.RSYNC.toString(), true); } @@ -915,7 +919,8 @@ public boolean isWebloaderUpload(){ public boolean isHTTPUpload(){ return getMethodAvailable(SystemConfig.FileUploadMethods.NATIVE.toString(), true); } - + + @Deprecated(forRemoval = true, since = "2024-07-07") public boolean isRsyncOnly(){ String downloadMethods = settingsService.getValueForKey(SettingsServiceBean.Key.DownloadMethods); if(downloadMethods == null){ @@ -931,11 +936,12 @@ public boolean isRsyncOnly(){ return Arrays.asList(uploadMethods.toLowerCase().split("\\s*,\\s*")).size() == 1 && uploadMethods.toLowerCase().equals(SystemConfig.FileUploadMethods.RSYNC.toString()); } } - + + @Deprecated(forRemoval = true, since = "2024-07-07") public boolean isRsyncDownload() { return getMethodAvailable(SystemConfig.FileUploadMethods.RSYNC.toString(), false); } - + public boolean isHTTPDownload() { return getMethodAvailable(SystemConfig.FileUploadMethods.NATIVE.toString(), false); } @@ -966,25 +972,6 @@ public Integer getUploadMethodCount(){ return Arrays.asList(uploadMethods.toLowerCase().split("\\s*,\\s*")).size(); } } - public boolean isDataFilePIDSequentialDependent(){ - String doiIdentifierType = settingsService.getValueForKey(SettingsServiceBean.Key.IdentifierGenerationStyle, "randomString"); - String doiDataFileFormat = settingsService.getValueForKey(SettingsServiceBean.Key.DataFilePIDFormat, "DEPENDENT"); - if (doiIdentifierType.equals("storedProcGenerated") && doiDataFileFormat.equals("DEPENDENT")){ - return true; - } - return false; - } - - public int getPIDAsynchRegFileCount() { - String fileCount = settingsService.getValueForKey(SettingsServiceBean.Key.PIDAsynchRegFileCount, "10"); - int retVal = 10; - try { - retVal = Integer.parseInt(fileCount); - } catch (NumberFormatException e) { - //if no number in the setting we'll return 10 - } - return retVal; - } public boolean isAllowCustomTerms() { boolean safeDefaultIfKeyNotFound = true; @@ -999,7 +986,7 @@ public boolean isFilePIDsEnabledForCollection(Dataverse collection) { Dataverse thisCollection = collection; // If neither enabled nor disabled specifically for this collection, - // the parent collection setting is inhereted (recursively): + // the parent collection setting is inherited (recursively): while (thisCollection.getFilePIDsEnabled() == null) { if (thisCollection.getOwner() == null) { // We've reached the root collection, and file PIDs registration @@ -1015,17 +1002,6 @@ public boolean isFilePIDsEnabledForCollection(Dataverse collection) { // takes precedent: return thisCollection.getFilePIDsEnabled(); } - - public boolean isIndependentHandleService() { - boolean safeDefaultIfKeyNotFound = false; - return settingsService.isTrueForKey(SettingsServiceBean.Key.IndependentHandleService, safeDefaultIfKeyNotFound); - - } - - public String getHandleAuthHandle() { - String handleAuthHandle = settingsService.getValueForKey(SettingsServiceBean.Key.HandleAuthHandle, null); - return handleAuthHandle; - } public String getMDCLogPath() { String mDCLogPath = settingsService.getValueForKey(SettingsServiceBean.Key.MDCLogPath, null); @@ -1173,4 +1149,22 @@ public boolean isStorageQuotasEnforced() { public Long getTestStorageQuotaLimit() { return settingsService.getValueForKeyAsLong(SettingsServiceBean.Key.StorageQuotaSizeInBytes); } + /** + * Should we store tab-delimited files produced during ingest *with* the + * variable name header line included? + * @return boolean - defaults to false. + */ + public boolean isStoringIngestedFilesWithHeaders() { + return settingsService.isTrueForKey(SettingsServiceBean.Key.StoreIngestedTabularFilesWithVarHeaders, false); + } + + /** + * RateLimitUtil will parse the json to create a List + */ + public String getRateLimitsJson() { + return settingsService.getValueForKey(SettingsServiceBean.Key.RateLimitingCapacityByTierAndAction, ""); + } + public String getRateLimitingDefaultCapacityTiers() { + return settingsService.getValueForKey(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers, ""); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/URLTokenUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/URLTokenUtil.java index a3293e0cd28..90557a530c9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/URLTokenUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/URLTokenUtil.java @@ -1,6 +1,7 @@ package edu.harvard.iq.dataverse.util; import java.util.Arrays; +import java.util.Random; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -261,7 +262,9 @@ public JsonObject getParams(JsonObject toolParameters) { public static String getScriptForUrl(String url) { String msg = BundleUtil.getStringFromBundle("externaltools.enable.browser.popups"); - String script = "const newWin = window.open('" + url + "', target='_blank'); if (!newWin || newWin.closed || typeof newWin.closed == \"undefined\") {alert(\"" + msg + "\");}"; + String newWin = "newWin" + (new Random()).nextInt(1000000000); + //Always use a unique identifier so that more than one script can run (or one can be rerun) without conflicts + String script = String.format("const %1$s = window.open('" + url + "', target='_blank'); if (!%1$s || %1$s.closed || typeof %1$s.closed == \"undefined\") {alert(\"" + msg + "\");}", newWin); return script; } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java index 29c4e8a6fb9..18ea3771301 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java @@ -2,7 +2,7 @@ import java.net.MalformedURLException; import java.net.URL; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -96,7 +96,7 @@ public static boolean isValidUrl(String signedUrl, String user, String method, S boolean valid = true; try { URL url = new URL(signedUrl); - List params = URLEncodedUtils.parse(url.getQuery(), Charset.forName("UTF-8")); + List params = URLEncodedUtils.parse(url.getQuery(), StandardCharsets.UTF_8); String hash = null; String dateString = null; String allowedMethod = null; @@ -156,7 +156,7 @@ public static boolean isValidUrl(String signedUrl, String user, String method, S public static boolean hasToken(String urlString) { try { URL url = new URL(urlString); - List params = URLEncodedUtils.parse(url.getQuery(), Charset.forName("UTF-8")); + List params = URLEncodedUtils.parse(url.getQuery(), StandardCharsets.UTF_8); for (NameValuePair nvp : params) { if (nvp.getName().equals(SIGNED_URL_TOKEN)) { return true; diff --git a/src/main/java/edu/harvard/iq/dataverse/util/bagit/BagGenerator.java b/src/main/java/edu/harvard/iq/dataverse/util/bagit/BagGenerator.java index b7c44014b80..e47426149f9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/bagit/BagGenerator.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/bagit/BagGenerator.java @@ -9,10 +9,10 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; -import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.security.KeyManagementException; import java.security.KeyStoreException; @@ -686,12 +686,7 @@ private void createFileFromString(final String relPath, final String content) archiveEntry.setMethod(ZipEntry.DEFLATED); InputStreamSupplier supp = new InputStreamSupplier() { public InputStream get() { - try { - return new ByteArrayInputStream(content.getBytes("UTF-8")); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - return null; + return new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); } }; diff --git a/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java b/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java index aa653a6e360..60ab9407269 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java @@ -1,19 +1,7 @@ package edu.harvard.iq.dataverse.util.bagit; -import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.Dataset; -import edu.harvard.iq.dataverse.DatasetField; -import edu.harvard.iq.dataverse.DatasetFieldCompoundValue; -import edu.harvard.iq.dataverse.DatasetFieldConstant; -import edu.harvard.iq.dataverse.DatasetFieldServiceBean; -import edu.harvard.iq.dataverse.DatasetFieldType; -import edu.harvard.iq.dataverse.DatasetVersion; +import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.DatasetVersion.VersionState; -import edu.harvard.iq.dataverse.Dataverse; -import edu.harvard.iq.dataverse.DvObjectContainer; -import edu.harvard.iq.dataverse.Embargo; -import edu.harvard.iq.dataverse.FileMetadata; -import edu.harvard.iq.dataverse.TermsOfUseAndAccess; import edu.harvard.iq.dataverse.branding.BrandingUtil; import edu.harvard.iq.dataverse.export.OAI_OREExporter; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; @@ -23,6 +11,7 @@ import edu.harvard.iq.dataverse.util.json.JsonPrinter; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.util.List; import java.util.Map; @@ -80,7 +69,7 @@ public OREMap(DatasetVersion dv, boolean exclude) { } public void writeOREMap(OutputStream outputStream) throws Exception { - outputStream.write(getOREMap().toString().getBytes("UTF8")); + outputStream.write(getOREMap().toString().getBytes(StandardCharsets.UTF_8)); outputStream.flush(); } @@ -236,6 +225,17 @@ public JsonObjectBuilder getOREMapBuilder(boolean aggregationOnly) { } aggRes.add(JsonLDTerm.DVCore("embargoed").getLabel(), embargoObject); } + Retention retention = df.getRetention(); + if(retention!=null) { + String date = retention.getFormattedDateUnavailable(); + String reason= retention.getReason(); + JsonObjectBuilder retentionObject = Json.createObjectBuilder(); + retentionObject.add(JsonLDTerm.DVCore("dateUnavailable").getLabel(), date); + if(reason!=null) { + retentionObject.add(JsonLDTerm.DVCore("reason").getLabel(), reason); + } + aggRes.add(JsonLDTerm.DVCore("retained").getLabel(), retentionObject); + } addIfNotNull(aggRes, JsonLDTerm.directoryLabel, fmd.getDirectoryLabel()); addIfNotNull(aggRes, JsonLDTerm.schemaOrg("version"), fmd.getVersion()); addIfNotNull(aggRes, JsonLDTerm.datasetVersionId, fmd.getDatasetVersion().getId()); diff --git a/src/main/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBean.java new file mode 100644 index 00000000000..36b2b35b48f --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBean.java @@ -0,0 +1,60 @@ +package edu.harvard.iq.dataverse.util.cache; + +import edu.harvard.iq.dataverse.authorization.users.User; +import edu.harvard.iq.dataverse.engine.command.Command; +import edu.harvard.iq.dataverse.util.SystemConfig; +import jakarta.annotation.PostConstruct; +import jakarta.ejb.EJB; +import jakarta.ejb.Singleton; +import jakarta.ejb.Startup; +import jakarta.inject.Inject; + +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.cache.configuration.CompleteConfiguration; +import javax.cache.configuration.MutableConfiguration; +import javax.cache.spi.CachingProvider; +import java.util.logging.Logger; + +@Singleton +@Startup +public class CacheFactoryBean implements java.io.Serializable { + private static final Logger logger = Logger.getLogger(CacheFactoryBean.class.getCanonicalName()); + // Retrieved from Hazelcast, implements ConcurrentMap and is threadsafe + Cache rateLimitCache; + @EJB + SystemConfig systemConfig; + @Inject + CacheManager manager; + @Inject + CachingProvider provider; + public final static String RATE_LIMIT_CACHE = "rateLimitCache"; + + @PostConstruct + public void init() { + rateLimitCache = manager.getCache(RATE_LIMIT_CACHE); + if (rateLimitCache == null) { + CompleteConfiguration config = + new MutableConfiguration() + .setTypes( String.class, String.class ); + rateLimitCache = manager.createCache(RATE_LIMIT_CACHE, config); + } + } + + /** + * Check if user can make this call or if they are rate limited + * @param user + * @param command + * @return true if user is superuser or rate not limited + */ + public boolean checkRate(User user, Command command) { + final String action = command.getClass().getSimpleName(); + int capacity = RateLimitUtil.getCapacity(systemConfig, user, action); + if (capacity == RateLimitUtil.NO_LIMIT) { + return true; + } else { + String cacheKey = RateLimitUtil.generateCacheKey(user, action); + return (!RateLimitUtil.rateLimited(rateLimitCache, cacheKey, capacity)); + } + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitSetting.java b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitSetting.java new file mode 100644 index 00000000000..54da5a46670 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitSetting.java @@ -0,0 +1,40 @@ +package edu.harvard.iq.dataverse.util.cache; + +import java.util.ArrayList; +import java.util.List; + +public class RateLimitSetting { + + private int tier; + private int limitPerHour = RateLimitUtil.NO_LIMIT; + private List actions = new ArrayList<>(); + + private int defaultLimitPerHour; + + public RateLimitSetting() {} + + public void setTier(int tier) { + this.tier = tier; + } + public int getTier() { + return this.tier; + } + public void setLimitPerHour(int limitPerHour) { + this.limitPerHour = limitPerHour; + } + public int getLimitPerHour() { + return this.limitPerHour; + } + public void setActions(List actions) { + this.actions = actions; + } + public List getActions() { + return this.actions; + } + public void setDefaultLimit(int defaultLimitPerHour) { + this.defaultLimitPerHour = defaultLimitPerHour; + } + public int getDefaultLimitPerHour() { + return this.defaultLimitPerHour; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java new file mode 100644 index 00000000000..b566cd42fe1 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java @@ -0,0 +1,134 @@ +package edu.harvard.iq.dataverse.util.cache; + +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.authorization.users.GuestUser; +import edu.harvard.iq.dataverse.authorization.users.User; +import edu.harvard.iq.dataverse.util.SystemConfig; +import jakarta.json.bind.Jsonb; +import jakarta.json.bind.JsonbBuilder; +import jakarta.json.bind.JsonbException; + +import javax.cache.Cache; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.logging.Logger; + +import static java.lang.Math.max; +import static java.lang.Math.min; + +public class RateLimitUtil { + private static final Logger logger = Logger.getLogger(RateLimitUtil.class.getCanonicalName()); + static final List rateLimits = new CopyOnWriteArrayList<>(); + static final Map rateLimitMap = new ConcurrentHashMap<>(); + public static final int NO_LIMIT = -1; + + static String generateCacheKey(final User user, final String action) { + return (user != null ? user.getIdentifier() : GuestUser.get().getIdentifier()) + + (action != null ? ":" + action : ""); + } + static int getCapacity(SystemConfig systemConfig, User user, String action) { + if (user != null && user.isSuperuser()) { + return NO_LIMIT; + } + // get the capacity, i.e. calls per hour, from config + return (user instanceof AuthenticatedUser authUser) ? + getCapacityByTierAndAction(systemConfig, authUser.getRateLimitTier(), action) : + getCapacityByTierAndAction(systemConfig, 0, action); + } + static boolean rateLimited(final Cache rateLimitCache, final String key, int capacityPerHour) { + if (capacityPerHour == NO_LIMIT) { + return false; + } + long currentTime = System.currentTimeMillis() / 60000L; // convert to minutes + double tokensPerMinute = (capacityPerHour / 60.0); + // Get the last time this bucket was added to + final String keyLastUpdate = String.format("%s:last_update",key); + long lastUpdate = longFromKey(rateLimitCache, keyLastUpdate); + long deltaTime = currentTime - lastUpdate; + // Get the current number of tokens in the bucket + long tokens = longFromKey(rateLimitCache, key); + long tokensToAdd = (long) (deltaTime * tokensPerMinute); + if (tokensToAdd > 0) { // Don't update timestamp if we aren't adding any tokens to the bucket + tokens = min(capacityPerHour, tokens + tokensToAdd); + rateLimitCache.put(keyLastUpdate, String.valueOf(currentTime)); + } + // Update with any added tokens and decrement 1 token for this call if not rate limited (0 tokens) + rateLimitCache.put(key, String.valueOf(max(0, tokens-1))); + return tokens < 1; + } + + static int getCapacityByTierAndAction(SystemConfig systemConfig, Integer tier, String action) { + if (rateLimits.isEmpty()) { + init(systemConfig); + } + + if (rateLimitMap.containsKey(getMapKey(tier, action))) { + return rateLimitMap.get(getMapKey(tier,action)); + } else if (rateLimitMap.containsKey(getMapKey(tier))) { + return rateLimitMap.get(getMapKey(tier)); + } else { + return getCapacityByTier(systemConfig, tier); + } + } + static int getCapacityByTier(SystemConfig systemConfig, int tier) { + int value = NO_LIMIT; + String csvString = systemConfig.getRateLimitingDefaultCapacityTiers(); + try { + if (!csvString.isEmpty()) { + int[] values = Arrays.stream(csvString.split(",")).mapToInt(Integer::parseInt).toArray(); + if (tier < values.length) { + value = values[tier]; + } + } + } catch (NumberFormatException nfe) { + logger.warning(nfe.getMessage()); + } + return value; + } + static void init(SystemConfig systemConfig) { + getRateLimitsFromJson(systemConfig); + /* Convert the List of Rate Limit Settings containing a list of Actions to a fast lookup Map where the key is: + for default if no action defined: "{tier}:" and the value is the default limit for the tier + for each action: "{tier}:{action}" and the value is the limit defined in the setting + */ + rateLimitMap.clear(); + rateLimits.forEach(r -> { + r.setDefaultLimit(getCapacityByTier(systemConfig, r.getTier())); + rateLimitMap.put(getMapKey(r.getTier()), r.getDefaultLimitPerHour()); + r.getActions().forEach(a -> rateLimitMap.put(getMapKey(r.getTier(), a), r.getLimitPerHour())); + }); + } + + @SuppressWarnings("java:S2133") // <- To enable casting to generic in JSON-B we need a class instance, false positive + static void getRateLimitsFromJson(SystemConfig systemConfig) { + String setting = systemConfig.getRateLimitsJson(); + rateLimits.clear(); + if (!setting.isEmpty()) { + try (Jsonb jsonb = JsonbBuilder.create()) { + rateLimits.addAll(jsonb.fromJson(setting, + new ArrayList() {}.getClass().getGenericSuperclass())); + } catch (JsonbException e) { + logger.warning("Unable to parse Rate Limit Json: " + e.getLocalizedMessage() + " Json:(" + setting + ")"); + rateLimits.add(new RateLimitSetting()); // add a default entry to prevent re-initialization + // Note: Usually using Exception in a catch block is an antipattern and should be avoided. + // As the JSON-B interface does not specify a non-generic type, we have to use this. + } catch (Exception e) { + logger.warning("Could not close JSON-B reader"); + } + } + } + static String getMapKey(int tier) { + return getMapKey(tier, null); + } + static String getMapKey(int tier, String action) { + return tier + ":" + (action != null ? action : ""); + } + static long longFromKey(Cache cache, String key) { + Object l = cache.get(key); + return l != null ? Long.parseLong(String.valueOf(l)) : 0L; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/BriefJsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/BriefJsonPrinter.java index 3fcaf6b11ff..c16a46a1765 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/BriefJsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/BriefJsonPrinter.java @@ -28,6 +28,7 @@ public JsonObjectBuilder json( MetadataBlock blk ) { ? null : jsonObjectBuilder().add("id", blk.getId()) .add("displayName", blk.getDisplayName()) + .add("displayOnCreate", blk.isDisplayOnCreate()) .add("name", blk.getName()) ; } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JSONLDUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JSONLDUtil.java index 4fb3ffe6c14..380cef6aa9d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JSONLDUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JSONLDUtil.java @@ -39,7 +39,6 @@ import edu.harvard.iq.dataverse.DatasetFieldValue; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.GlobalId; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; import edu.harvard.iq.dataverse.MetadataBlock; import edu.harvard.iq.dataverse.MetadataBlockServiceBean; import edu.harvard.iq.dataverse.TermsOfUseAndAccess; @@ -50,8 +49,11 @@ import com.apicatalog.jsonld.document.JsonDocument; import edu.harvard.iq.dataverse.DatasetVersion.VersionState; +import edu.harvard.iq.dataverse.dataset.DatasetType; +import edu.harvard.iq.dataverse.dataset.DatasetTypeServiceBean; import edu.harvard.iq.dataverse.license.License; import edu.harvard.iq.dataverse.license.LicenseServiceBean; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; import jakarta.json.JsonReader; public class JSONLDUtil { @@ -77,13 +79,13 @@ public static JsonObject getContext(Map contextMap) { public static Dataset updateDatasetMDFromJsonLD(Dataset ds, String jsonLDBody, MetadataBlockServiceBean metadataBlockSvc, DatasetFieldServiceBean datasetFieldSvc, boolean append, - boolean migrating, LicenseServiceBean licenseSvc) { + boolean migrating, LicenseServiceBean licenseSvc, DatasetTypeServiceBean datasetTypeSvc) { DatasetVersion dsv = new DatasetVersion(); JsonObject jsonld = decontextualizeJsonLD(jsonLDBody); if (migrating) { - Optional maybePid = GlobalIdServiceBean.parse(jsonld.getString("@id")); + Optional maybePid = PidProvider.parse(jsonld.getString("@id")); if (maybePid.isPresent()) { ds.setGlobalId(maybePid.get()); } else { @@ -96,6 +98,14 @@ public static Dataset updateDatasetMDFromJsonLD(Dataset ds, String jsonLDBody, //Store the metadatalanguage if sent - the caller needs to check whether it is allowed (as with any GlobalID) ds.setMetadataLanguage(jsonld.getString(JsonLDTerm.schemaOrg("inLanguage").getUrl(),null)); + String datasetTypeIn = jsonld.getString(JsonLDTerm.datasetType.getUrl(), DatasetType.DEFAULT_DATASET_TYPE); + DatasetType datasetType = datasetTypeSvc.getByName(datasetTypeIn); + if (datasetType != null) { + ds.setDatasetType(datasetType); + } else { + throw new BadRequestException("Invalid dataset type: " + datasetTypeIn); + } + dsv = updateDatasetVersionMDFromJsonLD(dsv, jsonld, metadataBlockSvc, datasetFieldSvc, append, migrating, licenseSvc); dsv.setDataset(ds); @@ -466,7 +476,6 @@ private static void addField(DatasetField dsf, JsonArray valArray, DatasetFieldT if(!datasetFieldSvc.isValidCVocValue(dsft, strValue)) { throw new BadRequestException("Invalid values submitted for " + dsft.getName() + " which is limited to specific vocabularies."); } - datasetFieldSvc.registerExternalTerm(cvocMap.get(dsft.getId()), strValue); } DatasetFieldValue datasetFieldValue = new DatasetFieldValue(); diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonLDTerm.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonLDTerm.java index 3193f762538..3166fa9dbfa 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonLDTerm.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonLDTerm.java @@ -52,6 +52,8 @@ public class JsonLDTerm { public static JsonLDTerm fileCount = JsonLDTerm.DVCore("fileCount"); public static JsonLDTerm maxFileSize = JsonLDTerm.DVCore("maxFileSize"); + public static JsonLDTerm datasetType = JsonLDTerm.DVCore("datasetType"); + public JsonLDTerm(JsonLDNamespace namespace, String term) { this.namespace = namespace; this.term = term; diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java index 984c607aac7..2f01c9bc2f2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java @@ -24,6 +24,8 @@ import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddressRange; import edu.harvard.iq.dataverse.authorization.groups.impl.maildomain.MailDomainGroup; +import edu.harvard.iq.dataverse.dataset.DatasetType; +import edu.harvard.iq.dataverse.dataset.DatasetTypeServiceBean; import edu.harvard.iq.dataverse.datasetutility.OptionalFileParams; import edu.harvard.iq.dataverse.harvest.client.HarvestingClient; import edu.harvard.iq.dataverse.license.License; @@ -38,7 +40,6 @@ import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -69,7 +70,9 @@ public class JsonParser { MetadataBlockServiceBean blockService; SettingsServiceBean settingsService; LicenseServiceBean licenseService; - HarvestingClient harvestingClient = null; + DatasetTypeServiceBean datasetTypeService; + HarvestingClient harvestingClient = null; + boolean allowHarvestingMissingCVV = false; /** * if lenient, we will accept alternate spellings for controlled vocabulary values @@ -83,16 +86,18 @@ public JsonParser(DatasetFieldServiceBean datasetFieldSvc, MetadataBlockServiceB this.settingsService = settingsService; } - public JsonParser(DatasetFieldServiceBean datasetFieldSvc, MetadataBlockServiceBean blockService, SettingsServiceBean settingsService, LicenseServiceBean licenseService) { - this(datasetFieldSvc, blockService, settingsService, licenseService, null); + public JsonParser(DatasetFieldServiceBean datasetFieldSvc, MetadataBlockServiceBean blockService, SettingsServiceBean settingsService, LicenseServiceBean licenseService, DatasetTypeServiceBean datasetTypeService) { + this(datasetFieldSvc, blockService, settingsService, licenseService, datasetTypeService, null); } - public JsonParser(DatasetFieldServiceBean datasetFieldSvc, MetadataBlockServiceBean blockService, SettingsServiceBean settingsService, LicenseServiceBean licenseService, HarvestingClient harvestingClient) { + public JsonParser(DatasetFieldServiceBean datasetFieldSvc, MetadataBlockServiceBean blockService, SettingsServiceBean settingsService, LicenseServiceBean licenseService, DatasetTypeServiceBean datasetTypeService, HarvestingClient harvestingClient) { this.datasetFieldSvc = datasetFieldSvc; this.blockService = blockService; this.settingsService = settingsService; this.licenseService = licenseService; + this.datasetTypeService = datasetTypeService; this.harvestingClient = harvestingClient; + this.allowHarvestingMissingCVV = harvestingClient != null && harvestingClient.getAllowHarvestingMissingCVV(); } public JsonParser() { @@ -318,8 +323,8 @@ public DatasetVersion parseDatasetVersion(JsonObject obj) throws JsonParseExcept public Dataset parseDataset(JsonObject obj) throws JsonParseException { Dataset dataset = new Dataset(); - dataset.setAuthority(obj.getString("authority", null) == null ? settingsService.getValueForKey(SettingsServiceBean.Key.Authority) : obj.getString("authority")); - dataset.setProtocol(obj.getString("protocol", null) == null ? settingsService.getValueForKey(SettingsServiceBean.Key.Protocol) : obj.getString("protocol")); + dataset.setAuthority(obj.getString("authority", null)); + dataset.setProtocol(obj.getString("protocol", null)); dataset.setIdentifier(obj.getString("identifier",null)); String mdl = obj.getString("metadataLanguage",null); if(mdl==null || settingsService.getBaseMetadataLanguageMap(new HashMap(), true).containsKey(mdl)) { @@ -327,7 +332,15 @@ public Dataset parseDataset(JsonObject obj) throws JsonParseException { }else { throw new JsonParseException("Specified metadatalanguage not allowed."); } - + String datasetTypeIn = obj.getString("datasetType", DatasetType.DEFAULT_DATASET_TYPE); + logger.fine("datasetTypeIn: " + datasetTypeIn); + DatasetType datasetType = datasetTypeService.getByName(datasetTypeIn); + if (datasetType != null) { + dataset.setDatasetType(datasetType); + } else { + throw new JsonParseException("Invalid dataset type: " + datasetTypeIn); + } + DatasetVersion dsv = new DatasetVersion(); dsv.setDataset(dataset); dsv = parseDatasetVersion(obj.getJsonObject("datasetVersion"), dsv); @@ -737,39 +750,23 @@ public DatasetField parseField(JsonObject json, Boolean testType) throws JsonPar ret.setDatasetFieldType(type); - - if (type.isCompound()) { - List vals = parseCompoundValue(type, json, testType); - for (DatasetFieldCompoundValue dsfcv : vals) { - dsfcv.setParentDatasetField(ret); - } - ret.setDatasetFieldCompoundValues(vals); + if (type.isCompound()) { + parseCompoundValue(ret, type, json, testType); } else if (type.isControlledVocabulary()) { - List vals = parseControlledVocabularyValue(type, json); - for (ControlledVocabularyValue cvv : vals) { - cvv.setDatasetFieldType(type); - } - ret.setControlledVocabularyValues(vals); - + parseControlledVocabularyValue(ret, type, json); } else { - // primitive - - List values = parsePrimitiveValue(type, json); - for (DatasetFieldValue val : values) { - val.setDatasetField(ret); - } - ret.setDatasetFieldValues(values); - } + parsePrimitiveValue(ret, type, json); + } return ret; } - public List parseCompoundValue(DatasetFieldType compoundType, JsonObject json) throws JsonParseException { - return parseCompoundValue(compoundType, json, true); + public void parseCompoundValue(DatasetField dsf, DatasetFieldType compoundType, JsonObject json) throws JsonParseException { + parseCompoundValue(dsf, compoundType, json, true); } - public List parseCompoundValue(DatasetFieldType compoundType, JsonObject json, Boolean testType) throws JsonParseException { + public void parseCompoundValue(DatasetField dsf, DatasetFieldType compoundType, JsonObject json, Boolean testType) throws JsonParseException { List vocabExceptions = new ArrayList<>(); List vals = new LinkedList<>(); if (compoundType.isAllowMultiples()) { @@ -836,18 +833,17 @@ public List parseCompoundValue(DatasetFieldType compo if (!vocabExceptions.isEmpty()) { throw new CompoundVocabularyException( "Invalid controlled vocabulary in compound field ", vocabExceptions, vals); } - return vals; + + for (DatasetFieldCompoundValue dsfcv : vals) { + dsfcv.setParentDatasetField(dsf); + } + dsf.setDatasetFieldCompoundValues(vals); } - public List parsePrimitiveValue(DatasetFieldType dft , JsonObject json) throws JsonParseException { + public void parsePrimitiveValue(DatasetField dsf, DatasetFieldType dft , JsonObject json) throws JsonParseException { Map cvocMap = datasetFieldSvc.getCVocConf(true); - boolean extVocab=false; - if(cvocMap.containsKey(dft.getId())) { - extVocab=true; - } - - + boolean extVocab = cvocMap.containsKey(dft.getId()); List vals = new LinkedList<>(); if (dft.isAllowMultiples()) { try { @@ -856,14 +852,13 @@ public List parsePrimitiveValue(DatasetFieldType dft , JsonOb throw new JsonParseException("Invalid values submitted for " + dft.getName() + ". It should be an array of values."); } for (JsonString val : json.getJsonArray("value").getValuesAs(JsonString.class)) { - DatasetFieldValue datasetFieldValue = new DatasetFieldValue(); + DatasetFieldValue datasetFieldValue = new DatasetFieldValue(dsf); datasetFieldValue.setDisplayOrder(vals.size() - 1); datasetFieldValue.setValue(val.getString().trim()); if(extVocab) { if(!datasetFieldSvc.isValidCVocValue(dft, datasetFieldValue.getValue())) { throw new JsonParseException("Invalid values submitted for " + dft.getName() + " which is limited to specific vocabularies."); } - datasetFieldSvc.registerExternalTerm(cvocMap.get(dft.getId()), datasetFieldValue.getValue()); } vals.add(datasetFieldValue); } @@ -875,16 +870,16 @@ public List parsePrimitiveValue(DatasetFieldType dft , JsonOb } DatasetFieldValue datasetFieldValue = new DatasetFieldValue(); datasetFieldValue.setValue(json.getString("value", "").trim()); + datasetFieldValue.setDatasetField(dsf); if(extVocab) { if(!datasetFieldSvc.isValidCVocValue(dft, datasetFieldValue.getValue())) { throw new JsonParseException("Invalid values submitted for " + dft.getName() + " which is limited to specific vocabularies."); } - datasetFieldSvc.registerExternalTerm(cvocMap.get(dft.getId()), datasetFieldValue.getValue()); } vals.add(datasetFieldValue); } - return vals; + dsf.setDatasetFieldValues(vals); } public Workflow parseWorkflow(JsonObject json) throws JsonParseException { @@ -929,31 +924,35 @@ private String jsonValueToString(JsonValue jv) { default: return jv.toString(); } } - - public List parseControlledVocabularyValue(DatasetFieldType cvvType, JsonObject json) throws JsonParseException { + + public void parseControlledVocabularyValue(DatasetField dsf, DatasetFieldType cvvType, JsonObject json) throws JsonParseException { + List vals = new LinkedList<>(); try { if (cvvType.isAllowMultiples()) { try { json.getJsonArray("value").getValuesAs(JsonObject.class); } catch (ClassCastException cce) { throw new JsonParseException("Invalid values submitted for " + cvvType.getName() + ". It should be an array of values."); - } - List vals = new LinkedList<>(); + } for (JsonString strVal : json.getJsonArray("value").getValuesAs(JsonString.class)) { String strValue = strVal.getString(); ControlledVocabularyValue cvv = datasetFieldSvc.findControlledVocabularyValueByDatasetFieldTypeAndStrValue(cvvType, strValue, lenient); if (cvv == null) { - throw new ControlledVocabularyException("Value '" + strValue + "' does not exist in type '" + cvvType.getName() + "'", cvvType, strValue); - } - // Only add value to the list if it is not a duplicate - if (strValue.equals("Other")) { - System.out.println("vals = " + vals + ", contains: " + vals.contains(cvv)); + if (allowHarvestingMissingCVV) { + // we need to process these as primitive values + logger.warning("Value '" + strValue + "' does not exist in type '" + cvvType.getName() + "'. Processing as primitive per setting override."); + parsePrimitiveValue(dsf, cvvType, json); + return; + } else { + throw new ControlledVocabularyException("Value '" + strValue + "' does not exist in type '" + cvvType.getName() + "'", cvvType, strValue); + } } + cvv.setDatasetFieldType(cvvType); + // Only add value to the list if it is not a duplicate if (!vals.contains(cvv)) { vals.add(cvv); } } - return vals; } else { try { @@ -964,13 +963,23 @@ public List parseControlledVocabularyValue(DatasetFie String strValue = json.getString("value", ""); ControlledVocabularyValue cvv = datasetFieldSvc.findControlledVocabularyValueByDatasetFieldTypeAndStrValue(cvvType, strValue, lenient); if (cvv == null) { - throw new ControlledVocabularyException("Value '" + strValue + "' does not exist in type '" + cvvType.getName() + "'", cvvType, strValue); + if (allowHarvestingMissingCVV) { + // we need to process this as a primitive value + logger.warning(">>>> Value '" + strValue + "' does not exist in type '" + cvvType.getName() + "'. Processing as primitive per setting override."); + parsePrimitiveValue(dsf, cvvType , json); + return; + } else { + throw new ControlledVocabularyException("Value '" + strValue + "' does not exist in type '" + cvvType.getName() + "'", cvvType, strValue); + } } - return Collections.singletonList(cvv); + cvv.setDatasetFieldType(cvvType); + vals.add(cvv); } } catch (ClassCastException cce) { throw new JsonParseException("Invalid values submitted for " + cvvType.getName()); } + + dsf.setControlledVocabularyValues(vals); } Date parseDate(String str) throws ParseException { @@ -1001,6 +1010,7 @@ public String parseHarvestingClient(JsonObject obj, HarvestingClient harvestingC harvestingClient.setMetadataPrefix(obj.getString("metadataFormat",null)); harvestingClient.setHarvestingSet(obj.getString("set",null)); harvestingClient.setCustomHttpHeaders(obj.getString("customHeaders", null)); + harvestingClient.setAllowHarvestingMissingCVV(obj.getBoolean("allowHarvestingMissingCVV", false)); return dataverseAlias; } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index 2eaf6b64579..34c8fc5c6a6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -17,6 +17,7 @@ import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.branding.BrandingUtil; import edu.harvard.iq.dataverse.dataaccess.DataAccess; +import edu.harvard.iq.dataverse.dataset.DatasetType; import edu.harvard.iq.dataverse.dataset.DatasetUtil; import edu.harvard.iq.dataverse.datavariable.CategoryMetadata; import edu.harvard.iq.dataverse.datavariable.DataVariable; @@ -78,7 +79,7 @@ public static void injectSettingsService(SettingsServiceBean ssb, DatasetFieldSe } public JsonPrinter() { - + } public static final BriefJsonPrinter brief = new BriefJsonPrinter(); @@ -121,7 +122,7 @@ public static JsonObjectBuilder json(AuthenticatedUser authenticatedUser) { .add("authenticationProviderId", authenticatedUser.getAuthenticatedUserLookup().getAuthenticationProviderId()); return builder; } - + public static JsonObjectBuilder json(RoleAssignment ra) { return jsonObjectBuilder() .add("id", ra.getId()) @@ -146,7 +147,7 @@ public static JsonObjectBuilder json(DatasetLock lock) { .add("dataset", lock.getDataset().getGlobalId().asString()) .add("message", lock.getInfo()); } - + public static JsonObjectBuilder json( RoleAssigneeDisplayInfo d ) { return jsonObjectBuilder() .add("title", d.getTitle()) @@ -170,17 +171,17 @@ public static JsonObjectBuilder json(IpGroup grp) { .add("id", grp.getId() ) .add("name", grp.getDisplayName() ) .add("description", grp.getDescription() ); - + if ( ! singles.isEmpty() ) { bld.add("addresses", asJsonArray(singles) ); } - + if ( ! ranges.isEmpty() ) { JsonArrayBuilder rangesBld = Json.createArrayBuilder(); ranges.forEach( r -> rangesBld.add( Json.createArrayBuilder().add(r.get(0)).add(r.get(1))) ); bld.add("ranges", rangesBld ); } - + return bld; } @@ -191,7 +192,7 @@ public static JsonObjectBuilder json(ShibGroup grp) { .add("pattern", grp.getPattern()) .add("id", grp.getId()); } - + public static JsonObjectBuilder json(MailDomainGroup grp) { JsonObjectBuilder bld = jsonObjectBuilder() .add("alias", grp.getPersistedGroupAlias() ) @@ -234,14 +235,14 @@ public static JsonObjectBuilder json(DataverseRole role) { return bld; } - + public static JsonObjectBuilder json(Workflow wf){ JsonObjectBuilder bld = jsonObjectBuilder(); bld.add("name", wf.getName()); if ( wf.getId() != null ) { bld.add("id", wf.getId()); } - + if ( wf.getSteps()!=null && !wf.getSteps().isEmpty()) { JsonArrayBuilder arr = Json.createArrayBuilder(); for ( WorkflowStepData stp : wf.getSteps() ) { @@ -252,25 +253,27 @@ public static JsonObjectBuilder json(Workflow wf){ } bld.add("steps", arr ); } - + return bld; } - + public static JsonObjectBuilder json(Dataverse dv) { - return json(dv, false); + return json(dv, false, false); } //TODO: Once we upgrade to Java EE 8 we can remove objects from the builder, and this email removal can be done in a better place. - public static JsonObjectBuilder json(Dataverse dv, Boolean hideEmail) { + public static JsonObjectBuilder json(Dataverse dv, Boolean hideEmail, Boolean returnOwners) { JsonObjectBuilder bld = jsonObjectBuilder() .add("id", dv.getId()) .add("alias", dv.getAlias()) .add("name", dv.getName()) .add("affiliation", dv.getAffiliation()); - if(!hideEmail) { + if(!hideEmail) { bld.add("dataverseContacts", JsonPrinter.json(dv.getDataverseContacts())); } - + if (returnOwners){ + bld.add("isPartOf", getOwnersFromDvObject(dv)); + } bld.add("permissionRoot", dv.isPermissionRoot()) .add("description", dv.getDescription()) .add("dataverseType", dv.getDataverseType().name()); @@ -289,6 +292,12 @@ public static JsonObjectBuilder json(Dataverse dv, Boolean hideEmail) { if (dv.getFilePIDsEnabled() != null) { bld.add("filePIDsEnabled", dv.getFilePIDsEnabled()); } + bld.add("isReleased", dv.isReleased()); + + List inputLevels = dv.getDataverseFieldTypeInputLevels(); + if(!inputLevels.isEmpty()) { + bld.add("inputLevels", JsonPrinter.jsonDataverseFieldTypeInputLevels(inputLevels)); + } return bld; } @@ -304,6 +313,56 @@ public static JsonArrayBuilder json(List dataverseContacts) { return jsonArrayOfContacts; } + public static JsonObjectBuilder getOwnersFromDvObject(DvObject dvObject){ + return getOwnersFromDvObject(dvObject, null); + } + + public static JsonObjectBuilder getOwnersFromDvObject(DvObject dvObject, DatasetVersion dsv) { + List ownerList = new ArrayList(); + dvObject = dvObject.getOwner(); // We're going to ignore the object itself + //Get "root" to top of list + while (dvObject != null) { + ownerList.add(0, dvObject); + dvObject = dvObject.getOwner(); + } + //then work "inside out" + JsonObjectBuilder saved = null; + for (DvObject dvo : ownerList) { + saved = addEmbeddedOwnerObject(dvo, saved, dsv); + } + return saved; + } + + private static JsonObjectBuilder addEmbeddedOwnerObject(DvObject dvo, JsonObjectBuilder isPartOf, DatasetVersion dsv ) { + JsonObjectBuilder ownerObject = jsonObjectBuilder(); + + if (dvo.isInstanceofDataverse()) { + ownerObject.add("type", "DATAVERSE"); + Dataverse in = (Dataverse) dvo; + ownerObject.add("identifier", in.getAlias()); + } + + if (dvo.isInstanceofDataset()) { + ownerObject.add("type", "DATASET"); + if (dvo.getGlobalId() != null) { + ownerObject.add("persistentIdentifier", dvo.getGlobalId().asString()); + } + ownerObject.add("identifier", dvo.getId()); + String versionString = dsv == null ? "" : dsv.getFriendlyVersionNumber(); + if (!versionString.isEmpty()){ + ownerObject.add("version", versionString); + } + } + + ownerObject.add("displayName", dvo.getDisplayName()); + + if (isPartOf != null) { + ownerObject.add("isPartOf", isPartOf); + } + + return ownerObject; + } + public static JsonObjectBuilder json( DataverseTheme theme ) { final NullSafeJsonBuilder baseObject = jsonObjectBuilder() .add("id", theme.getId() ) @@ -327,7 +386,11 @@ public static JsonObjectBuilder json(BuiltinUser user) { .add("userName", user.getUserName()); } - public static JsonObjectBuilder json(Dataset ds) { + public static JsonObjectBuilder json(Dataset ds){ + return json(ds, false); + } + + public static JsonObjectBuilder json(Dataset ds, Boolean returnOwners) { JsonObjectBuilder bld = jsonObjectBuilder() .add("id", ds.getId()) .add("identifier", ds.getIdentifier()) @@ -340,6 +403,10 @@ public static JsonObjectBuilder json(Dataset ds) { if (DvObjectContainer.isMetadataLanguageSet(ds.getMetadataLanguage())) { bld.add("metadataLanguage", ds.getMetadataLanguage()); } + if (returnOwners){ + bld.add("isPartOf", getOwnersFromDvObject(ds)); + } + bld.add("datasetType", ds.getDatasetType().getName()); return bld; } @@ -352,30 +419,33 @@ public static JsonObjectBuilder json(FileDetailsHolder ds) { } public static JsonObjectBuilder json(DatasetVersion dsv, boolean includeFiles) { - return json(dsv, null, includeFiles); + return json(dsv, null, includeFiles, false); } - public static JsonObjectBuilder json(DatasetVersion dsv, List anonymizedFieldTypeNamesList, boolean includeFiles) { - /* return json(dsv, null, includeFiles, null); - } - public static JsonObjectBuilder json(DatasetVersion dsv, List anonymizedFieldTypeNamesList, boolean includeFiles, Long numberOfFiles) {*/ + public static JsonObjectBuilder json(DatasetVersion dsv, List anonymizedFieldTypeNamesList, + boolean includeFiles, boolean returnOwners) { Dataset dataset = dsv.getDataset(); JsonObjectBuilder bld = jsonObjectBuilder() .add("id", dsv.getId()).add("datasetId", dataset.getId()) .add("datasetPersistentId", dataset.getGlobalId().asString()) .add("storageIdentifier", dataset.getStorageIdentifier()) - .add("versionNumber", dsv.getVersionNumber()).add("versionMinorNumber", dsv.getMinorVersionNumber()) - .add("versionState", dsv.getVersionState().name()).add("versionNote", dsv.getVersionNote()) - .add("archiveNote", dsv.getArchiveNote()).add("deaccessionLink", dsv.getDeaccessionLink()) - .add("distributionDate", dsv.getDistributionDate()).add("productionDate", dsv.getProductionDate()) + .add("versionNumber", dsv.getVersionNumber()) + .add("versionMinorNumber", dsv.getMinorVersionNumber()) + .add("versionState", dsv.getVersionState().name()) + .add("latestVersionPublishingState", dataset.getLatestVersion().getVersionState().name()) + .add("versionNote", dsv.getVersionNote()) + .add("archiveNote", dsv.getArchiveNote()) + .add("deaccessionLink", dsv.getDeaccessionLink()) + .add("distributionDate", dsv.getDistributionDate()) + .add("productionDate", dsv.getProductionDate()) .add("UNF", dsv.getUNF()).add("archiveTime", format(dsv.getArchiveTime())) - .add("lastUpdateTime", format(dsv.getLastUpdateTime())).add("releaseTime", format(dsv.getReleaseTime())) + .add("lastUpdateTime", format(dsv.getLastUpdateTime())) + .add("releaseTime", format(dsv.getReleaseTime())) .add("createTime", format(dsv.getCreateTime())) .add("alternativePersistentId", dataset.getAlternativePersistentIdentifier()) .add("publicationDate", dataset.getPublicationDateFormattedYYYYMMDD()) .add("citationDate", dataset.getCitationDateFormattedYYYYMMDD()); - //.add("numberOfFiles", numberOfFiles); - + License license = DatasetUtil.getLicense(dsv); if (license != null) { bld.add("license", jsonLicense(dsv)); @@ -403,6 +473,9 @@ public static JsonObjectBuilder json(DatasetVersion dsv, List anonymized jsonByBlocks(dsv.getDatasetFields(), anonymizedFieldTypeNamesList) : jsonByBlocks(dsv.getDatasetFields()) ); + if(returnOwners){ + bld.add("isPartOf", getOwnersFromDvObject(dataset)); + } if (includeFiles) { bld.add("files", jsonFileMetadatas(dsv.getFileMetadatas())); } @@ -411,19 +484,19 @@ public static JsonObjectBuilder json(DatasetVersion dsv, List anonymized } public static JsonObjectBuilder jsonDataFileList(List dataFiles){ - + if (dataFiles==null){ throw new NullPointerException("dataFiles cannot be null"); } - + JsonObjectBuilder bld = jsonObjectBuilder(); - - + + List dataFileList = dataFiles.stream() .map(x -> x.getFileMetadata()) .collect(Collectors.toList()); - + bld.add("files", jsonFileMetadatas(dataFileList)); return bld; @@ -512,7 +585,7 @@ public static JsonObjectBuilder json(MetadataBlock block, List fie blockBld.add("displayName", block.getDisplayName()); blockBld.add("name", block.getName()); - + final JsonArrayBuilder fieldsArray = Json.createArrayBuilder(); Map cvocMap = (datasetFieldService==null) ? new HashMap() :datasetFieldService.getCVocConf(true); DatasetFieldWalker.walk(fields, settingsService, cvocMap, new DatasetFieldsToJson(fieldsArray, anonymizedFieldTypeNamesList)); @@ -521,6 +594,18 @@ public static JsonObjectBuilder json(MetadataBlock block, List fie return blockBld; } + public static JsonArrayBuilder json(List metadataBlocks, boolean returnDatasetFieldTypes, boolean printOnlyDisplayedOnCreateDatasetFieldTypes) { + return json(metadataBlocks, returnDatasetFieldTypes, printOnlyDisplayedOnCreateDatasetFieldTypes, null); + } + + public static JsonArrayBuilder json(List metadataBlocks, boolean returnDatasetFieldTypes, boolean printOnlyDisplayedOnCreateDatasetFieldTypes, Dataverse ownerDataverse) { + JsonArrayBuilder arrayBuilder = Json.createArrayBuilder(); + for (MetadataBlock metadataBlock : metadataBlocks) { + arrayBuilder.add(returnDatasetFieldTypes ? json(metadataBlock, printOnlyDisplayedOnCreateDatasetFieldTypes, ownerDataverse) : brief.json(metadataBlock)); + } + return arrayBuilder; + } + public static String typeClassString(DatasetFieldType typ) { if (typ.isControlledVocabulary()) { return "controlledVocabulary"; @@ -543,26 +628,54 @@ public static JsonObject json(DatasetField dfv) { } } - public static JsonObjectBuilder json(MetadataBlock blk) { - JsonObjectBuilder bld = jsonObjectBuilder(); - bld.add("id", blk.getId()); - bld.add("name", blk.getName()); - bld.add("displayName", blk.getDisplayName()); + public static JsonObjectBuilder json(MetadataBlock metadataBlock) { + return json(metadataBlock, false, null); + } - JsonObjectBuilder fieldsBld = jsonObjectBuilder(); - for (DatasetFieldType df : new TreeSet<>(blk.getDatasetFieldTypes())) { - fieldsBld.add(df.getName(), JsonPrinter.json(df)); + public static JsonObjectBuilder json(MetadataBlock metadataBlock, boolean printOnlyDisplayedOnCreateDatasetFieldTypes, Dataverse ownerDataverse) { + JsonObjectBuilder jsonObjectBuilder = jsonObjectBuilder() + .add("id", metadataBlock.getId()) + .add("name", metadataBlock.getName()) + .add("displayName", metadataBlock.getDisplayName()) + .add("displayOnCreate", metadataBlock.isDisplayOnCreate()); + + Set datasetFieldTypes; + + if (ownerDataverse != null) { + datasetFieldTypes = new TreeSet<>(datasetFieldService.findAllInMetadataBlockAndDataverse( + metadataBlock, ownerDataverse, printOnlyDisplayedOnCreateDatasetFieldTypes)); + } else { + datasetFieldTypes = printOnlyDisplayedOnCreateDatasetFieldTypes + ? new TreeSet<>(datasetFieldService.findAllDisplayedOnCreateInMetadataBlock(metadataBlock)) + : new TreeSet<>(metadataBlock.getDatasetFieldTypes()); } - bld.add("fields", fieldsBld); + JsonObjectBuilder fieldsBuilder = Json.createObjectBuilder(); + for (DatasetFieldType datasetFieldType : datasetFieldTypes) { + fieldsBuilder.add(datasetFieldType.getName(), json(datasetFieldType, ownerDataverse)); + } - return bld; + jsonObjectBuilder.add("fields", fieldsBuilder); + return jsonObjectBuilder; + } + + public static JsonArrayBuilder jsonDatasetFieldTypes(List fields) { + JsonArrayBuilder fieldsJson = Json.createArrayBuilder(); + for (DatasetFieldType field : fields) { + fieldsJson.add(JsonPrinter.json(field)); + } + return fieldsJson; } public static JsonObjectBuilder json(DatasetFieldType fld) { + return json(fld, null); + } + + public static JsonObjectBuilder json(DatasetFieldType fld, Dataverse ownerDataverse) { JsonObjectBuilder fieldsBld = jsonObjectBuilder(); fieldsBld.add("name", fld.getName()); fieldsBld.add("displayName", fld.getDisplayName()); + fieldsBld.add("displayOnCreate", fld.isDisplayOnCreate()); fieldsBld.add("title", fld.getTitle()); fieldsBld.add("type", fld.getFieldType().toString()); fieldsBld.add("typeClass", typeClassString(fld)); @@ -571,8 +684,11 @@ public static JsonObjectBuilder json(DatasetFieldType fld) { fieldsBld.add("multiple", fld.isAllowMultiples()); fieldsBld.add("isControlledVocabulary", fld.isControlledVocabulary()); fieldsBld.add("displayFormat", fld.getDisplayFormat()); - fieldsBld.add("isRequired", fld.isRequired()); fieldsBld.add("displayOrder", fld.getDisplayOrder()); + + boolean requiredInOwnerDataverse = ownerDataverse != null && ownerDataverse.isDatasetFieldTypeRequiredAsInputLevel(fld.getId()); + fieldsBld.add("isRequired", requiredInOwnerDataverse || fld.isRequired()); + if (fld.isControlledVocabulary()) { // If the field has a controlled vocabulary, // add all values to the resulting JSON @@ -582,10 +698,11 @@ public static JsonObjectBuilder json(DatasetFieldType fld) { } fieldsBld.add("controlledVocabularyValues", jab); } + if (!fld.getChildDatasetFieldTypes().isEmpty()) { JsonObjectBuilder subFieldsBld = jsonObjectBuilder(); for (DatasetFieldType subFld : fld.getChildDatasetFieldTypes()) { - subFieldsBld.add(subFld.getName(), JsonPrinter.json(subFld)); + subFieldsBld.add(subFld.getName(), JsonPrinter.json(subFld, ownerDataverse)); } fieldsBld.add("childFields", subFieldsBld); } @@ -593,29 +710,40 @@ public static JsonObjectBuilder json(DatasetFieldType fld) { return fieldsBld; } - public static JsonObjectBuilder json(FileMetadata fmd) { - return jsonObjectBuilder() + public static JsonObjectBuilder json(FileMetadata fmd){ + return json(fmd, false, false); + } + + public static JsonObjectBuilder json(FileMetadata fmd, boolean returnOwners, boolean printDatasetVersion) { + NullSafeJsonBuilder builder = jsonObjectBuilder(); + // deprecated: .add("category", fmd.getCategory()) - // TODO: uh, figure out what to do here... it's deprecated - // in a sense that there's no longer the category field in the - // fileMetadata object; but there are now multiple, oneToMany file + // TODO: uh, figure out what to do here... it's deprecated + // in a sense that there's no longer the category field in the + // fileMetadata object; but there are now multiple, oneToMany file // categories - and we probably need to export them too!) -- L.A. 4.5 - // DONE: catgegories by name - .add("description", fmd.getDescription()) + // DONE: catgegories by name + builder.add("description", fmd.getDescription()) .add("label", fmd.getLabel()) // "label" is the filename - .add("restricted", fmd.isRestricted()) + .add("restricted", fmd.isRestricted()) .add("directoryLabel", fmd.getDirectoryLabel()) .add("version", fmd.getVersion()) .add("datasetVersionId", fmd.getDatasetVersion().getId()) .add("categories", getFileCategories(fmd)) - .add("dataFile", JsonPrinter.json(fmd.getDataFile(), fmd, false)); + .add("dataFile", JsonPrinter.json(fmd.getDataFile(), fmd, false, returnOwners)); + + if (printDatasetVersion) { + builder.add("datasetVersion", json(fmd.getDatasetVersion(), false)); + } + + return builder; } - public static JsonObjectBuilder json(AuxiliaryFile auxFile) { + public static JsonObjectBuilder json(AuxiliaryFile auxFile) { return jsonObjectBuilder() .add("formatTag", auxFile.getFormatTag()) .add("formatVersion", auxFile.getFormatVersion()) // "label" is the filename - .add("origin", auxFile.getOrigin()) + .add("origin", auxFile.getOrigin()) .add("isPublic", auxFile.getIsPublic()) .add("type", auxFile.getType()) .add("contentType", auxFile.getContentType()) @@ -623,11 +751,16 @@ public static JsonObjectBuilder json(AuxiliaryFile auxFile) { .add("checksum", auxFile.getChecksum()) .add("dataFile", JsonPrinter.json(auxFile.getDataFile())); } + public static JsonObjectBuilder json(DataFile df) { return JsonPrinter.json(df, null, false); } - - public static JsonObjectBuilder json(DataFile df, FileMetadata fileMetadata, boolean forExportDataProvider) { + + public static JsonObjectBuilder json(DataFile df, FileMetadata fileMetadata, boolean forExportDataProvider){ + return json(df, fileMetadata, forExportDataProvider, false); + } + + public static JsonObjectBuilder json(DataFile df, FileMetadata fileMetadata, boolean forExportDataProvider, boolean returnOwners) { // File names are no longer stored in the DataFile entity; // (they are instead in the FileMetadata (as "labels") - this way // the filename can change between versions... @@ -637,13 +770,13 @@ public static JsonObjectBuilder json(DataFile df, FileMetadata fileMetadata, boo // *correct* file name - i.e., that it comes from the right version. // (TODO...? L.A. 4.5, Aug 7 2016) String fileName = null; - + if (fileMetadata == null){ // Note that this may not necessarily grab the file metadata from the // version *you want*! (L.A.) fileMetadata = df.getFileMetadata(); } - + fileName = fileMetadata.getLabel(); GlobalId filePid = df.getGlobalId(); String pidURL = (filePid!=null)? filePid.asURL(): null; @@ -651,6 +784,7 @@ public static JsonObjectBuilder json(DataFile df, FileMetadata fileMetadata, boo String pidString = (filePid!=null)? filePid.asString(): ""; JsonObjectBuilder embargo = df.getEmbargo() != null ? JsonPrinter.json(df.getEmbargo()) : null; + JsonObjectBuilder retention = df.getRetention() != null ? JsonPrinter.json(df.getRetention()) : null; NullSafeJsonBuilder builder = jsonObjectBuilder() .add("id", df.getId()) @@ -663,6 +797,7 @@ public static JsonObjectBuilder json(DataFile df, FileMetadata fileMetadata, boo .add("description", fileMetadata.getDescription()) .add("categories", getFileCategories(fileMetadata)) .add("embargo", embargo) + .add("retention", retention) //.add("released", df.isReleased()) .add("storageIdentifier", df.getStorageIdentifier()) .add("originalFileFormat", df.getOriginalFileFormat()) @@ -703,9 +838,12 @@ public static JsonObjectBuilder json(DataFile df, FileMetadata fileMetadata, boo ? JsonPrinter.jsonVarGroup(fileMetadata.getVarGroups()) : null); } + if (returnOwners){ + builder.add("isPartOf", getOwnersFromDvObject(df, fileMetadata.getDatasetVersion())); + } return builder; } - + //Started from https://github.com/RENCI-NRIG/dataverse/, i.e. https://github.com/RENCI-NRIG/dataverse/commit/2b5a1225b42cf1caba85e18abfeb952171c6754a public static JsonArrayBuilder jsonDT(List ldt) { JsonArrayBuilder ldtArr = Json.createArrayBuilder(); @@ -746,8 +884,8 @@ public static JsonObjectBuilder json(DataVariable dv) { .add("variableFormatType", dv.getType().name()) // varFormat .add("formatCategory", dv.getFormatCategory()) .add("format", dv.getFormat()) - .add("isOrderedCategorical", dv.isOrderedCategorical()) - .add("fileOrder", dv.getFileOrder()) + .add("isOrderedCategorical", dv.isOrderedCategorical()) + .add("fileOrder", dv.getFileOrder()) .add("UNF",dv.getUnf()) .add("fileStartPosition", dv.getFileStartPosition()) .add("fileEndPosition", dv.getFileEndPosition()) @@ -775,7 +913,7 @@ private static JsonArrayBuilder jsonInvalidRanges(Collection inva .add("hasEndValueType", vr.getEndValueType()!=null) .add("endValueTypeMax", vr.isEndValueTypeMax()) .add("endValueTypeMaxExcl", vr.isEndValueTypeMaxExcl()); - + invRanges.add(job); } return invRanges; @@ -807,7 +945,7 @@ private static JsonArrayBuilder jsonCatStat(Collection catStat } return catArr; } - + private static JsonArrayBuilder jsonVarGroup(List varGroups) { JsonArrayBuilder vgArr = Json.createArrayBuilder(); for (VarGroup vg : varGroups) { @@ -821,7 +959,7 @@ private static JsonArrayBuilder jsonVarGroup(List varGroups) { } return vgArr; } - + private static JsonArrayBuilder jsonVarMetadata(Collection varMetadatas) { JsonArrayBuilder vmArr = Json.createArrayBuilder(); for (VariableMetadata vm : varMetadatas) { @@ -842,7 +980,7 @@ private static JsonArrayBuilder jsonVarMetadata(Collection var } return vmArr; } - + private static JsonArrayBuilder json(Collection categoriesMetadata) { JsonArrayBuilder cmArr = Json.createArrayBuilder(); for(CategoryMetadata cm: categoriesMetadata) { @@ -856,9 +994,9 @@ private static JsonArrayBuilder json(Collection categoriesMeta public static JsonObjectBuilder json(HarvestingClient harvestingClient) { if (harvestingClient == null) { - return null; + return null; } - + return jsonObjectBuilder().add("nickName", harvestingClient.getName()). add("dataverseAlias", harvestingClient.getDataverse().getAlias()). add("type", harvestingClient.getHarvestType()). @@ -871,6 +1009,7 @@ public static JsonObjectBuilder json(HarvestingClient harvestingClient) { add("schedule", harvestingClient.isScheduled() ? harvestingClient.getScheduleDescription() : "none"). add("status", harvestingClient.isHarvestingNow() ? "inProgress" : "inActive"). add("customHeaders", harvestingClient.getCustomHttpHeaders()). + add("allowHarvestingMissingCVV", harvestingClient.getAllowHarvestingMissingCVV()). add("lastHarvest", harvestingClient.getLastHarvestTime() == null ? null : harvestingClient.getLastHarvestTime().toString()). add("lastResult", harvestingClient.getLastResult()). add("lastSuccessful", harvestingClient.getLastSuccessfulHarvestTime() == null ? null : harvestingClient.getLastSuccessfulHarvestTime().toString()). @@ -879,7 +1018,7 @@ public static JsonObjectBuilder json(HarvestingClient harvestingClient) { add("lastDatasetsDeleted", harvestingClient.getLastDeletedDatasetCount()). // == null ? "N/A" : harvestingClient.getLastDeletedDatasetCount().toString()). add("lastDatasetsFailed", harvestingClient.getLastFailedDatasetCount()); // == null ? "N/A" : harvestingClient.getLastFailedDatasetCount().toString()); } - + public static String format(Date d) { return (d == null) ? null : Util.getDateTimeFormat().format(d); } @@ -916,7 +1055,7 @@ public static JsonArrayBuilder getTabularFileTags(DataFile df) { } return tabularTags; } - + private static class DatasetFieldsToJson implements DatasetFieldWalker.Listener { Deque objectStack = new LinkedList<>(); @@ -1052,11 +1191,20 @@ public static JsonObjectBuilder json( ExplicitGroup eg ) { .add("displayName", eg.getDisplayName()) .add("containedRoleAssignees", ras); } - - public static JsonObjectBuilder json( DataverseFacet aFacet ) { + + public static JsonArrayBuilder jsonDataverseFacets(List dataverseFacets) { + JsonArrayBuilder dataverseFacetsJson = Json.createArrayBuilder(); + for(DataverseFacet facet: dataverseFacets) { + dataverseFacetsJson.add(json(facet)); + } + return dataverseFacetsJson; + } + + public static JsonObjectBuilder json(DataverseFacet aFacet) { return jsonObjectBuilder() .add("id", String.valueOf(aFacet.getId())) // TODO should just be id I think - .add("name", aFacet.getDatasetFieldType().getDisplayName()); + .add("displayName", aFacet.getDatasetFieldType().getDisplayName()) + .add("name", aFacet.getDatasetFieldType().getName()); } public static JsonObjectBuilder json(Embargo embargo) { @@ -1064,6 +1212,11 @@ public static JsonObjectBuilder json(Embargo embargo) { embargo.getReason()); } + public static JsonObjectBuilder json(Retention retention) { + return jsonObjectBuilder().add("dateUnavailable", retention.getDateUnavailable().toString()).add("reason", + retention.getReason()); + } + public static JsonObjectBuilder json(License license) { return jsonObjectBuilder() .add("id", license.getId()) @@ -1189,7 +1342,7 @@ public static JsonObjectBuilder getChecksumTypeAndValue(DataFile.ChecksumType ch return null; } } - + /** * Takes a map, returns a Json object for this map. * If map is {@code null}, returns {@code null}. @@ -1232,4 +1385,32 @@ private static JsonObjectBuilder jsonLicense(DatasetVersion dsv) { } return licenseJsonObjectBuilder; } + + public static JsonArrayBuilder jsonDataverseFieldTypeInputLevels(List inputLevels) { + JsonArrayBuilder jsonArrayOfInputLevels = Json.createArrayBuilder(); + for (DataverseFieldTypeInputLevel inputLevel : inputLevels) { + NullSafeJsonBuilder inputLevelJsonObject = NullSafeJsonBuilder.jsonObjectBuilder(); + inputLevelJsonObject.add("datasetFieldTypeName", inputLevel.getDatasetFieldType().getName()); + inputLevelJsonObject.add("required", inputLevel.isRequired()); + inputLevelJsonObject.add("include", inputLevel.isInclude()); + jsonArrayOfInputLevels.add(inputLevelJsonObject); + } + return jsonArrayOfInputLevels; + } + + public static JsonArrayBuilder jsonDataverseInputLevels(List inputLevels) { + JsonArrayBuilder inputLevelsArrayBuilder = Json.createArrayBuilder(); + for (DataverseFieldTypeInputLevel inputLevel : inputLevels) { + inputLevelsArrayBuilder.add(jsonDataverseInputLevel(inputLevel)); + } + return inputLevelsArrayBuilder; + } + + private static JsonObjectBuilder jsonDataverseInputLevel(DataverseFieldTypeInputLevel inputLevel) { + JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder(); + jsonObjectBuilder.add("datasetFieldTypeName", inputLevel.getDatasetFieldType().getName()); + jsonObjectBuilder.add("required", inputLevel.isRequired()); + jsonObjectBuilder.add("include", inputLevel.isInclude()); + return jsonObjectBuilder; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/xml/XmlValidator.java b/src/main/java/edu/harvard/iq/dataverse/util/xml/XmlValidator.java index 586ca50b6fd..cec64ab95b7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/xml/XmlValidator.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/xml/XmlValidator.java @@ -24,7 +24,12 @@ public class XmlValidator { private static final Logger logger = Logger.getLogger(XmlValidator.class.getCanonicalName()); public static boolean validateXmlSchema(String fileToValidate, URL schemaToValidateAgainst) throws MalformedURLException, SAXException, IOException { + Source xmlFile = new StreamSource(new File(fileToValidate)); + return validateXmlSchema(xmlFile, schemaToValidateAgainst); + } + + public static boolean validateXmlSchema(Source xmlFile, URL schemaToValidateAgainst) throws MalformedURLException, SAXException, IOException { SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); Schema schema = schemaFactory.newSchema(schemaToValidateAgainst); Validator validator = schema.newValidator(); diff --git a/src/main/java/edu/harvard/iq/dataverse/util/xml/XmlWriterUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/xml/XmlWriterUtil.java new file mode 100644 index 00000000000..8ec426ead1f --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/util/xml/XmlWriterUtil.java @@ -0,0 +1,167 @@ +package edu.harvard.iq.dataverse.util.xml; + +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import org.apache.commons.lang3.StringUtils; + +import edu.harvard.iq.dataverse.ControlledVocabularyValue; +import edu.harvard.iq.dataverse.DvObjectContainer; +import edu.harvard.iq.dataverse.api.dto.DatasetVersionDTO; +import edu.harvard.iq.dataverse.api.dto.FieldDTO; +import edu.harvard.iq.dataverse.api.dto.MetadataBlockDTO; + +public class XmlWriterUtil { + + public static void writeFullElementList(XMLStreamWriter xmlw, String name, List values) throws XMLStreamException { + // For the simplest Elements we can + if (values != null && !values.isEmpty()) { + for (String value : values) { + xmlw.writeStartElement(name); + xmlw.writeCharacters(value); + xmlw.writeEndElement(); // labl + } + } + } + + public static void writeI18NElementList(XMLStreamWriter xmlw, String name, List values, + String fieldTypeName, String fieldTypeClass, String metadataBlockName, String lang) + throws XMLStreamException { + + if (values != null && !values.isEmpty()) { + Locale defaultLocale = Locale.getDefault(); + for (String value : values) { + if (fieldTypeClass.equals("controlledVocabulary")) { + String localeVal = ControlledVocabularyValue.getLocaleStrValue(value, fieldTypeName, metadataBlockName, defaultLocale, false); + if (localeVal != null) { + + value = localeVal; + writeFullElement(xmlw, name, value, defaultLocale.getLanguage()); + } else { + writeFullElement(xmlw, name, value); + } + } else { + writeFullElement(xmlw, name, value); + } + } + if (lang != null && !defaultLocale.getLanguage().equals(lang)) { + // Get values in dataset metadata language + // Loop before testing fieldTypeClass to be ready for external CVV + for (String value : values) { + if (fieldTypeClass.equals("controlledVocabulary")) { + String localeVal = ControlledVocabularyValue.getLocaleStrValue(value, fieldTypeName, metadataBlockName, new Locale(lang), false); + if (localeVal != null) { + writeFullElement(xmlw, name, localeVal, lang); + } + } + } + } + } + } + + public static void writeI18NElement(XMLStreamWriter xmlw, String name, DatasetVersionDTO version, + String fieldTypeName, String lang) throws XMLStreamException { + // Get the default value + String val = dto2Primitive(version, fieldTypeName); + Locale defaultLocale = Locale.getDefault(); + // Get the language-specific value for the default language + // A null value is returned if this is not a CVV field + String localeVal = dto2Primitive(version, fieldTypeName, defaultLocale); + String requestedLocaleVal = null; + if (lang != null && localeVal != null && !defaultLocale.getLanguage().equals(lang)) { + // Also get the value in the requested locale/lang if that's not the default + // lang. + requestedLocaleVal = dto2Primitive(version, fieldTypeName, new Locale(lang)); + } + // FWIW locale-specific vals will only be non-null for CVV values (at present) + if (localeVal == null && requestedLocaleVal == null) { + // Not CVV/no translations so print without lang tag + writeFullElement(xmlw, name, val); + } else { + // Print in either/both languages if we have values + if (localeVal != null) { + // Print the value for the default locale with it's own lang tag + writeFullElement(xmlw, name, localeVal, defaultLocale.getLanguage()); + } + // Also print in the request lang (i.e. the metadata language for the dataset) + // if a value exists, print it with a lang tag + if (requestedLocaleVal != null) { + writeFullElement(xmlw, name, requestedLocaleVal, lang); + } + } + } + + public static String dto2Primitive(DatasetVersionDTO datasetVersionDTO, String datasetFieldTypeName) { + for (Map.Entry entry : datasetVersionDTO.getMetadataBlocks().entrySet()) { + MetadataBlockDTO value = entry.getValue(); + for (FieldDTO fieldDTO : value.getFields()) { + if (datasetFieldTypeName.equals(fieldDTO.getTypeName())) { + return fieldDTO.getSinglePrimitive(); + } + } + } + return null; + } + + public static String dto2Primitive(DatasetVersionDTO datasetVersionDTO, String datasetFieldTypeName, Locale locale) { + for (Map.Entry entry : datasetVersionDTO.getMetadataBlocks().entrySet()) { + MetadataBlockDTO value = entry.getValue(); + for (FieldDTO fieldDTO : value.getFields()) { + if (datasetFieldTypeName.equals(fieldDTO.getTypeName())) { + String rawVal = fieldDTO.getSinglePrimitive(); + if (fieldDTO.isControlledVocabularyField()) { + return ControlledVocabularyValue.getLocaleStrValue(rawVal, datasetFieldTypeName, value.getName(), + locale, false); + } + } + } + } + return null; + } + + public static void writeFullElement(XMLStreamWriter xmlw, String name, String value) throws XMLStreamException { + writeFullElement(xmlw, name, value, null); + } + + public static void writeFullElement(XMLStreamWriter xmlw, String name, String value, String lang) throws XMLStreamException { + // For the simplest Elements we can + if (!StringUtils.isEmpty(value)) { + xmlw.writeStartElement(name); + if (DvObjectContainer.isMetadataLanguageSet(lang)) { + writeAttribute(xmlw, "xml:lang", lang); + } + xmlw.writeCharacters(value); + xmlw.writeEndElement(); // labl + } + } + + public static void writeAttribute(XMLStreamWriter xmlw, String name, String value) throws XMLStreamException { + if (!StringUtils.isEmpty(value)) { + xmlw.writeAttribute(name, value); + } + } + + + public static void writeFullElementWithAttributes(XMLStreamWriter xmlw, String name, Map attributeMap, String value) throws XMLStreamException { + if (!StringUtils.isEmpty(value)) { + xmlw.writeStartElement(name); + for (String key : attributeMap.keySet()) { + writeAttribute(xmlw, key, attributeMap.get(key)); + } + xmlw.writeCharacters(value); + xmlw.writeEndElement(); // labl + } + } + + public static boolean writeOpenTagIfNeeded(XMLStreamWriter xmlw, String tag, boolean element_check) throws XMLStreamException { + // check if the current tag isn't opened + if (!element_check) { + xmlw.writeStartElement(tag); // + } + return true; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/validation/EMailValidator.java b/src/main/java/edu/harvard/iq/dataverse/validation/EMailValidator.java index 624e49623f2..446f55a193f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/validation/EMailValidator.java +++ b/src/main/java/edu/harvard/iq/dataverse/validation/EMailValidator.java @@ -1,5 +1,6 @@ package edu.harvard.iq.dataverse.validation; +import edu.harvard.iq.dataverse.settings.JvmSettings; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; @@ -10,7 +11,7 @@ * @author skraffmi */ public class EMailValidator implements ConstraintValidator { - + @Override public boolean isValid(String value, ConstraintValidatorContext context) { return isEmailValid(value); @@ -23,6 +24,12 @@ public boolean isValid(String value, ConstraintValidatorContext context) { * @return true when valid, false when invalid (null = valid!) */ public static boolean isEmailValid(String value) { - return value == null || EmailValidator.getInstance().isValid(value); + // Must be looked up here - otherwise changes are not picked up (tests, live config, ...) + final boolean mtaSupportsUTF8 = JvmSettings.MAIL_MTA_SUPPORT_UTF8.lookup(Boolean.class); + return value == null || (EmailValidator.getInstance().isValid(value) && + // If the MTA isn't able to handle UTF-8 mail addresses following RFC 6530/6531/6532, we can only declare + // mail addresses using 7bit ASCII (RFC 821) as valid. + // Beyond scope for Apache Commons Validator, see also https://issues.apache.org/jira/browse/VALIDATOR-487 + (value.codePoints().noneMatch(c -> c > 127) || mtaSupportsUTF8) ); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/validation/JSONDataValidation.java b/src/main/java/edu/harvard/iq/dataverse/validation/JSONDataValidation.java new file mode 100644 index 00000000000..fb19a14e7de --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/validation/JSONDataValidation.java @@ -0,0 +1,190 @@ +package edu.harvard.iq.dataverse.validation; + +import com.mashape.unirest.http.JsonNode; +import edu.harvard.iq.dataverse.DatasetFieldServiceBean; +import edu.harvard.iq.dataverse.DatasetFieldType; +import edu.harvard.iq.dataverse.util.BundleUtil; +import jakarta.enterprise.inject.spi.CDI; +import org.everit.json.schema.Schema; +import org.everit.json.schema.ValidationException; +import org.json.JSONArray; + +import java.util.*; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class JSONDataValidation { + private static final Logger logger = Logger.getLogger(JSONDataValidation.class.getCanonicalName()); + private static DatasetFieldServiceBean datasetFieldService = null; + + /** + * + * @param schema Schema file defining the JSON objects to be validated + * @param jsonInput JSON string to validate against the schema + * @throws ValidationException + */ + public static void validate(Schema schema, Map>> schemaChildMap, String jsonInput) throws ValidationException { + if (datasetFieldService == null) { + datasetFieldService = CDI.current().select(DatasetFieldServiceBean.class).get(); + } + JsonNode node = new JsonNode(jsonInput); + if (node.isArray()) { + JSONArray arrayNode = node.getArray(); + validateObject(schema, schemaChildMap, "root", arrayNode.toList()); + } else { + node.getObject().toMap().forEach((k,v) -> { + validateObject(schema, schemaChildMap, k, (v instanceof JSONArray) ? ((JSONArray) v).toList() : v); + }); + } + } + + /* + * Validate objects recursively + */ + private static void validateObject(Schema schema, Map>> schemaChildMap, String key, Object value) { + if (value instanceof Map) { + validateSchemaObject(schema, schemaChildMap, key, (Map) value); + + ((Map) value).entrySet().forEach(e -> { + validateObject(schema, schemaChildMap, (String) e.getKey(), e.getValue()); + }); + } else if (value instanceof List) { + ((List) value).listIterator().forEachRemaining(v -> { + validateObject(schema, schemaChildMap, key, v); + }); + } + } + + /* + * Validate objects specific to a type. Currently only validating Datasets + */ + private static void validateSchemaObject(Schema schema, Map>> schemaChildMap, String key, Map valueMap) { + if (schema.definesProperty("datasetVersion")) { + validateDatasetObject(schema, schemaChildMap, key, valueMap); + } + } + + /* + * Specific validation for Dataset objects + */ + private static void validateDatasetObject(Schema schema, Map>> schemaChildMap, String key, Map valueMap) { + if (valueMap != null && valueMap.containsKey("typeClass")) { + validateTypeClass(schema, schemaChildMap, key, valueMap, valueMap.get("value"), "dataset"); + } + } + + /* + * key: The name of the parent object + * valueMap: Map of all the metadata of the object + * value: The value field of the object + * messageType: Refers to the parent: if this is an object from a dataset the messageType would be 'dataset' + * This needs to match the Bundle.properties for mapping the error messages when an exception occurs + * + * Rules for typeClass: + * The contents of value depend on the field attributes + * if single/primitive, value is a String + * if multiple, value is a JsonArray + * multiple/primitive: each JsonArray element will contain String + * multiple/compound: each JsonArray element will contain Set of FieldDTOs + */ + private static void validateTypeClass(Schema schema, Map>> schemaChildMap, String key, Map valueMap, Object value, String messageType) { + + String typeClass = valueMap.containsKey("typeClass") ? valueMap.get("typeClass").toString() : ""; + String typeName = valueMap.containsKey("typeName") ? valueMap.get("typeName").toString() : ""; + boolean multiple = Boolean.valueOf(String.valueOf(valueMap.getOrDefault("multiple", "false"))); + + // make sure there is a value since 'value' is required + if (value == null) { + throwValidationException("value.missing", List.of(key, typeName)); + } + + if (multiple && !(value instanceof List)) { + throwValidationException("notlist.multiple", List.of(key, typeName, typeClass)); + } + if (!multiple && value instanceof List) { + throwValidationException("list.notmultiple", List.of(key, typeName)); + } + if ("primitive".equals(typeClass) && !multiple && !(value instanceof String)) { + throwValidationException("type", List.of(key, typeName, typeClass)); + } + if ("primitive".equals(typeClass) && multiple) { + ((List) value).listIterator().forEachRemaining(primitive -> { + if (!(primitive instanceof String)) { + throwValidationException("type", List.of(key, typeName, typeClass)); + } + }); + } + if ("compound".equals(typeClass)) { + if (multiple && value instanceof List) { + ((List) value).listIterator().forEachRemaining(item -> { + if (!(item instanceof Map)) { + throwValidationException("compound", List.of(key, typeName, typeClass)); + } else { + ((Map) item).forEach((k,val) -> { + if (!(val instanceof Map)) { + throwValidationException("compound", List.of(key, typeName, typeClass)); + } + // validate mismatch between compound object key and typeName in value + String valTypeName = ((Map) val).containsKey("typeName") ? (String) ((Map) val).get("typeName") : ""; + if (!k.equals(valTypeName)) { + throwValidationException("compound.mismatch", List.of((String) k, valTypeName)); + } + }); + validateChildren(schema, schemaChildMap, key, ((Map) item).values(), typeName, messageType); + } + }); + } + } + + if ("controlledVocabulary".equals(typeClass)) { + DatasetFieldType dsft = datasetFieldService.findByName(typeName); + if (value instanceof List) { + ((List) value).listIterator().forEachRemaining(cvv -> { + if (datasetFieldService.findControlledVocabularyValueByDatasetFieldTypeAndStrValue(dsft, (String) cvv, true) == null) { + throwValidationException("dataset", "cvv.missing", List.of(key, typeName, (String) cvv)); + } + }); + } else { + if (datasetFieldService.findControlledVocabularyValueByDatasetFieldTypeAndStrValue(dsft, (String) value, true) == null) { + throwValidationException("dataset", "cvv.missing", List.of(key, typeName, (String) value)); + } + } + } + } + + // If value is another object or list of objects that need to be further validated then childType refers to the parent + // Example: If this is a dsDescriptionValue from a dataset the messageType would be dataset.dsDescriptionValue + // This needs to match the Bundle.properties for mapping the error messages when an exception occurs + private static void validateChildren(Schema schema, Map>> schemaChildMap, String key, Collection children, String typeName, String messageType) { + if (children == null || children.isEmpty()) { + return; + } + List requiredFields = new ArrayList<>(); + requiredFields.addAll((List)schemaChildMap.getOrDefault(typeName, Collections.EMPTY_MAP).getOrDefault("required", Collections.EMPTY_LIST)); + List allowedFields = (List)schemaChildMap.getOrDefault(typeName, Collections.EMPTY_MAP).getOrDefault("allowed", Collections.EMPTY_LIST); + children.forEach(child -> { + if (child instanceof Map) { + String childTypeName = ((Map) child).containsKey("typeName") ? (String)((Map) child).get("typeName") : ""; + if (!allowedFields.isEmpty() && !allowedFields.contains(childTypeName)) { + throwValidationException(messageType, "invalidType", List.of(typeName, childTypeName, allowedFields.stream().collect(Collectors.joining(", ")))); + } + if (!requiredFields.isEmpty() && requiredFields.contains(childTypeName)) { + requiredFields.remove(childTypeName); + } + } + }); + if (!requiredFields.isEmpty()) { + throwValidationException(messageType, "required.missing", List.of(typeName, requiredFields.stream().collect(Collectors.joining(", ")), typeName)); + } + } + private static void throwValidationException(String key, List argList) { + throw new ValidationException(BundleUtil.getStringFromBundle("schema.validation.exception." + key, argList)); + } + private static void throwValidationException(String type, String message, List argList) { + if (type != null) { + throwValidationException(type + "." + message, argList); + } else { + throwValidationException(message, argList); + } + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/validation/URLValidator.java b/src/main/java/edu/harvard/iq/dataverse/validation/URLValidator.java index 285f34d3f8c..8fde76d84e1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/validation/URLValidator.java +++ b/src/main/java/edu/harvard/iq/dataverse/validation/URLValidator.java @@ -41,7 +41,7 @@ public static boolean isURLValid(String value) { * @return true when valid (null is also valid) or false */ public static boolean isURLValid(String value, String[] schemes) { - UrlValidator urlValidator = new UrlValidator(schemes); + UrlValidator urlValidator = new UrlValidator(schemes, UrlValidator.ALLOW_2_SLASHES); return value == null || urlValidator.isValid(value); } diff --git a/src/main/java/edu/harvard/iq/dataverse/workflow/WorkflowServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/workflow/WorkflowServiceBean.java index 47f24c9b8bd..757d447b60a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/workflow/WorkflowServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/workflow/WorkflowServiceBean.java @@ -1,8 +1,10 @@ package edu.harvard.iq.dataverse.workflow; +import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetLock; import edu.harvard.iq.dataverse.DatasetServiceBean; import edu.harvard.iq.dataverse.DataverseRequestServiceBean; +import edu.harvard.iq.dataverse.DvObjectServiceBean; import edu.harvard.iq.dataverse.EjbDataverseEngine; import edu.harvard.iq.dataverse.RoleAssigneeServiceBean; import edu.harvard.iq.dataverse.UserNotification; @@ -58,6 +60,9 @@ public class WorkflowServiceBean { @EJB DatasetServiceBean datasets; + + @EJB + DvObjectServiceBean dvObjects; @EJB SettingsServiceBean settings; @@ -387,16 +392,11 @@ private void workflowCompleted(Workflow wf, WorkflowContext ctxt) { //Now lock for FinalizePublication - this block mirrors that in PublishDatasetCommand AuthenticatedUser user = ctxt.getRequest().getAuthenticatedUser(); DatasetLock lock = new DatasetLock(DatasetLock.Reason.finalizePublication, user); - lock.setDataset(ctxt.getDataset()); - String currentGlobalIdProtocol = settings.getValueForKey(SettingsServiceBean.Key.Protocol, ""); - String currentGlobalAuthority= settings.getValueForKey(SettingsServiceBean.Key.Authority, ""); - String dataFilePIDFormat = settings.getValueForKey(SettingsServiceBean.Key.DataFilePIDFormat, "DEPENDENT"); + Dataset dataset = ctxt.getDataset(); + lock.setDataset(dataset); boolean registerGlobalIdsForFiles = - (currentGlobalIdProtocol.equals(ctxt.getDataset().getProtocol()) || dataFilePIDFormat.equals("INDEPENDENT")) - && systemConfig.isFilePIDsEnabledForCollection(ctxt.getDataset().getOwner()); - if ( registerGlobalIdsForFiles ){ - registerGlobalIdsForFiles = currentGlobalAuthority.equals( ctxt.getDataset().getAuthority() ); - } + systemConfig.isFilePIDsEnabledForCollection(ctxt.getDataset().getOwner()) && + dvObjects.getEffectivePidGenerator(dataset).canCreatePidsLike(dataset.getGlobalId()); boolean validatePhysicalFiles = systemConfig.isDatafileValidationOnPublishEnabled(); String info = "Publishing the dataset; "; diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 157f2ecaf54..5f3e4c33e0b 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -3,7 +3,17 @@ newDataverse=New Dataverse hostDataverse=Host Dataverse dataverses=Dataverses passwd=Password +# BEGIN dataset types +# `dataset=Dataset` has been here since 4.0 but now that we have dataset types, +# we need to add the rest of the types here for two reasons. First, we want +# translators to be able to translate these types. Second, in English it looks +# weird to have only "Dataset" capitalized in the facet but not "software" and +# "workflow". This capitalization (looking up here in the bundle) is done by +# SearchServiceBean near the comment "This is where facets are capitalized". dataset=Dataset +software=Software +workflow=Workflow +# END dataset types datasets=Datasets newDataset=New Dataset files=Files @@ -15,6 +25,7 @@ embargoed=Embargoed embargoedaccess=Embargoed with Access embargoedandrestricted=Embargoed and then Restricted embargoedandrestrictedaccess=Embargoed and then Restricted with Access +retentionExpired=Retention Period Expired incomplete=Incomplete metadata valid=Valid find=Find @@ -30,6 +41,12 @@ embargoed.wasthrough=Was embargoed until embargoed.willbeuntil=Draft: will be embargoed until embargo.date.invalid=Date is outside the allowed range: ({0} to {1}) embargo.date.required=An embargo date is required +retention.after=Was retained until +retention.isfrom=Is retained until +retention.willbeafter=Draft: will be retained until +retention.enddateinfo=after which it will no longer be accessible +retention.date.invalid=Date is outside the allowed range: ({0} to {1}) +retention.date.required=A retention period end date is required cancel=Cancel ok=OK saveChanges=Save Changes @@ -56,6 +73,7 @@ storage=Storage curationLabels=Curation Labels metadataLanguage=Dataset Metadata Language guestbookEntryOption=Guestbook Entry Option +pidProviderOption=PID Provider Option createDataverse=Create Dataverse remove=Remove done=Done @@ -245,11 +263,16 @@ notification.mail.import.filesystem=Dataset {2} ({0}/dataset.xhtml?persistentId= notification.mail.globus.upload.completed=Globus transfer to Dataset {2} was successful. File(s) have been uploaded and verified.

    {3}
    notification.mail.globus.download.completed=Globus transfer of file(s) from the dataset {2} was successful.

    {3}
    notification.mail.globus.upload.completedWithErrors=Globus transfer to Dataset {2} is complete with errors.

    {3}
    +notification.mail.globus.upload.failedRemotely=Remote data transfer between Globus endpoints for Dataset {2} failed, as reported via Globus API.

    {3}
    +notification.mail.globus.upload.failedLocally=Dataverse received a confirmation of a successful Globus data transfer for Dataset {2}, but failed to add the files to the dataset locally.

    {3}
    notification.mail.globus.download.completedWithErrors=Globus transfer from the dataset {2} is complete with errors.

    {3}
    notification.import.filesystem=Dataset {1} has been successfully uploaded and verified. notification.globus.upload.completed=Globus transfer to Dataset {1} was successful. File(s) have been uploaded and verified. notification.globus.download.completed=Globus transfer from the dataset {1} was successful. notification.globus.upload.completedWithErrors=Globus transfer to Dataset {1} is complete with errors. +notification.globus.upload.failedRemotely=Remote data transfer between Globus collections for Dataset {2} failed, reported via Globus API.

    {3}
    +notification.globus.upload.failedLocally=Dataverse received a confirmation of a successful Globus data transfer for Dataset {2}, but failed to add the files to the dataset locally.

    {3}
    + notification.globus.download.completedWithErrors=Globus transfer from the dataset {1} is complete with errors. notification.import.checksum={1}, dataset had file checksums added via a batch job. removeNotification=Remove Notification @@ -749,8 +772,8 @@ dashboard.card.datamove.dataset.command.error.indexingProblem=Dataset could not notification.email.create.dataverse.subject={0}: Your dataverse has been created notification.email.create.dataset.subject={0}: Dataset "{1}" has been created notification.email.dataset.created.subject={0}: Dataset "{1}" has been created -notification.email.request.file.access.subject={0}: Access has been requested for a restricted file in dataset "{1}" -notification.email.requested.file.access.subject={0}: You have requested access to a restricted file in dataset "{1}" +notification.email.request.file.access.subject={0}: {1} {2} ({3}) requested access to dataset "{4}" +notification.email.requested.file.access.subject={0}: You have requested access to a restricted file in dataset "{1}" notification.email.grant.file.access.subject={0}: You have been granted access to a restricted file notification.email.rejected.file.access.subject={0}: Your request for access to a restricted file has been rejected notification.email.submit.dataset.subject={0}: Dataset "{1}" has been submitted for review @@ -783,10 +806,12 @@ notification.email.rejectFileAccess=Your request for access was rejected for the notification.email.createDataverse=Your new dataverse named {0} (view at {1} ) was created in {2} (view at {3} ). To learn more about what you can do with your dataverse, check out the Dataverse Management - User Guide at {4}/{5}/user/dataverse-management.html . # Bundle file editors, please note that "notification.email.createDataset" is used in a unit test notification.email.createDataset=Your new dataset named {0} (view at {1} ) was created in {2} (view at {3} ). To learn more about what you can do with a dataset, check out the Dataset Management - User Guide at {4}/{5}/user/dataset-management.html . -notification.email.wasSubmittedForReview={0} (view at {1} ) was submitted for review to be published in {2} (view at {3} ). Don''t forget to publish it or send it back to the contributor, {4} ({5})\! -notification.email.wasReturnedByReviewer={0} (view at {1} ) was returned by the curator of {2} (view at {3} ). -notification.email.wasPublished={0} (view at {1} ) was published in {2} (view at {3} ). -notification.email.publishFailedPidReg={0} (view at {1} ) in {2} (view at {3} ) could not be published due to a failure to register, or update the Global Identifier for the dataset or one of the files in it. Contact support if this continues to happen. +notification.email.wasSubmittedForReview=Your dataset named {0} (view at {1} ) was submitted for review to be published in {2} (view at {3} ). Don''t forget to publish it or send it back to the contributor, {4} ({5})\! +notification.email.wasReturnedByReviewer=Your dataset named {0} (view at {1} ) was returned by the curator of {2} (view at {3} ). +notification.email.wasReturnedByReviewerReason=Here is the curator comment: {0} +notification.email.wasReturnedByReviewer.collectionContacts=You may contact the collection administrator for more information: {0} +notification.email.wasPublished=Your dataset named {0} (view at {1} ) was published in {2} (view at {3} ). +notification.email.publishFailedPidReg=Your dataset named {0} (view at {1} ) in {2} (view at {3} ) could not be published due to a failure to register, or update the Global Identifier for the dataset or one of the files in it. Contact support if this continues to happen. notification.email.closing=\n\nYou may contact us for support at {0}.\n\nThank you,\n{1} notification.email.closing.html=

    You may contact us for support at {0}.

    Thank you,
    {1} notification.email.assignRole=You are now {0} for the {1} "{2}" (view at {3} ). @@ -813,8 +838,8 @@ notification.email.datasetWasMentioned.subject={0}: A Dataset Relationship has b notification.email.globus.uploadCompleted.subject={0}: Files uploaded successfully via Globus and verified notification.email.globus.downloadCompleted.subject={0}: Files downloaded successfully via Globus notification.email.globus.uploadCompletedWithErrors.subject={0}: Uploaded files via Globus with errors -notification.email.globus.downloadCompletedWithErrors.subject={0}: Downloaded files via Globus with errors - +notification.email.globus.uploadFailedRemotely.subject={0}: Failed to upload files via Globus +notification.email.globus.uploadFailedLocally.subject={0}: Failed to add files uploaded via Globus to dataset # dataverse.xhtml dataverse.name=Dataverse Name dataverse.name.title=The project, department, university, professor, or journal this dataverse will contain data for. @@ -831,6 +856,7 @@ dataverse.curationLabels.disabled=Disabled dataverse.category=Category dataverse.category.title=The type that most closely reflects this dataverse. dataverse.guestbookentryatrequest.title=Whether Guestbooks are displayed to users when they request file access or when they download files. +dataverse.pidProvider.title=The source of PIDs (DOIs, Handles, etc.) when a new PID is created. dataverse.type.selectTab.top=Select one... dataverse.type.selectTab.researchers=Researcher dataverse.type.selectTab.researchProjects=Research Project @@ -875,7 +901,7 @@ dataverse.option.deleteDataverse=Delete Dataverse dataverse.publish.btn=Publish dataverse.publish.header=Publish Dataverse dataverse.nopublished=No Published Dataverses -dataverse.nopublished.tip=In order to use this feature you must have at least one published dataverse. +dataverse.nopublished.tip=In order to use this feature you must have at least one published or linked dataverse. dataverse.contact=Email Dataverse Contact dataverse.link=Link Dataverse dataverse.link.btn.tip=Link to Your Dataverse @@ -888,6 +914,8 @@ dataverse.link.dataset.none=No linkable dataverses available. dataverse.link.no.choice=You have one dataverse you can add linked dataverses and datasets in. dataverse.link.no.linkable=To be able to link a dataverse or dataset, you need to have your own dataverse. Create a dataverse to get started. dataverse.link.no.linkable.remaining=You have already linked all of your eligible dataverses. +dataverse.unlink.dataset.choose=Enter the name of the dataverse you would like to unlink this dataset from. +dataverse.unlink.dataset.none=No linked dataverses available. dataverse.savedsearch.link=Link Search dataverse.savedsearch.searchquery=Search dataverse.savedsearch.filterQueries=Facets @@ -901,6 +929,7 @@ dataverse.linked.success= {0} has been successfully linked to {1}. dataverse.linked.success.wait= {0} has been successfully linked to {1}. Please wait for its contents to appear. dataverse.linked.internalerror={0} has been successfully linked to {1} but contents will not appear until an internal error has been fixed. dataverse.linked.error.alreadyLinked={0} has already been linked to {1}. +dataverse.unlinked.success= {0} has been successfully unlinked from {1}. dataverse.page.pre=Previous dataverse.page.next=Next dataverse.byCategory=Dataverses by Category @@ -936,6 +965,13 @@ dataverse.default=(Default) dataverse.metadatalanguage.setatdatasetcreation=Chosen at Dataset Creation dataverse.guestbookentry.atdownload=Guestbook Entry At Download dataverse.guestbookentry.atrequest=Guestbook Entry At Access Request +dataverse.inputlevels.error.invalidfieldtypename=Invalid dataset field type name: {0} +dataverse.inputlevels.error.cannotberequiredifnotincluded=The input level for the dataset field type {0} cannot be required if it is not included +dataverse.facets.error.fieldtypenotfound=Can't find dataset field type '{0}' +dataverse.facets.error.fieldtypenotfacetable=Dataset field type '{0}' is not facetable +dataverse.metadatablocks.error.invalidmetadatablockname=Invalid metadata block name: {0} +dataverse.create.error.jsonparse=Error parsing Json: {0} +dataverse.create.error.jsonparsetodataverse=Error parsing the POSTed json into a dataverse: {0} # rolesAndPermissionsFragment.xhtml # advanced.xhtml @@ -948,18 +984,16 @@ advanced.search.header.datasets=Datasets advanced.search.header.files=Files advanced.search.files.name.tip=The name given to identify the file. advanced.search.files.description.tip=A summary describing the file and its variables. -advanced.search.files.persistentId.tip=The persistent identifier for the file. advanced.search.files.persistentId=Data File Persistent ID -advanced.search.files.persistentId.tip=The unique persistent identifier for a data file, which can be a Handle or DOI in Dataverse. +advanced.search.files.persistentId.tip=The unique persistent identifier for the file. advanced.search.files.fileType=File Type advanced.search.files.fileType.tip=The file type, e.g. Comma Separated Values, Plain Text, R, etc. advanced.search.files.variableName=Variable Name advanced.search.files.variableName.tip=The name of the variable's column in the data frame. advanced.search.files.variableLabel=Variable Label advanced.search.files.variableLabel.tip=A short description of the variable. -advanced.search.datasets.persistentId.tip=The persistent identifier for the Dataset. advanced.search.datasets.persistentId=Persistent Identifier -advanced.search.datasets.persistentId.tip=The Dataset's unique persistent identifier, either a DOI or Handle +advanced.search.datasets.persistentId.tip=The persistent identifier for the Dataset. advanced.search.files.fileTags=File Tags advanced.search.files.fileTags.tip=Terms such "Documentation", "Data", or "Code" that have been applied to files. @@ -1023,7 +1057,7 @@ dataverse.theme.inheritCustomization.title=For this dataverse, use the same them dataverse.theme.inheritCustomization.label=Inherit Theme dataverse.theme.inheritCustomization.checkbox=Inherit theme from {0} dataverse.theme.logo=Logo -dataverse.theme.logo.tip=Supported image types are JPG, TIF, or PNG and should be no larger than 500 KB. The maximum display size for an image file in a dataverse's theme is 940 pixels wide by 120 pixels high. +dataverse.theme.logo.tip=Supported image types are JPG and PNG, must be no larger than 500 KB. The maximum display size for an image file in a dataverse's theme is 940 pixels wide by 120 pixels high. dataverse.theme.logo.format=Logo Format dataverse.theme.logo.format.selectTab.square=Square dataverse.theme.logo.format.selectTab.rectangle=Rectangle @@ -1042,10 +1076,12 @@ dataverse.theme.success=You have successfully updated the theme for this dataver dataverse.theme.failure=The dataverse theme has not been updated. dataverse.theme.logo.image=Logo Image dataverse.theme.logo.imageFooter=Footer Image +dataverse.theme.logo.imageThumbnail=Thumbnail Image dataverse.theme.logo.image.title=The logo or image file you wish to display in the header of this dataverse. dataverse.theme.logo.image.footer=The logo or image file you wish to display in the footer of this dataverse. +dataverse.theme.logo.image.thumbnail=The logo or image file you wish to display in the featured collection of this dataverse. dataverse.theme.logo.image.uploadNewFile=Upload New File -dataverse.theme.logo.image.invalidMsg=The image could not be uploaded. Please try again with a JPG, TIF, or PNG file. +dataverse.theme.logo.image.invalidMsg=The image could not be uploaded. Please try again with a JPG or PNG file. dataverse.theme.logo.image.uploadImgFile=Upload Image File dataverse.theme.logo.format.title=The shape for the logo or image file you upload for this dataverse. dataverse.theme.logo.alignment.title=Where the logo or image should display in the header or footer. @@ -1388,6 +1424,8 @@ dataset.guestbookResponse.respondent=Respondent dataset.guestbookResponse.question=Q dataset.guestbookResponse.answer=A dataset.guestbookResponse.noResponse=(No Response) +dataset.guestbookResponse.requestor.id=authenticatedUserId +dataset.guestbookResponse.requestor.identifier=authenticatedUserIdentifier # dataset.xhtml @@ -1405,6 +1443,7 @@ dataset.accessBtn.too.big=The dataset is too large to download. Please select th dataset.accessBtn.original.too.big=The dataset is too large to download in the original format. Please select the files you need from the files table. dataset.accessBtn.archival.too.big=The dataset is too large to download in the archival format. Please select the files you need from the files table. dataset.linkBtn=Link Dataset +dataset.unlinkBtn=Unlink Dataset dataset.contactBtn=Contact Owner dataset.shareBtn=Share @@ -1433,6 +1472,8 @@ dataset.exportBtn.itemLabel.json=JSON dataset.exportBtn.itemLabel.oai_ore=OAI_ORE dataset.exportBtn.itemLabel.dataciteOpenAIRE=OpenAIRE dataset.exportBtn.itemLabel.html=DDI HTML Codebook +license.none.chosen=No license or custom terms chosen +license.none.chosen.description=No custom terms have been entered for this dataset license.custom=Custom Dataset Terms license.custom.description=Custom terms specific to this dataset metrics.title=Metrics @@ -1475,6 +1516,7 @@ dataset.status.failure.notallowed=Status update failed - label not allowed dataset.status.failure.disabled=Status labeling disabled for this dataset dataset.status.failure.isReleased=Latest version of dataset is already released. Status can only be set on draft versions dataset.rejectMessage=Return this dataset to contributor for modification. +dataset.rejectMessageReason=The reason for return entered below will be sent by email to the author. dataset.rejectMessage.label=Return to Author Reason dataset.rejectWatermark=Please enter a reason for returning this dataset to its author(s). dataset.reject.enterReason.error=Reason for return to author is required. @@ -1482,6 +1524,7 @@ dataset.reject.success=This dataset has been sent back to the contributor. dataset.reject.failure=Dataset Submission Return Failed - {0} dataset.reject.datasetNull=Cannot return the dataset to the author(s) because it is null. dataset.reject.datasetNotInReview=This dataset cannot be return to the author(s) because the latest version is not In Review. The author(s) needs to click Submit for Review first. +dataset.reject.commentNull=You must enter a reason for returning a dataset to the author(s). dataset.publish.tip=Are you sure you want to publish this dataset? Once you do so it must remain published. dataset.publish.terms.tip=This version of the dataset will be published with the following terms: dataset.publish.terms.help.tip=To change the terms for this version, click the Cancel button and go to the Terms tab for this dataset. @@ -1508,6 +1551,8 @@ dataset.link.not.to.parent.dataverse=Can't link a dataset to its parent datavers dataset.link.not.published=Can't link a dataset that has not been published dataset.link.not.available=Can't link a dataset that has not been published or is not harvested dataset.link.not.already.linked=Can't link a dataset that has already been linked to this dataverse +dataset.unlink.title=Unlink Dataset +dataset.unlink.delete=Remove Linked Dataset dataset.email.datasetContactTitle=Contact Dataset Owner dataset.email.hiddenMessage= dataset.email.messageSubject=Test Message Subject @@ -1589,6 +1634,7 @@ dataset.message.createSuccess=This dataset has been created. dataset.message.createSuccess.failedToSaveFiles=Partial Success: The dataset has been created. But the file(s) could not be saved. Please try uploading the file(s) again. dataset.message.createSuccess.partialSuccessSavingFiles=Partial Success: The dataset has been created. But only {0} out of {1} files have been saved. Please try uploading the missing file(s) again. dataset.message.linkSuccess= {0} has been successfully linked to {1}. +dataset.message.unlinkSuccess= {0} has been successfully unlinked from {1}. dataset.message.metadataSuccess=The metadata for this dataset have been updated. dataset.message.termsSuccess=The terms for this dataset have been updated. dataset.message.filesSuccess=One or more files have been updated. @@ -1660,17 +1706,19 @@ dataset.noSelectedFiles=Please select one or more files. dataset.noSelectedFilesForDownload=Please select a file or files to be downloaded. dataset.noSelectedFilesForRequestAccess=Please select a file or files for access request. dataset.embargoedSelectedFilesForRequestAccess=Embargoed files cannot be accessed. Please select an unembargoed file or files for your access request. -dataset.inValidSelectedFilesForDownload=Restricted Files Selected -dataset.inValidSelectedFilesForDownloadWithEmbargo=Embargoed and/or Restricted Files Selected -dataset.noValidSelectedFilesForDownload=The selected file(s) may not be downloaded because you have not been granted access. -dataset.mixedSelectedFilesForDownload=The restricted file(s) selected may not be downloaded because you have not been granted access. -dataset.mixedSelectedFilesForDownloadWithEmbargo=The embargoed and/or restricted file(s) selected may not be downloaded because you have not been granted access. -dataset.mixedSelectedFilesForTransfer=Some file(s) cannot be transferred. (They are restricted, embargoed, or not Globus accessible.) +dataset.inValidSelectedFilesForDownload=Inaccessible Files Selected +dataset.inValidSelectedFilesForDownloadWithEmbargo=Inaccessible Files Selected +dataset.inValidSelectedFilesForTransferWithEmbargo=Inaccessible Files Selected +dataset.noValidSelectedFilesForDownload=The selected file(s) may not be downloaded because you have not been granted access or the file(s) have a retention period that has expired or the files can only be transferred via Globus. +dataset.noValidSelectedFilesForTransfer=The selected file(s) may not be transferred because you have not been granted access or the file(s) have a retention period that has expired or the files are not Globus accessible. +dataset.mixedSelectedFilesForDownload=The selected file(s) may not be downloaded because you have not been granted access or the file(s) have a retention period that has expired. +dataset.mixedSelectedFilesForDownloadWithEmbargo=Any embargoed and/or restricted file(s) selected may not be downloaded because you have not been granted access. Some files may have a retention period that has expired. Some files may only be accessible via Globus. +dataset.mixedSelectedFilesForTransfer=Some file(s) cannot be transferred. (They are restricted, embargoed, with an expired retention period, or not Globus accessible.) dataset.inValidSelectedFilesForTransfer=Ineligible Files Selected dataset.downloadUnrestricted=Click Continue to download the files you have access to download. dataset.transferUnrestricted=Click Continue to transfer the elligible files. -dataset.requestAccessToRestrictedFiles=You may request access to the restricted file(s) by clicking the Request Access button. +dataset.requestAccessToRestrictedFiles=You may request access to any restricted file(s) by clicking the Request Access button. dataset.requestAccessToRestrictedFilesWithEmbargo=Embargoed files cannot be accessed during the embargo period. If your selection contains restricted files, you may request access to them by clicking the Request Access button. dataset.privateurl.infoMessageAuthor=Privately share this dataset before it is published: {0} dataset.privateurl.infoMessageReviewer=This unpublished dataset is being privately shared. @@ -1736,6 +1784,7 @@ file.fromWebloaderAfterCreate.tip=An option to upload a folder of files will be file.fromWebloader=Upload a Folder file.api.httpDisabled=File upload via HTTP is not available for this installation of Dataverse. +file.api.globusUploadDisabled=File upload via Globus is not available for this installation of Dataverse. file.api.alreadyHasPackageFile=File upload via HTTP disabled since this dataset already contains a package file. file.replace.original=Original File file.editFiles=Edit Files @@ -1844,6 +1893,18 @@ file.editEmbargoDialog.newReason=Add a reason... file.editEmbargoDialog.newDate=Select the embargo end-date file.editEmbargoDialog.remove=Remove existing embargo(es) on selected files +file.retention=Retention Period +file.editRetention=Edit Retention Period +file.editRetention.add=Add or Change +file.editRetention.delete=Remove +file.editRetentionDialog.tip=Edit the planned retention period for the selected file or files. Once this dataset version is published, you will need to contact an administrator to change the retention period end date or reason of the file or files. \n After the retention period expires the files become unavailable for download. +file.editRetentionDialog.some.tip=One or more of the selected files have already been published. Contact an administrator to change the retention period date or reason of the file or files. +file.editRetentionDialog.none.tip=The selected file or files have already been published. Contact an administrator to change the retention period date or reason of the file or files. +file.editRetentionDialog.partial.tip=Any changes you make here will not be made to these files. +file.editRetentionDialog.reason.tip=Enter a short reason why this retention period exists +file.editRetentionDialog.newReason=Add a reason... +file.editRetentionDialog.newDate=Select the retention period end date +file.editRetentionDialog.remove=Remove existing retention period(s) on selected files file.setThumbnail=Set Thumbnail file.setThumbnail.header=Set Dataset Thumbnail @@ -1856,6 +1917,7 @@ file.advancedIngestOptions=Advanced Ingest Options file.assignedDataverseImage.success={0} has been saved as the thumbnail for this dataset. file.assignedTabFileTags.success=The tags were successfully added for {0}. file.assignedEmbargo.success=An Embargo was successfully added for {0}. +file.assignedRetention.success=A Retention Period was successfully added for {0}. file.tabularDataTags=Tabular Data Tags file.tabularDataTags.tip=Select a tag to describe the type(s) of data this is (survey, time series, geospatial, etc). file.spss-savEncoding=Language Encoding @@ -2002,7 +2064,8 @@ file.deleteFileDialog.immediate=The file will be deleted after you click on the file.deleteFileDialog.multiple.immediate=The file(s) will be deleted after you click on the Delete button. file.deleteFileDialog.header=Delete Files file.deleteFileDialog.failed.tip=Files will not be removed from previously published versions of the dataset. -file.deaccessionDialog.tip=Once you deaccession this dataset it will no longer be viewable by the public. +file.deaccessionDialog.tip.permanent=Deaccession is permanent. +file.deaccessionDialog.tip=This dataset will no longer be public and a tumbstone will display the reason for deaccessioning.
    Please read the documentation if you have any questions. file.deaccessionDialog.version=Version file.deaccessionDialog.reason.question1=Which version(s) do you want to deaccession? file.deaccessionDialog.reason.question2=What is the reason for deaccession? @@ -2016,8 +2079,8 @@ file.deaccessionDialog.reason.selectItem.other=Other (Please type reason in spac file.deaccessionDialog.enterInfo=Please enter additional information about the reason for deaccession. file.deaccessionDialog.leaveURL=If applicable, please leave a URL where this dataset can be accessed after deaccessioning. file.deaccessionDialog.leaveURL.watermark=Optional dataset site, http://... -file.deaccessionDialog.deaccession.tip=Are you sure you want to deaccession? The selected version(s) will no longer be viewable by the public. -file.deaccessionDialog.deaccessionDataset.tip=Are you sure you want to deaccession this dataset? It will no longer be viewable by the public. +file.deaccessionDialog.deaccession.tip=Are you sure you want to deaccession? This is permanent and the selected version(s) will no longer be viewable by the public. +file.deaccessionDialog.deaccessionDataset.tip=Are you sure you want to deaccession this dataset? This is permanent an it will no longer be viewable by the public. file.deaccessionDialog.dialog.selectVersion.error=Please select version(s) for deaccessioning. file.deaccessionDialog.dialog.reason.error=Please select reason for deaccessioning. file.deaccessionDialog.dialog.url.error=Please enter valid forwarding URL. @@ -2113,7 +2176,7 @@ dataset.thumbnailsAndWidget.thumbnails.title=Thumbnail dataset.thumbnailsAndWidget.widgets.title=Widgets dataset.thumbnailsAndWidget.thumbnailImage=Thumbnail Image dataset.thumbnailsAndWidget.thumbnailImage.title=The logo or image file you wish to display as the thumbnail of this dataset. -dataset.thumbnailsAndWidget.thumbnailImage.tip=Supported image types are JPG, TIF, or PNG and should be no larger than {0} KB. The maximum display size for an image file as a dataset thumbnail is 48 pixels wide by 48 pixels high. +dataset.thumbnailsAndWidget.thumbnailImage.tip=Supported image types are JPG and PNG, must be no larger than {0} KB. The maximum display size for an image file as a dataset thumbnail is 140 pixels wide by 140 pixels high. dataset.thumbnailsAndWidget.thumbnailImage.default=Default Icon dataset.thumbnailsAndWidget.thumbnailImage.selectAvailable=Select Available File dataset.thumbnailsAndWidget.thumbnailImage.selectThumbnail=Select Thumbnail @@ -2121,7 +2184,7 @@ dataset.thumbnailsAndWidget.thumbnailImage.selectAvailable.title=Select a thumbn dataset.thumbnailsAndWidget.thumbnailImage.uploadNew=Upload New File dataset.thumbnailsAndWidget.thumbnailImage.uploadNew.title=Upload an image file as your dataset thumbnail, which will be stored separately from the data files that belong to your dataset. dataset.thumbnailsAndWidget.thumbnailImage.upload=Upload Image -dataset.thumbnailsAndWidget.thumbnailImage.upload.invalidMsg=The image could not be uploaded. Please try again with a JPG, TIF, or PNG file. +dataset.thumbnailsAndWidget.thumbnailImage.upload.invalidMsg=The image could not be uploaded. Please try again with a JPG or PNG file. dataset.thumbnailsAndWidget.thumbnailImage.alt=Thumbnail image selected for dataset dataset.thumbnailsAndWidget.success=Dataset thumbnail updated. dataset.thumbnailsAndWidget.removeThumbnail=Remove Thumbnail @@ -2174,6 +2237,8 @@ file.metadataTab.fileMetadata.type.label=Type file.metadataTab.fileMetadata.description.label=Description file.metadataTab.fileMetadata.publicationDate.label=Publication Date file.metadataTab.fileMetadata.embargoReason.label=Embargo Reason +file.metadataTab.fileMetadata.retentionDate.label=Retention End Date +file.metadataTab.fileMetadata.retentionReason.label=Retention Reason file.metadataTab.fileMetadata.metadataReleaseDate.label=Metadata Release Date file.metadataTab.fileMetadata.depositDate.label=Deposit Date file.metadataTab.fileMetadata.hierarchy.label=File Path @@ -2203,6 +2268,15 @@ ingest.csv.lineMismatch=Mismatch between line counts in first and final passes!, ingest.csv.recordMismatch=Reading mismatch, line {0} of the Data file: {1} delimited values expected, {2} found. ingest.csv.nullStream=Stream can't be null. +file.ingest=Ingest +file.uningest=Uningest +file.ingest.alreadyIngestedWarning=This file has already been ingested +file.ingest.ingestInProgressWarning=Ingestion of this file is already in progress +file.ingest.cantIngestFileWarning=Ingest not supported for this file type +file.ingest.ingestQueued=Ingestion has been requested +file.ingest.cantUningestFileWarning=This file cannot be uningested +file.uningest.complete=Uningestion of this file has been completed + # editdatafile.xhtml # editFilesFragment.xhtml @@ -2298,20 +2372,6 @@ citationFrame.banner.closeIcon=Close this message, go to dataset citationFrame.banner.countdownMessage= This message will close in citationFrame.banner.countdownMessage.seconds=seconds -# Friendly AuthenticationProvider names -authenticationProvider.name.builtin=Dataverse -authenticationProvider.name.null=(provider is unknown) -authenticationProvider.name.github=GitHub -authenticationProvider.name.google=Google -authenticationProvider.name.orcid=ORCiD -authenticationProvider.name.orcid-sandbox=ORCiD Sandbox -authenticationProvider.name.shib=Shibboleth -ingest.csv.invalidHeader=Invalid header row. One of the cells is empty. -ingest.csv.lineMismatch=Mismatch between line counts in first and final passes!, {0} found on first pass, but {1} found on second. -ingest.csv.recordMismatch=Reading mismatch, line {0} of the Data file: {1} delimited values expected, {2} found. -ingest.csv.nullStream=Stream can't be null. -citationFrame.banner.countdownMessage.seconds=seconds - #file-edit-popup-fragment.xhtml #editFilesFragment.xhtml dataset.access.accessHeader=Restrict Access dataset.access.accessHeader.invalid.state=Define Data Access @@ -2411,7 +2471,7 @@ permission.publishDataverse=Publish a dataverse permission.managePermissionsDataFile=Manage permissions for a file permission.managePermissionsDataset=Manage permissions for a dataset permission.managePermissionsDataverse=Manage permissions for a dataverse -permission.editDataset=Edit a dataset's metadata +permission.editDataset=Edit a dataset's metadata, license, terms and add/delete files permission.editDataverse=Edit a dataverse's metadata, facets, customization, and templates permission.downloadFile=Download a file permission.viewUnpublishedDataset=View an unpublished dataset and its files @@ -2511,6 +2571,7 @@ dataset.registered.msg=Your dataset is now registered. dataset.notlinked=DatasetNotLinked dataset.notlinked.msg=There was a problem linking this dataset to yours: dataset.linking.popop.already.linked.note=Note: This dataset is already linked to the following dataverse(s): +dataset.linking.popup.not.linked.note=Note: This dataset is not linked to any of your accessible dataverses datasetversion.archive.success=Archival copy of Version successfully submitted datasetversion.archive.failure=Error in submitting an archival copy datasetversion.update.failure=Dataset Version Update failed. Changes are still in the DRAFT version. @@ -2614,6 +2675,7 @@ pid.allowedCharacters=^[A-Za-z0-9._/:\\-]* command.exception.only.superusers={1} can only be called by superusers. command.exception.user.deactivated={0} failed: User account has been deactivated. command.exception.user.deleted={0} failed: User account has been deleted. +command.exception.user.ratelimited={0} failed: Rate limited due to too many requests. #Admin-API admin.api.auth.mustBeSuperUser=Forbidden. You must be a superuser. @@ -2633,13 +2695,16 @@ admin.api.deleteUser.success=Authenticated User {0} deleted. #Files.java files.api.metadata.update.duplicateFile=Filename already exists at {0} files.api.no.draft=No draft available for this file +files.api.no.draftOrUnauth=Dataset version cannot be found or unauthorized. +files.api.notFoundInVersion="File metadata for file with id {0} in dataset version {1} not found" files.api.only.tabular.supported=This operation is only available for tabular files. +files.api.fileNotFound=File could not be found. #Datasets.java datasets.api.updatePIDMetadata.failure.dataset.must.be.released=Modify Registration Metadata must be run on a published dataset. datasets.api.updatePIDMetadata.auth.mustBeSuperUser=Forbidden. You must be a superuser. datasets.api.updatePIDMetadata.success.for.single.dataset=Dataset {0} PID Metadata updated successfully. -datasets.api.updatePIDMetadata.success.for.update.all=All Dataset PID Metadata update completed successfully. +datasets.api.updatePIDMetadata.success.for.update.all=All Dataset PID Metadata update completed. See log for any issues. datasets.api.moveDataset.error.targetDataverseNotFound=Target dataverse not found. datasets.api.moveDataset.error.suggestForce=Use the query parameter forceMove=true to complete the move. datasets.api.moveDataset.success=Dataset moved successfully. @@ -2650,6 +2715,7 @@ datasets.api.datasize.ioerror=Fatal IO error while trying to determine the total datasets.api.grant.role.not.found.error=Cannot find role named ''{0}'' in dataverse {1} datasets.api.grant.role.cant.create.assignment.error=Cannot create assignment: {0} datasets.api.grant.role.assignee.not.found.error=Assignee not found +datasets.api.grant.role.assignee.has.role.error=User already has this role for this dataset datasets.api.revoke.role.not.found.error="Role assignment {0} not found" datasets.api.revoke.role.success=Role {0} revoked for assignee {1} in {2} datasets.api.privateurl.error.datasetnotfound=Could not find dataset. @@ -2666,6 +2732,18 @@ datasets.api.deaccessionDataset.invalid.forward.url=Invalid deaccession forward datasets.api.globusdownloaddisabled=File transfer from Dataverse via Globus is not available for this dataset. datasets.api.globusdownloadnotfound=List of files to transfer not found. datasets.api.globusuploaddisabled=File transfer to Dataverse via Globus is not available for this dataset. +datasets.api.pidgenerator.notfound=No PID Generator configured for the give id. +datasets.api.thumbnail.fileToLarge=File is larger than maximum size: {0} +datasets.api.thumbnail.nonDatasetFailed=In setNonDatasetFileAsThumbnail could not generate thumbnail from uploaded file. +datasets.api.thumbnail.notDeleted=User wanted to remove the thumbnail it still has one! +datasets.api.thumbnail.actionNotSupported=Whatever you are trying to do to the dataset thumbnail is not supported. +datasets.api.thumbnail.nonDatasetsFileIsNull=In setNonDatasetFileAsThumbnail uploadedFile was null. +datasets.api.thumbnail.inputStreamToFile.exception=In setNonDatasetFileAsThumbnail caught exception calling inputStreamToFile: {0} +datasets.api.thumbnail.missing=Dataset thumbnail is unexpectedly absent. +datasets.api.thumbnail.basedOnWrongFileId=Dataset thumbnail should be based on file id {0} but instead it is {1} +datasets.api.thumbnail.fileNotFound=Could not find file based on id supplied: {0} +datasets.api.thumbnail.fileNotSupplied=A file was not selected to be the new dataset thumbnail. +datasets.api.thumbnail.noChange=No changes to save. #Dataverses.java dataverses.api.update.default.contributor.role.failure.role.not.found=Role {0} not found. @@ -2678,6 +2756,7 @@ dataverses.api.move.dataverse.failure.not.published=Published dataverse may not dataverses.api.move.dataverse.error.guestbook=Dataset guestbook is not in target dataverse. dataverses.api.move.dataverse.error.template=Dataverse template is not in target dataverse. dataverses.api.move.dataverse.error.featured=Dataverse is featured in current dataverse. +dataverses.api.delete.featured.collections.successful=Featured dataverses have been removed dataverses.api.move.dataverse.error.metadataBlock=Dataverse metadata block is not in target dataverse. dataverses.api.move.dataverse.error.dataverseLink=Dataverse is linked to target dataverse or one of its parents. dataverses.api.move.dataverse.error.datasetLink=Dataset is linked to target dataverse or one of its parents. @@ -2686,7 +2765,7 @@ dataverses.api.create.dataset.error.mustIncludeVersion=Please provide initial ve dataverses.api.create.dataset.error.superuserFiles=Only a superuser may add files via this api dataverses.api.create.dataset.error.mustIncludeAuthorName=Please provide author name in the dataset json dataverses.api.validate.json.succeeded=The Dataset JSON provided is valid for this Dataverse Collection. -dataverses.api.validate.json.failed=The Dataset JSON provided failed validation with the following error: +dataverses.api.validate.json.failed=The Dataset JSON provided failed validation with the following error: dataverses.api.validate.json.exception=Validation failed with following exception: #Access.java @@ -2700,6 +2779,8 @@ access.api.fileAccess.failure.noUser=Could not find user to execute command: {0} access.api.requestAccess.failure.commandError=Problem trying request access on {0} : {1} access.api.requestAccess.failure.requestExists=An access request for this file on your behalf already exists. access.api.requestAccess.failure.invalidRequest=You may not request access to this file. It may already be available to you. +access.api.requestAccess.failure.retentionExpired=You may not request access to this file. It is not available because its retention period has ended. + access.api.requestAccess.noKey=You must provide a key to request access to a file. access.api.requestAccess.fileNotFound=Could not find datafile with id {0}. access.api.requestAccess.invalidRequest=This file is already available to you for download or you have a pending request @@ -2745,7 +2826,7 @@ permission.PublishDataverse.desc=Publish a dataverse permission.ManageFilePermissions.desc=Manage permissions for a file permission.ManageDatasetPermissions.desc=Manage permissions for a dataset permission.ManageDataversePermissions.desc=Manage permissions for a dataverse -permission.EditDataset.desc=Edit a dataset's metadata +permission.EditDataset.desc=Edit a dataset's metadata, license, terms and add/delete files permission.EditDataverse.desc=Edit a dataverse's metadata, facets, customization, and templates permission.DownloadFile.desc=Download a file permission.ViewUnpublishedDataset.desc=View an unpublished dataset and its files @@ -2839,7 +2920,22 @@ passwdVal.passwdReq.lowercase=lowercase passwdVal.passwdReq.letter=letter passwdVal.passwdReq.numeral=numeral passwdVal.passwdReq.special=special +#mydata API (DataRetriverAPI.java and MyDataFinder.java) dataretrieverAPI.noMsgResultsFound=Sorry, no results were found. +dataretrieverAPI.authentication.required=Requires authentication. Please login. +dataretrieverAPI.authentication.required.opt=retrieveMyDataAsJsonString. User not found! Shouldn't be using this anyway. +dataretrieverAPI.user.not.found=No user found for: "{0}" +dataretrieverAPI.solr.error=Sorry! There was an error with the search service. +dataretrieverAPI.solr.error.opt=Sorry! There was a Solr Error. +myDataFilterParams.error.no.user=Sorry! No user was found! +myDataFilterParams.error.result.no.role=No results. Please select at least one Role. +myDataFilterParams.error.result.no.dvobject=No results. Please select one of Dataverses, Datasets, Files. +myDataFilterParams.error.result.no.publicationStatus=No results. Please select one of {0}. +myDataFinder.error.result.null=Sorry, the authenticated user ID could not be retrieved. +myDataFinder.error.result.no.role=Sorry, you have no assigned roles. +myDataFinder.error.result.role.empty=Sorry, nothing was found for this role: {0} +myDataFinder.error.result.roles.empty=Sorry, nothing was found for these roles: {0} +myDataFinder.error.result.no.dvobject=Sorry, you have no assigned Dataverses, Datasets, or Files. #xlsxfilereader.java xlsxfilereader.ioexception.parse=Could not parse Excel/XLSX spreadsheet. {0} @@ -2896,6 +2992,7 @@ Public=Public Restricted=Restricted EmbargoedThenPublic=Embargoed then Public EmbargoedThenRestricted=Embargoed then Restricted +RetentionPeriodExpired=Retention Period Expired #metadata source - Facet Label Harvested=Harvested @@ -2937,14 +3034,31 @@ pids.api.reservePid.success=PID reserved for {0} pids.api.deletePid.success=PID deleted for {0} pids.deletePid.failureExpected=Unable to delete PID {0}. Status code: {1}. pids.deletePid.failureOther=Problem deleting PID {0}: {1} -pids.commands.reservePid.failure=Problem reserving PID for dataset id {0}: {1}. pids.datacite.errors.noResponseCode=Problem getting HTTP status code from {0}. Is it in DNS? Is doi.dataciterestapiurlstring configured properly? pids.datacite.errors.DoiOnly=Only doi: is supported. -#PublishDatasetCommand -publishDatasetCommand.pidNotReserved=Cannot publish dataset because its persistent identifier has not been reserved. +#AbstractDatasetCommand +abstractDatasetCommand.pidNotReserved=Unable to reserve a persistent identifier for the dataset: {0}. +abstractDatasetCommand.filePidNotReserved=Unable to reserve a persistent identifier for one or more files in the dataset: {0}. +abstractDatasetCommand.pidReservationRetryExceeded="This dataset may not be registered because its identifier is already in use by another dataset: gave up after {0} attempts. Current (last requested) identifier: {1}" # APIs api.errors.invalidApiToken=Invalid API token. api.ldninbox.citation.alert={0},

    The {1} has just been notified that the {2}, {3}, cites "{6}" in this repository. api.ldninbox.citation.subject={0}: A Dataset Citation has been reported! + +#Schema Validation +schema.validation.exception.value.missing=Invalid data for key:{0} typeName:{1}. 'value' missing. +schema.validation.exception.list.notmultiple=Invalid data for key:{0} typeName:{1}. Found value as list but ''multiple'' is set to false. +schema.validation.exception.notlist.multiple=Invalid data for key:{0} typeName:{1}. Fields with ''multiple'' set to true must be a list. +schema.validation.exception.compound=Compound data type must be accompanied by a value that is either an object (multiple=false) or a list of objects (multiple=true) +schema.validation.exception.compound.mismatch=Compound value {0} must match typeName of the object. Found {1} +schema.validation.exception.dataset.cvv.missing=Controlled vocabulary for key:{0} typeName:{1} value:''{2}'' is not valid. +schema.validation.exception.dataset.invalidType=Invalid data for key:{0} typeName:{1}. Only {2} allowed. +schema.validation.exception.dataset.required.missing=Invalid data for key:{0}. {1} is(are) required if field type is {2}. + +#Info.java +openapi.exception.invalid.format=Invalid format {0}, currently supported formats are YAML and JSON. +openapi.exception=Supported format definition not found. +openapi.exception.unaligned=Unaligned parameters on Headers [{0}] and Request [{1}] + diff --git a/src/main/java/propertyFiles/License.properties b/src/main/java/propertyFiles/License.properties index 6ded8c41d5b..6602f408a12 100644 --- a/src/main/java/propertyFiles/License.properties +++ b/src/main/java/propertyFiles/License.properties @@ -2,3 +2,5 @@ license.cc0_1.0.description=Creative Commons CC0 1.0 Universal Public Domain Ded license.cc_by_4.0.description=Creative Commons Attribution 4.0 International License. license.cc0_1.0.name=CC0 1.0 license.cc_by_4.0.name=CC BY 4.0 +license.custom_terms.name=Custom Terms + diff --git a/src/main/java/propertyFiles/MimeTypeDetectionByFileExtension.properties b/src/main/java/propertyFiles/MimeTypeDetectionByFileExtension.properties index 97b2eed111c..630539d912e 100644 --- a/src/main/java/propertyFiles/MimeTypeDetectionByFileExtension.properties +++ b/src/main/java/propertyFiles/MimeTypeDetectionByFileExtension.properties @@ -38,3 +38,4 @@ nf=text/x-nextflow Rmd=text/x-r-notebook rb=text/x-ruby-script dag=text/x-dagman +glb=model/gltf-binary diff --git a/src/main/java/propertyFiles/MimeTypeDetectionByFileName.properties b/src/main/java/propertyFiles/MimeTypeDetectionByFileName.properties index 70b0c4e371e..5c1a22bfd5f 100644 --- a/src/main/java/propertyFiles/MimeTypeDetectionByFileName.properties +++ b/src/main/java/propertyFiles/MimeTypeDetectionByFileName.properties @@ -2,3 +2,5 @@ Makefile=text/x-makefile Snakemake=text/x-snakemake Dockerfile=application/x-docker-file Vagrantfile=application/x-vagrant-file +ro-crate-metadata.json=application/ld+json; profile="http://www.w3.org/ns/json-ld#flattened http://www.w3.org/ns/json-ld#compacted https://w3id.org/ro/crate" +ro-crate-metadata.jsonld=application/ld+json; profile="http://www.w3.org/ns/json-ld#flattened http://www.w3.org/ns/json-ld#compacted https://w3id.org/ro/crate" diff --git a/src/main/java/propertyFiles/MimeTypeDisplay.properties b/src/main/java/propertyFiles/MimeTypeDisplay.properties index 295ac226fa1..549b2b13442 100644 --- a/src/main/java/propertyFiles/MimeTypeDisplay.properties +++ b/src/main/java/propertyFiles/MimeTypeDisplay.properties @@ -207,6 +207,7 @@ audio/ogg=OGG Audio audio/wav=Waveform Audio audio/x-wav=Waveform Audio audio/x-wave=Waveform Audio +audio/vnd.wave=Waveform Audio # Video video/avi=AVI Video video/x-msvideo=AVI Video @@ -218,9 +219,12 @@ video/quicktime=Quicktime Video video/webm=WebM Video # Network Data text/xml-graphml=GraphML Network Data +# 3D Data +model/gltf-binary=3D Model # Other application/octet-stream=Unknown application/x-docker-file=Docker Image File application/x-vagrant-file=Vagrant Image File +application/ld+json;\u0020profile\u003d\u0022http\u003a//www.w3.org/ns/json-ld#flattened\u0020http\u003a//www.w3.org/ns/json-ld#compacted\u0020https\u003a//w3id.org/ro/crate\u0022=RO-Crate metadata # Dataverse-specific application/vnd.dataverse.file-package=Dataverse Package diff --git a/src/main/java/propertyFiles/MimeTypeFacets.properties b/src/main/java/propertyFiles/MimeTypeFacets.properties index aaab66f20ae..0b0fde89cbd 100644 --- a/src/main/java/propertyFiles/MimeTypeFacets.properties +++ b/src/main/java/propertyFiles/MimeTypeFacets.properties @@ -209,6 +209,7 @@ audio/ogg=Audio audio/wav=Audio audio/x-wav=Audio audio/x-wave=Audio +audio/vnd.wave=Audio # (anything else that looks like audio/* will also be indexed as facet type "Audio") # Video video/avi=Video @@ -222,7 +223,10 @@ video/webm=Video # (anything else that looks like image/* will also be indexed as facet type "Video") # Network Data text/xml-graphml=Network Data +# 3D Data +model/gltf-binary=3D Data # Other application/octet-stream=Unknown +application/ld+json;\u0020profile\u003d\u0022http\u003a//www.w3.org/ns/json-ld#flattened\u0020http\u003a//www.w3.org/ns/json-ld#compacted\u0020https\u003a//w3id.org/ro/crate\u0022=Metadata # Dataverse-specific application/vnd.dataverse.file-package=Data diff --git a/src/main/java/propertyFiles/biomedical.properties b/src/main/java/propertyFiles/biomedical.properties index 1bffed2ee03..7392ba823c4 100644 --- a/src/main/java/propertyFiles/biomedical.properties +++ b/src/main/java/propertyFiles/biomedical.properties @@ -96,7 +96,6 @@ controlledvocabulary.studyAssayMeasurementType.targeted_sequencing=targeted sequ controlledvocabulary.studyAssayMeasurementType.transcription_factor_binding_(chip-seq)=transcription factor binding (ChIP-Seq) controlledvocabulary.studyAssayMeasurementType.transcription_factor_binding_site_identification=transcription factor binding site identification controlledvocabulary.studyAssayMeasurementType.transcription_profiling=transcription profiling -controlledvocabulary.studyAssayMeasurementType.transcription_profiling=transcription profiling controlledvocabulary.studyAssayMeasurementType.transcription_profiling_(microarray)=transcription profiling (Microarray) controlledvocabulary.studyAssayMeasurementType.transcription_profiling_(rna-seq)=transcription profiling (RNA-Seq) controlledvocabulary.studyAssayMeasurementType.trap_translational_profiling=TRAP translational profiling diff --git a/src/main/java/propertyFiles/citation.properties b/src/main/java/propertyFiles/citation.properties index f35ede79b50..5899523da67 100644 --- a/src/main/java/propertyFiles/citation.properties +++ b/src/main/java/propertyFiles/citation.properties @@ -22,6 +22,7 @@ datasetfieldtype.dsDescriptionValue.title=Text datasetfieldtype.dsDescriptionDate.title=Date datasetfieldtype.subject.title=Subject datasetfieldtype.keyword.title=Keyword +datasetfieldtype.keywordTermURI.title=Term URI datasetfieldtype.keywordValue.title=Term datasetfieldtype.keywordVocabulary.title=Controlled Vocabulary Name datasetfieldtype.keywordVocabularyURI.title=Controlled Vocabulary URL @@ -30,6 +31,7 @@ datasetfieldtype.topicClassValue.title=Term datasetfieldtype.topicClassVocab.title=Controlled Vocabulary Name datasetfieldtype.topicClassVocabURI.title=Controlled Vocabulary URL datasetfieldtype.publication.title=Related Publication +datasetfieldtype.publicationRelationType.title=Relation Type datasetfieldtype.publicationCitation.title=Citation datasetfieldtype.publicationIDType.title=Identifier Type datasetfieldtype.publicationIDNumber.title=Identifier @@ -100,6 +102,7 @@ datasetfieldtype.dsDescriptionValue.description=A summary describing the purpose datasetfieldtype.dsDescriptionDate.description=The date when the description was added to the Dataset. If the Dataset contains more than one description, e.g. the data producer supplied one description and the data repository supplied another, this date is used to distinguish between the descriptions datasetfieldtype.subject.description=The area of study relevant to the Dataset datasetfieldtype.keyword.description=A key term that describes an important aspect of the Dataset and information about any controlled vocabulary used +datasetfieldtype.keywordTermURI.description=A URI that points to the web presence of the Keyword Term datasetfieldtype.keywordValue.description=A key term that describes important aspects of the Dataset datasetfieldtype.keywordVocabulary.description=The controlled vocabulary used for the keyword term (e.g. LCSH, MeSH) datasetfieldtype.keywordVocabularyURI.description=The URL where one can access information about the term's controlled vocabulary @@ -108,6 +111,7 @@ datasetfieldtype.topicClassValue.description=A topic or subject term datasetfieldtype.topicClassVocab.description=The controlled vocabulary used for the keyword term (e.g. LCSH, MeSH) datasetfieldtype.topicClassVocabURI.description=The URL where one can access information about the term's controlled vocabulary datasetfieldtype.publication.description=The article or report that uses the data in the Dataset. The full list of related publications will be displayed on the metadata tab +datasetfieldtype.publicationRelationType.description=The nature of the relationship between this Dataset and the related publication datasetfieldtype.publicationCitation.description=The full bibliographic citation for the related publication datasetfieldtype.publicationIDType.description=The type of identifier that uniquely identifies a related publication datasetfieldtype.publicationIDNumber.description=The identifier for a related publication @@ -178,6 +182,7 @@ datasetfieldtype.dsDescriptionValue.watermark= datasetfieldtype.dsDescriptionDate.watermark=YYYY-MM-DD datasetfieldtype.subject.watermark= datasetfieldtype.keyword.watermark= +datasetfieldtype.keywordTermURI.watermark=https:// datasetfieldtype.keywordValue.watermark= datasetfieldtype.keywordVocabulary.watermark= datasetfieldtype.keywordVocabularyURI.watermark=https:// @@ -186,6 +191,7 @@ datasetfieldtype.topicClassValue.watermark= datasetfieldtype.topicClassVocab.watermark= datasetfieldtype.topicClassVocabURI.watermark=https:// datasetfieldtype.publication.watermark= +datasetfieldtype.publicationRelationType.watermark= datasetfieldtype.publicationCitation.watermark= datasetfieldtype.publicationIDType.watermark= datasetfieldtype.publicationIDNumber.watermark= @@ -268,6 +274,12 @@ controlledvocabulary.publicationIDType.upc=upc controlledvocabulary.publicationIDType.url=url controlledvocabulary.publicationIDType.urn=urn controlledvocabulary.publicationIDType.dash-nrs=DASH-NRS +controlledvocabulary.publicationRelationType.iscitedby=Is Cited By +controlledvocabulary.publicationRelationType.cites=Cites +controlledvocabulary.publicationRelationType.issupplementto=Is Supplement To +controlledvocabulary.publicationRelationType.issupplementedby=Is Supplemented By +controlledvocabulary.publicationRelationType.isreferencedby=Is Referenced By +controlledvocabulary.publicationRelationType.references=References controlledvocabulary.contributorType.data_collector=Data Collector controlledvocabulary.contributorType.data_curator=Data Curator controlledvocabulary.contributorType.data_manager=Data Manager @@ -293,189 +305,7919 @@ controlledvocabulary.authorIdentifierScheme.gnd=GND controlledvocabulary.authorIdentifierScheme.dai=DAI controlledvocabulary.authorIdentifierScheme.researcherid=ResearcherID controlledvocabulary.authorIdentifierScheme.scopusid=ScopusID -controlledvocabulary.language.abkhaz=Abkhaz +controlledvocabulary.language.'are'are='Are'are +controlledvocabulary.language.'auhelawa='Auhelawa +controlledvocabulary.language.a'ou=A'ou +controlledvocabulary.language.a-pucikwar=A-Pucikwar +controlledvocabulary.language.aari=Aari +controlledvocabulary.language.aasax=Aasáx +controlledvocabulary.language.abadi=Abadi +controlledvocabulary.language.abaga=Abaga +controlledvocabulary.language.abai_sungai=Abai Sungai +controlledvocabulary.language.abanyom=Abanyom +controlledvocabulary.language.abar=Abar +controlledvocabulary.language.abau=Abau +controlledvocabulary.language.abaza=Abaza +controlledvocabulary.language.abellen_ayta=Abellen Ayta +controlledvocabulary.language.abidji=Abidji +controlledvocabulary.language.abinomn=Abinomn +controlledvocabulary.language.abipon=Abipon +controlledvocabulary.language.abishira=Abishira +controlledvocabulary.language.abkhaz=Abkhaz, Abkhazian +controlledvocabulary.language.abom=Abom +controlledvocabulary.language.abon=Abon +controlledvocabulary.language.abron=Abron +controlledvocabulary.language.abu=Abu +controlledvocabulary.language.abu'_arapesh=Abu' Arapesh +controlledvocabulary.language.abua=Abua +controlledvocabulary.language.abui=Abui +controlledvocabulary.language.abun=Abun +controlledvocabulary.language.abure=Abure +controlledvocabulary.language.abureni=Abureni +controlledvocabulary.language.abe=Abé +controlledvocabulary.language.acatepec_me'phaa=Acatepec Me'phaa +controlledvocabulary.language.achagua=Achagua +controlledvocabulary.language.achang=Achang +controlledvocabulary.language.ache=Ache +controlledvocabulary.language.acheron=Acheron +controlledvocabulary.language.achi=Achi +controlledvocabulary.language.achinese=Achinese +controlledvocabulary.language.achterhoeks=Achterhoeks +controlledvocabulary.language.achuar-shiwiar=Achuar-Shiwiar +controlledvocabulary.language.achumawi=Achumawi +controlledvocabulary.language.acoli=Acoli +controlledvocabulary.language.acroa=Acroá +controlledvocabulary.language.adai=Adai +controlledvocabulary.language.adamawa_fulfulde=Adamawa Fulfulde +controlledvocabulary.language.adamorobe_sign_language=Adamorobe Sign Language +controlledvocabulary.language.adang=Adang +controlledvocabulary.language.adangbe=Adangbe +controlledvocabulary.language.adangme=Adangme +controlledvocabulary.language.adara=Adara +controlledvocabulary.language.adasen=Adasen +controlledvocabulary.language.adele=Adele +controlledvocabulary.language.adhola=Adhola +controlledvocabulary.language.adi=Adi +controlledvocabulary.language.adilabad_gondi=Adilabad Gondi +controlledvocabulary.language.adioukrou=Adioukrou +controlledvocabulary.language.adithinngithigh=Adithinngithigh +controlledvocabulary.language.adivasi_oriya=Adivasi Oriya +controlledvocabulary.language.adiwasi_garasia=Adiwasi Garasia +controlledvocabulary.language.adnyamathanha=Adnyamathanha +controlledvocabulary.language.adonara=Adonara +controlledvocabulary.language.aduge=Aduge +controlledvocabulary.language.adyghe=Adyghe +controlledvocabulary.language.adzera=Adzera +controlledvocabulary.language.aeka=Aeka +controlledvocabulary.language.aekyom=Aekyom +controlledvocabulary.language.aequian=Aequian +controlledvocabulary.language.aer=Aer +controlledvocabulary.language.afade=Afade controlledvocabulary.language.afar=Afar +controlledvocabulary.language.afghan_sign_language=Afghan Sign Language +controlledvocabulary.language.afitti=Afitti +controlledvocabulary.language.afrihili=Afrihili controlledvocabulary.language.afrikaans=Afrikaans +controlledvocabulary.language.afro-seminole_creole=Afro-Seminole Creole +controlledvocabulary.language.agarabi=Agarabi +controlledvocabulary.language.agariya=Agariya +controlledvocabulary.language.agatu=Agatu +controlledvocabulary.language.agavotaguerra=Agavotaguerra +controlledvocabulary.language.aghem=Aghem +controlledvocabulary.language.aghu=Aghu +controlledvocabulary.language.aghu-tharnggala=Aghu-Tharnggala +controlledvocabulary.language.aghul=Aghul +controlledvocabulary.language.aghwan=Aghwan +controlledvocabulary.language.agi=Agi +controlledvocabulary.language.agob=Agob +controlledvocabulary.language.agoi=Agoi +controlledvocabulary.language.aguacateco=Aguacateco +controlledvocabulary.language.aguano=Aguano +controlledvocabulary.language.aguaruna=Aguaruna +controlledvocabulary.language.aguna=Aguna +controlledvocabulary.language.agusan_manobo=Agusan Manobo +controlledvocabulary.language.agutaynen=Agutaynen +controlledvocabulary.language.agwagwune=Agwagwune +controlledvocabulary.language.ahanta=Ahanta +controlledvocabulary.language.aheri_gondi=Aheri Gondi +controlledvocabulary.language.aheu=Aheu +controlledvocabulary.language.ahirani=Ahirani +controlledvocabulary.language.ahom=Ahom +controlledvocabulary.language.ahtena=Ahtena +controlledvocabulary.language.ahwai=Ahwai +controlledvocabulary.language.ai-cham=Ai-Cham +controlledvocabulary.language.aighon=Aighon +controlledvocabulary.language.aikana=Aikanã +controlledvocabulary.language.aiklep=Aiklep +controlledvocabulary.language.aimaq=Aimaq +controlledvocabulary.language.aimele=Aimele +controlledvocabulary.language.aimol=Aimol +controlledvocabulary.language.ainbai=Ainbai +controlledvocabulary.language.ainu_(china)=Ainu (China) +controlledvocabulary.language.ainu_(japan)=Ainu (Japan) +controlledvocabulary.language.aiome=Aiome +controlledvocabulary.language.airoran=Airoran +controlledvocabulary.language.aiton=Aiton +controlledvocabulary.language.aja_(benin)=Aja (Benin) +controlledvocabulary.language.aja_(south_sudan)=Aja (South Sudan) +controlledvocabulary.language.ajawa=Ajawa +controlledvocabulary.language.ajie=Ajië +controlledvocabulary.language.ajumbu=Ajumbu +controlledvocabulary.language.ajyininka_apurucayali=Ajyíninka Apurucayali +controlledvocabulary.language.ak=Ak +controlledvocabulary.language.aka=Aka +controlledvocabulary.language.aka-bea=Aka-Bea +controlledvocabulary.language.aka-bo=Aka-Bo +controlledvocabulary.language.aka-cari=Aka-Cari +controlledvocabulary.language.aka-jeru=Aka-Jeru +controlledvocabulary.language.aka-kede=Aka-Kede +controlledvocabulary.language.aka-kol=Aka-Kol +controlledvocabulary.language.aka-kora=Aka-Kora controlledvocabulary.language.akan=Akan +controlledvocabulary.language.akar-bale=Akar-Bale +controlledvocabulary.language.akaselem=Akaselem +controlledvocabulary.language.akawaio=Akawaio +controlledvocabulary.language.ake=Ake +controlledvocabulary.language.akebu=Akebu +controlledvocabulary.language.akei=Akei +controlledvocabulary.language.akeu=Akeu +controlledvocabulary.language.akha=Akha +controlledvocabulary.language.akhvakh=Akhvakh +controlledvocabulary.language.akkadian=Akkadian +controlledvocabulary.language.akkala_sami=Akkala Sami +controlledvocabulary.language.aklanon=Aklanon +controlledvocabulary.language.akolet=Akolet +controlledvocabulary.language.akoose=Akoose +controlledvocabulary.language.akoye=Akoye +controlledvocabulary.language.akpa=Akpa +controlledvocabulary.language.akpes=Akpes +controlledvocabulary.language.akrukay=Akrukay +controlledvocabulary.language.akukem=Akukem +controlledvocabulary.language.akuku=Akuku +controlledvocabulary.language.akum=Akum +controlledvocabulary.language.akuntsu=Akuntsu +controlledvocabulary.language.akurio=Akurio +controlledvocabulary.language.akwa=Akwa +controlledvocabulary.language.akyaung_ari_naga=Akyaung Ari Naga +controlledvocabulary.language.al-sayyid_bedouin_sign_language=Al-Sayyid Bedouin Sign Language +controlledvocabulary.language.alaba-k'abeena=Alaba-K’abeena +controlledvocabulary.language.alabama=Alabama +controlledvocabulary.language.alabat_island_agta=Alabat Island Agta +controlledvocabulary.language.alacatlatzala_mixtec=Alacatlatzala Mixtec +controlledvocabulary.language.alago=Alago +controlledvocabulary.language.alagwa=Alagwa +controlledvocabulary.language.alak=Alak +controlledvocabulary.language.alamblak=Alamblak +controlledvocabulary.language.alangan=Alangan +controlledvocabulary.language.alanic=Alanic +controlledvocabulary.language.alapmunte=Alapmunte +controlledvocabulary.language.alawa=Alawa controlledvocabulary.language.albanian=Albanian +controlledvocabulary.language.albanian_sign_language=Albanian Sign Language +controlledvocabulary.language.albarradas_sign_language=Albarradas Sign Language +controlledvocabulary.language.alcozauca_mixtec=Alcozauca Mixtec +controlledvocabulary.language.alege=Alege +controlledvocabulary.language.alekano=Alekano +controlledvocabulary.language.aleut=Aleut +controlledvocabulary.language.algerian_arabic=Algerian Arabic +controlledvocabulary.language.algerian_jewish_sign_language=Algerian Jewish Sign Language +controlledvocabulary.language.algerian_saharan_arabic=Algerian Saharan Arabic +controlledvocabulary.language.algerian_sign_language=Algerian Sign Language +controlledvocabulary.language.algonquin=Algonquin +controlledvocabulary.language.ali=Ali +controlledvocabulary.language.alladian=Alladian +controlledvocabulary.language.allar=Allar +controlledvocabulary.language.alngith=Alngith +controlledvocabulary.language.alo_phola=Alo Phola +controlledvocabulary.language.alor=Alor +controlledvocabulary.language.aloapam_zapotec=Aloápam Zapotec +controlledvocabulary.language.alsea=Alsea +controlledvocabulary.language.alu_kurumba=Alu Kurumba +controlledvocabulary.language.alugu=Alugu +controlledvocabulary.language.alumu-tesu=Alumu-Tesu +controlledvocabulary.language.alune=Alune +controlledvocabulary.language.aluo=Aluo +controlledvocabulary.language.alur=Alur +controlledvocabulary.language.alutor=Alutor +controlledvocabulary.language.alviri-vidari=Alviri-Vidari +controlledvocabulary.language.alyawarr=Alyawarr +controlledvocabulary.language.ama_(papua_new_guinea)=Ama (Papua New Guinea) +controlledvocabulary.language.ama_(sudan)=Ama (Sudan) +controlledvocabulary.language.amahai=Amahai +controlledvocabulary.language.amahuaca=Amahuaca +controlledvocabulary.language.amaimon=Amaimon +controlledvocabulary.language.amal=Amal +controlledvocabulary.language.amami_koniya_sign_language=Amami Koniya Sign Language +controlledvocabulary.language.amanab=Amanab +controlledvocabulary.language.amanaye=Amanayé +controlledvocabulary.language.amara=Amara +controlledvocabulary.language.amarakaeri=Amarakaeri +controlledvocabulary.language.amarasi=Amarasi +controlledvocabulary.language.amatlan_zapotec=Amatlán Zapotec +controlledvocabulary.language.amba_(solomon_islands)=Amba (Solomon Islands) +controlledvocabulary.language.amba_(uganda)=Amba (Uganda) +controlledvocabulary.language.ambai=Ambai +controlledvocabulary.language.ambakich=Ambakich +controlledvocabulary.language.ambala_ayta=Ambala Ayta +controlledvocabulary.language.ambelau=Ambelau +controlledvocabulary.language.ambele=Ambele +controlledvocabulary.language.amblong=Amblong +controlledvocabulary.language.ambo=Ambo +controlledvocabulary.language.ambo-pasco_quechua=Ambo-Pasco Quechua +controlledvocabulary.language.ambonese_malay=Ambonese Malay +controlledvocabulary.language.ambrak=Ambrak +controlledvocabulary.language.ambul=Ambul +controlledvocabulary.language.ambulas=Ambulas +controlledvocabulary.language.amdang=Amdang +controlledvocabulary.language.amdo_tibetan=Amdo Tibetan +controlledvocabulary.language.amele=Amele +controlledvocabulary.language.american_sign_language=American Sign Language +controlledvocabulary.language.amganad_ifugao=Amganad Ifugao controlledvocabulary.language.amharic=Amharic +controlledvocabulary.language.ami=Ami +controlledvocabulary.language.amis=Amis +controlledvocabulary.language.amo=Amo +controlledvocabulary.language.amol=Amol +controlledvocabulary.language.amoltepec_mixtec=Amoltepec Mixtec +controlledvocabulary.language.ampanang=Ampanang +controlledvocabulary.language.ampari_dogon=Ampari Dogon +controlledvocabulary.language.amri_karbi=Amri Karbi +controlledvocabulary.language.amto=Amto +controlledvocabulary.language.amundava=Amundava +controlledvocabulary.language.amurdak=Amurdak +controlledvocabulary.language.ana_tinga_dogon=Ana Tinga Dogon +controlledvocabulary.language.anaang=Anaang +controlledvocabulary.language.anakalangu=Anakalangu +controlledvocabulary.language.anal=Anal +controlledvocabulary.language.anam=Anam +controlledvocabulary.language.anambe=Anambé +controlledvocabulary.language.anamgura=Anamgura +controlledvocabulary.language.anasi=Anasi +controlledvocabulary.language.ancient_greek_(to_1453)=Ancient Greek (to 1453) +controlledvocabulary.language.ancient_hebrew=Ancient Hebrew +controlledvocabulary.language.ancient_macedonian=Ancient Macedonian +controlledvocabulary.language.ancient_north_arabian=Ancient North Arabian +controlledvocabulary.language.ancient_zapotec=Ancient Zapotec +controlledvocabulary.language.andaandi=Andaandi +controlledvocabulary.language.andai=Andai +controlledvocabulary.language.andajin=Andajin +controlledvocabulary.language.andalusian_arabic=Andalusian Arabic +controlledvocabulary.language.andaman_creole_hindi=Andaman Creole Hindi +controlledvocabulary.language.andaqui=Andaqui +controlledvocabulary.language.andarum=Andarum +controlledvocabulary.language.andegerebinha=Andegerebinha +controlledvocabulary.language.andh=Andh +controlledvocabulary.language.andi=Andi +controlledvocabulary.language.andio=Andio +controlledvocabulary.language.andoa=Andoa +controlledvocabulary.language.andoque=Andoque +controlledvocabulary.language.andra-hus=Andra-Hus +controlledvocabulary.language.aneityum=Aneityum +controlledvocabulary.language.anem=Anem +controlledvocabulary.language.aneme_wake=Aneme Wake +controlledvocabulary.language.anfillo=Anfillo +controlledvocabulary.language.angaataha=Angaataha +controlledvocabulary.language.angaite=Angaité +controlledvocabulary.language.angal=Angal +controlledvocabulary.language.angal_enen=Angal Enen +controlledvocabulary.language.angal_heneng=Angal Heneng +controlledvocabulary.language.angami_naga=Angami Naga +controlledvocabulary.language.angguruk_yali=Angguruk Yali +controlledvocabulary.language.angika=Angika +controlledvocabulary.language.angkamuthi=Angkamuthi +controlledvocabulary.language.anglo-norman=Anglo-Norman +controlledvocabulary.language.angloromani=Angloromani +controlledvocabulary.language.angolar=Angolar +controlledvocabulary.language.angor=Angor +controlledvocabulary.language.angoram=Angoram +controlledvocabulary.language.angosturas_tunebo=Angosturas Tunebo +controlledvocabulary.language.anguthimri=Anguthimri +controlledvocabulary.language.ani_phowa=Ani Phowa +controlledvocabulary.language.anii=Anii +controlledvocabulary.language.animere=Animere +controlledvocabulary.language.anindilyakwa=Anindilyakwa +controlledvocabulary.language.aninka=Aninka +controlledvocabulary.language.anjam=Anjam +controlledvocabulary.language.ankave=Ankave +controlledvocabulary.language.anmatyerre=Anmatyerre +controlledvocabulary.language.anong=Anong +controlledvocabulary.language.anor=Anor +controlledvocabulary.language.anserma=Anserma +controlledvocabulary.language.ansus=Ansus +controlledvocabulary.language.antakarinya=Antakarinya +controlledvocabulary.language.antankarana_malagasy=Antankarana Malagasy +controlledvocabulary.language.antigua_and_barbuda_creole_english=Antigua and Barbuda Creole English +controlledvocabulary.language.anu-hkongso_chin=Anu-Hkongso Chin +controlledvocabulary.language.anuak=Anuak +controlledvocabulary.language.anufo=Anufo +controlledvocabulary.language.anuki=Anuki +controlledvocabulary.language.anus=Anus +controlledvocabulary.language.anuta=Anuta +controlledvocabulary.language.anyin=Anyin +controlledvocabulary.language.anyin_morofo=Anyin Morofo +controlledvocabulary.language.ao_naga=Ao Naga +controlledvocabulary.language.aoheng=Aoheng +controlledvocabulary.language.aore=Aore +controlledvocabulary.language.ap_ma=Ap Ma +controlledvocabulary.language.apalachee=Apalachee +controlledvocabulary.language.apalai=Apalaí +controlledvocabulary.language.apali=Apali +controlledvocabulary.language.apasco-apoala_mixtec=Apasco-Apoala Mixtec +controlledvocabulary.language.apatani=Apatani +controlledvocabulary.language.apiaka=Apiaká +controlledvocabulary.language.apinaye=Apinayé +controlledvocabulary.language.apma=Apma +controlledvocabulary.language.aproumu_aizi=Aproumu Aizi +controlledvocabulary.language.apurina=Apurinã +controlledvocabulary.language.aputai=Aputai +controlledvocabulary.language.aquitanian=Aquitanian +controlledvocabulary.language.arabana=Arabana +controlledvocabulary.language.arabela=Arabela controlledvocabulary.language.arabic=Arabic controlledvocabulary.language.aragonese=Aragonese +controlledvocabulary.language.araki=Araki +controlledvocabulary.language.arakwal=Arakwal +controlledvocabulary.language.aralle-tabulahan=Aralle-Tabulahan +controlledvocabulary.language.arammba=Arammba +controlledvocabulary.language.aranadan=Aranadan +controlledvocabulary.language.aranama-tamique=Aranama-Tamique +controlledvocabulary.language.arandai=Arandai +controlledvocabulary.language.araona=Araona +controlledvocabulary.language.arapaho=Arapaho +controlledvocabulary.language.arapaso=Arapaso +controlledvocabulary.language.ararandewara=Ararandewára +controlledvocabulary.language.arawak=Arawak +controlledvocabulary.language.arawete=Araweté +controlledvocabulary.language.arawum=Arawum +controlledvocabulary.language.arbore=Arbore +controlledvocabulary.language.arbereshe_albanian=Arbëreshë Albanian +controlledvocabulary.language.archi=Archi +controlledvocabulary.language.ardhamagadhi_prakrit=Ardhamāgadhī Prākrit +controlledvocabulary.language.are=Are +controlledvocabulary.language.areba=Areba +controlledvocabulary.language.arem=Arem +controlledvocabulary.language.arequipa-la_union_quechua=Arequipa-La Unión Quechua +controlledvocabulary.language.argentine_sign_language=Argentine Sign Language +controlledvocabulary.language.argobba=Argobba +controlledvocabulary.language.arguni=Arguni +controlledvocabulary.language.arhuaco=Arhuaco +controlledvocabulary.language.arha=Arhâ +controlledvocabulary.language.arho=Arhö +controlledvocabulary.language.ari=Ari +controlledvocabulary.language.aribwatsa=Aribwatsa +controlledvocabulary.language.aribwaung=Aribwaung +controlledvocabulary.language.arifama-miniafia=Arifama-Miniafia +controlledvocabulary.language.arigidi=Arigidi +controlledvocabulary.language.arikapu=Arikapú +controlledvocabulary.language.arikara=Arikara +controlledvocabulary.language.arikem=Arikem +controlledvocabulary.language.arin=Arin +controlledvocabulary.language.aringa=Aringa +controlledvocabulary.language.armazic=Armazic controlledvocabulary.language.armenian=Armenian +controlledvocabulary.language.armenian_sign_language=Armenian Sign Language +controlledvocabulary.language.arop-lokep=Arop-Lokep +controlledvocabulary.language.arop-sissano=Arop-Sissano +controlledvocabulary.language.arosi=Arosi +controlledvocabulary.language.arpitan=Arpitan +controlledvocabulary.language.arritinngithigh=Arritinngithigh +controlledvocabulary.language.arta=Arta +controlledvocabulary.language.aruamu=Aruamu +controlledvocabulary.language.aruek=Aruek +controlledvocabulary.language.aruop=Aruop +controlledvocabulary.language.arutani=Arutani +controlledvocabulary.language.arua_(amazonas_state)=Aruá (Amazonas State) +controlledvocabulary.language.arua_(rodonia_state)=Aruá (Rodonia State) +controlledvocabulary.language.arvanitika_albanian=Arvanitika Albanian +controlledvocabulary.language.as=As +controlledvocabulary.language.asaro'o=Asaro'o +controlledvocabulary.language.ashe=Ashe +controlledvocabulary.language.ashkun=Ashkun +controlledvocabulary.language.asho_chin=Asho Chin +controlledvocabulary.language.ashtiani=Ashtiani +controlledvocabulary.language.ashaninka=Asháninka +controlledvocabulary.language.asheninka_pajonal=Ashéninka Pajonal +controlledvocabulary.language.asheninka_perene=Ashéninka Perené +controlledvocabulary.language.asilulu=Asilulu +controlledvocabulary.language.askopan=Askopan +controlledvocabulary.language.asoa=Asoa controlledvocabulary.language.assamese=Assamese +controlledvocabulary.language.assangori=Assangori +controlledvocabulary.language.assiniboine=Assiniboine +controlledvocabulary.language.assyrian_neo-aramaic=Assyrian Neo-Aramaic +controlledvocabulary.language.asturian=Asturian +controlledvocabulary.language.asu_(nigeria)=Asu (Nigeria) +controlledvocabulary.language.asu_(tanzania)=Asu (Tanzania) +controlledvocabulary.language.asue_awyu=Asue Awyu +controlledvocabulary.language.asumboa=Asumboa +controlledvocabulary.language.asuncion_mixtepec_zapotec=Asunción Mixtepec Zapotec +controlledvocabulary.language.asuri=Asuri +controlledvocabulary.language.ata=Ata +controlledvocabulary.language.ata_manobo=Ata Manobo +controlledvocabulary.language.atakapa=Atakapa +controlledvocabulary.language.atampaya=Atampaya +controlledvocabulary.language.atatlahuca_mixtec=Atatláhuca Mixtec +controlledvocabulary.language.atauran=Atauran +controlledvocabulary.language.atayal=Atayal +controlledvocabulary.language.atemble=Atemble +controlledvocabulary.language.athpariya=Athpariya +controlledvocabulary.language.ati=Ati +controlledvocabulary.language.atikamekw=Atikamekw +controlledvocabulary.language.atohwaim=Atohwaim +controlledvocabulary.language.atong_(cameroon)=Atong (Cameroon) +controlledvocabulary.language.atong_(india)=Atong (India) +controlledvocabulary.language.atorada=Atorada +controlledvocabulary.language.atsahuaca=Atsahuaca +controlledvocabulary.language.atsam=Atsam +controlledvocabulary.language.atsugewi=Atsugewi +controlledvocabulary.language.attapady_kurumba=Attapady Kurumba +controlledvocabulary.language.attie=Attié +controlledvocabulary.language.atzingo_matlatzinca=Atzingo Matlatzinca +controlledvocabulary.language.au=Au +controlledvocabulary.language.aulua=Aulua +controlledvocabulary.language.aura=Aurá +controlledvocabulary.language.aushi=Aushi +controlledvocabulary.language.aushiri=Aushiri +controlledvocabulary.language.auslan=Auslan +controlledvocabulary.language.austral=Austral +controlledvocabulary.language.australian_aborigines_sign_language=Australian Aborigines Sign Language +controlledvocabulary.language.austrian_sign_language=Austrian Sign Language +controlledvocabulary.language.auwe=Auwe +controlledvocabulary.language.auye=Auye +controlledvocabulary.language.auyokawa=Auyokawa controlledvocabulary.language.avaric=Avaric +controlledvocabulary.language.avatime=Avatime +controlledvocabulary.language.avau=Avau controlledvocabulary.language.avestan=Avestan +controlledvocabulary.language.avikam=Avikam +controlledvocabulary.language.avokaya=Avokaya +controlledvocabulary.language.ava-canoeiro=Avá-Canoeiro +controlledvocabulary.language.awa_(china)=Awa (China) +controlledvocabulary.language.awa_(papua_new_guinea)=Awa (Papua New Guinea) +controlledvocabulary.language.awa-cuaiquer=Awa-Cuaiquer +controlledvocabulary.language.awabakal=Awabakal +controlledvocabulary.language.awad_bing=Awad Bing +controlledvocabulary.language.awadhi=Awadhi +controlledvocabulary.language.awak=Awak +controlledvocabulary.language.awar=Awar +controlledvocabulary.language.awara=Awara +controlledvocabulary.language.awbono=Awbono +controlledvocabulary.language.aweer=Aweer +controlledvocabulary.language.awera=Awera +controlledvocabulary.language.aweti=Awetí +controlledvocabulary.language.awing=Awing +controlledvocabulary.language.awiyaana=Awiyaana +controlledvocabulary.language.awjilah=Awjilah +controlledvocabulary.language.awngi=Awngi +controlledvocabulary.language.awngthim=Awngthim +controlledvocabulary.language.awtuw=Awtuw +controlledvocabulary.language.awu=Awu +controlledvocabulary.language.awun=Awun +controlledvocabulary.language.awutu=Awutu +controlledvocabulary.language.awyi=Awyi +controlledvocabulary.language.axamb=Axamb +controlledvocabulary.language.axi_yi=Axi Yi +controlledvocabulary.language.ayabadhu=Ayabadhu +controlledvocabulary.language.ayacucho_quechua=Ayacucho Quechua +controlledvocabulary.language.ayautla_mazatec=Ayautla Mazatec +controlledvocabulary.language.ayere=Ayere +controlledvocabulary.language.ayerrerenge=Ayerrerenge +controlledvocabulary.language.ayi_(papua_new_guinea)=Ayi (Papua New Guinea) +controlledvocabulary.language.ayiwo=Ayiwo +controlledvocabulary.language.ayizi=Ayizi +controlledvocabulary.language.ayizo_gbe=Ayizo Gbe controlledvocabulary.language.aymara=Aymara +controlledvocabulary.language.ayoquesco_zapotec=Ayoquesco Zapotec +controlledvocabulary.language.ayoreo=Ayoreo +controlledvocabulary.language.ayu=Ayu +controlledvocabulary.language.ayutla_mixtec=Ayutla Mixtec controlledvocabulary.language.azerbaijani=Azerbaijani +controlledvocabulary.language.azha=Azha +controlledvocabulary.language.azhe=Azhe +controlledvocabulary.language.azoyu_me'phaa=Azoyú Me'phaa +controlledvocabulary.language.baan=Baan +controlledvocabulary.language.baangi=Baangi +controlledvocabulary.language.baatonum=Baatonum +controlledvocabulary.language.baba=Baba +controlledvocabulary.language.baba_malay=Baba Malay +controlledvocabulary.language.babango=Babango +controlledvocabulary.language.babanki=Babanki +controlledvocabulary.language.babatana=Babatana +controlledvocabulary.language.babine=Babine +controlledvocabulary.language.babuza=Babuza +controlledvocabulary.language.bacama=Bacama +controlledvocabulary.language.bacanese_malay=Bacanese Malay +controlledvocabulary.language.bactrian=Bactrian +controlledvocabulary.language.bada_(indonesia)=Bada (Indonesia) +controlledvocabulary.language.bada_(nigeria)=Bada (Nigeria) +controlledvocabulary.language.badaga=Badaga +controlledvocabulary.language.bade=Bade +controlledvocabulary.language.badeshi=Badeshi +controlledvocabulary.language.badimaya=Badimaya +controlledvocabulary.language.badjiri=Badjiri +controlledvocabulary.language.badui=Badui +controlledvocabulary.language.badyara=Badyara +controlledvocabulary.language.baeggu=Baeggu +controlledvocabulary.language.baelelea=Baelelea +controlledvocabulary.language.baetora=Baetora +controlledvocabulary.language.bafanji=Bafanji +controlledvocabulary.language.bafaw-balong=Bafaw-Balong +controlledvocabulary.language.bafia=Bafia +controlledvocabulary.language.bafut=Bafut +controlledvocabulary.language.baga_kaloum=Baga Kaloum +controlledvocabulary.language.baga_koga=Baga Koga +controlledvocabulary.language.baga_manduri=Baga Manduri +controlledvocabulary.language.baga_pokur=Baga Pokur +controlledvocabulary.language.baga_sitemu=Baga Sitemu +controlledvocabulary.language.baga_sobane=Baga Sobané +controlledvocabulary.language.bagheli=Bagheli +controlledvocabulary.language.bagirmi=Bagirmi +controlledvocabulary.language.bagirmi_fulfulde=Bagirmi Fulfulde +controlledvocabulary.language.bago-kusuntu=Bago-Kusuntu +controlledvocabulary.language.bagri=Bagri +controlledvocabulary.language.bagupi=Bagupi +controlledvocabulary.language.bagusa=Bagusa +controlledvocabulary.language.bagvalal=Bagvalal +controlledvocabulary.language.baha_buyang=Baha Buyang +controlledvocabulary.language.baham=Baham +controlledvocabulary.language.bahamas_creole_english=Bahamas Creole English +controlledvocabulary.language.baharna_arabic=Baharna Arabic +controlledvocabulary.language.bahau=Bahau +controlledvocabulary.language.bahinemo=Bahinemo +controlledvocabulary.language.bahing=Bahing +controlledvocabulary.language.bahnar=Bahnar +controlledvocabulary.language.bahonsuai=Bahonsuai +controlledvocabulary.language.bai_(south_sudan)=Bai (South Sudan) +controlledvocabulary.language.baibai=Baibai +controlledvocabulary.language.baikeno=Baikeno +controlledvocabulary.language.baima=Baima +controlledvocabulary.language.baimak=Baimak +controlledvocabulary.language.bainouk-gunyaamolo=Bainouk-Gunyaamolo +controlledvocabulary.language.bainouk-gunyuno=Bainouk-Gunyuño +controlledvocabulary.language.bainouk-samik=Bainouk-Samik +controlledvocabulary.language.baiso=Baiso +controlledvocabulary.language.baissa_fali=Baissa Fali +controlledvocabulary.language.bajan=Bajan +controlledvocabulary.language.bajelani=Bajelani +controlledvocabulary.language.bajjika=Bajjika +controlledvocabulary.language.baka_(cameroon)=Baka (Cameroon) +controlledvocabulary.language.baka_(south_sudan)=Baka (South Sudan) +controlledvocabulary.language.bakairi=Bakairí +controlledvocabulary.language.bakaka=Bakaka +controlledvocabulary.language.bakhtiari=Bakhtiari +controlledvocabulary.language.baki=Baki +controlledvocabulary.language.bakoko=Bakoko +controlledvocabulary.language.bakole=Bakole +controlledvocabulary.language.bakpinka=Bakpinka +controlledvocabulary.language.bakumpai=Bakumpai +controlledvocabulary.language.bakwe=Bakwé +controlledvocabulary.language.balaesang=Balaesang +controlledvocabulary.language.balaibalan=Balaibalan +controlledvocabulary.language.balangao=Balangao +controlledvocabulary.language.balangingi=Balangingi +controlledvocabulary.language.balanta-ganja=Balanta-Ganja +controlledvocabulary.language.balanta-kentohe=Balanta-Kentohe +controlledvocabulary.language.balantak=Balantak +controlledvocabulary.language.baldemu=Baldemu +controlledvocabulary.language.bali_(democratic_republic_of_congo)=Bali (Democratic Republic of Congo) +controlledvocabulary.language.bali_(nigeria)=Bali (Nigeria) +controlledvocabulary.language.balinese=Balinese +controlledvocabulary.language.balinese_malay=Balinese Malay +controlledvocabulary.language.balkan_gagauz_turkish=Balkan Gagauz Turkish +controlledvocabulary.language.balkan_romani=Balkan Romani +controlledvocabulary.language.balo=Balo +controlledvocabulary.language.baloi=Baloi +controlledvocabulary.language.balti=Balti +controlledvocabulary.language.baltic_romani=Baltic Romani +controlledvocabulary.language.baluan-pam=Baluan-Pam +controlledvocabulary.language.baluchi=Baluchi +controlledvocabulary.language.bamako_sign_language=Bamako Sign Language +controlledvocabulary.language.bamali=Bamali +controlledvocabulary.language.bambalang=Bambalang +controlledvocabulary.language.bambam=Bambam controlledvocabulary.language.bambara=Bambara +controlledvocabulary.language.bambassi=Bambassi +controlledvocabulary.language.bambili-bambui=Bambili-Bambui +controlledvocabulary.language.bamenyam=Bamenyam +controlledvocabulary.language.bamu=Bamu +controlledvocabulary.language.bamukumbit=Bamukumbit +controlledvocabulary.language.bamun=Bamun +controlledvocabulary.language.bamunka=Bamunka +controlledvocabulary.language.bamwe=Bamwe +controlledvocabulary.language.ban_khor_sign_language=Ban Khor Sign Language +controlledvocabulary.language.bana=Bana +controlledvocabulary.language.banao_itneg=Banao Itneg +controlledvocabulary.language.banaro=Banaro +controlledvocabulary.language.banda_(indonesia)=Banda (Indonesia) +controlledvocabulary.language.banda_malay=Banda Malay +controlledvocabulary.language.banda-bambari=Banda-Bambari +controlledvocabulary.language.banda-banda=Banda-Banda +controlledvocabulary.language.banda-mbres=Banda-Mbrès +controlledvocabulary.language.banda-ndele=Banda-Ndélé +controlledvocabulary.language.banda-yangere=Banda-Yangere +controlledvocabulary.language.bandi=Bandi +controlledvocabulary.language.bandial=Bandial +controlledvocabulary.language.bandjalang=Bandjalang +controlledvocabulary.language.bangala=Bangala +controlledvocabulary.language.bangandu=Bangandu +controlledvocabulary.language.bangba=Bangba +controlledvocabulary.language.banggai=Banggai +controlledvocabulary.language.banggarla=Banggarla +controlledvocabulary.language.bangi=Bangi +controlledvocabulary.language.bangime=Bangime +controlledvocabulary.language.bangka=Bangka +controlledvocabulary.language.bangolan=Bangolan +controlledvocabulary.language.bangubangu=Bangubangu +controlledvocabulary.language.bangwinji=Bangwinji +controlledvocabulary.language.baniva=Baniva +controlledvocabulary.language.baniwa=Baniwa +controlledvocabulary.language.banjar=Banjar +controlledvocabulary.language.bankagooma=Bankagooma +controlledvocabulary.language.bankal=Bankal +controlledvocabulary.language.bankan_tey_dogon=Bankan Tey Dogon +controlledvocabulary.language.bankon=Bankon +controlledvocabulary.language.bannoni=Bannoni +controlledvocabulary.language.bantawa=Bantawa +controlledvocabulary.language.bantayanon=Bantayanon +controlledvocabulary.language.bantik=Bantik +controlledvocabulary.language.bantoanon=Bantoanon +controlledvocabulary.language.banyjima=Banyjima +controlledvocabulary.language.baoule=Baoulé +controlledvocabulary.language.bara_malagasy=Bara Malagasy +controlledvocabulary.language.baraamu=Baraamu +controlledvocabulary.language.barababaraba=Barababaraba +controlledvocabulary.language.barai=Barai +controlledvocabulary.language.barakai=Barakai +controlledvocabulary.language.baram_kayan=Baram Kayan +controlledvocabulary.language.barama=Barama +controlledvocabulary.language.barambu=Barambu +controlledvocabulary.language.baramu=Baramu +controlledvocabulary.language.barapasi=Barapasi +controlledvocabulary.language.baras=Baras +controlledvocabulary.language.barasana-eduria=Barasana-Eduria +controlledvocabulary.language.barbaram=Barbaram +controlledvocabulary.language.barbareno=Barbareño +controlledvocabulary.language.barclayville_grebo=Barclayville Grebo +controlledvocabulary.language.bardi=Bardi +controlledvocabulary.language.barein=Barein +controlledvocabulary.language.bargam=Bargam +controlledvocabulary.language.bari=Bari +controlledvocabulary.language.bariai=Bariai +controlledvocabulary.language.bariji=Bariji +controlledvocabulary.language.barikanchi=Barikanchi +controlledvocabulary.language.barikewa=Barikewa +controlledvocabulary.language.barok=Barok +controlledvocabulary.language.barombi=Barombi +controlledvocabulary.language.barro_negro_tunebo=Barro Negro Tunebo +controlledvocabulary.language.barrow_point=Barrow Point +controlledvocabulary.language.baruga=Baruga +controlledvocabulary.language.baruya=Baruya +controlledvocabulary.language.barwe=Barwe +controlledvocabulary.language.barzani_jewish_neo-aramaic=Barzani Jewish Neo-Aramaic +controlledvocabulary.language.bare=Baré +controlledvocabulary.language.basa_(cameroon)=Basa (Cameroon) +controlledvocabulary.language.basa_(nigeria)=Basa (Nigeria) +controlledvocabulary.language.basa-gumna=Basa-Gumna +controlledvocabulary.language.basa-gurmana=Basa-Gurmana +controlledvocabulary.language.basap=Basap +controlledvocabulary.language.basay=Basay +controlledvocabulary.language.bashkardi=Bashkardi controlledvocabulary.language.bashkir=Bashkir +controlledvocabulary.language.basketo=Basketo controlledvocabulary.language.basque=Basque +controlledvocabulary.language.bassa=Bassa +controlledvocabulary.language.bassa-kontagora=Bassa-Kontagora +controlledvocabulary.language.bassari=Bassari +controlledvocabulary.language.bassossi=Bassossi +controlledvocabulary.language.bata=Bata +controlledvocabulary.language.batad_ifugao=Batad Ifugao +controlledvocabulary.language.batak=Batak +controlledvocabulary.language.batak_alas-kluet=Batak Alas-Kluet +controlledvocabulary.language.batak_angkola=Batak Angkola +controlledvocabulary.language.batak_dairi=Batak Dairi +controlledvocabulary.language.batak_karo=Batak Karo +controlledvocabulary.language.batak_mandailing=Batak Mandailing +controlledvocabulary.language.batak_simalungun=Batak Simalungun +controlledvocabulary.language.batak_toba=Batak Toba +controlledvocabulary.language.batanga=Batanga +controlledvocabulary.language.batek=Batek +controlledvocabulary.language.bateri=Bateri +controlledvocabulary.language.bathari=Bathari +controlledvocabulary.language.bati_(cameroon)=Bati (Cameroon) +controlledvocabulary.language.bati_(indonesia)=Bati (Indonesia) +controlledvocabulary.language.batjala=Batjala +controlledvocabulary.language.bats=Bats +controlledvocabulary.language.batu=Batu +controlledvocabulary.language.batui=Batui +controlledvocabulary.language.batuley=Batuley +controlledvocabulary.language.bau=Bau +controlledvocabulary.language.bau_bidayuh=Bau Bidayuh +controlledvocabulary.language.bauchi=Bauchi +controlledvocabulary.language.bauni=Bauni +controlledvocabulary.language.baure=Baure +controlledvocabulary.language.bauria=Bauria +controlledvocabulary.language.bauwaki=Bauwaki +controlledvocabulary.language.bauzi=Bauzi +controlledvocabulary.language.bavarian=Bavarian +controlledvocabulary.language.bawm_chin=Bawm Chin +controlledvocabulary.language.bay_miwok=Bay Miwok +controlledvocabulary.language.bayali=Bayali +controlledvocabulary.language.baybayanon=Baybayanon +controlledvocabulary.language.baygo=Baygo +controlledvocabulary.language.bayono=Bayono +controlledvocabulary.language.bayot=Bayot +controlledvocabulary.language.bayungu=Bayungu +controlledvocabulary.language.bazigar=Bazigar +controlledvocabulary.language.beami=Beami +controlledvocabulary.language.beaver=Beaver +controlledvocabulary.language.beba=Beba +controlledvocabulary.language.bebele=Bebele +controlledvocabulary.language.bebeli=Bebeli +controlledvocabulary.language.bebil=Bebil +controlledvocabulary.language.bedjond=Bedjond +controlledvocabulary.language.bedoanas=Bedoanas +controlledvocabulary.language.beeke=Beeke +controlledvocabulary.language.beele=Beele +controlledvocabulary.language.beembe=Beembe +controlledvocabulary.language.beezen=Beezen +controlledvocabulary.language.befang=Befang +controlledvocabulary.language.beginci=Beginci +controlledvocabulary.language.beja=Beja +controlledvocabulary.language.bekati'=Bekati' +controlledvocabulary.language.bekwarra=Bekwarra +controlledvocabulary.language.bekwel=Bekwel +controlledvocabulary.language.belait=Belait +controlledvocabulary.language.belanda_bor=Belanda Bor +controlledvocabulary.language.belanda_viri=Belanda Viri controlledvocabulary.language.belarusian=Belarusian +controlledvocabulary.language.belhariya=Belhariya +controlledvocabulary.language.beli_(papua_new_guinea)=Beli (Papua New Guinea) +controlledvocabulary.language.beli_(south_sudan)=Beli (South Sudan) +controlledvocabulary.language.belize_kriol_english=Belize Kriol English +controlledvocabulary.language.bella_coola=Bella Coola +controlledvocabulary.language.bellari=Bellari +controlledvocabulary.language.belning=Belning +controlledvocabulary.language.bemba_(zambia)=Bemba (Zambia) +controlledvocabulary.language.bembe=Bembe +controlledvocabulary.language.ben_tey_dogon=Ben Tey Dogon +controlledvocabulary.language.bena_(nigeria)=Bena (Nigeria) +controlledvocabulary.language.bena_(tanzania)=Bena (Tanzania) +controlledvocabulary.language.benabena=Benabena +controlledvocabulary.language.benamanga=Benamanga +controlledvocabulary.language.bench=Bench +controlledvocabulary.language.bende=Bende +controlledvocabulary.language.bendi=Bendi +controlledvocabulary.language.beng=Beng +controlledvocabulary.language.benga=Benga controlledvocabulary.language.bengali,_bangla=Bengali, Bangla +controlledvocabulary.language.benggoi=Benggoi +controlledvocabulary.language.bengkala_sign_language=Bengkala Sign Language +controlledvocabulary.language.bentong=Bentong +controlledvocabulary.language.benyadu'=Benyadu' +controlledvocabulary.language.beothuk=Beothuk +controlledvocabulary.language.bepour=Bepour +controlledvocabulary.language.berakou=Berakou +controlledvocabulary.language.berau_malay=Berau Malay +controlledvocabulary.language.berbice_creole_dutch=Berbice Creole Dutch +controlledvocabulary.language.berik=Berik +controlledvocabulary.language.berinomo=Berinomo +controlledvocabulary.language.berom=Berom +controlledvocabulary.language.berta=Berta +controlledvocabulary.language.berti=Berti +controlledvocabulary.language.besisi=Besisi +controlledvocabulary.language.besme=Besme +controlledvocabulary.language.besoa=Besoa +controlledvocabulary.language.betaf=Betaf +controlledvocabulary.language.betawi=Betawi +controlledvocabulary.language.bete=Bete +controlledvocabulary.language.bete-bendi=Bete-Bendi +controlledvocabulary.language.beti_(cote_d'ivoire)=Beti (Côte d'Ivoire) +controlledvocabulary.language.betta_kurumba=Betta Kurumba +controlledvocabulary.language.bezhta=Bezhta +controlledvocabulary.language.bhadrawahi=Bhadrawahi +controlledvocabulary.language.bhalay=Bhalay +controlledvocabulary.language.bharia=Bharia +controlledvocabulary.language.bhatri=Bhatri +controlledvocabulary.language.bhattiyali=Bhattiyali +controlledvocabulary.language.bhaya=Bhaya +controlledvocabulary.language.bhele=Bhele +controlledvocabulary.language.bhilali=Bhilali +controlledvocabulary.language.bhili=Bhili +controlledvocabulary.language.bhojpuri=Bhojpuri +controlledvocabulary.language.bhoti_kinnauri=Bhoti Kinnauri +controlledvocabulary.language.bhujel=Bhujel +controlledvocabulary.language.bhunjia=Bhunjia +controlledvocabulary.language.biafada=Biafada +controlledvocabulary.language.biage=Biage +controlledvocabulary.language.biak=Biak +controlledvocabulary.language.biali=Biali +controlledvocabulary.language.bian_marind=Bian Marind +controlledvocabulary.language.biangai=Biangai +controlledvocabulary.language.biao=Biao +controlledvocabulary.language.biao_mon=Biao Mon +controlledvocabulary.language.biao-jiao_mien=Biao-Jiao Mien +controlledvocabulary.language.biatah_bidayuh=Biatah Bidayuh +controlledvocabulary.language.bibbulman=Bibbulman +controlledvocabulary.language.bidhawal=Bidhawal +controlledvocabulary.language.bidiyo=Bidiyo +controlledvocabulary.language.bidjara=Bidjara +controlledvocabulary.language.bidyogo=Bidyogo +controlledvocabulary.language.biem=Biem +controlledvocabulary.language.bierebo=Bierebo +controlledvocabulary.language.bieria=Bieria +controlledvocabulary.language.biete=Biete +controlledvocabulary.language.big_nambas=Big Nambas +controlledvocabulary.language.biga=Biga +controlledvocabulary.language.bigambal=Bigambal +controlledvocabulary.language.bih=Bih controlledvocabulary.language.bihari=Bihari +controlledvocabulary.language.bijim=Bijim +controlledvocabulary.language.bijori=Bijori +controlledvocabulary.language.bikol=Bikol +controlledvocabulary.language.bikya=Bikya +controlledvocabulary.language.bila=Bila +controlledvocabulary.language.bilakura=Bilakura +controlledvocabulary.language.bilaspuri=Bilaspuri +controlledvocabulary.language.bilba=Bilba +controlledvocabulary.language.bilbil=Bilbil +controlledvocabulary.language.bile=Bile +controlledvocabulary.language.bilin=Bilin +controlledvocabulary.language.bilma_kanuri=Bilma Kanuri +controlledvocabulary.language.biloxi=Biloxi +controlledvocabulary.language.bilua=Bilua +controlledvocabulary.language.bilur=Bilur +controlledvocabulary.language.bima=Bima +controlledvocabulary.language.bimin=Bimin +controlledvocabulary.language.bimoba=Bimoba +controlledvocabulary.language.bina_(nigeria)=Bina (Nigeria) +controlledvocabulary.language.bina_(papua_new_guinea)=Bina (Papua New Guinea) +controlledvocabulary.language.binahari=Binahari +controlledvocabulary.language.binandere=Binandere +controlledvocabulary.language.bindal=Bindal +controlledvocabulary.language.bine=Bine +controlledvocabulary.language.bini=Bini +controlledvocabulary.language.binji=Binji +controlledvocabulary.language.binongan_itneg=Binongan Itneg +controlledvocabulary.language.bintauna=Bintauna +controlledvocabulary.language.bintulu=Bintulu +controlledvocabulary.language.binukid=Binukid +controlledvocabulary.language.binumarien=Binumarien +controlledvocabulary.language.bipi=Bipi +controlledvocabulary.language.bira=Bira +controlledvocabulary.language.birale=Birale +controlledvocabulary.language.birao=Birao +controlledvocabulary.language.birgit=Birgit +controlledvocabulary.language.birhor=Birhor +controlledvocabulary.language.biri=Biri +controlledvocabulary.language.biritai=Biritai +controlledvocabulary.language.birked=Birked +controlledvocabulary.language.birri=Birri +controlledvocabulary.language.birrpayi=Birrpayi +controlledvocabulary.language.birwa=Birwa +controlledvocabulary.language.biseni=Biseni +controlledvocabulary.language.bishnupriya=Bishnupriya +controlledvocabulary.language.bishuo=Bishuo +controlledvocabulary.language.bisis=Bisis controlledvocabulary.language.bislama=Bislama +controlledvocabulary.language.bisorio=Bisorio +controlledvocabulary.language.bissa=Bissa +controlledvocabulary.language.bisu=Bisu +controlledvocabulary.language.bit=Bit +controlledvocabulary.language.bitare=Bitare +controlledvocabulary.language.bitur=Bitur +controlledvocabulary.language.biwat=Biwat +controlledvocabulary.language.biyo=Biyo +controlledvocabulary.language.biyom=Biyom +controlledvocabulary.language.blablanga=Blablanga +controlledvocabulary.language.blafe=Blafe +controlledvocabulary.language.blagar=Blagar +controlledvocabulary.language.blang=Blang +controlledvocabulary.language.blissymbols=Blissymbols +controlledvocabulary.language.bo_(laos)=Bo (Laos) +controlledvocabulary.language.bo_(papua_new_guinea)=Bo (Papua New Guinea) +controlledvocabulary.language.bo-rukul=Bo-Rukul +controlledvocabulary.language.bo-ung=Bo-Ung +controlledvocabulary.language.boano_(maluku)=Boano (Maluku) +controlledvocabulary.language.boano_(sulawesi)=Boano (Sulawesi) +controlledvocabulary.language.bobongko=Bobongko +controlledvocabulary.language.bobot=Bobot +controlledvocabulary.language.bodo_(central_african_republic)=Bodo (Central African Republic) +controlledvocabulary.language.bodo_(india)=Bodo (India) +controlledvocabulary.language.bodo_gadaba=Bodo Gadaba +controlledvocabulary.language.bodo_parja=Bodo Parja +controlledvocabulary.language.bofi=Bofi +controlledvocabulary.language.boga=Boga +controlledvocabulary.language.bogaya=Bogaya +controlledvocabulary.language.boghom=Boghom +controlledvocabulary.language.boguru=Boguru +controlledvocabulary.language.bohtan_neo-aramaic=Bohtan Neo-Aramaic +controlledvocabulary.language.boikin=Boikin +controlledvocabulary.language.bokha=Bokha +controlledvocabulary.language.boko_(benin)=Boko (Benin) +controlledvocabulary.language.boko_(democratic_republic_of_congo)=Boko (Democratic Republic of Congo) +controlledvocabulary.language.bokobaru=Bokobaru +controlledvocabulary.language.bokoto=Bokoto +controlledvocabulary.language.bokyi=Bokyi +controlledvocabulary.language.bola=Bola +controlledvocabulary.language.bolango=Bolango +controlledvocabulary.language.bole=Bole +controlledvocabulary.language.bolgarian=Bolgarian +controlledvocabulary.language.bolgo=Bolgo +controlledvocabulary.language.bolia=Bolia +controlledvocabulary.language.bolinao=Bolinao +controlledvocabulary.language.bolivian_sign_language=Bolivian Sign Language +controlledvocabulary.language.boloki=Boloki +controlledvocabulary.language.bolon=Bolon +controlledvocabulary.language.bolondo=Bolondo +controlledvocabulary.language.bolongan=Bolongan +controlledvocabulary.language.bolyu=Bolyu +controlledvocabulary.language.bom-kim=Bom-Kim +controlledvocabulary.language.boma=Boma +controlledvocabulary.language.bomboli=Bomboli +controlledvocabulary.language.bomboma=Bomboma +controlledvocabulary.language.bomitaba=Bomitaba +controlledvocabulary.language.bomu=Bomu +controlledvocabulary.language.bomwali=Bomwali +controlledvocabulary.language.bon_gula=Bon Gula +controlledvocabulary.language.bonan=Bonan +controlledvocabulary.language.bondei=Bondei +controlledvocabulary.language.bondo=Bondo +controlledvocabulary.language.bondoukou_kulango=Bondoukou Kulango +controlledvocabulary.language.bondum_dom_dogon=Bondum Dom Dogon +controlledvocabulary.language.bonerate=Bonerate +controlledvocabulary.language.bonerif=Bonerif +controlledvocabulary.language.bonggi=Bonggi +controlledvocabulary.language.bonggo=Bonggo +controlledvocabulary.language.bongili=Bongili +controlledvocabulary.language.bongo=Bongo +controlledvocabulary.language.bongu=Bongu +controlledvocabulary.language.bonjo=Bonjo +controlledvocabulary.language.bonkeng=Bonkeng +controlledvocabulary.language.bonkiman=Bonkiman +controlledvocabulary.language.bontok=Bontok +controlledvocabulary.language.bookan=Bookan +controlledvocabulary.language.boon=Boon +controlledvocabulary.language.boor=Boor +controlledvocabulary.language.bora=Bora +controlledvocabulary.language.borana-arsi-guji_oromo=Borana-Arsi-Guji Oromo +controlledvocabulary.language.border_kuna=Border Kuna +controlledvocabulary.language.borei=Borei +controlledvocabulary.language.borgu_fulfulde=Borgu Fulfulde +controlledvocabulary.language.boro_(ethiopia)=Boro (Ethiopia) +controlledvocabulary.language.boro_(ghana)=Boro (Ghana) +controlledvocabulary.language.borong=Borong +controlledvocabulary.language.boruca=Boruca +controlledvocabulary.language.bororo=Borôro +controlledvocabulary.language.boselewa=Boselewa +controlledvocabulary.language.bosngun=Bosngun controlledvocabulary.language.bosnian=Bosnian +controlledvocabulary.language.bote-majhi=Bote-Majhi +controlledvocabulary.language.botlikh=Botlikh +controlledvocabulary.language.botolan_sambal=Botolan Sambal +controlledvocabulary.language.bouna_kulango=Bouna Kulango +controlledvocabulary.language.bouni=Bouni +controlledvocabulary.language.bouyei=Bouyei +controlledvocabulary.language.bozaba=Bozaba +controlledvocabulary.language.bragat=Bragat +controlledvocabulary.language.brahui=Brahui +controlledvocabulary.language.braj=Braj +controlledvocabulary.language.brao=Brao +controlledvocabulary.language.brazilian_sign_language=Brazilian Sign Language +controlledvocabulary.language.brem=Brem +controlledvocabulary.language.breri=Breri controlledvocabulary.language.breton=Breton +controlledvocabulary.language.bribri=Bribri +controlledvocabulary.language.bribri_sign_language=Bribri Sign Language +controlledvocabulary.language.brithenig=Brithenig +controlledvocabulary.language.british_sign_language=British Sign Language +controlledvocabulary.language.brokkat=Brokkat +controlledvocabulary.language.brokpake=Brokpake +controlledvocabulary.language.brokskat=Brokskat +controlledvocabulary.language.brooke's_point_palawano=Brooke's Point Palawano +controlledvocabulary.language.broome_pearling_lugger_pidgin=Broome Pearling Lugger Pidgin +controlledvocabulary.language.brunca_sign_language=Brunca Sign Language +controlledvocabulary.language.brunei=Brunei +controlledvocabulary.language.brunei_bisaya=Brunei Bisaya +controlledvocabulary.language.bruny_island_tasmanian=Bruny Island Tasmanian +controlledvocabulary.language.bu_(bauchi_state)=Bu (Bauchi State) +controlledvocabulary.language.bu_(kaduna_state)=Bu (Kaduna State) +controlledvocabulary.language.bu-nao_bunu=Bu-Nao Bunu +controlledvocabulary.language.bua=Bua +controlledvocabulary.language.bualkhaw_chin=Bualkhaw Chin +controlledvocabulary.language.buamu=Buamu +controlledvocabulary.language.bube=Bube +controlledvocabulary.language.bubi=Bubi +controlledvocabulary.language.bubia=Bubia +controlledvocabulary.language.budeh_stieng=Budeh Stieng +controlledvocabulary.language.budibud=Budibud +controlledvocabulary.language.budong-budong=Budong-Budong +controlledvocabulary.language.budu=Budu +controlledvocabulary.language.budukh=Budukh +controlledvocabulary.language.buduma=Buduma +controlledvocabulary.language.budza=Budza +controlledvocabulary.language.bugan=Bugan +controlledvocabulary.language.bugawac=Bugawac +controlledvocabulary.language.bughotu=Bughotu +controlledvocabulary.language.buginese=Buginese +controlledvocabulary.language.buglere=Buglere +controlledvocabulary.language.bugun=Bugun +controlledvocabulary.language.buhi'non_bikol=Buhi'non Bikol +controlledvocabulary.language.buhid=Buhid +controlledvocabulary.language.buhutu=Buhutu +controlledvocabulary.language.bukar-sadung_bidayuh=Bukar-Sadung Bidayuh +controlledvocabulary.language.bukat=Bukat +controlledvocabulary.language.bukharic=Bukharic +controlledvocabulary.language.bukit_malay=Bukit Malay +controlledvocabulary.language.bukitan=Bukitan +controlledvocabulary.language.bukiyip=Bukiyip +controlledvocabulary.language.buksa=Buksa +controlledvocabulary.language.bukusu=Bukusu +controlledvocabulary.language.bukwen=Bukwen controlledvocabulary.language.bulgarian=Bulgarian +controlledvocabulary.language.bulgarian_sign_language=Bulgarian Sign Language +controlledvocabulary.language.bulgebi=Bulgebi +controlledvocabulary.language.buli=Buli +controlledvocabulary.language.buli_(ghana)=Buli (Ghana) +controlledvocabulary.language.buli_(indonesia)=Buli (Indonesia) +controlledvocabulary.language.bullom_so=Bullom So +controlledvocabulary.language.bulo_stieng=Bulo Stieng +controlledvocabulary.language.bulu_(cameroon)=Bulu (Cameroon) +controlledvocabulary.language.bulu_(papua_new_guinea)=Bulu (Papua New Guinea) +controlledvocabulary.language.bum=Bum +controlledvocabulary.language.bumaji=Bumaji +controlledvocabulary.language.bumang=Bumang +controlledvocabulary.language.bumbita_arapesh=Bumbita Arapesh +controlledvocabulary.language.bumthangkha=Bumthangkha +controlledvocabulary.language.bun=Bun +controlledvocabulary.language.buna=Buna +controlledvocabulary.language.bunak=Bunak +controlledvocabulary.language.bunama=Bunama +controlledvocabulary.language.bundeli=Bundeli +controlledvocabulary.language.bung=Bung +controlledvocabulary.language.bungain=Bungain +controlledvocabulary.language.bunganditj=Bunganditj +controlledvocabulary.language.bungku=Bungku +controlledvocabulary.language.bungu=Bungu +controlledvocabulary.language.bunoge_dogon=Bunoge Dogon +controlledvocabulary.language.bunuba=Bunuba +controlledvocabulary.language.bunun=Bunun +controlledvocabulary.language.buol=Buol +controlledvocabulary.language.bura-pabir=Bura-Pabir +controlledvocabulary.language.burak=Burak +controlledvocabulary.language.buraka=Buraka +controlledvocabulary.language.burarra=Burarra +controlledvocabulary.language.burate=Burate +controlledvocabulary.language.burduna=Burduna +controlledvocabulary.language.bure=Bure +controlledvocabulary.language.buriat=Buriat +controlledvocabulary.language.burji=Burji +controlledvocabulary.language.burmbar=Burmbar controlledvocabulary.language.burmese=Burmese -controlledvocabulary.language.catalan,valencian=Catalan,Valencian +controlledvocabulary.language.burmeso=Burmeso +controlledvocabulary.language.buru_(indonesia)=Buru (Indonesia) +controlledvocabulary.language.buru_(nigeria)=Buru (Nigeria) +controlledvocabulary.language.burui=Burui +controlledvocabulary.language.burumakok=Burumakok +controlledvocabulary.language.burun=Burun +controlledvocabulary.language.burundian_sign_language=Burundian Sign Language +controlledvocabulary.language.burunge=Burunge +controlledvocabulary.language.burushaski=Burushaski +controlledvocabulary.language.burusu=Burusu +controlledvocabulary.language.buruwai=Buruwai +controlledvocabulary.language.busa=Busa +controlledvocabulary.language.busam=Busam +controlledvocabulary.language.busami=Busami +controlledvocabulary.language.busang_kayan=Busang Kayan +controlledvocabulary.language.bushi=Bushi +controlledvocabulary.language.bushoong=Bushoong +controlledvocabulary.language.buso=Buso +controlledvocabulary.language.busoa=Busoa +controlledvocabulary.language.bussa=Bussa +controlledvocabulary.language.busuu=Busuu +controlledvocabulary.language.butbut_kalinga=Butbut Kalinga +controlledvocabulary.language.butmas-tur=Butmas-Tur +controlledvocabulary.language.butuanon=Butuanon +controlledvocabulary.language.buwal=Buwal +controlledvocabulary.language.buyu=Buyu +controlledvocabulary.language.buyuan_jinuo=Buyuan Jinuo +controlledvocabulary.language.bwa=Bwa +controlledvocabulary.language.bwaidoka=Bwaidoka +controlledvocabulary.language.bwanabwana=Bwanabwana +controlledvocabulary.language.bwatoo=Bwatoo +controlledvocabulary.language.bwe_karen=Bwe Karen +controlledvocabulary.language.bwela=Bwela +controlledvocabulary.language.bwile=Bwile +controlledvocabulary.language.bwisi=Bwisi +controlledvocabulary.language.byangsi=Byangsi +controlledvocabulary.language.byep=Byep +controlledvocabulary.language.badi_kanum=Bädi Kanum +controlledvocabulary.language.c'lela=C'Lela +controlledvocabulary.language.caac=Caac +controlledvocabulary.language.cabiyari=Cabiyarí +controlledvocabulary.language.cabecar=Cabécar +controlledvocabulary.language.cacaloxtepec_mixtec=Cacaloxtepec Mixtec +controlledvocabulary.language.cacaopera=Cacaopera +controlledvocabulary.language.cacgia_roglai=Cacgia Roglai +controlledvocabulary.language.cacua=Cacua +controlledvocabulary.language.caddo=Caddo +controlledvocabulary.language.cafundo_creole=Cafundo Creole +controlledvocabulary.language.cahuarano=Cahuarano +controlledvocabulary.language.cahuilla=Cahuilla +controlledvocabulary.language.cajamarca_quechua=Cajamarca Quechua +controlledvocabulary.language.cajatambo_north_lima_quechua=Cajatambo North Lima Quechua +controlledvocabulary.language.cajonos_zapotec=Cajonos Zapotec +controlledvocabulary.language.cajun_french=Cajun French +controlledvocabulary.language.caka=Caka +controlledvocabulary.language.cakchiquel-quiche_mixed_language=Cakchiquel-Quiché Mixed Language +controlledvocabulary.language.cakfem-mushere=Cakfem-Mushere +controlledvocabulary.language.calamian_tagbanwa=Calamian Tagbanwa +controlledvocabulary.language.calderon_highland_quichua=Calderón Highland Quichua +controlledvocabulary.language.callawalla=Callawalla +controlledvocabulary.language.caluyanun=Caluyanun +controlledvocabulary.language.calo=Caló +controlledvocabulary.language.cambodian_sign_language=Cambodian Sign Language +controlledvocabulary.language.cameroon_mambila=Cameroon Mambila +controlledvocabulary.language.cameroon_pidgin=Cameroon Pidgin +controlledvocabulary.language.camling=Camling +controlledvocabulary.language.campalagian=Campalagian +controlledvocabulary.language.campidanese_sardinian=Campidanese Sardinian +controlledvocabulary.language.camsa=Camsá +controlledvocabulary.language.camtho=Camtho +controlledvocabulary.language.camunic=Camunic +controlledvocabulary.language.candoshi-shapra=Candoshi-Shapra +controlledvocabulary.language.canela=Canela +controlledvocabulary.language.canichana=Canichana +controlledvocabulary.language.cao_lan=Cao Lan +controlledvocabulary.language.cao_miao=Cao Miao +controlledvocabulary.language.capanahua=Capanahua +controlledvocabulary.language.capiznon=Capiznon +controlledvocabulary.language.cappadocian_greek=Cappadocian Greek +controlledvocabulary.language.caquinte=Caquinte +controlledvocabulary.language.car_nicobarese=Car Nicobarese +controlledvocabulary.language.cara=Cara +controlledvocabulary.language.carabayo=Carabayo +controlledvocabulary.language.caramanta=Caramanta +controlledvocabulary.language.carapana=Carapana +controlledvocabulary.language.carian=Carian +controlledvocabulary.language.caribbean_hindustani=Caribbean Hindustani +controlledvocabulary.language.caribbean_javanese=Caribbean Javanese +controlledvocabulary.language.carijona=Carijona +controlledvocabulary.language.carolina_algonquian=Carolina Algonquian +controlledvocabulary.language.carolinian=Carolinian +controlledvocabulary.language.carpathian_romani=Carpathian Romani +controlledvocabulary.language.carrier=Carrier +controlledvocabulary.language.cashibo-cacataibo=Cashibo-Cacataibo +controlledvocabulary.language.cashinahua=Cashinahua +controlledvocabulary.language.casiguran_dumagat_agta=Casiguran Dumagat Agta +controlledvocabulary.language.spanish,_castilian=Castilian, Spanish +controlledvocabulary.language.casuarina_coast_asmat=Casuarina Coast Asmat +controlledvocabulary.language.catalan_sign_language=Catalan Sign Language +controlledvocabulary.language.catalan,_valencian=Catalan, Valencian +controlledvocabulary.language.catawba=Catawba +controlledvocabulary.language.cavinena=Cavineña +controlledvocabulary.language.cayubaba=Cayubaba +controlledvocabulary.language.cayuga=Cayuga +controlledvocabulary.language.cayuse=Cayuse +controlledvocabulary.language.canar_highland_quichua=Cañar Highland Quichua +controlledvocabulary.language.cahungwarya=Ca̱hungwa̱rya̱ +controlledvocabulary.language.cebaara_senoufo=Cebaara Senoufo +controlledvocabulary.language.cebuano=Cebuano +controlledvocabulary.language.celtiberian=Celtiberian +controlledvocabulary.language.cemuhi=Cemuhî +controlledvocabulary.language.cen=Cen +controlledvocabulary.language.central_asmat=Central Asmat +controlledvocabulary.language.central_atlas_tamazight=Central Atlas Tamazight +controlledvocabulary.language.central_awyu=Central Awyu +controlledvocabulary.language.central_aymara=Central Aymara +controlledvocabulary.language.central_bai=Central Bai +controlledvocabulary.language.central_berawan=Central Berawan +controlledvocabulary.language.central_bikol=Central Bikol +controlledvocabulary.language.central_bontok=Central Bontok +controlledvocabulary.language.central_cagayan_agta=Central Cagayan Agta +controlledvocabulary.language.central_grebo=Central Grebo +controlledvocabulary.language.central_hongshuihe_zhuang=Central Hongshuihe Zhuang +controlledvocabulary.language.central_huasteca_nahuatl=Central Huasteca Nahuatl +controlledvocabulary.language.central_huishui_hmong=Central Huishui Hmong +controlledvocabulary.language.central_kanuri=Central Kanuri +controlledvocabulary.language.central_kurdish=Central Kurdish +controlledvocabulary.language.central_maewo=Central Maewo +controlledvocabulary.language.central_malay=Central Malay +controlledvocabulary.language.central_masela=Central Masela +controlledvocabulary.language.central_mashan_hmong=Central Mashan Hmong +controlledvocabulary.language.central_mazahua=Central Mazahua +controlledvocabulary.language.central_melanau=Central Melanau +controlledvocabulary.language.central_mnong=Central Mnong +controlledvocabulary.language.central_nahuatl=Central Nahuatl +controlledvocabulary.language.central_nicobarese=Central Nicobarese +controlledvocabulary.language.central_ojibwa=Central Ojibwa +controlledvocabulary.language.central_okinawan=Central Okinawan +controlledvocabulary.language.central_palawano=Central Palawano +controlledvocabulary.language.central_pame=Central Pame +controlledvocabulary.language.central_pashto=Central Pashto +controlledvocabulary.language.central_pomo=Central Pomo +controlledvocabulary.language.central_puebla_nahuatl=Central Puebla Nahuatl +controlledvocabulary.language.central_sama=Central Sama +controlledvocabulary.language.central_siberian_yupik=Central Siberian Yupik +controlledvocabulary.language.central_sierra_miwok=Central Sierra Miwok +controlledvocabulary.language.central_subanen=Central Subanen +controlledvocabulary.language.central_tagbanwa=Central Tagbanwa +controlledvocabulary.language.central_tarahumara=Central Tarahumara +controlledvocabulary.language.central_tunebo=Central Tunebo +controlledvocabulary.language.central_yupik=Central Yupik +controlledvocabulary.language.central-eastern_niger_fulfulde=Central-Eastern Niger Fulfulde +controlledvocabulary.language.centuum=Centúúm +controlledvocabulary.language.cerma=Cerma +controlledvocabulary.language.cha'ari=Cha'ari +controlledvocabulary.language.chabu=Chabu +controlledvocabulary.language.chachapoyas_quechua=Chachapoyas Quechua +controlledvocabulary.language.chachi=Chachi +controlledvocabulary.language.chadian_arabic=Chadian Arabic +controlledvocabulary.language.chadian_sign_language=Chadian Sign Language +controlledvocabulary.language.chadong=Chadong +controlledvocabulary.language.chagatai=Chagatai +controlledvocabulary.language.chaima=Chaima +controlledvocabulary.language.chak=Chak +controlledvocabulary.language.chakali=Chakali +controlledvocabulary.language.chakavian=Chakavian +controlledvocabulary.language.chakma=Chakma +controlledvocabulary.language.chala=Chala +controlledvocabulary.language.chaldean_neo-aramaic=Chaldean Neo-Aramaic +controlledvocabulary.language.chalikha=Chalikha +controlledvocabulary.language.chamacoco=Chamacoco +controlledvocabulary.language.chamalal=Chamalal +controlledvocabulary.language.chambeali=Chambeali +controlledvocabulary.language.chambri=Chambri +controlledvocabulary.language.chamicuro=Chamicuro controlledvocabulary.language.chamorro=Chamorro +controlledvocabulary.language.chang_naga=Chang Naga +controlledvocabulary.language.changriwa=Changriwa +controlledvocabulary.language.changthang=Changthang +controlledvocabulary.language.chantyal=Chantyal +controlledvocabulary.language.chane=Chané +controlledvocabulary.language.chara=Chara +controlledvocabulary.language.chaudangsi=Chaudangsi +controlledvocabulary.language.chaura=Chaura +controlledvocabulary.language.chavacano=Chavacano +controlledvocabulary.language.chayahuita=Chayahuita +controlledvocabulary.language.chayuco_mixtec=Chayuco Mixtec +controlledvocabulary.language.chazumba_mixtec=Chazumba Mixtec +controlledvocabulary.language.che=Che controlledvocabulary.language.chechen=Chechen +controlledvocabulary.language.cheke_holo=Cheke Holo +controlledvocabulary.language.chemakum=Chemakum +controlledvocabulary.language.chenapian=Chenapian +controlledvocabulary.language.chenchu=Chenchu +controlledvocabulary.language.chenoua=Chenoua +controlledvocabulary.language.chepang=Chepang +controlledvocabulary.language.chepya=Chepya +controlledvocabulary.language.cherepon=Cherepon +controlledvocabulary.language.cherokee=Cherokee +controlledvocabulary.language.chesu=Chesu +controlledvocabulary.language.chetco=Chetco +controlledvocabulary.language.chewong=Chewong +controlledvocabulary.language.cheyenne=Cheyenne +controlledvocabulary.language.chhattisgarhi=Chhattisgarhi +controlledvocabulary.language.chhintange=Chhintange +controlledvocabulary.language.chhulung=Chhulung +controlledvocabulary.language.chiangmai_sign_language=Chiangmai Sign Language +controlledvocabulary.language.chiapanec=Chiapanec +controlledvocabulary.language.chibcha=Chibcha +controlledvocabulary.language.chicahuaxtla_triqui=Chicahuaxtla Triqui controlledvocabulary.language.chichewa,_chewa,_nyanja=Chichewa, Chewa, Nyanja +controlledvocabulary.language.chichicapan_zapotec=Chichicapan Zapotec +controlledvocabulary.language.chichimeca-jonaz=Chichimeca-Jonaz +controlledvocabulary.language.chickasaw=Chickasaw +controlledvocabulary.language.chicomuceltec=Chicomuceltec +controlledvocabulary.language.chiga=Chiga +controlledvocabulary.language.chigmecatitlan_mixtec=Chigmecatitlán Mixtec +controlledvocabulary.language.chilcotin=Chilcotin +controlledvocabulary.language.chilean_sign_language=Chilean Sign Language +controlledvocabulary.language.chilisso=Chilisso +controlledvocabulary.language.chiltepec_chinantec=Chiltepec Chinantec +controlledvocabulary.language.chimalapa_zoque=Chimalapa Zoque +controlledvocabulary.language.chimariko=Chimariko +controlledvocabulary.language.chimborazo_highland_quichua=Chimborazo Highland Quichua +controlledvocabulary.language.chimila=Chimila +controlledvocabulary.language.china_buriat=China Buriat +controlledvocabulary.language.chinali=Chinali +controlledvocabulary.language.chinbon_chin=Chinbon Chin +controlledvocabulary.language.chincha_quechua=Chincha Quechua controlledvocabulary.language.chinese=Chinese +controlledvocabulary.language.chinese_pidgin_english=Chinese Pidgin English +controlledvocabulary.language.chinese_sign_language=Chinese Sign Language +controlledvocabulary.language.chinook=Chinook +controlledvocabulary.language.chinook_jargon=Chinook jargon +controlledvocabulary.language.chipaya=Chipaya +controlledvocabulary.language.chipewyan=Chipewyan +controlledvocabulary.language.chippewa=Chippewa +controlledvocabulary.language.chiquihuitlan_mazatec=Chiquihuitlán Mazatec +controlledvocabulary.language.chiquitano=Chiquitano +controlledvocabulary.language.chiquian_ancash_quechua=Chiquián Ancash Quechua +controlledvocabulary.language.chiripa=Chiripá +controlledvocabulary.language.chiru=Chiru +controlledvocabulary.language.chitimacha=Chitimacha +controlledvocabulary.language.chitkuli_kinnauri=Chitkuli Kinnauri +controlledvocabulary.language.chittagonian=Chittagonian +controlledvocabulary.language.chitwania_tharu=Chitwania Tharu +controlledvocabulary.language.choapan_zapotec=Choapan Zapotec +controlledvocabulary.language.chocangacakha=Chocangacakha +controlledvocabulary.language.chochotec=Chochotec +controlledvocabulary.language.choctaw=Choctaw +controlledvocabulary.language.chodri=Chodri +controlledvocabulary.language.chokri_naga=Chokri Naga +controlledvocabulary.language.chokwe=Chokwe +controlledvocabulary.language.chol=Chol +controlledvocabulary.language.cholon=Cholón +controlledvocabulary.language.chong=Chong +controlledvocabulary.language.choni=Choni +controlledvocabulary.language.chonyi-dzihana-kauma=Chonyi-Dzihana-Kauma +controlledvocabulary.language.chopi=Chopi +controlledvocabulary.language.chorasmian=Chorasmian +controlledvocabulary.language.chorti=Chortí +controlledvocabulary.language.chothe_naga=Chothe Naga +controlledvocabulary.language.chrau=Chrau +controlledvocabulary.language.chru=Chru +controlledvocabulary.language.chuanqiandian_cluster_miao=Chuanqiandian Cluster Miao +controlledvocabulary.language.chuave=Chuave +controlledvocabulary.language.chug=Chug +controlledvocabulary.language.chuj=Chuj +controlledvocabulary.language.chuka=Chuka +controlledvocabulary.language.chukot=Chukot +controlledvocabulary.language.chukwa=Chukwa +controlledvocabulary.language.chulym=Chulym +controlledvocabulary.language.chumburung=Chumburung +controlledvocabulary.language.chung=Chung +controlledvocabulary.language.churahi=Churahi +controlledvocabulary.language.old_church_slavonic=Church Slavonic, Church Slavic, Old Church Slavonic, Old Bulgarian +controlledvocabulary.language.chut=Chut +controlledvocabulary.language.chuukese=Chuukese +controlledvocabulary.language.chuvantsy=Chuvantsy controlledvocabulary.language.chuvash=Chuvash +controlledvocabulary.language.chuwabu=Chuwabu +controlledvocabulary.language.chacobo=Chácobo +controlledvocabulary.language.ci_gbe=Ci Gbe +controlledvocabulary.language.cia-cia=Cia-Cia +controlledvocabulary.language.cibak=Cibak +controlledvocabulary.language.cicipu=Cicipu +controlledvocabulary.language.cimbrian=Cimbrian +controlledvocabulary.language.cinda-regi-tiyal=Cinda-Regi-Tiyal +controlledvocabulary.language.cineni=Cineni +controlledvocabulary.language.cinta_larga=Cinta Larga +controlledvocabulary.language.cisalpine_gaulish=Cisalpine Gaulish +controlledvocabulary.language.cishingini=Cishingini +controlledvocabulary.language.citak=Citak +controlledvocabulary.language.ciwogai=Ciwogai +controlledvocabulary.language.clallam=Clallam +controlledvocabulary.language.classical_armenian=Classical Armenian +controlledvocabulary.language.classical_mandaic=Classical Mandaic +controlledvocabulary.language.classical_mongolian=Classical Mongolian +controlledvocabulary.language.classical_nahuatl=Classical Nahuatl +controlledvocabulary.language.classical_newari=Classical Newari +controlledvocabulary.language.classical_quechua=Classical Quechua +controlledvocabulary.language.classical_sanskrit=Classical Sanskrit +controlledvocabulary.language.classical_syriac=Classical Syriac +controlledvocabulary.language.classical_tibetan=Classical Tibetan +controlledvocabulary.language.coahuilteco=Coahuilteco +controlledvocabulary.language.coast_miwok=Coast Miwok +controlledvocabulary.language.coastal_konjo=Coastal Konjo +controlledvocabulary.language.coatecas_altas_zapotec=Coatecas Altas Zapotec +controlledvocabulary.language.coatepec_nahuatl=Coatepec Nahuatl +controlledvocabulary.language.coatlan_mixe=Coatlán Mixe +controlledvocabulary.language.coatlan_zapotec=Coatlán Zapotec +controlledvocabulary.language.coatzospan_mixtec=Coatzospan Mixtec +controlledvocabulary.language.cocama-cocamilla=Cocama-Cocamilla +controlledvocabulary.language.cochimi=Cochimi +controlledvocabulary.language.cocopa=Cocopa +controlledvocabulary.language.cocos_islands_malay=Cocos Islands Malay +controlledvocabulary.language.coeur_d'alene=Coeur d'Alene +controlledvocabulary.language.cofan=Cofán +controlledvocabulary.language.cogui=Cogui +controlledvocabulary.language.col=Col +controlledvocabulary.language.colombian_sign_language=Colombian Sign Language +controlledvocabulary.language.colonia_tovar_german=Colonia Tovar German +controlledvocabulary.language.colorado=Colorado +controlledvocabulary.language.columbia-wenatchi=Columbia-Wenatchi +controlledvocabulary.language.comaltepec_chinantec=Comaltepec Chinantec +controlledvocabulary.language.comanche=Comanche +controlledvocabulary.language.comecrudo=Comecrudo +controlledvocabulary.language.como_karim=Como Karim +controlledvocabulary.language.comox=Comox +controlledvocabulary.language.con=Con +controlledvocabulary.language.congo_swahili=Congo Swahili +controlledvocabulary.language.coos=Coos +controlledvocabulary.language.copainala_zoque=Copainalá Zoque +controlledvocabulary.language.copala_triqui=Copala Triqui +controlledvocabulary.language.coptic=Coptic +controlledvocabulary.language.coquille=Coquille +controlledvocabulary.language.cori=Cori controlledvocabulary.language.cornish=Cornish +controlledvocabulary.language.corongo_ancash_quechua=Corongo Ancash Quechua controlledvocabulary.language.corsican=Corsican +controlledvocabulary.language.costa_rican_sign_language=Costa Rican Sign Language +controlledvocabulary.language.cotabato_manobo=Cotabato Manobo +controlledvocabulary.language.cotoname=Cotoname +controlledvocabulary.language.cowlitz=Cowlitz +controlledvocabulary.language.coyotepec_popoloca=Coyotepec Popoloca +controlledvocabulary.language.coyutla_totonac=Coyutla Totonac controlledvocabulary.language.cree=Cree -controlledvocabulary.language.croatian=Croatian +controlledvocabulary.language.creek=Creek +controlledvocabulary.language.crimean_tatar=Crimean Tatar +controlledvocabulary.language.croatia_sign_language=Croatia Sign Language +controlledvocabulary.language.cross_river_mbembe=Cross River Mbembe +controlledvocabulary.language.crow=Crow +controlledvocabulary.language.cruzeno=Cruzeño +controlledvocabulary.language.cua=Cua +controlledvocabulary.language.cuba_sign_language=Cuba Sign Language +controlledvocabulary.language.cubeo=Cubeo +controlledvocabulary.language.cuiba=Cuiba +controlledvocabulary.language.cuitlatec=Cuitlatec +controlledvocabulary.language.culina=Culina +controlledvocabulary.language.cumanagoto=Cumanagoto +controlledvocabulary.language.cumbric=Cumbric +controlledvocabulary.language.cun=Cun +controlledvocabulary.language.cuneiform_luwian=Cuneiform Luwian +controlledvocabulary.language.cupeno=Cupeño +controlledvocabulary.language.curonian=Curonian +controlledvocabulary.language.curripaco=Curripaco +controlledvocabulary.language.cusco_quechua=Cusco Quechua +controlledvocabulary.language.cutchi-swahili=Cutchi-Swahili +controlledvocabulary.language.cuvok=Cuvok +controlledvocabulary.language.cuyamecalco_mixtec=Cuyamecalco Mixtec +controlledvocabulary.language.cuyonon=Cuyonon +controlledvocabulary.language.cwi_bwamu=Cwi Bwamu +controlledvocabulary.language.cypriot_arabic=Cypriot Arabic controlledvocabulary.language.czech=Czech +controlledvocabulary.language.czech_sign_language=Czech Sign Language +controlledvocabulary.language.coong=Côông +controlledvocabulary.language.da'a_kaili=Da'a Kaili +controlledvocabulary.language.daai_chin=Daai Chin +controlledvocabulary.language.daakaka=Daakaka +controlledvocabulary.language.daantanai'=Daantanai' +controlledvocabulary.language.daasanach=Daasanach +controlledvocabulary.language.daatsʼiin=Daatsʼíin +controlledvocabulary.language.daba=Daba +controlledvocabulary.language.dabarre=Dabarre +controlledvocabulary.language.dabe=Dabe +controlledvocabulary.language.dacian=Dacian +controlledvocabulary.language.dadi_dadi=Dadi Dadi +controlledvocabulary.language.dadibi=Dadibi +controlledvocabulary.language.dadiya=Dadiya +controlledvocabulary.language.daga=Daga +controlledvocabulary.language.dagaari_dioula=Dagaari Dioula +controlledvocabulary.language.dagba=Dagba +controlledvocabulary.language.dagbani=Dagbani +controlledvocabulary.language.dagik=Dagik +controlledvocabulary.language.dagoman=Dagoman +controlledvocabulary.language.dahalik=Dahalik +controlledvocabulary.language.dahalo=Dahalo +controlledvocabulary.language.daho-doo=Daho-Doo +controlledvocabulary.language.dai=Dai +controlledvocabulary.language.dai_zhuang=Dai Zhuang +controlledvocabulary.language.dair=Dair +controlledvocabulary.language.dakka=Dakka +controlledvocabulary.language.dakota=Dakota +controlledvocabulary.language.dakpakha=Dakpakha +controlledvocabulary.language.dalabon=Dalabon +controlledvocabulary.language.dalmatian=Dalmatian +controlledvocabulary.language.daloa_bete=Daloa Bété +controlledvocabulary.language.dama=Dama +controlledvocabulary.language.damakawa=Damakawa +controlledvocabulary.language.damal=Damal +controlledvocabulary.language.dambi=Dambi +controlledvocabulary.language.dameli=Dameli +controlledvocabulary.language.dampelas=Dampelas +controlledvocabulary.language.dan=Dan +controlledvocabulary.language.danaru=Danaru +controlledvocabulary.language.danau=Danau +controlledvocabulary.language.dandami_maria=Dandami Maria +controlledvocabulary.language.dangaleat=Dangaléat +controlledvocabulary.language.dangaura_tharu=Dangaura Tharu controlledvocabulary.language.danish=Danish -controlledvocabulary.language.divehi,_dhivehi,_maldivian=Divehi, Dhivehi, Maldivian +controlledvocabulary.language.danish_sign_language=Danish Sign Language +controlledvocabulary.language.dano=Dano +controlledvocabulary.language.danu=Danu +controlledvocabulary.language.dao=Dao +controlledvocabulary.language.daonda=Daonda +controlledvocabulary.language.dar_daju_daju=Dar Daju Daju +controlledvocabulary.language.dar_fur_daju=Dar Fur Daju +controlledvocabulary.language.dar_sila_daju=Dar Sila Daju +controlledvocabulary.language.darai=Darai +controlledvocabulary.language.dargwa=Dargwa +controlledvocabulary.language.dari=Dari +controlledvocabulary.language.darkinyung=Darkinyung +controlledvocabulary.language.darlong=Darlong +controlledvocabulary.language.darmiya=Darmiya +controlledvocabulary.language.daro-matu_melanau=Daro-Matu Melanau +controlledvocabulary.language.dass=Dass +controlledvocabulary.language.datooga=Datooga +controlledvocabulary.language.daungwurrung=Daungwurrung +controlledvocabulary.language.daur=Daur +controlledvocabulary.language.davawenyo=Davawenyo +controlledvocabulary.language.dawawa=Dawawa +controlledvocabulary.language.dawera-daweloor=Dawera-Daweloor +controlledvocabulary.language.dawik_kui=Dawik Kui +controlledvocabulary.language.dawro=Dawro +controlledvocabulary.language.day=Day +controlledvocabulary.language.dayi=Dayi +controlledvocabulary.language.daza=Daza +controlledvocabulary.language.dazaga=Dazaga +controlledvocabulary.language.deccan=Deccan +controlledvocabulary.language.dedua=Dedua +controlledvocabulary.language.defaka=Defaka +controlledvocabulary.language.defi_gbe=Defi Gbe +controlledvocabulary.language.deg=Deg +controlledvocabulary.language.degema=Degema +controlledvocabulary.language.degenan=Degenan +controlledvocabulary.language.degexit'an=Degexit'an +controlledvocabulary.language.dehu=Dehu +controlledvocabulary.language.dehwari=Dehwari +controlledvocabulary.language.dek=Dek +controlledvocabulary.language.dela-oenale=Dela-Oenale +controlledvocabulary.language.delaware=Delaware +controlledvocabulary.language.delo=Delo +controlledvocabulary.language.dem=Dem +controlledvocabulary.language.dema=Dema +controlledvocabulary.language.demisa=Demisa +controlledvocabulary.language.demta=Demta +controlledvocabulary.language.dendi_(benin)=Dendi (Benin) +controlledvocabulary.language.dendi_(central_african_republic)=Dendi (Central African Republic) +controlledvocabulary.language.dengese=Dengese +controlledvocabulary.language.dengka=Dengka +controlledvocabulary.language.deno=Deno +controlledvocabulary.language.denya=Denya +controlledvocabulary.language.deni=Dení +controlledvocabulary.language.deori=Deori +controlledvocabulary.language.dera_(indonesia)=Dera (Indonesia) +controlledvocabulary.language.dera_(nigeria)=Dera (Nigeria) +controlledvocabulary.language.desano=Desano +controlledvocabulary.language.desiya=Desiya +controlledvocabulary.language.dewas_rai=Dewas Rai +controlledvocabulary.language.dewoin=Dewoin +controlledvocabulary.language.dezfuli=Dezfuli +controlledvocabulary.language.dghwede=Dghwede +controlledvocabulary.language.dhaiso=Dhaiso +controlledvocabulary.language.dhalandji=Dhalandji +controlledvocabulary.language.dhangu-djangu=Dhangu-Djangu +controlledvocabulary.language.dhanki=Dhanki +controlledvocabulary.language.dhanwar_(nepal)=Dhanwar (Nepal) +controlledvocabulary.language.dhao=Dhao +controlledvocabulary.language.dharawal=Dharawal +controlledvocabulary.language.dhargari=Dhargari +controlledvocabulary.language.dharuk=Dharuk +controlledvocabulary.language.dharumbal=Dharumbal +controlledvocabulary.language.dhatki=Dhatki +controlledvocabulary.language.dhimal=Dhimal +controlledvocabulary.language.dhodia=Dhodia +controlledvocabulary.language.dhofari_arabic=Dhofari Arabic +controlledvocabulary.language.dhudhuroa=Dhudhuroa +controlledvocabulary.language.dhundari=Dhundari +controlledvocabulary.language.dhungaloo=Dhungaloo +controlledvocabulary.language.dhurga=Dhurga +controlledvocabulary.language.dhuwal=Dhuwal +controlledvocabulary.language.dhuwaya=Dhuwaya +controlledvocabulary.language.dia=Dia +controlledvocabulary.language.dibabawon_manobo=Dibabawon Manobo +controlledvocabulary.language.dibiyaso=Dibiyaso +controlledvocabulary.language.dibo=Dibo +controlledvocabulary.language.dibole=Dibole +controlledvocabulary.language.dicamay_agta=Dicamay Agta +controlledvocabulary.language.didinga=Didinga +controlledvocabulary.language.dido=Dido +controlledvocabulary.language.dieri=Dieri +controlledvocabulary.language.digaro-mishmi=Digaro-Mishmi +controlledvocabulary.language.digo=Digo +controlledvocabulary.language.dii=Dii +controlledvocabulary.language.dijim-bwilim=Dijim-Bwilim +controlledvocabulary.language.dilling=Dilling +controlledvocabulary.language.dima=Dima +controlledvocabulary.language.dimasa=Dimasa +controlledvocabulary.language.dimbong=Dimbong +controlledvocabulary.language.dime=Dime +controlledvocabulary.language.dimli_(individual_language)=Dimli (individual language) +controlledvocabulary.language.ding=Ding +controlledvocabulary.language.dinka=Dinka +controlledvocabulary.language.dir-nyamzak-mbarimi=Dir-Nyamzak-Mbarimi +controlledvocabulary.language.dirasha=Dirasha +controlledvocabulary.language.diri=Diri +controlledvocabulary.language.diriku=Diriku +controlledvocabulary.language.dirim=Dirim +controlledvocabulary.language.disa=Disa +controlledvocabulary.language.ditammari=Ditammari +controlledvocabulary.language.ditidaht=Ditidaht +controlledvocabulary.language.diuwe=Diuwe +controlledvocabulary.language.diuxi-tilantongo_mixtec=Diuxi-Tilantongo Mixtec +controlledvocabulary.language.dixon_reef=Dixon Reef +controlledvocabulary.language.dizin=Dizin +controlledvocabulary.language.djabugay=Djabugay +controlledvocabulary.language.djabwurrung=Djabwurrung +controlledvocabulary.language.djadjawurrung=Djadjawurrung +controlledvocabulary.language.djambarrpuyngu=Djambarrpuyngu +controlledvocabulary.language.djamindjung=Djamindjung +controlledvocabulary.language.djangun=Djangun +controlledvocabulary.language.djawi=Djawi +controlledvocabulary.language.djeebbana=Djeebbana +controlledvocabulary.language.djimini_senoufo=Djimini Senoufo +controlledvocabulary.language.djinang=Djinang +controlledvocabulary.language.djinba=Djinba +controlledvocabulary.language.djiwarli=Djiwarli +controlledvocabulary.language.dobel=Dobel +controlledvocabulary.language.dobu=Dobu +controlledvocabulary.language.doe=Doe +controlledvocabulary.language.doga=Doga +controlledvocabulary.language.doghoro=Doghoro +controlledvocabulary.language.dogoso=Dogoso +controlledvocabulary.language.dogose=Dogosé +controlledvocabulary.language.dogri_(individual_language)=Dogri (individual language) +controlledvocabulary.language.dogri_(macrolanguage)=Dogri (macrolanguage) +controlledvocabulary.language.dogul_dom_dogon=Dogul Dom Dogon +controlledvocabulary.language.doka=Doka +controlledvocabulary.language.doko-uyanga=Doko-Uyanga +controlledvocabulary.language.dokshi=Dokshi +controlledvocabulary.language.dolgan=Dolgan +controlledvocabulary.language.dolpo=Dolpo +controlledvocabulary.language.dom=Dom +controlledvocabulary.language.domaaki=Domaaki +controlledvocabulary.language.domari=Domari +controlledvocabulary.language.dombe=Dombe +controlledvocabulary.language.dominican_sign_language=Dominican Sign Language +controlledvocabulary.language.dompo=Dompo +controlledvocabulary.language.domu=Domu +controlledvocabulary.language.domung=Domung +controlledvocabulary.language.dondo=Dondo +controlledvocabulary.language.dong=Dong +controlledvocabulary.language.dongo=Dongo +controlledvocabulary.language.dongotono=Dongotono +controlledvocabulary.language.dongshanba_lalo=Dongshanba Lalo +controlledvocabulary.language.dongxiang=Dongxiang +controlledvocabulary.language.donno_so_dogon=Donno So Dogon +controlledvocabulary.language.doondo=Doondo +controlledvocabulary.language.dori'o=Dori'o +controlledvocabulary.language.doromu-koki=Doromu-Koki +controlledvocabulary.language.dorze=Dorze +controlledvocabulary.language.doso=Doso +controlledvocabulary.language.dotyali=Dotyali +controlledvocabulary.language.doutai=Doutai +controlledvocabulary.language.doyayo=Doyayo +controlledvocabulary.language.drents=Drents +controlledvocabulary.language.drung=Drung +controlledvocabulary.language.duala=Duala +controlledvocabulary.language.duano=Duano +controlledvocabulary.language.duau=Duau +controlledvocabulary.language.dubli=Dubli +controlledvocabulary.language.dubu=Dubu +controlledvocabulary.language.dugun=Dugun +controlledvocabulary.language.duguri=Duguri +controlledvocabulary.language.dugwor=Dugwor +controlledvocabulary.language.duhwa=Duhwa +controlledvocabulary.language.duke=Duke +controlledvocabulary.language.dulbu=Dulbu +controlledvocabulary.language.duli-gey=Duli-Gey +controlledvocabulary.language.duma=Duma +controlledvocabulary.language.dumbea=Dumbea +controlledvocabulary.language.dumi=Dumi +controlledvocabulary.language.dumpas=Dumpas +controlledvocabulary.language.dumun=Dumun +controlledvocabulary.language.duna=Duna +controlledvocabulary.language.dungan=Dungan +controlledvocabulary.language.dungmali=Dungmali +controlledvocabulary.language.dungra_bhil=Dungra Bhil +controlledvocabulary.language.dungu=Dungu +controlledvocabulary.language.dupaninan_agta=Dupaninan Agta +controlledvocabulary.language.dura=Dura +controlledvocabulary.language.duri=Duri +controlledvocabulary.language.duriankere=Duriankere +controlledvocabulary.language.durop=Durop +controlledvocabulary.language.duruma=Duruma +controlledvocabulary.language.duruwa=Duruwa +controlledvocabulary.language.dusner=Dusner +controlledvocabulary.language.dusun_deyah=Dusun Deyah +controlledvocabulary.language.dusun_malang=Dusun Malang +controlledvocabulary.language.dusun_witu=Dusun Witu controlledvocabulary.language.dutch=Dutch +controlledvocabulary.language.dutch_sign_language=Dutch Sign Language +controlledvocabulary.language.dutton_world_speedwords=Dutton World Speedwords +controlledvocabulary.language.duungooma=Duungooma +controlledvocabulary.language.duupa=Duupa +controlledvocabulary.language.duvle=Duvle +controlledvocabulary.language.duwai=Duwai +controlledvocabulary.language.duwet=Duwet +controlledvocabulary.language.duya=Dũya +controlledvocabulary.language.dwang=Dwang +controlledvocabulary.language.dyaberdyaber=Dyaberdyaber +controlledvocabulary.language.dyan=Dyan +controlledvocabulary.language.dyangadi=Dyangadi +controlledvocabulary.language.dyarim=Dyarim +controlledvocabulary.language.dyirbal=Dyirbal +controlledvocabulary.language.dyugun=Dyugun +controlledvocabulary.language.dyula=Dyula +controlledvocabulary.language.dza=Dza +controlledvocabulary.language.dzalakha=Dzalakha +controlledvocabulary.language.dzando=Dzando +controlledvocabulary.language.dzao_min=Dzao Min controlledvocabulary.language.dzongkha=Dzongkha +controlledvocabulary.language.dzuungoo=Dzùùngoo +controlledvocabulary.language.daw=Dâw +controlledvocabulary.language.e=E +controlledvocabulary.language.e'ma_buyang=E'ma Buyang +controlledvocabulary.language.e'napa_woromaipu=E'ñapa Woromaipu +controlledvocabulary.language.early_tripuri=Early Tripuri +controlledvocabulary.language.east_ambae=East Ambae +controlledvocabulary.language.east_berawan=East Berawan +controlledvocabulary.language.east_damar=East Damar +controlledvocabulary.language.east_futuna=East Futuna +controlledvocabulary.language.east_kewa=East Kewa +controlledvocabulary.language.east_limba=East Limba +controlledvocabulary.language.east_makian=East Makian +controlledvocabulary.language.east_masela=East Masela +controlledvocabulary.language.east_nyala=East Nyala +controlledvocabulary.language.east_tarangan=East Tarangan +controlledvocabulary.language.east_yugur=East Yugur +controlledvocabulary.language.eastern_abnaki=Eastern Abnaki +controlledvocabulary.language.eastern_acipa=Eastern Acipa +controlledvocabulary.language.eastern_apurimac_quechua=Eastern Apurímac Quechua +controlledvocabulary.language.eastern_arrernte=Eastern Arrernte +controlledvocabulary.language.eastern_balochi=Eastern Balochi +controlledvocabulary.language.eastern_bolivian_guarani=Eastern Bolivian Guaraní +controlledvocabulary.language.eastern_bontok=Eastern Bontok +controlledvocabulary.language.eastern_bru=Eastern Bru +controlledvocabulary.language.eastern_canadian_inuktitut=Eastern Canadian Inuktitut +controlledvocabulary.language.eastern_cham=Eastern Cham +controlledvocabulary.language.eastern_durango_nahuatl=Eastern Durango Nahuatl +controlledvocabulary.language.eastern_egyptian_bedawi_arabic=Eastern Egyptian Bedawi Arabic +controlledvocabulary.language.eastern_frisian=Eastern Frisian +controlledvocabulary.language.eastern_gorkha_tamang=Eastern Gorkha Tamang +controlledvocabulary.language.eastern_highland_chatino=Eastern Highland Chatino +controlledvocabulary.language.eastern_highland_otomi=Eastern Highland Otomi +controlledvocabulary.language.eastern_hongshuihe_zhuang=Eastern Hongshuihe Zhuang +controlledvocabulary.language.eastern_huasteca_nahuatl=Eastern Huasteca Nahuatl +controlledvocabulary.language.eastern_huishui_hmong=Eastern Huishui Hmong +controlledvocabulary.language.eastern_karaboro=Eastern Karaboro +controlledvocabulary.language.eastern_katu=Eastern Katu +controlledvocabulary.language.eastern_kayah=Eastern Kayah +controlledvocabulary.language.eastern_keres=Eastern Keres +controlledvocabulary.language.eastern_khumi_chin=Eastern Khumi Chin +controlledvocabulary.language.eastern_krahn=Eastern Krahn +controlledvocabulary.language.eastern_lalu=Eastern Lalu +controlledvocabulary.language.eastern_lawa=Eastern Lawa +controlledvocabulary.language.eastern_magar=Eastern Magar +controlledvocabulary.language.eastern_maninkakan=Eastern Maninkakan +controlledvocabulary.language.eastern_mari=Eastern Mari +controlledvocabulary.language.eastern_maroon_creole=Eastern Maroon Creole +controlledvocabulary.language.eastern_meohang=Eastern Meohang +controlledvocabulary.language.eastern_minyag=Eastern Minyag +controlledvocabulary.language.eastern_mnong=Eastern Mnong +controlledvocabulary.language.eastern_muria=Eastern Muria +controlledvocabulary.language.eastern_ngad'a=Eastern Ngad'a +controlledvocabulary.language.eastern_nisu=Eastern Nisu +controlledvocabulary.language.eastern_ojibwa=Eastern Ojibwa +controlledvocabulary.language.eastern_oromo=Eastern Oromo +controlledvocabulary.language.eastern_parbate_kham=Eastern Parbate Kham +controlledvocabulary.language.eastern_penan=Eastern Penan +controlledvocabulary.language.eastern_pomo=Eastern Pomo +controlledvocabulary.language.eastern_qiandong_miao=Eastern Qiandong Miao +controlledvocabulary.language.eastern_subanen=Eastern Subanen +controlledvocabulary.language.eastern_tamang=Eastern Tamang +controlledvocabulary.language.eastern_tawbuid=Eastern Tawbuid +controlledvocabulary.language.eastern_xiangxi_miao=Eastern Xiangxi Miao +controlledvocabulary.language.eastern_xwla_gbe=Eastern Xwla Gbe +controlledvocabulary.language.eastern_yiddish=Eastern Yiddish +controlledvocabulary.language.ebira=Ebira +controlledvocabulary.language.eblan=Eblan +controlledvocabulary.language.ebrie=Ebrié +controlledvocabulary.language.ebughu=Ebughu +controlledvocabulary.language.ecuadorian_sign_language=Ecuadorian Sign Language +controlledvocabulary.language.ede_cabe=Ede Cabe +controlledvocabulary.language.ede_ica=Ede Ica +controlledvocabulary.language.ede_idaca=Ede Idaca +controlledvocabulary.language.ede_ije=Ede Ije +controlledvocabulary.language.edera_awyu=Edera Awyu +controlledvocabulary.language.edolo=Edolo +controlledvocabulary.language.edomite=Edomite +controlledvocabulary.language.edopi=Edopi +controlledvocabulary.language.efai=Efai +controlledvocabulary.language.efe=Efe +controlledvocabulary.language.efik=Efik +controlledvocabulary.language.efutop=Efutop +controlledvocabulary.language.ega=Ega +controlledvocabulary.language.eggon=Eggon +controlledvocabulary.language.egypt_sign_language=Egypt Sign Language +controlledvocabulary.language.egyptian_(ancient)=Egyptian (Ancient) +controlledvocabulary.language.egyptian_arabic=Egyptian Arabic +controlledvocabulary.language.ehueun=Ehueun +controlledvocabulary.language.eipomek=Eipomek +controlledvocabulary.language.eitiep=Eitiep +controlledvocabulary.language.ejagham=Ejagham +controlledvocabulary.language.ejamat=Ejamat +controlledvocabulary.language.ekai_chin=Ekai Chin +controlledvocabulary.language.ekajuk=Ekajuk +controlledvocabulary.language.ekari=Ekari +controlledvocabulary.language.eki=Eki +controlledvocabulary.language.ekit=Ekit +controlledvocabulary.language.ekpeye=Ekpeye +controlledvocabulary.language.el_alto_zapotec=El Alto Zapotec +controlledvocabulary.language.el_hugeirat=El Hugeirat +controlledvocabulary.language.el_molo=El Molo +controlledvocabulary.language.el_nayar_cora=El Nayar Cora +controlledvocabulary.language.elamite=Elamite +controlledvocabulary.language.eleme=Eleme +controlledvocabulary.language.elepi=Elepi +controlledvocabulary.language.elfdalian=Elfdalian +controlledvocabulary.language.elip=Elip +controlledvocabulary.language.elkei=Elkei +controlledvocabulary.language.elotepec_zapotec=Elotepec Zapotec +controlledvocabulary.language.eloyi=Eloyi +controlledvocabulary.language.elseng=Elseng +controlledvocabulary.language.elu=Elu +controlledvocabulary.language.elymian=Elymian +controlledvocabulary.language.emae=Emae +controlledvocabulary.language.emai-iuleha-ora=Emai-Iuleha-Ora +controlledvocabulary.language.eman=Eman +controlledvocabulary.language.embaloh=Embaloh +controlledvocabulary.language.embera-baudo=Emberá-Baudó +controlledvocabulary.language.embera-catio=Emberá-Catío +controlledvocabulary.language.embera-chami=Emberá-Chamí +controlledvocabulary.language.embera-tado=Emberá-Tadó +controlledvocabulary.language.embu=Embu +controlledvocabulary.language.emerillon=Emerillon +controlledvocabulary.language.emilian=Emilian +controlledvocabulary.language.emplawas=Emplawas +controlledvocabulary.language.emumu=Emumu +controlledvocabulary.language.en=En +controlledvocabulary.language.enawene-nawe=Enawené-Nawé +controlledvocabulary.language.ende=Ende +controlledvocabulary.language.enga=Enga +controlledvocabulary.language.engdewu=Engdewu +controlledvocabulary.language.engenni=Engenni +controlledvocabulary.language.enggano=Enggano controlledvocabulary.language.english=English +controlledvocabulary.language.enlhet=Enlhet +controlledvocabulary.language.enrekang=Enrekang +controlledvocabulary.language.enu=Enu +controlledvocabulary.language.enwan_(akwa_ibom_state)=Enwan (Akwa Ibom State) +controlledvocabulary.language.enwan_(edo_state)=Enwan (Edo State) +controlledvocabulary.language.enxet=Enxet +controlledvocabulary.language.enya=Enya +controlledvocabulary.language.epena=Epena +controlledvocabulary.language.epi-olmec=Epi-Olmec +controlledvocabulary.language.epie=Epie +controlledvocabulary.language.epigraphic_mayan=Epigraphic Mayan +controlledvocabulary.language.eravallan=Eravallan +controlledvocabulary.language.erave=Erave +controlledvocabulary.language.ere=Ere +controlledvocabulary.language.eritai=Eritai +controlledvocabulary.language.erokwanas=Erokwanas +controlledvocabulary.language.erre=Erre +controlledvocabulary.language.erromintxela=Erromintxela +controlledvocabulary.language.ersu=Ersu +controlledvocabulary.language.eruwa=Eruwa +controlledvocabulary.language.erzya=Erzya +controlledvocabulary.language.esan=Esan +controlledvocabulary.language.ese=Ese +controlledvocabulary.language.ese_ejja=Ese Ejja +controlledvocabulary.language.eshtehardi=Eshtehardi +controlledvocabulary.language.esimbi=Esimbi +controlledvocabulary.language.eskayan=Eskayan controlledvocabulary.language.esperanto=Esperanto +controlledvocabulary.language.esselen=Esselen +controlledvocabulary.language.estado_de_mexico_otomi=Estado de México Otomi controlledvocabulary.language.estonian=Estonian +controlledvocabulary.language.estonian_sign_language=Estonian Sign Language +controlledvocabulary.language.esuma=Esuma +controlledvocabulary.language.etchemin=Etchemin +controlledvocabulary.language.etebi=Etebi +controlledvocabulary.language.eten=Eten +controlledvocabulary.language.eteocretan=Eteocretan +controlledvocabulary.language.eteocypriot=Eteocypriot +controlledvocabulary.language.ethiopian_sign_language=Ethiopian Sign Language +controlledvocabulary.language.etkywan=Etkywan +controlledvocabulary.language.eton_(cameroon)=Eton (Cameroon) +controlledvocabulary.language.eton_(vanuatu)=Eton (Vanuatu) +controlledvocabulary.language.etruscan=Etruscan +controlledvocabulary.language.etulo=Etulo +controlledvocabulary.language.eudeve=Eudeve +controlledvocabulary.language.evant=Evant +controlledvocabulary.language.even=Even +controlledvocabulary.language.evenki=Evenki +controlledvocabulary.language.eviya=Eviya +controlledvocabulary.language.ewage-notu=Ewage-Notu controlledvocabulary.language.ewe=Ewe +controlledvocabulary.language.ewondo=Ewondo +controlledvocabulary.language.extremaduran=Extremaduran +controlledvocabulary.language.eyak=Eyak +controlledvocabulary.language.ezaa=Ezaa +controlledvocabulary.language.fa_d'ambu=Fa d'Ambu +controlledvocabulary.language.fagani=Fagani +controlledvocabulary.language.faifi=Faifi +controlledvocabulary.language.faire_atta=Faire Atta +controlledvocabulary.language.faita=Faita +controlledvocabulary.language.faiwol=Faiwol +controlledvocabulary.language.fala=Fala +controlledvocabulary.language.falam_chin=Falam Chin +controlledvocabulary.language.fali=Fali +controlledvocabulary.language.faliscan=Faliscan +controlledvocabulary.language.fam=Fam +controlledvocabulary.language.fanagalo=Fanagalo +controlledvocabulary.language.fanamaket=Fanamaket +controlledvocabulary.language.fanbak=Fanbak +controlledvocabulary.language.fang_(cameroon)=Fang (Cameroon) +controlledvocabulary.language.fang_(equatorial_guinea)=Fang (Equatorial Guinea) +controlledvocabulary.language.fania=Fania +controlledvocabulary.language.fanti=Fanti +controlledvocabulary.language.far_western_muria=Far Western Muria +controlledvocabulary.language.farefare=Farefare controlledvocabulary.language.faroese=Faroese +controlledvocabulary.language.fas=Fas +controlledvocabulary.language.fasu=Fasu +controlledvocabulary.language.fataleka=Fataleka +controlledvocabulary.language.fataluku=Fataluku +controlledvocabulary.language.fayu=Fayu +controlledvocabulary.language.fe'fe'=Fe'fe' +controlledvocabulary.language.fembe=Fembe +controlledvocabulary.language.fernando_po_creole_english=Fernando Po Creole English +controlledvocabulary.language.feroge=Feroge +controlledvocabulary.language.fiji_hindi=Fiji Hindi controlledvocabulary.language.fijian=Fijian +controlledvocabulary.language.filipino=Filipino +controlledvocabulary.language.filomena_mata-coahuitlan_totonac=Filomena Mata-Coahuitlán Totonac +controlledvocabulary.language.finland-swedish_sign_language=Finland-Swedish Sign Language controlledvocabulary.language.finnish=Finnish +controlledvocabulary.language.finnish_sign_language=Finnish Sign Language +controlledvocabulary.language.finongan=Finongan +controlledvocabulary.language.fipa=Fipa +controlledvocabulary.language.firan=Firan +controlledvocabulary.language.fiwaga=Fiwaga +controlledvocabulary.language.flaaitaal=Flaaitaal +controlledvocabulary.language.flinders_island=Flinders Island +controlledvocabulary.language.foau=Foau +controlledvocabulary.language.foi=Foi +controlledvocabulary.language.foia_foia=Foia Foia +controlledvocabulary.language.folopa=Folopa +controlledvocabulary.language.foma=Foma +controlledvocabulary.language.fon=Fon +controlledvocabulary.language.fongoro=Fongoro +controlledvocabulary.language.foodo=Foodo +controlledvocabulary.language.forak=Forak +controlledvocabulary.language.fordata=Fordata +controlledvocabulary.language.fore=Fore +controlledvocabulary.language.forest_enets=Forest Enets +controlledvocabulary.language.fortsenal=Fortsenal +controlledvocabulary.language.francisco_leon_zoque=Francisco León Zoque +controlledvocabulary.language.frankish=Frankish controlledvocabulary.language.french=French -controlledvocabulary.language.fula,_fulah,_pulaar,_pular=Fula, Fulah, Pulaar, Pular +controlledvocabulary.language.french_sign_language=French Sign Language +controlledvocabulary.language.friulian=Friulian +controlledvocabulary.language.fula,_fulah=Fula, Fulah +controlledvocabulary.language.fuliiru=Fuliiru +controlledvocabulary.language.fulnio=Fulniô +controlledvocabulary.language.fum=Fum +controlledvocabulary.language.fungwa=Fungwa +controlledvocabulary.language.fur=Fur +controlledvocabulary.language.furu=Furu +controlledvocabulary.language.futuna-aniwa=Futuna-Aniwa +controlledvocabulary.language.fuyug=Fuyug +controlledvocabulary.language.fwe=Fwe +controlledvocabulary.language.fwai=Fwâi +controlledvocabulary.language.fyam=Fyam +controlledvocabulary.language.fyer=Fyer +controlledvocabulary.language.ga=Ga +controlledvocabulary.language.ga'anda=Ga'anda +controlledvocabulary.language.ga'dang=Ga'dang +controlledvocabulary.language.gaa=Gaa +controlledvocabulary.language.gaam=Gaam +controlledvocabulary.language.gabi-gabi=Gabi-Gabi +controlledvocabulary.language.gabri=Gabri +controlledvocabulary.language.gabrielino-fernandeno=Gabrielino-Fernandeño +controlledvocabulary.language.gadang=Gadang +controlledvocabulary.language.gaddang=Gaddang +controlledvocabulary.language.gaddi=Gaddi +controlledvocabulary.language.gade=Gade +controlledvocabulary.language.gade_lohar=Gade Lohar +controlledvocabulary.language.gadjerawang=Gadjerawang +controlledvocabulary.language.gadsup=Gadsup +controlledvocabulary.language.gaelic,_scottish_gaelic=Gaelic, Scottish Gaelic +controlledvocabulary.language.gafat=Gafat +controlledvocabulary.language.gagadu=Gagadu +controlledvocabulary.language.gagauz=Gagauz +controlledvocabulary.language.gagnoa_bete=Gagnoa Bété +controlledvocabulary.language.gagu=Gagu +controlledvocabulary.language.gahri=Gahri +controlledvocabulary.language.gaikundi=Gaikundi +controlledvocabulary.language.gail=Gail +controlledvocabulary.language.gaina=Gaina +controlledvocabulary.language.gal=Gal +controlledvocabulary.language.galambu=Galambu +controlledvocabulary.language.galatian=Galatian +controlledvocabulary.language.galela=Galela +controlledvocabulary.language.galeya=Galeya +controlledvocabulary.language.galibi_carib=Galibi Carib +controlledvocabulary.language.galice=Galice controlledvocabulary.language.galician=Galician +controlledvocabulary.language.galindan=Galindan +controlledvocabulary.language.gallurese_sardinian=Gallurese Sardinian +controlledvocabulary.language.galo=Galo +controlledvocabulary.language.galolen=Galolen +controlledvocabulary.language.gamale_kham=Gamale Kham +controlledvocabulary.language.gambera=Gambera +controlledvocabulary.language.gambian_wolof=Gambian Wolof +controlledvocabulary.language.gamilaraay=Gamilaraay +controlledvocabulary.language.gamit=Gamit +controlledvocabulary.language.gamkonora=Gamkonora +controlledvocabulary.language.gamo=Gamo +controlledvocabulary.language.gamo-ningi=Gamo-Ningi +controlledvocabulary.language.gan_chinese=Gan Chinese +controlledvocabulary.language.gana=Gana +controlledvocabulary.language.ganang=Ganang +controlledvocabulary.language.ganda=Ganda +controlledvocabulary.language.gane=Gane +controlledvocabulary.language.ganggalida=Ganggalida +controlledvocabulary.language.ganglau=Ganglau +controlledvocabulary.language.gangte=Gangte +controlledvocabulary.language.gangulu=Gangulu +controlledvocabulary.language.gants=Gants +controlledvocabulary.language.ganza=Ganza +controlledvocabulary.language.ganzi=Ganzi +controlledvocabulary.language.gao=Gao +controlledvocabulary.language.gapapaiwa=Gapapaiwa +controlledvocabulary.language.garhwali=Garhwali +controlledvocabulary.language.garifuna=Garifuna +controlledvocabulary.language.garig-ilgar=Garig-Ilgar +controlledvocabulary.language.garingbal=Garingbal +controlledvocabulary.language.garlali=Garlali +controlledvocabulary.language.garo=Garo +controlledvocabulary.language.garre=Garre +controlledvocabulary.language.garrwa=Garrwa +controlledvocabulary.language.garus=Garus +controlledvocabulary.language.garza=Garza +controlledvocabulary.language.gata'=Gata' +controlledvocabulary.language.gavak=Gavak +controlledvocabulary.language.gavar=Gavar +controlledvocabulary.language.gaviao_do_jiparana=Gavião Do Jiparaná +controlledvocabulary.language.gawar-bati=Gawar-Bati +controlledvocabulary.language.gawri=Gawri +controlledvocabulary.language.gawwada=Gawwada +controlledvocabulary.language.gayil=Gayil +controlledvocabulary.language.gayo=Gayo +controlledvocabulary.language.gazi=Gazi +controlledvocabulary.language.gaɓogbo=Gaɓogbo +controlledvocabulary.language.gbagyi=Gbagyi +controlledvocabulary.language.gbanu=Gbanu +controlledvocabulary.language.gbanziri=Gbanziri +controlledvocabulary.language.gbari=Gbari +controlledvocabulary.language.gbaya_(central_african_republic)=Gbaya (Central African Republic) +controlledvocabulary.language.gbaya_(sudan)=Gbaya (Sudan) +controlledvocabulary.language.gbaya-bossangoa=Gbaya-Bossangoa +controlledvocabulary.language.gbaya-bozoum=Gbaya-Bozoum +controlledvocabulary.language.gbaya-mbodomo=Gbaya-Mbodomo +controlledvocabulary.language.gbayi=Gbayi +controlledvocabulary.language.gbesi_gbe=Gbesi Gbe +controlledvocabulary.language.gbii=Gbii +controlledvocabulary.language.gbin=Gbin +controlledvocabulary.language.gbiri-niragu=Gbiri-Niragu +controlledvocabulary.language.gboloo_grebo=Gboloo Grebo +controlledvocabulary.language.ge=Ge +controlledvocabulary.language.geba_karen=Geba Karen +controlledvocabulary.language.gebe=Gebe +controlledvocabulary.language.gedaged=Gedaged +controlledvocabulary.language.gedeo=Gedeo +controlledvocabulary.language.geez=Geez +controlledvocabulary.language.geji=Geji +controlledvocabulary.language.geko_karen=Geko Karen +controlledvocabulary.language.gela=Gela +controlledvocabulary.language.geme=Geme +controlledvocabulary.language.gen=Gen +controlledvocabulary.language.gende=Gende +controlledvocabulary.language.gengle=Gengle controlledvocabulary.language.georgian=Georgian +controlledvocabulary.language.gepo=Gepo +controlledvocabulary.language.gera=Gera +controlledvocabulary.language.gerai=Gerai controlledvocabulary.language.german=German -controlledvocabulary.language.greek_(modern)=Greek (modern) -controlledvocabulary.language.guarani=Guaraní +controlledvocabulary.language.german_sign_language=German Sign Language +controlledvocabulary.language.geruma=Geruma +controlledvocabulary.language.geser-gorom=Geser-Gorom +controlledvocabulary.language.ghadames=Ghadamès +controlledvocabulary.language.ghanaian_pidgin_english=Ghanaian Pidgin English +controlledvocabulary.language.ghanaian_sign_language=Ghanaian Sign Language +controlledvocabulary.language.ghandruk_sign_language=Ghandruk Sign Language +controlledvocabulary.language.ghanongga=Ghanongga +controlledvocabulary.language.ghari=Ghari +controlledvocabulary.language.ghayavi=Ghayavi +controlledvocabulary.language.gheg_albanian=Gheg Albanian +controlledvocabulary.language.ghera=Ghera +controlledvocabulary.language.ghodoberi=Ghodoberi +controlledvocabulary.language.ghomara=Ghomara +controlledvocabulary.language.ghomala'=Ghomálá' +controlledvocabulary.language.ghotuo=Ghotuo +controlledvocabulary.language.ghulfan=Ghulfan +controlledvocabulary.language.giangan=Giangan +controlledvocabulary.language.gibanawa=Gibanawa +controlledvocabulary.language.gidar=Gidar +controlledvocabulary.language.giiwo=Giiwo +controlledvocabulary.language.gikyode=Gikyode +controlledvocabulary.language.gilaki=Gilaki +controlledvocabulary.language.gilbertese=Gilbertese +controlledvocabulary.language.gilima=Gilima +controlledvocabulary.language.gilyak=Gilyak +controlledvocabulary.language.gimi_(eastern_highlands)=Gimi (Eastern Highlands) +controlledvocabulary.language.gimi_(west_new_britain)=Gimi (West New Britain) +controlledvocabulary.language.gimme=Gimme +controlledvocabulary.language.gimnime=Gimnime +controlledvocabulary.language.ginuman=Ginuman +controlledvocabulary.language.ginyanga=Ginyanga +controlledvocabulary.language.girawa=Girawa +controlledvocabulary.language.girirra=Girirra +controlledvocabulary.language.giryama=Giryama +controlledvocabulary.language.githabul=Githabul +controlledvocabulary.language.gitonga=Gitonga +controlledvocabulary.language.gitua=Gitua +controlledvocabulary.language.gitxsan=Gitxsan +controlledvocabulary.language.giyug=Giyug +controlledvocabulary.language.gizrra=Gizrra +controlledvocabulary.language.glaro-twabo=Glaro-Twabo +controlledvocabulary.language.glavda=Glavda +controlledvocabulary.language.glio-oubi=Glio-Oubi +controlledvocabulary.language.gnau=Gnau +controlledvocabulary.language.goan_konkani=Goan Konkani +controlledvocabulary.language.goaria=Goaria +controlledvocabulary.language.gobasi=Gobasi +controlledvocabulary.language.gobu=Gobu +controlledvocabulary.language.godie=Godié +controlledvocabulary.language.godwari=Godwari +controlledvocabulary.language.goemai=Goemai +controlledvocabulary.language.gofa=Gofa +controlledvocabulary.language.gogo=Gogo +controlledvocabulary.language.gogodala=Gogodala +controlledvocabulary.language.gokana=Gokana +controlledvocabulary.language.gola=Gola +controlledvocabulary.language.golin=Golin +controlledvocabulary.language.golpa=Golpa +controlledvocabulary.language.gondi=Gondi +controlledvocabulary.language.gone_dau=Gone Dau +controlledvocabulary.language.gongduk=Gongduk +controlledvocabulary.language.gonja=Gonja +controlledvocabulary.language.goo=Goo +controlledvocabulary.language.gooniyandi=Gooniyandi +controlledvocabulary.language.gor=Gor +controlledvocabulary.language.gorakor=Gorakor +controlledvocabulary.language.gorap=Gorap +controlledvocabulary.language.goreng=Goreng +controlledvocabulary.language.gorontalo=Gorontalo +controlledvocabulary.language.gorovu=Gorovu +controlledvocabulary.language.gorowa=Gorowa +controlledvocabulary.language.gothic=Gothic +controlledvocabulary.language.goundo=Goundo +controlledvocabulary.language.gourmanchema=Gourmanchéma +controlledvocabulary.language.gowlan=Gowlan +controlledvocabulary.language.gowli=Gowli +controlledvocabulary.language.gowro=Gowro +controlledvocabulary.language.gozarkhani=Gozarkhani +controlledvocabulary.language.grangali=Grangali +controlledvocabulary.language.grass_koiari=Grass Koiari +controlledvocabulary.language.grebo=Grebo +controlledvocabulary.language.greek_sign_language=Greek Sign Language +controlledvocabulary.language.green_gelao=Green Gelao +controlledvocabulary.language.greenlandic,_kalaallisut=Greenlandic, Kalaallisut +controlledvocabulary.language.grenadian_creole_english=Grenadian Creole English +controlledvocabulary.language.gresi=Gresi +controlledvocabulary.language.groma=Groma +controlledvocabulary.language.gronings=Gronings +controlledvocabulary.language.gros_ventre=Gros Ventre +controlledvocabulary.language.gua=Gua +controlledvocabulary.language.guadeloupean_creole_french=Guadeloupean Creole French +controlledvocabulary.language.guahibo=Guahibo +controlledvocabulary.language.guajajara=Guajajára +controlledvocabulary.language.guaja=Guajá +controlledvocabulary.language.guambiano=Guambiano +controlledvocabulary.language.guana_(brazil)=Guana (Brazil) +controlledvocabulary.language.guana_(paraguay)=Guana (Paraguay) +controlledvocabulary.language.guanano=Guanano +controlledvocabulary.language.guanche=Guanche +controlledvocabulary.language.guanyinqiao=Guanyinqiao +controlledvocabulary.language.guarani=Guarani, Guaraní +controlledvocabulary.language.guarayu=Guarayu +controlledvocabulary.language.guarequena=Guarequena +controlledvocabulary.language.guatemalan_sign_language=Guatemalan Sign Language +controlledvocabulary.language.guato=Guató +controlledvocabulary.language.guayabero=Guayabero +controlledvocabulary.language.gudang=Gudang +controlledvocabulary.language.gudanji=Gudanji +controlledvocabulary.language.gude=Gude +controlledvocabulary.language.gudu=Gudu +controlledvocabulary.language.guduf-gava=Guduf-Gava +controlledvocabulary.language.guerrero_amuzgo=Guerrero Amuzgo +controlledvocabulary.language.guerrero_nahuatl=Guerrero Nahuatl +controlledvocabulary.language.guevea_de_humboldt_zapotec=Guevea De Humboldt Zapotec +controlledvocabulary.language.gugadj=Gugadj +controlledvocabulary.language.gugu_badhun=Gugu Badhun +controlledvocabulary.language.gugu_warra=Gugu Warra +controlledvocabulary.language.gugubera=Gugubera +controlledvocabulary.language.guhu-samane=Guhu-Samane +controlledvocabulary.language.guianese_creole_french=Guianese Creole French +controlledvocabulary.language.guibei_zhuang=Guibei Zhuang +controlledvocabulary.language.guiberoua_bete=Guiberoua Béte +controlledvocabulary.language.guibian_zhuang=Guibian Zhuang +controlledvocabulary.language.guinea_kpelle=Guinea Kpelle +controlledvocabulary.language.guinea-bissau_sign_language=Guinea-Bissau Sign Language +controlledvocabulary.language.guinean_sign_language=Guinean Sign Language +controlledvocabulary.language.guiqiong=Guiqiong controlledvocabulary.language.gujarati=Gujarati -controlledvocabulary.language.haitian,_haitian_creole=Haitian, Haitian Creole +controlledvocabulary.language.gujari=Gujari +controlledvocabulary.language.gula_(central_african_republic)=Gula (Central African Republic) +controlledvocabulary.language.gula_(chad)=Gula (Chad) +controlledvocabulary.language.gula_iro=Gula Iro +controlledvocabulary.language.gula'alaa=Gula'alaa +controlledvocabulary.language.gulay=Gulay +controlledvocabulary.language.gule=Gule +controlledvocabulary.language.gulf_arabic=Gulf Arabic +controlledvocabulary.language.gumalu=Gumalu +controlledvocabulary.language.gumatj=Gumatj +controlledvocabulary.language.gumawana=Gumawana +controlledvocabulary.language.gumuz=Gumuz +controlledvocabulary.language.gun=Gun +controlledvocabulary.language.gundi=Gundi +controlledvocabulary.language.gunditjmara=Gunditjmara +controlledvocabulary.language.gundungurra=Gundungurra +controlledvocabulary.language.gungabula=Gungabula +controlledvocabulary.language.gungu=Gungu +controlledvocabulary.language.guntai=Guntai +controlledvocabulary.language.gunwinggu=Gunwinggu +controlledvocabulary.language.gunya=Gunya +controlledvocabulary.language.gupa-abawa=Gupa-Abawa +controlledvocabulary.language.gupapuyngu=Gupapuyngu +controlledvocabulary.language.guramalum=Guramalum +controlledvocabulary.language.gurani=Gurani +controlledvocabulary.language.gurdjar=Gurdjar +controlledvocabulary.language.gureng_gureng=Gureng Gureng +controlledvocabulary.language.gurgula=Gurgula +controlledvocabulary.language.guriaso=Guriaso +controlledvocabulary.language.gurindji=Gurindji +controlledvocabulary.language.gurindji_kriol=Gurindji Kriol +controlledvocabulary.language.gurmana=Gurmana +controlledvocabulary.language.guro=Guro +controlledvocabulary.language.gurr-goni=Gurr-goni +controlledvocabulary.language.gurung=Gurung +controlledvocabulary.language.guruntum-mbaaru=Guruntum-Mbaaru +controlledvocabulary.language.gusii=Gusii +controlledvocabulary.language.gusilay=Gusilay +controlledvocabulary.language.guugu_yimidhirr=Guugu Yimidhirr +controlledvocabulary.language.guwa=Guwa +controlledvocabulary.language.guwamu=Guwamu +controlledvocabulary.language.guya=Guya +controlledvocabulary.language.guyanese_creole_english=Guyanese Creole English +controlledvocabulary.language.guyani=Guyani +controlledvocabulary.language.gvoko=Gvoko +controlledvocabulary.language.gwa=Gwa +controlledvocabulary.language.gwahatike=Gwahatike +controlledvocabulary.language.gwak=Gwak +controlledvocabulary.language.gwamhi-wuri=Gwamhi-Wuri +controlledvocabulary.language.gwandara=Gwandara +controlledvocabulary.language.gweda=Gweda +controlledvocabulary.language.gweno=Gweno +controlledvocabulary.language.gwere=Gwere +controlledvocabulary.language.gwich'in=Gwichʼin +controlledvocabulary.language.gyalsumdo=Gyalsumdo +controlledvocabulary.language.gyele=Gyele +controlledvocabulary.language.gyem=Gyem +controlledvocabulary.language.guila_zapotec=Güilá Zapotec +controlledvocabulary.language.gandhari=Gāndhārī +controlledvocabulary.language.ha=Ha +controlledvocabulary.language.habu=Habu +controlledvocabulary.language.hadiyya=Hadiyya +controlledvocabulary.language.hadothi=Hadothi +controlledvocabulary.language.hadrami=Hadrami +controlledvocabulary.language.hadrami_arabic=Hadrami Arabic +controlledvocabulary.language.hadza=Hadza +controlledvocabulary.language.haeke=Haeke +controlledvocabulary.language.hahon=Hahon +controlledvocabulary.language.haida=Haida +controlledvocabulary.language.haigwai=Haigwai +controlledvocabulary.language.haiphong_sign_language=Haiphong Sign Language +controlledvocabulary.language.haisla=Haisla +controlledvocabulary.language.haitian_creole,_haitian=Haitian Creole, Haitian +controlledvocabulary.language.haitian_vodoun_culture_language=Haitian Vodoun Culture Language +controlledvocabulary.language.haiǁom=Haiǁom +controlledvocabulary.language.haji=Haji +controlledvocabulary.language.hajong=Hajong +controlledvocabulary.language.hakha_chin=Hakha Chin +controlledvocabulary.language.hakka_chinese=Hakka Chinese +controlledvocabulary.language.hako=Hakö +controlledvocabulary.language.halang=Halang +controlledvocabulary.language.halang_doan=Halang Doan +controlledvocabulary.language.halbi=Halbi +controlledvocabulary.language.halh_mongolian=Halh Mongolian +controlledvocabulary.language.halia=Halia +controlledvocabulary.language.halkomelem=Halkomelem +controlledvocabulary.language.hamap=Hamap +controlledvocabulary.language.hamba=Hamba +controlledvocabulary.language.hamer-banna=Hamer-Banna +controlledvocabulary.language.hamtai=Hamtai +controlledvocabulary.language.han=Han +controlledvocabulary.language.hanga=Hanga +controlledvocabulary.language.hanga_hundi=Hanga Hundi +controlledvocabulary.language.hangaza=Hangaza +controlledvocabulary.language.hani=Hani +controlledvocabulary.language.hano=Hano +controlledvocabulary.language.hanoi_sign_language=Hanoi Sign Language +controlledvocabulary.language.hanunoo=Hanunoo +controlledvocabulary.language.harami=Harami +controlledvocabulary.language.harari=Harari +controlledvocabulary.language.harijan_kinnauri=Harijan Kinnauri +controlledvocabulary.language.haroi=Haroi +controlledvocabulary.language.harsusi=Harsusi +controlledvocabulary.language.haruai=Haruai +controlledvocabulary.language.haruku=Haruku +controlledvocabulary.language.haryanvi=Haryanvi +controlledvocabulary.language.harzani=Harzani +controlledvocabulary.language.hasha=Hasha +controlledvocabulary.language.hassaniyya=Hassaniyya +controlledvocabulary.language.hatam=Hatam +controlledvocabulary.language.hattic=Hattic controlledvocabulary.language.hausa=Hausa -controlledvocabulary.language.hebrew_(modern)=Hebrew (modern) +controlledvocabulary.language.hausa_sign_language=Hausa Sign Language +controlledvocabulary.language.havasupai-walapai-yavapai=Havasupai-Walapai-Yavapai +controlledvocabulary.language.haveke=Haveke +controlledvocabulary.language.havu=Havu +controlledvocabulary.language.hawai'i_creole_english=Hawai'i Creole English +controlledvocabulary.language.hawai'i_sign_language_(hsl)=Hawai'i Sign Language (HSL) +controlledvocabulary.language.hawaiian=Hawaiian +controlledvocabulary.language.haya=Haya +controlledvocabulary.language.hazaragi=Hazaragi +controlledvocabulary.language.hdi=Hdi +controlledvocabulary.language.hebrew_(modern)=Hebrew (modern), Hebrew +controlledvocabulary.language.hehe=Hehe +controlledvocabulary.language.heiban=Heiban +controlledvocabulary.language.heiltsuk=Heiltsuk +controlledvocabulary.language.helong=Helong +controlledvocabulary.language.hema=Hema +controlledvocabulary.language.hemba=Hemba +controlledvocabulary.language.herde=Herdé controlledvocabulary.language.herero=Herero +controlledvocabulary.language.hermit=Hermit +controlledvocabulary.language.hernican=Hernican +controlledvocabulary.language.hewa=Hewa +controlledvocabulary.language.heyo=Heyo +controlledvocabulary.language.hiberno-scottish_gaelic=Hiberno-Scottish Gaelic +controlledvocabulary.language.hibito=Hibito +controlledvocabulary.language.hidatsa=Hidatsa +controlledvocabulary.language.hieroglyphic_luwian=Hieroglyphic Luwian +controlledvocabulary.language.higaonon=Higaonon +controlledvocabulary.language.highland_konjo=Highland Konjo +controlledvocabulary.language.highland_oaxaca_chontal=Highland Oaxaca Chontal +controlledvocabulary.language.highland_popoluca=Highland Popoluca +controlledvocabulary.language.highland_puebla_nahuatl=Highland Puebla Nahuatl +controlledvocabulary.language.highland_totonac=Highland Totonac +controlledvocabulary.language.hijazi_arabic=Hijazi Arabic +controlledvocabulary.language.hijuk=Hijuk +controlledvocabulary.language.hiligaynon=Hiligaynon +controlledvocabulary.language.himarima=Himarimã controlledvocabulary.language.hindi=Hindi +controlledvocabulary.language.hinduri=Hinduri +controlledvocabulary.language.hinukh=Hinukh controlledvocabulary.language.hiri_motu=Hiri Motu +controlledvocabulary.language.hittite=Hittite +controlledvocabulary.language.hitu=Hitu +controlledvocabulary.language.hiw=Hiw +controlledvocabulary.language.hixkaryana=Hixkaryána +controlledvocabulary.language.hlai=Hlai +controlledvocabulary.language.hlepho_phowa=Hlepho Phowa +controlledvocabulary.language.hlersu=Hlersu +controlledvocabulary.language.hmar=Hmar +controlledvocabulary.language.hmong=Hmong +controlledvocabulary.language.hmong_daw=Hmong Daw +controlledvocabulary.language.hmong_don=Hmong Don +controlledvocabulary.language.hmong_do=Hmong Dô +controlledvocabulary.language.hmong_njua=Hmong Njua +controlledvocabulary.language.hmong_shua=Hmong Shua +controlledvocabulary.language.hmwaveke=Hmwaveke +controlledvocabulary.language.ho=Ho +controlledvocabulary.language.ho_chi_minh_city_sign_language=Ho Chi Minh City Sign Language +controlledvocabulary.language.ho-chunk=Ho-Chunk +controlledvocabulary.language.hoava=Hoava +controlledvocabulary.language.hobyot=Hobyót +controlledvocabulary.language.hoia_hoia=Hoia Hoia +controlledvocabulary.language.holikachuk=Holikachuk +controlledvocabulary.language.holiya=Holiya +controlledvocabulary.language.holma=Holma +controlledvocabulary.language.holoholo=Holoholo +controlledvocabulary.language.holu=Holu +controlledvocabulary.language.homa=Homa +controlledvocabulary.language.honduras_sign_language=Honduras Sign Language +controlledvocabulary.language.hong_kong_sign_language=Hong Kong Sign Language +controlledvocabulary.language.honi=Honi +controlledvocabulary.language.hopi=Hopi +controlledvocabulary.language.horned_miao=Horned Miao +controlledvocabulary.language.horo=Horo +controlledvocabulary.language.horom=Horom +controlledvocabulary.language.horpa=Horpa +controlledvocabulary.language.hote=Hote +controlledvocabulary.language.hoti=Hoti +controlledvocabulary.language.hovongan=Hovongan +controlledvocabulary.language.hoyahoya=Hoyahoya +controlledvocabulary.language.hozo=Hozo +controlledvocabulary.language.hpon=Hpon +controlledvocabulary.language.hrangkhol=Hrangkhol +controlledvocabulary.language.hre=Hre +controlledvocabulary.language.hruso=Hruso +controlledvocabulary.language.hu=Hu +controlledvocabulary.language.huachipaeri=Huachipaeri +controlledvocabulary.language.huallaga_huanuco_quechua=Huallaga Huánuco Quechua +controlledvocabulary.language.huamalies-dos_de_mayo_huanuco_quechua=Huamalíes-Dos de Mayo Huánuco Quechua +controlledvocabulary.language.huambisa=Huambisa +controlledvocabulary.language.huarijio=Huarijio +controlledvocabulary.language.huastec=Huastec +controlledvocabulary.language.huaulu=Huaulu +controlledvocabulary.language.huautla_mazatec=Huautla Mazatec +controlledvocabulary.language.huaxcaleca_nahuatl=Huaxcaleca Nahuatl +controlledvocabulary.language.huaylas_ancash_quechua=Huaylas Ancash Quechua +controlledvocabulary.language.huaylla_wanca_quechua=Huaylla Wanca Quechua +controlledvocabulary.language.huba=Huba +controlledvocabulary.language.huehuetla_tepehua=Huehuetla Tepehua +controlledvocabulary.language.huichol=Huichol +controlledvocabulary.language.huilliche=Huilliche +controlledvocabulary.language.huitepec_mixtec=Huitepec Mixtec +controlledvocabulary.language.huizhou_chinese=Huizhou Chinese +controlledvocabulary.language.hukumina=Hukumina +controlledvocabulary.language.hula=Hula +controlledvocabulary.language.hulaula=Hulaulá +controlledvocabulary.language.huli=Huli +controlledvocabulary.language.hulung=Hulung +controlledvocabulary.language.humburi_senni_songhay=Humburi Senni Songhay +controlledvocabulary.language.humene=Humene +controlledvocabulary.language.humla=Humla +controlledvocabulary.language.hunde=Hunde +controlledvocabulary.language.hung=Hung +controlledvocabulary.language.hungana=Hungana controlledvocabulary.language.hungarian=Hungarian -controlledvocabulary.language.interlingua=Interlingua +controlledvocabulary.language.hungarian_sign_language=Hungarian Sign Language +controlledvocabulary.language.hungu=Hungu +controlledvocabulary.language.hunjara-kaina_ke=Hunjara-Kaina Ke +controlledvocabulary.language.hunnic=Hunnic +controlledvocabulary.language.hunsrik=Hunsrik +controlledvocabulary.language.hunzib=Hunzib +controlledvocabulary.language.hupa=Hupa +controlledvocabulary.language.hupde=Hupdë +controlledvocabulary.language.hupla=Hupla +controlledvocabulary.language.hurrian=Hurrian +controlledvocabulary.language.hutterite_german=Hutterite German +controlledvocabulary.language.hwana=Hwana +controlledvocabulary.language.hya=Hya +controlledvocabulary.language.hyam=Hyam +controlledvocabulary.language.hyolmo=Hyolmo +controlledvocabulary.language.hertevin=Hértevin +controlledvocabulary.language.hone=Hõne +controlledvocabulary.language.i-wak=I-Wak +controlledvocabulary.language.iaai=Iaai +controlledvocabulary.language.iamalele=Iamalele +controlledvocabulary.language.iatmul=Iatmul +controlledvocabulary.language.iau=Iau +controlledvocabulary.language.ibali_teke=Ibali Teke +controlledvocabulary.language.ibaloi=Ibaloi +controlledvocabulary.language.iban=Iban +controlledvocabulary.language.ibanag=Ibanag +controlledvocabulary.language.ibani=Ibani +controlledvocabulary.language.ibatan=Ibatan +controlledvocabulary.language.iberian=Iberian +controlledvocabulary.language.ibibio=Ibibio +controlledvocabulary.language.ibino=Ibino +controlledvocabulary.language.ibu=Ibu +controlledvocabulary.language.ibuoro=Ibuoro +controlledvocabulary.language.icelandic=Icelandic +controlledvocabulary.language.icelandic_sign_language=Icelandic Sign Language +controlledvocabulary.language.iceve-maci=Iceve-Maci +controlledvocabulary.language.ida'an=Ida'an +controlledvocabulary.language.idakho-isukha-tiriki=Idakho-Isukha-Tiriki +controlledvocabulary.language.idate=Idaté +controlledvocabulary.language.idere=Idere +controlledvocabulary.language.idesa=Idesa +controlledvocabulary.language.idi=Idi +controlledvocabulary.language.ido=Ido +controlledvocabulary.language.idoma=Idoma +controlledvocabulary.language.idon=Idon +controlledvocabulary.language.idu-mishmi=Idu-Mishmi +controlledvocabulary.language.iduna=Iduna +controlledvocabulary.language.ifo=Ifo +controlledvocabulary.language.ife=Ifè +controlledvocabulary.language.igala=Igala +controlledvocabulary.language.igana=Igana +controlledvocabulary.language.igbo=Igbo +controlledvocabulary.language.igede=Igede +controlledvocabulary.language.ignaciano=Ignaciano +controlledvocabulary.language.igo=Igo +controlledvocabulary.language.iguta=Iguta +controlledvocabulary.language.igwe=Igwe +controlledvocabulary.language.iha=Iha +controlledvocabulary.language.iha_based_pidgin=Iha Based Pidgin +controlledvocabulary.language.ihievbe=Ihievbe +controlledvocabulary.language.ik=Ik +controlledvocabulary.language.ika=Ika +controlledvocabulary.language.ikaranggal=Ikaranggal +controlledvocabulary.language.ikhin-arokho=Ikhin-Arokho +controlledvocabulary.language.ikizu=Ikizu +controlledvocabulary.language.iko=Iko +controlledvocabulary.language.ikobi=Ikobi +controlledvocabulary.language.ikoma-nata-isenye=Ikoma-Nata-Isenye +controlledvocabulary.language.ikpeng=Ikpeng +controlledvocabulary.language.ikpeshi=Ikpeshi +controlledvocabulary.language.ikposo=Ikposo +controlledvocabulary.language.iku-gora-ankwa=Iku-Gora-Ankwa +controlledvocabulary.language.ikulu=Ikulu +controlledvocabulary.language.ikwere=Ikwere +controlledvocabulary.language.ikwo=Ikwo +controlledvocabulary.language.ila=Ila +controlledvocabulary.language.ile_ape=Ile Ape +controlledvocabulary.language.ili_turki=Ili Turki +controlledvocabulary.language.ili'uun=Ili'uun +controlledvocabulary.language.ilianen_manobo=Ilianen Manobo +controlledvocabulary.language.illyrian=Illyrian +controlledvocabulary.language.iloko=Iloko +controlledvocabulary.language.ilongot=Ilongot +controlledvocabulary.language.ilue=Ilue +controlledvocabulary.language.ilwana=Ilwana +controlledvocabulary.language.imbabura_highland_quichua=Imbabura Highland Quichua +controlledvocabulary.language.imbongu=Imbongu +controlledvocabulary.language.imonda=Imonda +controlledvocabulary.language.imotong=Imotong +controlledvocabulary.language.imroing=Imroing +controlledvocabulary.language.inabaknon=Inabaknon +controlledvocabulary.language.inapang=Inapang +controlledvocabulary.language.inari_sami=Inari Sami +controlledvocabulary.language.indian_sign_language=Indian Sign Language +controlledvocabulary.language.indo-portuguese=Indo-Portuguese controlledvocabulary.language.indonesian=Indonesian +controlledvocabulary.language.indonesian_bajau=Indonesian Bajau +controlledvocabulary.language.indonesian_sign_language=Indonesian Sign Language +controlledvocabulary.language.indri=Indri +controlledvocabulary.language.indus_kohistani=Indus Kohistani +controlledvocabulary.language.indus_valley_language=Indus Valley Language +controlledvocabulary.language.inebu_one=Inebu One +controlledvocabulary.language.ineseno=Ineseño +controlledvocabulary.language.inga=Inga +controlledvocabulary.language.ingrian=Ingrian +controlledvocabulary.language.ingush=Ingush +controlledvocabulary.language.inlaod_itneg=Inlaod Itneg +controlledvocabulary.language.innu=Innu +controlledvocabulary.language.inoke-yate=Inoke-Yate +controlledvocabulary.language.inonhan=Inonhan +controlledvocabulary.language.inor=Inor +controlledvocabulary.language.inpui_naga=Inpui Naga +controlledvocabulary.language.interglossa=Interglossa +controlledvocabulary.language.interlingua=Interlingua, Interlingua (International Auxiliary Language Association) controlledvocabulary.language.interlingue=Interlingue -controlledvocabulary.language.irish=Irish -controlledvocabulary.language.igbo=Igbo +controlledvocabulary.language.international_sign=International Sign +controlledvocabulary.language.interslavic=Interslavic +controlledvocabulary.language.intha=Intha +controlledvocabulary.language.inuinnaqtun=Inuinnaqtun +controlledvocabulary.language.inuit_sign_language=Inuit Sign Language +controlledvocabulary.language.inuktitut=Inuktitut controlledvocabulary.language.inupiaq=Inupiaq -controlledvocabulary.language.ido=Ido -controlledvocabulary.language.icelandic=Icelandic +controlledvocabulary.language.iowa-oto=Iowa-Oto +controlledvocabulary.language.ipalapa_amuzgo=Ipalapa Amuzgo +controlledvocabulary.language.ipiko=Ipiko +controlledvocabulary.language.ipili=Ipili +controlledvocabulary.language.ipulo=Ipulo +controlledvocabulary.language.iquito=Iquito +controlledvocabulary.language.ir=Ir +controlledvocabulary.language.iranian_persian=Iranian Persian +controlledvocabulary.language.iranian_sign_language=Iranian Sign Language +controlledvocabulary.language.iranun_(malaysia)=Iranun (Malaysia) +controlledvocabulary.language.iranun_(philippines)=Iranun (Philippines) +controlledvocabulary.language.iraqw=Iraqw +controlledvocabulary.language.irarutu=Irarutu +controlledvocabulary.language.iraya=Iraya +controlledvocabulary.language.iresim=Iresim +controlledvocabulary.language.irish=Irish +controlledvocabulary.language.irish_sign_language=Irish Sign Language +controlledvocabulary.language.irula=Irula +controlledvocabulary.language.irantxe=Irántxe +controlledvocabulary.language.isabi=Isabi +controlledvocabulary.language.isanzu=Isanzu +controlledvocabulary.language.isarog_agta=Isarog Agta +controlledvocabulary.language.isconahua=Isconahua +controlledvocabulary.language.isebe=Isebe +controlledvocabulary.language.isekiri=Isekiri +controlledvocabulary.language.ishkashimi=Ishkashimi +controlledvocabulary.language.isinai=Isinai +controlledvocabulary.language.isirawa=Isirawa +controlledvocabulary.language.island_carib=Island Carib +controlledvocabulary.language.islander_creole_english=Islander Creole English +controlledvocabulary.language.isnag=Isnag +controlledvocabulary.language.isoko=Isoko +controlledvocabulary.language.israeli_sign_language=Israeli Sign Language +controlledvocabulary.language.isthmus_mixe=Isthmus Mixe +controlledvocabulary.language.isthmus_zapotec=Isthmus Zapotec +controlledvocabulary.language.isthmus-cosoleacaque_nahuatl=Isthmus-Cosoleacaque Nahuatl +controlledvocabulary.language.isthmus-mecayapan_nahuatl=Isthmus-Mecayapan Nahuatl +controlledvocabulary.language.isthmus-pajapan_nahuatl=Isthmus-Pajapan Nahuatl +controlledvocabulary.language.istriot=Istriot +controlledvocabulary.language.istro_romanian=Istro Romanian +controlledvocabulary.language.isu_(fako_division)=Isu (Fako Division) +controlledvocabulary.language.isu_(menchum_division)=Isu (Menchum Division) controlledvocabulary.language.italian=Italian -controlledvocabulary.language.inuktitut=Inuktitut +controlledvocabulary.language.italian_sign_language=Italian Sign Language +controlledvocabulary.language.itawit=Itawit +controlledvocabulary.language.itelmen=Itelmen +controlledvocabulary.language.itene=Itene +controlledvocabulary.language.iteri=Iteri +controlledvocabulary.language.itik=Itik +controlledvocabulary.language.ito=Ito +controlledvocabulary.language.itonama=Itonama +controlledvocabulary.language.itu_mbon_uzo=Itu Mbon Uzo +controlledvocabulary.language.itundujia_mixtec=Itundujia Mixtec +controlledvocabulary.language.itza=Itzá +controlledvocabulary.language.iu_mien=Iu Mien +controlledvocabulary.language.ivatan=Ivatan +controlledvocabulary.language.ivbie_north-okpela-arhe=Ivbie North-Okpela-Arhe +controlledvocabulary.language.iwaidja=Iwaidja +controlledvocabulary.language.iwal=Iwal +controlledvocabulary.language.iwam=Iwam +controlledvocabulary.language.iwur=Iwur +controlledvocabulary.language.ixcatec=Ixcatec +controlledvocabulary.language.ixcatlan_mazatec=Ixcatlán Mazatec +controlledvocabulary.language.ixil=Ixil +controlledvocabulary.language.ixtayutla_mixtec=Ixtayutla Mixtec +controlledvocabulary.language.ixtenco_otomi=Ixtenco Otomi +controlledvocabulary.language.iyayu=Iyayu +controlledvocabulary.language.iyive=Iyive +controlledvocabulary.language.iyo=Iyo +controlledvocabulary.language.iyo'wujwa_chorote=Iyo'wujwa Chorote +controlledvocabulary.language.iyojwa'ja_chorote=Iyojwa'ja Chorote +controlledvocabulary.language.izere=Izere +controlledvocabulary.language.izii=Izii +controlledvocabulary.language.izon=Izon +controlledvocabulary.language.izora=Izora +controlledvocabulary.language.inapari=Iñapari +controlledvocabulary.language.jabuti=Jabutí +controlledvocabulary.language.jad=Jad +controlledvocabulary.language.jadgali=Jadgali +controlledvocabulary.language.jah_hut=Jah Hut +controlledvocabulary.language.jahanka=Jahanka +controlledvocabulary.language.jair_awyu=Jair Awyu +controlledvocabulary.language.jaitmatang=Jaitmatang +controlledvocabulary.language.jakati=Jakati +controlledvocabulary.language.jakattoe=Jakattoe +controlledvocabulary.language.jakun=Jakun +controlledvocabulary.language.jalapa_de_diaz_mazatec=Jalapa De Díaz Mazatec +controlledvocabulary.language.jalkunan=Jalkunan +controlledvocabulary.language.jamaican_country_sign_language=Jamaican Country Sign Language +controlledvocabulary.language.jamaican_creole_english=Jamaican Creole English +controlledvocabulary.language.jamaican_sign_language=Jamaican Sign Language +controlledvocabulary.language.jamamadi=Jamamadí +controlledvocabulary.language.jambi_malay=Jambi Malay +controlledvocabulary.language.jamiltepec_mixtec=Jamiltepec Mixtec +controlledvocabulary.language.jamsay_dogon=Jamsay Dogon +controlledvocabulary.language.jandai=Jandai +controlledvocabulary.language.jandavra=Jandavra +controlledvocabulary.language.jangkang=Jangkang +controlledvocabulary.language.jangshung=Jangshung +controlledvocabulary.language.janji=Janji controlledvocabulary.language.japanese=Japanese +controlledvocabulary.language.japanese_sign_language=Japanese Sign Language +controlledvocabulary.language.japreria=Japrería +controlledvocabulary.language.jaqaru=Jaqaru +controlledvocabulary.language.jara=Jara +controlledvocabulary.language.jarai=Jarai +controlledvocabulary.language.jarawa_(india)=Jarawa (India) +controlledvocabulary.language.jaru=Jaru +controlledvocabulary.language.jauja_wanca_quechua=Jauja Wanca Quechua +controlledvocabulary.language.jaunsari=Jaunsari controlledvocabulary.language.javanese=Javanese -controlledvocabulary.language.kalaallisut,_greenlandic=Kalaallisut, Greenlandic +controlledvocabulary.language.javindo=Javindo +controlledvocabulary.language.jawe=Jawe +controlledvocabulary.language.jawoyn=Jawoyn +controlledvocabulary.language.jaya=Jaya +controlledvocabulary.language.jebero=Jebero +controlledvocabulary.language.jeh=Jeh +controlledvocabulary.language.jehai=Jehai +controlledvocabulary.language.jejara_naga=Jejara Naga +controlledvocabulary.language.jejueo=Jejueo +controlledvocabulary.language.jemez=Jemez +controlledvocabulary.language.jenaama_bozo=Jenaama Bozo +controlledvocabulary.language.jennu_kurumba=Jennu Kurumba +controlledvocabulary.language.jere=Jere +controlledvocabulary.language.jeri_kuo=Jeri Kuo +controlledvocabulary.language.jerung=Jerung +controlledvocabulary.language.jewish_babylonian_aramaic_(ca._200-1200_ce)=Jewish Babylonian Aramaic (ca. 200-1200 CE) +controlledvocabulary.language.jewish_palestinian_aramaic=Jewish Palestinian Aramaic +controlledvocabulary.language.jhankot_sign_language=Jhankot Sign Language +controlledvocabulary.language.jiamao=Jiamao +controlledvocabulary.language.jiarong=Jiarong +controlledvocabulary.language.jiba=Jiba +controlledvocabulary.language.jibu=Jibu +controlledvocabulary.language.jicarilla_apache=Jicarilla Apache +controlledvocabulary.language.jiiddu=Jiiddu +controlledvocabulary.language.jilbe=Jilbe +controlledvocabulary.language.jilim=Jilim +controlledvocabulary.language.jimi_(cameroon)=Jimi (Cameroon) +controlledvocabulary.language.jimi_(nigeria)=Jimi (Nigeria) +controlledvocabulary.language.jina=Jina +controlledvocabulary.language.jingulu=Jingulu +controlledvocabulary.language.jinyu_chinese=Jinyu Chinese +controlledvocabulary.language.jiongnai_bunu=Jiongnai Bunu +controlledvocabulary.language.jirel=Jirel +controlledvocabulary.language.jiru=Jiru +controlledvocabulary.language.jita=Jita +controlledvocabulary.language.jju=Jju +controlledvocabulary.language.joba=Joba +controlledvocabulary.language.jofotek-bromnya=Jofotek-Bromnya +controlledvocabulary.language.jogi=Jogi +controlledvocabulary.language.jola-fonyi=Jola-Fonyi +controlledvocabulary.language.jola-kasa=Jola-Kasa +controlledvocabulary.language.jonkor_bourmataguil=Jonkor Bourmataguil +controlledvocabulary.language.jordanian_sign_language=Jordanian Sign Language +controlledvocabulary.language.jora=Jorá +controlledvocabulary.language.jowulu=Jowulu +controlledvocabulary.language.ju=Ju +controlledvocabulary.language.juang=Juang +controlledvocabulary.language.judeo-arabic=Judeo-Arabic +controlledvocabulary.language.judeo-berber=Judeo-Berber +controlledvocabulary.language.judeo-georgian=Judeo-Georgian +controlledvocabulary.language.judeo-iraqi_arabic=Judeo-Iraqi Arabic +controlledvocabulary.language.judeo-italian=Judeo-Italian +controlledvocabulary.language.judeo-moroccan_arabic=Judeo-Moroccan Arabic +controlledvocabulary.language.judeo-persian=Judeo-Persian +controlledvocabulary.language.judeo-tat=Judeo-Tat +controlledvocabulary.language.judeo-tripolitanian_arabic=Judeo-Tripolitanian Arabic +controlledvocabulary.language.judeo-yemeni_arabic=Judeo-Yemeni Arabic +controlledvocabulary.language.jukun_takum=Jukun Takum +controlledvocabulary.language.jumjum=Jumjum +controlledvocabulary.language.jumla_sign_language=Jumla Sign Language +controlledvocabulary.language.jumli=Jumli +controlledvocabulary.language.jungle_inga=Jungle Inga +controlledvocabulary.language.juquila_mixe=Juquila Mixe +controlledvocabulary.language.jur_modo=Jur Modo +controlledvocabulary.language.juray=Juray +controlledvocabulary.language.jurchen=Jurchen +controlledvocabulary.language.juruna=Jurúna +controlledvocabulary.language.jutish=Jutish +controlledvocabulary.language.juwal=Juwal +controlledvocabulary.language.juxtlahuaca_mixtec=Juxtlahuaca Mixtec +controlledvocabulary.language.juǀ'hoan=Juǀʼhoan +controlledvocabulary.language.jwira-pepesa=Jwira-Pepesa +controlledvocabulary.language.jerriais=Jèrriais +controlledvocabulary.language.juma=Júma +controlledvocabulary.language.k'iche'=K'iche' +controlledvocabulary.language.kaamba=Kaamba +controlledvocabulary.language.kaan=Kaan +controlledvocabulary.language.kaang_chin=Kaang Chin +controlledvocabulary.language.kaansa=Kaansa +controlledvocabulary.language.kaba=Kaba +controlledvocabulary.language.kabalai=Kabalai +controlledvocabulary.language.kabardian=Kabardian +controlledvocabulary.language.kabatei=Kabatei +controlledvocabulary.language.kabiye=Kabiyè +controlledvocabulary.language.kabola=Kabola +controlledvocabulary.language.kabore_one=Kabore One +controlledvocabulary.language.kabras=Kabras +controlledvocabulary.language.kaburi=Kaburi +controlledvocabulary.language.kabutra=Kabutra +controlledvocabulary.language.kabuverdianu=Kabuverdianu +controlledvocabulary.language.kabwa=Kabwa +controlledvocabulary.language.kabwari=Kabwari +controlledvocabulary.language.kabyle=Kabyle +controlledvocabulary.language.kachama-ganjule=Kachama-Ganjule +controlledvocabulary.language.kachari=Kachari +controlledvocabulary.language.kachhi=Kachhi +controlledvocabulary.language.kachi_koli=Kachi Koli +controlledvocabulary.language.kachin=Kachin +controlledvocabulary.language.kachok=Kachok +controlledvocabulary.language.kacipo-bale_suri=Kacipo-Bale Suri +controlledvocabulary.language.kadai=Kadai +controlledvocabulary.language.kadar=Kadar +controlledvocabulary.language.kadaru=Kadaru +controlledvocabulary.language.kadazan_dusun=Kadazan Dusun +controlledvocabulary.language.kadiweu=Kadiwéu +controlledvocabulary.language.kadu=Kadu +controlledvocabulary.language.kadung=Kadung +controlledvocabulary.language.kaduo=Kaduo +controlledvocabulary.language.kaeku=Kaeku +controlledvocabulary.language.kaera=Kaera +controlledvocabulary.language.kafa=Kafa +controlledvocabulary.language.kafoa=Kafoa +controlledvocabulary.language.kagan_kalagan=Kagan Kalagan +controlledvocabulary.language.kagate=Kagate +controlledvocabulary.language.kagayanen=Kagayanen +controlledvocabulary.language.kagoma=Kagoma +controlledvocabulary.language.kagoro=Kagoro +controlledvocabulary.language.kagulu=Kagulu +controlledvocabulary.language.kahe=Kahe +controlledvocabulary.language.kahua=Kahua +controlledvocabulary.language.kaian=Kaian +controlledvocabulary.language.kaibobo=Kaibobo +controlledvocabulary.language.kaidipang=Kaidipang +controlledvocabulary.language.kaiep=Kaiep +controlledvocabulary.language.kaikadi=Kaikadi +controlledvocabulary.language.kaikavian_literary_language=Kaikavian Literary Language +controlledvocabulary.language.kaike=Kaike +controlledvocabulary.language.kaimbulawa=Kaimbulawa +controlledvocabulary.language.kaimbe=Kaimbé +controlledvocabulary.language.kaingang=Kaingang +controlledvocabulary.language.kairak=Kairak +controlledvocabulary.language.kairiru=Kairiru +controlledvocabulary.language.kairui-midiki=Kairui-Midiki +controlledvocabulary.language.kais=Kais +controlledvocabulary.language.kaitag=Kaitag +controlledvocabulary.language.kaivi=Kaivi +controlledvocabulary.language.kaiwa=Kaiwá +controlledvocabulary.language.kaiy=Kaiy +controlledvocabulary.language.kajakse=Kajakse +controlledvocabulary.language.kajali=Kajali +controlledvocabulary.language.kajaman=Kajaman +controlledvocabulary.language.kakabai=Kakabai +controlledvocabulary.language.kakabe=Kakabe +controlledvocabulary.language.kakanda=Kakanda +controlledvocabulary.language.kaki_ae=Kaki Ae +controlledvocabulary.language.kako=Kako +controlledvocabulary.language.kakwa=Kakwa +controlledvocabulary.language.kala_lagaw_ya=Kala Lagaw Ya +controlledvocabulary.language.kalaamaya=Kalaamaya +controlledvocabulary.language.kalabakan=Kalabakan +controlledvocabulary.language.kalabari=Kalabari +controlledvocabulary.language.kalabra=Kalabra +controlledvocabulary.language.kalagan=Kalagan +controlledvocabulary.language.kalaktang_monpa=Kalaktang Monpa +controlledvocabulary.language.kalam=Kalam +controlledvocabulary.language.kalamse=Kalamsé +controlledvocabulary.language.kalanadi=Kalanadi +controlledvocabulary.language.kalanga=Kalanga +controlledvocabulary.language.kalanguya=Kalanguya +controlledvocabulary.language.kalao=Kalao +controlledvocabulary.language.kalapuya=Kalapuya +controlledvocabulary.language.kalarko=Kalarko +controlledvocabulary.language.kalasha=Kalasha +controlledvocabulary.language.kalenjin=Kalenjin +controlledvocabulary.language.kalispel-pend_d'oreille=Kalispel-Pend d'Oreille +controlledvocabulary.language.kalkoti=Kalkoti +controlledvocabulary.language.kalkutung=Kalkutung +controlledvocabulary.language.kalmyk=Kalmyk +controlledvocabulary.language.kalo_finnish_romani=Kalo Finnish Romani +controlledvocabulary.language.kalou=Kalou +controlledvocabulary.language.kaluli=Kaluli +controlledvocabulary.language.kalumpang=Kalumpang +controlledvocabulary.language.kam=Kam +controlledvocabulary.language.kamakan=Kamakan +controlledvocabulary.language.kamang=Kamang +controlledvocabulary.language.kamano=Kamano +controlledvocabulary.language.kamantan=Kamantan +controlledvocabulary.language.kamar=Kamar +controlledvocabulary.language.kamara=Kamara +controlledvocabulary.language.kamarian=Kamarian +controlledvocabulary.language.kamaru=Kamaru +controlledvocabulary.language.kamas=Kamas +controlledvocabulary.language.kamasa=Kamasa +controlledvocabulary.language.kamasau=Kamasau +controlledvocabulary.language.kamayo=Kamayo +controlledvocabulary.language.kamayura=Kamayurá +controlledvocabulary.language.kamba_(kenya)=Kamba (Kenya) +controlledvocabulary.language.kambaata=Kambaata +controlledvocabulary.language.kambaira=Kambaira +controlledvocabulary.language.kambera=Kambera +controlledvocabulary.language.kamberau=Kamberau +controlledvocabulary.language.kambiwa=Kambiwá +controlledvocabulary.language.kami_(nigeria)=Kami (Nigeria) +controlledvocabulary.language.kami_(tanzania)=Kami (Tanzania) +controlledvocabulary.language.kamo=Kamo +controlledvocabulary.language.kamoro=Kamoro +controlledvocabulary.language.kamu=Kamu +controlledvocabulary.language.kamula=Kamula +controlledvocabulary.language.kamviri=Kamviri +controlledvocabulary.language.kamwe=Kamwe +controlledvocabulary.language.kanakanabu=Kanakanabu +controlledvocabulary.language.kanamari=Kanamarí +controlledvocabulary.language.kanan=Kanan +controlledvocabulary.language.kanashi=Kanashi +controlledvocabulary.language.kanasi=Kanasi +controlledvocabulary.language.kanauji=Kanauji +controlledvocabulary.language.kandas=Kandas +controlledvocabulary.language.kandawo=Kandawo +controlledvocabulary.language.kande=Kande +controlledvocabulary.language.kanembu=Kanembu +controlledvocabulary.language.kang=Kang +controlledvocabulary.language.kanga=Kanga +controlledvocabulary.language.kangean=Kangean +controlledvocabulary.language.kanggape=Kanggape +controlledvocabulary.language.kangjia=Kangjia +controlledvocabulary.language.kango_(bas-uele_district)=Kango (Bas-Uélé District) +controlledvocabulary.language.kango_(tshopo_district)=Kango (Tshopo District) +controlledvocabulary.language.kangri=Kangri +controlledvocabulary.language.kaniet=Kaniet +controlledvocabulary.language.kanikkaran=Kanikkaran +controlledvocabulary.language.kaningdon-nindem=Kaningdon-Nindem +controlledvocabulary.language.kaningi=Kaningi +controlledvocabulary.language.kaningra=Kaningra +controlledvocabulary.language.kaninuwa=Kaninuwa +controlledvocabulary.language.kanite=Kanite +controlledvocabulary.language.kanjari=Kanjari +controlledvocabulary.language.kanju=Kanju +controlledvocabulary.language.kankanaey=Kankanaey controlledvocabulary.language.kannada=Kannada +controlledvocabulary.language.kannada_kurumba=Kannada Kurumba +controlledvocabulary.language.kanowit-tanjong_melanau=Kanowit-Tanjong Melanau +controlledvocabulary.language.kanoe=Kanoé +controlledvocabulary.language.kansa=Kansa +controlledvocabulary.language.kantosi=Kantosi +controlledvocabulary.language.kanu=Kanu +controlledvocabulary.language.kanufi=Kanufi controlledvocabulary.language.kanuri=Kanuri +controlledvocabulary.language.kanyok=Kanyok +controlledvocabulary.language.kao=Kao +controlledvocabulary.language.kaonde=Kaonde +controlledvocabulary.language.kap=Kap +controlledvocabulary.language.kapin=Kapin +controlledvocabulary.language.kapinawa=Kapinawá +controlledvocabulary.language.kapingamarangi=Kapingamarangi +controlledvocabulary.language.kapori=Kapori +controlledvocabulary.language.kapriman=Kapriman +controlledvocabulary.language.kaptiau=Kaptiau +controlledvocabulary.language.kapya=Kapya +controlledvocabulary.language.kaqchikel=Kaqchikel +controlledvocabulary.language.kara_(central_african_republic)=Kara (Central African Republic) +controlledvocabulary.language.kara_(korea)=Kara (Korea) +controlledvocabulary.language.kara_(papua_new_guinea)=Kara (Papua New Guinea) +controlledvocabulary.language.kara_(tanzania)=Kara (Tanzania) +controlledvocabulary.language.kara-kalpak=Kara-Kalpak +controlledvocabulary.language.karachay-balkar=Karachay-Balkar +controlledvocabulary.language.karagas=Karagas +controlledvocabulary.language.karaim=Karaim +controlledvocabulary.language.karajarri=Karajarri +controlledvocabulary.language.karaja=Karajá +controlledvocabulary.language.karakhanid=Karakhanid +controlledvocabulary.language.karami=Karami +controlledvocabulary.language.karamojong=Karamojong +controlledvocabulary.language.karang=Karang +controlledvocabulary.language.karanga=Karanga +controlledvocabulary.language.karankawa=Karankawa +controlledvocabulary.language.karao=Karao +controlledvocabulary.language.karas=Karas +controlledvocabulary.language.karata=Karata +controlledvocabulary.language.karawa=Karawa +controlledvocabulary.language.karbi=Karbi +controlledvocabulary.language.kare_(central_african_republic)=Kare (Central African Republic) +controlledvocabulary.language.kare_(papua_new_guinea)=Kare (Papua New Guinea) +controlledvocabulary.language.karekare=Karekare +controlledvocabulary.language.karelian=Karelian +controlledvocabulary.language.karenggapa=Karenggapa +controlledvocabulary.language.karey=Karey +controlledvocabulary.language.kari=Kari +controlledvocabulary.language.karingani=Karingani +controlledvocabulary.language.karipuna=Karipuna +controlledvocabulary.language.karipuna_creole_french=Karipúna Creole French +controlledvocabulary.language.kariri-xoco=Karirí-Xocó +controlledvocabulary.language.karitiana=Karitiâna +controlledvocabulary.language.kariya=Kariya +controlledvocabulary.language.kariyarra=Kariyarra +controlledvocabulary.language.karkar-yuri=Karkar-Yuri +controlledvocabulary.language.karkin=Karkin +controlledvocabulary.language.karko=Karko +controlledvocabulary.language.karnai=Karnai +controlledvocabulary.language.karo_(brazil)=Karo (Brazil) +controlledvocabulary.language.karo_(ethiopia)=Karo (Ethiopia) +controlledvocabulary.language.karok=Karok +controlledvocabulary.language.karon=Karon +controlledvocabulary.language.karon_dori=Karon Dori +controlledvocabulary.language.karore=Karore +controlledvocabulary.language.karuwali=Karuwali +controlledvocabulary.language.kasanga=Kasanga +controlledvocabulary.language.kasem=Kasem +controlledvocabulary.language.kashaya=Kashaya controlledvocabulary.language.kashmiri=Kashmiri +controlledvocabulary.language.kashubian=Kashubian +controlledvocabulary.language.kasiguranin=Kasiguranin +controlledvocabulary.language.kaska=Kaska +controlledvocabulary.language.kaskean=Kaskean +controlledvocabulary.language.kasua=Kasua +controlledvocabulary.language.katabaga=Katabaga +controlledvocabulary.language.katawixi=Katawixi +controlledvocabulary.language.katbol=Katbol +controlledvocabulary.language.katcha-kadugli-miri=Katcha-Kadugli-Miri +controlledvocabulary.language.kathoriya_tharu=Kathoriya Tharu +controlledvocabulary.language.kathu=Kathu +controlledvocabulary.language.kati=Kati +controlledvocabulary.language.katkari=Katkari +controlledvocabulary.language.katla=Katla +controlledvocabulary.language.kato=Kato +controlledvocabulary.language.katso=Katso +controlledvocabulary.language.katua=Katua +controlledvocabulary.language.katukina=Katukína +controlledvocabulary.language.kaulong=Kaulong +controlledvocabulary.language.kaur=Kaur +controlledvocabulary.language.kaure=Kaure +controlledvocabulary.language.kaurna=Kaurna +controlledvocabulary.language.kauwera=Kauwera +controlledvocabulary.language.kavalan=Kavalan +controlledvocabulary.language.kavet=Kavet +controlledvocabulary.language.kawacha=Kawacha +controlledvocabulary.language.kawaiisu=Kawaiisu +controlledvocabulary.language.kawe=Kawe +controlledvocabulary.language.kawi=Kawi +controlledvocabulary.language.kaxarari=Kaxararí +controlledvocabulary.language.kaxuiana=Kaxuiâna +controlledvocabulary.language.kayabi=Kayabí +controlledvocabulary.language.kayagar=Kayagar +controlledvocabulary.language.kayan=Kayan +controlledvocabulary.language.kayan_mahakam=Kayan Mahakam +controlledvocabulary.language.kayan_river_kayan=Kayan River Kayan +controlledvocabulary.language.kayapo=Kayapó +controlledvocabulary.language.kayardild=Kayardild +controlledvocabulary.language.kayaw=Kayaw +controlledvocabulary.language.kayeli=Kayeli +controlledvocabulary.language.kayong=Kayong +controlledvocabulary.language.kayort=Kayort +controlledvocabulary.language.kaytetye=Kaytetye +controlledvocabulary.language.kayupulau=Kayupulau controlledvocabulary.language.kazakh=Kazakh +controlledvocabulary.language.kazukuru=Kazukuru +controlledvocabulary.language.ke'o=Ke'o +controlledvocabulary.language.keak=Keak +controlledvocabulary.language.keapara=Keapara +controlledvocabulary.language.kedah_malay=Kedah Malay +controlledvocabulary.language.kedang=Kedang +controlledvocabulary.language.keder=Keder +controlledvocabulary.language.keerray-woorroong=Keerray-Woorroong +controlledvocabulary.language.kehu=Kehu +controlledvocabulary.language.kei=Kei +controlledvocabulary.language.keiga=Keiga +controlledvocabulary.language.kein=Kein +controlledvocabulary.language.keiyo=Keiyo +controlledvocabulary.language.kekchi=Kekchí +controlledvocabulary.language.kela_(democratic_republic_of_congo)=Kela (Democratic Republic of Congo) +controlledvocabulary.language.kela_(papua_new_guinea)=Kela (Papua New Guinea) +controlledvocabulary.language.kelabit=Kelabit +controlledvocabulary.language.kele_(democratic_republic_of_congo)=Kele (Democratic Republic of Congo) +controlledvocabulary.language.kele_(papua_new_guinea)=Kele (Papua New Guinea) +controlledvocabulary.language.keley-i_kallahan=Keley-I Kallahan +controlledvocabulary.language.keliko=Keliko +controlledvocabulary.language.kelo=Kelo +controlledvocabulary.language.kelon=Kelon +controlledvocabulary.language.kemak=Kemak +controlledvocabulary.language.kembayan=Kembayan +controlledvocabulary.language.kemberano=Kemberano +controlledvocabulary.language.kembra=Kembra +controlledvocabulary.language.kemedzung=Kemedzung +controlledvocabulary.language.kemi_sami=Kemi Sami +controlledvocabulary.language.kemiehua=Kemiehua +controlledvocabulary.language.kemtuik=Kemtuik +controlledvocabulary.language.kenaboi=Kenaboi +controlledvocabulary.language.kenati=Kenati +controlledvocabulary.language.kendayan=Kendayan +controlledvocabulary.language.kendeje=Kendeje +controlledvocabulary.language.kendem=Kendem +controlledvocabulary.language.kenga=Kenga +controlledvocabulary.language.keningau_murut=Keningau Murut +controlledvocabulary.language.keninjal=Keninjal +controlledvocabulary.language.kensiu=Kensiu +controlledvocabulary.language.kenswei_nsei=Kenswei Nsei +controlledvocabulary.language.kenyan_sign_language=Kenyan Sign Language +controlledvocabulary.language.kenyang=Kenyang +controlledvocabulary.language.kenyi=Kenyi +controlledvocabulary.language.kenzi=Kenzi +controlledvocabulary.language.keoru-ahia=Keoru-Ahia +controlledvocabulary.language.kepkiriwat=Kepkiriwát +controlledvocabulary.language.kepo'=Kepo' +controlledvocabulary.language.kera=Kera +controlledvocabulary.language.kerak=Kerak +controlledvocabulary.language.kereho=Kereho +controlledvocabulary.language.kerek=Kerek +controlledvocabulary.language.kerewe=Kerewe +controlledvocabulary.language.kerewo=Kerewo +controlledvocabulary.language.kerinci=Kerinci +controlledvocabulary.language.kesawai=Kesawai +controlledvocabulary.language.ket=Ket +controlledvocabulary.language.ketangalan=Ketangalan +controlledvocabulary.language.kete=Kete +controlledvocabulary.language.ketengban=Ketengban +controlledvocabulary.language.ketum=Ketum +controlledvocabulary.language.keyagana=Keyagana +controlledvocabulary.language.kgalagadi=Kgalagadi +controlledvocabulary.language.khah=Khah +controlledvocabulary.language.khakas=Khakas +controlledvocabulary.language.khalaj=Khalaj +controlledvocabulary.language.khaling=Khaling +controlledvocabulary.language.khamba=Khamba +controlledvocabulary.language.khamnigan_mongol=Khamnigan Mongol +controlledvocabulary.language.khams_tibetan=Khams Tibetan +controlledvocabulary.language.khamti=Khamti +controlledvocabulary.language.khamyang=Khamyang +controlledvocabulary.language.khana=Khana +controlledvocabulary.language.khandesi=Khandesi +controlledvocabulary.language.khanty=Khanty +controlledvocabulary.language.khao=Khao +controlledvocabulary.language.kharam_naga=Kharam Naga +controlledvocabulary.language.kharia=Kharia +controlledvocabulary.language.kharia_thar=Kharia Thar +controlledvocabulary.language.khasi=Khasi +controlledvocabulary.language.khayo=Khayo +controlledvocabulary.language.khazar=Khazar +controlledvocabulary.language.khe=Khe +controlledvocabulary.language.khehek=Khehek +controlledvocabulary.language.khengkha=Khengkha +controlledvocabulary.language.khetrani=Khetrani +controlledvocabulary.language.khezha_naga=Khezha Naga +controlledvocabulary.language.khiamniungan_naga=Khiamniungan Naga +controlledvocabulary.language.khinalugh=Khinalugh +controlledvocabulary.language.khirwar=Khirwar +controlledvocabulary.language.khisa=Khisa +controlledvocabulary.language.khlula=Khlula controlledvocabulary.language.khmer=Khmer +controlledvocabulary.language.khmu=Khmu +controlledvocabulary.language.kho'ini=Kho'ini +controlledvocabulary.language.khoekhoe=Khoekhoe +controlledvocabulary.language.khoibu_naga=Khoibu Naga +controlledvocabulary.language.kholok=Kholok +controlledvocabulary.language.khorasani_turkish=Khorasani Turkish +controlledvocabulary.language.khorezmian=Khorezmian +controlledvocabulary.language.khotanese=Khotanese +controlledvocabulary.language.khowar=Khowar +controlledvocabulary.language.khua=Khua +controlledvocabulary.language.khuen=Khuen +controlledvocabulary.language.khumi_chin=Khumi Chin +controlledvocabulary.language.khunsari=Khunsari +controlledvocabulary.language.khvarshi=Khvarshi +controlledvocabulary.language.khang=Kháng +controlledvocabulary.language.khun=Khün +controlledvocabulary.language.kibala=Kibala +controlledvocabulary.language.kibet=Kibet +controlledvocabulary.language.kibiri=Kibiri +controlledvocabulary.language.kickapoo=Kickapoo +controlledvocabulary.language.kija=Kija +controlledvocabulary.language.kikai=Kikai controlledvocabulary.language.kikuyu,_gikuyu=Kikuyu, Gikuyu +controlledvocabulary.language.kildin_sami=Kildin Sami +controlledvocabulary.language.kilivila=Kilivila +controlledvocabulary.language.kiliwa=Kiliwa +controlledvocabulary.language.kilmeri=Kilmeri +controlledvocabulary.language.kim=Kim +controlledvocabulary.language.kim_mun=Kim Mun +controlledvocabulary.language.kimaama=Kimaama +controlledvocabulary.language.kimaragang=Kimaragang +controlledvocabulary.language.kimbu=Kimbu +controlledvocabulary.language.kimbundu=Kimbundu +controlledvocabulary.language.kimki=Kimki +controlledvocabulary.language.kimre=Kimré +controlledvocabulary.language.kinabalian=Kinabalian +controlledvocabulary.language.kinalakna=Kinalakna +controlledvocabulary.language.kinamiging_manobo=Kinamiging Manobo +controlledvocabulary.language.kinaray-a=Kinaray-A +controlledvocabulary.language.kinga=Kinga +controlledvocabulary.language.kinnauri=Kinnauri +controlledvocabulary.language.kintaq=Kintaq +controlledvocabulary.language.kinuku=Kinuku controlledvocabulary.language.kinyarwanda=Kinyarwanda -controlledvocabulary.language.kyrgyz=Kyrgyz +controlledvocabulary.language.kioko=Kioko +controlledvocabulary.language.kiong=Kiong +controlledvocabulary.language.kiorr=Kiorr +controlledvocabulary.language.kiowa=Kiowa +controlledvocabulary.language.kiowa_apache=Kiowa Apache +controlledvocabulary.language.kipsigis=Kipsigis +controlledvocabulary.language.kiput=Kiput +controlledvocabulary.language.kir-balar=Kir-Balar +controlledvocabulary.language.kire=Kire +controlledvocabulary.language.kirghiz=Kirghiz, Kyrgyz +controlledvocabulary.language.kirike=Kirike +controlledvocabulary.language.kirikiri=Kirikiri +controlledvocabulary.language.kirmanjki_(individual_language)=Kirmanjki (individual language) +controlledvocabulary.language.kirya-konzəl=Kirya-Konzəl +controlledvocabulary.language.kis=Kis +controlledvocabulary.language.kisa=Kisa +controlledvocabulary.language.kisan=Kisan +controlledvocabulary.language.kisankasa=Kisankasa +controlledvocabulary.language.kisar=Kisar +controlledvocabulary.language.kisi=Kisi +controlledvocabulary.language.kistane=Kistane +controlledvocabulary.language.kita_maninkakan=Kita Maninkakan +controlledvocabulary.language.kitan=Kitan +controlledvocabulary.language.kitsai=Kitsai +controlledvocabulary.language.kituba_(congo)=Kituba (Congo) +controlledvocabulary.language.kituba_(democratic_republic_of_congo)=Kituba (Democratic Republic of Congo) +controlledvocabulary.language.kiunum=Kiunum +controlledvocabulary.language.kizamani=Kizamani +controlledvocabulary.language.kla-dan=Kla-Dan +controlledvocabulary.language.klamath-modoc=Klamath-Modoc +controlledvocabulary.language.klao=Klao +controlledvocabulary.language.klias_river_kadazan=Klias River Kadazan +controlledvocabulary.language.klingon=Klingon +controlledvocabulary.language.knaanic=Knaanic +controlledvocabulary.language.ko=Ko +controlledvocabulary.language.koalib=Koalib +controlledvocabulary.language.koasati=Koasati +controlledvocabulary.language.koba=Koba +controlledvocabulary.language.kobiana=Kobiana +controlledvocabulary.language.kobo=Kobo +controlledvocabulary.language.kobol=Kobol +controlledvocabulary.language.kobon=Kobon +controlledvocabulary.language.koch=Koch +controlledvocabulary.language.kochila_tharu=Kochila Tharu +controlledvocabulary.language.koda=Koda +controlledvocabulary.language.kodaku=Kodaku +controlledvocabulary.language.kodava=Kodava +controlledvocabulary.language.kodeoha=Kodeoha +controlledvocabulary.language.kodi=Kodi +controlledvocabulary.language.kodia=Kodia +controlledvocabulary.language.koenoem=Koenoem +controlledvocabulary.language.kofa=Kofa +controlledvocabulary.language.kofei=Kofei +controlledvocabulary.language.kofyar=Kofyar +controlledvocabulary.language.koguryo=Koguryo +controlledvocabulary.language.kohin=Kohin +controlledvocabulary.language.kohistani_shina=Kohistani Shina +controlledvocabulary.language.koho=Koho +controlledvocabulary.language.kohumono=Kohumono +controlledvocabulary.language.koi=Koi +controlledvocabulary.language.koireng=Koireng +controlledvocabulary.language.koitabu=Koitabu +controlledvocabulary.language.koiwat=Koiwat +controlledvocabulary.language.kok_borok=Kok Borok +controlledvocabulary.language.kok-nar=Kok-Nar +controlledvocabulary.language.kokata=Kokata +controlledvocabulary.language.koke=Koke +controlledvocabulary.language.koki_naga=Koki Naga +controlledvocabulary.language.koko_babangk=Koko Babangk +controlledvocabulary.language.kokoda=Kokoda +controlledvocabulary.language.kokola=Kokola +controlledvocabulary.language.kokota=Kokota +controlledvocabulary.language.kol_(bangladesh)=Kol (Bangladesh) +controlledvocabulary.language.kol_(cameroon)=Kol (Cameroon) +controlledvocabulary.language.kol_(papua_new_guinea)=Kol (Papua New Guinea) +controlledvocabulary.language.kola=Kola +controlledvocabulary.language.kolbila=Kolbila +controlledvocabulary.language.kolibugan_subanon=Kolibugan Subanon +controlledvocabulary.language.koluwawa=Koluwawa +controlledvocabulary.language.kom_(cameroon)=Kom (Cameroon) +controlledvocabulary.language.kom_(india)=Kom (India) +controlledvocabulary.language.koma=Koma +controlledvocabulary.language.komba=Komba +controlledvocabulary.language.kombai=Kombai +controlledvocabulary.language.kombio=Kombio +controlledvocabulary.language.komering=Komering controlledvocabulary.language.komi=Komi +controlledvocabulary.language.komi-permyak=Komi-Permyak +controlledvocabulary.language.komi-zyrian=Komi-Zyrian +controlledvocabulary.language.kominimung=Kominimung +controlledvocabulary.language.komo_(democratic_republic_of_congo)=Komo (Democratic Republic of Congo) +controlledvocabulary.language.komo_(sudan)=Komo (Sudan) +controlledvocabulary.language.komodo=Komodo +controlledvocabulary.language.kompane=Kompane +controlledvocabulary.language.komyandaret=Komyandaret +controlledvocabulary.language.kon_keu=Kon Keu +controlledvocabulary.language.konai=Konai +controlledvocabulary.language.konda=Konda +controlledvocabulary.language.konda-dora=Konda-Dora +controlledvocabulary.language.koneraw=Koneraw controlledvocabulary.language.kongo=Kongo +controlledvocabulary.language.konkani_(individual_language)=Konkani (individual language) +controlledvocabulary.language.konkani_(macrolanguage)=Konkani (macrolanguage) +controlledvocabulary.language.konkomba=Konkomba +controlledvocabulary.language.konni=Konni +controlledvocabulary.language.kono_(guinea)=Kono (Guinea) +controlledvocabulary.language.kono_(nigeria)=Kono (Nigeria) +controlledvocabulary.language.kono_(sierra_leone)=Kono (Sierra Leone) +controlledvocabulary.language.konomala=Konomala +controlledvocabulary.language.konongo=Konongo +controlledvocabulary.language.konso=Konso +controlledvocabulary.language.konyak_naga=Konyak Naga +controlledvocabulary.language.konyanka_maninka=Konyanka Maninka +controlledvocabulary.language.konzo=Konzo +controlledvocabulary.language.koongo=Koongo +controlledvocabulary.language.koonzime=Koonzime +controlledvocabulary.language.koorete=Koorete +controlledvocabulary.language.kopar=Kopar +controlledvocabulary.language.kopkaka=Kopkaka +controlledvocabulary.language.korafe-yegha=Korafe-Yegha +controlledvocabulary.language.korak=Korak +controlledvocabulary.language.korana=Korana +controlledvocabulary.language.korandje=Korandje controlledvocabulary.language.korean=Korean +controlledvocabulary.language.korean_sign_language=Korean Sign Language +controlledvocabulary.language.koreguaje=Koreguaje +controlledvocabulary.language.koresh-e_rostam=Koresh-e Rostam +controlledvocabulary.language.korku=Korku +controlledvocabulary.language.korlai_creole_portuguese=Korlai Creole Portuguese +controlledvocabulary.language.koro_(cote_d'ivoire)=Koro (Côte d'Ivoire) +controlledvocabulary.language.koro_(india)=Koro (India) +controlledvocabulary.language.koro_(papua_new_guinea)=Koro (Papua New Guinea) +controlledvocabulary.language.koro_(vanuatu)=Koro (Vanuatu) +controlledvocabulary.language.koro_nulu=Koro Nulu +controlledvocabulary.language.koro_wachi=Koro Wachi +controlledvocabulary.language.koro_zuba=Koro Zuba +controlledvocabulary.language.koromfe=Koromfé +controlledvocabulary.language.koromira=Koromira +controlledvocabulary.language.koronadal_blaan=Koronadal Blaan +controlledvocabulary.language.koroni=Koroni +controlledvocabulary.language.koropo=Koropó +controlledvocabulary.language.koroshi=Koroshi +controlledvocabulary.language.korowai=Korowai +controlledvocabulary.language.korra_koraga=Korra Koraga +controlledvocabulary.language.korubo=Korubo +controlledvocabulary.language.korupun-sela=Korupun-Sela +controlledvocabulary.language.korwa=Korwa +controlledvocabulary.language.koryak=Koryak +controlledvocabulary.language.kosadle=Kosadle +controlledvocabulary.language.kosarek_yale=Kosarek Yale +controlledvocabulary.language.kosena=Kosena +controlledvocabulary.language.koshin=Koshin +controlledvocabulary.language.kosraean=Kosraean +controlledvocabulary.language.kota_(gabon)=Kota (Gabon) +controlledvocabulary.language.kota_(india)=Kota (India) +controlledvocabulary.language.kota_bangun_kutai_malay=Kota Bangun Kutai Malay +controlledvocabulary.language.kota_marudu_talantang=Kota Marudu Talantang +controlledvocabulary.language.kotafon_gbe=Kotafon Gbe +controlledvocabulary.language.kotava=Kotava +controlledvocabulary.language.koti=Koti +controlledvocabulary.language.kott=Kott +controlledvocabulary.language.kou=Kou +controlledvocabulary.language.kouya=Kouya +controlledvocabulary.language.kovai=Kovai +controlledvocabulary.language.kove=Kove +controlledvocabulary.language.kowaki=Kowaki +controlledvocabulary.language.kowiai=Kowiai +controlledvocabulary.language.koy_sanjaq_surat=Koy Sanjaq Surat +controlledvocabulary.language.koya=Koya +controlledvocabulary.language.koyaga=Koyaga +controlledvocabulary.language.koyo=Koyo +controlledvocabulary.language.koyra_chiini_songhay=Koyra Chiini Songhay +controlledvocabulary.language.koyraboro_senni_songhai=Koyraboro Senni Songhai +controlledvocabulary.language.koyukon=Koyukon +controlledvocabulary.language.kpagua=Kpagua +controlledvocabulary.language.kpala=Kpala +controlledvocabulary.language.kpan=Kpan +controlledvocabulary.language.kpasam=Kpasam +controlledvocabulary.language.kpati=Kpati +controlledvocabulary.language.kpatili=Kpatili +controlledvocabulary.language.kpeego=Kpeego +controlledvocabulary.language.kpelle=Kpelle +controlledvocabulary.language.kpessi=Kpessi +controlledvocabulary.language.kplang=Kplang +controlledvocabulary.language.krache=Krache +controlledvocabulary.language.kraho=Krahô +controlledvocabulary.language.kraol=Kraol +controlledvocabulary.language.krenak=Krenak +controlledvocabulary.language.krevinian=Krevinian +controlledvocabulary.language.kreye=Kreye +controlledvocabulary.language.kriang=Kriang +controlledvocabulary.language.krikati-timbira=Krikati-Timbira +controlledvocabulary.language.krio=Krio +controlledvocabulary.language.kriol=Kriol +controlledvocabulary.language.krisa=Krisa +controlledvocabulary.language.krobu=Krobu +controlledvocabulary.language.krongo=Krongo +controlledvocabulary.language.krung=Krung +controlledvocabulary.language.krymchak=Krymchak +controlledvocabulary.language.kryts=Kryts +controlledvocabulary.language.kua=Kua +controlledvocabulary.language.kua-nsi=Kua-nsi +controlledvocabulary.language.kuamasi=Kuamasi +controlledvocabulary.language.kuan=Kuan +controlledvocabulary.language.kuanhua=Kuanhua +controlledvocabulary.language.kuanua=Kuanua +controlledvocabulary.language.kuanyama,_kwanyama=Kuanyama, Kwanyama +controlledvocabulary.language.kubachi=Kubachi +controlledvocabulary.language.kube=Kube +controlledvocabulary.language.kubi=Kubi +controlledvocabulary.language.kubo=Kubo +controlledvocabulary.language.kubu=Kubu +controlledvocabulary.language.kucong=Kucong +controlledvocabulary.language.kudiya=Kudiya +controlledvocabulary.language.kudmali=Kudmali +controlledvocabulary.language.kudu-camo=Kudu-Camo +controlledvocabulary.language.kufr_qassem_sign_language_(kqsl)=Kufr Qassem Sign Language (KQSL) +controlledvocabulary.language.kugama=Kugama +controlledvocabulary.language.kugbo=Kugbo +controlledvocabulary.language.kugu-muminh=Kugu-Muminh +controlledvocabulary.language.kui_(india)=Kui (India) +controlledvocabulary.language.kui_(indonesia)=Kui (Indonesia) +controlledvocabulary.language.kuijau=Kuijau +controlledvocabulary.language.kuikuro-kalapalo=Kuikúro-Kalapálo +controlledvocabulary.language.kujarge=Kujarge +controlledvocabulary.language.kuk=Kuk +controlledvocabulary.language.kukatja=Kukatja +controlledvocabulary.language.kuke=Kuke +controlledvocabulary.language.kukele=Kukele +controlledvocabulary.language.kukna=Kukna +controlledvocabulary.language.kuku=Kuku +controlledvocabulary.language.kuku-mangk=Kuku-Mangk +controlledvocabulary.language.kuku-mu'inh=Kuku-Mu'inh +controlledvocabulary.language.kuku-ugbanh=Kuku-Ugbanh +controlledvocabulary.language.kuku-uwanh=Kuku-Uwanh +controlledvocabulary.language.kuku-yalanji=Kuku-Yalanji +controlledvocabulary.language.kula=Kula +controlledvocabulary.language.kulere=Kulere +controlledvocabulary.language.kulfa=Kulfa +controlledvocabulary.language.kulina_pano=Kulina Pano +controlledvocabulary.language.kulisusu=Kulisusu +controlledvocabulary.language.kullu_pahari=Kullu Pahari +controlledvocabulary.language.kulon=Kulon +controlledvocabulary.language.kulung_(nepal)=Kulung (Nepal) +controlledvocabulary.language.kulung_(nigeria)=Kulung (Nigeria) +controlledvocabulary.language.kumalu=Kumalu +controlledvocabulary.language.kumam=Kumam +controlledvocabulary.language.kuman_(papua_new_guinea)=Kuman (Papua New Guinea) +controlledvocabulary.language.kuman_(russia)=Kuman (Russia) +controlledvocabulary.language.kumaoni=Kumaoni +controlledvocabulary.language.kumarbhag_paharia=Kumarbhag Paharia +controlledvocabulary.language.kumba=Kumba +controlledvocabulary.language.kumbainggar=Kumbainggar +controlledvocabulary.language.kumbaran=Kumbaran +controlledvocabulary.language.kumbewaha=Kumbewaha +controlledvocabulary.language.kumhali=Kumhali +controlledvocabulary.language.kumiai=Kumiai +controlledvocabulary.language.kumukio=Kumukio +controlledvocabulary.language.kumyk=Kumyk +controlledvocabulary.language.kumzari=Kumzari +controlledvocabulary.language.kunama=Kunama +controlledvocabulary.language.kunbarlang=Kunbarlang +controlledvocabulary.language.kunda=Kunda +controlledvocabulary.language.kundal_shahi=Kundal Shahi +controlledvocabulary.language.kunduvadi=Kunduvadi +controlledvocabulary.language.kung=Kung +controlledvocabulary.language.kung-ekoka=Kung-Ekoka +controlledvocabulary.language.kungarakany=Kungarakany +controlledvocabulary.language.kungardutyi=Kungardutyi +controlledvocabulary.language.kunggari=Kunggari +controlledvocabulary.language.kungkari=Kungkari +controlledvocabulary.language.kuni=Kuni +controlledvocabulary.language.kuni-boazi=Kuni-Boazi +controlledvocabulary.language.kunigami=Kunigami +controlledvocabulary.language.kunimaipa=Kunimaipa +controlledvocabulary.language.kunja=Kunja +controlledvocabulary.language.kunjen=Kunjen +controlledvocabulary.language.kunyi=Kunyi +controlledvocabulary.language.kunza=Kunza +controlledvocabulary.language.kuo=Kuo +controlledvocabulary.language.kuot=Kuot +controlledvocabulary.language.kupa=Kupa +controlledvocabulary.language.kupang_malay=Kupang Malay +controlledvocabulary.language.kupia=Kupia +controlledvocabulary.language.kupsabiny=Kupsabiny +controlledvocabulary.language.kur=Kur +controlledvocabulary.language.kura_ede_nago=Kura Ede Nago +controlledvocabulary.language.kurama=Kurama +controlledvocabulary.language.kuranko=Kuranko controlledvocabulary.language.kurdish=Kurdish -controlledvocabulary.language.kwanyama,_kuanyama=Kwanyama, Kuanyama +controlledvocabulary.language.kuri=Kuri +controlledvocabulary.language.kuria=Kuria +controlledvocabulary.language.kurichiya=Kurichiya +controlledvocabulary.language.kurmukar=Kurmukar +controlledvocabulary.language.kurnai=Kurnai +controlledvocabulary.language.kurrama=Kurrama +controlledvocabulary.language.kurti=Kurti +controlledvocabulary.language.kurtokha=Kurtokha +controlledvocabulary.language.kurudu=Kurudu +controlledvocabulary.language.kurukh=Kurukh +controlledvocabulary.language.kuruaya=Kuruáya +controlledvocabulary.language.kusaal=Kusaal +controlledvocabulary.language.kusaghe=Kusaghe +controlledvocabulary.language.kushi=Kushi +controlledvocabulary.language.kusu=Kusu +controlledvocabulary.language.kusunda=Kusunda +controlledvocabulary.language.kutenai=Kutenai +controlledvocabulary.language.kutep=Kutep +controlledvocabulary.language.kuthant=Kuthant +controlledvocabulary.language.kutong=Kutong +controlledvocabulary.language.kutto=Kutto +controlledvocabulary.language.kutu=Kutu +controlledvocabulary.language.kuturmi=Kuturmi +controlledvocabulary.language.kuuk_thaayorre=Kuuk Thaayorre +controlledvocabulary.language.kuuk-yak=Kuuk-Yak +controlledvocabulary.language.kuuku-ya'u=Kuuku-Ya'u +controlledvocabulary.language.kuvale=Kuvale +controlledvocabulary.language.kuvi=Kuvi +controlledvocabulary.language.kuwaa=Kuwaa +controlledvocabulary.language.kuwaataay=Kuwaataay +controlledvocabulary.language.kuwema=Kuwema +controlledvocabulary.language.kuy=Kuy +controlledvocabulary.language.kven_finnish=Kven Finnish +controlledvocabulary.language.kw'adza=Kw'adza +controlledvocabulary.language.kwa=Kwa +controlledvocabulary.language.kwa'=Kwa' +controlledvocabulary.language.kwaami=Kwaami +controlledvocabulary.language.kwadi=Kwadi +controlledvocabulary.language.kwaio=Kwaio +controlledvocabulary.language.kwaja=Kwaja +controlledvocabulary.language.kwakiutl=Kwakiutl +controlledvocabulary.language.kwakum=Kwakum +controlledvocabulary.language.kwalhioqua-tlatskanai=Kwalhioqua-Tlatskanai +controlledvocabulary.language.kwama=Kwama +controlledvocabulary.language.kwambi=Kwambi +controlledvocabulary.language.kwamera=Kwamera +controlledvocabulary.language.kwami=Kwami +controlledvocabulary.language.kwamtim_one=Kwamtim One +controlledvocabulary.language.kwandu=Kwandu +controlledvocabulary.language.kwang=Kwang +controlledvocabulary.language.kwanga=Kwanga +controlledvocabulary.language.kwangali=Kwangali +controlledvocabulary.language.kwanja=Kwanja +controlledvocabulary.language.kwara'ae=Kwara'ae +controlledvocabulary.language.kwasio=Kwasio +controlledvocabulary.language.kwaya=Kwaya +controlledvocabulary.language.kwaza=Kwaza +controlledvocabulary.language.kwegu=Kwegu +controlledvocabulary.language.kwer=Kwer +controlledvocabulary.language.kwerba=Kwerba +controlledvocabulary.language.kwerba_mamberamo=Kwerba Mamberamo +controlledvocabulary.language.kwere=Kwere +controlledvocabulary.language.kwerisa=Kwerisa +controlledvocabulary.language.kwese=Kwese +controlledvocabulary.language.kwesten=Kwesten +controlledvocabulary.language.kwini=Kwini +controlledvocabulary.language.kwinsu=Kwinsu +controlledvocabulary.language.kwinti=Kwinti +controlledvocabulary.language.kwoma=Kwoma +controlledvocabulary.language.kwomtari=Kwomtari +controlledvocabulary.language.kxoe=Kxoe +controlledvocabulary.language.kyak=Kyak +controlledvocabulary.language.kyaka=Kyaka +controlledvocabulary.language.kyan-karyaw_naga=Kyan-Karyaw Naga +controlledvocabulary.language.kyanga=Kyanga +controlledvocabulary.language.kyenele=Kyenele +controlledvocabulary.language.kyerung=Kyerung +controlledvocabulary.language.kate=Kâte +controlledvocabulary.language.kele=Kélé +controlledvocabulary.language.kolsch=Kölsch +controlledvocabulary.language.kɛlɛngaxo_bozo=Kɛlɛngaxo Bozo +controlledvocabulary.language.la'bi=La'bi +controlledvocabulary.language.laal=Laal +controlledvocabulary.language.laari=Laari +controlledvocabulary.language.laarim=Laarim +controlledvocabulary.language.laba=Laba +controlledvocabulary.language.label=Label +controlledvocabulary.language.labir=Labir +controlledvocabulary.language.labo=Labo +controlledvocabulary.language.labo_phowa=Labo Phowa +controlledvocabulary.language.labu=Labu +controlledvocabulary.language.labuk-kinabatangan_kadazan=Labuk-Kinabatangan Kadazan +controlledvocabulary.language.lacandon=Lacandon +controlledvocabulary.language.lachi=Lachi +controlledvocabulary.language.lachiguiri_zapotec=Lachiguiri Zapotec +controlledvocabulary.language.lachixio_zapotec=Lachixío Zapotec +controlledvocabulary.language.ladakhi=Ladakhi +controlledvocabulary.language.ladin=Ladin +controlledvocabulary.language.ladino=Ladino +controlledvocabulary.language.ladji_ladji=Ladji Ladji +controlledvocabulary.language.laeko-libuat=Laeko-Libuat +controlledvocabulary.language.lafofa=Lafofa +controlledvocabulary.language.laghu=Laghu +controlledvocabulary.language.laghuu=Laghuu +controlledvocabulary.language.lagwan=Lagwan +controlledvocabulary.language.laha_(indonesia)=Laha (Indonesia) +controlledvocabulary.language.laha_(viet_nam)=Laha (Viet Nam) +controlledvocabulary.language.lahanan=Lahanan +controlledvocabulary.language.lahnda=Lahnda +controlledvocabulary.language.lahta_karen=Lahta Karen +controlledvocabulary.language.lahu=Lahu +controlledvocabulary.language.lahu_shi=Lahu Shi +controlledvocabulary.language.lahul_lohar=Lahul Lohar +controlledvocabulary.language.laimbue=Laimbue +controlledvocabulary.language.laitu_chin=Laitu Chin +controlledvocabulary.language.laiyolo=Laiyolo +controlledvocabulary.language.lak=Lak +controlledvocabulary.language.laka_(chad)=Laka (Chad) +controlledvocabulary.language.lakalei=Lakalei +controlledvocabulary.language.lake_miwok=Lake Miwok +controlledvocabulary.language.lakha=Lakha +controlledvocabulary.language.laki=Laki +controlledvocabulary.language.lakkia=Lakkia +controlledvocabulary.language.lakon=Lakon +controlledvocabulary.language.lakonde=Lakondê +controlledvocabulary.language.lakota=Lakota +controlledvocabulary.language.lakota_dida=Lakota Dida +controlledvocabulary.language.lakurumau=Lakurumau +controlledvocabulary.language.lala=Lala +controlledvocabulary.language.lala-bisa=Lala-Bisa +controlledvocabulary.language.lala-roba=Lala-Roba +controlledvocabulary.language.lalana_chinantec=Lalana Chinantec +controlledvocabulary.language.lalia=Lalia +controlledvocabulary.language.lama_(togo)=Lama (Togo) +controlledvocabulary.language.lama_bai=Lama Bai +controlledvocabulary.language.lamaholot=Lamaholot +controlledvocabulary.language.lamalama=Lamalama +controlledvocabulary.language.lamalera=Lamalera +controlledvocabulary.language.lamang=Lamang +controlledvocabulary.language.lamatuka=Lamatuka +controlledvocabulary.language.lamba=Lamba +controlledvocabulary.language.lambadi=Lambadi +controlledvocabulary.language.lambayeque_quechua=Lambayeque Quechua +controlledvocabulary.language.lambichhong=Lambichhong +controlledvocabulary.language.lamboya=Lamboya +controlledvocabulary.language.lambya=Lambya +controlledvocabulary.language.lame=Lame +controlledvocabulary.language.lamenu=Lamenu +controlledvocabulary.language.lamja-dengsa-tola=Lamja-Dengsa-Tola +controlledvocabulary.language.lamkang=Lamkang +controlledvocabulary.language.lamma=Lamma +controlledvocabulary.language.lamnso'=Lamnso' +controlledvocabulary.language.lamogai=Lamogai +controlledvocabulary.language.lampung_api=Lampung Api +controlledvocabulary.language.lampung_nyo=Lampung Nyo +controlledvocabulary.language.lamu=Lamu +controlledvocabulary.language.lanas_lobu=Lanas Lobu +controlledvocabulary.language.landoma=Landoma +controlledvocabulary.language.lang'e=Lang'e +controlledvocabulary.language.langam=Langam +controlledvocabulary.language.langbashe=Langbashe +controlledvocabulary.language.langnian_buyang=Langnian Buyang +controlledvocabulary.language.lango_(south_sudan)=Lango (South Sudan) +controlledvocabulary.language.lango_(uganda)=Lango (Uganda) +controlledvocabulary.language.langobardic=Langobardic +controlledvocabulary.language.langue_des_signes_de_belgique_francophone=Langue des signes de Belgique Francophone +controlledvocabulary.language.lanima=Lanima +controlledvocabulary.language.lanoh=Lanoh +controlledvocabulary.language.lao=Lao +controlledvocabulary.language.lao_naga=Lao Naga +controlledvocabulary.language.laomian=Laomian +controlledvocabulary.language.laopang=Laopang +controlledvocabulary.language.laos_sign_language=Laos Sign Language +controlledvocabulary.language.lapaguia-guivini_zapotec=Lapaguía-Guivini Zapotec +controlledvocabulary.language.laragia=Laragia +controlledvocabulary.language.larantuka_malay=Larantuka Malay +controlledvocabulary.language.lardil=Lardil +controlledvocabulary.language.larevat=Larevat +controlledvocabulary.language.large_flowery_miao=Large Flowery Miao +controlledvocabulary.language.lari=Lari +controlledvocabulary.language.larike-wakasihu=Larike-Wakasihu +controlledvocabulary.language.laro=Laro +controlledvocabulary.language.larteh=Larteh +controlledvocabulary.language.laru=Laru +controlledvocabulary.language.las_delicias_zapotec=Las Delicias Zapotec +controlledvocabulary.language.lasalimu=Lasalimu +controlledvocabulary.language.lasgerdi=Lasgerdi +controlledvocabulary.language.lashi=Lashi +controlledvocabulary.language.lasi=Lasi +controlledvocabulary.language.late_middle_chinese=Late Middle Chinese +controlledvocabulary.language.latgalian=Latgalian controlledvocabulary.language.latin=Latin -controlledvocabulary.language.luxembourgish,_letzeburgesch=Luxembourgish, Letzeburgesch -controlledvocabulary.language.ganda=Ganda -controlledvocabulary.language.limburgish,_limburgan,_limburger=Limburgish, Limburgan, Limburger +controlledvocabulary.language.latu=Latu +controlledvocabulary.language.latunde=Latundê +controlledvocabulary.language.latvian=Latvian +controlledvocabulary.language.latvian_sign_language=Latvian Sign Language +controlledvocabulary.language.lau=Lau +controlledvocabulary.language.laua=Laua +controlledvocabulary.language.lauan=Lauan +controlledvocabulary.language.lauje=Lauje +controlledvocabulary.language.laura=Laura +controlledvocabulary.language.laurentian=Laurentian +controlledvocabulary.language.lautu_chin=Lautu Chin +controlledvocabulary.language.lavatbura-lamusong=Lavatbura-Lamusong +controlledvocabulary.language.laven=Laven +controlledvocabulary.language.lavi=Lavi +controlledvocabulary.language.lavukaleve=Lavukaleve +controlledvocabulary.language.lawangan=Lawangan +controlledvocabulary.language.lawu=Lawu +controlledvocabulary.language.lawunuia=Lawunuia +controlledvocabulary.language.layakha=Layakha +controlledvocabulary.language.laz=Laz +controlledvocabulary.language.lealao_chinantec=Lealao Chinantec +controlledvocabulary.language.leco=Leco +controlledvocabulary.language.ledo_kaili=Ledo Kaili +controlledvocabulary.language.leelau=Leelau +controlledvocabulary.language.lefa=Lefa +controlledvocabulary.language.lega-mwenga=Lega-Mwenga +controlledvocabulary.language.lega-shabunda=Lega-Shabunda +controlledvocabulary.language.legbo=Legbo +controlledvocabulary.language.legenyem=Legenyem +controlledvocabulary.language.lehali=Lehali +controlledvocabulary.language.lehalurup=Lehalurup +controlledvocabulary.language.lehar=Lehar +controlledvocabulary.language.leinong_naga=Leinong Naga +controlledvocabulary.language.leipon=Leipon +controlledvocabulary.language.lelak=Lelak +controlledvocabulary.language.lele_(chad)=Lele (Chad) +controlledvocabulary.language.lele_(democratic_republic_of_congo)=Lele (Democratic Republic of Congo) +controlledvocabulary.language.lele_(guinea)=Lele (Guinea) +controlledvocabulary.language.lele_(papua_new_guinea)=Lele (Papua New Guinea) +controlledvocabulary.language.lelemi=Lelemi +controlledvocabulary.language.lelepa=Lelepa +controlledvocabulary.language.lembena=Lembena +controlledvocabulary.language.lemerig=Lemerig +controlledvocabulary.language.lemio=Lemio +controlledvocabulary.language.lemnian=Lemnian +controlledvocabulary.language.lemolang=Lemolang +controlledvocabulary.language.lemoro=Lemoro +controlledvocabulary.language.lenakel=Lenakel +controlledvocabulary.language.lenca=Lenca +controlledvocabulary.language.lendu=Lendu +controlledvocabulary.language.lengilu=Lengilu +controlledvocabulary.language.lengo=Lengo +controlledvocabulary.language.lengola=Lengola +controlledvocabulary.language.leningitij=Leningitij +controlledvocabulary.language.lenje=Lenje +controlledvocabulary.language.lenkau=Lenkau +controlledvocabulary.language.lenyima=Lenyima +controlledvocabulary.language.lepcha=Lepcha +controlledvocabulary.language.lepki=Lepki +controlledvocabulary.language.lepontic=Lepontic +controlledvocabulary.language.lere=Lere +controlledvocabulary.language.lese=Lese +controlledvocabulary.language.lesing-gelimi=Lesing-Gelimi +controlledvocabulary.language.letemboi=Letemboi +controlledvocabulary.language.leti_(cameroon)=Leti (Cameroon) +controlledvocabulary.language.leti_(indonesia)=Leti (Indonesia) +controlledvocabulary.language.levantine_arabic=Levantine Arabic +controlledvocabulary.language.levuka=Levuka +controlledvocabulary.language.lewo=Lewo +controlledvocabulary.language.lewo_eleng=Lewo Eleng +controlledvocabulary.language.lewotobi=Lewotobi +controlledvocabulary.language.leyigha=Leyigha +controlledvocabulary.language.lezghian=Lezghian +controlledvocabulary.language.lhokpu=Lhokpu +controlledvocabulary.language.lhomi=Lhomi +controlledvocabulary.language.li'o=Li'o +controlledvocabulary.language.liabuku=Liabuku +controlledvocabulary.language.liana-seti=Liana-Seti +controlledvocabulary.language.liangmai_naga=Liangmai Naga +controlledvocabulary.language.lianshan_zhuang=Lianshan Zhuang +controlledvocabulary.language.liberia_kpelle=Liberia Kpelle +controlledvocabulary.language.liberian_english=Liberian English +controlledvocabulary.language.libido=Libido +controlledvocabulary.language.libinza=Libinza +controlledvocabulary.language.libon_bikol=Libon Bikol +controlledvocabulary.language.liburnian=Liburnian +controlledvocabulary.language.libyan_arabic=Libyan Arabic +controlledvocabulary.language.libyan_sign_language=Libyan Sign Language +controlledvocabulary.language.lidzonka=Lidzonka +controlledvocabulary.language.ligbi=Ligbi +controlledvocabulary.language.ligenza=Ligenza +controlledvocabulary.language.ligurian=Ligurian +controlledvocabulary.language.ligurian_(ancient)=Ligurian (Ancient) +controlledvocabulary.language.lihir=Lihir +controlledvocabulary.language.lijili=Lijili +controlledvocabulary.language.lika=Lika +controlledvocabulary.language.liki=Liki +controlledvocabulary.language.likila=Likila +controlledvocabulary.language.likuba=Likuba +controlledvocabulary.language.likum=Likum +controlledvocabulary.language.likwala=Likwala +controlledvocabulary.language.lilau=Lilau +controlledvocabulary.language.lillooet=Lillooet +controlledvocabulary.language.limassa=Limassa +controlledvocabulary.language.limbu=Limbu +controlledvocabulary.language.limbum=Limbum +controlledvocabulary.language.limburgan,_limburger,_limburgish=Limburgan, Limburger, Limburgish +controlledvocabulary.language.limi=Limi +controlledvocabulary.language.limilngan=Limilngan +controlledvocabulary.language.limos_kalinga=Limos Kalinga +controlledvocabulary.language.linear_a=Linear A controlledvocabulary.language.lingala=Lingala -controlledvocabulary.language.lao=Lao +controlledvocabulary.language.lingao=Lingao +controlledvocabulary.language.lingarak=Lingarak +controlledvocabulary.language.lingua_franca=Lingua Franca +controlledvocabulary.language.lingua_franca_nova=Lingua Franca Nova +controlledvocabulary.language.lipan_apache=Lipan Apache +controlledvocabulary.language.lipo=Lipo +controlledvocabulary.language.lisabata-nuniali=Lisabata-Nuniali +controlledvocabulary.language.lisela=Lisela +controlledvocabulary.language.lish=Lish +controlledvocabulary.language.lishana_deni=Lishana Deni +controlledvocabulary.language.lishanid_noshan=Lishanid Noshan +controlledvocabulary.language.lishan_didan=Lishán Didán +controlledvocabulary.language.lisu=Lisu +controlledvocabulary.language.literary_chinese=Literary Chinese controlledvocabulary.language.lithuanian=Lithuanian +controlledvocabulary.language.lithuanian_sign_language=Lithuanian Sign Language +controlledvocabulary.language.litzlitz=Litzlitz +controlledvocabulary.language.liujiang_zhuang=Liujiang Zhuang +controlledvocabulary.language.liuqian_zhuang=Liuqian Zhuang +controlledvocabulary.language.liv=Liv +controlledvocabulary.language.livvi=Livvi +controlledvocabulary.language.lo-toga=Lo-Toga +controlledvocabulary.language.loarki=Loarki +controlledvocabulary.language.lobala=Lobala +controlledvocabulary.language.lobi=Lobi +controlledvocabulary.language.lodhi=Lodhi +controlledvocabulary.language.logba=Logba +controlledvocabulary.language.logir=Logir +controlledvocabulary.language.logo=Logo +controlledvocabulary.language.logol=Logol +controlledvocabulary.language.logooli=Logooli +controlledvocabulary.language.logorik=Logorik +controlledvocabulary.language.croatian=Logudorese Sardinian, Croatian +controlledvocabulary.language.lohorung=Lohorung +controlledvocabulary.language.loja_highland_quichua=Loja Highland Quichua +controlledvocabulary.language.lojban=Lojban +controlledvocabulary.language.lokaa=Lokaa +controlledvocabulary.language.loke=Loke +controlledvocabulary.language.loko=Loko +controlledvocabulary.language.lokoya=Lokoya +controlledvocabulary.language.lola=Lola +controlledvocabulary.language.lolak=Lolak +controlledvocabulary.language.lole=Lole +controlledvocabulary.language.lolo=Lolo +controlledvocabulary.language.loloda=Loloda +controlledvocabulary.language.lolopo=Lolopo +controlledvocabulary.language.loma_(cote_d'ivoire)=Loma (Côte d'Ivoire) +controlledvocabulary.language.loma_(liberia)=Loma (Liberia) +controlledvocabulary.language.lomaiviti=Lomaiviti +controlledvocabulary.language.lomavren=Lomavren +controlledvocabulary.language.lombard=Lombard +controlledvocabulary.language.lombi=Lombi +controlledvocabulary.language.lombo=Lombo +controlledvocabulary.language.lomwe=Lomwe +controlledvocabulary.language.loncong=Loncong +controlledvocabulary.language.long_phuri_naga=Long Phuri Naga +controlledvocabulary.language.long_wat=Long Wat +controlledvocabulary.language.longgu=Longgu +controlledvocabulary.language.longto=Longto +controlledvocabulary.language.longuda=Longuda +controlledvocabulary.language.loniu=Loniu +controlledvocabulary.language.lonwolwol=Lonwolwol +controlledvocabulary.language.lonzo=Lonzo +controlledvocabulary.language.loo=Loo +controlledvocabulary.language.lopa=Lopa +controlledvocabulary.language.lopi=Lopi +controlledvocabulary.language.lopit=Lopit +controlledvocabulary.language.lorang=Lorang +controlledvocabulary.language.lorediakarkar=Lorediakarkar +controlledvocabulary.language.loreto-ucayali_spanish=Loreto-Ucayali Spanish +controlledvocabulary.language.lote=Lote +controlledvocabulary.language.lotha_naga=Lotha Naga +controlledvocabulary.language.lotud=Lotud +controlledvocabulary.language.lou=Lou +controlledvocabulary.language.louisiana_creole=Louisiana Creole +controlledvocabulary.language.loun=Loun +controlledvocabulary.language.loup_a=Loup A +controlledvocabulary.language.loup_b=Loup B +controlledvocabulary.language.low_german=Low German +controlledvocabulary.language.lower_burdekin=Lower Burdekin +controlledvocabulary.language.lower_chehalis=Lower Chehalis +controlledvocabulary.language.lower_grand_valley_dani=Lower Grand Valley Dani +controlledvocabulary.language.lower_nossob=Lower Nossob +controlledvocabulary.language.lower_silesian=Lower Silesian +controlledvocabulary.language.lower_sorbian=Lower Sorbian +controlledvocabulary.language.lower_southern_aranda=Lower Southern Aranda +controlledvocabulary.language.lower_ta'oih=Lower Ta'oih +controlledvocabulary.language.lower_tanana=Lower Tanana +controlledvocabulary.language.lowland_oaxaca_chontal=Lowland Oaxaca Chontal +controlledvocabulary.language.lowland_tarahumara=Lowland Tarahumara +controlledvocabulary.language.loxicha_zapotec=Loxicha Zapotec +controlledvocabulary.language.lozi=Lozi +controlledvocabulary.language.luang=Luang controlledvocabulary.language.luba-katanga=Luba-Katanga -controlledvocabulary.language.latvian=Latvian -controlledvocabulary.language.manx=Manx +controlledvocabulary.language.luba-lulua=Luba-Lulua +controlledvocabulary.language.lubila=Lubila +controlledvocabulary.language.lubu=Lubu +controlledvocabulary.language.lubuagan_kalinga=Lubuagan Kalinga +controlledvocabulary.language.luchazi=Luchazi +controlledvocabulary.language.lucumi=Lucumi +controlledvocabulary.language.ludian=Ludian +controlledvocabulary.language.lufu=Lufu +controlledvocabulary.language.lugbara=Lugbara +controlledvocabulary.language.luguru=Luguru +controlledvocabulary.language.luhu=Luhu +controlledvocabulary.language.luimbi=Luimbi +controlledvocabulary.language.luiseno=Luiseno +controlledvocabulary.language.lukpa=Lukpa +controlledvocabulary.language.lule=Lule +controlledvocabulary.language.lule_sami=Lule Sami +controlledvocabulary.language.lumba-yakkha=Lumba-Yakkha +controlledvocabulary.language.lumbu=Lumbu +controlledvocabulary.language.lumun=Lumun +controlledvocabulary.language.luna=Luna +controlledvocabulary.language.lunanakha=Lunanakha +controlledvocabulary.language.lunda=Lunda +controlledvocabulary.language.lundayeh=Lundayeh +controlledvocabulary.language.lungalunga=Lungalunga +controlledvocabulary.language.lungga=Lungga +controlledvocabulary.language.luo_(cameroon)=Luo (Cameroon) +controlledvocabulary.language.luo_(kenya_and_tanzania)=Luo (Kenya and Tanzania) +controlledvocabulary.language.luopohe_hmong=Luopohe Hmong +controlledvocabulary.language.luri=Luri +controlledvocabulary.language.lusengo=Lusengo +controlledvocabulary.language.lushai=Lushai +controlledvocabulary.language.lushootseed=Lushootseed +controlledvocabulary.language.lusi=Lusi +controlledvocabulary.language.lusitanian=Lusitanian +controlledvocabulary.language.lutos=Lutos +controlledvocabulary.language.luvale=Luvale +controlledvocabulary.language.luwati=Luwati +controlledvocabulary.language.luwo=Luwo +controlledvocabulary.language.luxembourgish,_letzeburgesch=Luxembourgish, Letzeburgesch +controlledvocabulary.language.luyana=Luyana +controlledvocabulary.language.luyia=Luyia +controlledvocabulary.language.lwalu=Lwalu +controlledvocabulary.language.lwel=Lwel +controlledvocabulary.language.lycian=Lycian +controlledvocabulary.language.lydian=Lydian +controlledvocabulary.language.lyngngam=Lyngngam +controlledvocabulary.language.lyele=Lyélé +controlledvocabulary.language.laadan=Láadan +controlledvocabulary.language.laa_laa_bwamu=Láá Láá Bwamu +controlledvocabulary.language.lu=Lü +controlledvocabulary.language.ma_(democratic_republic_of_congo)=Ma (Democratic Republic of Congo) +controlledvocabulary.language.ma_(papua_new_guinea)=Ma (Papua New Guinea) +controlledvocabulary.language.ma_manda=Ma Manda +controlledvocabulary.language.ma'anyan=Ma'anyan +controlledvocabulary.language.ma'di=Ma'di +controlledvocabulary.language.ma'ya=Ma'ya +controlledvocabulary.language.maa=Maa +controlledvocabulary.language.maaka=Maaka +controlledvocabulary.language.maasina_fulfulde=Maasina Fulfulde +controlledvocabulary.language.maay=Maay +controlledvocabulary.language.maba_(chad)=Maba (Chad) +controlledvocabulary.language.maba_(indonesia)=Maba (Indonesia) +controlledvocabulary.language.mabaale=Mabaale +controlledvocabulary.language.mabaan=Mabaan +controlledvocabulary.language.mabaka_valley_kalinga=Mabaka Valley Kalinga +controlledvocabulary.language.mabire=Mabire +controlledvocabulary.language.maca=Maca +controlledvocabulary.language.macaguaje=Macaguaje +controlledvocabulary.language.macaguan=Macaguán +controlledvocabulary.language.macanese=Macanese +controlledvocabulary.language.macedo-romanian=Macedo-Romanian controlledvocabulary.language.macedonian=Macedonian +controlledvocabulary.language.machame=Machame +controlledvocabulary.language.machiguenga=Machiguenga +controlledvocabulary.language.machinere=Machinere +controlledvocabulary.language.machinga=Machinga +controlledvocabulary.language.maco=Maco +controlledvocabulary.language.macuna=Macuna +controlledvocabulary.language.macushi=Macushi +controlledvocabulary.language.mada_(cameroon)=Mada (Cameroon) +controlledvocabulary.language.mada_(nigeria)=Mada (Nigeria) +controlledvocabulary.language.madagascar_sign_language=Madagascar Sign Language +controlledvocabulary.language.madak=Madak +controlledvocabulary.language.madhi_madhi=Madhi Madhi +controlledvocabulary.language.madi=Madi +controlledvocabulary.language.madurese=Madurese +controlledvocabulary.language.mae=Mae +controlledvocabulary.language.maek=Maek +controlledvocabulary.language.maeng_itneg=Maeng Itneg +controlledvocabulary.language.mafa=Mafa +controlledvocabulary.language.mafea=Mafea +controlledvocabulary.language.mag-indi_ayta=Mag-Indi Ayta +controlledvocabulary.language.mag-antsi_ayta=Mag-antsi Ayta +controlledvocabulary.language.magahi=Magahi +controlledvocabulary.language.magbukun_ayta=Magbukun Ayta +controlledvocabulary.language.magdalena_penasco_mixtec=Magdalena Peñasco Mixtec +controlledvocabulary.language.magoma=Magoma +controlledvocabulary.language.magori=Magori +controlledvocabulary.language.maguindanaon=Maguindanaon +controlledvocabulary.language.magɨ_(madang_province)=Magɨ (Madang Province) +controlledvocabulary.language.magɨyi=Magɨyi +controlledvocabulary.language.mahali=Mahali +controlledvocabulary.language.mahasu_pahari=Mahasu Pahari +controlledvocabulary.language.mahican=Mahican +controlledvocabulary.language.mahongwe=Mahongwe +controlledvocabulary.language.mahou=Mahou +controlledvocabulary.language.mai_brat=Mai Brat +controlledvocabulary.language.maia=Maia +controlledvocabulary.language.maiadomu=Maiadomu +controlledvocabulary.language.maiani=Maiani +controlledvocabulary.language.maii=Maii +controlledvocabulary.language.mailu=Mailu +controlledvocabulary.language.maindo=Maindo +controlledvocabulary.language.mainfrankisch=Mainfränkisch +controlledvocabulary.language.mainstream_kenyah=Mainstream Kenyah +controlledvocabulary.language.mairasi=Mairasi +controlledvocabulary.language.maisin=Maisin +controlledvocabulary.language.maithili=Maithili +controlledvocabulary.language.maiwa_(indonesia)=Maiwa (Indonesia) +controlledvocabulary.language.maiwa_(papua_new_guinea)=Maiwa (Papua New Guinea) +controlledvocabulary.language.maiwala=Maiwala +controlledvocabulary.language.majang=Majang +controlledvocabulary.language.majera=Majera +controlledvocabulary.language.majhi=Majhi +controlledvocabulary.language.majhwar=Majhwar +controlledvocabulary.language.majukayang_kalinga=Majukayang Kalinga +controlledvocabulary.language.mak_(china)=Mak (China) +controlledvocabulary.language.mak_(nigeria)=Mak (Nigeria) +controlledvocabulary.language.makaa=Makaa +controlledvocabulary.language.makah=Makah +controlledvocabulary.language.makalero=Makalero +controlledvocabulary.language.makasae=Makasae +controlledvocabulary.language.makasar=Makasar +controlledvocabulary.language.makassar_malay=Makassar Malay +controlledvocabulary.language.makayam=Makayam +controlledvocabulary.language.makhuwa=Makhuwa +controlledvocabulary.language.makhuwa-marrevone=Makhuwa-Marrevone +controlledvocabulary.language.makhuwa-meetto=Makhuwa-Meetto +controlledvocabulary.language.makhuwa-moniga=Makhuwa-Moniga +controlledvocabulary.language.makhuwa-saka=Makhuwa-Saka +controlledvocabulary.language.makhuwa-shirima=Makhuwa-Shirima +controlledvocabulary.language.maklew=Maklew +controlledvocabulary.language.makolkol=Makolkol +controlledvocabulary.language.makonde=Makonde +controlledvocabulary.language.maku'a=Maku'a +controlledvocabulary.language.makuri_naga=Makuri Naga +controlledvocabulary.language.makurap=Makuráp +controlledvocabulary.language.makwe=Makwe +controlledvocabulary.language.makyan_naga=Makyan Naga +controlledvocabulary.language.mal=Mal +controlledvocabulary.language.mal_paharia=Mal Paharia +controlledvocabulary.language.mala_(nigeria)=Mala (Nigeria) +controlledvocabulary.language.mala_(papua_new_guinea)=Mala (Papua New Guinea) +controlledvocabulary.language.mala_malasar=Mala Malasar +controlledvocabulary.language.malaccan_creole_malay=Malaccan Creole Malay +controlledvocabulary.language.malaccan_creole_portuguese=Malaccan Creole Portuguese controlledvocabulary.language.malagasy=Malagasy -controlledvocabulary.language.malay=Malay +controlledvocabulary.language.malak_malak=Malak Malak +controlledvocabulary.language.malalamai=Malalamai +controlledvocabulary.language.malango=Malango +controlledvocabulary.language.malankuravan=Malankuravan +controlledvocabulary.language.malapandaram=Malapandaram +controlledvocabulary.language.malaryan=Malaryan +controlledvocabulary.language.malas=Malas +controlledvocabulary.language.malasar=Malasar +controlledvocabulary.language.malavedan=Malavedan +controlledvocabulary.language.malawi_lomwe=Malawi Lomwe +controlledvocabulary.language.malawi_sena=Malawi Sena +controlledvocabulary.language.malawian_sign_language=Malawian Sign Language +controlledvocabulary.language.malay_(individual_language)=Malay (individual language) +controlledvocabulary.language.malay=Malay, Malay (macrolanguage) controlledvocabulary.language.malayalam=Malayalam +controlledvocabulary.language.malayic_dayak=Malayic Dayak +controlledvocabulary.language.malaynon=Malaynon +controlledvocabulary.language.malayo=Malayo +controlledvocabulary.language.malaysian_sign_language=Malaysian Sign Language +controlledvocabulary.language.malba_birifor=Malba Birifor +controlledvocabulary.language.maldivian,_dhivehi,_divehi=Maldivian, Dhivehi, Divehi +controlledvocabulary.language.male_(ethiopia)=Male (Ethiopia) +controlledvocabulary.language.male_(papua_new_guinea)=Male (Papua New Guinea) +controlledvocabulary.language.malecite-passamaquoddy=Malecite-Passamaquoddy +controlledvocabulary.language.maleng=Maleng +controlledvocabulary.language.maleu-kilenge=Maleu-Kilenge +controlledvocabulary.language.malfaxal=Malfaxal +controlledvocabulary.language.malgana=Malgana +controlledvocabulary.language.malgbe=Malgbe +controlledvocabulary.language.mali=Mali +controlledvocabulary.language.malila=Malila +controlledvocabulary.language.malimba=Malimba +controlledvocabulary.language.malimpung=Malimpung +controlledvocabulary.language.malinaltepec_me'phaa=Malinaltepec Me'phaa +controlledvocabulary.language.malo=Malo +controlledvocabulary.language.malol=Malol controlledvocabulary.language.maltese=Maltese -controlledvocabulary.language.maori=M\u0101ori -controlledvocabulary.language.marathi_(marathi)=Marathi (Mar\u0101\u1E6Dh\u012B) +controlledvocabulary.language.maltese_sign_language=Maltese Sign Language +controlledvocabulary.language.malua_bay=Malua Bay +controlledvocabulary.language.malvi=Malvi +controlledvocabulary.language.malyangapa=Malyangapa +controlledvocabulary.language.maleku_jaika=Maléku Jaíka +controlledvocabulary.language.mam=Mam +controlledvocabulary.language.mama=Mama +controlledvocabulary.language.mamaa=Mamaa +controlledvocabulary.language.mamainde=Mamaindé +controlledvocabulary.language.mamanwa=Mamanwa +controlledvocabulary.language.mamara_senoufo=Mamara Senoufo +controlledvocabulary.language.mamasa=Mamasa +controlledvocabulary.language.mambae=Mambae +controlledvocabulary.language.mambai=Mambai +controlledvocabulary.language.mamboru=Mamboru +controlledvocabulary.language.mambwe-lungu=Mambwe-Lungu +controlledvocabulary.language.mampruli=Mampruli +controlledvocabulary.language.mamuju=Mamuju +controlledvocabulary.language.mamulique=Mamulique +controlledvocabulary.language.mamusi=Mamusi +controlledvocabulary.language.mamvu=Mamvu +controlledvocabulary.language.man_met=Man Met +controlledvocabulary.language.manado_malay=Manado Malay +controlledvocabulary.language.manam=Manam +controlledvocabulary.language.manambu=Manambu +controlledvocabulary.language.manangba=Manangba +controlledvocabulary.language.manangkari=Manangkari +controlledvocabulary.language.manchu=Manchu +controlledvocabulary.language.manda_(australia)=Manda (Australia) +controlledvocabulary.language.manda_(india)=Manda (India) +controlledvocabulary.language.manda_(tanzania)=Manda (Tanzania) +controlledvocabulary.language.mandahuaca=Mandahuaca +controlledvocabulary.language.mandaic=Mandaic +controlledvocabulary.language.mandan=Mandan +controlledvocabulary.language.mandandanyi=Mandandanyi +controlledvocabulary.language.mandar=Mandar +controlledvocabulary.language.mandara=Mandara +controlledvocabulary.language.mandari=Mandari +controlledvocabulary.language.mandarin_chinese=Mandarin Chinese +controlledvocabulary.language.mandaya=Mandaya +controlledvocabulary.language.mandeali=Mandeali +controlledvocabulary.language.mander=Mander +controlledvocabulary.language.mandingo=Mandingo +controlledvocabulary.language.mandinka=Mandinka +controlledvocabulary.language.mandjak=Mandjak +controlledvocabulary.language.mandobo_atas=Mandobo Atas +controlledvocabulary.language.mandobo_bawah=Mandobo Bawah +controlledvocabulary.language.manem=Manem +controlledvocabulary.language.mang=Mang +controlledvocabulary.language.manga_kanuri=Manga Kanuri +controlledvocabulary.language.mangala=Mangala +controlledvocabulary.language.mangareva=Mangareva +controlledvocabulary.language.mangarrayi=Mangarrayi +controlledvocabulary.language.mangas=Mangas +controlledvocabulary.language.mangayat=Mangayat +controlledvocabulary.language.mangbetu=Mangbetu +controlledvocabulary.language.mangbutu=Mangbutu +controlledvocabulary.language.mangerr=Mangerr +controlledvocabulary.language.mangga_buang=Mangga Buang +controlledvocabulary.language.manggarai=Manggarai +controlledvocabulary.language.mango=Mango +controlledvocabulary.language.mangole=Mangole +controlledvocabulary.language.mangseng=Mangseng +controlledvocabulary.language.mangue=Mangue +controlledvocabulary.language.manichaean_middle_persian=Manichaean Middle Persian +controlledvocabulary.language.manide=Manide +controlledvocabulary.language.manikion=Manikion +controlledvocabulary.language.manipa=Manipa +controlledvocabulary.language.manipuri=Manipuri +controlledvocabulary.language.mankanya=Mankanya +controlledvocabulary.language.mankiyali=Mankiyali +controlledvocabulary.language.manna-dora=Manna-Dora +controlledvocabulary.language.mannan=Mannan +controlledvocabulary.language.mano=Mano +controlledvocabulary.language.manombai=Manombai +controlledvocabulary.language.mansaka=Mansaka +controlledvocabulary.language.mansi=Mansi +controlledvocabulary.language.mansoanka=Mansoanka +controlledvocabulary.language.manta=Manta +controlledvocabulary.language.mantsi=Mantsi +controlledvocabulary.language.manumanaw_karen=Manumanaw Karen +controlledvocabulary.language.manx=Manx +controlledvocabulary.language.manya=Manya +controlledvocabulary.language.manyawa=Manyawa +controlledvocabulary.language.manyika=Manyika +controlledvocabulary.language.manza=Manza +controlledvocabulary.language.mao_naga=Mao Naga +controlledvocabulary.language.maonan=Maonan +controlledvocabulary.language.maore_comorian=Maore Comorian +controlledvocabulary.language.mape=Mape +controlledvocabulary.language.mapena=Mapena +controlledvocabulary.language.mapia=Mapia +controlledvocabulary.language.mapidian=Mapidian +controlledvocabulary.language.mapos_buang=Mapos Buang +controlledvocabulary.language.mapoyo=Mapoyo +controlledvocabulary.language.mapudungun=Mapudungun +controlledvocabulary.language.mapun=Mapun +controlledvocabulary.language.maquiritari=Maquiritari +controlledvocabulary.language.mara_chin=Mara Chin +controlledvocabulary.language.marachi=Marachi +controlledvocabulary.language.maraghei=Maraghei +controlledvocabulary.language.maragus=Maragus +controlledvocabulary.language.maram_naga=Maram Naga +controlledvocabulary.language.marama=Marama +controlledvocabulary.language.maranao=Maranao +controlledvocabulary.language.maranunggu=Maranunggu +controlledvocabulary.language.mararit=Mararit +controlledvocabulary.language.marathi_(marathi)=Marathi, Marathi (Marāṭhī) +controlledvocabulary.language.marau=Marau +controlledvocabulary.language.marba=Marba +controlledvocabulary.language.mardin_sign_language=Mardin Sign Language +controlledvocabulary.language.maremgi=Maremgi +controlledvocabulary.language.marenje=Marenje +controlledvocabulary.language.marfa=Marfa +controlledvocabulary.language.margany=Margany +controlledvocabulary.language.marghi_central=Marghi Central +controlledvocabulary.language.marghi_south=Marghi South +controlledvocabulary.language.margos-yarowilca-lauricocha_quechua=Margos-Yarowilca-Lauricocha Quechua +controlledvocabulary.language.margu=Margu +controlledvocabulary.language.mari_(east_sepik_province)=Mari (East Sepik Province) +controlledvocabulary.language.mari_(madang_province)=Mari (Madang Province) +controlledvocabulary.language.mari_(russia)=Mari (Russia) +controlledvocabulary.language.maria_(india)=Maria (India) +controlledvocabulary.language.maria_(papua_new_guinea)=Maria (Papua New Guinea) +controlledvocabulary.language.maricopa=Maricopa +controlledvocabulary.language.maridan=Maridan +controlledvocabulary.language.maridjabin=Maridjabin +controlledvocabulary.language.marik=Marik +controlledvocabulary.language.marimanindji=Marimanindji +controlledvocabulary.language.marind=Marind +controlledvocabulary.language.maring=Maring +controlledvocabulary.language.maring_naga=Maring Naga +controlledvocabulary.language.maringarr=Maringarr +controlledvocabulary.language.marino=Marino +controlledvocabulary.language.mariri=Mariri +controlledvocabulary.language.maritime_sign_language=Maritime Sign Language +controlledvocabulary.language.maritsaua=Maritsauá +controlledvocabulary.language.mariyedi=Mariyedi +controlledvocabulary.language.marka=Marka +controlledvocabulary.language.markweeta=Markweeta +controlledvocabulary.language.marma=Marma +controlledvocabulary.language.marovo=Marovo +controlledvocabulary.language.marra=Marra +controlledvocabulary.language.marriammu=Marriammu +controlledvocabulary.language.marrithiyel=Marrithiyel +controlledvocabulary.language.marrucinian=Marrucinian controlledvocabulary.language.marshallese=Marshallese +controlledvocabulary.language.marsian=Marsian +controlledvocabulary.language.martha's_vineyard_sign_language=Martha's Vineyard Sign Language +controlledvocabulary.language.marti_ke=Marti Ke +controlledvocabulary.language.martu_wangka=Martu Wangka +controlledvocabulary.language.martuyhunira=Martuyhunira +controlledvocabulary.language.maru=Maru +controlledvocabulary.language.marwari=Marwari +controlledvocabulary.language.marwari_(india)=Marwari (India) +controlledvocabulary.language.marwari_(pakistan)=Marwari (Pakistan) +controlledvocabulary.language.marubo=Marúbo +controlledvocabulary.language.masaaba=Masaaba +controlledvocabulary.language.masadiit_itneg=Masadiit Itneg +controlledvocabulary.language.masai=Masai +controlledvocabulary.language.masalit=Masalit +controlledvocabulary.language.masana=Masana +controlledvocabulary.language.masbatenyo=Masbatenyo +controlledvocabulary.language.mashco_piro=Mashco Piro +controlledvocabulary.language.mashi_(nigeria)=Mashi (Nigeria) +controlledvocabulary.language.mashi_(zambia)=Mashi (Zambia) +controlledvocabulary.language.masikoro_malagasy=Masikoro Malagasy +controlledvocabulary.language.masimasi=Masimasi +controlledvocabulary.language.masiwang=Masiwang +controlledvocabulary.language.maskelynes=Maskelynes +controlledvocabulary.language.maslam=Maslam +controlledvocabulary.language.masmaje=Masmaje +controlledvocabulary.language.massalat=Massalat +controlledvocabulary.language.massep=Massep +controlledvocabulary.language.matagalpa=Matagalpa +controlledvocabulary.language.matal=Matal +controlledvocabulary.language.matambwe=Matambwe +controlledvocabulary.language.matbat=Matbat +controlledvocabulary.language.matengo=Matengo +controlledvocabulary.language.matepi=Matepi +controlledvocabulary.language.matigsalug_manobo=Matigsalug Manobo +controlledvocabulary.language.matipuhy=Matipuhy +controlledvocabulary.language.matngala=Matngala +controlledvocabulary.language.mato=Mato +controlledvocabulary.language.mato_grosso_arara=Mato Grosso Arára +controlledvocabulary.language.mator=Mator +controlledvocabulary.language.matses=Matsés +controlledvocabulary.language.mattole=Mattole +controlledvocabulary.language.matu_chin=Matu Chin +controlledvocabulary.language.matukar=Matukar +controlledvocabulary.language.matumbi=Matumbi +controlledvocabulary.language.matya_samo=Matya Samo +controlledvocabulary.language.matis=Matís +controlledvocabulary.language.maung=Maung +controlledvocabulary.language.mauritian_sign_language=Mauritian Sign Language +controlledvocabulary.language.mauwake=Mauwake +controlledvocabulary.language.mawa_(chad)=Mawa (Chad) +controlledvocabulary.language.mawa_(nigeria)=Mawa (Nigeria) +controlledvocabulary.language.mawak=Mawak +controlledvocabulary.language.mawan=Mawan +controlledvocabulary.language.mawayana=Mawayana +controlledvocabulary.language.mawchi=Mawchi +controlledvocabulary.language.mawes=Mawes +controlledvocabulary.language.maxakali=Maxakalí +controlledvocabulary.language.maxi_gbe=Maxi Gbe +controlledvocabulary.language.maya_samo=Maya Samo +controlledvocabulary.language.mayaguduna=Mayaguduna +controlledvocabulary.language.mayangna=Mayangna +controlledvocabulary.language.mayawali=Mayawali +controlledvocabulary.language.mayeka=Mayeka +controlledvocabulary.language.mayi-kulan=Mayi-Kulan +controlledvocabulary.language.mayi-thakurti=Mayi-Thakurti +controlledvocabulary.language.mayi-yapi=Mayi-Yapi +controlledvocabulary.language.mayo=Mayo +controlledvocabulary.language.mayogo=Mayogo +controlledvocabulary.language.mayoyao_ifugao=Mayoyao Ifugao +controlledvocabulary.language.mazagway=Mazagway +controlledvocabulary.language.mazaltepec_zapotec=Mazaltepec Zapotec +controlledvocabulary.language.mazanderani=Mazanderani +controlledvocabulary.language.mazatlan_mazatec=Mazatlán Mazatec +controlledvocabulary.language.mazatlan_mixe=Mazatlán Mixe +controlledvocabulary.language.mba=Mba +controlledvocabulary.language.mbala=Mbala +controlledvocabulary.language.mbalanhu=Mbalanhu +controlledvocabulary.language.mbandja=Mbandja +controlledvocabulary.language.mbangala=Mbangala +controlledvocabulary.language.mbangi=Mbangi +controlledvocabulary.language.mbangwe=Mbangwe +controlledvocabulary.language.mbara_(australia)=Mbara (Australia) +controlledvocabulary.language.mbara_(chad)=Mbara (Chad) +controlledvocabulary.language.mbariman-gudhinma=Mbariman-Gudhinma +controlledvocabulary.language.mbati=Mbati +controlledvocabulary.language.mbato=Mbato +controlledvocabulary.language.mbay=Mbay +controlledvocabulary.language.mbe=Mbe +controlledvocabulary.language.mbe'=Mbe' +controlledvocabulary.language.mbelime=Mbelime +controlledvocabulary.language.mbere=Mbere +controlledvocabulary.language.mbesa=Mbesa +controlledvocabulary.language.mbessa=Mbessa +controlledvocabulary.language.mbo_(cameroon)=Mbo (Cameroon) +controlledvocabulary.language.mbo_(democratic_republic_of_congo)=Mbo (Democratic Republic of Congo) +controlledvocabulary.language.mboi=Mboi +controlledvocabulary.language.mboko=Mboko +controlledvocabulary.language.mbole=Mbole +controlledvocabulary.language.mbonga=Mbonga +controlledvocabulary.language.mbongno=Mbongno +controlledvocabulary.language.mbosi=Mbosi +controlledvocabulary.language.mbowe=Mbowe +controlledvocabulary.language.mbre=Mbre +controlledvocabulary.language.mbudum=Mbudum +controlledvocabulary.language.mbugu=Mbugu +controlledvocabulary.language.mbugwe=Mbugwe +controlledvocabulary.language.mbuk=Mbuk +controlledvocabulary.language.mbuko=Mbuko +controlledvocabulary.language.mbukushu=Mbukushu +controlledvocabulary.language.mbula=Mbula +controlledvocabulary.language.mbula-bwazza=Mbula-Bwazza +controlledvocabulary.language.mbule=Mbule +controlledvocabulary.language.mbulungish=Mbulungish +controlledvocabulary.language.mbum=Mbum +controlledvocabulary.language.mbunda=Mbunda +controlledvocabulary.language.mbunga=Mbunga +controlledvocabulary.language.mburku=Mburku +controlledvocabulary.language.mbwela=Mbwela +controlledvocabulary.language.mbya_guarani=Mbyá Guaraní +controlledvocabulary.language.me'en=Me'en +controlledvocabulary.language.medebur=Medebur +controlledvocabulary.language.medefaidrin=Medefaidrin +controlledvocabulary.language.media_lengua=Media Lengua +controlledvocabulary.language.median=Median +controlledvocabulary.language.mednyj_aleut=Mednyj Aleut +controlledvocabulary.language.medumba=Medumba +controlledvocabulary.language.mefele=Mefele +controlledvocabulary.language.megam=Megam +controlledvocabulary.language.megleno_romanian=Megleno Romanian +controlledvocabulary.language.mehek=Mehek +controlledvocabulary.language.mehinaku=Mehináku +controlledvocabulary.language.mehri=Mehri +controlledvocabulary.language.mekeo=Mekeo +controlledvocabulary.language.mekmek=Mekmek +controlledvocabulary.language.mekwei=Mekwei +controlledvocabulary.language.mel-khaonh=Mel-Khaonh +controlledvocabulary.language.mele-fila=Mele-Fila +controlledvocabulary.language.melo=Melo +controlledvocabulary.language.melpa=Melpa +controlledvocabulary.language.memoni=Memoni +controlledvocabulary.language.mendalam_kayan=Mendalam Kayan +controlledvocabulary.language.mendankwe-nkwen=Mendankwe-Nkwen +controlledvocabulary.language.mende_(papua_new_guinea)=Mende (Papua New Guinea) +controlledvocabulary.language.mende_(sierra_leone)=Mende (Sierra Leone) +controlledvocabulary.language.mengaka=Mengaka +controlledvocabulary.language.mengen=Mengen +controlledvocabulary.language.mengisa=Mengisa +controlledvocabulary.language.menka=Menka +controlledvocabulary.language.menominee=Menominee +controlledvocabulary.language.mentawai=Mentawai +controlledvocabulary.language.menya=Menya +controlledvocabulary.language.meoswar=Meoswar +controlledvocabulary.language.mer=Mer +controlledvocabulary.language.meramera=Meramera +controlledvocabulary.language.merei=Merei +controlledvocabulary.language.merey=Merey +controlledvocabulary.language.meriam_mir=Meriam Mir +controlledvocabulary.language.merlav=Merlav +controlledvocabulary.language.meroitic=Meroitic +controlledvocabulary.language.meru=Meru +controlledvocabulary.language.merwari=Merwari +controlledvocabulary.language.mesaka=Mesaka +controlledvocabulary.language.mescalero-chiricahua_apache=Mescalero-Chiricahua Apache +controlledvocabulary.language.mese=Mese +controlledvocabulary.language.meskwaki=Meskwaki +controlledvocabulary.language.mesme=Mesme +controlledvocabulary.language.mesmes=Mesmes +controlledvocabulary.language.mesopotamian_arabic=Mesopotamian Arabic +controlledvocabulary.language.mesqan=Mesqan +controlledvocabulary.language.messapic=Messapic +controlledvocabulary.language.meta'=Meta' +controlledvocabulary.language.metlatonoc_mixtec=Metlatónoc Mixtec +controlledvocabulary.language.mewari=Mewari +controlledvocabulary.language.mewati=Mewati +controlledvocabulary.language.mexican_sign_language=Mexican Sign Language +controlledvocabulary.language.meyah=Meyah +controlledvocabulary.language.mezontla_popoloca=Mezontla Popoloca +controlledvocabulary.language.mezquital_otomi=Mezquital Otomi +controlledvocabulary.language.mfinu=Mfinu +controlledvocabulary.language.mfumte=Mfumte +controlledvocabulary.language.mgbolizhia=Mgbolizhia +controlledvocabulary.language.mi'kmaq=Mi'kmaq +controlledvocabulary.language.miahuatlan_zapotec=Miahuatlán Zapotec +controlledvocabulary.language.miami=Miami +controlledvocabulary.language.mian=Mian +controlledvocabulary.language.miani=Miani +controlledvocabulary.language.michif=Michif +controlledvocabulary.language.michigamea=Michigamea +controlledvocabulary.language.michoacan_mazahua=Michoacán Mazahua +controlledvocabulary.language.michoacan_nahuatl=Michoacán Nahuatl +controlledvocabulary.language.mid_grand_valley_dani=Mid Grand Valley Dani +controlledvocabulary.language.mid-southern_banda=Mid-Southern Banda +controlledvocabulary.language.middle_armenian=Middle Armenian +controlledvocabulary.language.middle_breton=Middle Breton +controlledvocabulary.language.middle_cornish=Middle Cornish +controlledvocabulary.language.middle_dutch_(ca._1050-1350)=Middle Dutch (ca. 1050-1350) +controlledvocabulary.language.middle_english_(1100-1500)=Middle English (1100-1500) +controlledvocabulary.language.middle_french_(ca._1400-1600)=Middle French (ca. 1400-1600) +controlledvocabulary.language.middle_high_german_(ca._1050-1500)=Middle High German (ca. 1050-1500) +controlledvocabulary.language.middle_hittite=Middle Hittite +controlledvocabulary.language.middle_irish_(900-1200)=Middle Irish (900-1200) +controlledvocabulary.language.middle_khmer_(1400_to_1850_ce)=Middle Khmer (1400 to 1850 CE) +controlledvocabulary.language.middle_korean_(10th-16th_cent.)=Middle Korean (10th-16th cent.) +controlledvocabulary.language.middle_low_german=Middle Low German +controlledvocabulary.language.middle_mongolian=Middle Mongolian +controlledvocabulary.language.middle_newar=Middle Newar +controlledvocabulary.language.middle_watut=Middle Watut +controlledvocabulary.language.middle_welsh=Middle Welsh +controlledvocabulary.language.midob=Midob +controlledvocabulary.language.migaama=Migaama +controlledvocabulary.language.migabac=Migabac +controlledvocabulary.language.migum=Migum +controlledvocabulary.language.miju-mishmi=Miju-Mishmi +controlledvocabulary.language.mikasuki=Mikasuki +controlledvocabulary.language.mili=Mili +controlledvocabulary.language.miltu=Miltu +controlledvocabulary.language.miluk=Miluk +controlledvocabulary.language.milyan=Milyan +controlledvocabulary.language.min_bei_chinese=Min Bei Chinese +controlledvocabulary.language.min_dong_chinese=Min Dong Chinese +controlledvocabulary.language.min_nan_chinese=Min Nan Chinese +controlledvocabulary.language.min_zhong_chinese=Min Zhong Chinese +controlledvocabulary.language.mina_(cameroon)=Mina (Cameroon) +controlledvocabulary.language.minaean=Minaean +controlledvocabulary.language.minang=Minang +controlledvocabulary.language.minangkabau=Minangkabau +controlledvocabulary.language.minanibai=Minanibai +controlledvocabulary.language.minaveha=Minaveha +controlledvocabulary.language.minderico=Minderico +controlledvocabulary.language.mindiri=Mindiri +controlledvocabulary.language.mingang_doso=Mingang Doso +controlledvocabulary.language.mingrelian=Mingrelian +controlledvocabulary.language.minica_huitoto=Minica Huitoto +controlledvocabulary.language.minidien=Minidien +controlledvocabulary.language.minjungbal=Minjungbal +controlledvocabulary.language.minkin=Minkin +controlledvocabulary.language.minoan=Minoan +controlledvocabulary.language.minokok=Minokok +controlledvocabulary.language.minriq=Minriq +controlledvocabulary.language.mintil=Mintil +controlledvocabulary.language.minz_zhuang=Minz Zhuang +controlledvocabulary.language.miqie=Miqie +controlledvocabulary.language.mirandese=Mirandese +controlledvocabulary.language.miraya_bikol=Miraya Bikol +controlledvocabulary.language.mirgan=Mirgan +controlledvocabulary.language.miriti=Miriti +controlledvocabulary.language.miriwoong=Miriwoong +controlledvocabulary.language.miriwoong_sign_language=Miriwoong Sign Language +controlledvocabulary.language.mirning=Mirning +controlledvocabulary.language.miship=Miship +controlledvocabulary.language.misima-panaeati=Misima-Panaeati +controlledvocabulary.language.mising=Mising +controlledvocabulary.language.mitla_zapotec=Mitla Zapotec +controlledvocabulary.language.mitlatongo_mixtec=Mitlatongo Mixtec +controlledvocabulary.language.mittu=Mittu +controlledvocabulary.language.mituku=Mituku +controlledvocabulary.language.miu=Miu +controlledvocabulary.language.miwa=Miwa +controlledvocabulary.language.mixed_great_andamanese=Mixed Great Andamanese controlledvocabulary.language.mixtepec_mixtec=Mixtepec Mixtec +controlledvocabulary.language.mixtepec_zapotec=Mixtepec Zapotec +controlledvocabulary.language.miya=Miya +controlledvocabulary.language.miyako=Miyako +controlledvocabulary.language.miyakubo_sign_language=Miyakubo Sign Language +controlledvocabulary.language.miyobe=Miyobe +controlledvocabulary.language.mlabri=Mlabri +controlledvocabulary.language.mlahso=Mlahsö +controlledvocabulary.language.mlap=Mlap +controlledvocabulary.language.mlomp=Mlomp +controlledvocabulary.language.mmaala=Mmaala +controlledvocabulary.language.mmen=Mmen +controlledvocabulary.language.mo'da=Mo'da +controlledvocabulary.language.moabite=Moabite +controlledvocabulary.language.moba=Moba +controlledvocabulary.language.mobilian=Mobilian +controlledvocabulary.language.mobumrin_aizi=Mobumrin Aizi +controlledvocabulary.language.mobwa_karen=Mobwa Karen +controlledvocabulary.language.mochi=Mochi +controlledvocabulary.language.mochica=Mochica +controlledvocabulary.language.mocho=Mocho +controlledvocabulary.language.mocovi=Mocoví +controlledvocabulary.language.modang=Modang +controlledvocabulary.language.greek_(modern)=Modern Greek (1453-), Greek (modern) +controlledvocabulary.language.modole=Modole +controlledvocabulary.language.moere=Moere +controlledvocabulary.language.mofu-gudur=Mofu-Gudur +controlledvocabulary.language.mogholi=Mogholi +controlledvocabulary.language.mogofin=Mogofin +controlledvocabulary.language.mogum=Mogum +controlledvocabulary.language.mohave=Mohave +controlledvocabulary.language.mohawk=Mohawk +controlledvocabulary.language.mohegan-pequot=Mohegan-Pequot +controlledvocabulary.language.moi_(congo)=Moi (Congo) +controlledvocabulary.language.moi_(indonesia)=Moi (Indonesia) +controlledvocabulary.language.moikodi=Moikodi +controlledvocabulary.language.moingi=Moingi +controlledvocabulary.language.moji=Moji +controlledvocabulary.language.mok=Mok +controlledvocabulary.language.mokati=Mokati +controlledvocabulary.language.moken=Moken +controlledvocabulary.language.mokerang=Mokerang +controlledvocabulary.language.mokilese=Mokilese +controlledvocabulary.language.moklen=Moklen +controlledvocabulary.language.mokole=Mokole +controlledvocabulary.language.mokpwe=Mokpwe +controlledvocabulary.language.moksela=Moksela +controlledvocabulary.language.moksha=Moksha +controlledvocabulary.language.molale=Molale +controlledvocabulary.language.molbog=Molbog +controlledvocabulary.language.moldova_sign_language=Moldova Sign Language +controlledvocabulary.language.molengue=Molengue +controlledvocabulary.language.molima=Molima +controlledvocabulary.language.molmo_one=Molmo One +controlledvocabulary.language.molo=Molo +controlledvocabulary.language.molof=Molof +controlledvocabulary.language.moloko=Moloko +controlledvocabulary.language.mom_jango=Mom Jango +controlledvocabulary.language.moma=Moma +controlledvocabulary.language.momare=Momare +controlledvocabulary.language.mombo_dogon=Mombo Dogon +controlledvocabulary.language.mombum=Mombum +controlledvocabulary.language.momina=Momina +controlledvocabulary.language.momuna=Momuna +controlledvocabulary.language.mon=Mon +controlledvocabulary.language.monastic_sign_language=Monastic Sign Language +controlledvocabulary.language.mondropolon=Mondropolon +controlledvocabulary.language.monde=Mondé +controlledvocabulary.language.mongo=Mongo +controlledvocabulary.language.mongol=Mongol +controlledvocabulary.language.mongolia_buriat=Mongolia Buriat controlledvocabulary.language.mongolian=Mongolian +controlledvocabulary.language.mongolian_sign_language=Mongolian Sign Language +controlledvocabulary.language.mongondow=Mongondow +controlledvocabulary.language.moni=Moni +controlledvocabulary.language.mono_(cameroon)=Mono (Cameroon) +controlledvocabulary.language.mono_(democratic_republic_of_congo)=Mono (Democratic Republic of Congo) +controlledvocabulary.language.mono_(solomon_islands)=Mono (Solomon Islands) +controlledvocabulary.language.mono_(usa)=Mono (USA) +controlledvocabulary.language.monom=Monom +controlledvocabulary.language.monsang_naga=Monsang Naga +controlledvocabulary.language.montenegrin=Montenegrin +controlledvocabulary.language.montol=Montol +controlledvocabulary.language.monumbo=Monumbo +controlledvocabulary.language.monzombo=Monzombo +controlledvocabulary.language.moo=Moo +controlledvocabulary.language.moose_cree=Moose Cree +controlledvocabulary.language.mopan_maya=Mopán Maya +controlledvocabulary.language.mor_(bomberai_peninsula)=Mor (Bomberai Peninsula) +controlledvocabulary.language.mor_(mor_islands)=Mor (Mor Islands) +controlledvocabulary.language.moraid=Moraid +controlledvocabulary.language.morawa=Morawa +controlledvocabulary.language.morelos_nahuatl=Morelos Nahuatl +controlledvocabulary.language.morerebi=Morerebi +controlledvocabulary.language.moresada=Moresada +controlledvocabulary.language.mori_atas=Mori Atas +controlledvocabulary.language.mori_bawah=Mori Bawah +controlledvocabulary.language.morigi=Morigi +controlledvocabulary.language.moriori=Moriori +controlledvocabulary.language.morisyen=Morisyen +controlledvocabulary.language.moro=Moro +controlledvocabulary.language.moroccan_arabic=Moroccan Arabic +controlledvocabulary.language.moroccan_sign_language=Moroccan Sign Language +controlledvocabulary.language.morokodo=Morokodo +controlledvocabulary.language.morom=Morom +controlledvocabulary.language.moronene=Moronene +controlledvocabulary.language.morori=Morori +controlledvocabulary.language.morouas=Morouas +controlledvocabulary.language.morrobalama=Morrobalama +controlledvocabulary.language.mortlockese=Mortlockese +controlledvocabulary.language.moru=Moru +controlledvocabulary.language.mosimo=Mosimo +controlledvocabulary.language.moskona=Moskona +controlledvocabulary.language.mossi=Mossi +controlledvocabulary.language.mota=Mota +controlledvocabulary.language.motlav=Motlav +controlledvocabulary.language.motu=Motu +controlledvocabulary.language.mouk-aria=Mouk-Aria +controlledvocabulary.language.moundadan_chetty=Moundadan Chetty +controlledvocabulary.language.mountain_koiali=Mountain Koiali +controlledvocabulary.language.mouwase=Mouwase +controlledvocabulary.language.movima=Movima +controlledvocabulary.language.moyadan_itneg=Moyadan Itneg +controlledvocabulary.language.moyon_naga=Moyon Naga +controlledvocabulary.language.mozambican_sign_language=Mozambican Sign Language +controlledvocabulary.language.mozarabic=Mozarabic +controlledvocabulary.language.mpade=Mpade +controlledvocabulary.language.mpalitjanh=Mpalitjanh +controlledvocabulary.language.mpi=Mpi +controlledvocabulary.language.mpiemo=Mpiemo +controlledvocabulary.language.mpinda=Mpinda +controlledvocabulary.language.mpoto=Mpoto +controlledvocabulary.language.mpotovoro=Mpotovoro +controlledvocabulary.language.mpumpong=Mpumpong +controlledvocabulary.language.mpuono=Mpuono +controlledvocabulary.language.mpur=Mpur +controlledvocabulary.language.mro-khimi_chin=Mro-Khimi Chin +controlledvocabulary.language.mru=Mru +controlledvocabulary.language.mser=Mser +controlledvocabulary.language.mt._iraya_agta=Mt. Iraya Agta +controlledvocabulary.language.mt._iriga_agta=Mt. Iriga Agta +controlledvocabulary.language.muak_sa-aak=Muak Sa-aak +controlledvocabulary.language.mualang=Mualang +controlledvocabulary.language.mubami=Mubami +controlledvocabulary.language.mubi=Mubi +controlledvocabulary.language.muda=Muda +controlledvocabulary.language.mudburra=Mudburra +controlledvocabulary.language.mudhili_gadaba=Mudhili Gadaba +controlledvocabulary.language.mudu_koraga=Mudu Koraga +controlledvocabulary.language.muduga=Muduga +controlledvocabulary.language.mufian=Mufian +controlledvocabulary.language.mugom=Mugom +controlledvocabulary.language.muinane=Muinane +controlledvocabulary.language.mukha-dora=Mukha-Dora +controlledvocabulary.language.mukulu=Mukulu +controlledvocabulary.language.mulaha=Mulaha +controlledvocabulary.language.mulam=Mulam +controlledvocabulary.language.mulao=Mulao +controlledvocabulary.language.mulgi=Mulgi +controlledvocabulary.language.mullu_kurumba=Mullu Kurumba +controlledvocabulary.language.multiple_languages=Multiple languages +controlledvocabulary.language.muluridyi=Muluridyi +controlledvocabulary.language.mum=Mum +controlledvocabulary.language.mumuye=Mumuye +controlledvocabulary.language.muna=Muna +controlledvocabulary.language.munda=Munda +controlledvocabulary.language.mundabli=Mundabli +controlledvocabulary.language.mundang=Mundang +controlledvocabulary.language.mundani=Mundani +controlledvocabulary.language.mundari=Mundari +controlledvocabulary.language.mundat=Mundat +controlledvocabulary.language.munduruku=Mundurukú +controlledvocabulary.language.mungaka=Mungaka +controlledvocabulary.language.munggui=Munggui +controlledvocabulary.language.mungkip=Mungkip +controlledvocabulary.language.muniche=Muniche +controlledvocabulary.language.munit=Munit +controlledvocabulary.language.munji=Munji +controlledvocabulary.language.munsee=Munsee +controlledvocabulary.language.muong=Muong +controlledvocabulary.language.mur_pano=Mur Pano +controlledvocabulary.language.muratayak=Muratayak +controlledvocabulary.language.murik_(malaysia)=Murik (Malaysia) +controlledvocabulary.language.murik_(papua_new_guinea)=Murik (Papua New Guinea) +controlledvocabulary.language.murkim=Murkim +controlledvocabulary.language.murle=Murle +controlledvocabulary.language.murrinh-patha=Murrinh-Patha +controlledvocabulary.language.mursi=Mursi +controlledvocabulary.language.murui_huitoto=Murui Huitoto +controlledvocabulary.language.murupi=Murupi +controlledvocabulary.language.muruwari=Muruwari +controlledvocabulary.language.musak=Musak +controlledvocabulary.language.musar=Musar +controlledvocabulary.language.musasa=Musasa +controlledvocabulary.language.musey=Musey +controlledvocabulary.language.musgu=Musgu +controlledvocabulary.language.mushungulu=Mushungulu +controlledvocabulary.language.musi=Musi +controlledvocabulary.language.muskum=Muskum +controlledvocabulary.language.muslim_tat=Muslim Tat +controlledvocabulary.language.musom=Musom +controlledvocabulary.language.mussau-emira=Mussau-Emira +controlledvocabulary.language.muthuvan=Muthuvan +controlledvocabulary.language.mutu=Mutu +controlledvocabulary.language.muyang=Muyang +controlledvocabulary.language.muyuw=Muyuw +controlledvocabulary.language.muzi=Muzi +controlledvocabulary.language.mvanip=Mvanip +controlledvocabulary.language.mvuba=Mvuba +controlledvocabulary.language.mwaghavul=Mwaghavul +controlledvocabulary.language.mwali_comorian=Mwali Comorian +controlledvocabulary.language.mwan=Mwan +controlledvocabulary.language.mwani=Mwani +controlledvocabulary.language.mwatebu=Mwatebu +controlledvocabulary.language.mwera_(chimwera)=Mwera (Chimwera) +controlledvocabulary.language.mwera_(nyasa)=Mwera (Nyasa) +controlledvocabulary.language.mwimbi-muthambi=Mwimbi-Muthambi +controlledvocabulary.language.myanmar_sign_language=Myanmar Sign Language +controlledvocabulary.language.mycenaean_greek=Mycenaean Greek +controlledvocabulary.language.myene=Myene +controlledvocabulary.language.mysian=Mysian +controlledvocabulary.language.mzieme_naga=Mzieme Naga +controlledvocabulary.language.maghdi=Mághdì +controlledvocabulary.language.maku=Máku +controlledvocabulary.language.menik=Ménik +controlledvocabulary.language.miskito=Mískito +controlledvocabulary.language.mocheno=Mócheno +controlledvocabulary.language.mun_chin=Mün Chin +controlledvocabulary.language.mundu=Mündü +controlledvocabulary.language.maharastri_prakrit=Māhārāṣṭri Prākrit +controlledvocabulary.language.maori=Māori, Maori +controlledvocabulary.language.n'ko=N'Ko +controlledvocabulary.language.na=Na +controlledvocabulary.language.na-kara=Na-kara +controlledvocabulary.language.naaba=Naaba +controlledvocabulary.language.naami=Naami +controlledvocabulary.language.naasioi=Naasioi +controlledvocabulary.language.naba=Naba +controlledvocabulary.language.nabak=Nabak +controlledvocabulary.language.nabi=Nabi +controlledvocabulary.language.nachering=Nachering +controlledvocabulary.language.nadruvian=Nadruvian +controlledvocabulary.language.nadeb=Nadëb +controlledvocabulary.language.nafaanra=Nafaanra +controlledvocabulary.language.nafi=Nafi +controlledvocabulary.language.nafri=Nafri +controlledvocabulary.language.nafusi=Nafusi +controlledvocabulary.language.naga_pidgin=Naga Pidgin +controlledvocabulary.language.nagarchal=Nagarchal +controlledvocabulary.language.nage=Nage +controlledvocabulary.language.nagumi=Nagumi +controlledvocabulary.language.nahali=Nahali +controlledvocabulary.language.nahari=Nahari +controlledvocabulary.language.nai=Nai +controlledvocabulary.language.najdi_arabic=Najdi Arabic +controlledvocabulary.language.naka'ela=Naka'ela +controlledvocabulary.language.nakai=Nakai +controlledvocabulary.language.nakame=Nakame +controlledvocabulary.language.nakanai=Nakanai +controlledvocabulary.language.nake=Nake +controlledvocabulary.language.naki=Naki +controlledvocabulary.language.nakwi=Nakwi +controlledvocabulary.language.nalca=Nalca +controlledvocabulary.language.nali=Nali +controlledvocabulary.language.nalik=Nalik +controlledvocabulary.language.nalu=Nalu +controlledvocabulary.language.naluo_yi=Naluo Yi +controlledvocabulary.language.nalogo=Nalögo +controlledvocabulary.language.nama_(papua_new_guinea)=Nama (Papua New Guinea) +controlledvocabulary.language.namakura=Namakura +controlledvocabulary.language.namat=Namat +controlledvocabulary.language.nambo=Nambo +controlledvocabulary.language.nambya=Nambya +controlledvocabulary.language.namia=Namia +controlledvocabulary.language.namiae=Namiae +controlledvocabulary.language.namibian_sign_language=Namibian Sign Language +controlledvocabulary.language.namla=Namla +controlledvocabulary.language.namo=Namo +controlledvocabulary.language.namonuito=Namonuito +controlledvocabulary.language.namosi-naitasiri-serua=Namosi-Naitasiri-Serua +controlledvocabulary.language.namuyi=Namuyi +controlledvocabulary.language.nanai=Nanai +controlledvocabulary.language.nancere=Nancere +controlledvocabulary.language.nande=Nande +controlledvocabulary.language.nandi=Nandi +controlledvocabulary.language.nanerige_senoufo=Nanerigé Sénoufo +controlledvocabulary.language.nanga_dama_dogon=Nanga Dama Dogon +controlledvocabulary.language.nankina=Nankina +controlledvocabulary.language.nanti=Nanti +controlledvocabulary.language.nanticoke=Nanticoke +controlledvocabulary.language.nanubae=Nanubae +controlledvocabulary.language.napo_lowland_quechua=Napo Lowland Quechua +controlledvocabulary.language.napu=Napu +controlledvocabulary.language.nar_phu=Nar Phu +controlledvocabulary.language.nara=Nara +controlledvocabulary.language.narak=Narak +controlledvocabulary.language.narango=Narango +controlledvocabulary.language.nari_nari=Nari Nari +controlledvocabulary.language.naro=Naro +controlledvocabulary.language.narom=Narom +controlledvocabulary.language.narragansett=Narragansett +controlledvocabulary.language.narua=Narua +controlledvocabulary.language.narungga=Narungga +controlledvocabulary.language.nasal=Nasal +controlledvocabulary.language.nasarian=Nasarian +controlledvocabulary.language.naskapi=Naskapi +controlledvocabulary.language.natanzi=Natanzi +controlledvocabulary.language.natchez=Natchez +controlledvocabulary.language.nateni=Nateni +controlledvocabulary.language.nathembo=Nathembo +controlledvocabulary.language.natioro=Natioro +controlledvocabulary.language.natugu=Natügu +controlledvocabulary.language.nauete=Nauete +controlledvocabulary.language.naukan_yupik=Naukan Yupik +controlledvocabulary.language.nauna=Nauna +controlledvocabulary.language.nauo=Nauo controlledvocabulary.language.nauru=Nauru controlledvocabulary.language.navajo,_navaho=Navajo, Navaho -controlledvocabulary.language.northern_ndebele=Northern Ndebele -controlledvocabulary.language.nepali=Nepali +controlledvocabulary.language.navut=Navut +controlledvocabulary.language.nawaru=Nawaru +controlledvocabulary.language.nawathinehena=Nawathinehena +controlledvocabulary.language.nawdm=Nawdm +controlledvocabulary.language.nawuri=Nawuri +controlledvocabulary.language.naxi=Naxi +controlledvocabulary.language.nayi=Nayi +controlledvocabulary.language.nayini=Nayini +controlledvocabulary.language.ncane=Ncane +controlledvocabulary.language.nchumbulu=Nchumbulu +controlledvocabulary.language.nda'nda'=Nda'nda' +controlledvocabulary.language.ndai=Ndai +controlledvocabulary.language.ndaka=Ndaka +controlledvocabulary.language.ndali=Ndali +controlledvocabulary.language.ndam=Ndam +controlledvocabulary.language.ndamba=Ndamba +controlledvocabulary.language.ndambomo=Ndambomo +controlledvocabulary.language.ndasa=Ndasa +controlledvocabulary.language.ndau=Ndau +controlledvocabulary.language.nde-gbite=Nde-Gbite +controlledvocabulary.language.nde-nsele-nta=Nde-Nsele-Nta +controlledvocabulary.language.ndemli=Ndemli +controlledvocabulary.language.ndendeule=Ndendeule +controlledvocabulary.language.ndengereko=Ndengereko +controlledvocabulary.language.nding=Nding +controlledvocabulary.language.ndo=Ndo +controlledvocabulary.language.ndobo=Ndobo +controlledvocabulary.language.ndoe=Ndoe +controlledvocabulary.language.ndogo=Ndogo +controlledvocabulary.language.ndolo=Ndolo +controlledvocabulary.language.ndom=Ndom +controlledvocabulary.language.ndombe=Ndombe +controlledvocabulary.language.ndonde_hamba=Ndonde Hamba controlledvocabulary.language.ndonga=Ndonga +controlledvocabulary.language.ndoola=Ndoola +controlledvocabulary.language.ndra'ngith=Ndra'ngith +controlledvocabulary.language.ndrulo=Ndrulo +controlledvocabulary.language.nduga=Nduga +controlledvocabulary.language.ndumu=Ndumu +controlledvocabulary.language.ndunda=Ndunda +controlledvocabulary.language.ndunga=Ndunga +controlledvocabulary.language.ndut=Ndut +controlledvocabulary.language.ndwewe=Ndwewe +controlledvocabulary.language.ndyuka-trio_pidgin=Ndyuka-Trio Pidgin +controlledvocabulary.language.ndzwani_comorian=Ndzwani Comorian +controlledvocabulary.language.neapolitan=Neapolitan +controlledvocabulary.language.nedebang=Nedebang +controlledvocabulary.language.nefamese=Nefamese +controlledvocabulary.language.negerhollands=Negerhollands +controlledvocabulary.language.negeri_sembilan_malay=Negeri Sembilan Malay +controlledvocabulary.language.negidal=Negidal +controlledvocabulary.language.nehan=Nehan +controlledvocabulary.language.nek=Nek +controlledvocabulary.language.nekgini=Nekgini +controlledvocabulary.language.neko=Neko +controlledvocabulary.language.neku=Neku +controlledvocabulary.language.nema=Nema +controlledvocabulary.language.neme=Neme +controlledvocabulary.language.nemi=Nemi +controlledvocabulary.language.nen=Nen +controlledvocabulary.language.nend=Nend +controlledvocabulary.language.nenets=Nenets +controlledvocabulary.language.nengone=Nengone +controlledvocabulary.language.neo=Neo +controlledvocabulary.language.neo-hittite=Neo-Hittite +controlledvocabulary.language.nepalese_sign_language=Nepalese Sign Language +controlledvocabulary.language.nepali_(individual_language)=Nepali (individual language) +controlledvocabulary.language.nepali=Nepali (macrolanguage), Nepali +controlledvocabulary.language.nete=Nete +controlledvocabulary.language.new_caledonian_javanese=New Caledonian Javanese +controlledvocabulary.language.new_zealand_sign_language=New Zealand Sign Language +controlledvocabulary.language.newari=Newari +controlledvocabulary.language.neyo=Neyo +controlledvocabulary.language.nez_perce=Nez Perce +controlledvocabulary.language.ngaanyatjarra=Ngaanyatjarra +controlledvocabulary.language.ngad'a=Ngad'a +controlledvocabulary.language.ngadjunmaya=Ngadjunmaya +controlledvocabulary.language.ngadjuri=Ngadjuri +controlledvocabulary.language.ngaing=Ngaing +controlledvocabulary.language.ngaju=Ngaju +controlledvocabulary.language.ngala=Ngala +controlledvocabulary.language.ngalakgan=Ngalakgan +controlledvocabulary.language.ngalum=Ngalum +controlledvocabulary.language.ngam=Ngam +controlledvocabulary.language.ngamambo=Ngamambo +controlledvocabulary.language.ngambay=Ngambay +controlledvocabulary.language.ngamini=Ngamini +controlledvocabulary.language.ngamo=Ngamo +controlledvocabulary.language.ngan'gityemerri=Ngan'gityemerri +controlledvocabulary.language.nganakarti=Nganakarti +controlledvocabulary.language.nganasan=Nganasan +controlledvocabulary.language.ngandi=Ngandi +controlledvocabulary.language.ngando_(central_african_republic)=Ngando (Central African Republic) +controlledvocabulary.language.ngando_(democratic_republic_of_congo)=Ngando (Democratic Republic of Congo) +controlledvocabulary.language.ngandyera=Ngandyera +controlledvocabulary.language.ngangam=Ngangam +controlledvocabulary.language.ngantangarra=Ngantangarra +controlledvocabulary.language.nganyaywana=Nganyaywana +controlledvocabulary.language.ngardi=Ngardi +controlledvocabulary.language.ngarigu=Ngarigu +controlledvocabulary.language.ngarinyin=Ngarinyin +controlledvocabulary.language.ngarinyman=Ngarinyman +controlledvocabulary.language.ngarla=Ngarla +controlledvocabulary.language.ngarluma=Ngarluma +controlledvocabulary.language.ngarrindjeri=Ngarrindjeri +controlledvocabulary.language.ngas=Ngas +controlledvocabulary.language.ngasa=Ngasa +controlledvocabulary.language.ngatik_men's_creole=Ngatik Men's Creole +controlledvocabulary.language.ngawn_chin=Ngawn Chin +controlledvocabulary.language.ngawun=Ngawun +controlledvocabulary.language.ngayawung=Ngayawung +controlledvocabulary.language.ngazidja_comorian=Ngazidja Comorian +controlledvocabulary.language.ngbaka=Ngbaka +controlledvocabulary.language.ngbaka_ma'bo=Ngbaka Ma'bo +controlledvocabulary.language.ngbaka_manza=Ngbaka Manza +controlledvocabulary.language.ngbee=Ngbee +controlledvocabulary.language.ngbinda=Ngbinda +controlledvocabulary.language.ngbundu=Ngbundu +controlledvocabulary.language.ngelima=Ngelima +controlledvocabulary.language.ngemba=Ngemba +controlledvocabulary.language.ngen=Ngen +controlledvocabulary.language.ngendelengo=Ngendelengo +controlledvocabulary.language.ngete=Ngete +controlledvocabulary.language.nggem=Nggem +controlledvocabulary.language.nggwahyi=Nggwahyi +controlledvocabulary.language.ngie=Ngie +controlledvocabulary.language.ngiemboon=Ngiemboon +controlledvocabulary.language.ngile=Ngile +controlledvocabulary.language.ngindo=Ngindo +controlledvocabulary.language.ngiti=Ngiti +controlledvocabulary.language.ngizim=Ngizim +controlledvocabulary.language.ngkalmpw_kanum=Ngkâlmpw Kanum +controlledvocabulary.language.ngom=Ngom +controlledvocabulary.language.ngomba=Ngomba +controlledvocabulary.language.ngombale=Ngombale +controlledvocabulary.language.ngombe_(central_african_republic)=Ngombe (Central African Republic) +controlledvocabulary.language.ngombe_(democratic_republic_of_congo)=Ngombe (Democratic Republic of Congo) +controlledvocabulary.language.ngongo=Ngongo +controlledvocabulary.language.ngoni_(mozambique)=Ngoni (Mozambique) +controlledvocabulary.language.ngoni_(tanzania)=Ngoni (Tanzania) +controlledvocabulary.language.ngoshie=Ngoshie +controlledvocabulary.language.ngul=Ngul +controlledvocabulary.language.ngulu=Ngulu +controlledvocabulary.language.nguluwan=Nguluwan +controlledvocabulary.language.ngumbarl=Ngumbarl +controlledvocabulary.language.ngumbi=Ngumbi +controlledvocabulary.language.ngunawal=Ngunawal +controlledvocabulary.language.ngundi=Ngundi +controlledvocabulary.language.ngundu=Ngundu +controlledvocabulary.language.ngungwel=Ngungwel +controlledvocabulary.language.ngurimi=Ngurimi +controlledvocabulary.language.ngurmbur=Ngurmbur +controlledvocabulary.language.nguon=Nguôn +controlledvocabulary.language.ngwaba=Ngwaba +controlledvocabulary.language.ngwe=Ngwe +controlledvocabulary.language.ngwo=Ngwo +controlledvocabulary.language.ngabere=Ngäbere +controlledvocabulary.language.nhanda=Nhanda +controlledvocabulary.language.nhengatu=Nhengatu +controlledvocabulary.language.nhirrpi=Nhirrpi +controlledvocabulary.language.nhuwala=Nhuwala +controlledvocabulary.language.nias=Nias +controlledvocabulary.language.nicaragua_creole_english=Nicaragua Creole English +controlledvocabulary.language.nicaraguan_sign_language=Nicaraguan Sign Language +controlledvocabulary.language.niellim=Niellim +controlledvocabulary.language.nigeria_mambila=Nigeria Mambila +controlledvocabulary.language.nigerian_fulfulde=Nigerian Fulfulde +controlledvocabulary.language.nigerian_pidgin=Nigerian Pidgin +controlledvocabulary.language.nigerian_sign_language=Nigerian Sign Language +controlledvocabulary.language.nihali=Nihali +controlledvocabulary.language.nii=Nii +controlledvocabulary.language.niksek=Niksek +controlledvocabulary.language.nila=Nila +controlledvocabulary.language.nilamba=Nilamba +controlledvocabulary.language.nimadi=Nimadi +controlledvocabulary.language.nimanbur=Nimanbur +controlledvocabulary.language.nimbari=Nimbari +controlledvocabulary.language.nimboran=Nimboran +controlledvocabulary.language.nimi=Nimi +controlledvocabulary.language.nimo=Nimo +controlledvocabulary.language.nimoa=Nimoa +controlledvocabulary.language.ninam=Ninam +controlledvocabulary.language.nindi=Nindi +controlledvocabulary.language.ningera=Ningera +controlledvocabulary.language.ninggerum=Ninggerum +controlledvocabulary.language.ningil=Ningil +controlledvocabulary.language.ninia_yali=Ninia Yali +controlledvocabulary.language.ninzo=Ninzo +controlledvocabulary.language.nipsan=Nipsan +controlledvocabulary.language.nisa=Nisa +controlledvocabulary.language.nisenan=Nisenan +controlledvocabulary.language.nisga'a=Nisga'a +controlledvocabulary.language.nisi_(china)=Nisi (China) +controlledvocabulary.language.niuafo'ou=Niuafo'ou +controlledvocabulary.language.niuatoputapu=Niuatoputapu +controlledvocabulary.language.niuean=Niuean +controlledvocabulary.language.nivacle=Nivaclé +controlledvocabulary.language.niwer_mil=Niwer Mil +controlledvocabulary.language.njalgulgule=Njalgulgule +controlledvocabulary.language.njebi=Njebi +controlledvocabulary.language.njen=Njen +controlledvocabulary.language.njerep=Njerep +controlledvocabulary.language.njyem=Njyem +controlledvocabulary.language.nkami=Nkami +controlledvocabulary.language.nkangala=Nkangala +controlledvocabulary.language.nkari=Nkari +controlledvocabulary.language.nkem-nkum=Nkem-Nkum +controlledvocabulary.language.nkhumbi=Nkhumbi +controlledvocabulary.language.nkongho=Nkongho +controlledvocabulary.language.nkonya=Nkonya +controlledvocabulary.language.nkoroo=Nkoroo +controlledvocabulary.language.nkoya=Nkoya +controlledvocabulary.language.nkukoli=Nkukoli +controlledvocabulary.language.nkutu=Nkutu +controlledvocabulary.language.nnam=Nnam +controlledvocabulary.language.no_linguistic_content=No linguistic content +controlledvocabulary.language.nobiin=Nobiin +controlledvocabulary.language.nobonob=Nobonob +controlledvocabulary.language.nocte_naga=Nocte Naga +controlledvocabulary.language.nogai=Nogai +controlledvocabulary.language.noipx=Noipx +controlledvocabulary.language.noiri=Noiri +controlledvocabulary.language.nokuku=Nokuku +controlledvocabulary.language.nomaande=Nomaande +controlledvocabulary.language.nomane=Nomane +controlledvocabulary.language.nomatsiguenga=Nomatsiguenga +controlledvocabulary.language.nomlaki=Nomlaki +controlledvocabulary.language.nomu=Nomu +controlledvocabulary.language.nong_zhuang=Nong Zhuang +controlledvocabulary.language.nonuya=Nonuya +controlledvocabulary.language.nooksack=Nooksack +controlledvocabulary.language.noon=Noon +controlledvocabulary.language.noone=Noone +controlledvocabulary.language.nopala_chatino=Nopala Chatino +controlledvocabulary.language.noric=Noric +controlledvocabulary.language.norn=Norn +controlledvocabulary.language.norra=Norra +controlledvocabulary.language.north_alaskan_inupiatun=North Alaskan Inupiatun +controlledvocabulary.language.north_ambrym=North Ambrym +controlledvocabulary.language.north_asmat=North Asmat +controlledvocabulary.language.north_awyu=North Awyu +controlledvocabulary.language.north_azerbaijani=North Azerbaijani +controlledvocabulary.language.north_babar=North Babar +controlledvocabulary.language.north_bolivian_quechua=North Bolivian Quechua +controlledvocabulary.language.north_central_mixe=North Central Mixe +controlledvocabulary.language.north_efate=North Efate +controlledvocabulary.language.north_fali=North Fali +controlledvocabulary.language.north_giziga=North Giziga +controlledvocabulary.language.north_junin_quechua=North Junín Quechua +controlledvocabulary.language.north_marquesan=North Marquesan +controlledvocabulary.language.north_mesopotamian_arabic=North Mesopotamian Arabic +controlledvocabulary.language.north_midlands_tasmanian=North Midlands Tasmanian +controlledvocabulary.language.north_mofu=North Mofu +controlledvocabulary.language.north_moluccan_malay=North Moluccan Malay +controlledvocabulary.language.north_muyu=North Muyu +controlledvocabulary.language.north_nuaulu=North Nuaulu +controlledvocabulary.language.north_picene=North Picene +controlledvocabulary.language.north_slavey=North Slavey +controlledvocabulary.language.north_tairora=North Tairora +controlledvocabulary.language.north_tanna=North Tanna +controlledvocabulary.language.north_wahgi=North Wahgi +controlledvocabulary.language.north_watut=North Watut +controlledvocabulary.language.northeast_kiwai=Northeast Kiwai +controlledvocabulary.language.northeast_maidu=Northeast Maidu +controlledvocabulary.language.northeast_pashai=Northeast Pashai +controlledvocabulary.language.northeastern_dinka=Northeastern Dinka +controlledvocabulary.language.northeastern_pomo=Northeastern Pomo +controlledvocabulary.language.northeastern_tasmanian=Northeastern Tasmanian +controlledvocabulary.language.northeastern_thai=Northeastern Thai +controlledvocabulary.language.northern_alta=Northern Alta +controlledvocabulary.language.northern_altai=Northern Altai +controlledvocabulary.language.northern_amami-oshima=Northern Amami-Oshima +controlledvocabulary.language.northern_betsimisaraka_malagasy=Northern Betsimisaraka Malagasy +controlledvocabulary.language.northern_binukidnon=Northern Binukidnon +controlledvocabulary.language.northern_bobo_madare=Northern Bobo Madaré +controlledvocabulary.language.northern_bontok=Northern Bontok +controlledvocabulary.language.northern_catanduanes_bikol=Northern Catanduanes Bikol +controlledvocabulary.language.northern_conchucos_ancash_quechua=Northern Conchucos Ancash Quechua +controlledvocabulary.language.northern_dagara=Northern Dagara +controlledvocabulary.language.northern_dong=Northern Dong +controlledvocabulary.language.northern_east_cree=Northern East Cree +controlledvocabulary.language.northern_embera=Northern Emberá +controlledvocabulary.language.northern_frisian=Northern Frisian +controlledvocabulary.language.northern_ghale=Northern Ghale +controlledvocabulary.language.northern_gondi=Northern Gondi +controlledvocabulary.language.northern_grebo=Northern Grebo +controlledvocabulary.language.northern_guiyang_hmong=Northern Guiyang Hmong +controlledvocabulary.language.northern_haida=Northern Haida +controlledvocabulary.language.northern_hindko=Northern Hindko +controlledvocabulary.language.northern_huishui_hmong=Northern Huishui Hmong +controlledvocabulary.language.northern_kalapuya=Northern Kalapuya +controlledvocabulary.language.northern_kankanay=Northern Kankanay +controlledvocabulary.language.northern_katang=Northern Katang +controlledvocabulary.language.northern_khmer=Northern Khmer +controlledvocabulary.language.northern_kissi=Northern Kissi +controlledvocabulary.language.northern_kurdish=Northern Kurdish +controlledvocabulary.language.northern_luri=Northern Luri +controlledvocabulary.language.northern_mashan_hmong=Northern Mashan Hmong +controlledvocabulary.language.northern_muji=Northern Muji +controlledvocabulary.language.northern_nago=Northern Nago +controlledvocabulary.language.northern_ndebele=Northern Ndebele, North Ndebele +controlledvocabulary.language.northern_ngbandi=Northern Ngbandi +controlledvocabulary.language.northern_nisu=Northern Nisu +controlledvocabulary.language.northern_nuni=Northern Nuni +controlledvocabulary.language.northern_oaxaca_nahuatl=Northern Oaxaca Nahuatl +controlledvocabulary.language.northern_ohlone=Northern Ohlone +controlledvocabulary.language.northern_one=Northern One +controlledvocabulary.language.northern_paiute=Northern Paiute +controlledvocabulary.language.northern_pame=Northern Pame +controlledvocabulary.language.northern_pashto=Northern Pashto +controlledvocabulary.language.northern_pastaza_quichua=Northern Pastaza Quichua +controlledvocabulary.language.northern_ping_chinese=Northern Ping Chinese +controlledvocabulary.language.northern_pomo=Northern Pomo +controlledvocabulary.language.northern_puebla_nahuatl=Northern Puebla Nahuatl +controlledvocabulary.language.northern_pumi=Northern Pumi +controlledvocabulary.language.northern_qiandong_miao=Northern Qiandong Miao +controlledvocabulary.language.northern_qiang=Northern Qiang +controlledvocabulary.language.northern_rengma_naga=Northern Rengma Naga +controlledvocabulary.language.northern_roglai=Northern Roglai +controlledvocabulary.language.northern_sami=Northern Sami +controlledvocabulary.language.northern_sierra_miwok=Northern Sierra Miwok +controlledvocabulary.language.northern_sorsoganon=Northern Sorsoganon +controlledvocabulary.language.northern_subanen=Northern Subanen +controlledvocabulary.language.northern_tarahumara=Northern Tarahumara +controlledvocabulary.language.northern_tasmanian=Northern Tasmanian +controlledvocabulary.language.northern_tepehuan=Northern Tepehuan +controlledvocabulary.language.northern_thai=Northern Thai +controlledvocabulary.language.northern_tidung=Northern Tidung +controlledvocabulary.language.northern_tiwa=Northern Tiwa +controlledvocabulary.language.northern_tlaxiaco_mixtec=Northern Tlaxiaco Mixtec +controlledvocabulary.language.northern_toussian=Northern Toussian +controlledvocabulary.language.northern_tujia=Northern Tujia +controlledvocabulary.language.northern_tutchone=Northern Tutchone +controlledvocabulary.language.northern_uzbek=Northern Uzbek +controlledvocabulary.language.northern_yukaghir=Northern Yukaghir +controlledvocabulary.language.northwest_alaska_inupiatun=Northwest Alaska Inupiatun +controlledvocabulary.language.northwest_gbaya=Northwest Gbaya +controlledvocabulary.language.northwest_maidu=Northwest Maidu +controlledvocabulary.language.northwest_oaxaca_mixtec=Northwest Oaxaca Mixtec +controlledvocabulary.language.northwest_pashai=Northwest Pashai +controlledvocabulary.language.northwestern_dinka=Northwestern Dinka +controlledvocabulary.language.northwestern_fars=Northwestern Fars +controlledvocabulary.language.northwestern_kolami=Northwestern Kolami +controlledvocabulary.language.northwestern_nisu=Northwestern Nisu +controlledvocabulary.language.northwestern_ojibwa=Northwestern Ojibwa +controlledvocabulary.language.northwestern_tasmanian=Northwestern Tasmanian +controlledvocabulary.language.norwegian=Norwegian controlledvocabulary.language.norwegian_bokmal=Norwegian Bokmål controlledvocabulary.language.norwegian_nynorsk=Norwegian Nynorsk -controlledvocabulary.language.norwegian=Norwegian -controlledvocabulary.language.nuosu=Nuosu -controlledvocabulary.language.southern_ndebele=Southern Ndebele -controlledvocabulary.language.occitan=Occitan +controlledvocabulary.language.norwegian_sign_language=Norwegian Sign Language +controlledvocabulary.language.notre=Notre +controlledvocabulary.language.notsi=Notsi +controlledvocabulary.language.nottoway=Nottoway +controlledvocabulary.language.nottoway-meherrin=Nottoway-Meherrin +controlledvocabulary.language.novial=Novial +controlledvocabulary.language.noy=Noy +controlledvocabulary.language.nsenga=Nsenga +controlledvocabulary.language.nshi=Nshi +controlledvocabulary.language.nsongo=Nsongo +controlledvocabulary.language.ntcham=Ntcham +controlledvocabulary.language.nteng=Nteng +controlledvocabulary.language.ntomba=Ntomba +controlledvocabulary.language.nubaca=Nubaca +controlledvocabulary.language.nubi=Nubi +controlledvocabulary.language.nubri=Nubri +controlledvocabulary.language.nuer=Nuer +controlledvocabulary.language.nugunu_(australia)=Nugunu (Australia) +controlledvocabulary.language.nugunu_(cameroon)=Nugunu (Cameroon) +controlledvocabulary.language.nuk=Nuk +controlledvocabulary.language.nukak_maku=Nukak Makú +controlledvocabulary.language.nukna=Nukna +controlledvocabulary.language.nukuini=Nukuini +controlledvocabulary.language.nukumanu=Nukumanu +controlledvocabulary.language.nukunul=Nukunul +controlledvocabulary.language.nukuoro=Nukuoro +controlledvocabulary.language.nukuria=Nukuria +controlledvocabulary.language.numana=Numana +controlledvocabulary.language.numanggang=Numanggang +controlledvocabulary.language.numbami=Numbami +controlledvocabulary.language.nume=Nume +controlledvocabulary.language.numidian=Numidian +controlledvocabulary.language.numee=Numèè +controlledvocabulary.language.nung_(viet_nam)=Nung (Viet Nam) +controlledvocabulary.language.nungali=Nungali +controlledvocabulary.language.nunggubuyu=Nunggubuyu +controlledvocabulary.language.nungu=Nungu +controlledvocabulary.language.nupbikha=Nupbikha +controlledvocabulary.language.nupe-nupe-tako=Nupe-Nupe-Tako +controlledvocabulary.language.nusa_laut=Nusa Laut +controlledvocabulary.language.nusu=Nusu +controlledvocabulary.language.nuu-chah-nulth=Nuu-chah-nulth +controlledvocabulary.language.nyabwa=Nyabwa +controlledvocabulary.language.nyaheun=Nyaheun +controlledvocabulary.language.nyahkur=Nyahkur +controlledvocabulary.language.nyakyusa-ngonde=Nyakyusa-Ngonde +controlledvocabulary.language.nyali=Nyali +controlledvocabulary.language.nyam=Nyam +controlledvocabulary.language.nyamal=Nyamal +controlledvocabulary.language.nyambo=Nyambo +controlledvocabulary.language.nyamusa-molo=Nyamusa-Molo +controlledvocabulary.language.nyamwanga=Nyamwanga +controlledvocabulary.language.nyamwezi=Nyamwezi +controlledvocabulary.language.nyaneka=Nyaneka +controlledvocabulary.language.nyang'i=Nyang'i +controlledvocabulary.language.nyanga=Nyanga +controlledvocabulary.language.nyanga-li=Nyanga-li +controlledvocabulary.language.nyangatom=Nyangatom +controlledvocabulary.language.nyangbo=Nyangbo +controlledvocabulary.language.nyangga=Nyangga +controlledvocabulary.language.nyangumarta=Nyangumarta +controlledvocabulary.language.nyankole=Nyankole +controlledvocabulary.language.nyankpa=Nyankpa +controlledvocabulary.language.nyarafolo_senoufo=Nyarafolo Senoufo +controlledvocabulary.language.nyaturu=Nyaturu +controlledvocabulary.language.nyaw=Nyaw +controlledvocabulary.language.nyawaygi=Nyawaygi +controlledvocabulary.language.nyemba=Nyemba +controlledvocabulary.language.nyengo=Nyengo +controlledvocabulary.language.nyenkha=Nyenkha +controlledvocabulary.language.nyeu=Nyeu +controlledvocabulary.language.nyiha_(malawi)=Nyiha (Malawi) +controlledvocabulary.language.nyiha_(tanzania)=Nyiha (Tanzania) +controlledvocabulary.language.nyika_(malawi_and_zambia)=Nyika (Malawi and Zambia) +controlledvocabulary.language.nyika_(tanzania)=Nyika (Tanzania) +controlledvocabulary.language.nyikina=Nyikina +controlledvocabulary.language.nyindrou=Nyindrou +controlledvocabulary.language.nyindu=Nyindu +controlledvocabulary.language.nyishi=Nyishi +controlledvocabulary.language.nyiyaparli=Nyiyaparli +controlledvocabulary.language.nyokon=Nyokon +controlledvocabulary.language.nyole=Nyole +controlledvocabulary.language.nyong=Nyong +controlledvocabulary.language.nyore=Nyore +controlledvocabulary.language.nyoro=Nyoro +controlledvocabulary.language.nyulnyul=Nyulnyul +controlledvocabulary.language.nyungar=Nyungar +controlledvocabulary.language.nyungwe=Nyungwe +controlledvocabulary.language.nyalayu=Nyâlayu +controlledvocabulary.language.nzadi=Nzadi +controlledvocabulary.language.nzakambay=Nzakambay +controlledvocabulary.language.nzakara=Nzakara +controlledvocabulary.language.nzanyi=Nzanyi +controlledvocabulary.language.nzima=Nzima +controlledvocabulary.language.na-meo=Ná-Meo +controlledvocabulary.language.nelemwa-nixumwak=Nêlêmwa-Nixumwak +controlledvocabulary.language.nupode_huitoto=Nüpode Huitoto +controlledvocabulary.language.nǁng=Nǁng +controlledvocabulary.language.o'chi'chi'=O'chi'chi' +controlledvocabulary.language.o'du=O'du +controlledvocabulary.language.obanliku=Obanliku +controlledvocabulary.language.obispeno=Obispeño +controlledvocabulary.language.oblo=Oblo +controlledvocabulary.language.obo_manobo=Obo Manobo +controlledvocabulary.language.obokuitai=Obokuitai +controlledvocabulary.language.obolo=Obolo +controlledvocabulary.language.obulom=Obulom +controlledvocabulary.language.ocaina=Ocaina +controlledvocabulary.language.occitan=Occitan (post 1500), Occitan +controlledvocabulary.language.ocotepec_mixtec=Ocotepec Mixtec +controlledvocabulary.language.ocotlan_zapotec=Ocotlán Zapotec +controlledvocabulary.language.od=Od +controlledvocabulary.language.odia=Odia +controlledvocabulary.language.odiai=Odiai +controlledvocabulary.language.odoodee=Odoodee +controlledvocabulary.language.odual=Odual +controlledvocabulary.language.odut=Odut +controlledvocabulary.language.ofaye=Ofayé +controlledvocabulary.language.official_aramaic_(700-300_bce)=Official Aramaic (700-300 BCE) +controlledvocabulary.language.ofo=Ofo +controlledvocabulary.language.ogbah=Ogbah +controlledvocabulary.language.ogbia=Ogbia +controlledvocabulary.language.ogbogolo=Ogbogolo +controlledvocabulary.language.ogbronuagum=Ogbronuagum +controlledvocabulary.language.ogea=Ogea +controlledvocabulary.language.oirata=Oirata controlledvocabulary.language.ojibwe,_ojibwa=Ojibwe, Ojibwa -controlledvocabulary.language.old_church_slavonic,church_slavonic,old_bulgarian=Old Church Slavonic,Church Slavonic,Old Bulgarian +controlledvocabulary.language.ojitlan_chinantec=Ojitlán Chinantec +controlledvocabulary.language.okanagan=Okanagan +controlledvocabulary.language.oki-no-erabu=Oki-No-Erabu +controlledvocabulary.language.okiek=Okiek +controlledvocabulary.language.oko-eni-osayen=Oko-Eni-Osayen +controlledvocabulary.language.oko-juwoi=Oko-Juwoi +controlledvocabulary.language.okobo=Okobo +controlledvocabulary.language.okodia=Okodia +controlledvocabulary.language.okolie=Okolie +controlledvocabulary.language.okolod=Okolod +controlledvocabulary.language.okpamheri=Okpamheri +controlledvocabulary.language.okpe_(northwestern_edo)=Okpe (Northwestern Edo) +controlledvocabulary.language.okpe_(southwestern_edo)=Okpe (Southwestern Edo) +controlledvocabulary.language.oksapmin=Oksapmin +controlledvocabulary.language.oku=Oku +controlledvocabulary.language.old_aramaic_(up_to_700_bce)=Old Aramaic (up to 700 BCE) +controlledvocabulary.language.old_avar=Old Avar +controlledvocabulary.language.old_breton=Old Breton +controlledvocabulary.language.old_burmese=Old Burmese +controlledvocabulary.language.old_cham=Old Cham +controlledvocabulary.language.old_chinese=Old Chinese +controlledvocabulary.language.old_cornish=Old Cornish +controlledvocabulary.language.old_dutch=Old Dutch +controlledvocabulary.language.old_english_(ca._450-1100)=Old English (ca. 450-1100) +controlledvocabulary.language.old_french_(842-ca._1400)=Old French (842-ca. 1400) +controlledvocabulary.language.old_frisian=Old Frisian +controlledvocabulary.language.old_georgian=Old Georgian +controlledvocabulary.language.old_high_german_(ca._750-1050)=Old High German (ca. 750-1050) +controlledvocabulary.language.old_hittite=Old Hittite +controlledvocabulary.language.old_hungarian=Old Hungarian +controlledvocabulary.language.old_irish_(to_900)=Old Irish (to 900) +controlledvocabulary.language.old_japanese=Old Japanese +controlledvocabulary.language.old_kentish_sign_language=Old Kentish Sign Language +controlledvocabulary.language.old_khmer=Old Khmer +controlledvocabulary.language.old_korean_(3rd-9th_cent.)=Old Korean (3rd-9th cent.) +controlledvocabulary.language.old_lithuanian=Old Lithuanian +controlledvocabulary.language.old_malay=Old Malay +controlledvocabulary.language.old_manipuri=Old Manipuri +controlledvocabulary.language.old_marathi=Old Marathi +controlledvocabulary.language.old_mon=Old Mon +controlledvocabulary.language.old_norse=Old Norse +controlledvocabulary.language.old_nubian=Old Nubian +controlledvocabulary.language.old_ossetic=Old Ossetic +controlledvocabulary.language.old_persian_(ca._600-400_b.c.)=Old Persian (ca. 600-400 B.C.) +controlledvocabulary.language.old_provencal_(to_1500)=Old Provençal (to 1500) +controlledvocabulary.language.old_russian=Old Russian +controlledvocabulary.language.old_saxon=Old Saxon +controlledvocabulary.language.old_spanish=Old Spanish +controlledvocabulary.language.old_sundanese=Old Sundanese +controlledvocabulary.language.old_tamil=Old Tamil +controlledvocabulary.language.old_tibetan=Old Tibetan +controlledvocabulary.language.old_turkish=Old Turkish +controlledvocabulary.language.old_uighur=Old Uighur +controlledvocabulary.language.old_welsh=Old Welsh +controlledvocabulary.language.olekha=Olekha +controlledvocabulary.language.olkol=Olkol +controlledvocabulary.language.olo=Olo +controlledvocabulary.language.oloma=Oloma +controlledvocabulary.language.olrat=Olrat +controlledvocabulary.language.olu'bo=Olu'bo +controlledvocabulary.language.olulumo-ikom=Olulumo-Ikom +controlledvocabulary.language.oluta_popoluca=Oluta Popoluca +controlledvocabulary.language.omagua=Omagua +controlledvocabulary.language.omaha-ponca=Omaha-Ponca +controlledvocabulary.language.omani_arabic=Omani Arabic +controlledvocabulary.language.ombamba=Ombamba +controlledvocabulary.language.ombo=Ombo +controlledvocabulary.language.ometepec_nahuatl=Ometepec Nahuatl +controlledvocabulary.language.omi=Omi +controlledvocabulary.language.omok=Omok +controlledvocabulary.language.omotik=Omotik +controlledvocabulary.language.omurano=Omurano +controlledvocabulary.language.ona=Ona +controlledvocabulary.language.oneida=Oneida +controlledvocabulary.language.ong=Ong +controlledvocabulary.language.onin=Onin +controlledvocabulary.language.onin_based_pidgin=Onin Based Pidgin +controlledvocabulary.language.onjob=Onjob +controlledvocabulary.language.ono=Ono +controlledvocabulary.language.onobasulu=Onobasulu +controlledvocabulary.language.onondaga=Onondaga +controlledvocabulary.language.ontenu=Ontenu +controlledvocabulary.language.ontong_java=Ontong Java +controlledvocabulary.language.oorlams=Oorlams +controlledvocabulary.language.opao=Opao +controlledvocabulary.language.opata=Opata +controlledvocabulary.language.orang_kanaq=Orang Kanaq +controlledvocabulary.language.orang_seletar=Orang Seletar +controlledvocabulary.language.oraon_sadri=Oraon Sadri +controlledvocabulary.language.orejon=Orejón +controlledvocabulary.language.oring=Oring +controlledvocabulary.language.oriya=Oriya, Oriya (macrolanguage) +controlledvocabulary.language.orizaba_nahuatl=Orizaba Nahuatl +controlledvocabulary.language.orma=Orma +controlledvocabulary.language.ormu=Ormu +controlledvocabulary.language.ormuri=Ormuri +controlledvocabulary.language.oro=Oro +controlledvocabulary.language.oro_win=Oro Win +controlledvocabulary.language.oroch=Oroch +controlledvocabulary.language.oroha=Oroha +controlledvocabulary.language.orok=Orok +controlledvocabulary.language.orokaiva=Orokaiva +controlledvocabulary.language.oroko=Oroko +controlledvocabulary.language.orokolo=Orokolo controlledvocabulary.language.oromo=Oromo -controlledvocabulary.language.oriya=Oriya +controlledvocabulary.language.oroqen=Oroqen +controlledvocabulary.language.orowe=Orowe +controlledvocabulary.language.oruma=Oruma +controlledvocabulary.language.orya=Orya +controlledvocabulary.language.osage=Osage +controlledvocabulary.language.osatu=Osatu +controlledvocabulary.language.oscan=Oscan +controlledvocabulary.language.osing=Osing +controlledvocabulary.language.ososo=Ososo controlledvocabulary.language.ossetian,_ossetic=Ossetian, Ossetic +controlledvocabulary.language.ot_danum=Ot Danum +controlledvocabulary.language.otank=Otank +controlledvocabulary.language.oti=Oti +controlledvocabulary.language.otoro=Otoro +controlledvocabulary.language.ottawa=Ottawa +controlledvocabulary.language.ottoman_turkish_(1500-1928)=Ottoman Turkish (1500-1928) +controlledvocabulary.language.otuho=Otuho +controlledvocabulary.language.otuke=Otuke +controlledvocabulary.language.ouma=Ouma +controlledvocabulary.language.oune=Oune +controlledvocabulary.language.owa=Owa +controlledvocabulary.language.owenia=Owenia +controlledvocabulary.language.owiniga=Owiniga +controlledvocabulary.language.oy=Oy +controlledvocabulary.language.oya'oya=Oya'oya +controlledvocabulary.language.oyda=Oyda +controlledvocabulary.language.oyster_bay_tasmanian=Oyster Bay Tasmanian +controlledvocabulary.language.ozolotepec_zapotec=Ozolotepec Zapotec +controlledvocabulary.language.ozumacin_chinantec=Ozumacín Chinantec +controlledvocabulary.language.pa_di=Pa Di +controlledvocabulary.language.pa'a=Pa'a +controlledvocabulary.language.pa'o_karen=Pa'o Karen +controlledvocabulary.language.pa-hng=Pa-Hng +controlledvocabulary.language.paakantyi=Paakantyi +controlledvocabulary.language.paama=Paama +controlledvocabulary.language.paasaal=Paasaal +controlledvocabulary.language.pacahuara=Pacahuara +controlledvocabulary.language.pacaraos_quechua=Pacaraos Quechua +controlledvocabulary.language.pacific_gulf_yupik=Pacific Gulf Yupik +controlledvocabulary.language.pacoh=Pacoh +controlledvocabulary.language.padoe=Padoe +controlledvocabulary.language.paekche=Paekche +controlledvocabulary.language.paelignian=Paelignian +controlledvocabulary.language.pagi=Pagi +controlledvocabulary.language.pagibete=Pagibete +controlledvocabulary.language.pagu=Pagu +controlledvocabulary.language.pahanan_agta=Pahanan Agta +controlledvocabulary.language.pahari=Pahari +controlledvocabulary.language.pahari-potwari=Pahari-Potwari +controlledvocabulary.language.pahi=Pahi +controlledvocabulary.language.pahlavani=Pahlavani +controlledvocabulary.language.pahlavi=Pahlavi +controlledvocabulary.language.pai_tavytera=Pai Tavytera +controlledvocabulary.language.paici=Paicî +controlledvocabulary.language.paipai=Paipai +controlledvocabulary.language.paite_chin=Paite Chin +controlledvocabulary.language.paiwan=Paiwan +controlledvocabulary.language.pak-tong=Pak-Tong +controlledvocabulary.language.pakanha=Pakanha +controlledvocabulary.language.pakaasnovos=Pakaásnovos +controlledvocabulary.language.pakistan_sign_language=Pakistan Sign Language +controlledvocabulary.language.paku=Paku +controlledvocabulary.language.paku_karen=Paku Karen +controlledvocabulary.language.pal=Pal +controlledvocabulary.language.palaic=Palaic +controlledvocabulary.language.palaka_senoufo=Palaka Senoufo +controlledvocabulary.language.palantla_chinantec=Palantla Chinantec +controlledvocabulary.language.palauan=Palauan +controlledvocabulary.language.paleni=Paleni +controlledvocabulary.language.palenquero=Palenquero +controlledvocabulary.language.palikur=Palikúr +controlledvocabulary.language.paliyan=Paliyan +controlledvocabulary.language.pallanganmiddang=Pallanganmiddang +controlledvocabulary.language.paloor=Paloor +controlledvocabulary.language.palu'e=Palu'e +controlledvocabulary.language.paluan=Paluan +controlledvocabulary.language.palya_bareli=Palya Bareli +controlledvocabulary.language.pam=Pam +controlledvocabulary.language.pambia=Pambia +controlledvocabulary.language.pamona=Pamona +controlledvocabulary.language.pamosu=Pamosu +controlledvocabulary.language.pampanga=Pampanga +controlledvocabulary.language.pamplona_atta=Pamplona Atta +controlledvocabulary.language.pana_(burkina_faso)=Pana (Burkina Faso) +controlledvocabulary.language.pana_(central_african_republic)=Pana (Central African Republic) +controlledvocabulary.language.panamanian_sign_language=Panamanian Sign Language +controlledvocabulary.language.panamint=Panamint +controlledvocabulary.language.panao_huanuco_quechua=Panao Huánuco Quechua +controlledvocabulary.language.panara=Panará +controlledvocabulary.language.panasuan=Panasuan +controlledvocabulary.language.panawa=Panawa +controlledvocabulary.language.pancana=Pancana +controlledvocabulary.language.panchpargania=Panchpargania +controlledvocabulary.language.pande=Pande +controlledvocabulary.language.pangasinan=Pangasinan +controlledvocabulary.language.pangseng=Pangseng +controlledvocabulary.language.pangu=Pangu +controlledvocabulary.language.pangutaran_sama=Pangutaran Sama +controlledvocabulary.language.pangwa=Pangwa +controlledvocabulary.language.pangwali=Pangwali +controlledvocabulary.language.panim=Panim +controlledvocabulary.language.paniya=Paniya controlledvocabulary.language.panjabi,_punjabi=Panjabi, Punjabi -controlledvocabulary.language.pali=P\u0101li -controlledvocabulary.language.persian_(farsi)=Persian (Farsi) -controlledvocabulary.language.polish=Polish +controlledvocabulary.language.pankarare=Pankararé +controlledvocabulary.language.pankararu=Pankararú +controlledvocabulary.language.pankhu=Pankhu +controlledvocabulary.language.pannei=Pannei +controlledvocabulary.language.pano=Pano +controlledvocabulary.language.panoan_katukina=Panoan Katukína +controlledvocabulary.language.panobo=Panobo +controlledvocabulary.language.panyi_bai=Panyi Bai +controlledvocabulary.language.papantla_totonac=Papantla Totonac +controlledvocabulary.language.papapana=Papapana +controlledvocabulary.language.papar=Papar +controlledvocabulary.language.papasena=Papasena +controlledvocabulary.language.papel=Papel +controlledvocabulary.language.papi=Papi +controlledvocabulary.language.papiamento=Papiamento +controlledvocabulary.language.papora=Papora +controlledvocabulary.language.papua_new_guinean_sign_language=Papua New Guinean Sign Language +controlledvocabulary.language.papuan_malay=Papuan Malay +controlledvocabulary.language.papuma=Papuma +controlledvocabulary.language.parachi=Parachi +controlledvocabulary.language.paraguayan_guarani=Paraguayan Guaraní +controlledvocabulary.language.paraguayan_sign_language=Paraguayan Sign Language +controlledvocabulary.language.parakana=Parakanã +controlledvocabulary.language.paranan=Paranan +controlledvocabulary.language.paranawat=Paranawát +controlledvocabulary.language.paraujano=Paraujano +controlledvocabulary.language.parauk=Parauk +controlledvocabulary.language.parawen=Parawen +controlledvocabulary.language.pardhan=Pardhan +controlledvocabulary.language.pardhi=Pardhi +controlledvocabulary.language.pare=Pare +controlledvocabulary.language.parecis=Parecís +controlledvocabulary.language.parenga=Parenga +controlledvocabulary.language.parkari_koli=Parkari Koli +controlledvocabulary.language.parkwa=Parkwa +controlledvocabulary.language.parsi-dari=Parsi-Dari +controlledvocabulary.language.parthian=Parthian +controlledvocabulary.language.parya=Parya +controlledvocabulary.language.para_arara=Pará Arára +controlledvocabulary.language.para_gaviao=Pará Gavião controlledvocabulary.language.pashto,_pushto=Pashto, Pushto +controlledvocabulary.language.pasi=Pasi +controlledvocabulary.language.pass_valley_yali=Pass Valley Yali +controlledvocabulary.language.patamona=Patamona +controlledvocabulary.language.patani=Patani +controlledvocabulary.language.pataxo_ha-ha-hae=Pataxó Hã-Ha-Hãe +controlledvocabulary.language.patep=Patep +controlledvocabulary.language.pathiya=Pathiya +controlledvocabulary.language.patpatar=Patpatar +controlledvocabulary.language.pattani=Pattani +controlledvocabulary.language.pattani_malay=Pattani Malay +controlledvocabulary.language.pattapu=Pattapu +controlledvocabulary.language.patwin=Patwin +controlledvocabulary.language.paulohi=Paulohi +controlledvocabulary.language.paumari=Paumarí +controlledvocabulary.language.paunaka=Paunaka +controlledvocabulary.language.pauri_bareli=Pauri Bareli +controlledvocabulary.language.pauserna=Pauserna +controlledvocabulary.language.pawaia=Pawaia +controlledvocabulary.language.pawnee=Pawnee +controlledvocabulary.language.paynamar=Paynamar +controlledvocabulary.language.pazeh=Pazeh +controlledvocabulary.language.pe=Pe +controlledvocabulary.language.pear=Pear +controlledvocabulary.language.pech=Pech +controlledvocabulary.language.pecheneg=Pecheneg +controlledvocabulary.language.pedi=Pedi +controlledvocabulary.language.pei=Pei +controlledvocabulary.language.pekal=Pekal +controlledvocabulary.language.pela=Pela +controlledvocabulary.language.pele-ata=Pele-Ata +controlledvocabulary.language.pelende=Pelende +controlledvocabulary.language.pemon=Pemon +controlledvocabulary.language.penang_sign_language=Penang Sign Language +controlledvocabulary.language.penchal=Penchal +controlledvocabulary.language.pendau=Pendau +controlledvocabulary.language.pengo=Pengo +controlledvocabulary.language.pennsylvania_german=Pennsylvania German +controlledvocabulary.language.penrhyn=Penrhyn +controlledvocabulary.language.pentlatch=Pentlatch +controlledvocabulary.language.perai=Perai +controlledvocabulary.language.peranakan_indonesian=Peranakan Indonesian +controlledvocabulary.language.pere=Pere +controlledvocabulary.language.peripheral_mongolian=Peripheral Mongolian +controlledvocabulary.language.pero=Pero +controlledvocabulary.language.persian_(farsi)=Persian, Persian (Farsi) +controlledvocabulary.language.peruvian_sign_language=Peruvian Sign Language +controlledvocabulary.language.pesse=Pesse +controlledvocabulary.language.petapa_zapotec=Petapa Zapotec +controlledvocabulary.language.petats=Petats +controlledvocabulary.language.petjo=Petjo +controlledvocabulary.language.penoles_mixtec=Peñoles Mixtec +controlledvocabulary.language.pfaelzisch=Pfaelzisch +controlledvocabulary.language.phai=Phai +controlledvocabulary.language.phake=Phake +controlledvocabulary.language.phala=Phala +controlledvocabulary.language.phalura=Phalura +controlledvocabulary.language.phana'=Phana' +controlledvocabulary.language.phangduwali=Phangduwali +controlledvocabulary.language.phende=Phende +controlledvocabulary.language.philippine_sign_language=Philippine Sign Language +controlledvocabulary.language.phimbi=Phimbi +controlledvocabulary.language.phoenician=Phoenician +controlledvocabulary.language.phola=Phola +controlledvocabulary.language.pholo=Pholo +controlledvocabulary.language.phom_naga=Phom Naga +controlledvocabulary.language.phong-kniang=Phong-Kniang +controlledvocabulary.language.phrae_pwo_karen=Phrae Pwo Karen +controlledvocabulary.language.phrygian=Phrygian +controlledvocabulary.language.phu_thai=Phu Thai +controlledvocabulary.language.phuan=Phuan +controlledvocabulary.language.phudagi=Phudagi +controlledvocabulary.language.phuie=Phuie +controlledvocabulary.language.phukha=Phukha +controlledvocabulary.language.phuma=Phuma +controlledvocabulary.language.phunoi=Phunoi +controlledvocabulary.language.phuong=Phuong +controlledvocabulary.language.phupa=Phupa +controlledvocabulary.language.phupha=Phupha +controlledvocabulary.language.phuza=Phuza +controlledvocabulary.language.piamatsina=Piamatsina +controlledvocabulary.language.piame=Piame +controlledvocabulary.language.piapoco=Piapoco +controlledvocabulary.language.piaroa=Piaroa +controlledvocabulary.language.picard=Picard +controlledvocabulary.language.pichis_asheninka=Pichis Ashéninka +controlledvocabulary.language.pictish=Pictish +controlledvocabulary.language.pidgin_delaware=Pidgin Delaware +controlledvocabulary.language.piemontese=Piemontese +controlledvocabulary.language.pijao=Pijao +controlledvocabulary.language.pije=Pije +controlledvocabulary.language.pijin=Pijin +controlledvocabulary.language.pilaga=Pilagá +controlledvocabulary.language.pileni=Pileni +controlledvocabulary.language.pima_bajo=Pima Bajo +controlledvocabulary.language.pimbwe=Pimbwe +controlledvocabulary.language.pinai-hagahai=Pinai-Hagahai +controlledvocabulary.language.pindiini=Pindiini +controlledvocabulary.language.pingelapese=Pingelapese +controlledvocabulary.language.pinigura=Pinigura +controlledvocabulary.language.pinjarup=Pinjarup +controlledvocabulary.language.pinji=Pinji +controlledvocabulary.language.pinotepa_nacional_mixtec=Pinotepa Nacional Mixtec +controlledvocabulary.language.pintupi-luritja=Pintupi-Luritja +controlledvocabulary.language.pinyin=Pinyin +controlledvocabulary.language.pipil=Pipil +controlledvocabulary.language.piraha=Pirahã +controlledvocabulary.language.piratapuyo=Piratapuyo +controlledvocabulary.language.pirlatapa=Pirlatapa +controlledvocabulary.language.piro=Piro +controlledvocabulary.language.pirriya=Pirriya +controlledvocabulary.language.pisabo=Pisabo +controlledvocabulary.language.pisaflores_tepehua=Pisaflores Tepehua +controlledvocabulary.language.piscataway=Piscataway +controlledvocabulary.language.pisidian=Pisidian +controlledvocabulary.language.pitcairn-norfolk=Pitcairn-Norfolk +controlledvocabulary.language.pite_sami=Pite Sami +controlledvocabulary.language.piti=Piti +controlledvocabulary.language.pitjantjatjara=Pitjantjatjara +controlledvocabulary.language.pitta_pitta=Pitta Pitta +controlledvocabulary.language.piu=Piu +controlledvocabulary.language.piya-kwonci=Piya-Kwonci +controlledvocabulary.language.plains_cree=Plains Cree +controlledvocabulary.language.plains_indian_sign_language=Plains Indian Sign Language +controlledvocabulary.language.plains_miwok=Plains Miwok +controlledvocabulary.language.plapo_krumen=Plapo Krumen +controlledvocabulary.language.plateau_malagasy=Plateau Malagasy +controlledvocabulary.language.plautdietsch=Plautdietsch +controlledvocabulary.language.playero=Playero +controlledvocabulary.language.pnar=Pnar +controlledvocabulary.language.pochuri_naga=Pochuri Naga +controlledvocabulary.language.pochutec=Pochutec +controlledvocabulary.language.podena=Podena +controlledvocabulary.language.pogolo=Pogolo +controlledvocabulary.language.pohnpeian=Pohnpeian +controlledvocabulary.language.pokanga=Pokangá +controlledvocabulary.language.poke=Poke +controlledvocabulary.language.pokomo=Pokomo +controlledvocabulary.language.polabian=Polabian +controlledvocabulary.language.polari=Polari +controlledvocabulary.language.polish=Polish +controlledvocabulary.language.polish_sign_language=Polish Sign Language +controlledvocabulary.language.polonombauk=Polonombauk +controlledvocabulary.language.pom=Pom +controlledvocabulary.language.pomo=Pomo +controlledvocabulary.language.ponam=Ponam +controlledvocabulary.language.ponosakan=Ponosakan +controlledvocabulary.language.pontic=Pontic +controlledvocabulary.language.ponyo-gongwang_naga=Ponyo-Gongwang Naga +controlledvocabulary.language.popti'=Popti' +controlledvocabulary.language.poqomam=Poqomam +controlledvocabulary.language.poqomchi'=Poqomchi' +controlledvocabulary.language.porohanon=Porohanon +controlledvocabulary.language.port_sandwich=Port Sandwich +controlledvocabulary.language.port_sorell_tasmanian=Port Sorell Tasmanian +controlledvocabulary.language.port_vato=Port Vato controlledvocabulary.language.portuguese=Portuguese +controlledvocabulary.language.portuguese_sign_language=Portuguese Sign Language +controlledvocabulary.language.potawatomi=Potawatomi +controlledvocabulary.language.potiguara=Potiguára +controlledvocabulary.language.pottangi_ollar_gadaba=Pottangi Ollar Gadaba +controlledvocabulary.language.poumei_naga=Poumei Naga +controlledvocabulary.language.pouye=Pouye +controlledvocabulary.language.powari=Powari +controlledvocabulary.language.powhatan=Powhatan +controlledvocabulary.language.poyanawa=Poyanáwa +controlledvocabulary.language.prasuni=Prasuni +controlledvocabulary.language.primitive_irish=Primitive Irish +controlledvocabulary.language.principense=Principense +controlledvocabulary.language.providencia_sign_language=Providencia Sign Language +controlledvocabulary.language.prussian=Prussian +controlledvocabulary.language.psikye=Psikye +controlledvocabulary.language.pu-xian_chinese=Pu-Xian Chinese +controlledvocabulary.language.puare=Puare +controlledvocabulary.language.pudtol_atta=Pudtol Atta +controlledvocabulary.language.puebla_mazatec=Puebla Mazatec +controlledvocabulary.language.puelche=Puelche +controlledvocabulary.language.puerto_rican_sign_language=Puerto Rican Sign Language +controlledvocabulary.language.puimei_naga=Puimei Naga +controlledvocabulary.language.puinave=Puinave +controlledvocabulary.language.pukapuka=Pukapuka +controlledvocabulary.language.pulaar=Pulaar +controlledvocabulary.language.pulabu=Pulabu +controlledvocabulary.language.pular=Pular +controlledvocabulary.language.puluwatese=Puluwatese +controlledvocabulary.language.puma=Puma +controlledvocabulary.language.pumpokol=Pumpokol +controlledvocabulary.language.pume=Pumé +controlledvocabulary.language.punan_aput=Punan Aput +controlledvocabulary.language.punan_bah-biau=Punan Bah-Biau +controlledvocabulary.language.punan_batu_1=Punan Batu 1 +controlledvocabulary.language.punan_merah=Punan Merah +controlledvocabulary.language.punan_merap=Punan Merap +controlledvocabulary.language.punan_tubu=Punan Tubu +controlledvocabulary.language.punic=Punic +controlledvocabulary.language.puno_quechua=Puno Quechua +controlledvocabulary.language.punthamara=Punthamara +controlledvocabulary.language.punu=Punu +controlledvocabulary.language.puoc=Puoc +controlledvocabulary.language.puquina=Puquina +controlledvocabulary.language.puragi=Puragi +controlledvocabulary.language.purari=Purari +controlledvocabulary.language.purepecha=Purepecha +controlledvocabulary.language.puri=Puri +controlledvocabulary.language.purik=Purik +controlledvocabulary.language.purisimeno=Purisimeño +controlledvocabulary.language.puroik=Puroik +controlledvocabulary.language.purubora=Puruborá +controlledvocabulary.language.purum=Purum +controlledvocabulary.language.putai=Putai +controlledvocabulary.language.putoh=Putoh +controlledvocabulary.language.putukwam=Putukwam +controlledvocabulary.language.puyo=Puyo +controlledvocabulary.language.puyo-paekche=Puyo-Paekche +controlledvocabulary.language.puyuma=Puyuma +controlledvocabulary.language.pwaamei=Pwaamei +controlledvocabulary.language.pwapwa=Pwapwâ +controlledvocabulary.language.pwo_eastern_karen=Pwo Eastern Karen +controlledvocabulary.language.pwo_northern_karen=Pwo Northern Karen +controlledvocabulary.language.pwo_western_karen=Pwo Western Karen +controlledvocabulary.language.pyapun=Pyapun +controlledvocabulary.language.pye_krumen=Pye Krumen +controlledvocabulary.language.pyen=Pyen +controlledvocabulary.language.pyu_(myanmar)=Pyu (Myanmar) +controlledvocabulary.language.pyu_(papua_new_guinea)=Pyu (Papua New Guinea) +controlledvocabulary.language.paez=Páez +controlledvocabulary.language.paafang=Pááfang +controlledvocabulary.language.pari=Päri +controlledvocabulary.language.pemono=Pémono +controlledvocabulary.language.peve=Pévé +controlledvocabulary.language.pokoot=Pökoot +controlledvocabulary.language.pali=Pāli, Pali +controlledvocabulary.language.q'anjob'al=Q'anjob'al +controlledvocabulary.language.qabiao=Qabiao +controlledvocabulary.language.qaqet=Qaqet +controlledvocabulary.language.qashqa'i=Qashqa'i +controlledvocabulary.language.qatabanian=Qatabanian +controlledvocabulary.language.qau=Qau +controlledvocabulary.language.qawasqar=Qawasqar +controlledvocabulary.language.qila_muji=Qila Muji +controlledvocabulary.language.qimant=Qimant +controlledvocabulary.language.qiubei_zhuang=Qiubei Zhuang +controlledvocabulary.language.quapaw=Quapaw +controlledvocabulary.language.quebec_sign_language=Quebec Sign Language +controlledvocabulary.language.quechan=Quechan controlledvocabulary.language.quechua=Quechua -controlledvocabulary.language.romansh=Romansh -controlledvocabulary.language.kirundi=Kirundi +controlledvocabulary.language.quenya=Quenya +controlledvocabulary.language.queretaro_otomi=Querétaro Otomi +controlledvocabulary.language.quetzaltepec_mixe=Quetzaltepec Mixe +controlledvocabulary.language.queyu=Queyu +controlledvocabulary.language.quiavicuzas_zapotec=Quiavicuzas Zapotec +controlledvocabulary.language.quileute=Quileute +controlledvocabulary.language.quinault=Quinault +controlledvocabulary.language.quinqui=Quinqui +controlledvocabulary.language.quioquitani-quieri_zapotec=Quioquitani-Quierí Zapotec +controlledvocabulary.language.quiotepec_chinantec=Quiotepec Chinantec +controlledvocabulary.language.quiripi=Quiripi +controlledvocabulary.language.rabha=Rabha +controlledvocabulary.language.rade=Rade +controlledvocabulary.language.raetic=Raetic +controlledvocabulary.language.rahambuu=Rahambuu +controlledvocabulary.language.rajah_kabunsuwan_manobo=Rajah Kabunsuwan Manobo +controlledvocabulary.language.rajasthani=Rajasthani +controlledvocabulary.language.rajbanshi=Rajbanshi +controlledvocabulary.language.raji=Raji +controlledvocabulary.language.rajong=Rajong +controlledvocabulary.language.rajput_garasia=Rajput Garasia +controlledvocabulary.language.rakahanga-manihiki=Rakahanga-Manihiki +controlledvocabulary.language.rakhine=Rakhine +controlledvocabulary.language.ralte=Ralte +controlledvocabulary.language.rama=Rama +controlledvocabulary.language.ramoaaina=Ramoaaina +controlledvocabulary.language.ramopa=Ramopa +controlledvocabulary.language.rampi=Rampi +controlledvocabulary.language.rana_tharu=Rana Tharu +controlledvocabulary.language.rang=Rang +controlledvocabulary.language.rangi=Rangi +controlledvocabulary.language.rangkas=Rangkas +controlledvocabulary.language.ranglong=Ranglong +controlledvocabulary.language.rangpuri=Rangpuri +controlledvocabulary.language.rao=Rao +controlledvocabulary.language.rapa=Rapa +controlledvocabulary.language.rapanui=Rapanui +controlledvocabulary.language.rapoisi=Rapoisi +controlledvocabulary.language.rapting=Rapting +controlledvocabulary.language.rara_bakati'=Rara Bakati' +controlledvocabulary.language.rarotongan=Rarotongan +controlledvocabulary.language.rasawa=Rasawa +controlledvocabulary.language.ratagnon=Ratagnon +controlledvocabulary.language.ratahan=Ratahan +controlledvocabulary.language.rathawi=Rathawi +controlledvocabulary.language.rathwi_bareli=Rathwi Bareli +controlledvocabulary.language.raute=Raute +controlledvocabulary.language.ravula=Ravula +controlledvocabulary.language.rawa=Rawa +controlledvocabulary.language.rawang=Rawang +controlledvocabulary.language.rawat=Rawat +controlledvocabulary.language.rawngtu_chin=Rawngtu Chin +controlledvocabulary.language.rawo=Rawo +controlledvocabulary.language.rayon_zoque=Rayón Zoque +controlledvocabulary.language.razajerdi=Razajerdi +controlledvocabulary.language.red_gelao=Red Gelao +controlledvocabulary.language.reel=Reel +controlledvocabulary.language.rejang=Rejang +controlledvocabulary.language.rejang_kayan=Rejang Kayan +controlledvocabulary.language.reli=Reli +controlledvocabulary.language.rema=Rema +controlledvocabulary.language.rembarrnga=Rembarrnga +controlledvocabulary.language.rembong=Rembong +controlledvocabulary.language.remo=Remo +controlledvocabulary.language.remontado_dumagat=Remontado Dumagat +controlledvocabulary.language.rempi=Rempi +controlledvocabulary.language.remun=Remun +controlledvocabulary.language.rendille=Rendille +controlledvocabulary.language.rengao=Rengao +controlledvocabulary.language.rennell-bellona=Rennell-Bellona +controlledvocabulary.language.repanbitip=Repanbitip +controlledvocabulary.language.rer_bare=Rer Bare +controlledvocabulary.language.rerau=Rerau +controlledvocabulary.language.rerep=Rerep +controlledvocabulary.language.reshe=Reshe +controlledvocabulary.language.resigaro=Resígaro +controlledvocabulary.language.retta=Retta +controlledvocabulary.language.reyesano=Reyesano +controlledvocabulary.language.riang_(india)=Riang (India) +controlledvocabulary.language.riang_lai=Riang Lai +controlledvocabulary.language.riang_lang=Riang Lang +controlledvocabulary.language.riantana=Riantana +controlledvocabulary.language.ribun=Ribun +controlledvocabulary.language.rigwe=Rigwe +controlledvocabulary.language.rikbaktsa=Rikbaktsa +controlledvocabulary.language.rinconada_bikol=Rinconada Bikol +controlledvocabulary.language.rincon_zapotec=Rincón Zapotec +controlledvocabulary.language.ringgou=Ringgou +controlledvocabulary.language.ririo=Ririo +controlledvocabulary.language.rishiwa=Rishiwa +controlledvocabulary.language.ritharrngu=Ritharrngu +controlledvocabulary.language.riung=Riung +controlledvocabulary.language.riverain_sango=Riverain Sango +controlledvocabulary.language.rmeet=Rmeet +controlledvocabulary.language.rogo=Rogo +controlledvocabulary.language.rohingya=Rohingya +controlledvocabulary.language.roma=Roma +controlledvocabulary.language.romagnol=Romagnol +controlledvocabulary.language.romam=Romam controlledvocabulary.language.romanian=Romanian +controlledvocabulary.language.romanian_sign_language=Romanian Sign Language +controlledvocabulary.language.romano-greek=Romano-Greek +controlledvocabulary.language.romano-serbian=Romano-Serbian +controlledvocabulary.language.romanova=Romanova +controlledvocabulary.language.romansh=Romansh +controlledvocabulary.language.romany=Romany +controlledvocabulary.language.romblomanon=Romblomanon +controlledvocabulary.language.rombo=Rombo +controlledvocabulary.language.romkun=Romkun +controlledvocabulary.language.ron=Ron +controlledvocabulary.language.ronga=Ronga +controlledvocabulary.language.rongga=Rongga +controlledvocabulary.language.rongmei_naga=Rongmei Naga +controlledvocabulary.language.rongpo=Rongpo +controlledvocabulary.language.ronji=Ronji +controlledvocabulary.language.roon=Roon +controlledvocabulary.language.roria=Roria +controlledvocabulary.language.rotokas=Rotokas +controlledvocabulary.language.rotuman=Rotuman +controlledvocabulary.language.roviana=Roviana +controlledvocabulary.language.ruching_palaung=Ruching Palaung +controlledvocabulary.language.rudbari=Rudbari +controlledvocabulary.language.rufiji=Rufiji +controlledvocabulary.language.ruga=Ruga +controlledvocabulary.language.rukai=Rukai +controlledvocabulary.language.ruma=Ruma +controlledvocabulary.language.rumai_palaung=Rumai Palaung +controlledvocabulary.language.rumu=Rumu +controlledvocabulary.language.kirundi=Rundi, Kirundi +controlledvocabulary.language.runga=Runga +controlledvocabulary.language.rungtu_chin=Rungtu Chin +controlledvocabulary.language.rungus=Rungus +controlledvocabulary.language.rungwa=Rungwa +controlledvocabulary.language.russia_buriat=Russia Buriat controlledvocabulary.language.russian=Russian -controlledvocabulary.language.sanskrit_(samskrta)=Sanskrit (Sa\u1E41sk\u1E5Bta) -controlledvocabulary.language.sardinian=Sardinian -controlledvocabulary.language.sindhi=Sindhi -controlledvocabulary.language.northern_sami=Northern Sami +controlledvocabulary.language.russian_sign_language=Russian Sign Language +controlledvocabulary.language.rusyn=Rusyn +controlledvocabulary.language.ruthenian=Ruthenian +controlledvocabulary.language.rutul=Rutul +controlledvocabulary.language.ruuli=Ruuli +controlledvocabulary.language.ruund=Ruund +controlledvocabulary.language.ruwila=Ruwila +controlledvocabulary.language.rwa=Rwa +controlledvocabulary.language.rwandan_sign_language=Rwandan Sign Language +controlledvocabulary.language.reunion_creole_french=Réunion Creole French +controlledvocabulary.language.razihi=Rāziḥī +controlledvocabulary.language.s'gaw_karen=S'gaw Karen +controlledvocabulary.language.sa=Sa +controlledvocabulary.language.sa'a=Sa'a +controlledvocabulary.language.sa'ban=Sa'ban +controlledvocabulary.language.sa'och=Sa'och +controlledvocabulary.language.saafi-saafi=Saafi-Saafi +controlledvocabulary.language.saam=Saam +controlledvocabulary.language.saamia=Saamia +controlledvocabulary.language.saaroa=Saaroa +controlledvocabulary.language.saba=Saba +controlledvocabulary.language.sabaean=Sabaean +controlledvocabulary.language.sabah_bisaya=Sabah Bisaya +controlledvocabulary.language.sabah_malay=Sabah Malay +controlledvocabulary.language.sabane=Sabanê +controlledvocabulary.language.sabaot=Sabaot +controlledvocabulary.language.sabine=Sabine +controlledvocabulary.language.sabu=Sabu +controlledvocabulary.language.sabum=Sabüm +controlledvocabulary.language.sacapulteco=Sacapulteco +controlledvocabulary.language.sadri=Sadri +controlledvocabulary.language.saek=Saek +controlledvocabulary.language.saep=Saep +controlledvocabulary.language.safaliba=Safaliba +controlledvocabulary.language.safeyoka=Safeyoka +controlledvocabulary.language.safwa=Safwa +controlledvocabulary.language.sagala=Sagala +controlledvocabulary.language.sagalla=Sagalla +controlledvocabulary.language.saho=Saho +controlledvocabulary.language.sahu=Sahu +controlledvocabulary.language.saidi_arabic=Saidi Arabic +controlledvocabulary.language.saint_lucian_creole_french=Saint Lucian Creole French +controlledvocabulary.language.saisiyat=Saisiyat +controlledvocabulary.language.sajalong=Sajalong +controlledvocabulary.language.sajau_basap=Sajau Basap +controlledvocabulary.language.sakachep=Sakachep +controlledvocabulary.language.sakalava_malagasy=Sakalava Malagasy +controlledvocabulary.language.sakao=Sakao +controlledvocabulary.language.sakata=Sakata +controlledvocabulary.language.sake=Sake +controlledvocabulary.language.sakirabia=Sakirabiá +controlledvocabulary.language.sakizaya=Sakizaya +controlledvocabulary.language.sala=Sala +controlledvocabulary.language.salampasu=Salampasu +controlledvocabulary.language.salar=Salar +controlledvocabulary.language.salas=Salas +controlledvocabulary.language.salasaca_highland_quichua=Salasaca Highland Quichua +controlledvocabulary.language.salawati=Salawati +controlledvocabulary.language.saleman=Saleman +controlledvocabulary.language.saliba=Saliba +controlledvocabulary.language.salinan=Salinan +controlledvocabulary.language.sallands=Sallands +controlledvocabulary.language.salt-yui=Salt-Yui +controlledvocabulary.language.saluan=Saluan +controlledvocabulary.language.saluma=Salumá +controlledvocabulary.language.salvadoran_sign_language=Salvadoran Sign Language +controlledvocabulary.language.sam=Sam +controlledvocabulary.language.samaritan=Samaritan +controlledvocabulary.language.samaritan_aramaic=Samaritan Aramaic +controlledvocabulary.language.samarokena=Samarokena +controlledvocabulary.language.samatao=Samatao +controlledvocabulary.language.samay=Samay +controlledvocabulary.language.samba=Samba +controlledvocabulary.language.samba_daka=Samba Daka +controlledvocabulary.language.samba_leko=Samba Leko +controlledvocabulary.language.sambal=Sambal +controlledvocabulary.language.sambalpuri=Sambalpuri +controlledvocabulary.language.sambe=Sambe +controlledvocabulary.language.samberigi=Samberigi +controlledvocabulary.language.samburu=Samburu +controlledvocabulary.language.samei=Samei +controlledvocabulary.language.samo=Samo controlledvocabulary.language.samoan=Samoan +controlledvocabulary.language.samogitian=Samogitian +controlledvocabulary.language.samosa=Samosa +controlledvocabulary.language.sampang=Sampang +controlledvocabulary.language.samre=Samre +controlledvocabulary.language.samtao=Samtao +controlledvocabulary.language.samvedi=Samvedi +controlledvocabulary.language.san_agustin_mixtepec_zapotec=San Agustín Mixtepec Zapotec +controlledvocabulary.language.san_baltazar_loxicha_zapotec=San Baltazar Loxicha Zapotec +controlledvocabulary.language.san_blas_kuna=San Blas Kuna +controlledvocabulary.language.san_dionisio_del_mar_huave=San Dionisio Del Mar Huave +controlledvocabulary.language.san_felipe_otlaltepec_popoloca=San Felipe Otlaltepec Popoloca +controlledvocabulary.language.san_francisco_del_mar_huave=San Francisco Del Mar Huave +controlledvocabulary.language.san_francisco_matlatzinca=San Francisco Matlatzinca +controlledvocabulary.language.san_jeronimo_tecoatl_mazatec=San Jerónimo Tecóatl Mazatec +controlledvocabulary.language.san_juan_atzingo_popoloca=San Juan Atzingo Popoloca +controlledvocabulary.language.san_juan_colorado_mixtec=San Juan Colorado Mixtec +controlledvocabulary.language.san_juan_teita_mixtec=San Juan Teita Mixtec +controlledvocabulary.language.san_luis_temalacayuca_popoloca=San Luís Temalacayuca Popoloca +controlledvocabulary.language.san_marcos_tlacoyalco_popoloca=San Marcos Tlacoyalco Popoloca +controlledvocabulary.language.san_martin_itunyoso_triqui=San Martín Itunyoso Triqui +controlledvocabulary.language.san_martin_quechua=San Martín Quechua +controlledvocabulary.language.san_mateo_del_mar_huave=San Mateo Del Mar Huave +controlledvocabulary.language.san_miguel_creole_french=San Miguel Creole French +controlledvocabulary.language.san_miguel_el_grande_mixtec=San Miguel El Grande Mixtec +controlledvocabulary.language.san_miguel_piedras_mixtec=San Miguel Piedras Mixtec +controlledvocabulary.language.san_pedro_amuzgos_amuzgo=San Pedro Amuzgos Amuzgo +controlledvocabulary.language.san_pedro_quiatoni_zapotec=San Pedro Quiatoni Zapotec +controlledvocabulary.language.san_salvador_kongo=San Salvador Kongo +controlledvocabulary.language.san_vicente_coatlan_zapotec=San Vicente Coatlán Zapotec +controlledvocabulary.language.sanaani_arabic=Sanaani Arabic +controlledvocabulary.language.sanapana=Sanapaná +controlledvocabulary.language.sandawe=Sandawe +controlledvocabulary.language.sanga_(democratic_republic_of_congo)=Sanga (Democratic Republic of Congo) +controlledvocabulary.language.sanga_(nigeria)=Sanga (Nigeria) +controlledvocabulary.language.sanggau=Sanggau +controlledvocabulary.language.sangil=Sangil +controlledvocabulary.language.sangir=Sangir +controlledvocabulary.language.sangisari=Sangisari +controlledvocabulary.language.sangkong=Sangkong +controlledvocabulary.language.sanglechi=Sanglechi controlledvocabulary.language.sango=Sango +controlledvocabulary.language.sangtam_naga=Sangtam Naga +controlledvocabulary.language.sangu_(gabon)=Sangu (Gabon) +controlledvocabulary.language.sangu_(tanzania)=Sangu (Tanzania) +controlledvocabulary.language.sani=Sani +controlledvocabulary.language.sanie=Sanie +controlledvocabulary.language.saniyo-hiyewe=Saniyo-Hiyewe +controlledvocabulary.language.sankaran_maninka=Sankaran Maninka +controlledvocabulary.language.sansi=Sansi +controlledvocabulary.language.sanskrit_(samskrta)=Sanskrit (Saṁskṛta), Sanskrit +controlledvocabulary.language.santa_ana_de_tusi_pasco_quechua=Santa Ana de Tusi Pasco Quechua +controlledvocabulary.language.santa_catarina_albarradas_zapotec=Santa Catarina Albarradas Zapotec +controlledvocabulary.language.santa_ines_ahuatempan_popoloca=Santa Inés Ahuatempan Popoloca +controlledvocabulary.language.santa_ines_yatzechi_zapotec=Santa Inés Yatzechi Zapotec +controlledvocabulary.language.santa_lucia_monteverde_mixtec=Santa Lucía Monteverde Mixtec +controlledvocabulary.language.santa_maria_del_mar_huave=Santa María Del Mar Huave +controlledvocabulary.language.santa_maria_la_alta_nahuatl=Santa María La Alta Nahuatl +controlledvocabulary.language.santa_maria_quiegolani_zapotec=Santa María Quiegolani Zapotec +controlledvocabulary.language.santa_maria_zacatepec_mixtec=Santa María Zacatepec Mixtec +controlledvocabulary.language.santa_teresa_cora=Santa Teresa Cora +controlledvocabulary.language.santali=Santali +controlledvocabulary.language.santiago_xanica_zapotec=Santiago Xanica Zapotec +controlledvocabulary.language.santiago_del_estero_quichua=Santiago del Estero Quichua +controlledvocabulary.language.santo_domingo_albarradas_zapotec=Santo Domingo Albarradas Zapotec +controlledvocabulary.language.sanuma=Sanumá +controlledvocabulary.language.saparua=Saparua +controlledvocabulary.language.sapo=Sapo +controlledvocabulary.language.saponi=Saponi +controlledvocabulary.language.saposa=Saposa +controlledvocabulary.language.sapuan=Sapuan +controlledvocabulary.language.sape=Sapé +controlledvocabulary.language.sar=Sar +controlledvocabulary.language.sara=Sara +controlledvocabulary.language.sara_kaba=Sara Kaba +controlledvocabulary.language.sara_kaba_deme=Sara Kaba Deme +controlledvocabulary.language.sara_kaba_naa=Sara Kaba Náà +controlledvocabulary.language.saraiki=Saraiki +controlledvocabulary.language.saramaccan=Saramaccan +controlledvocabulary.language.sarangani_blaan=Sarangani Blaan +controlledvocabulary.language.sarangani_manobo=Sarangani Manobo +controlledvocabulary.language.sarasira=Sarasira +controlledvocabulary.language.saraveca=Saraveca +controlledvocabulary.language.sardinian=Sardinian +controlledvocabulary.language.sari=Sari +controlledvocabulary.language.sarikoli=Sarikoli +controlledvocabulary.language.sarli=Sarli +controlledvocabulary.language.sarsi=Sarsi +controlledvocabulary.language.sartang=Sartang +controlledvocabulary.language.sarua=Sarua +controlledvocabulary.language.sarudu=Sarudu +controlledvocabulary.language.saruga=Saruga +controlledvocabulary.language.sasak=Sasak +controlledvocabulary.language.sasaru=Sasaru +controlledvocabulary.language.sassarese_sardinian=Sassarese Sardinian +controlledvocabulary.language.satawalese=Satawalese +controlledvocabulary.language.saterfriesisch=Saterfriesisch +controlledvocabulary.language.satere-mawe=Sateré-Mawé +controlledvocabulary.language.saudi_arabian_sign_language=Saudi Arabian Sign Language +controlledvocabulary.language.sauraseni_prakrit=Sauraseni Prākrit +controlledvocabulary.language.saurashtra=Saurashtra +controlledvocabulary.language.sauri=Sauri +controlledvocabulary.language.sauria_paharia=Sauria Paharia +controlledvocabulary.language.sause=Sause +controlledvocabulary.language.sausi=Sausi +controlledvocabulary.language.savi=Savi +controlledvocabulary.language.savosavo=Savosavo +controlledvocabulary.language.sawai=Sawai +controlledvocabulary.language.saweru=Saweru +controlledvocabulary.language.sawi=Sawi +controlledvocabulary.language.sawila=Sawila +controlledvocabulary.language.sawknah=Sawknah +controlledvocabulary.language.saxwe_gbe=Saxwe Gbe +controlledvocabulary.language.saya=Saya +controlledvocabulary.language.sayula_popoluca=Sayula Popoluca +controlledvocabulary.language.scots=Scots +controlledvocabulary.language.scythian=Scythian +controlledvocabulary.language.sea_island_creole_english=Sea Island Creole English +controlledvocabulary.language.seba=Seba +controlledvocabulary.language.sebat_bet_gurage=Sebat Bet Gurage +controlledvocabulary.language.seberuang=Seberuang +controlledvocabulary.language.sebop=Sebop +controlledvocabulary.language.sechelt=Sechelt +controlledvocabulary.language.secoya=Secoya +controlledvocabulary.language.sedang=Sedang +controlledvocabulary.language.sediq=Sediq +controlledvocabulary.language.sedoa=Sedoa +controlledvocabulary.language.seeku=Seeku +controlledvocabulary.language.segai=Segai +controlledvocabulary.language.segeju=Segeju +controlledvocabulary.language.seget=Seget +controlledvocabulary.language.sehwi=Sehwi +controlledvocabulary.language.seimat=Seimat +controlledvocabulary.language.seit-kaitetu=Seit-Kaitetu +controlledvocabulary.language.sekani=Sekani +controlledvocabulary.language.sekapan=Sekapan +controlledvocabulary.language.sekar=Sekar +controlledvocabulary.language.seke_(nepal)=Seke (Nepal) +controlledvocabulary.language.seke_(vanuatu)=Seke (Vanuatu) +controlledvocabulary.language.sekele=Sekele +controlledvocabulary.language.seki=Seki +controlledvocabulary.language.seko_padang=Seko Padang +controlledvocabulary.language.seko_tengah=Seko Tengah +controlledvocabulary.language.sekpele=Sekpele +controlledvocabulary.language.selangor_sign_language=Selangor Sign Language +controlledvocabulary.language.selaru=Selaru +controlledvocabulary.language.selayar=Selayar +controlledvocabulary.language.selee=Selee +controlledvocabulary.language.selepet=Selepet +controlledvocabulary.language.selian=Selian +controlledvocabulary.language.selkup=Selkup +controlledvocabulary.language.selungai_murut=Selungai Murut +controlledvocabulary.language.seluwasan=Seluwasan +controlledvocabulary.language.semai=Semai +controlledvocabulary.language.semandang=Semandang +controlledvocabulary.language.semaq_beri=Semaq Beri +controlledvocabulary.language.sembakung_murut=Sembakung Murut +controlledvocabulary.language.semelai=Semelai +controlledvocabulary.language.semimi=Semimi +controlledvocabulary.language.semnam=Semnam +controlledvocabulary.language.semnani=Semnani +controlledvocabulary.language.sempan=Sempan +controlledvocabulary.language.sena=Sena +controlledvocabulary.language.senara_senoufo=Senara Sénoufo +controlledvocabulary.language.senaya=Senaya +controlledvocabulary.language.sene=Sene +controlledvocabulary.language.seneca=Seneca +controlledvocabulary.language.sened=Sened +controlledvocabulary.language.sengele=Sengele +controlledvocabulary.language.senggi=Senggi +controlledvocabulary.language.sengo=Sengo +controlledvocabulary.language.sengseng=Sengseng +controlledvocabulary.language.senhaja_de_srair=Senhaja De Srair +controlledvocabulary.language.sensi=Sensi +controlledvocabulary.language.sentani=Sentani +controlledvocabulary.language.senthang_chin=Senthang Chin +controlledvocabulary.language.sentinel=Sentinel +controlledvocabulary.language.sepa_(indonesia)=Sepa (Indonesia) +controlledvocabulary.language.sepa_(papua_new_guinea)=Sepa (Papua New Guinea) +controlledvocabulary.language.sepik_iwam=Sepik Iwam +controlledvocabulary.language.sera=Sera controlledvocabulary.language.serbian=Serbian -controlledvocabulary.language.scottish_gaelic,_gaelic=Scottish Gaelic, Gaelic +controlledvocabulary.language.serbo-croatian=Serbo-Croatian +controlledvocabulary.language.sere=Sere +controlledvocabulary.language.serer=Serer +controlledvocabulary.language.seri=Seri +controlledvocabulary.language.serili=Serili +controlledvocabulary.language.seroa=Seroa +controlledvocabulary.language.serrano=Serrano +controlledvocabulary.language.serua=Serua +controlledvocabulary.language.serudung_murut=Serudung Murut +controlledvocabulary.language.serui-laut=Serui-Laut +controlledvocabulary.language.seselwa_creole_french=Seselwa Creole French +controlledvocabulary.language.seta=Seta +controlledvocabulary.language.setaman=Setaman +controlledvocabulary.language.seti=Seti +controlledvocabulary.language.settla=Settla +controlledvocabulary.language.severn_ojibwa=Severn Ojibwa +controlledvocabulary.language.sewa_bay=Sewa Bay +controlledvocabulary.language.seychelles_sign_language=Seychelles Sign Language +controlledvocabulary.language.seze=Seze +controlledvocabulary.language.sha=Sha +controlledvocabulary.language.shabak=Shabak +controlledvocabulary.language.shahmirzadi=Shahmirzadi +controlledvocabulary.language.shahrudi=Shahrudi +controlledvocabulary.language.shall-zwall=Shall-Zwall +controlledvocabulary.language.shama-sambuga=Shama-Sambuga +controlledvocabulary.language.shamang=Shamang +controlledvocabulary.language.shambala=Shambala +controlledvocabulary.language.shan=Shan +controlledvocabulary.language.shanenawa=Shanenawa +controlledvocabulary.language.shanga=Shanga +controlledvocabulary.language.sharanahua=Sharanahua +controlledvocabulary.language.shark_bay=Shark Bay +controlledvocabulary.language.sharwa=Sharwa +controlledvocabulary.language.shasta=Shasta +controlledvocabulary.language.shatt=Shatt +controlledvocabulary.language.shau=Shau +controlledvocabulary.language.shawnee=Shawnee +controlledvocabulary.language.she=She +controlledvocabulary.language.shehri=Shehri +controlledvocabulary.language.shekhawati=Shekhawati +controlledvocabulary.language.shekkacho=Shekkacho +controlledvocabulary.language.sheko=Sheko +controlledvocabulary.language.shelta=Shelta +controlledvocabulary.language.shempire_senoufo=Shempire Senoufo +controlledvocabulary.language.shendu=Shendu +controlledvocabulary.language.sheni=Sheni +controlledvocabulary.language.sherbro=Sherbro +controlledvocabulary.language.sherdukpen=Sherdukpen +controlledvocabulary.language.sherpa=Sherpa +controlledvocabulary.language.sheshi_kham=Sheshi Kham +controlledvocabulary.language.shi=Shi +controlledvocabulary.language.shihhi_arabic=Shihhi Arabic +controlledvocabulary.language.shiki=Shiki +controlledvocabulary.language.shilluk=Shilluk +controlledvocabulary.language.shina=Shina +controlledvocabulary.language.shipibo-conibo=Shipibo-Conibo +controlledvocabulary.language.sholaga=Sholaga +controlledvocabulary.language.shom_peng=Shom Peng controlledvocabulary.language.shona=Shona +controlledvocabulary.language.shoo-minda-nye=Shoo-Minda-Nye +controlledvocabulary.language.shor=Shor +controlledvocabulary.language.shoshoni=Shoshoni +controlledvocabulary.language.shua=Shua +controlledvocabulary.language.shuadit=Shuadit +controlledvocabulary.language.shuar=Shuar +controlledvocabulary.language.shubi=Shubi +controlledvocabulary.language.shughni=Shughni +controlledvocabulary.language.shuhi=Shuhi +controlledvocabulary.language.shumashti=Shumashti +controlledvocabulary.language.shumcho=Shumcho +controlledvocabulary.language.shuswap=Shuswap +controlledvocabulary.language.shwai=Shwai +controlledvocabulary.language.shwe_palaung=Shwe Palaung +controlledvocabulary.language.sialum=Sialum +controlledvocabulary.language.siamou=Siamou +controlledvocabulary.language.sian=Sian +controlledvocabulary.language.siane=Siane +controlledvocabulary.language.siang=Siang +controlledvocabulary.language.siar-lak=Siar-Lak +controlledvocabulary.language.siawi=Siawi +controlledvocabulary.language.sibe=Sibe +controlledvocabulary.language.siberian_tatar=Siberian Tatar +controlledvocabulary.language.sibu_melanau=Sibu Melanau +controlledvocabulary.language.sicanian=Sicanian +controlledvocabulary.language.sicel=Sicel +controlledvocabulary.language.nuosu=Sichuan Yi, Nuosu +controlledvocabulary.language.sicilian=Sicilian +controlledvocabulary.language.siculo_arabic=Siculo Arabic +controlledvocabulary.language.sidamo=Sidamo +controlledvocabulary.language.sidetic=Sidetic +controlledvocabulary.language.sie=Sie +controlledvocabulary.language.sierra_leone_sign_language=Sierra Leone Sign Language +controlledvocabulary.language.sierra_negra_nahuatl=Sierra Negra Nahuatl +controlledvocabulary.language.sierra_de_juarez_zapotec=Sierra de Juárez Zapotec +controlledvocabulary.language.sighu=Sighu +controlledvocabulary.language.sihan=Sihan +controlledvocabulary.language.sihuas_ancash_quechua=Sihuas Ancash Quechua +controlledvocabulary.language.sika=Sika +controlledvocabulary.language.sikaiana=Sikaiana +controlledvocabulary.language.sikaritai=Sikaritai +controlledvocabulary.language.sikiana=Sikiana +controlledvocabulary.language.sikkimese=Sikkimese +controlledvocabulary.language.siksika=Siksika +controlledvocabulary.language.sikule=Sikule +controlledvocabulary.language.sila=Sila +controlledvocabulary.language.silacayoapan_mixtec=Silacayoapan Mixtec +controlledvocabulary.language.sileibi=Sileibi +controlledvocabulary.language.silesian=Silesian +controlledvocabulary.language.silimo=Silimo +controlledvocabulary.language.siliput=Siliput +controlledvocabulary.language.silopi=Silopi +controlledvocabulary.language.silt'e=Silt'e +controlledvocabulary.language.simaa=Simaa +controlledvocabulary.language.simba=Simba +controlledvocabulary.language.simbali=Simbali +controlledvocabulary.language.simbari=Simbari +controlledvocabulary.language.simbo=Simbo +controlledvocabulary.language.simeku=Simeku +controlledvocabulary.language.simeulue=Simeulue +controlledvocabulary.language.simte=Simte +controlledvocabulary.language.sinagen=Sinagen +controlledvocabulary.language.sinasina=Sinasina +controlledvocabulary.language.sinaugoro=Sinaugoro +controlledvocabulary.language.sindarin=Sindarin +controlledvocabulary.language.sindhi=Sindhi +controlledvocabulary.language.sindhi_bhil=Sindhi Bhil +controlledvocabulary.language.sindihui_mixtec=Sindihui Mixtec +controlledvocabulary.language.singa=Singa +controlledvocabulary.language.singapore_sign_language=Singapore Sign Language +controlledvocabulary.language.singpho=Singpho controlledvocabulary.language.sinhala,_sinhalese=Sinhala, Sinhalese +controlledvocabulary.language.sinicahua_mixtec=Sinicahua Mixtec +controlledvocabulary.language.sininkere=Sininkere +controlledvocabulary.language.sinte_romani=Sinte Romani +controlledvocabulary.language.sinyar=Sinyar +controlledvocabulary.language.sio=Sio +controlledvocabulary.language.siona=Siona +controlledvocabulary.language.sipacapense=Sipacapense +controlledvocabulary.language.sira=Sira +controlledvocabulary.language.siraya=Siraya +controlledvocabulary.language.sirenik_yupik=Sirenik Yupik +controlledvocabulary.language.siri=Siri +controlledvocabulary.language.siriano=Siriano +controlledvocabulary.language.siriono=Sirionó +controlledvocabulary.language.sirmauri=Sirmauri +controlledvocabulary.language.siroi=Siroi +controlledvocabulary.language.sissala=Sissala +controlledvocabulary.language.sissano=Sissano +controlledvocabulary.language.siuslaw=Siuslaw +controlledvocabulary.language.sivandi=Sivandi +controlledvocabulary.language.sivia_sign_language=Sivia Sign Language +controlledvocabulary.language.siwai=Siwai +controlledvocabulary.language.siwi=Siwi +controlledvocabulary.language.siwu=Siwu +controlledvocabulary.language.siyin_chin=Siyin Chin +controlledvocabulary.language.skagit=Skagit +controlledvocabulary.language.skalvian=Skalvian +controlledvocabulary.language.skepi_creole_dutch=Skepi Creole Dutch +controlledvocabulary.language.skolt_sami=Skolt Sami +controlledvocabulary.language.skou=Skou +controlledvocabulary.language.slave_(athapascan)=Slave (Athapascan) +controlledvocabulary.language.slavomolisano=Slavomolisano controlledvocabulary.language.slovak=Slovak -controlledvocabulary.language.slovene=Slovene +controlledvocabulary.language.slovakian_sign_language=Slovakian Sign Language +controlledvocabulary.language.slovenian=Slovenian, Slovene +controlledvocabulary.language.small_flowery_miao=Small Flowery Miao +controlledvocabulary.language.smarky_kanum=Smärky Kanum +controlledvocabulary.language.snohomish=Snohomish +controlledvocabulary.language.so_(democratic_republic_of_congo)=So (Democratic Republic of Congo) +controlledvocabulary.language.so'a=So'a +controlledvocabulary.language.sobei=Sobei +controlledvocabulary.language.sochiapam_chinantec=Sochiapam Chinantec +controlledvocabulary.language.soga=Soga +controlledvocabulary.language.sogdian=Sogdian +controlledvocabulary.language.soi=Soi +controlledvocabulary.language.sokoro=Sokoro +controlledvocabulary.language.solano=Solano +controlledvocabulary.language.soli=Soli +controlledvocabulary.language.solomon_islands_sign_language=Solomon Islands Sign Language +controlledvocabulary.language.solong=Solong +controlledvocabulary.language.solos=Solos +controlledvocabulary.language.som=Som controlledvocabulary.language.somali=Somali +controlledvocabulary.language.somba-siawari=Somba-Siawari +controlledvocabulary.language.somrai=Somrai +controlledvocabulary.language.somray=Somray +controlledvocabulary.language.somyev=Somyev +controlledvocabulary.language.sonaga=Sonaga +controlledvocabulary.language.sonde=Sonde +controlledvocabulary.language.songe=Songe +controlledvocabulary.language.songlai_chin=Songlai Chin +controlledvocabulary.language.songo=Songo +controlledvocabulary.language.songomeno=Songomeno +controlledvocabulary.language.songoora=Songoora +controlledvocabulary.language.sonha=Sonha +controlledvocabulary.language.sonia=Sonia +controlledvocabulary.language.soninke=Soninke +controlledvocabulary.language.sonsorol=Sonsorol +controlledvocabulary.language.soo=Soo +controlledvocabulary.language.sop=Sop +controlledvocabulary.language.soqotri=Soqotri +controlledvocabulary.language.sora=Sora +controlledvocabulary.language.sori-harengan=Sori-Harengan +controlledvocabulary.language.sorkhei=Sorkhei +controlledvocabulary.language.sorothaptic=Sorothaptic +controlledvocabulary.language.sorsogon_ayta=Sorsogon Ayta +controlledvocabulary.language.sos_kundi=Sos Kundi +controlledvocabulary.language.sota_kanum=Sota Kanum +controlledvocabulary.language.sou=Sou +controlledvocabulary.language.sou_nama=Sou Nama +controlledvocabulary.language.sou_upaa=Sou Upaa +controlledvocabulary.language.south_african_sign_language=South African Sign Language +controlledvocabulary.language.south_awyu=South Awyu +controlledvocabulary.language.south_azerbaijani=South Azerbaijani +controlledvocabulary.language.south_bolivian_quechua=South Bolivian Quechua +controlledvocabulary.language.south_central_banda=South Central Banda +controlledvocabulary.language.south_central_dinka=South Central Dinka +controlledvocabulary.language.south_efate=South Efate +controlledvocabulary.language.south_fali=South Fali +controlledvocabulary.language.south_giziga=South Giziga +controlledvocabulary.language.south_lembata=South Lembata +controlledvocabulary.language.south_marquesan=South Marquesan +controlledvocabulary.language.south_muyu=South Muyu +controlledvocabulary.language.southern_ndebele=South Ndebele, Southern Ndebele +controlledvocabulary.language.south_nuaulu=South Nuaulu +controlledvocabulary.language.south_picene=South Picene +controlledvocabulary.language.south_slavey=South Slavey +controlledvocabulary.language.south_tairora=South Tairora +controlledvocabulary.language.south_ucayali_asheninka=South Ucayali Ashéninka +controlledvocabulary.language.south_watut=South Watut +controlledvocabulary.language.south_west_bay=South West Bay +controlledvocabulary.language.southeast_ambrym=Southeast Ambrym +controlledvocabulary.language.southeast_babar=Southeast Babar +controlledvocabulary.language.southeast_ijo=Southeast Ijo +controlledvocabulary.language.southeast_pashai=Southeast Pashai +controlledvocabulary.language.southeast_tasmanian=Southeast Tasmanian +controlledvocabulary.language.southeastern_dinka=Southeastern Dinka +controlledvocabulary.language.southeastern_ixtlan_zapotec=Southeastern Ixtlán Zapotec +controlledvocabulary.language.southeastern_kolami=Southeastern Kolami +controlledvocabulary.language.southeastern_nochixtlan_mixtec=Southeastern Nochixtlán Mixtec +controlledvocabulary.language.southeastern_pomo=Southeastern Pomo +controlledvocabulary.language.southeastern_puebla_nahuatl=Southeastern Puebla Nahuatl +controlledvocabulary.language.southeastern_tarahumara=Southeastern Tarahumara +controlledvocabulary.language.southeastern_tepehuan=Southeastern Tepehuan +controlledvocabulary.language.southern_alta=Southern Alta +controlledvocabulary.language.southern_altai=Southern Altai +controlledvocabulary.language.southern_amami-oshima=Southern Amami-Oshima +controlledvocabulary.language.southern_aymara=Southern Aymara +controlledvocabulary.language.southern_bai=Southern Bai +controlledvocabulary.language.southern_balochi=Southern Balochi +controlledvocabulary.language.southern_betsimisaraka_malagasy=Southern Betsimisaraka Malagasy +controlledvocabulary.language.southern_binukidnon=Southern Binukidnon +controlledvocabulary.language.southern_birifor=Southern Birifor +controlledvocabulary.language.southern_bobo_madare=Southern Bobo Madaré +controlledvocabulary.language.southern_bontok=Southern Bontok +controlledvocabulary.language.southern_carrier=Southern Carrier +controlledvocabulary.language.southern_catanduanes_bikol=Southern Catanduanes Bikol +controlledvocabulary.language.southern_conchucos_ancash_quechua=Southern Conchucos Ancash Quechua +controlledvocabulary.language.southern_dagaare=Southern Dagaare +controlledvocabulary.language.southern_dong=Southern Dong +controlledvocabulary.language.southern_east_cree=Southern East Cree +controlledvocabulary.language.southern_ghale=Southern Ghale +controlledvocabulary.language.southern_grebo=Southern Grebo +controlledvocabulary.language.southern_guiyang_hmong=Southern Guiyang Hmong +controlledvocabulary.language.southern_haida=Southern Haida +controlledvocabulary.language.southern_hindko=Southern Hindko +controlledvocabulary.language.southern_kalapuya=Southern Kalapuya +controlledvocabulary.language.southern_kalinga=Southern Kalinga +controlledvocabulary.language.southern_katang=Southern Katang +controlledvocabulary.language.southern_kisi=Southern Kisi +controlledvocabulary.language.southern_kiwai=Southern Kiwai +controlledvocabulary.language.southern_kurdish=Southern Kurdish +controlledvocabulary.language.southern_lolopo=Southern Lolopo +controlledvocabulary.language.southern_luri=Southern Luri +controlledvocabulary.language.southern_ma'di=Southern Ma'di +controlledvocabulary.language.southern_mashan_hmong=Southern Mashan Hmong +controlledvocabulary.language.southern_mnong=Southern Mnong +controlledvocabulary.language.southern_muji=Southern Muji +controlledvocabulary.language.southern_nago=Southern Nago +controlledvocabulary.language.southern_nambikuara=Southern Nambikuára +controlledvocabulary.language.southern_ngbandi=Southern Ngbandi +controlledvocabulary.language.southern_nicobarese=Southern Nicobarese +controlledvocabulary.language.southern_nisu=Southern Nisu +controlledvocabulary.language.southern_nuni=Southern Nuni +controlledvocabulary.language.southern_ohlone=Southern Ohlone +controlledvocabulary.language.southern_one=Southern One +controlledvocabulary.language.southern_pame=Southern Pame +controlledvocabulary.language.southern_pashto=Southern Pashto +controlledvocabulary.language.southern_pastaza_quechua=Southern Pastaza Quechua +controlledvocabulary.language.southern_ping_chinese=Southern Ping Chinese +controlledvocabulary.language.southern_pomo=Southern Pomo +controlledvocabulary.language.southern_puebla_mixtec=Southern Puebla Mixtec +controlledvocabulary.language.southern_puget_sound_salish=Southern Puget Sound Salish +controlledvocabulary.language.southern_pumi=Southern Pumi +controlledvocabulary.language.southern_qiandong_miao=Southern Qiandong Miao +controlledvocabulary.language.southern_qiang=Southern Qiang +controlledvocabulary.language.southern_rengma_naga=Southern Rengma Naga +controlledvocabulary.language.southern_rincon_zapotec=Southern Rincon Zapotec +controlledvocabulary.language.southern_roglai=Southern Roglai +controlledvocabulary.language.southern_sama=Southern Sama +controlledvocabulary.language.southern_sami=Southern Sami +controlledvocabulary.language.southern_samo=Southern Samo +controlledvocabulary.language.southern_sierra_miwok=Southern Sierra Miwok +controlledvocabulary.language.southern_sorsoganon=Southern Sorsoganon controlledvocabulary.language.southern_sotho=Southern Sotho -controlledvocabulary.language.spanish,_castilian=Spanish, Castilian +controlledvocabulary.language.southern_subanen=Southern Subanen +controlledvocabulary.language.southern_thai=Southern Thai +controlledvocabulary.language.southern_tidung=Southern Tidung +controlledvocabulary.language.southern_tiwa=Southern Tiwa +controlledvocabulary.language.southern_toussian=Southern Toussian +controlledvocabulary.language.southern_tujia=Southern Tujia +controlledvocabulary.language.southern_tutchone=Southern Tutchone +controlledvocabulary.language.southern_uzbek=Southern Uzbek +controlledvocabulary.language.southern_yamphu=Southern Yamphu +controlledvocabulary.language.southern_yukaghir=Southern Yukaghir +controlledvocabulary.language.southwest_gbaya=Southwest Gbaya +controlledvocabulary.language.southwest_palawano=Southwest Palawano +controlledvocabulary.language.southwest_pashai=Southwest Pashai +controlledvocabulary.language.southwest_tanna=Southwest Tanna +controlledvocabulary.language.southwestern_bontok=Southwestern Bontok +controlledvocabulary.language.southwestern_dinka=Southwestern Dinka +controlledvocabulary.language.southwestern_fars=Southwestern Fars +controlledvocabulary.language.southwestern_guiyang_hmong=Southwestern Guiyang Hmong +controlledvocabulary.language.southwestern_huishui_hmong=Southwestern Huishui Hmong +controlledvocabulary.language.southwestern_nisu=Southwestern Nisu +controlledvocabulary.language.southwestern_tarahumara=Southwestern Tarahumara +controlledvocabulary.language.southwestern_tasmanian=Southwestern Tasmanian +controlledvocabulary.language.southwestern_tepehuan=Southwestern Tepehuan +controlledvocabulary.language.southwestern_tlaxiaco_mixtec=Southwestern Tlaxiaco Mixtec +controlledvocabulary.language.sowa=Sowa +controlledvocabulary.language.sowanda=Sowanda +controlledvocabulary.language.soyaltepec_mazatec=Soyaltepec Mazatec +controlledvocabulary.language.soyaltepec_mixtec=Soyaltepec Mixtec +controlledvocabulary.language.spanish_sign_language=Spanish Sign Language +controlledvocabulary.language.spiti_bhoti=Spiti Bhoti +controlledvocabulary.language.spokane=Spokane +controlledvocabulary.language.squamish=Squamish +controlledvocabulary.language.sranan_tongo=Sranan Tongo +controlledvocabulary.language.sri_lankan_creole_malay=Sri Lankan Creole Malay +controlledvocabulary.language.sri_lankan_sign_language=Sri Lankan Sign Language +controlledvocabulary.language.standard_arabic=Standard Arabic +controlledvocabulary.language.standard_estonian=Standard Estonian +controlledvocabulary.language.standard_latvian=Standard Latvian +controlledvocabulary.language.standard_malay=Standard Malay +controlledvocabulary.language.standard_moroccan_tamazight=Standard Moroccan Tamazight +controlledvocabulary.language.stellingwerfs=Stellingwerfs +controlledvocabulary.language.stod_bhoti=Stod Bhoti +controlledvocabulary.language.stoney=Stoney +controlledvocabulary.language.straits_salish=Straits Salish +controlledvocabulary.language.suabo=Suabo +controlledvocabulary.language.suarmin=Suarmin +controlledvocabulary.language.suau=Suau +controlledvocabulary.language.suba=Suba +controlledvocabulary.language.suba-simbiti=Suba-Simbiti +controlledvocabulary.language.subi=Subi +controlledvocabulary.language.subiya=Subiya +controlledvocabulary.language.subtiaba=Subtiaba +controlledvocabulary.language.sudanese_arabic=Sudanese Arabic +controlledvocabulary.language.sudanese_creole_arabic=Sudanese Creole Arabic +controlledvocabulary.language.sudest=Sudest +controlledvocabulary.language.sudovian=Sudovian +controlledvocabulary.language.suena=Suena +controlledvocabulary.language.suga=Suga +controlledvocabulary.language.suganga=Suganga +controlledvocabulary.language.sugut_dusun=Sugut Dusun +controlledvocabulary.language.sui=Sui +controlledvocabulary.language.suki=Suki +controlledvocabulary.language.suku=Suku +controlledvocabulary.language.sukuma=Sukuma +controlledvocabulary.language.sukur=Sukur +controlledvocabulary.language.sukurum=Sukurum +controlledvocabulary.language.sula=Sula +controlledvocabulary.language.sulka=Sulka +controlledvocabulary.language.sulod=Sulod +controlledvocabulary.language.suma=Suma +controlledvocabulary.language.sumariup=Sumariup +controlledvocabulary.language.sumau=Sumau +controlledvocabulary.language.sumbawa=Sumbawa +controlledvocabulary.language.sumbwa=Sumbwa +controlledvocabulary.language.sumerian=Sumerian +controlledvocabulary.language.sumi_naga=Sumi Naga +controlledvocabulary.language.sumtu_chin=Sumtu Chin +controlledvocabulary.language.sunam=Sunam controlledvocabulary.language.sundanese=Sundanese -controlledvocabulary.language.swahili=Swahili +controlledvocabulary.language.sunwar=Sunwar +controlledvocabulary.language.suoy=Suoy +controlledvocabulary.language.supyire_senoufo=Supyire Senoufo +controlledvocabulary.language.sur=Sur +controlledvocabulary.language.surbakhal=Surbakhal +controlledvocabulary.language.surgujia=Surgujia +controlledvocabulary.language.surigaonon=Surigaonon +controlledvocabulary.language.surjapuri=Surjapuri +controlledvocabulary.language.sursurunga=Sursurunga +controlledvocabulary.language.suruaha=Suruahá +controlledvocabulary.language.surubu=Surubu +controlledvocabulary.language.surui=Suruí +controlledvocabulary.language.surui_do_para=Suruí Do Pará +controlledvocabulary.language.susquehannock=Susquehannock +controlledvocabulary.language.susu=Susu +controlledvocabulary.language.susuami=Susuami +controlledvocabulary.language.suundi=Suundi +controlledvocabulary.language.suwawa=Suwawa +controlledvocabulary.language.suya=Suyá +controlledvocabulary.language.svan=Svan +controlledvocabulary.language.swabian=Swabian +controlledvocabulary.language.swahili_(individual_language)=Swahili (individual language) +controlledvocabulary.language.swahili=Swahili, Swahili (macrolanguage) +controlledvocabulary.language.swampy_cree=Swampy Cree controlledvocabulary.language.swati=Swati controlledvocabulary.language.swedish=Swedish +controlledvocabulary.language.swedish_sign_language=Swedish Sign Language +controlledvocabulary.language.swiss_german=Swiss German +controlledvocabulary.language.swiss-french_sign_language=Swiss-French Sign Language +controlledvocabulary.language.swiss-german_sign_language=Swiss-German Sign Language +controlledvocabulary.language.swiss-italian_sign_language=Swiss-Italian Sign Language +controlledvocabulary.language.swo=Swo +controlledvocabulary.language.syenara_senoufo=Syenara Senoufo +controlledvocabulary.language.sylheti=Sylheti +controlledvocabulary.language.syriac=Syriac +controlledvocabulary.language.sao_paulo_kaingang=São Paulo Kaingáng +controlledvocabulary.language.saotomense=Sãotomense +controlledvocabulary.language.sicite_senoufo=Sìcìté Sénoufo +controlledvocabulary.language.so=Sô +controlledvocabulary.language.t'apo=T'apo +controlledvocabulary.language.t'en=T'en +controlledvocabulary.language.ta'izzi-adeni_arabic=Ta'izzi-Adeni Arabic +controlledvocabulary.language.taabwa=Taabwa +controlledvocabulary.language.tabaa_zapotec=Tabaa Zapotec +controlledvocabulary.language.tabaru=Tabaru +controlledvocabulary.language.tabasco_chontal=Tabasco Chontal +controlledvocabulary.language.tabasco_nahuatl=Tabasco Nahuatl +controlledvocabulary.language.tabasco_zoque=Tabasco Zoque +controlledvocabulary.language.tabassaran=Tabassaran +controlledvocabulary.language.tabla=Tabla +controlledvocabulary.language.tabo=Tabo +controlledvocabulary.language.tabriak=Tabriak +controlledvocabulary.language.tacahua_mixtec=Tacahua Mixtec +controlledvocabulary.language.tacana=Tacana +controlledvocabulary.language.tachawit=Tachawit +controlledvocabulary.language.tachelhit=Tachelhit +controlledvocabulary.language.tachoni=Tachoni +controlledvocabulary.language.tadaksahak=Tadaksahak +controlledvocabulary.language.tado=Tado +controlledvocabulary.language.tadyawan=Tadyawan +controlledvocabulary.language.tae'=Tae' +controlledvocabulary.language.tafi=Tafi +controlledvocabulary.language.tagabawa=Tagabawa +controlledvocabulary.language.tagakaulo=Tagakaulo +controlledvocabulary.language.tagal_murut=Tagal Murut +controlledvocabulary.language.tagalaka=Tagalaka +controlledvocabulary.language.tagalog=Tagalog +controlledvocabulary.language.tagargrent=Tagargrent +controlledvocabulary.language.tagbanwa=Tagbanwa +controlledvocabulary.language.tagbu=Tagbu +controlledvocabulary.language.tagdal=Tagdal +controlledvocabulary.language.tagin=Tagin +controlledvocabulary.language.tagish=Tagish +controlledvocabulary.language.tagoi=Tagoi +controlledvocabulary.language.tagwana_senoufo=Tagwana Senoufo +controlledvocabulary.language.tahaggart_tamahaq=Tahaggart Tamahaq +controlledvocabulary.language.tahitian=Tahitian +controlledvocabulary.language.tahltan=Tahltan +controlledvocabulary.language.tai=Tai +controlledvocabulary.language.tai_daeng=Tai Daeng +controlledvocabulary.language.tai_dam=Tai Dam +controlledvocabulary.language.tai_do=Tai Do +controlledvocabulary.language.tai_don=Tai Dón +controlledvocabulary.language.tai_hongjin=Tai Hongjin +controlledvocabulary.language.tai_laing=Tai Laing +controlledvocabulary.language.tai_loi=Tai Loi +controlledvocabulary.language.tai_long=Tai Long +controlledvocabulary.language.tai_nua=Tai Nüa +controlledvocabulary.language.tai_pao=Tai Pao +controlledvocabulary.language.tai_thanh=Tai Thanh +controlledvocabulary.language.tai_ya=Tai Ya +controlledvocabulary.language.taiap=Taiap +controlledvocabulary.language.taikat=Taikat +controlledvocabulary.language.tainae=Tainae +controlledvocabulary.language.taino=Taino +controlledvocabulary.language.tairaha=Tairaha +controlledvocabulary.language.tairuma=Tairuma +controlledvocabulary.language.taita=Taita +controlledvocabulary.language.taivoan=Taivoan +controlledvocabulary.language.taiwan_sign_language=Taiwan Sign Language +controlledvocabulary.language.taje=Taje +controlledvocabulary.language.tajik=Tajik +controlledvocabulary.language.tajiki_arabic=Tajiki Arabic +controlledvocabulary.language.tajio=Tajio +controlledvocabulary.language.tajuasohn=Tajuasohn +controlledvocabulary.language.takelma=Takelma +controlledvocabulary.language.takestani=Takestani +controlledvocabulary.language.takia=Takia +controlledvocabulary.language.takua=Takua +controlledvocabulary.language.takuu=Takuu +controlledvocabulary.language.takwane=Takwane +controlledvocabulary.language.tal=Tal +controlledvocabulary.language.tala=Tala +controlledvocabulary.language.talaud=Talaud +controlledvocabulary.language.taliabu=Taliabu +controlledvocabulary.language.talieng=Talieng +controlledvocabulary.language.talinga-bwisi=Talinga-Bwisi +controlledvocabulary.language.talise=Talise +controlledvocabulary.language.talodi=Talodi +controlledvocabulary.language.taloki=Taloki +controlledvocabulary.language.talondo'=Talondo' +controlledvocabulary.language.talossan=Talossan +controlledvocabulary.language.talu=Talu +controlledvocabulary.language.talysh=Talysh +controlledvocabulary.language.tama_(chad)=Tama (Chad) +controlledvocabulary.language.tama_(colombia)=Tama (Colombia) +controlledvocabulary.language.tamagario=Tamagario +controlledvocabulary.language.taman_(indonesia)=Taman (Indonesia) +controlledvocabulary.language.taman_(myanmar)=Taman (Myanmar) +controlledvocabulary.language.tamanaku=Tamanaku +controlledvocabulary.language.tamashek=Tamashek +controlledvocabulary.language.tamasheq=Tamasheq +controlledvocabulary.language.tamazola_mixtec=Tamazola Mixtec +controlledvocabulary.language.tambas=Tambas +controlledvocabulary.language.tambora=Tambora +controlledvocabulary.language.tambotalo=Tambotalo +controlledvocabulary.language.tami=Tami controlledvocabulary.language.tamil=Tamil +controlledvocabulary.language.tamki=Tamki +controlledvocabulary.language.tamnim_citak=Tamnim Citak +controlledvocabulary.language.tampias_lobu=Tampias Lobu +controlledvocabulary.language.tampuan=Tampuan +controlledvocabulary.language.tampulma=Tampulma +controlledvocabulary.language.tanacross=Tanacross +controlledvocabulary.language.tanahmerah=Tanahmerah +controlledvocabulary.language.tanaina=Tanaina +controlledvocabulary.language.tanapag=Tanapag +controlledvocabulary.language.tandaganon=Tandaganon +controlledvocabulary.language.tandia=Tandia +controlledvocabulary.language.tandroy-mahafaly_malagasy=Tandroy-Mahafaly Malagasy +controlledvocabulary.language.tanema=Tanema +controlledvocabulary.language.tangale=Tangale +controlledvocabulary.language.tangchangya=Tangchangya +controlledvocabulary.language.tanggu=Tanggu +controlledvocabulary.language.tangkhul_naga_(india)=Tangkhul Naga (India) +controlledvocabulary.language.tangkhul_naga_(myanmar)=Tangkhul Naga (Myanmar) +controlledvocabulary.language.tangko=Tangko +controlledvocabulary.language.tanglang=Tanglang +controlledvocabulary.language.tangoa=Tangoa +controlledvocabulary.language.tanguat=Tanguat +controlledvocabulary.language.tangut=Tangut +controlledvocabulary.language.tanimbili=Tanimbili +controlledvocabulary.language.tanimuca-retuara=Tanimuca-Retuarã +controlledvocabulary.language.tanjijili=Tanjijili +controlledvocabulary.language.tanosy_malagasy=Tanosy Malagasy +controlledvocabulary.language.tanudan_kalinga=Tanudan Kalinga +controlledvocabulary.language.tanzanian_sign_language=Tanzanian Sign Language +controlledvocabulary.language.tapei=Tapei +controlledvocabulary.language.tapiete=Tapieté +controlledvocabulary.language.tapirape=Tapirapé +controlledvocabulary.language.tarao_naga=Tarao Naga +controlledvocabulary.language.tareng=Tareng +controlledvocabulary.language.tariana=Tariana +controlledvocabulary.language.tarifit=Tarifit +controlledvocabulary.language.tarjumo=Tarjumo +controlledvocabulary.language.tarok=Tarok +controlledvocabulary.language.tarpia=Tarpia +controlledvocabulary.language.tartessian=Tartessian +controlledvocabulary.language.taruma=Taruma +controlledvocabulary.language.tasawaq=Tasawaq +controlledvocabulary.language.tase_naga=Tase Naga +controlledvocabulary.language.tasmate=Tasmate +controlledvocabulary.language.tataltepec_chatino=Tataltepec Chatino +controlledvocabulary.language.tatana=Tatana +controlledvocabulary.language.tatar=Tatar +controlledvocabulary.language.tatuyo=Tatuyo +controlledvocabulary.language.tauade=Tauade +controlledvocabulary.language.taulil=Taulil +controlledvocabulary.language.taungyo=Taungyo +controlledvocabulary.language.taupota=Taupota +controlledvocabulary.language.tause=Tause +controlledvocabulary.language.taushiro=Taushiro +controlledvocabulary.language.tausug=Tausug +controlledvocabulary.language.tauya=Tauya +controlledvocabulary.language.taveta=Taveta +controlledvocabulary.language.tavoyan=Tavoyan +controlledvocabulary.language.tavringer_romani=Tavringer Romani +controlledvocabulary.language.tawala=Tawala +controlledvocabulary.language.tawallammat_tamajaq=Tawallammat Tamajaq +controlledvocabulary.language.tawande=Tawandê +controlledvocabulary.language.tawang_monpa=Tawang Monpa +controlledvocabulary.language.tawara=Tawara +controlledvocabulary.language.taworta=Taworta +controlledvocabulary.language.tawoyan=Tawoyan +controlledvocabulary.language.tawr_chin=Tawr Chin +controlledvocabulary.language.tay_boi=Tay Boi +controlledvocabulary.language.tay_khang=Tay Khang +controlledvocabulary.language.tayart_tamajeq=Tayart Tamajeq +controlledvocabulary.language.tayo=Tayo +controlledvocabulary.language.taznatit=Taznatit +controlledvocabulary.language.tboli=Tboli +controlledvocabulary.language.tchitchege=Tchitchege +controlledvocabulary.language.tchumbuli=Tchumbuli +controlledvocabulary.language.te'un=Te'un +controlledvocabulary.language.teanu=Teanu +controlledvocabulary.language.tebul_sign_language=Tebul Sign Language +controlledvocabulary.language.tebul_ure_dogon=Tebul Ure Dogon +controlledvocabulary.language.tecpatlan_totonac=Tecpatlán Totonac +controlledvocabulary.language.tedaga=Tedaga +controlledvocabulary.language.tedim_chin=Tedim Chin +controlledvocabulary.language.tee=Tee +controlledvocabulary.language.tefaro=Tefaro +controlledvocabulary.language.tegali=Tegali +controlledvocabulary.language.tehit=Tehit +controlledvocabulary.language.tehuelche=Tehuelche +controlledvocabulary.language.tejalapan_zapotec=Tejalapan Zapotec +controlledvocabulary.language.teke-ebo=Teke-Ebo +controlledvocabulary.language.teke-fuumu=Teke-Fuumu +controlledvocabulary.language.teke-kukuya=Teke-Kukuya +controlledvocabulary.language.teke-laali=Teke-Laali +controlledvocabulary.language.teke-nzikou=Teke-Nzikou +controlledvocabulary.language.teke-tege=Teke-Tege +controlledvocabulary.language.teke-tsaayi=Teke-Tsaayi +controlledvocabulary.language.teke-tyee=Teke-Tyee +controlledvocabulary.language.tektiteko=Tektiteko +controlledvocabulary.language.tela-masbuar=Tela-Masbuar +controlledvocabulary.language.telefol=Telefol controlledvocabulary.language.telugu=Telugu -controlledvocabulary.language.tajik=Tajik +controlledvocabulary.language.tem=Tem +controlledvocabulary.language.temacine_tamazight=Temacine Tamazight +controlledvocabulary.language.temascaltepec_nahuatl=Temascaltepec Nahuatl +controlledvocabulary.language.tembo_(kitembo)=Tembo (Kitembo) +controlledvocabulary.language.tembo_(motembo)=Tembo (Motembo) +controlledvocabulary.language.tembe=Tembé +controlledvocabulary.language.teme=Teme +controlledvocabulary.language.temein=Temein +controlledvocabulary.language.temi=Temi +controlledvocabulary.language.temiar=Temiar +controlledvocabulary.language.temoaya_otomi=Temoaya Otomi +controlledvocabulary.language.temoq=Temoq +controlledvocabulary.language.temuan=Temuan +controlledvocabulary.language.ten'edn=Ten'edn +controlledvocabulary.language.tena_lowland_quichua=Tena Lowland Quichua +controlledvocabulary.language.tenango_otomi=Tenango Otomi +controlledvocabulary.language.tene_kan_dogon=Tene Kan Dogon +controlledvocabulary.language.tenggarong_kutai_malay=Tenggarong Kutai Malay +controlledvocabulary.language.tengger=Tengger +controlledvocabulary.language.tenharim=Tenharim +controlledvocabulary.language.tenino=Tenino +controlledvocabulary.language.tenis=Tenis +controlledvocabulary.language.tennet=Tennet +controlledvocabulary.language.teop=Teop +controlledvocabulary.language.teor=Teor +controlledvocabulary.language.tepecano=Tepecano +controlledvocabulary.language.tepetotutla_chinantec=Tepetotutla Chinantec +controlledvocabulary.language.tepeuxila_cuicatec=Tepeuxila Cuicatec +controlledvocabulary.language.tepinapa_chinantec=Tepinapa Chinantec +controlledvocabulary.language.tepo_krumen=Tepo Krumen +controlledvocabulary.language.ter_sami=Ter Sami +controlledvocabulary.language.tera=Tera +controlledvocabulary.language.terebu=Terebu +controlledvocabulary.language.terei=Terei +controlledvocabulary.language.tereno=Tereno +controlledvocabulary.language.teressa=Teressa +controlledvocabulary.language.tereweng=Tereweng +controlledvocabulary.language.teribe=Teribe +controlledvocabulary.language.terik=Terik +controlledvocabulary.language.termanu=Termanu +controlledvocabulary.language.ternate=Ternate +controlledvocabulary.language.ternateno=Ternateño +controlledvocabulary.language.tesaka_malagasy=Tesaka Malagasy +controlledvocabulary.language.tese=Tese +controlledvocabulary.language.teshenawa=Teshenawa +controlledvocabulary.language.teso=Teso +controlledvocabulary.language.tetela=Tetela +controlledvocabulary.language.tetelcingo_nahuatl=Tetelcingo Nahuatl +controlledvocabulary.language.tetete=Tetete +controlledvocabulary.language.tetserret=Tetserret +controlledvocabulary.language.tetum=Tetum +controlledvocabulary.language.tetun_dili=Tetun Dili +controlledvocabulary.language.teutila_cuicatec=Teutila Cuicatec +controlledvocabulary.language.tewa_(indonesia)=Tewa (Indonesia) +controlledvocabulary.language.tewa_(usa)=Tewa (USA) +controlledvocabulary.language.tewe=Tewe +controlledvocabulary.language.texcatepec_otomi=Texcatepec Otomi +controlledvocabulary.language.texistepec_popoluca=Texistepec Popoluca +controlledvocabulary.language.texmelucan_zapotec=Texmelucan Zapotec +controlledvocabulary.language.tezoatlan_mixtec=Tezoatlán Mixtec +controlledvocabulary.language.tha=Tha +controlledvocabulary.language.thachanadan=Thachanadan +controlledvocabulary.language.thado_chin=Thado Chin controlledvocabulary.language.thai=Thai +controlledvocabulary.language.thai_sign_language=Thai Sign Language +controlledvocabulary.language.thai_song=Thai Song +controlledvocabulary.language.thaiphum_chin=Thaiphum Chin +controlledvocabulary.language.thakali=Thakali +controlledvocabulary.language.thangal_naga=Thangal Naga +controlledvocabulary.language.thangmi=Thangmi +controlledvocabulary.language.thao=Thao +controlledvocabulary.language.tharaka=Tharaka +controlledvocabulary.language.thawa=Thawa +controlledvocabulary.language.thaypan=Thaypan +controlledvocabulary.language.thiin=Thiin +controlledvocabulary.language.tho=Tho +controlledvocabulary.language.thompson=Thompson +controlledvocabulary.language.thopho=Thopho +controlledvocabulary.language.thracian=Thracian +controlledvocabulary.language.thu_lao=Thu Lao +controlledvocabulary.language.thulung=Thulung +controlledvocabulary.language.thur=Thur +controlledvocabulary.language.thuri=Thuri +controlledvocabulary.language.tiagbamrin_aizi=Tiagbamrin Aizi +controlledvocabulary.language.tiale=Tiale +controlledvocabulary.language.tiang=Tiang +controlledvocabulary.language.tibea=Tibea +controlledvocabulary.language.tibetan_sign_language=Tibetan Sign Language +controlledvocabulary.language.tibetan,_tibetan_standard,_central=Tibetan, Tibetan Standard, Central +controlledvocabulary.language.tichurong=Tichurong +controlledvocabulary.language.ticuna=Ticuna +controlledvocabulary.language.tidaa_mixtec=Tidaá Mixtec +controlledvocabulary.language.tidikelt_tamazight=Tidikelt Tamazight +controlledvocabulary.language.tidore=Tidore +controlledvocabulary.language.tiemacewe_bozo=Tiemacèwè Bozo +controlledvocabulary.language.tiene=Tiene +controlledvocabulary.language.tifal=Tifal +controlledvocabulary.language.tigak=Tigak +controlledvocabulary.language.tigon_mbembe=Tigon Mbembe +controlledvocabulary.language.tigre=Tigre controlledvocabulary.language.tigrinya=Tigrinya -controlledvocabulary.language.tibetan_standard,_tibetan,_central=Tibetan Standard, Tibetan, Central -controlledvocabulary.language.turkmen=Turkmen -controlledvocabulary.language.tagalog=Tagalog -controlledvocabulary.language.tswana=Tswana +controlledvocabulary.language.tii=Tii +controlledvocabulary.language.tijaltepec_mixtec=Tijaltepec Mixtec +controlledvocabulary.language.tikar=Tikar +controlledvocabulary.language.tikopia=Tikopia +controlledvocabulary.language.tilapa_otomi=Tilapa Otomi +controlledvocabulary.language.tillamook=Tillamook +controlledvocabulary.language.tilquiapan_zapotec=Tilquiapan Zapotec +controlledvocabulary.language.tilung=Tilung +controlledvocabulary.language.tima=Tima +controlledvocabulary.language.timbe=Timbe +controlledvocabulary.language.timne=Timne +controlledvocabulary.language.timor_pidgin=Timor Pidgin +controlledvocabulary.language.timucua=Timucua +controlledvocabulary.language.timugon_murut=Timugon Murut +controlledvocabulary.language.tinani=Tinani +controlledvocabulary.language.tindi=Tindi +controlledvocabulary.language.tingui-boto=Tingui-Boto +controlledvocabulary.language.tinigua=Tinigua +controlledvocabulary.language.tinputz=Tinputz +controlledvocabulary.language.tippera=Tippera +controlledvocabulary.language.tira=Tira +controlledvocabulary.language.tirahi=Tirahi +controlledvocabulary.language.tiranige_diga_dogon=Tiranige Diga Dogon +controlledvocabulary.language.tiri=Tiri +controlledvocabulary.language.tirmaga-chai_suri=Tirmaga-Chai Suri +controlledvocabulary.language.tiruray=Tiruray +controlledvocabulary.language.tita=Tita +controlledvocabulary.language.titan=Titan +controlledvocabulary.language.tiv=Tiv +controlledvocabulary.language.tiwa=Tiwa +controlledvocabulary.language.tiwi=Tiwi +controlledvocabulary.language.tiyaa=Tiyaa +controlledvocabulary.language.tiefo=Tiéfo +controlledvocabulary.language.tieyaxo_bozo=Tiéyaxo Bozo +controlledvocabulary.language.tjungundji=Tjungundji +controlledvocabulary.language.tjupany=Tjupany +controlledvocabulary.language.tjurruru=Tjurruru +controlledvocabulary.language.tlachichilco_tepehua=Tlachichilco Tepehua +controlledvocabulary.language.tlacoapa_me'phaa=Tlacoapa Me'phaa +controlledvocabulary.language.tlacoatzintepec_chinantec=Tlacoatzintepec Chinantec +controlledvocabulary.language.tlacolulita_zapotec=Tlacolulita Zapotec +controlledvocabulary.language.tlahuitoltepec_mixe=Tlahuitoltepec Mixe +controlledvocabulary.language.tlamacazapa_nahuatl=Tlamacazapa Nahuatl +controlledvocabulary.language.tlazoyaltepec_mixtec=Tlazoyaltepec Mixtec +controlledvocabulary.language.tlicho=Tlicho +controlledvocabulary.language.tlingit=Tlingit +controlledvocabulary.language.to=To +controlledvocabulary.language.to'abaita=To'abaita +controlledvocabulary.language.toaripi=Toaripi +controlledvocabulary.language.toba=Toba +controlledvocabulary.language.toba-maskoy=Toba-Maskoy +controlledvocabulary.language.tobagonian_creole_english=Tobagonian Creole English +controlledvocabulary.language.tobanga=Tobanga +controlledvocabulary.language.tobati=Tobati +controlledvocabulary.language.tobelo=Tobelo +controlledvocabulary.language.tobian=Tobian +controlledvocabulary.language.tobilung=Tobilung +controlledvocabulary.language.tobo=Tobo +controlledvocabulary.language.tocantins_asurini=Tocantins Asurini +controlledvocabulary.language.tocho=Tocho +controlledvocabulary.language.toda=Toda +controlledvocabulary.language.todrah=Todrah +controlledvocabulary.language.tofanma=Tofanma +controlledvocabulary.language.tofin_gbe=Tofin Gbe +controlledvocabulary.language.togbo-vara_banda=Togbo-Vara Banda +controlledvocabulary.language.togoyo=Togoyo +controlledvocabulary.language.tohono_o'odham=Tohono O'odham +controlledvocabulary.language.tojolabal=Tojolabal +controlledvocabulary.language.tok_pisin=Tok Pisin +controlledvocabulary.language.tokano=Tokano +controlledvocabulary.language.tokelau=Tokelau +controlledvocabulary.language.tokharian_a=Tokharian A +controlledvocabulary.language.tokharian_b=Tokharian B +controlledvocabulary.language.toki_pona=Toki Pona +controlledvocabulary.language.toku-no-shima=Toku-No-Shima +controlledvocabulary.language.tol=Tol +controlledvocabulary.language.tolaki=Tolaki +controlledvocabulary.language.tolomako=Tolomako +controlledvocabulary.language.tolowa=Tolowa +controlledvocabulary.language.toma=Toma +controlledvocabulary.language.tomadino=Tomadino +controlledvocabulary.language.tombelala=Tombelala +controlledvocabulary.language.tombonuo=Tombonuo +controlledvocabulary.language.tombulu=Tombulu +controlledvocabulary.language.tomini=Tomini +controlledvocabulary.language.tommo_so_dogon=Tommo So Dogon +controlledvocabulary.language.tomo_kan_dogon=Tomo Kan Dogon +controlledvocabulary.language.tomoip=Tomoip +controlledvocabulary.language.tondano=Tondano +controlledvocabulary.language.tondi_songway_kiini=Tondi Songway Kiini +controlledvocabulary.language.tonga_(nyasa)=Tonga (Nyasa) controlledvocabulary.language.tonga_(tonga_islands)=Tonga (Tonga Islands) -controlledvocabulary.language.turkish=Turkish +controlledvocabulary.language.tonga_(zambia)=Tonga (Zambia) +controlledvocabulary.language.tongwe=Tongwe +controlledvocabulary.language.tonjon=Tonjon +controlledvocabulary.language.tonkawa=Tonkawa +controlledvocabulary.language.tonsawang=Tonsawang +controlledvocabulary.language.tonsea=Tonsea +controlledvocabulary.language.tontemboan=Tontemboan +controlledvocabulary.language.tooro=Tooro +controlledvocabulary.language.topoiyo=Topoiyo +controlledvocabulary.language.toposa=Toposa +controlledvocabulary.language.toraja-sa'dan=Toraja-Sa'dan +controlledvocabulary.language.toram=Toram +controlledvocabulary.language.torau=Torau +controlledvocabulary.language.tornedalen_finnish=Tornedalen Finnish +controlledvocabulary.language.toro=Toro +controlledvocabulary.language.toro_so_dogon=Toro So Dogon +controlledvocabulary.language.toro_tegu_dogon=Toro Tegu Dogon +controlledvocabulary.language.toromono=Toromono +controlledvocabulary.language.torona=Torona +controlledvocabulary.language.torres_strait_creole=Torres Strait Creole +controlledvocabulary.language.torricelli=Torricelli +controlledvocabulary.language.torwali=Torwali +controlledvocabulary.language.tora=Torá +controlledvocabulary.language.tosk_albanian=Tosk Albanian +controlledvocabulary.language.totela=Totela +controlledvocabulary.language.toto=Toto +controlledvocabulary.language.totoli=Totoli +controlledvocabulary.language.totomachapan_zapotec=Totomachapan Zapotec +controlledvocabulary.language.totontepec_mixe=Totontepec Mixe +controlledvocabulary.language.totoro=Totoro +controlledvocabulary.language.touo=Touo +controlledvocabulary.language.toura_(cote_d'ivoire)=Toura (Côte d'Ivoire) +controlledvocabulary.language.toura_(papua_new_guinea)=Toura (Papua New Guinea) +controlledvocabulary.language.towei=Towei +controlledvocabulary.language.transalpine_gaulish=Transalpine Gaulish +controlledvocabulary.language.traveller_danish=Traveller Danish +controlledvocabulary.language.traveller_norwegian=Traveller Norwegian +controlledvocabulary.language.traveller_scottish=Traveller Scottish +controlledvocabulary.language.tregami=Tregami +controlledvocabulary.language.tremembe=Tremembé +controlledvocabulary.language.trieng=Trieng +controlledvocabulary.language.trimuris=Trimuris +controlledvocabulary.language.tring=Tring +controlledvocabulary.language.tringgus-sembaan_bidayuh=Tringgus-Sembaan Bidayuh +controlledvocabulary.language.trinidad_and_tobago_sign_language=Trinidad and Tobago Sign Language +controlledvocabulary.language.trinidadian_creole_english=Trinidadian Creole English +controlledvocabulary.language.trinitario=Trinitario +controlledvocabulary.language.trio=Trió +controlledvocabulary.language.truka=Truká +controlledvocabulary.language.trumai=Trumai +controlledvocabulary.language.ts'un-lao=Ts'ün-Lao +controlledvocabulary.language.tsaangi=Tsaangi +controlledvocabulary.language.tsakhur=Tsakhur +controlledvocabulary.language.tsakonian=Tsakonian +controlledvocabulary.language.tsakwambo=Tsakwambo +controlledvocabulary.language.tsamai=Tsamai +controlledvocabulary.language.tsat=Tsat +controlledvocabulary.language.tseku=Tseku +controlledvocabulary.language.tsetsaut=Tsetsaut +controlledvocabulary.language.tshangla=Tshangla +controlledvocabulary.language.tsikimba=Tsikimba +controlledvocabulary.language.tsimane=Tsimané +controlledvocabulary.language.tsimihety_malagasy=Tsimihety Malagasy +controlledvocabulary.language.tsimshian=Tsimshian +controlledvocabulary.language.tsishingini=Tsishingini +controlledvocabulary.language.tso=Tso +controlledvocabulary.language.tsoa=Tsoa +controlledvocabulary.language.tsogo=Tsogo controlledvocabulary.language.tsonga=Tsonga -controlledvocabulary.language.tatar=Tatar +controlledvocabulary.language.tsotso=Tsotso +controlledvocabulary.language.tsou=Tsou +controlledvocabulary.language.tsucuba=Tsucuba +controlledvocabulary.language.tsum=Tsum +controlledvocabulary.language.tsuvadi=Tsuvadi +controlledvocabulary.language.tsuvan=Tsuvan +controlledvocabulary.language.tswa=Tswa +controlledvocabulary.language.tswana=Tswana +controlledvocabulary.language.tswapong=Tswapong +controlledvocabulary.language.tu=Tu +controlledvocabulary.language.tuamotuan=Tuamotuan +controlledvocabulary.language.tubar=Tubar +controlledvocabulary.language.tucano=Tucano +controlledvocabulary.language.tugen=Tugen +controlledvocabulary.language.tugun=Tugun +controlledvocabulary.language.tugutil=Tugutil +controlledvocabulary.language.tukang_besi_north=Tukang Besi North +controlledvocabulary.language.tukang_besi_south=Tukang Besi South +controlledvocabulary.language.tuki=Tuki +controlledvocabulary.language.tukpa=Tukpa +controlledvocabulary.language.tukudede=Tukudede +controlledvocabulary.language.tukumanfed=Tukumanféd +controlledvocabulary.language.tula=Tula +controlledvocabulary.language.tulai=Tulai +controlledvocabulary.language.tulehu=Tulehu +controlledvocabulary.language.tulishi=Tulishi +controlledvocabulary.language.tulu=Tulu +controlledvocabulary.language.tulu-bohuai=Tulu-Bohuai +controlledvocabulary.language.tuma-irumu=Tuma-Irumu +controlledvocabulary.language.tumak=Tumak +controlledvocabulary.language.tumari_kanuri=Tumari Kanuri +controlledvocabulary.language.tumbuka=Tumbuka +controlledvocabulary.language.tumi=Tumi +controlledvocabulary.language.tumleo=Tumleo +controlledvocabulary.language.tumshuqese=Tumshuqese +controlledvocabulary.language.tumtum=Tumtum +controlledvocabulary.language.tumulung_sisaala=Tumulung Sisaala +controlledvocabulary.language.tumzabt=Tumzabt +controlledvocabulary.language.tundra_enets=Tundra Enets +controlledvocabulary.language.tunen=Tunen +controlledvocabulary.language.tungag=Tungag +controlledvocabulary.language.tunggare=Tunggare +controlledvocabulary.language.tunia=Tunia +controlledvocabulary.language.tunica=Tunica +controlledvocabulary.language.tunisian_arabic=Tunisian Arabic +controlledvocabulary.language.tunisian_sign_language=Tunisian Sign Language +controlledvocabulary.language.tunjung=Tunjung +controlledvocabulary.language.tunni=Tunni +controlledvocabulary.language.tunzu=Tunzu +controlledvocabulary.language.tuotomb=Tuotomb +controlledvocabulary.language.tupari=Tuparí +controlledvocabulary.language.tupinamba=Tupinambá +controlledvocabulary.language.tupinikin=Tupinikin +controlledvocabulary.language.tupuri=Tupuri +controlledvocabulary.language.turaka=Turaka +controlledvocabulary.language.turi=Turi +controlledvocabulary.language.turiwara=Turiwára +controlledvocabulary.language.turka=Turka +controlledvocabulary.language.turkana=Turkana +controlledvocabulary.language.turkish=Turkish +controlledvocabulary.language.turkish_sign_language=Turkish Sign Language +controlledvocabulary.language.turkmen=Turkmen +controlledvocabulary.language.turks_and_caicos_creole_english=Turks And Caicos Creole English +controlledvocabulary.language.turoyo=Turoyo +controlledvocabulary.language.turumsa=Turumsa +controlledvocabulary.language.turung=Turung +controlledvocabulary.language.tuscarora=Tuscarora +controlledvocabulary.language.tutelo=Tutelo +controlledvocabulary.language.tutong=Tutong +controlledvocabulary.language.tutsa_naga=Tutsa Naga +controlledvocabulary.language.tutuba=Tutuba +controlledvocabulary.language.tututepec_mixtec=Tututepec Mixtec +controlledvocabulary.language.tututni=Tututni +controlledvocabulary.language.tuvalu=Tuvalu +controlledvocabulary.language.tuvinian=Tuvinian +controlledvocabulary.language.tuwali_ifugao=Tuwali Ifugao +controlledvocabulary.language.tuwari=Tuwari +controlledvocabulary.language.tuwuli=Tuwuli +controlledvocabulary.language.tuxinawa=Tuxináwa +controlledvocabulary.language.tuxa=Tuxá +controlledvocabulary.language.tuyuca=Tuyuca +controlledvocabulary.language.twana=Twana +controlledvocabulary.language.twendi=Twendi +controlledvocabulary.language.twents=Twents controlledvocabulary.language.twi=Twi -controlledvocabulary.language.tahitian=Tahitian -controlledvocabulary.language.uyghur,_uighur=Uyghur, Uighur +controlledvocabulary.language.tyap=Tyap +controlledvocabulary.language.tz'utujil=Tz'utujil +controlledvocabulary.language.tzeltal=Tzeltal +controlledvocabulary.language.tzotzil=Tzotzil +controlledvocabulary.language.tay=Tày +controlledvocabulary.language.tay_sa_pa=Tày Sa Pa +controlledvocabulary.language.tay_tac=Tày Tac +controlledvocabulary.language.teen=Téén +controlledvocabulary.language.tubatulabal=Tübatulabal +controlledvocabulary.language.u=U +controlledvocabulary.language.uab_meto=Uab Meto +controlledvocabulary.language.uamue=Uamué +controlledvocabulary.language.uare=Uare +controlledvocabulary.language.ubaghara=Ubaghara +controlledvocabulary.language.ubang=Ubang +controlledvocabulary.language.ubi=Ubi +controlledvocabulary.language.ubir=Ubir +controlledvocabulary.language.ubykh=Ubykh +controlledvocabulary.language.ucayali-yurua_asheninka=Ucayali-Yurúa Ashéninka +controlledvocabulary.language.uda=Uda +controlledvocabulary.language.udi=Udi +controlledvocabulary.language.udihe=Udihe +controlledvocabulary.language.udmurt=Udmurt +controlledvocabulary.language.uduk=Uduk +controlledvocabulary.language.ufim=Ufim +controlledvocabulary.language.ugandan_sign_language=Ugandan Sign Language +controlledvocabulary.language.ugaritic=Ugaritic +controlledvocabulary.language.ughele=Ughele +controlledvocabulary.language.ugong=Ugong +controlledvocabulary.language.uhami=Uhami +controlledvocabulary.language.uighur,_uyghur=Uighur, Uyghur +controlledvocabulary.language.uisai=Uisai +controlledvocabulary.language.ujir=Ujir +controlledvocabulary.language.ukaan=Ukaan +controlledvocabulary.language.ukhwejo=Ukhwejo +controlledvocabulary.language.ukit=Ukit +controlledvocabulary.language.ukpe-bayobiri=Ukpe-Bayobiri +controlledvocabulary.language.ukpet-ehom=Ukpet-Ehom controlledvocabulary.language.ukrainian=Ukrainian +controlledvocabulary.language.ukrainian_sign_language=Ukrainian Sign Language +controlledvocabulary.language.ukue=Ukue +controlledvocabulary.language.ukuriguma=Ukuriguma +controlledvocabulary.language.ukwa=Ukwa +controlledvocabulary.language.ukwuani-aboh-ndoni=Ukwuani-Aboh-Ndoni +controlledvocabulary.language.ulau-suain=Ulau-Suain +controlledvocabulary.language.ulch=Ulch +controlledvocabulary.language.ulithian=Ulithian +controlledvocabulary.language.ullatan=Ullatan +controlledvocabulary.language.ulukwumi=Ulukwumi +controlledvocabulary.language.ulumanda'=Ulumanda' +controlledvocabulary.language.ulwa=Ulwa +controlledvocabulary.language.uma=Uma +controlledvocabulary.language.uma'_lasan=Uma' Lasan +controlledvocabulary.language.uma'_lung=Uma' Lung +controlledvocabulary.language.umanakaina=Umanakaina +controlledvocabulary.language.umatilla=Umatilla +controlledvocabulary.language.umbindhamu=Umbindhamu +controlledvocabulary.language.umbrian=Umbrian +controlledvocabulary.language.umbu-ungu=Umbu-Ungu +controlledvocabulary.language.umbugarla=Umbugarla +controlledvocabulary.language.umbundu=Umbundu +controlledvocabulary.language.ume_sami=Ume Sami +controlledvocabulary.language.umeda=Umeda +controlledvocabulary.language.umiida=Umiida +controlledvocabulary.language.umiray_dumaget_agta=Umiray Dumaget Agta +controlledvocabulary.language.umon=Umon +controlledvocabulary.language.umotina=Umotína +controlledvocabulary.language.umpila=Umpila +controlledvocabulary.language.una=Una +controlledvocabulary.language.unami=Unami +controlledvocabulary.language.uncoded_languages=Uncoded languages +controlledvocabulary.language.unde_kaili=Unde Kaili +controlledvocabulary.language.undetermined=Undetermined +controlledvocabulary.language.uneapa=Uneapa +controlledvocabulary.language.uneme=Uneme +controlledvocabulary.language.unggaranggu=Unggaranggu +controlledvocabulary.language.unggumi=Unggumi +controlledvocabulary.language.uni=Uni +controlledvocabulary.language.unserdeutsch=Unserdeutsch +controlledvocabulary.language.unua=Unua +controlledvocabulary.language.unubahe=Unubahe +controlledvocabulary.language.upper_chehalis=Upper Chehalis +controlledvocabulary.language.upper_grand_valley_dani=Upper Grand Valley Dani +controlledvocabulary.language.upper_guinea_crioulo=Upper Guinea Crioulo +controlledvocabulary.language.upper_kinabatangan=Upper Kinabatangan +controlledvocabulary.language.upper_kuskokwim=Upper Kuskokwim +controlledvocabulary.language.upper_necaxa_totonac=Upper Necaxa Totonac +controlledvocabulary.language.upper_saxon=Upper Saxon +controlledvocabulary.language.upper_sorbian=Upper Sorbian +controlledvocabulary.language.upper_ta'oih=Upper Ta'oih +controlledvocabulary.language.upper_tanana=Upper Tanana +controlledvocabulary.language.upper_taromi=Upper Taromi +controlledvocabulary.language.upper_umpqua=Upper Umpqua +controlledvocabulary.language.ura_(papua_new_guinea)=Ura (Papua New Guinea) +controlledvocabulary.language.ura_(vanuatu)=Ura (Vanuatu) +controlledvocabulary.language.uradhi=Uradhi +controlledvocabulary.language.urak_lawoi'=Urak Lawoi' +controlledvocabulary.language.urali=Urali +controlledvocabulary.language.urapmin=Urapmin +controlledvocabulary.language.urarina=Urarina +controlledvocabulary.language.urartian=Urartian +controlledvocabulary.language.urat=Urat controlledvocabulary.language.urdu=Urdu +controlledvocabulary.language.urhobo=Urhobo +controlledvocabulary.language.uri=Uri +controlledvocabulary.language.urigina=Urigina +controlledvocabulary.language.urim=Urim +controlledvocabulary.language.urimo=Urimo +controlledvocabulary.language.uripiv-wala-rano-atchin=Uripiv-Wala-Rano-Atchin +controlledvocabulary.language.urningangg=Urningangg +controlledvocabulary.language.uru=Uru +controlledvocabulary.language.uru-eu-wau-wau=Uru-Eu-Wau-Wau +controlledvocabulary.language.uru-pa-in=Uru-Pa-In +controlledvocabulary.language.uruangnirin=Uruangnirin +controlledvocabulary.language.uruava=Uruava +controlledvocabulary.language.urubu-kaapor=Urubú-Kaapor +controlledvocabulary.language.urubu-kaapor_sign_language=Urubú-Kaapor Sign Language +controlledvocabulary.language.uruguayan_sign_language=Uruguayan Sign Language +controlledvocabulary.language.urum=Urum +controlledvocabulary.language.urumi=Urumi +controlledvocabulary.language.usaghade=Usaghade +controlledvocabulary.language.usan=Usan +controlledvocabulary.language.usarufa=Usarufa +controlledvocabulary.language.ushojo=Ushojo +controlledvocabulary.language.usila_chinantec=Usila Chinantec +controlledvocabulary.language.usku=Usku +controlledvocabulary.language.uspanteco=Uspanteco +controlledvocabulary.language.usui=Usui +controlledvocabulary.language.utarmbung=Utarmbung +controlledvocabulary.language.ute-southern_paiute=Ute-Southern Paiute +controlledvocabulary.language.utu=Utu +controlledvocabulary.language.uvbie=Uvbie +controlledvocabulary.language.uya=Uya +controlledvocabulary.language.uyajitaya=Uyajitaya controlledvocabulary.language.uzbek=Uzbek +controlledvocabulary.language.uzbeki_arabic=Uzbeki Arabic +controlledvocabulary.language.uzekwe=Uzekwe +controlledvocabulary.language.vaagri_booli=Vaagri Booli +controlledvocabulary.language.vafsi=Vafsi +controlledvocabulary.language.vaghri=Vaghri +controlledvocabulary.language.vaghua=Vaghua +controlledvocabulary.language.vagla=Vagla +controlledvocabulary.language.vai=Vai +controlledvocabulary.language.vaiphei=Vaiphei +controlledvocabulary.language.vale=Vale +controlledvocabulary.language.valencian_sign_language=Valencian Sign Language +controlledvocabulary.language.valle_nacional_chinantec=Valle Nacional Chinantec +controlledvocabulary.language.valley_maidu=Valley Maidu +controlledvocabulary.language.valman=Valman +controlledvocabulary.language.valpei=Valpei +controlledvocabulary.language.vamale=Vamale +controlledvocabulary.language.vame=Vame +controlledvocabulary.language.vandalic=Vandalic +controlledvocabulary.language.vangunu=Vangunu +controlledvocabulary.language.vanimo=Vanimo +controlledvocabulary.language.vano=Vano +controlledvocabulary.language.vanuma=Vanuma +controlledvocabulary.language.vao=Vao +controlledvocabulary.language.varhadi-nagpuri=Varhadi-Nagpuri +controlledvocabulary.language.varisi=Varisi +controlledvocabulary.language.varli=Varli +controlledvocabulary.language.vasavi=Vasavi +controlledvocabulary.language.veddah=Veddah +controlledvocabulary.language.vedic_sanskrit=Vedic Sanskrit +controlledvocabulary.language.vehes=Vehes +controlledvocabulary.language.veluws=Veluws +controlledvocabulary.language.vemgo-mabas=Vemgo-Mabas controlledvocabulary.language.venda=Venda +controlledvocabulary.language.venetian=Venetian +controlledvocabulary.language.venetic=Venetic +controlledvocabulary.language.venezuelan_sign_language=Venezuelan Sign Language +controlledvocabulary.language.vengo=Vengo +controlledvocabulary.language.ventureno=Ventureño +controlledvocabulary.language.veps=Veps +controlledvocabulary.language.vera'a=Vera'a +controlledvocabulary.language.vestinian=Vestinian +controlledvocabulary.language.vidunda=Vidunda +controlledvocabulary.language.viemo=Viemo controlledvocabulary.language.vietnamese=Vietnamese +controlledvocabulary.language.vilela=Vilela +controlledvocabulary.language.vili=Vili +controlledvocabulary.language.villa_viciosa_agta=Villa Viciosa Agta +controlledvocabulary.language.vincentian_creole_english=Vincentian Creole English +controlledvocabulary.language.vinmavis=Vinmavis +controlledvocabulary.language.vinza=Vinza +controlledvocabulary.language.virgin_islands_creole_english=Virgin Islands Creole English +controlledvocabulary.language.vishavan=Vishavan +controlledvocabulary.language.viti=Viti +controlledvocabulary.language.vitou=Vitou +controlledvocabulary.language.vitu=Vitu +controlledvocabulary.language.vlaams=Vlaams +controlledvocabulary.language.vlaamse_gebarentaal=Vlaamse Gebarentaal +controlledvocabulary.language.vlax_romani=Vlax Romani controlledvocabulary.language.volapuk=Volapük +controlledvocabulary.language.volscian=Volscian +controlledvocabulary.language.vono=Vono +controlledvocabulary.language.voro=Voro +controlledvocabulary.language.votic=Votic +controlledvocabulary.language.vumbu=Vumbu +controlledvocabulary.language.vunapu=Vunapu +controlledvocabulary.language.vunjo=Vunjo +controlledvocabulary.language.vures=Vurës +controlledvocabulary.language.vute=Vute +controlledvocabulary.language.vwanji=Vwanji +controlledvocabulary.language.wa=Wa +controlledvocabulary.language.wa'ema=Wa'ema +controlledvocabulary.language.waama=Waama +controlledvocabulary.language.waamwang=Waamwang +controlledvocabulary.language.waata=Waata +controlledvocabulary.language.wab=Wab +controlledvocabulary.language.wabo=Wabo +controlledvocabulary.language.waboda=Waboda +controlledvocabulary.language.waci_gbe=Waci Gbe +controlledvocabulary.language.wadaginam=Wadaginam +controlledvocabulary.language.waddar=Waddar +controlledvocabulary.language.wadi_wadi=Wadi Wadi +controlledvocabulary.language.wadikali=Wadikali +controlledvocabulary.language.wadiyara_koli=Wadiyara Koli +controlledvocabulary.language.wadjabangayi=Wadjabangayi +controlledvocabulary.language.wadjiginy=Wadjiginy +controlledvocabulary.language.wadjigu=Wadjigu +controlledvocabulary.language.wae_rana=Wae Rana +controlledvocabulary.language.waffa=Waffa +controlledvocabulary.language.wagawaga=Wagawaga +controlledvocabulary.language.wagaya=Wagaya +controlledvocabulary.language.wagdi=Wagdi +controlledvocabulary.language.wagi=Wagi +controlledvocabulary.language.wagiman=Wagiman +controlledvocabulary.language.wahau_kayan=Wahau Kayan +controlledvocabulary.language.wahau_kenyah=Wahau Kenyah +controlledvocabulary.language.wahgi=Wahgi +controlledvocabulary.language.waigali=Waigali +controlledvocabulary.language.waigeo=Waigeo +controlledvocabulary.language.wailaki=Wailaki +controlledvocabulary.language.wailapa=Wailapa +controlledvocabulary.language.waima=Waima +controlledvocabulary.language.waima'a=Waima'a +controlledvocabulary.language.waimaha=Waimaha +controlledvocabulary.language.waimiri-atroari=Waimiri-Atroari +controlledvocabulary.language.waioli=Waioli +controlledvocabulary.language.waiwai=Waiwai +controlledvocabulary.language.waja=Waja +controlledvocabulary.language.wajarri=Wajarri +controlledvocabulary.language.wajuk=Wajuk +controlledvocabulary.language.waka=Waka +controlledvocabulary.language.wakabunga=Wakabunga +controlledvocabulary.language.wakawaka=Wakawaka +controlledvocabulary.language.wakde=Wakde +controlledvocabulary.language.wakhi=Wakhi +controlledvocabulary.language.wakona=Wakoná +controlledvocabulary.language.wala=Wala +controlledvocabulary.language.walak=Walak +controlledvocabulary.language.walangama=Walangama +controlledvocabulary.language.wali_(ghana)=Wali (Ghana) +controlledvocabulary.language.wali_(sudan)=Wali (Sudan) +controlledvocabulary.language.waling=Waling +controlledvocabulary.language.walio=Walio +controlledvocabulary.language.walla_walla=Walla Walla +controlledvocabulary.language.wallisian=Wallisian controlledvocabulary.language.walloon=Walloon +controlledvocabulary.language.walmajarri=Walmajarri +controlledvocabulary.language.walser=Walser +controlledvocabulary.language.walungge=Walungge +controlledvocabulary.language.waluwarra=Waluwarra +controlledvocabulary.language.wamas=Wamas +controlledvocabulary.language.wambaya=Wambaya +controlledvocabulary.language.wambon=Wambon +controlledvocabulary.language.wambule=Wambule +controlledvocabulary.language.wamesa=Wamesa +controlledvocabulary.language.wamey=Wamey +controlledvocabulary.language.wamin=Wamin +controlledvocabulary.language.wampanoag=Wampanoag +controlledvocabulary.language.wampar=Wampar +controlledvocabulary.language.wampur=Wampur +controlledvocabulary.language.wan=Wan +controlledvocabulary.language.wanap=Wanap +controlledvocabulary.language.wancho_naga=Wancho Naga +controlledvocabulary.language.wanda=Wanda +controlledvocabulary.language.wandala=Wandala +controlledvocabulary.language.wandarang=Wandarang +controlledvocabulary.language.wandji=Wandji +controlledvocabulary.language.waneci=Waneci +controlledvocabulary.language.wanga=Wanga +controlledvocabulary.language.wangaaybuwan-ngiyambaa=Wangaaybuwan-Ngiyambaa +controlledvocabulary.language.wanggamala=Wanggamala +controlledvocabulary.language.wanggom=Wanggom +controlledvocabulary.language.wangkangurru=Wangkangurru +controlledvocabulary.language.wangkayutyuru=Wangkayutyuru +controlledvocabulary.language.wangkumara=Wangkumara +controlledvocabulary.language.wannu=Wannu +controlledvocabulary.language.wano=Wano +controlledvocabulary.language.wantoat=Wantoat +controlledvocabulary.language.wanukaka=Wanukaka +controlledvocabulary.language.wanyi=Wanyi +controlledvocabulary.language.wane=Wané +controlledvocabulary.language.waorani=Waorani +controlledvocabulary.language.wapan=Wapan +controlledvocabulary.language.wapishana=Wapishana +controlledvocabulary.language.wappo=Wappo +controlledvocabulary.language.war-jaintia=War-Jaintia +controlledvocabulary.language.wara=Wara Wára +controlledvocabulary.language.warao=Warao +controlledvocabulary.language.waray_(australia)=Waray (Australia) +controlledvocabulary.language.waray_(philippines)=Waray (Philippines) +controlledvocabulary.language.wardaman=Wardaman +controlledvocabulary.language.wardandi=Wardandi +controlledvocabulary.language.warembori=Warembori +controlledvocabulary.language.wares=Wares +controlledvocabulary.language.waris=Waris +controlledvocabulary.language.waritai=Waritai +controlledvocabulary.language.wariyangga=Wariyangga +controlledvocabulary.language.warji=Warji +controlledvocabulary.language.warkay-bipim=Warkay-Bipim +controlledvocabulary.language.warlmanpa=Warlmanpa +controlledvocabulary.language.warlpiri=Warlpiri +controlledvocabulary.language.warnang=Warnang +controlledvocabulary.language.warnman=Warnman +controlledvocabulary.language.waropen=Waropen +controlledvocabulary.language.warrgamay=Warrgamay +controlledvocabulary.language.warrwa=Warrwa +controlledvocabulary.language.waru=Waru +controlledvocabulary.language.warumungu=Warumungu +controlledvocabulary.language.waruna=Waruna +controlledvocabulary.language.warungu=Warungu +controlledvocabulary.language.warwar_feni=Warwar Feni +controlledvocabulary.language.wasa=Wasa +controlledvocabulary.language.wasco-wishram=Wasco-Wishram +controlledvocabulary.language.wasembo=Wasembo +controlledvocabulary.language.washo=Washo +controlledvocabulary.language.waskia=Waskia +controlledvocabulary.language.wasu=Wasu +controlledvocabulary.language.watakataui=Watakataui +controlledvocabulary.language.watam=Watam +controlledvocabulary.language.wathawurrung=Wathawurrung +controlledvocabulary.language.watiwa=Watiwa +controlledvocabulary.language.watubela=Watubela +controlledvocabulary.language.waube=Waube +controlledvocabulary.language.waura=Waurá +controlledvocabulary.language.wauyai=Wauyai +controlledvocabulary.language.wawa=Wawa +controlledvocabulary.language.wawonii=Wawonii +controlledvocabulary.language.waxianghua=Waxianghua +controlledvocabulary.language.wayampi=Wayampi +controlledvocabulary.language.wayana=Wayana +controlledvocabulary.language.wayanad_chetti=Wayanad Chetti +controlledvocabulary.language.wayoro=Wayoró +controlledvocabulary.language.wayu=Wayu +controlledvocabulary.language.wayuu=Wayuu +controlledvocabulary.language.wedau=Wedau +controlledvocabulary.language.weh=Weh +controlledvocabulary.language.wejewa=Wejewa +controlledvocabulary.language.welaun=Welaun +controlledvocabulary.language.weliki=Weliki controlledvocabulary.language.welsh=Welsh -controlledvocabulary.language.wolof=Wolof +controlledvocabulary.language.welsh_romani=Welsh Romani +controlledvocabulary.language.wemale=Wemale +controlledvocabulary.language.wemba_wemba=Wemba Wemba +controlledvocabulary.language.weme_gbe=Weme Gbe +controlledvocabulary.language.wendat=Wendat +controlledvocabulary.language.wergaia=Wergaia +controlledvocabulary.language.weri=Weri +controlledvocabulary.language.wersing=Wersing +controlledvocabulary.language.west_albay_bikol=West Albay Bikol +controlledvocabulary.language.west_ambae=West Ambae +controlledvocabulary.language.west_bengal_sign_language=West Bengal Sign Language +controlledvocabulary.language.west_berawan=West Berawan +controlledvocabulary.language.west_central_banda=West Central Banda +controlledvocabulary.language.west_central_oromo=West Central Oromo +controlledvocabulary.language.west_coast_bajau=West Coast Bajau +controlledvocabulary.language.west_damar=West Damar +controlledvocabulary.language.west_goodenough=West Goodenough +controlledvocabulary.language.west_kewa=West Kewa +controlledvocabulary.language.west_lembata=West Lembata +controlledvocabulary.language.west_makian=West Makian +controlledvocabulary.language.west_masela=West Masela +controlledvocabulary.language.west_tarangan=West Tarangan +controlledvocabulary.language.west_uvean=West Uvean +controlledvocabulary.language.west_yugur=West Yugur +controlledvocabulary.language.west-central_limba=West-Central Limba +controlledvocabulary.language.western_abnaki=Western Abnaki +controlledvocabulary.language.western_apache=Western Apache +controlledvocabulary.language.western_armenian=Western Armenian +controlledvocabulary.language.western_arrarnta=Western Arrarnta +controlledvocabulary.language.western_balochi=Western Balochi +controlledvocabulary.language.western_bolivian_guarani=Western Bolivian Guaraní +controlledvocabulary.language.western_bru=Western Bru +controlledvocabulary.language.western_bukidnon_manobo=Western Bukidnon Manobo +controlledvocabulary.language.western_cham=Western Cham +controlledvocabulary.language.western_dani=Western Dani +controlledvocabulary.language.western_durango_nahuatl=Western Durango Nahuatl +controlledvocabulary.language.western_fijian=Western Fijian controlledvocabulary.language.western_frisian=Western Frisian +controlledvocabulary.language.western_highland_chatino=Western Highland Chatino +controlledvocabulary.language.western_highland_purepecha=Western Highland Purepecha +controlledvocabulary.language.western_huasteca_nahuatl=Western Huasteca Nahuatl +controlledvocabulary.language.western_juxtlahuaca_mixtec=Western Juxtlahuaca Mixtec +controlledvocabulary.language.western_kanjobal=Western Kanjobal +controlledvocabulary.language.western_karaboro=Western Karaboro +controlledvocabulary.language.western_katu=Western Katu +controlledvocabulary.language.western_kayah=Western Kayah +controlledvocabulary.language.western_keres=Western Keres +controlledvocabulary.language.western_krahn=Western Krahn +controlledvocabulary.language.western_lalu=Western Lalu +controlledvocabulary.language.western_lawa=Western Lawa +controlledvocabulary.language.western_magar=Western Magar +controlledvocabulary.language.western_maninkakan=Western Maninkakan +controlledvocabulary.language.western_mari=Western Mari +controlledvocabulary.language.western_mashan_hmong=Western Mashan Hmong +controlledvocabulary.language.western_meohang=Western Meohang +controlledvocabulary.language.western_minyag=Western Minyag +controlledvocabulary.language.western_muria=Western Muria +controlledvocabulary.language.western_neo-aramaic=Western Neo-Aramaic +controlledvocabulary.language.western_niger_fulfulde=Western Niger Fulfulde +controlledvocabulary.language.western_ojibwa=Western Ojibwa +controlledvocabulary.language.western_panjabi=Western Panjabi +controlledvocabulary.language.western_parbate_kham=Western Parbate Kham +controlledvocabulary.language.western_penan=Western Penan +controlledvocabulary.language.western_sisaala=Western Sisaala +controlledvocabulary.language.western_subanon=Western Subanon +controlledvocabulary.language.western_tamang=Western Tamang +controlledvocabulary.language.western_tawbuid=Western Tawbuid +controlledvocabulary.language.western_tlacolula_valley_zapotec=Western Tlacolula Valley Zapotec +controlledvocabulary.language.western_totonac=Western Totonac +controlledvocabulary.language.western_tunebo=Western Tunebo +controlledvocabulary.language.western_xiangxi_miao=Western Xiangxi Miao +controlledvocabulary.language.western_xwla_gbe=Western Xwla Gbe +controlledvocabulary.language.western_yiddish=Western Yiddish +controlledvocabulary.language.westphalien=Westphalien +controlledvocabulary.language.wetamut=Wetamut +controlledvocabulary.language.wewaw=Wewaw +controlledvocabulary.language.weyto=Weyto +controlledvocabulary.language.white_gelao=White Gelao +controlledvocabulary.language.white_lachi=White Lachi +controlledvocabulary.language.whitesands=Whitesands +controlledvocabulary.language.wiarumus=Wiarumus +controlledvocabulary.language.wichita=Wichita +controlledvocabulary.language.wichi_lhamtes_guisnay=Wichí Lhamtés Güisnay +controlledvocabulary.language.wichi_lhamtes_nocten=Wichí Lhamtés Nocten +controlledvocabulary.language.wichi_lhamtes_vejoz=Wichí Lhamtés Vejoz +controlledvocabulary.language.wik_ngathan=Wik Ngathan +controlledvocabulary.language.wik-epa=Wik-Epa +controlledvocabulary.language.wik-iiyanh=Wik-Iiyanh +controlledvocabulary.language.wik-keyangan=Wik-Keyangan +controlledvocabulary.language.wik-me'anha=Wik-Me'anha +controlledvocabulary.language.wik-mungkan=Wik-Mungkan +controlledvocabulary.language.wikalkan=Wikalkan +controlledvocabulary.language.wikngenchera=Wikngenchera +controlledvocabulary.language.wilawila=Wilawila +controlledvocabulary.language.wintu=Wintu +controlledvocabulary.language.winye=Winyé +controlledvocabulary.language.wipi=Wipi +controlledvocabulary.language.wiradjuri=Wiradjuri +controlledvocabulary.language.wirafed=Wiraféd +controlledvocabulary.language.wirangu=Wirangu +controlledvocabulary.language.wiru=Wiru +controlledvocabulary.language.wiyot=Wiyot +controlledvocabulary.language.woccon=Woccon +controlledvocabulary.language.wogamusin=Wogamusin +controlledvocabulary.language.wogeo=Wogeo +controlledvocabulary.language.woi=Woi +controlledvocabulary.language.woiwurrung=Woiwurrung +controlledvocabulary.language.wojenaka=Wojenaka +controlledvocabulary.language.wolane=Wolane +controlledvocabulary.language.wolani=Wolani +controlledvocabulary.language.wolaytta=Wolaytta +controlledvocabulary.language.woleaian=Woleaian +controlledvocabulary.language.wolio=Wolio +controlledvocabulary.language.wolof=Wolof +controlledvocabulary.language.wom_(nigeria)=Wom (Nigeria) +controlledvocabulary.language.wom_(papua_new_guinea)=Wom (Papua New Guinea) +controlledvocabulary.language.womo=Womo +controlledvocabulary.language.wongo=Wongo +controlledvocabulary.language.woods_cree=Woods Cree +controlledvocabulary.language.woria=Woria +controlledvocabulary.language.worimi=Worimi +controlledvocabulary.language.worodougou=Worodougou +controlledvocabulary.language.worrorra=Worrorra +controlledvocabulary.language.wotapuri-katarqalai=Wotapuri-Katarqalai +controlledvocabulary.language.wotjobaluk=Wotjobaluk +controlledvocabulary.language.wotu=Wotu +controlledvocabulary.language.woun_meu=Woun Meu +controlledvocabulary.language.written_oirat=Written Oirat +controlledvocabulary.language.wu_chinese=Wu Chinese +controlledvocabulary.language.wuding-luquan_yi=Wuding-Luquan Yi +controlledvocabulary.language.wudu=Wudu +controlledvocabulary.language.wuliwuli=Wuliwuli +controlledvocabulary.language.wulna=Wulna +controlledvocabulary.language.wumboko=Wumboko +controlledvocabulary.language.wumbvu=Wumbvu +controlledvocabulary.language.wumeng_nasu=Wumeng Nasu +controlledvocabulary.language.wunai_bunu=Wunai Bunu +controlledvocabulary.language.wunambal=Wunambal +controlledvocabulary.language.wunumara=Wunumara +controlledvocabulary.language.wurrugu=Wurrugu +controlledvocabulary.language.wusa_nasu=Wusa Nasu +controlledvocabulary.language.wushi=Wushi +controlledvocabulary.language.wusi=Wusi +controlledvocabulary.language.wutung=Wutung +controlledvocabulary.language.wutunhua=Wutunhua +controlledvocabulary.language.wuvulu-aua=Wuvulu-Aua +controlledvocabulary.language.wuzlam=Wuzlam +controlledvocabulary.language.wyandot=Wyandot +controlledvocabulary.language.wymysorys=Wymysorys +controlledvocabulary.language.wapha=Wãpha +controlledvocabulary.language.we_northern=Wè Northern +controlledvocabulary.language.we_southern=Wè Southern +controlledvocabulary.language.we_western=Wè Western +controlledvocabulary.language.xaasongaxango=Xaasongaxango +controlledvocabulary.language.xadani_zapotec=Xadani Zapotec +controlledvocabulary.language.xakriaba=Xakriabá +controlledvocabulary.language.xamtanga=Xamtanga +controlledvocabulary.language.xanaguia_zapotec=Xanaguía Zapotec +controlledvocabulary.language.xavante=Xavánte +controlledvocabulary.language.xerente=Xerénte +controlledvocabulary.language.xeta=Xetá controlledvocabulary.language.xhosa=Xhosa +controlledvocabulary.language.xiang_chinese=Xiang Chinese +controlledvocabulary.language.xibe=Xibe +controlledvocabulary.language.xicotepec_de_juarez_totonac=Xicotepec De Juárez Totonac +controlledvocabulary.language.xinca=Xinca +controlledvocabulary.language.xingu_asurini=Xingú Asuriní +controlledvocabulary.language.xipaya=Xipaya +controlledvocabulary.language.xiri=Xiri +controlledvocabulary.language.xiriana=Xiriâna +controlledvocabulary.language.xishanba_lalo=Xishanba Lalo +controlledvocabulary.language.xokleng=Xokleng +controlledvocabulary.language.xukuru=Xukurú +controlledvocabulary.language.xwela_gbe=Xwela Gbe +controlledvocabulary.language.xaracuu=Xârâcùù +controlledvocabulary.language.xaragure=Xârâgurè +controlledvocabulary.language.yaaku=Yaaku +controlledvocabulary.language.yabarana=Yabarana +controlledvocabulary.language.yabaana=Yabaâna +controlledvocabulary.language.yabem=Yabem +controlledvocabulary.language.yaben=Yaben +controlledvocabulary.language.yabong=Yabong +controlledvocabulary.language.yabula_yabula=Yabula Yabula +controlledvocabulary.language.yace=Yace +controlledvocabulary.language.yaeyama=Yaeyama +controlledvocabulary.language.yafi=Yafi +controlledvocabulary.language.yagara=Yagara +controlledvocabulary.language.yagaria=Yagaria +controlledvocabulary.language.yagnobi=Yagnobi +controlledvocabulary.language.yagomi=Yagomi +controlledvocabulary.language.yagua=Yagua +controlledvocabulary.language.yagwoia=Yagwoia +controlledvocabulary.language.yahadian=Yahadian +controlledvocabulary.language.yahang=Yahang +controlledvocabulary.language.yahuna=Yahuna +controlledvocabulary.language.yaka_(central_african_republic)=Yaka (Central African Republic) +controlledvocabulary.language.yaka_(congo)=Yaka (Congo) +controlledvocabulary.language.yaka_(democratic_republic_of_congo)=Yaka (Democratic Republic of Congo) +controlledvocabulary.language.yakaikeke=Yakaikeke +controlledvocabulary.language.yakama=Yakama +controlledvocabulary.language.yakan=Yakan +controlledvocabulary.language.yakha=Yakha +controlledvocabulary.language.yakoma=Yakoma +controlledvocabulary.language.yakut=Yakut +controlledvocabulary.language.yala=Yala +controlledvocabulary.language.yalahatan=Yalahatan +controlledvocabulary.language.yalakalore=Yalakalore +controlledvocabulary.language.yalarnnga=Yalarnnga +controlledvocabulary.language.yale=Yale +controlledvocabulary.language.yaleba=Yaleba +controlledvocabulary.language.yalunka=Yalunka +controlledvocabulary.language.yalalag_zapotec=Yalálag Zapotec +controlledvocabulary.language.yamap=Yamap +controlledvocabulary.language.yamba=Yamba +controlledvocabulary.language.yambes=Yambes +controlledvocabulary.language.yambeta=Yambeta +controlledvocabulary.language.yamdena=Yamdena +controlledvocabulary.language.yameo=Yameo +controlledvocabulary.language.yami=Yami +controlledvocabulary.language.yaminahua=Yaminahua +controlledvocabulary.language.yamna=Yamna +controlledvocabulary.language.yamongeri=Yamongeri +controlledvocabulary.language.yamphu=Yamphu +controlledvocabulary.language.yan-nhangu=Yan-nhangu +controlledvocabulary.language.yan-nhaŋu_sign_language=Yan-nhaŋu Sign Language +controlledvocabulary.language.yana=Yana +controlledvocabulary.language.yanahuanca_pasco_quechua=Yanahuanca Pasco Quechua +controlledvocabulary.language.yanda=Yanda +controlledvocabulary.language.yanda_dom_dogon=Yanda Dom Dogon +controlledvocabulary.language.yandjibara=Yandjibara +controlledvocabulary.language.yandruwandha=Yandruwandha +controlledvocabulary.language.yanesha'=Yanesha' +controlledvocabulary.language.yang_zhuang=Yang Zhuang +controlledvocabulary.language.yangben=Yangben +controlledvocabulary.language.yangkam=Yangkam +controlledvocabulary.language.yangman=Yangman +controlledvocabulary.language.yango=Yango +controlledvocabulary.language.yangulam=Yangulam +controlledvocabulary.language.yangum_dey=Yangum Dey +controlledvocabulary.language.yangum_gel=Yangum Gel +controlledvocabulary.language.yangum_mon=Yangum Mon +controlledvocabulary.language.yankunytjatjara=Yankunytjatjara +controlledvocabulary.language.yanomamo=Yanomamö +controlledvocabulary.language.yanomami=Yanomámi +controlledvocabulary.language.yansi=Yansi +controlledvocabulary.language.yanyuwa=Yanyuwa +controlledvocabulary.language.yao=Yao +controlledvocabulary.language.yaosakor_asmat=Yaosakor Asmat +controlledvocabulary.language.yaoure=Yaouré +controlledvocabulary.language.yapese=Yapese +controlledvocabulary.language.yapunda=Yapunda +controlledvocabulary.language.yaqay=Yaqay +controlledvocabulary.language.yaqui=Yaqui +controlledvocabulary.language.yarawata=Yarawata +controlledvocabulary.language.yardliyawarra=Yardliyawarra +controlledvocabulary.language.yareba=Yareba +controlledvocabulary.language.yareni_zapotec=Yareni Zapotec +controlledvocabulary.language.yarluyandi=Yarluyandi +controlledvocabulary.language.yaroame=Yaroamë +controlledvocabulary.language.yarsun=Yarsun +controlledvocabulary.language.yasa=Yasa +controlledvocabulary.language.yassic=Yassic +controlledvocabulary.language.yatay=Yatay +controlledvocabulary.language.yatee_zapotec=Yatee Zapotec +controlledvocabulary.language.yatzachi_zapotec=Yatzachi Zapotec +controlledvocabulary.language.yau_(morobe_province)=Yau (Morobe Province) +controlledvocabulary.language.yau_(sandaun_province)=Yau (Sandaun Province) +controlledvocabulary.language.yaul=Yaul +controlledvocabulary.language.yauma=Yauma +controlledvocabulary.language.yaur=Yaur +controlledvocabulary.language.yautepec_zapotec=Yautepec Zapotec +controlledvocabulary.language.yauyos_quechua=Yauyos Quechua +controlledvocabulary.language.yavitero=Yavitero +controlledvocabulary.language.yawa=Yawa +controlledvocabulary.language.yawalapiti=Yawalapití +controlledvocabulary.language.yawanawa=Yawanawa +controlledvocabulary.language.yawarawarga=Yawarawarga +controlledvocabulary.language.yaweyuha=Yaweyuha +controlledvocabulary.language.yawijibaya=Yawijibaya +controlledvocabulary.language.yawiyo=Yawiyo +controlledvocabulary.language.yawuru=Yawuru +controlledvocabulary.language.yaygir=Yaygir +controlledvocabulary.language.yazgulyam=Yazgulyam +controlledvocabulary.language.yecuatla_totonac=Yecuatla Totonac +controlledvocabulary.language.yei=Yei +controlledvocabulary.language.yekhee=Yekhee +controlledvocabulary.language.yekora=Yekora +controlledvocabulary.language.yela=Yela +controlledvocabulary.language.yele=Yele +controlledvocabulary.language.yelmek=Yelmek +controlledvocabulary.language.yelogu=Yelogu +controlledvocabulary.language.yemba=Yemba +controlledvocabulary.language.yemsa=Yemsa +controlledvocabulary.language.yendang=Yendang +controlledvocabulary.language.yeni=Yeni +controlledvocabulary.language.yeniche=Yeniche +controlledvocabulary.language.yerakai=Yerakai +controlledvocabulary.language.yeretuar=Yeretuar +controlledvocabulary.language.yerong=Yerong +controlledvocabulary.language.yerukula=Yerukula +controlledvocabulary.language.yessan-mayo=Yessan-Mayo +controlledvocabulary.language.yetfa=Yetfa +controlledvocabulary.language.yevanic=Yevanic +controlledvocabulary.language.yeyi=Yeyi controlledvocabulary.language.yiddish=Yiddish +controlledvocabulary.language.yidgha=Yidgha +controlledvocabulary.language.yidiny=Yidiny +controlledvocabulary.language.yil=Yil +controlledvocabulary.language.yilan_creole=Yilan Creole +controlledvocabulary.language.yimas=Yimas +controlledvocabulary.language.yimchungru_naga=Yimchungru Naga +controlledvocabulary.language.yinbaw_karen=Yinbaw Karen +controlledvocabulary.language.yindjibarndi=Yindjibarndi +controlledvocabulary.language.yindjilandji=Yindjilandji +controlledvocabulary.language.yine=Yine +controlledvocabulary.language.yinggarda=Yinggarda +controlledvocabulary.language.yinhawangka=Yinhawangka +controlledvocabulary.language.yiningayi=Yiningayi +controlledvocabulary.language.yintale_karen=Yintale Karen +controlledvocabulary.language.yinwum=Yinwum +controlledvocabulary.language.yir_yoront=Yir Yoront +controlledvocabulary.language.yirandali=Yirandali +controlledvocabulary.language.yirrk-mel=Yirrk-Mel +controlledvocabulary.language.yis=Yis +controlledvocabulary.language.yitha_yitha=Yitha Yitha +controlledvocabulary.language.yoba=Yoba +controlledvocabulary.language.yocoboue_dida=Yocoboué Dida +controlledvocabulary.language.yogad=Yogad +controlledvocabulary.language.yoidik=Yoidik +controlledvocabulary.language.yoke=Yoke +controlledvocabulary.language.yokuts=Yokuts +controlledvocabulary.language.yola=Yola +controlledvocabulary.language.yoloxochitl_mixtec=Yoloxochitl Mixtec +controlledvocabulary.language.yolŋu_sign_language=Yolŋu Sign Language +controlledvocabulary.language.yom=Yom +controlledvocabulary.language.yombe=Yombe +controlledvocabulary.language.yonaguni=Yonaguni +controlledvocabulary.language.yong=Yong +controlledvocabulary.language.yongbei_zhuang=Yongbei Zhuang +controlledvocabulary.language.yongkom=Yongkom +controlledvocabulary.language.yongnan_zhuang=Yongnan Zhuang +controlledvocabulary.language.yopno=Yopno +controlledvocabulary.language.yora=Yora +controlledvocabulary.language.yoron=Yoron +controlledvocabulary.language.yorta_yorta=Yorta Yorta controlledvocabulary.language.yoruba=Yoruba +controlledvocabulary.language.yosondua_mixtec=Yosondúa Mixtec +controlledvocabulary.language.yotti=Yotti +controlledvocabulary.language.youjiang_zhuang=Youjiang Zhuang +controlledvocabulary.language.youle_jinuo=Youle Jinuo +controlledvocabulary.language.younuo_bunu=Younuo Bunu +controlledvocabulary.language.yout_wam=Yout Wam +controlledvocabulary.language.yoy=Yoy +controlledvocabulary.language.yuanga=Yuanga +controlledvocabulary.language.yucatec_maya_sign_language=Yucatec Maya Sign Language +controlledvocabulary.language.yucateco=Yucateco +controlledvocabulary.language.yuchi=Yuchi +controlledvocabulary.language.yucuane_mixtec=Yucuañe Mixtec +controlledvocabulary.language.yucuna=Yucuna +controlledvocabulary.language.yue_chinese=Yue Chinese +controlledvocabulary.language.yug=Yug +controlledvocabulary.language.yugambal=Yugambal +controlledvocabulary.language.yugoslavian_sign_language=Yugoslavian Sign Language +controlledvocabulary.language.yugul=Yugul +controlledvocabulary.language.yuhup=Yuhup +controlledvocabulary.language.yuki=Yuki +controlledvocabulary.language.yukpa=Yukpa +controlledvocabulary.language.yukuben=Yukuben +controlledvocabulary.language.yulu=Yulu +controlledvocabulary.language.yuqui=Yuqui +controlledvocabulary.language.yuracare=Yuracare +controlledvocabulary.language.yurats=Yurats +controlledvocabulary.language.yurok=Yurok +controlledvocabulary.language.yuru=Yuru +controlledvocabulary.language.yuruti=Yurutí +controlledvocabulary.language.yutanduchi_mixtec=Yutanduchi Mixtec +controlledvocabulary.language.yuwana=Yuwana +controlledvocabulary.language.yuyu=Yuyu +controlledvocabulary.language.ywom=Ywom +controlledvocabulary.language.yamana=Yámana +controlledvocabulary.language.zaachila_zapotec=Zaachila Zapotec +controlledvocabulary.language.zabana=Zabana +controlledvocabulary.language.zacatepec_chatino=Zacatepec Chatino +controlledvocabulary.language.zacatlan-ahuacatlan-tepetzintla_nahuatl=Zacatlán-Ahuacatlán-Tepetzintla Nahuatl +controlledvocabulary.language.zaghawa=Zaghawa +controlledvocabulary.language.zaiwa=Zaiwa +controlledvocabulary.language.zakhring=Zakhring +controlledvocabulary.language.zambian_sign_language=Zambian Sign Language +controlledvocabulary.language.zan_gula=Zan Gula +controlledvocabulary.language.zanaki=Zanaki +controlledvocabulary.language.zande_(individual_language)=Zande (individual language) +controlledvocabulary.language.zangskari=Zangskari +controlledvocabulary.language.zangwal=Zangwal +controlledvocabulary.language.zaniza_zapotec=Zaniza Zapotec +controlledvocabulary.language.zapotec=Zapotec +controlledvocabulary.language.zaramo=Zaramo +controlledvocabulary.language.zari=Zari +controlledvocabulary.language.zarma=Zarma +controlledvocabulary.language.zarphatic=Zarphatic +controlledvocabulary.language.zauzou=Zauzou +controlledvocabulary.language.zay=Zay +controlledvocabulary.language.zayein_karen=Zayein Karen +controlledvocabulary.language.zayse-zergulla=Zayse-Zergulla +controlledvocabulary.language.zaza=Zaza +controlledvocabulary.language.zazao=Zazao +controlledvocabulary.language.zeem=Zeem +controlledvocabulary.language.zeeuws=Zeeuws +controlledvocabulary.language.zemba=Zemba +controlledvocabulary.language.zeme_naga=Zeme Naga +controlledvocabulary.language.zemgalian=Zemgalian +controlledvocabulary.language.zenag=Zenag +controlledvocabulary.language.zenaga=Zenaga +controlledvocabulary.language.zenzontepec_chatino=Zenzontepec Chatino +controlledvocabulary.language.zerenkel=Zerenkel +controlledvocabulary.language.zhaba=Zhaba +controlledvocabulary.language.zhang-zhung=Zhang-Zhung +controlledvocabulary.language.zhire=Zhire +controlledvocabulary.language.zhoa=Zhoa controlledvocabulary.language.zhuang,_chuang=Zhuang, Chuang +controlledvocabulary.language.zia=Zia +controlledvocabulary.language.zialo=Zialo +controlledvocabulary.language.zigula=Zigula +controlledvocabulary.language.zimakani=Zimakani +controlledvocabulary.language.zimba=Zimba +controlledvocabulary.language.zimbabwe_sign_language=Zimbabwe Sign Language +controlledvocabulary.language.zinza=Zinza +controlledvocabulary.language.zire=Zire +controlledvocabulary.language.zizilivakan=Zizilivakan +controlledvocabulary.language.zo'e=Zo'é +controlledvocabulary.language.zokhuo=Zokhuo +controlledvocabulary.language.zoogocho_zapotec=Zoogocho Zapotec +controlledvocabulary.language.zoroastrian_dari=Zoroastrian Dari +controlledvocabulary.language.zotung_chin=Zotung Chin +controlledvocabulary.language.zou=Zou +controlledvocabulary.language.zul=Zul +controlledvocabulary.language.zula=Zula +controlledvocabulary.language.zulgo-gemzek=Zulgo-Gemzek controlledvocabulary.language.zulu=Zulu -controlledvocabulary.language.not_applicable=Not applicable +controlledvocabulary.language.zumaya=Zumaya +controlledvocabulary.language.zumbun=Zumbun +controlledvocabulary.language.zuni=Zuni +controlledvocabulary.language.zuojiang_zhuang=Zuojiang Zhuang +controlledvocabulary.language.zyphe_chin=Zyphe Chin +controlledvocabulary.language.zaparo=Záparo +controlledvocabulary.language.stodsde=sTodsde +controlledvocabulary.language.us-saare=us-Saare +controlledvocabulary.language.ut-hun=ut-Hun +controlledvocabulary.language.ut-ma'in=ut-Ma'in +controlledvocabulary.language.ahan=Àhàn +controlledvocabulary.language.anca=Áncá +controlledvocabulary.language.omie=Ömie +controlledvocabulary.language.onge=Önge +controlledvocabulary.language.ǀgwi=ǀGwi +controlledvocabulary.language.ǀxam=ǀXam +controlledvocabulary.language.ǁani=ǁAni +controlledvocabulary.language.ǁgana=ǁGana +controlledvocabulary.language.ǁxegwi=ǁXegwi +controlledvocabulary.language.ǂhua=ǂHua +controlledvocabulary.language.ǂungkue=ǂUngkue +controlledvocabulary.language.ǃxoo=ǃXóõ +controlledvocabulary.language.not_applicable=Not Applicable diff --git a/src/main/java/propertyFiles/codeMeta20.properties b/src/main/java/propertyFiles/codeMeta20.properties index c0e7eac6d4a..4f3eb087aa4 100644 --- a/src/main/java/propertyFiles/codeMeta20.properties +++ b/src/main/java/propertyFiles/codeMeta20.properties @@ -1,5 +1,6 @@ metadatablock.name=codeMeta20 -metadatablock.displayName=Software Metadata (CodeMeta 2.0) +metadatablock.displayName=Software Metadata (CodeMeta v2.0) +metadatablock.displayFacet=Software datasetfieldtype.codeVersion.title=Software Version datasetfieldtype.codeVersion.description=Version of the software instance, usually following some convention like SemVer etc. datasetfieldtype.codeVersion.watermark=e.g. 0.2.1 or 1.3 or 2021.1 etc diff --git a/src/main/java/propertyFiles/customGSD.properties b/src/main/java/propertyFiles/customGSD.properties index 40dc0328053..2375596fe2f 100644 --- a/src/main/java/propertyFiles/customGSD.properties +++ b/src/main/java/propertyFiles/customGSD.properties @@ -161,7 +161,6 @@ controlledvocabulary.gsdFacultyName.mcloskey,_karen=MCloskey, Karen controlledvocabulary.gsdFacultyName.mehrotra,_rahul=Mehrotra, Rahul controlledvocabulary.gsdFacultyName.menchaca,_alejandra=Menchaca, Alejandra controlledvocabulary.gsdFacultyName.menges,_achim=Menges, Achim -controlledvocabulary.gsdFacultyName.menges,_achim=Menges, Achim controlledvocabulary.gsdFacultyName.michalatos,_panagiotis=Michalatos, Panagiotis controlledvocabulary.gsdFacultyName.moe,_kiel=Moe, Kiel controlledvocabulary.gsdFacultyName.molinsky,_jennifer=Molinsky, Jennifer @@ -507,7 +506,6 @@ controlledvocabulary.gsdCourseName.06323:_brownfields_practicum=06323: Brownfiel controlledvocabulary.gsdCourseName.06333:_aquatic_ecology=06333: Aquatic Ecology controlledvocabulary.gsdCourseName.06335:_phytotechnologies=06335: Phytotechnologies controlledvocabulary.gsdCourseName.06337:_changing_natural_and_built_coastal_environments=06337: Changing Natural and Built Coastal Environments -controlledvocabulary.gsdCourseName.06337:_changing_natural_and_built_coastal_environments=06337: Changing Natural and Built Coastal Environments controlledvocabulary.gsdCourseName.06338:_introduction_to_computational_design=06338: Introduction to Computational Design controlledvocabulary.gsdCourseName.06436:_expanded_mechanisms_/_empirical_materialisms=06436: Expanded Mechanisms / Empirical Materialisms controlledvocabulary.gsdCourseName.06450:_high_performance_buildings_and_systems_integration=06450: High Performance Buildings and Systems Integration diff --git a/src/main/java/propertyFiles/geospatial.properties b/src/main/java/propertyFiles/geospatial.properties index 86f297c29b9..2659c2a3cc9 100644 --- a/src/main/java/propertyFiles/geospatial.properties +++ b/src/main/java/propertyFiles/geospatial.properties @@ -10,8 +10,8 @@ datasetfieldtype.geographicUnit.title=Geographic Unit datasetfieldtype.geographicBoundingBox.title=Geographic Bounding Box datasetfieldtype.westLongitude.title=Westernmost (Left) Longitude datasetfieldtype.eastLongitude.title=Easternmost (Right) Longitude -datasetfieldtype.northLongitude.title=Northernmost (Top) Latitude -datasetfieldtype.southLongitude.title=Southernmost (Bottom) Latitude +datasetfieldtype.northLatitude.title=Northernmost (Top) Latitude +datasetfieldtype.southLatitude.title=Southernmost (Bottom) Latitude datasetfieldtype.geographicCoverage.description=Information on the geographic coverage of the data. Includes the total geographic scope of the data. datasetfieldtype.country.description=The country or nation that the Dataset is about. datasetfieldtype.state.description=The state or province that the Dataset is about. Use GeoNames for correct spelling and avoid abbreviations. @@ -19,10 +19,10 @@ datasetfieldtype.city.description=The name of the city that the Dataset is about datasetfieldtype.otherGeographicCoverage.description=Other information on the geographic coverage of the data. datasetfieldtype.geographicUnit.description=Lowest level of geographic aggregation covered by the Dataset, e.g., village, county, region. datasetfieldtype.geographicBoundingBox.description=The fundamental geometric description for any Dataset that models geography is the geographic bounding box. It describes the minimum box, defined by west and east longitudes and north and south latitudes, which includes the largest geographic extent of the Dataset's geographic coverage. This element is used in the first pass of a coordinate-based search. Inclusion of this element in the codebook is recommended, but is required if the bound polygon box is included. -datasetfieldtype.westLongitude.description=Westernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -180,0 <= West Bounding Longitude Value <= 180,0. -datasetfieldtype.eastLongitude.description=Easternmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -180,0 <= East Bounding Longitude Value <= 180,0. -datasetfieldtype.northLongitude.description=Northernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -90,0 <= North Bounding Latitude Value <= 90,0. -datasetfieldtype.southLongitude.description=Southernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -90,0 <= South Bounding Latitude Value <= 90,0. +datasetfieldtype.westLongitude.description=Westernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -180.0 <= West Bounding Longitude Value <= 180.0. +datasetfieldtype.eastLongitude.description=Easternmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -180.0 <= East Bounding Longitude Value <= 180.0. +datasetfieldtype.northLatitude.description=Northernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -90.0 <= North Bounding Latitude Value <= 90.0. +datasetfieldtype.southLatitude.description=Southernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -90.0 <= South Bounding Latitude Value <= 90.0. datasetfieldtype.geographicCoverage.watermark= datasetfieldtype.country.watermark= datasetfieldtype.state.watermark= @@ -32,8 +32,8 @@ datasetfieldtype.geographicUnit.watermark= datasetfieldtype.geographicBoundingBox.watermark= datasetfieldtype.westLongitude.watermark= datasetfieldtype.eastLongitude.watermark= -datasetfieldtype.northLongitude.watermark= -datasetfieldtype.southLongitude.watermark= +datasetfieldtype.northLatitude.watermark= +datasetfieldtype.southLatitude.watermark= controlledvocabulary.country.afghanistan=Afghanistan controlledvocabulary.country.albania=Albania controlledvocabulary.country.algeria=Algeria diff --git a/src/main/java/propertyFiles/staticSearchFields.properties b/src/main/java/propertyFiles/staticSearchFields.properties index ab03de64f23..9a208e841d6 100644 --- a/src/main/java/propertyFiles/staticSearchFields.properties +++ b/src/main/java/propertyFiles/staticSearchFields.properties @@ -3,9 +3,11 @@ staticSearchFields.metadata_type_ss=Dataset Feature staticSearchFields.dvCategory=Dataverse Category staticSearchFields.metadataSource=Metadata Source staticSearchFields.publicationDate=Publication Year +staticSearchFields.license=License staticSearchFields.fileTypeGroupFacet=File Type staticSearchFields.dvObjectType=Type staticSearchFields.fileTag=File Tag staticSearchFields.fileAccess=Access staticSearchFields.publicationStatus=Publication Status -staticSearchFields.subject_ss=Subject \ No newline at end of file +staticSearchFields.subject_ss=Subject +staticSearchFields.datasetType=Dataset Type diff --git a/src/main/resources/META-INF/javamail.default.address.map b/src/main/resources/META-INF/javamail.default.address.map new file mode 100644 index 00000000000..b1115c9dc8c --- /dev/null +++ b/src/main/resources/META-INF/javamail.default.address.map @@ -0,0 +1,2 @@ +# See https://jakartaee.github.io/mail-api/docs/api/jakarta.mail/jakarta/mail/Session.html +rfc822=smtp diff --git a/src/main/resources/META-INF/microprofile-config.properties b/src/main/resources/META-INF/microprofile-config.properties index ec8427795ee..b0bc92cf975 100644 --- a/src/main/resources/META-INF/microprofile-config.properties +++ b/src/main/resources/META-INF/microprofile-config.properties @@ -42,6 +42,11 @@ dataverse.rserve.user=rserve dataverse.rserve.password=rserve dataverse.rserve.tempdir=/tmp/Rserv +# MAIL +dataverse.mail.debug=false +dataverse.mail.mta.auth=false +dataverse.mail.mta.allow-utf8-addresses=true + # OAI SERVER dataverse.oai.server.maxidentifiers=100 dataverse.oai.server.maxrecords=10 @@ -50,17 +55,6 @@ dataverse.oai.server.maxsets=100 # can be customized via the setting below: #dataverse.oai.server.repositoryname= -# PERSISTENT IDENTIFIER PROVIDERS -# EZID -dataverse.pid.ezid.api-url=https://ezid.cdlib.org - -# DataCite -dataverse.pid.datacite.mds-api-url=https://mds.test.datacite.org -dataverse.pid.datacite.rest-api-url=https://api.test.datacite.org - -# Handle.Net -dataverse.pid.handlenet.index=300 - # AUTHENTICATION dataverse.auth.oidc.pkce.max-cache-size=10000 dataverse.auth.oidc.pkce.max-cache-age=300 diff --git a/src/main/resources/db/migration/V6.1.0.2__8524-store-tabular-files-with-varheaders.sql b/src/main/resources/db/migration/V6.1.0.2__8524-store-tabular-files-with-varheaders.sql new file mode 100644 index 00000000000..7c52a00107a --- /dev/null +++ b/src/main/resources/db/migration/V6.1.0.2__8524-store-tabular-files-with-varheaders.sql @@ -0,0 +1 @@ +ALTER TABLE datatable ADD COLUMN IF NOT EXISTS storedWithVariableHeader BOOLEAN DEFAULT FALSE; diff --git a/src/main/resources/db/migration/V6.1.0.3__9983-missing-unique-constraints.sql b/src/main/resources/db/migration/V6.1.0.3__9983-missing-unique-constraints.sql new file mode 100644 index 00000000000..6cb3a455e4e --- /dev/null +++ b/src/main/resources/db/migration/V6.1.0.3__9983-missing-unique-constraints.sql @@ -0,0 +1,16 @@ +DO $$ +BEGIN + + BEGIN + ALTER TABLE externalvocabularyvalue ADD CONSTRAINT externalvocabularvalue_uri_key UNIQUE(uri); + EXCEPTION + WHEN duplicate_table THEN RAISE NOTICE 'Table unique constraint externalvocabularvalue_uri_key already exists'; + END; + + BEGIN + ALTER TABLE oaiset ADD CONSTRAINT oaiset_spec_key UNIQUE(spec); + EXCEPTION + WHEN duplicate_table THEN RAISE NOTICE 'Table unique constraint oaiset_spec_key already exists'; + END; + +END $$; \ No newline at end of file diff --git a/src/main/resources/db/migration/V6.1.0.4__5645-geospatial-fieldname-fix.sql b/src/main/resources/db/migration/V6.1.0.4__5645-geospatial-fieldname-fix.sql new file mode 100644 index 00000000000..2ab8cbc802e --- /dev/null +++ b/src/main/resources/db/migration/V6.1.0.4__5645-geospatial-fieldname-fix.sql @@ -0,0 +1,7 @@ +UPDATE datasetfieldtype +SET name = 'northLatitude' +WHERE name = 'northLongitude'; + +UPDATE datasetfieldtype +SET name = 'southLatitude' +WHERE name = 'southLongitude'; \ No newline at end of file diff --git a/src/main/resources/db/migration/V6.1.0.5__3623-multiple-pid-providers.sql b/src/main/resources/db/migration/V6.1.0.5__3623-multiple-pid-providers.sql new file mode 100644 index 00000000000..1d11e178abf --- /dev/null +++ b/src/main/resources/db/migration/V6.1.0.5__3623-multiple-pid-providers.sql @@ -0,0 +1,2 @@ +ALTER TABLE dataverse ADD COLUMN IF NOT EXISTS pidgeneratorspecs TEXT; +ALTER TABLE dataset ADD COLUMN IF NOT EXISTS pidgeneratorspecs TEXT; diff --git a/src/main/resources/db/migration/V6.1.0.6.sql b/src/main/resources/db/migration/V6.1.0.6.sql new file mode 100644 index 00000000000..c9942fb8480 --- /dev/null +++ b/src/main/resources/db/migration/V6.1.0.6.sql @@ -0,0 +1,2 @@ +-- Add flag to allow harvesting client to handle missing CVV values +ALTER TABLE harvestingclient ADD COLUMN IF NOT EXISTS allowharvestingmissingcvv BOOLEAN; diff --git a/src/main/resources/db/migration/V6.1.0.7.sql b/src/main/resources/db/migration/V6.1.0.7.sql new file mode 100644 index 00000000000..470483e2bf4 --- /dev/null +++ b/src/main/resources/db/migration/V6.1.0.7.sql @@ -0,0 +1 @@ +ALTER TABLE authenticateduser ADD COLUMN IF NOT EXISTS ratelimittier int DEFAULT 1; diff --git a/src/main/resources/db/migration/V6.2.0.1.sql b/src/main/resources/db/migration/V6.2.0.1.sql new file mode 100644 index 00000000000..cb23d589542 --- /dev/null +++ b/src/main/resources/db/migration/V6.2.0.1.sql @@ -0,0 +1 @@ +ALTER TABLE datafile ADD COLUMN IF NOT EXISTS retention_id BIGINT; \ No newline at end of file diff --git a/src/main/resources/db/migration/V6.3.0.1.sql b/src/main/resources/db/migration/V6.3.0.1.sql new file mode 100644 index 00000000000..fd9cd823868 --- /dev/null +++ b/src/main/resources/db/migration/V6.3.0.1.sql @@ -0,0 +1,10 @@ +UPDATE termsofuseandaccess SET license_id = (SELECT license.id FROM license WHERE license.name = 'CC0 1.0'), termsofuse = NULL +WHERE termsofuse = 'This dataset is made available under a Creative Commons CC0 license with the following additional/modified terms and conditions: CC0 Waiver' + AND license_id IS null + AND confidentialitydeclaration IS null + AND specialpermissions IS null + AND restrictions IS null + AND citationrequirements IS null + AND depositorrequirements IS null + AND conditions IS null + AND disclaimer IS null; diff --git a/src/main/resources/db/migration/V6.3.0.2.sql b/src/main/resources/db/migration/V6.3.0.2.sql new file mode 100644 index 00000000000..7cf96538c95 --- /dev/null +++ b/src/main/resources/db/migration/V6.3.0.2.sql @@ -0,0 +1,2 @@ +-- Add thumbnail logo for featured dataverses +ALTER TABLE dataversetheme ADD COLUMN IF NOT EXISTS logothumbnail VARCHAR(255); diff --git a/src/main/resources/db/migration/V6.3.0.3.sql b/src/main/resources/db/migration/V6.3.0.3.sql new file mode 100644 index 00000000000..ece87767bcb --- /dev/null +++ b/src/main/resources/db/migration/V6.3.0.3.sql @@ -0,0 +1,30 @@ +-- Dataset types have been added. See #10517 and #10694 +-- +-- Insert the default dataset type: dataset (if not present). +-- Inspired by https://stackoverflow.com/questions/4069718/postgres-insert-if-does-not-exist-already/13342031#13342031 +INSERT INTO datasettype + (name) +SELECT 'dataset' +WHERE + NOT EXISTS ( + SELECT name FROM datasettype WHERE name = 'dataset' + ); +-- +-- Add the new column (if it doesn't exist). +ALTER TABLE dataset ADD COLUMN IF NOT EXISTS datasettype_id bigint; +-- +-- Add the foreign key. +DO $$ +BEGIN + BEGIN + ALTER TABLE dataset ADD CONSTRAINT fk_dataset_datasettype_id FOREIGN KEY (datasettype_id) REFERENCES datasettype(id); + EXCEPTION + WHEN duplicate_object THEN RAISE NOTICE 'Table constraint fk_dataset_datasettype_id already exists'; + END; +END $$; +-- +-- Give existing datasets a type of "dataset". +UPDATE dataset SET datasettype_id = (SELECT id FROM datasettype WHERE name = 'dataset'); +-- +-- Make the column non-null. +ALTER TABLE dataset ALTER COLUMN datasettype_id SET NOT NULL; diff --git a/src/main/resources/edu/harvard/iq/dataverse/datacite_metadata_template.xml b/src/main/resources/edu/harvard/iq/dataverse/datacite_metadata_template.xml deleted file mode 100644 index abe7ce79972..00000000000 --- a/src/main/resources/edu/harvard/iq/dataverse/datacite_metadata_template.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - ${identifier} - ${creators} - - ${title} - - ${publisher} - ${publisherYear} - - ${relatedIdentifiers} - - ${description} - - {$contributors} - diff --git a/src/main/resources/edu/harvard/iq/dataverse/pidproviders/doi/crossref_metadata_template.xml b/src/main/resources/edu/harvard/iq/dataverse/pidproviders/doi/crossref_metadata_template.xml new file mode 100644 index 00000000000..f37ed63cfff --- /dev/null +++ b/src/main/resources/edu/harvard/iq/dataverse/pidproviders/doi/crossref_metadata_template.xml @@ -0,0 +1,29 @@ + + + + ${batchId} + ${timestamp} + + ${depositor} + ${depositorEmail} + + Crossref + + + + + + ${title} + + + ${institution} + + + ${datasets} + + + diff --git a/src/main/webapp/404static.xhtml b/src/main/webapp/404static.xhtml index 69ff17ebc0f..dabcb299aa5 100644 --- a/src/main/webapp/404static.xhtml +++ b/src/main/webapp/404static.xhtml @@ -93,7 +93,7 @@
    -

    Copyright © 2023, The President & Fellows of Harvard College | Privacy Policy +

    Copyright ©, The President & Fellows of Harvard College | Privacy Policy

    @@ -106,4 +106,13 @@
    - + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/pretty-config.xml b/src/main/webapp/WEB-INF/pretty-config.xml index ab5f37a1051..5f8f4877af8 100644 --- a/src/main/webapp/WEB-INF/pretty-config.xml +++ b/src/main/webapp/WEB-INF/pretty-config.xml @@ -27,4 +27,9 @@ + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 427615f2f0b..732c634205f 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -182,6 +182,11 @@ webmanifest application/manifest+json + + xhtml + text/html + + diff --git a/src/main/webapp/dataset-citation.xhtml b/src/main/webapp/dataset-citation.xhtml index b42dd5e563f..346e39dc463 100644 --- a/src/main/webapp/dataset-citation.xhtml +++ b/src/main/webapp/dataset-citation.xhtml @@ -33,13 +33,13 @@
    diff --git a/src/main/webapp/dataset-license-terms.xhtml b/src/main/webapp/dataset-license-terms.xhtml index c54d94442ea..03173faf989 100644 --- a/src/main/webapp/dataset-license-terms.xhtml +++ b/src/main/webapp/dataset-license-terms.xhtml @@ -12,7 +12,7 @@ or !empty termsOfUseAndAccess.originalArchive or !empty termsOfUseAndAccess.availabilityStatus or !empty termsOfUseAndAccess.contactForAccess or !empty termsOfUseAndAccess.sizeOfCollection or !empty termsOfUseAndAccess.studyCompletion - or termsOfUseAndAccess.fileAccessRequest}"/> + }"/>

    - +

    - - + +

    @@ -482,7 +482,7 @@
    - #{bundle['file.dataFilesTab.terms.list.guestbook']}   + #{bundle['file.dataFilesTab.terms.list.guestbook']}  
    @@ -524,7 +524,7 @@ + update=":datasetForm:previewGuestbook" oncomplete="PF('viewGuestbook').show();"/>
    @@ -563,7 +563,7 @@ + update=":datasetForm:previewGuestbook" oncomplete="PF('viewGuestbook').show();"/> diff --git a/src/main/webapp/dataset.xhtml b/src/main/webapp/dataset.xhtml index e50e68ec162..6de0f00e94e 100644 --- a/src/main/webapp/dataset.xhtml +++ b/src/main/webapp/dataset.xhtml @@ -32,7 +32,7 @@ and !permissionsWrapper.canIssuePublishDatasetCommand(DatasetPage.dataset)}"/> - + @@ -228,20 +236,21 @@ - -
  • - - #{bundle.transfer} - - - - - -
  • -
    - + +
  • + + #{bundle.transfer} + + + + + +
  • +
    + + @@ -513,6 +522,16 @@
    + +
    + + #{bundle['dataset.unlinkBtn']} + +
    +
    @@ -576,16 +595,16 @@ - + - ) - +
    -
    +
    @@ -622,6 +641,7 @@ or !empty DatasetPage.datasetVersionUI.keywordDisplay or !empty DatasetPage.datasetVersionUI.subject.value or !empty DatasetPage.datasetVersionUI.relPublicationCitation + or !empty DatasetPage.datasetVersionUI.relPublicationUrl or !empty DatasetPage.datasetVersionUI.notes.value) and !empty DatasetPage.datasetSummaryFields}"> @@ -641,8 +661,10 @@ data-toggle="tooltip" data-placement="auto right" data-original-title="#{DatasetPage.datasetVersionUI.datasetRelPublications.get(0).description}">