From bb7a599cd27715335d81b2b4c1509a9dbb751955 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Thu, 18 Jun 2026 13:29:15 +0200 Subject: [PATCH 01/60] Port CLARIN backend data layer to DSpace 9 (migrations + entities/services) First foundation tranche of the CLARIN-DSpace 7.6.5 -> 9.3 forward-port. What's included (compiles, checkstyle + license headers clean, entity/schema validated on h2 via hbm2ddl=validate, migrations applied at DB init): - 19 CLARIN Flyway migrations (h2 x9, postgres x10) at original version numbers (Lindat schema, preview/report/matomo/clarin_token tables, share token, default licenses, 7z format). Purely additive. - ~107 CLARIN dspace-api classes: license framework, user metadata/registration, verification + clarin tokens, item/workspace services, handle service + external handle, shibboleth headers, featured services, provenance, EPIC/PID base, ORCID caching, factories + DAOs. - v9 API adaptation of ported code: javax.* -> jakarta.*, commons-lang -> lang3, NullArgumentException -> IllegalArgumentException, Hibernate 6 ORDINAL enum JdbcTypeCode fix (ClarinLicense.confirmation). - 9 CLARIN entity entries in hibernate.cfg.xml. - dspace-api/pom.xml deps: matomo-java-tracker, nimbus-jose-jwt, itextpdf, jfree, zjsonpatch. - Minimal additions to vanilla files: Handle (url/dead/deadSince), Util.formatNetId, HandlePlugin.getRepositoryName/getCanonicalHandlePrefix. Deferred to later tranches (tracked in CLARIN_DSPACE_V9_PROGRESS.md): S3/sync storage (AWS SDK v1->v2), preview, report/health/diff, matomo runtime, PID/EPIC clients, versioning CLI, Spring bean wiring, REST layer, frontend. See CLARIN_DSPACE_V9_PROGRESS.md for full inventory, status and testing evidence. Co-Authored-By: Claude Opus 4.8 --- CLARIN_DSPACE_V9_PROGRESS.md | 322 ++++ dspace-api/pom.xml | 34 + .../administer/ClarinTokenAdministrator.java | 197 +++ .../administer/ClarinTokenConfiguration.java | 86 + .../dspace/administer/ClarinTokenCreator.java | 221 +++ .../dspace/administer/ClarinTokenUtils.java | 80 + .../org/dspace/administer/FileDownloader.java | 229 +++ .../FileDownloaderConfiguration.java | 91 ++ .../ConfigFileNotAllowedException.java | 24 + .../ConfigFileNotFoundException.java | 24 + .../exception/ConfigFileUpdateException.java | 24 + .../service/ConfigFileService.java | 139 ++ .../service/ConfigFileServiceImpl.java | 247 +++ .../itemupdate/ItemFilesMetadataRepair.java | 257 +++ .../main/java/org/dspace/app/util/ACE.java | 170 ++ .../main/java/org/dspace/app/util/ACL.java | 142 ++ .../main/java/org/dspace/app/util/Util.java | 15 + .../clarin/ClarinShibAuthentication.java | 1384 +++++++++++++++++ .../dspace/authenticate/clarin/Headers.java | 215 +++ .../dspace/authenticate/clarin/ShibGroup.java | 307 ++++ .../authenticate/clarin/ShibHeaders.java | 160 ++ .../AuthorizationBitstreamUtils.java | 214 +++ .../DownloadTokenExpiredException.java | 19 + .../MissingLicenseAgreementException.java | 19 + .../DspaceObjectClarinServiceImpl.java | 83 + .../authority/SimpleORCIDAuthority.java | 164 ++ .../content/clarin/ClarinFeaturedService.java | 80 + .../clarin/ClarinFeaturedServiceLink.java | 37 + .../content/clarin/ClarinItemServiceImpl.java | 277 ++++ .../dspace/content/clarin/ClarinLicense.java | 227 +++ .../content/clarin/ClarinLicenseLabel.java | 115 ++ .../clarin/ClarinLicenseLabelServiceImpl.java | 109 ++ .../clarin/ClarinLicenseResourceMapping.java | 85 + ...arinLicenseResourceMappingServiceImpl.java | 269 ++++ .../ClarinLicenseResourceUserAllowance.java | 109 ++ ...censeResourceUserAllowanceServiceImpl.java | 173 +++ .../clarin/ClarinLicenseServiceImpl.java | 220 +++ .../dspace/content/clarin/ClarinToken.java | 119 ++ .../clarin/ClarinTokenServiceImpl.java | 205 +++ .../content/clarin/ClarinUserMetadata.java | 90 ++ .../clarin/ClarinUserMetadataServiceImpl.java | 175 +++ .../clarin/ClarinUserRegistration.java | 137 ++ .../ClarinUserRegistrationServiceImpl.java | 155 ++ .../clarin/ClarinVerificationToken.java | 105 ++ .../ClarinVerificationTokenServiceImpl.java | 113 ++ .../ClarinWorkspaceItemServiceImpl.java | 77 + .../clarin/MatomoReportSubscription.java | 102 ++ .../MatomoReportSubscriptionServiceImpl.java | 112 ++ .../content/dao/clarin/ClarinItemDAO.java | 22 + .../content/dao/clarin/ClarinLicenseDAO.java | 31 + .../dao/clarin/ClarinLicenseLabelDAO.java | 22 + .../ClarinLicenseResourceMappingDAO.java | 23 + ...ClarinLicenseResourceUserAllowanceDAO.java | 24 + .../content/dao/clarin/ClarinTokenDAO.java | 31 + .../dao/clarin/ClarinUserMetadataDAO.java | 21 + .../dao/clarin/ClarinUserRegistrationDAO.java | 23 + .../clarin/ClarinVerificationTokenDAO.java | 28 + .../clarin/MatomoReportSubscriptionDAO.java | 33 + .../dao/impl/clarin/ClarinItemDAOImpl.java | 45 + .../dao/impl/clarin/ClarinLicenseDAOImpl.java | 57 + .../clarin/ClarinLicenseLabelDAOImpl.java | 26 + .../ClarinLicenseResourceMappingDAOImpl.java | 74 + ...inLicenseResourceUserAllowanceDAOImpl.java | 81 + .../dao/impl/clarin/ClarinTokenDAOImpl.java | 44 + .../clarin/ClarinUserMetadataDAOImpl.java | 44 + .../clarin/ClarinUserRegistrationDAOImpl.java | 48 + .../ClarinVerificationTokenDAOImpl.java | 56 + .../MatomoReportSubscriptionDAOImpl.java | 65 + .../content/factory/ClarinServiceFactory.java | 61 + .../factory/ClarinServiceFactoryImpl.java | 127 ++ .../service/DspaceObjectClarinService.java | 32 + .../service/clarin/ClarinItemService.java | 105 ++ .../clarin/ClarinLicenseLabelService.java | 81 + .../ClarinLicenseResourceMappingService.java | 47 + ...inLicenseResourceUserAllowanceService.java | 33 + .../service/clarin/ClarinLicenseService.java | 115 ++ .../service/clarin/ClarinTokenService.java | 95 ++ .../clarin/ClarinUserMetadataService.java | 30 + .../clarin/ClarinUserRegistrationService.java | 31 + .../ClarinVerificationTokenService.java | 101 ++ .../clarin/ClarinWorkspaceItemService.java | 60 + .../MatomoReportSubscriptionService.java | 83 + .../core/ProvenanceMessageFormatter.java | 142 ++ .../core/ProvenanceMessageTemplates.java | 41 + .../org/dspace/core/ProvenanceService.java | 215 +++ .../dspace/core/ProvenanceServiceImpl.java | 465 ++++++ .../ctask/general/ItemHandleChecker.java | 189 +++ .../ClarinSolrItemsCommunityIndexPlugin.java | 49 + .../org/dspace/discovery/IsoLangCodes.java | 117 ++ .../external/CachingOrcidRestConnector.java | 224 +++ .../provider/orcid/xml/CacheLogger.java | 25 + .../orcid/xml/ExpandedSearchConverter.java | 199 +++ .../org/dspace/handle/AbstractPIDService.java | 91 ++ .../dspace/handle/EpicHandleRestHelper.java | 145 ++ .../dspace/handle/EpicHandleServiceImpl.java | 235 +++ .../main/java/org/dspace/handle/Handle.java | 39 + .../handle/HandleClarinServiceImpl.java | 553 +++++++ .../java/org/dspace/handle/HandlePlugin.java | 53 + .../java/org/dspace/handle/PIDService.java | 133 ++ .../dspace/handle/dao/HandleClarinDAO.java | 33 + .../handle/dao/impl/HandleClarinDAOImpl.java | 102 ++ .../external/ExternalHandleConstants.java | 20 + .../org/dspace/handle/external/Handle.java | 133 ++ .../dspace/handle/external/HandleRest.java | 81 + .../factory/HandleClarinServiceFactory.java | 30 + .../HandleClarinServiceFactoryImpl.java | 37 + .../handle/service/EpicHandleService.java | 124 ++ .../handle/service/HandleClarinService.java | 266 ++++ .../ClarinVersionedDOIIdentifierProvider.java | 271 ++++ .../main/java/org/dspace/util/FileInfo.java | 48 + .../dspace/util/FileTreeViewGenerator.java | 102 ++ ...07.28__Upgrade_to_Lindat_Clarin_schema.sql | 413 +++++ ...7.6_2024.01.25__insert_checksum_result.sql | 14 + .../V7.6_2024.08.05__Added_Preview_Tables.sql | 78 + ...9.30__Add_share_token_to_workspaceitem.sql | 9 + ...2025.06.03__Create_table_report_result.sql | 19 + ...06.09__Added_Indexes_To_Preview_Tables.sql | 25 + ...25.07.29__Matomo_report_registry_table.sql | 24 + .../h2/V7.6_2025.09.18__Clarin_token.sql | 20 + .../V7.6_2025.10.30__7z_bitstream_format.sql | 12 + ...07.28__Upgrade_to_Lindat_Clarin_schema.sql | 490 ++++++ ...7.6_2024.01.25__insert_checksum_result.sql | 14 + .../V7.6_2024.08.05__Added_Preview_Tables.sql | 88 ++ ...9.30__Add_share_token_to_workspaceitem.sql | 9 + ....6_2024.10.25__insert_default_licenses.sql | 202 +++ ...2025.06.03__Create_table_report_result.sql | 31 + ...06.09__Added_Indexes_To_Preview_Tables.sql | 25 + ...25.07.29__Matomo_report_registry_table.sql | 24 + .../V7.6_2025.09.18__Clarin_token.sql | 20 + .../V7.6_2025.10.30__7z_bitstream_format.sql | 12 + dspace/config/hibernate.cfg.xml | 13 + 131 files changed, 15461 insertions(+) create mode 100644 CLARIN_DSPACE_V9_PROGRESS.md create mode 100644 dspace-api/src/main/java/org/dspace/administer/ClarinTokenAdministrator.java create mode 100644 dspace-api/src/main/java/org/dspace/administer/ClarinTokenConfiguration.java create mode 100644 dspace-api/src/main/java/org/dspace/administer/ClarinTokenCreator.java create mode 100644 dspace-api/src/main/java/org/dspace/administer/ClarinTokenUtils.java create mode 100644 dspace-api/src/main/java/org/dspace/administer/FileDownloader.java create mode 100644 dspace-api/src/main/java/org/dspace/administer/FileDownloaderConfiguration.java create mode 100644 dspace-api/src/main/java/org/dspace/app/configuration/exception/ConfigFileNotAllowedException.java create mode 100644 dspace-api/src/main/java/org/dspace/app/configuration/exception/ConfigFileNotFoundException.java create mode 100644 dspace-api/src/main/java/org/dspace/app/configuration/exception/ConfigFileUpdateException.java create mode 100644 dspace-api/src/main/java/org/dspace/app/configuration/service/ConfigFileService.java create mode 100644 dspace-api/src/main/java/org/dspace/app/configuration/service/ConfigFileServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/app/itemupdate/ItemFilesMetadataRepair.java create mode 100644 dspace-api/src/main/java/org/dspace/app/util/ACE.java create mode 100644 dspace-api/src/main/java/org/dspace/app/util/ACL.java create mode 100644 dspace-api/src/main/java/org/dspace/authenticate/clarin/ClarinShibAuthentication.java create mode 100644 dspace-api/src/main/java/org/dspace/authenticate/clarin/Headers.java create mode 100644 dspace-api/src/main/java/org/dspace/authenticate/clarin/ShibGroup.java create mode 100644 dspace-api/src/main/java/org/dspace/authenticate/clarin/ShibHeaders.java create mode 100644 dspace-api/src/main/java/org/dspace/authorize/AuthorizationBitstreamUtils.java create mode 100644 dspace-api/src/main/java/org/dspace/authorize/DownloadTokenExpiredException.java create mode 100644 dspace-api/src/main/java/org/dspace/authorize/MissingLicenseAgreementException.java create mode 100644 dspace-api/src/main/java/org/dspace/content/DspaceObjectClarinServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/authority/SimpleORCIDAuthority.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinFeaturedService.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinFeaturedServiceLink.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinItemServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicense.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseLabel.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseLabelServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseResourceMapping.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseResourceMappingServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseResourceUserAllowance.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseResourceUserAllowanceServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinToken.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinTokenServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinUserMetadata.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinUserMetadataServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinUserRegistration.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinUserRegistrationServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinVerificationToken.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinVerificationTokenServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/ClarinWorkspaceItemServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/MatomoReportSubscription.java create mode 100644 dspace-api/src/main/java/org/dspace/content/clarin/MatomoReportSubscriptionServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinItemDAO.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinLicenseDAO.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinLicenseLabelDAO.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinLicenseResourceMappingDAO.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinLicenseResourceUserAllowanceDAO.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinTokenDAO.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinUserMetadataDAO.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinUserRegistrationDAO.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinVerificationTokenDAO.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/clarin/MatomoReportSubscriptionDAO.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinItemDAOImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinLicenseDAOImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinLicenseLabelDAOImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinLicenseResourceMappingDAOImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinLicenseResourceUserAllowanceDAOImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinTokenDAOImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinUserMetadataDAOImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinUserRegistrationDAOImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinVerificationTokenDAOImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/MatomoReportSubscriptionDAOImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/factory/ClarinServiceFactory.java create mode 100644 dspace-api/src/main/java/org/dspace/content/factory/ClarinServiceFactoryImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/content/service/DspaceObjectClarinService.java create mode 100644 dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinItemService.java create mode 100644 dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinLicenseLabelService.java create mode 100644 dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinLicenseResourceMappingService.java create mode 100644 dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinLicenseResourceUserAllowanceService.java create mode 100644 dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinLicenseService.java create mode 100644 dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinTokenService.java create mode 100644 dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinUserMetadataService.java create mode 100644 dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinUserRegistrationService.java create mode 100644 dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinVerificationTokenService.java create mode 100644 dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinWorkspaceItemService.java create mode 100644 dspace-api/src/main/java/org/dspace/content/service/clarin/MatomoReportSubscriptionService.java create mode 100644 dspace-api/src/main/java/org/dspace/core/ProvenanceMessageFormatter.java create mode 100644 dspace-api/src/main/java/org/dspace/core/ProvenanceMessageTemplates.java create mode 100644 dspace-api/src/main/java/org/dspace/core/ProvenanceService.java create mode 100644 dspace-api/src/main/java/org/dspace/core/ProvenanceServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/ctask/general/ItemHandleChecker.java create mode 100644 dspace-api/src/main/java/org/dspace/discovery/ClarinSolrItemsCommunityIndexPlugin.java create mode 100644 dspace-api/src/main/java/org/dspace/discovery/IsoLangCodes.java create mode 100644 dspace-api/src/main/java/org/dspace/external/CachingOrcidRestConnector.java create mode 100644 dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/CacheLogger.java create mode 100644 dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/ExpandedSearchConverter.java create mode 100644 dspace-api/src/main/java/org/dspace/handle/AbstractPIDService.java create mode 100644 dspace-api/src/main/java/org/dspace/handle/EpicHandleRestHelper.java create mode 100644 dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/handle/HandleClarinServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/handle/PIDService.java create mode 100644 dspace-api/src/main/java/org/dspace/handle/dao/HandleClarinDAO.java create mode 100644 dspace-api/src/main/java/org/dspace/handle/dao/impl/HandleClarinDAOImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/handle/external/ExternalHandleConstants.java create mode 100644 dspace-api/src/main/java/org/dspace/handle/external/Handle.java create mode 100644 dspace-api/src/main/java/org/dspace/handle/external/HandleRest.java create mode 100644 dspace-api/src/main/java/org/dspace/handle/factory/HandleClarinServiceFactory.java create mode 100644 dspace-api/src/main/java/org/dspace/handle/factory/HandleClarinServiceFactoryImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/handle/service/EpicHandleService.java create mode 100644 dspace-api/src/main/java/org/dspace/handle/service/HandleClarinService.java create mode 100644 dspace-api/src/main/java/org/dspace/identifier/ClarinVersionedDOIIdentifierProvider.java create mode 100644 dspace-api/src/main/java/org/dspace/util/FileInfo.java create mode 100644 dspace-api/src/main/java/org/dspace/util/FileTreeViewGenerator.java create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.2_2022.07.28__Upgrade_to_Lindat_Clarin_schema.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2024.01.25__insert_checksum_result.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2024.08.05__Added_Preview_Tables.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2024.09.30__Add_share_token_to_workspaceitem.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.06.03__Create_table_report_result.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.06.09__Added_Indexes_To_Preview_Tables.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.07.29__Matomo_report_registry_table.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.09.18__Clarin_token.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.10.30__7z_bitstream_format.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.2_2022.07.28__Upgrade_to_Lindat_Clarin_schema.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2024.01.25__insert_checksum_result.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2024.08.05__Added_Preview_Tables.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2024.09.30__Add_share_token_to_workspaceitem.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2024.10.25__insert_default_licenses.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.06.03__Create_table_report_result.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.06.09__Added_Indexes_To_Preview_Tables.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.07.29__Matomo_report_registry_table.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.09.18__Clarin_token.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.10.30__7z_bitstream_format.sql diff --git a/CLARIN_DSPACE_V9_PROGRESS.md b/CLARIN_DSPACE_V9_PROGRESS.md new file mode 100644 index 000000000000..0b538a0443c7 --- /dev/null +++ b/CLARIN_DSPACE_V9_PROGRESS.md @@ -0,0 +1,322 @@ +# CLARIN-DSpace → DSpace 9 Upgrade — Progress (source of truth) + +> Read this file before starting or resuming. Update it after every meaningful step +> and before any context compaction/handoff. This is the durable state of the effort. +> Last updated: 2026-06-18 (session 1 — initial ground-truth + inventory). + +## 0. TL;DR / current status + +- **Phase:** Foundation / discovery complete; porting **not yet started** (code-wise). +- **Backend PR:** https://github.com/dataquest-dev/DSpace/pull/1339 — base `dtq-dev`, head `ufal/clarin-dspace-upgrade-v9`, **OPEN, CONFLICTING**. Head is currently **pristine vanilla DSpace 9.3** (no CLARIN code yet). +- **Frontend PR:** https://github.com/dataquest-dev/dspace-angular/pull/1316 — base `dtq-dev`, head `ufal/clarin-dspace-upgrade-v9`, **OPEN, CONFLICTING**. Head is currently **pristine vanilla dspace-angular 9.x**. +- **What "the PR diff" really is:** Because head = vanilla 9.3 and base = old CLARIN `dtq-dev` (7.6.5), the GitHub PR diff (BE 2372 files, FE 3944 files) is *the entire 7.6.5↔9.3 gap*, NOT work done. It would currently *delete* CLARIN. The real task = re-apply the CLARIN fork-delta on top of v9. + +## 1. Objective + +Port all CLARIN-DSpace (LINDAT/CLARIN, a.k.a. UFAL) features from the customized +fork (`dtq-dev`, DSpace **7.6.5**) onto vanilla **DSpace 9.3** backend and +**dspace-angular 9.x** frontend, on branch `ufal/clarin-dspace-upgrade-v9` in both +repos, preserving compatibility with the `dtq-dev` workflow, CI, Docker and local dev. +Get the full stack running locally in Docker and the `dataquest-dev/dspace-ui-tests` +Playwright suite passing. + +## 2. Environment (verified this session) + +| Tool | Version | Notes | +|------|---------|-------| +| Docker | 29.4.3 | daemon up, 12 CPUs, 15.58 GiB RAM | +| Docker Compose | v5.1.4 | | +| Java | Temurin 17.0.16 | DSpace 9 requires JDK 17 ✓ | +| Maven | 3.8.3 | | +| Node | 20.19.0 | | +| npm | 10.8.2 | | +| gh CLI | 2.67.0 | authed as `milanmajchrak` (ssh, repo scope) | +| git | 2.45.1 | SSH remotes; push allowed only to the 2 PR branches | + +Workspace (do not leave it): `C:\workspace\clarin-dspace-v9-upgrade\` +- `DSpace/` — backend repo, branch `ufal/clarin-dspace-upgrade-v9` +- `dspace-angular/` — frontend repo, branch `ufal/clarin-dspace-upgrade-v9` + +## 3. Ground-truth branch map + +| Ref | What it is | +|-----|-----------| +| `origin/dtq-dev` (BE) | CLARIN-DSpace **7.6.5** — THE SOURCE of CLARIN features. Active (last commit 2026-06-17). | +| `origin/dtq-dev` (FE) | dspace-angular **7.6.5** CLARIN fork — SOURCE for FE features. | +| `origin/dtq-dev-9` (BE) | plain vanilla DSpace **9.1** tracking branch — *no CLARIN work*. Not useful except as reference. | +| `ufal/clarin-dspace-upgrade-v9` (BE) | TARGET — currently vanilla **9.3** (`e0fae432ff`). | +| `ufal/clarin-dspace-upgrade-v9` (FE) | TARGET — currently vanilla **9.x**. | +| tags `dspace-7.6.5`, `dspace-9.3` (BE) / `dspace-7.6.3`, `dspace-9.0` (FE) | vanilla baselines used to compute fork-delta. | + +## 4. Methodology — fork-delta porting + +The 7.6.5→9.3 PR diff mixes DSpace's own evolution with CLARIN changes. To isolate the +**CLARIN customization (fork-delta)**: + +``` +# backend CLARIN delta = vanilla 7.6.5 -> dtq-dev +git diff dspace-7.6.5 origin/dtq-dev +# frontend CLARIN delta = vanilla 7.6.3 -> dtq-dev +git diff dspace-7.6.3 origin/dtq-dev # (merge-base is 7.6.1/7.6.3 era) +``` + +Then re-apply that delta on top of v9, adapting for API changes. ADDED files port +near-verbatim (just package/API tweaks); MODIFIED vanilla files are the hard ports +(v9 refactored many APIs). + +### Fork-delta size (measured this session) + +**Backend (`dspace-7.6.5` → `dtq-dev`): 838 files, +107009 / −2677** +- 521 **added** files (187 Java classes named `Clarin*`/in `clarin/` pkgs, + migrations, + config) +- 314 **modified** vanilla files → **218 modified .java** (need v9 API adaptation) + 52 config/xml/properties +- By area: dspace-api/src 357, dspace-server-webapp/src 295, dspace/config 85, dspace-oai/src 32, .github/workflows 10, scripts ~25 + +**Frontend (`dspace-7.6.3` → `dtq-dev`): 1985 files, +191114 / −31092** +- ~959 are `src/assets/images` (license/label icons — copy verbatim) → ~1000 real code files +- By area: app/shared 199, item-page 155, core 85, submission 48, handle-page 28, clarin-licenses 24, epic-handle 23, login-page 21, entity-groups 21, bitstream-page 25, discojuice 7, license-contract-page 6, share-submission 6 … + +## 5. CLARIN feature inventory (from wiki + delta) and porting status + +Source: https://github.com/ufal/clarin-dspace/wiki/Features (+ linked pages). +Status legend: ⬜ not started · 🟦 in progress · ✅ ported+verified · ⛔ blocked · ➖ N/A. + +### Licensing & Access Control +| # | Feature | BE | FE | Notes / source classes | +|---|---------|----|----|------------------------| +| L1 | CLARIN Licenses (label framework, confirmation) | ⬜ | ⬜ | `content/clarin/ClarinLicense*`, `app/clarin-licenses` | +| L2 | License Agreement Dialog (user details at download) | ⬜ | ⬜ | `ClarinLicenseResourceUserAllowance*`, `ClarinUserMetadata*` | +| L3 | Creative Commons license submission step | ⬜ | ⬜ | | +| L4 | Field-Level Permissions (ACL) | ⬜ | ⬜ | wiki FineGrainedFieldPermissions | +| L5 | Bitstream Download Tokens (time-limited) | ⬜ | ⬜ | `ClarinBitstreamServiceImpl`, download-token | + +### Persistent Identifiers +| # | Feature | BE | FE | Notes | +|---|---------|----|----|-------| +| P1 | PIDs & Handles (per-community prefixes, external handles, content negotiation) | ⬜ | ⬜ | handle-page, epic-handle | +| P2 | EPIC PID API v2 integration | ⬜ | ⬜ | | +| P3 | Handle management GUI | ⬜ | ⬜ | | +| P4 | DOI registration (DataCite: reserve/register/update) | ⬜ | ⬜ | | +| P5 | DOI config per community | ⬜ | ⬜ | | +| P6 | ORCID authority (no Solr) | ⬜ | ⬜ | | +| P7 | ROR authority | ⬜ | ⬜ | | + +### Submission Workflow +| # | Feature | BE | FE | Notes | +|---|---------|----|----|-------| +| S1 | Sharing a submission (share token) | ⬜ | ⬜ | `Add_share_token_to_workspaceitem` migration, app/share-submission | +| S2 | Complex fields (contact_person, funding, sizeInfo) | ⬜ | ⬜ | | +| S3 | Admin-only fields | ⬜ | ⬜ | | +| S4 | Autocomplete (Solr / static JSON) | ⬜ | ⬜ | | +| S5 | CMDI metadata file upload (METADATA bundle) | ⬜ | ⬜ | | +| S6 | CLARIN license steps (Distribution + Resource) | ⬜ | ⬜ | | +| S7 | CLARIN notice step | ⬜ | ⬜ | | + +### Authentication & User Management +| # | Feature | BE | FE | Notes | +|---|---------|----|----|-------| +| A1 | Shibboleth AAI + DiscoJuice | ⬜ | ⬜ | `authenticate/clarin/ClarinShibAuthentication`, aai/discojuice | +| A2 | Shibboleth auto-registration | ⬜ | ⬜ | `ClarinUserRegistration*`, `ClarinVerificationToken*` | +| A3 | User registration + email verification | ⬜ | ⬜ | | +| A4 | Personal Access Tokens (PAT) | ⬜ | ⬜ | `ClarinToken*`, `Clarin_token` migration | + +### Item Display & Services +| # | Feature | BE | FE | Notes | +|---|---------|----|----|-------| +| I1 | Featured services / Refbox | ⬜ | ⬜ | `ClarinFeaturedService*` | +| I2 | File previews (+directory tree) | ⬜ | ⬜ | `Added_Preview_Tables` migration | +| I3 | Item tombstones (withdrawn/replaced) | ⬜ | ⬜ | | +| I4 | ZIP download (all bitstreams) | ⬜ | ⬜ | | +| I5 | Item versioning (CLARIN handles/DOIs) | ⬜ | ⬜ | `ItemVersionLinker` | +| I6 | WebLicht integration (CMDI/OAI) | ⬜ | ⬜ | dspace-oai delta | + +### Analytics & Monitoring +| # | Feature | BE | FE | Notes | +|---|---------|----|----|-------| +| M1 | Matomo analytics (bitstream + OAI tracking, report subscriptions) | ⬜ | ⬜ | `ClarinMatomo*Tracker`, `MatomoReportSubscription`, migration | +| M2 | Health report + report diff (DB snapshots, email) | ⬜ | ⬜ | `report_result` migration | +| M3 | Google Dataset Search structured data | ⬜ | ⬜ | | + +### Operations & Admin Tools +| # | Feature | BE | FE | Notes | +|---|---------|----|----|-------| +| O1 | S3 storage integration (presigned URLs) | ⬜ | ⬜ | | +| O2 | File downloader CLI | ⬜ | ⬜ | `administer/FileDownloader` | +| O3 | Curation tasks (requiredmetadata, metadataqa, checklinks, checkhandles, registerdoi, profileformats) | ⬜ | ⬜ | | +| O4 | CLI scripts (clarin-token, process-cleaner, item-version-linker, file-downloader, file-preview, health-report, report-diff) | ⬜ | ⬜ | `administer/Clarin*`, `ItemVersionLinker` | +| O5 | Bulk import REST API (handles, users, licenses, metadata, bitstreams, logos) | ⬜ | ⬜ | import endpoints | +| O6 | Configuration file admin API | ⬜ | ⬜ | `app/configuration/service/ConfigFileService` | +| O7 | CMDI metadata export REST endpoint | ⬜ | ⬜ | | + +## 6. Foundational: CLARIN DB migrations to port (Flyway) + +Both `h2` (tests) and `postgres` (runtime) copies needed. v9 may already include some of +these upstream (e.g. orcid/supervision/system-wide-alerts were 7.x upstream additions and +are likely already in 9.x — DO NOT double-add). **Verify each against v9 before porting.** + +CLARIN-specific (must port): +- `V7.2_2022.07.28__Upgrade_to_Lindat_Clarin_schema.sql` ← the big CLARIN schema (license tables, handles, user metadata, etc.) +- `V7.6_2024.08.05__Added_Preview_Tables.sql` + `V7.6_2025.06.09__Added_Indexes_To_Preview_Tables.sql` +- `V7.6_2024.09.30__Add_share_token_to_workspaceitem.sql` +- `V7.6_2024.10.25__insert_default_licenses.sql` +- `V7.6_2024.01.25__insert_checksum_result.sql` +- `V7.6_2025.06.03__Create_table_report_result.sql` +- `V7.6_2025.07.29__Matomo_report_registry_table.sql` +- `V7.6_2025.09.18__Clarin_token.sql` +- `V7.6_2025.10.30__7z_bitstream_format.sql` + +> NOTE: version-numbering. These are `V7.x` prefixed. On v9 the Flyway baseline differs; +> CLARIN migrations may need renumbering to `V9.x_...` or placed so they run after the v9 +> schema. Decide & document (see Decisions). + +## 6b. Playwright suite (dataquest-dev/dspace-ui-tests) — how to run + +Cloned to `C:\workspace\clarin-dspace-v9-upgrade\dspace-ui-tests` (private, branch `master`). +- **Stack:** Playwright 1.57 + TypeScript, npm. 6 spec files under `tests/tests/` + (homePage, itemPage, loginPage, searchPage, submissionPage, universalPage) with page + objects under `tests/pages/`. +- **Config:** `playwright.config.ts` (`globalSetup: ./scripts/merge-config.ts` merges + `customer-constants/config.default.json` + any other `*.json`). No baseURL hardcoded; + uses env. `ignoreHTTPSErrors:true`, 3 browser projects (chromium/firefox/webkit). +- **Run recipe (from CI `.github/workflows`):** env `HOME_URL=https://dev-5.pc:8443/repository/`, + `NAME=DEFAULT`; `cd scripts && ./test.sh`. Credentials via `.env` + (admin `dspace.admin.dev@dataquest.sk`/`admin`, user `dspace.user.dev@dataquest.sk`/`user`). +- **⚠ Hard dependency — seeded test data:** tests assert against FIXED production data: + - handles: restricted_download `11234/1-2683`, bitstreams `11234/1-5419`, + doi `20.500.12801/3901359-01`, new_version `11234/1-5677`, restricted collection + `11234/1-f26b6363`, icons `11234/1-4875`, version redirect `11858/00-097C-...`. + - branding: home title "LINDAT/CLARIAH-CZ Repository Home"; URL prefix `/repository`. + - OAI endpoints, license manage-table, handle-table, bulk-access pages. + → To run locally we must (a) point HOME_URL at the local stack, (b) seed matching + CLARIN items/handles/licenses/DOIs/versions, (c) set `NAME`/locators for our config, + or relax data-specific assertions. **This is a substantial test-data task, tracked separately.** + +## 6c. Manual test specs +- **dspace-customers#55** "[TEST] Testing scenarios" (OPEN): step-by-step manual scenarios — + create EPerson (→ `user_registration` row), restricted-download redirect to login, + download from restricted item, license manage-table, handle-table, bulk-access, OAI cmdi + exposure, item versions, icons, etc. Mirrors the Playwright assertions. +- **dspace-customers#411**: (to summarize next session) additional manual test spec. + +## 7. Execution plan (phased) + +1. **Foundation (this/next sessions)** + - [x] Ground truth, fork-delta sizing, feature inventory, this file. + - [x] Playwright suite + manual specs analyzed (run recipe + data dependency captured). + - [ ] Docker baseline: bring up vanilla 9.3 BE + FE locally; confirm healthy. (Establishes that the platform runs before adding CLARIN.) — baseline mvn build running. + - [x] Port CLARIN Flyway migrations (19 files: 9 h2 + 10 postgres) at ORIGINAL version numbers (faithful fresh-install order; preserves real upgrade path). **Validation pending: run Flyway against Docker postgres / h2 IT.** +2. **Backend port (by module, additive first)** + - [ ] Entities/DAOs/services in `content/clarin`, `authenticate/clarin`, `administer/Clarin*` (additive — port first). + - [ ] REST endpoints (`dspace-server-webapp`) — adapt to v9 REST/HATEOAS API. + - [ ] Modified vanilla files (218 .java) — the hard ports; adapt per v9 API. + - [ ] Config (`dspace/config` 85 files): spring beans, `*.cfg`, item-submission, etc. +3. **Frontend port (by feature module)** + - [ ] Copy assets (license icons), i18n keys. + - [ ] Port standalone feature modules (clarin-licenses, handle-page, epic-handle, share-submission, login/shibboleth, discojuice…). + - [ ] Adapt core models/data-services to angular v9 (standalone components, new APIs). +4. **Integration & tests** + - [ ] BE unit + IT (h2) green; FE lint + unit green. + - [ ] Full Docker stack up with CLARIN config. + - [ ] Configure + run Playwright `dataquest-dev/dspace-ui-tests` against local stack. + - [ ] Manual test specs (dspace-customers #55, #411). +5. **Hardening** + - [ ] Resolve PR conflicts; keep CI green; independent review agents; fix findings. + +## 8. Tests executed (log) + +| Date | Scope | Command | Result | +|------|-------|---------|--------| +| 2026-06-18 | env probe | `docker/java/mvn/node --version` | all present (see §2) | +| 2026-06-18 | baseline build | `mvn -q -T 1C -DskipTests -pl dspace-api,dspace-server-webapp,dspace-oai -am install` | ✅ BUILD SUCCESS in ~4 min; `dspace-api-9.3.jar` installed. Vanilla 9.3 BE compiles. Maven cache warm. | +| 2026-06-18 | migration validation attempt (unit test) | `mvn install -DskipUnitTests=false -pl dspace-api -am -Dtest=AccessStatusServiceTest` | ⚠ Test errored in setup with "DSpace home directory could not be determined / config-definition.xml" — kernel never started, Flyway never ran. **NOT a migration problem.** Pre-existing harness wrinkle (see KI-1). Migration validation deferred to Docker/postgres. | + +### Work done in working tree (not yet pushed) +- **Backend migrations:** 19 CLARIN Flyway migrations ported from `dtq-dev` (h2×9, postgres×10) into + `dspace-api/.../sqlmigration/{h2,postgres}/`. Purely additive; original version numbers kept. + **VALIDATED on h2** (AccessStatusServiceTest ran 3 tests, Flyway applied them at DB init). +- **Backend dspace-api code (compiles ✅ `mvn -o -pl dspace-api compile` EXIT=0):** + - 107 CLARIN java files ported into core (license framework, user metadata/registration, + verification token, clarin token, item/workspace services, handle service + external handle, + shibboleth ShibHeaders, featured services, matomo report subscription, etc.). + - Systematic v9 migrations applied to ported files: `javax.*`→`jakarta.*` (persistence/ws.rs/ + servlet/validation/mail/annotation); `org.apache.commons.lang`(v2)→`lang3`; + `NullArgumentException`→`IllegalArgumentException` (removed in lang3). + - Maven deps added to `dspace-api/pom.xml`: matomo-java-tracker-java11 3.4.0, itextpdf 5.5.13.4, + jfree jcommon/jfreechart, zjsonpatch 0.4.16, nimbus-jose-jwt ${nimbus-jose-jwt.version}. + - **Modified vanilla files ported** (CLARIN deltas re-applied on v9): `handle/Handle.java` + (+url/dead/deadSince), `app/util/Util.java` (+formatNetId), `handle/HandlePlugin.java` + (+getRepositoryName/getCanonicalHandlePrefix statics). + - Entities registered in `dspace/config/hibernate.cfg.xml` (9 CLARIN `` entries). + - **Hibernate 6 fix:** `ClarinLicense.confirmation` ORDINAL enum pinned `@JdbcTypeCode(SqlTypes.INTEGER)` + (H6 defaults ORDINAL→TINYINT; column is INTEGER). Entity↔schema validation iterating (see §6d). + +### Deferred backend files (in `_deferred/`, OUTSIDE repo — re-port as their feature lands) — 42 files +Reason: depend on bigger v9 rewrites or are leaf features, kept out to reach a compiling core. +- **S3 storage (AWS SDK v1→v2 rewrite needed):** `SyncS3BitStoreService`, `S3DirectDownloadServiceImpl`, + `S3DirectDownloadService`, `SyncBitstreamStorageServiceImpl`, `ClarinBitstreamServiceImpl`, + `service/clarin/ClarinBitstreamService`. (v9 uses `software.amazon.awssdk`, CLARIN used `com.amazonaws`.) +- **Hibernate type:** `storage/rdbms/hibernate/DatabaseAwareLobType` (H5 `SqlTypeDescriptor`→H6 `JdbcType`). +- **Preview feature:** `content/PreviewContent*`, `service/PreviewContentService`, `dao(/impl)/PreviewContentDAO*`, + `scripts/filepreview/*`. +- **Report/health/diff:** `content/ReportResult*`, `app/healthreport/*`, `health/*`, `app/reportdiff/*`, + `ctask/general/ItemMetadataQAChecker`, `curate/reporters/*`. +- **PID/EPIC:** `handle/PIDServiceEPICv2`, `handle/PIDConfiguration`, `handle/PIDCommunityConfiguration`, + `handle/external/*` UN-deferred (core), but EPIC parts deferred. +- **Matomo runtime:** `app/statistics/clarin/ClarinMatomo*`, `matomo/*`. +- **Versioning/identifier CLI:** `administer/ItemVersionLinker*`, `identifier/ClarinVersionedHandleIdentifierProvider`, + `api/DSpaceApi`. + +### 6d. Entity↔schema reconciliation (Hibernate 6 `validate`) — ✅ PASSED (core) +DSpace boots with `hibernate.hbm2ddl.auto=validate`, so every ported entity must exactly match its +migrated table. **2026-06-18: `AccessStatusServiceTest` → 3 tests, 0 failures, BUILD SUCCESS** with all +9 CLARIN entities mapped → SessionFactory builds + Flyway migrations apply + schema validation passes. +This proves migrations↔entities are consistent on h2/Hibernate 6. Only fix needed: `confirmation` +ORDINAL enum `@JdbcTypeCode(SqlTypes.INTEGER)`. (Diagnostic note: a SessionFactory build failure +manifests as an NPE in the `EntityTypeServiceInitializer` afterMigrate callback — look earlier in the +surefire `*-output.txt` for the real `Schema-validation:` cause.) + +## 9. Decisions / assumptions / open questions + +- **D1 (CONFIRMED by maintainer 2026-06-18):** `dtq-dev` (7.6.5) is the authoritative feature source — "where CLARIN-DSpace is now with all the features". `ufal/clarin-dspace-upgrade-v9` is a fresh branch off vanilla 9.3 pushed to the dataquest repo. Port fresh from `dtq-dev`; `dtq-dev-9` is irrelevant (plain vanilla 9.1). +- **D7 (CONFIRMED):** Strategy = **foundation-first / horizontal** (migrations → entities/DAOs → services → REST → config → frontend; keep backend compiling/booting, then layer features). +- **D8 (CONFIRMED):** Push cadence = push **coherent building tranches** to PRs #1339/#1316 (only when the tranche compiles and self-tests pass + progress file updated). +- **D2 (open):** Flyway migration renumbering strategy for v9 (V7.x → V9.x vs ordering). Must inspect v9's existing migration set before porting. +- **D3 (assumption):** Frontend angular 9 uses standalone components / new control-flow; many 7.x CLARIN components will need conversion. Confirm scale during FE port. +- **D4 (process):** Do NOT push to PRs until a coherent, building tranche exists and the change set is reviewed. Only the two named PR branches may be pushed. +- **D5 (open):** Which upstream-7.x migrations are already in v9 (orcid, supervision, system-wide-alerts) — must not double-apply. +- **D6 (scope reality):** Full port is ~838 BE + ~1000 FE code files. This is a multi-session effort; progress is tracked here incrementally. No feature is silently skipped — anything not done is listed ⬜ above. + +## 9b. Known issues (KI) + +- **KI-1 — Local unit-test harness (Windows):** `mvn ... -pl dspace-api test -DskipUnitTests=false` + fails at kernel start: "DSpace home directory could not be determined … config-definition.xml". + Root cause: DSpace deliberately EXCLUDES `config-definition.xml` from `testEnvironment.zip`; + unit tests resolve it from the classpath via the `dspace-services` **test-jar**. A piecemeal + reactor build does not put that test-jar's resources on dspace-api's test classpath. Affects + vanilla 9.3 too (not caused by CLARIN changes). **Resolution options:** (a) run full + `mvn install -DskipUnitTests=false` from repo root once (CI does this on Linux), or + (b) validate via Docker. Until resolved, validate DB/migrations on the Docker postgres stack. + +## 10. Next steps (immediate) + +1. **Validate ported migrations on Docker postgres** (decisive). Bring up `dspacedb` (postgres-pgcrypto 9.x) + run DSpace `database migrate` (or boot vanilla BE pointed at it) to confirm the 10 CLARIN migrations apply cleanly on top of the v9 schema. This also = first Docker stack milestone. +2. **Resolve KI-1** OR rely on Docker: do one full `mvn install -DskipUnitTests=false` from root (Linux/Docker) to get a working unit-test loop for verifying future backend tranches. +3. **Backend additive port — module 1: CLARIN content/persistence layer** (~54 files): + `content/clarin` (22 entities+impls) + `content/service/clarin` (12) + `content/dao/clarin` (10) + + `content/dao/impl/clarin` (10) + `content/factory` (ClarinServiceFactory). Wire into + `hibernate.cfg.xml`/persistence + `core-services.xml`/`core-factory-services.xml`. Adapt to v9 + DAO/entity base-class APIs. Keep `mvn -pl dspace-api compile` green at each checkpoint. +4. Then: handle/PID layer (`org/dspace/handle*` 8+3+2+2), authenticate/clarin (Shibboleth), administer + CLI tools, statistics/matomo, health/reportdiff, then `dspace-server-webapp` REST, then config. +5. Pull full PR/issue lists (BE/FE) and dspace-customers#411 into the inventory. +6. Push tranche 1 (migrations + progress doc + content layer) to PR #1339 once it compiles. NOTE: + migrations alone are intentionally held back from push until paired with the entity layer that uses + them AND validated on postgres (avoid turning PR CI red). + +## 11. Reference links + +- Wiki features: https://github.com/ufal/clarin-dspace/wiki/Features +- BE PRs/issues: https://github.com/dataquest-dev/DSpace/pulls · /issues +- FE PRs/issues: https://github.com/dataquest-dev/dspace-angular/pulls · /issues +- Manual test specs: dataquest-dev/dspace-customers#55 · #411 +- Playwright: https://github.com/dataquest-dev/dspace-ui-tests +- Target PRs: dataquest-dev/DSpace#1339 (BE) · dataquest-dev/dspace-angular#1316 (FE) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index d70c4fb3c799..a6e215669004 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -323,6 +323,40 @@ + + + + org.piwik.java.tracking + matomo-java-tracker-java11 + 3.4.0 + + + + com.itextpdf + itextpdf + 5.5.13.4 + + + org.jfree + jcommon + 1.0.24 + + + org.jfree + jfreechart + 1.5.6 + + + com.flipkart.zjsonpatch + zjsonpatch + 0.4.16 + + + + com.nimbusds + nimbus-jose-jwt + ${nimbus-jose-jwt.version} + org.apache.logging.log4j log4j-api diff --git a/dspace-api/src/main/java/org/dspace/administer/ClarinTokenAdministrator.java b/dspace-api/src/main/java/org/dspace/administer/ClarinTokenAdministrator.java new file mode 100644 index 000000000000..4a3bdb5bcea3 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/administer/ClarinTokenAdministrator.java @@ -0,0 +1,197 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.administer; + +import static org.dspace.administer.ClarinTokenCreator.getExpirationDate; +import static org.dspace.administer.ClarinTokenCreator.getMaskedToken; + +import java.security.NoSuchAlgorithmException; +import java.sql.SQLException; +import java.util.Base64; +import java.util.Date; +import java.util.UUID; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + +import com.nimbusds.jose.EncryptionMethod; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.factory.ClarinServiceFactory; +import org.dspace.content.service.clarin.ClarinTokenService; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.eperson.service.EPersonService; + +/** + * This admin script is used to manage(create or invalidate) clarin tokens, for the user specified by ID or e-mail. + *

+ * For the token creation, the user and the expiration time must be set in script options. + * Created token string is then printed on the console, and admin can send it to the user (e.g. via the e-mail). + *

+ * For the token invalidation, either the token string or the user need to be set in script options. + * When neither token nor user is set, all tokens are deleted. + *

+ * Admin user can also use this script to generate encryption/decryption secret key and store it into + * "clarin.token.encryption.secret" config property. + * + * @author Milan Kuchtiak + */ +public class ClarinTokenAdministrator { + + private static final Logger log = LogManager.getLogger(ClarinTokenAdministrator.class); + + private ClarinTokenAdministrator() { + } + + public static void main(String args[]) throws Exception { + log.info("Clarin Token administrator started ...."); + + Options options = new Options(); + options.addOption("c", "create", false, "create token for ePerson specified by ID or email"); + options.addOption("d", "delete", false, + "delete specified token, or delete all tokens for given ePerson, " + + "or delete all tokens when -t, -u, and -e options are missing)"); + options.addOption("g", "generateEncryptionKey", false, + "generate encryption/decryption secret key for clarin.token.encryption.secret property"); + options.addOption("u", "ePerson_ID", true, "ePerson UUID"); + options.addOption("e", "email", true, "ePerson email"); + options.addOption("x", "expiration", true, + "token expiration time in days or hours, (e.g. 3d or 48h), for -c option only [required for create]"); + options.addOption("t", "token", true, + "token string [optional for delete]"); + options.addOption("h", "help", false, "help"); + + CommandLineParser parser = new DefaultParser(); + try { + CommandLine line = parser.parse(options, args); + if (line.hasOption('h') || (!line.hasOption('c') && !line.hasOption('d') && !line.hasOption('g')) ) { + printHelpAndExit(options); + } + boolean isCreate = line.hasOption('c'); + boolean isDelete = line.hasOption('d'); + boolean generateEncryptionKey = line.hasOption('g'); + + if (isCreate && isDelete || isCreate && generateEncryptionKey || isDelete && generateEncryptionKey) { + throw new ParseException("Create, delete and generate options are mutually exclusive"); + } + + if (isCreate && !line.hasOption("u") && !line.hasOption("e")) { + throw new ParseException("either ePerson UUID or ePerson e-mail option is needed to create token"); + } + + if (isCreate && !line.hasOption("x")) { + throw new ParseException("Token expiration time option is missing"); + } + + UUID ePersonUUID = null; + if (line.hasOption("u")) { + ePersonUUID = UUID.fromString(line.getOptionValue("u")); + } + + String email = null; + if (line.hasOption("e")) { + email = line.getOptionValue("e"); + } + + String token = null; + if (line.hasOption("t")) { + token = line.getOptionValue("t"); + } + + ClarinTokenService clarinTokenService = + ClarinServiceFactory.getInstance().getClarinTokenService(); + EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); + + try (Context context = new Context()) { + try { + context.turnOffAuthorisationSystem(); + EPerson ePerson = getEPerson(context, ePersonService, ePersonUUID, email); + if (isCreate) { + if (ePerson == null) { + throw new IllegalArgumentException("Invalid ePerson UUID or email"); + } + Date expirationDate = getExpirationDate(line.getOptionValue("x").toLowerCase()); + createToken(context, clarinTokenService, ePerson, expirationDate); + } else if (isDelete) { + deleteToken(context, clarinTokenService, token, ePerson); + } else { + generateEncryptionKey(); + } + } finally { + context.restoreAuthSystemState(); + context.complete(); + } + } + + } catch (ParseException e) { + System.out.printf("Invalid command options: %s\n", e.getMessage()); + printHelpAndExit(options); + } + + log.info("Clarin Token administrator finished."); + } + + private static void createToken(Context context, + ClarinTokenService clarinTokenService, + EPerson ePerson, + Date expirationDate) throws SQLException, AuthorizeException { + String token = clarinTokenService.createToken(context, ePerson, expirationDate); + log.debug("Clarin Token created: {}", getMaskedToken(token)); + System.out.printf("Clarin Token created: %s\n", token); + System.out.printf("For user: %s, with ID: %s\n", ePerson.getEmail(), ePerson.getID()); + } + + private static void deleteToken(Context context, + ClarinTokenService clarinTokenService, + String token, + EPerson ePerson) throws SQLException, AuthorizeException { + if (token != null) { + clarinTokenService.delete(context, token); + System.out.println("Clarin Token removed."); + } else if (ePerson != null) { + clarinTokenService.delete(context, ePerson); + System.out.println("Clarin Tokens removed."); + System.out.printf("For user: %s, with ID: %s\n", ePerson.getEmail(), ePerson.getID()); + } else { + clarinTokenService.deleteAll(context); + System.out.println("All Clarin Tokens removed"); + } + } + + private static void generateEncryptionKey() throws NoSuchAlgorithmException { + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(EncryptionMethod.A256GCM.cekBitLength()); + SecretKey aesKey = keyGen.generateKey(); + + String encodedAesKey = Base64.getEncoder().encodeToString(aesKey.getEncoded()); + log.debug("Encryption Key generated: {}", getMaskedToken(encodedAesKey)); + System.out.printf("Encryption Key: %s\n", encodedAesKey); + } + + + private static void printHelpAndExit(Options options) { + // print the help message + HelpFormatter myHelp = new HelpFormatter(); + myHelp.printHelp("clarin-token\n", options); + System.exit(0); + } + + private static EPerson getEPerson(Context context, EPersonService ePersonService, UUID ePersonID, String email) + throws SQLException { + return ePersonID == null ? ePersonService.findByEmail(context, email) : ePersonService.find(context, ePersonID); + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/administer/ClarinTokenConfiguration.java b/dspace-api/src/main/java/org/dspace/administer/ClarinTokenConfiguration.java new file mode 100644 index 000000000000..d4eb48c1587c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/administer/ClarinTokenConfiguration.java @@ -0,0 +1,86 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.administer; + +import java.util.List; + +import org.apache.commons.cli.Options; +import org.dspace.core.Context; +import org.dspace.scripts.DSpaceCommandLineParameter; +import org.dspace.scripts.configuration.ScriptConfiguration; + +/** + * This is a ScriptConfiguration for {@link ClarinTokenCreator}. + * + * @author Milan Kuchtiak + */ +public class ClarinTokenConfiguration extends ScriptConfiguration { + + private Class dspaceRunnableClass; + + /** + * Generic getter for the dspaceRunnableClass + * + * @return the dspaceRunnableClass value of this ScriptConfiguration + */ + @Override + public Class getDspaceRunnableClass() { + return dspaceRunnableClass; + } + + /** + * Generic setter for the dspaceRunnableClass + * + * @param dspaceRunnableClass The dspaceRunnableClass to be set on this IndexDiscoveryScriptConfiguration + */ + @Override + public void setDspaceRunnableClass(Class dspaceRunnableClass) { + this.dspaceRunnableClass = dspaceRunnableClass; + } + + /** + * This script is allowed to execute to any authorized user. Further access control mechanism then checks, + * if the current user is authorized to download a file to the item specified in command line parameters. + * + * @param context The relevant DSpace context + * @param commandLineParameters the parameters that will be used to start the process if known, + * null otherwise + * @return A boolean indicating whether the script is allowed to execute or not + */ + @Override + public boolean isAllowedToExecute(Context context, List commandLineParameters) { + return context.getCurrentUser() != null; + } + + /** + * The getter for the options of the Script + * + * @return the options value of this ScriptConfiguration + */ + @Override + public Options getOptions() { + if (options == null) { + + Options options = new Options(); + + options.addOption("h", "help", false, "help"); + + options.addOption("c", "create", false, "create new token"); + options.addOption("d", "delete", false, "delete/deactivate token"); + + options.addOption("x", "expiration", true, + "token expiration in days or hours, e.g. 3d or 48h [required for token create]"); + options.addOption("e", "email", true, + "e-mail to send newly created access token [optional for token create]"); + options.addOption("t", "token", true, "token to delete/deactivate [required for token delete]"); + + super.options = options; + } + return options; + } +} diff --git a/dspace-api/src/main/java/org/dspace/administer/ClarinTokenCreator.java b/dspace-api/src/main/java/org/dspace/administer/ClarinTokenCreator.java new file mode 100644 index 000000000000..3e5f3f89243c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/administer/ClarinTokenCreator.java @@ -0,0 +1,221 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.administer; + +import static org.dspace.content.clarin.ClarinToken.MASKED_TOKEN_SIZE; +import static org.dspace.content.clarin.ClarinToken.UNMASKED_TOKEN_SIZE; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.Date; +import java.util.Locale; +import java.util.UUID; + +import jakarta.mail.MessagingException; +import org.apache.commons.cli.ParseException; +import org.dspace.content.clarin.ClarinToken; +import org.dspace.content.factory.ClarinServiceFactory; +import org.dspace.content.service.clarin.ClarinTokenService; +import org.dspace.core.Context; +import org.dspace.core.Email; +import org.dspace.core.I18nUtil; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.eperson.service.EPersonService; +import org.dspace.scripts.DSpaceRunnable; +import org.dspace.scripts.configuration.ScriptConfiguration; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.utils.DSpace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This script creates or invalidates a clarin token, with given expiration time, for the logged-in user. + * The created token is sent to user by e-mail. + * + * @author Milan Kuchtiak + */ +public class ClarinTokenCreator extends DSpaceRunnable { + + private final static int HOURS_TO_MILLIS = 60 * 60 * 1000; + private final static int DAYS_TO_MILLIS = 24 * HOURS_TO_MILLIS; + + private static final Logger log = LoggerFactory.getLogger(ClarinTokenCreator.class); + private boolean help = false; + private String email; + private Date expirationDate; + private String token; + private ClarinTokenService clarinTokenService; + private EPersonService ePersonService; + + /** + * This method will return the Configuration that the implementing DSpaceRunnable uses + * + * @return The {@link ScriptConfiguration} that this implementing DspaceRunnable uses + */ + @Override + public ClarinTokenConfiguration getScriptConfiguration() { + return new DSpace().getServiceManager().getServiceByName("clarin-token", + ClarinTokenConfiguration.class); + } + + /** + * This method has to be included in every script and handles the setup of the script by parsing the CommandLine + * and setting the variables + * + * @throws ParseException If something goes wrong + */ + @Override + public void setup() throws ParseException { + log.debug("Setting up {}", ClarinTokenCreator.class.getName()); + if (commandLine.hasOption("h") || (!commandLine.hasOption("c") && !commandLine.hasOption("d"))) { + help = true; + return; + } + + if (commandLine.hasOption("c") && commandLine.hasOption("d")) { + throw new ParseException("Select either create or delete option, not both"); + } + + if (commandLine.hasOption("c") && !commandLine.hasOption("x")) { + throw new ParseException("No token expiration time specified"); + } + + if (commandLine.hasOption("d") && !commandLine.hasOption("t")) { + throw new ParseException("No token specified"); + } + + if (commandLine.hasOption("c")) { + expirationDate = getExpirationDate(commandLine.getOptionValue("x").toLowerCase()); + if (commandLine.hasOption("e")) { + email = commandLine.getOptionValue("e"); + } + } else if (commandLine.hasOption("d")) { + token = commandLine.getOptionValue("t"); + } + + clarinTokenService = ClarinServiceFactory.getInstance().getClarinTokenService(); + ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); + + } + + /** + * This method has to be included in every script and this will be the main execution block for the script that'll + * contain all the logic needed + * + * @throws Exception If something goes wrong + */ + @Override + public void internalRun() throws Exception { + log.debug("Running {}", ClarinTokenCreator.class.getName()); + if (help) { + printHelp(); + return; + } + + Context context = new Context(); + EPerson ePerson = getEperson(context); + // ePerson cannot be null here because the script can only be run by an authenticated user + context.setCurrentUser(ePerson); + try { + if (expirationDate != null) { + performCreate(context, ePerson); + } else { + performDelete(context, token); + } + } finally { + context.complete(); + } + + } + + protected void performCreate(Context context, EPerson ePerson) throws Exception { + String token = clarinTokenService.createToken(context, ePerson, expirationDate); + + log.debug("Clarin Token created: {}", getMaskedToken(token)); + + String emailToSend = email != null ? email : ePerson.getEmail(); + + sendEmail(ePerson, emailToSend, token, expirationDate); + + handler.logInfo("Clarin Token created: " + getMaskedToken(token)); + handler.logInfo("Exact token string has been sent to: " + emailToSend); + } + + protected void performDelete(Context context, String token) throws Exception { + clarinTokenService.delete(context, token); + + log.debug("Clarin Token deleted: {}", getMaskedToken(token)); + + handler.logInfo("Clarin Token deleted"); + } + + private EPerson getEperson(Context context) throws SQLException { + UUID ePersonIdentifier = getEpersonIdentifier(); + return ePersonIdentifier == null ? null : ePersonService.find(context, ePersonIdentifier); + } + + static Date getExpirationDate(String expiration) throws ParseException { + if (expiration.length() < 2 || (!expiration.endsWith("d") && !expiration.endsWith("h"))) { + throw new ParseException("Invalid expiration time value"); + } + long expirationTime; + try { + expirationTime = Integer.parseInt(expiration.substring(0, expiration.length() - 1)); + } catch (NumberFormatException e) { + throw new ParseException("Invalid expiration time value"); + } + if (expirationTime < 0) { + throw new ParseException("Invalid expiration time value"); + } + boolean inDays = expiration.endsWith("d"); + boolean inHours = !inDays; + + long maxExpirationTimeInDays = getMaxExpirationTimeInDays(); + if ((inDays && expirationTime > maxExpirationTimeInDays) || + (inHours && expirationTime > maxExpirationTimeInDays * 24)) { + throw new ParseException("The maximum expiration time is " + maxExpirationTimeInDays + " days"); + } + + long currentDate = new Date().getTime(); + + if (inDays) { + return new Date(currentDate + DAYS_TO_MILLIS * expirationTime); + } else { + return new Date(currentDate + HOURS_TO_MILLIS * expirationTime); + } + } + + static long getMaxExpirationTimeInDays() { + ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + return configurationService.getLongProperty(ClarinToken.PROPERTY_MAX_EXPIRATION_TIME_IN_DAYS, 90); + } + + static String getMaskedToken(String token) { + String maskedTokenPart = "*".repeat(MASKED_TOKEN_SIZE); + String unmaskedTokenPart = token.substring(token.length() - UNMASKED_TOKEN_SIZE); + return maskedTokenPart + unmaskedTokenPart; + } + + private static void sendEmail(EPerson ePerson, String to, String token, Date validUntil) + throws IOException, MessagingException { + + // Get a resource bundle according to the ePerson language preferences + Locale supportedLocale = I18nUtil.getEPersonLocale(ePerson); + + Email email = Email.getEmail(I18nUtil.getEmailFilename(supportedLocale, "clarin_token")); + email.addArgument(token); + email.addArgument(ePerson.getFullName()); + email.addArgument(validUntil.toString()); + + email.addRecipient(to); + email.send(); + } +} + diff --git a/dspace-api/src/main/java/org/dspace/administer/ClarinTokenUtils.java b/dspace-api/src/main/java/org/dspace/administer/ClarinTokenUtils.java new file mode 100644 index 000000000000..a25a4148346e --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/administer/ClarinTokenUtils.java @@ -0,0 +1,80 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.administer; + +import static org.dspace.content.clarin.ClarinToken.E_PERSON_ID; + +import java.text.ParseException; +import java.util.Date; +import java.util.Optional; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWEObject; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.MACVerifier; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.jwt.util.DateUtils; +import org.dspace.content.clarin.ClarinToken; + +/** + * Utility methods for Clarin Token. + * + * @author Milan Kuchtiak + */ +public final class ClarinTokenUtils { + + private ClarinTokenUtils() { + } + + public static boolean isClarinToken(String token) throws ParseException { + JWT jwtToken = JWTParser.parse(token); + return ClarinToken.TOKEN_TYPE.equals(jwtToken.getHeader().getType()); + } + + public static Integer getTokenId(String token) throws ParseException { + JWEObject jweObj = JWEObject.parse(token); + try { + return Integer.parseInt(jweObj.getHeader().getKeyID()); + } catch (NumberFormatException e) { + throw new ParseException("Invalid KeyID: not a valid integer", 0); + } + } + + public static SecretKey getSecretKeyFromBase64EncodedString(String encodedSecretKey) { + byte[] decodedKey = java.util.Base64.getDecoder().decode(encodedSecretKey); + return new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES"); + } + + public static boolean isSignedJWTValid(SignedJWT signedJWT, ClarinToken clarinToken) + throws ParseException, JOSEException { + JWSVerifier verifier = new MACVerifier(clarinToken.getSignKey()); + if (signedJWT.verify(verifier) && JWSAlgorithm.HS256.equals(getAlgorithm(signedJWT))) { + JWTClaimsSet jwtClaimsSet = signedJWT.getJWTClaimsSet(); + if (ClarinToken.TOKEN_ISSUER.equals(jwtClaimsSet.getIssuer()) && + clarinToken.getEPerson().getID().toString().equals(jwtClaimsSet.getClaim(E_PERSON_ID))) { + Date expirationTime = jwtClaimsSet.getExpirationTime(); + return expirationTime != null + // Ensure expiration timestamp is after the current time, with zero acceptable clock skew + && DateUtils.isAfter(expirationTime, new Date(), 0); + } + } + return false; + } + + private static JWSAlgorithm getAlgorithm(SignedJWT signedJWT) { + return Optional.ofNullable(signedJWT.getHeader()).map(JWSHeader::getAlgorithm).orElse(null); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/administer/FileDownloader.java b/dspace-api/src/main/java/org/dspace/administer/FileDownloader.java new file mode 100644 index 000000000000..fb592627adef --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/administer/FileDownloader.java @@ -0,0 +1,229 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.administer; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; + +import org.apache.commons.cli.ParseException; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Bitstream; +import org.dspace.content.BitstreamFormat; +import org.dspace.content.Bundle; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.BitstreamFormatService; +import org.dspace.content.service.BitstreamService; +import org.dspace.content.service.ItemService; +import org.dspace.content.service.WorkspaceItemService; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.eperson.service.EPersonService; +import org.dspace.identifier.IdentifierNotFoundException; +import org.dspace.identifier.IdentifierNotResolvableException; +import org.dspace.identifier.factory.IdentifierServiceFactory; +import org.dspace.identifier.service.IdentifierService; +import org.dspace.scripts.DSpaceRunnable; +import org.dspace.scripts.configuration.ScriptConfiguration; +import org.dspace.utils.DSpace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class FileDownloader extends DSpaceRunnable { + + private static final Logger log = LoggerFactory.getLogger(FileDownloader.class); + private boolean help = false; + private UUID itemUUID; + private int workspaceID; + private String pid; + private URI uri; + private String epersonMail; + private String bitstreamName; + private EPersonService epersonService; + private ItemService itemService; + private WorkspaceItemService workspaceItemService; + private IdentifierService identifierService; + private BitstreamService bitstreamService; + private BitstreamFormatService bitstreamFormatService; + private final HttpClient httpClient = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + + /** + * This method will return the Configuration that the implementing DSpaceRunnable uses + * + * @return The {@link ScriptConfiguration} that this implementing DspaceRunnable uses + */ + @Override + public FileDownloaderConfiguration getScriptConfiguration() { + return new DSpace().getServiceManager().getServiceByName("file-downloader", + FileDownloaderConfiguration.class); + } + + /** + * This method has to be included in every script and handles the setup of the script by parsing the CommandLine + * and setting the variables + * + * @throws ParseException If something goes wrong + */ + @Override + public void setup() throws ParseException { + log.debug("Setting up {}", FileDownloader.class.getName()); + if (commandLine.hasOption("h")) { + help = true; + return; + } + + if (!commandLine.hasOption("u")) { + throw new ParseException("No URL option has been provided"); + } + + if (!commandLine.hasOption("i") && !commandLine.hasOption("w") && !commandLine.hasOption("p")) { + throw new ParseException("No item id option has been provided"); + } + + if (getEpersonIdentifier() == null && !commandLine.hasOption("e")) { + throw new ParseException("No eperson option has been provided"); + } + + + this.epersonService = EPersonServiceFactory.getInstance().getEPersonService(); + this.itemService = ContentServiceFactory.getInstance().getItemService(); + this.workspaceItemService = ContentServiceFactory.getInstance().getWorkspaceItemService(); + this.bitstreamService = ContentServiceFactory.getInstance().getBitstreamService(); + this.bitstreamFormatService = ContentServiceFactory.getInstance().getBitstreamFormatService(); + this.identifierService = IdentifierServiceFactory.getInstance().getIdentifierService(); + + try { + uri = new URI(commandLine.getOptionValue("u")); + } catch (URISyntaxException e) { + throw new ParseException("The provided URL is not a valid URL"); + } + + if (commandLine.hasOption("i")) { + itemUUID = UUID.fromString(commandLine.getOptionValue("i")); + } else if (commandLine.hasOption("w")) { + workspaceID = Integer.parseInt(commandLine.getOptionValue("w")); + } else if (commandLine.hasOption("p")) { + pid = commandLine.getOptionValue("p"); + } + + epersonMail = commandLine.getOptionValue("e"); + + if (commandLine.hasOption("n")) { + bitstreamName = commandLine.getOptionValue("n"); + } + } + + /** + * This method has to be included in every script and this will be the main execution block for the script that'll + * contain all the logic needed + * + * @throws Exception If something goes wrong + */ + @Override + public void internalRun() throws Exception { + log.debug("Running {}", FileDownloader.class.getName()); + if (help) { + printHelp(); + return; + } + + Context context = new Context(); + context.setCurrentUser(getEperson(context)); + + //find the item by the given id + Item item = findItem(context); + if (item == null) { + throw new IllegalArgumentException("No item found for the given ID"); + } + + HttpRequest request = HttpRequest.newBuilder() + .uri(uri) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); + + if (response.statusCode() >= 400) { + throw new IllegalArgumentException("The provided URL returned a status code of " + response.statusCode()); + } + + //use the provided value, the content-disposition header, the last part of the uri + if (bitstreamName == null) { + bitstreamName = response.headers().firstValue("Content-Disposition") + .filter(value -> value.contains("filename=")).flatMap(value -> Stream.of(value.split(";")) + .filter(v -> v.contains("filename=")) + .findFirst() + .map(fvalue -> fvalue.replaceFirst("filename=", "").replaceAll("\"", ""))) + .orElse(uri.getPath().substring(uri.getPath().lastIndexOf('/') + 1)); + } + + try (InputStream is = response.body()) { + saveFileToItem(context, item, is, bitstreamName); + } + + context.commit(); + } + + private Item findItem(Context context) throws SQLException { + if (itemUUID != null) { + return itemService.find(context, itemUUID); + } else if (workspaceID != 0) { + return workspaceItemService.find(context, workspaceID).getItem(); + } else { + try { + DSpaceObject dso = identifierService.resolve(context, pid); + if (dso instanceof Item) { + return (Item) dso; + } else { + throw new IllegalArgumentException("The provided identifier does not resolve to an item"); + } + } catch (IdentifierNotFoundException | IdentifierNotResolvableException e) { + throw new IllegalArgumentException(e); + } + } + } + + private void saveFileToItem(Context context, Item item, InputStream is, String name) + throws SQLException, AuthorizeException, IOException { + log.debug("Saving file to item {}", item.getID()); + List originals = item.getBundles("ORIGINAL"); + Bitstream b; + if (originals.isEmpty()) { + b = itemService.createSingleBitstream(context, is, item); + } else { + Bundle bundle = originals.get(0); + b = bitstreamService.create(context, bundle, is); + } + b.setName(context, name); + //now guess format of the bitstream + BitstreamFormat bf = bitstreamFormatService.guessFormat(context, b); + b.setFormat(context, bf); + } + + private EPerson getEperson(Context context) throws SQLException { + if (getEpersonIdentifier() != null) { + return epersonService.find(context, getEpersonIdentifier()); + } else { + return epersonService.findByEmail(context, epersonMail); + } + } +} + diff --git a/dspace-api/src/main/java/org/dspace/administer/FileDownloaderConfiguration.java b/dspace-api/src/main/java/org/dspace/administer/FileDownloaderConfiguration.java new file mode 100644 index 000000000000..5f3447e624fd --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/administer/FileDownloaderConfiguration.java @@ -0,0 +1,91 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.administer; + +import java.util.List; + +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.dspace.core.Context; +import org.dspace.scripts.DSpaceCommandLineParameter; +import org.dspace.scripts.configuration.ScriptConfiguration; + +public class FileDownloaderConfiguration extends ScriptConfiguration { + + private Class dspaceRunnableClass; + + /** + * Generic getter for the dspaceRunnableClass + * + * @return the dspaceRunnableClass value of this ScriptConfiguration + */ + @Override + public Class getDspaceRunnableClass() { + return dspaceRunnableClass; + } + + /** + * Generic setter for the dspaceRunnableClass + * + * @param dspaceRunnableClass The dspaceRunnableClass to be set on this IndexDiscoveryScriptConfiguration + */ + @Override + public void setDspaceRunnableClass(Class dspaceRunnableClass) { + this.dspaceRunnableClass = dspaceRunnableClass; + } + + /** + * This script is allowed to execute to any authorized user. Further access control mechanism then checks, + * if the current user is authorized to download a file to the item specified in command line parameters. + * + * @param context The relevant DSpace context + * @param commandLineParameters the parameters that will be used to start the process if known, + * null otherwise + * @return A boolean indicating whether the script is allowed to execute or not + */ + @Override + public boolean isAllowedToExecute(Context context, List commandLineParameters) { + return context.getCurrentUser() != null; + } + + /** + * The getter for the options of the Script + * + * @return the options value of this ScriptConfiguration + */ + @Override + public Options getOptions() { + if (options == null) { + + Options options = new Options(); + OptionGroup ids = new OptionGroup(); + + options.addOption("h", "help", false, "help"); + + options.addOption("u", "url", true, "source url"); + options.getOption("u").setRequired(true); + + options.addOption("i", "uuid", true, "item uuid"); + options.addOption("w", "wsid", true, "workspace id"); + options.addOption("p", "pid", true, "item pid (e.g. handle or doi)"); + ids.addOption(options.getOption("i")); + ids.addOption(options.getOption("w")); + ids.addOption(options.getOption("p")); + ids.setRequired(true); + + options.addOption("e", "eperson", true, "eperson email"); + options.getOption("e").setRequired(false); + + options.addOption("n", "name", true, "name of the file/bitstream"); + options.getOption("n").setRequired(false); + + super.options = options; + } + return options; + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/configuration/exception/ConfigFileNotAllowedException.java b/dspace-api/src/main/java/org/dspace/app/configuration/exception/ConfigFileNotAllowedException.java new file mode 100644 index 000000000000..15d11faf05b8 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/configuration/exception/ConfigFileNotAllowedException.java @@ -0,0 +1,24 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.configuration.exception; + +/** + * Exception thrown when access to a configuration file is not allowed + * + * @author Milan Majchrak (dspace at dataquest.sk) + */ +public class ConfigFileNotAllowedException extends Exception { + + public ConfigFileNotAllowedException(String message) { + super(message); + } + + public ConfigFileNotAllowedException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/configuration/exception/ConfigFileNotFoundException.java b/dspace-api/src/main/java/org/dspace/app/configuration/exception/ConfigFileNotFoundException.java new file mode 100644 index 000000000000..371e262bfa27 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/configuration/exception/ConfigFileNotFoundException.java @@ -0,0 +1,24 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.configuration.exception; + +/** + * Exception thrown when a requested configuration file is not found + * + * @author Milan Majchrak (dspace at dataquest.sk) + */ +public class ConfigFileNotFoundException extends Exception { + + public ConfigFileNotFoundException(String message) { + super(message); + } + + public ConfigFileNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/configuration/exception/ConfigFileUpdateException.java b/dspace-api/src/main/java/org/dspace/app/configuration/exception/ConfigFileUpdateException.java new file mode 100644 index 000000000000..8fe89d900f68 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/configuration/exception/ConfigFileUpdateException.java @@ -0,0 +1,24 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.configuration.exception; + +/** + * Exception thrown when a configuration file update operation fails + * + * @author Milan Majchrak (dspace at dataquest.sk) + */ +public class ConfigFileUpdateException extends Exception { + + public ConfigFileUpdateException(String message) { + super(message); + } + + public ConfigFileUpdateException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/configuration/service/ConfigFileService.java b/dspace-api/src/main/java/org/dspace/app/configuration/service/ConfigFileService.java new file mode 100644 index 000000000000..6d0d806d320b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/configuration/service/ConfigFileService.java @@ -0,0 +1,139 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.configuration.service; + +import java.io.IOException; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.List; + +import org.dspace.app.configuration.exception.ConfigFileNotAllowedException; +import org.dspace.app.configuration.exception.ConfigFileNotFoundException; +import org.dspace.app.configuration.exception.ConfigFileUpdateException; + +/** + * Service interface for managing DSpace configuration files + * + * This service provides secure access to configuration files with proper + * validation and backup functionality. + * + * @author Milan Majchrak (dspace at dataquest.sk) + */ +public interface ConfigFileService { + + /** + * Get list of configuration files that are allowed to be managed via API + * + * @return List of allowed configuration file names + */ + List getAllowedConfigFiles(); + + /** + * Validate if a file can be accessed via the API + * + * @param fileName Name of the file to validate + * @throws ConfigFileNotFoundException if file doesn't exist + * @throws ConfigFileNotAllowedException if file access is not permitted + */ + void validateFileAccess(String fileName) + throws ConfigFileNotFoundException, ConfigFileNotAllowedException; + + /** + * Read the contents of a configuration file + * + * @param fileName Name of the configuration file + * @return File contents as string + * @throws ConfigFileNotFoundException if file doesn't exist + * @throws ConfigFileNotAllowedException if file access is not permitted + * @throws IOException if file cannot be read + */ + String readConfigFile(String fileName) + throws ConfigFileNotFoundException, ConfigFileNotAllowedException, IOException; + + /** + * Write new contents to a configuration file + * + * Creates an automatic backup before updating the file + * + * @param fileName Name of the configuration file + * @param content New file content + * @throws ConfigFileNotFoundException if file doesn't exist + * @throws ConfigFileNotAllowedException if file access is not permitted + * @throws ConfigFileUpdateException if update operation fails + * @throws IOException if file cannot be written + */ + void writeConfigFile(String fileName, String content) + throws ConfigFileNotFoundException, ConfigFileNotAllowedException, + ConfigFileUpdateException, IOException; + + /** + * Create a backup copy of a configuration file + * + * @param filePath Path to the file to backup + * @return Path to the backup file, or null if no backup was created + * @throws IOException if backup cannot be created + */ + Path createBackup(Path filePath) throws IOException; + + /** + * Get metadata information about a configuration file + * + * @param fileName Name of the configuration file + * @return ConfigFileMetadata object with file information + * @throws ConfigFileNotFoundException if file doesn't exist + * @throws ConfigFileNotAllowedException if file access is not permitted + */ + ConfigFileMetadata getFileMetadata(String fileName) + throws ConfigFileNotFoundException, ConfigFileNotAllowedException; + + /** + * Configuration file metadata container + */ + class ConfigFileMetadata { + private final String name; + private final Path path; + private final Long size; + private final LocalDateTime lastModified; + private final Boolean readable; + private final Boolean writable; + + public ConfigFileMetadata(String name, Path path, Long size, + LocalDateTime lastModified, Boolean readable, Boolean writable) { + this.name = name; + this.path = path; + this.size = size; + this.lastModified = lastModified; + this.readable = readable; + this.writable = writable; + } + + public String getName() { + return name; + } + + public Path getPath() { + return path; + } + + public Long getSize() { + return size; + } + + public LocalDateTime getLastModified() { + return lastModified; + } + + public Boolean getReadable() { + return readable; + } + + public Boolean getWritable() { + return writable; + } + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/configuration/service/ConfigFileServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/configuration/service/ConfigFileServiceImpl.java new file mode 100644 index 000000000000..11b0accc2bb2 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/configuration/service/ConfigFileServiceImpl.java @@ -0,0 +1,247 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.configuration.service; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.app.configuration.exception.ConfigFileNotAllowedException; +import org.dspace.app.configuration.exception.ConfigFileNotFoundException; +import org.dspace.app.configuration.exception.ConfigFileUpdateException; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * Implementation of ConfigFileService for managing DSpace configuration files + * + * This service provides secure access to configuration files with: + * - File access validation based on configuration + * - Automatic backup creation before updates + * - Path traversal attack prevention + * - Proper error handling and logging + * + * @author Milan Majchrak (dspace at dataquest.sk) + */ +@Service +public class ConfigFileServiceImpl implements ConfigFileService { + + private static final Logger log = LogManager.getLogger(ConfigFileServiceImpl.class); + + @Autowired + private ConfigurationService configurationService; + + /** + * Get list of configuration files that are allowed to be managed via API. + * Files are defined in the 'config.admin.updateable.files' configuration property. + */ + @Override + public List getAllowedConfigFiles() { + String[] allowedFiles = configurationService.getArrayProperty("config.admin.updateable.files"); + + if (allowedFiles == null || allowedFiles.length == 0) { + log.warn("No configuration files are allowed for API access. " + + "Configure 'config.admin.updateable.files' property to enable file management."); + return List.of(); + } + + return Arrays.stream(allowedFiles) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toList()); + } + + /** + * Check if a given file is allowed to be accessed via the API + */ + protected boolean isFileAllowed(String fileName) { + List allowedFiles = getAllowedConfigFiles(); + boolean isAllowed = allowedFiles.contains(fileName); + + if (!isAllowed) { + log.debug("File '{}' is not in the list of allowed files: {}", fileName, allowedFiles); + } + + return isAllowed; + } + + /** + * Get the full path to the config directory + */ + protected Path getConfigDirectory() { + String dspaceDir = configurationService.getProperty("dspace.dir"); + if (StringUtils.isBlank(dspaceDir)) { + throw new IllegalStateException("DSpace directory not configured (dspace.dir property is missing)"); + } + + Path configDir = Paths.get(dspaceDir, "config"); + log.debug("Using config directory: {}", configDir); + + return configDir; + } + + /** + * Get the full path to a configuration file + */ + protected Path getConfigFilePath(String fileName) { + return getConfigDirectory().resolve(fileName); + } + + @Override + public void validateFileAccess(String fileName) + throws ConfigFileNotFoundException, ConfigFileNotAllowedException { + + // Check for null or empty filename + if (StringUtils.isBlank(fileName)) { + throw new ConfigFileNotAllowedException("File name cannot be null or empty"); + } + + // Check if file is in allowed list + if (!isFileAllowed(fileName)) { + log.warn("Attempt to access non-allowed configuration file: {}", fileName); + throw new ConfigFileNotAllowedException( + "File '" + fileName + "' is not allowed for API access. " + + "Configure 'config.admin.updateable.files' property to allow this file."); + } + + Path filePath = getConfigFilePath(fileName); + + // Check if file exists + if (!Files.exists(filePath)) { + log.warn("Configuration file not found: {}", filePath); + throw new ConfigFileNotFoundException("Configuration file '" + fileName + "' not found at: " + filePath); + } + + // Security check: ensure the resolved path is still within the config directory + Path configDir = getConfigDirectory().normalize(); + Path normalizedFilePath = filePath.normalize(); + + if (!normalizedFilePath.startsWith(configDir)) { + log.error("Path traversal attack detected! Requested file '{}' resolves to '{}' " + + "which is outside config directory '{}'", fileName, normalizedFilePath, configDir); + throw new ConfigFileNotAllowedException("Invalid file path detected - path traversal attack prevented"); + } + + log.debug("File access validation passed for: {}", fileName); + } + + @Override + public String readConfigFile(String fileName) + throws ConfigFileNotFoundException, ConfigFileNotAllowedException, IOException { + + validateFileAccess(fileName); + Path filePath = getConfigFilePath(fileName); + + try { + String content = Files.readString(filePath); + log.debug("Successfully read configuration file '{}' ({} characters)", fileName, content.length()); + return content; + } catch (IOException e) { + log.error("Failed to read configuration file '{}': {}", fileName, e.getMessage(), e); + throw new IOException("Failed to read configuration file '" + fileName + "': " + e.getMessage(), e); + } + } + + @Override + public void writeConfigFile(String fileName, String content) + throws ConfigFileNotFoundException, ConfigFileNotAllowedException, + ConfigFileUpdateException, IOException { + + validateFileAccess(fileName); + Path filePath = getConfigFilePath(fileName); + + if (content == null) { + throw new IllegalArgumentException("Content cannot be null"); + } + + try { + // Create backup before updating (if file exists) + Path backupPath = null; + if (Files.exists(filePath)) { + backupPath = createBackup(filePath); + log.info("Created backup of '{}' at: {}", fileName, backupPath); + } + + // Write the new content + Files.writeString(filePath, content); + + log.info("Successfully updated configuration file '{}' ({} characters written)", + fileName, content.length()); + + } catch (IOException e) { + log.error("Failed to update configuration file '{}': {}", fileName, e.getMessage(), e); + throw new ConfigFileUpdateException( + "Failed to update configuration file '" + fileName + "': " + e.getMessage(), e); + } + } + + @Override + public Path createBackup(Path filePath) throws IOException { + if (!Files.exists(filePath)) { + log.debug("No backup created - file does not exist: {}", filePath); + return null; + } + + // Create timestamped backup filename + String timestamp = String.valueOf(System.currentTimeMillis()); + String fileName = filePath.getFileName().toString(); + String backupName = fileName + ".backup." + timestamp; + Path backupPath = filePath.getParent().resolve(backupName); + + try { + Files.copy(filePath, backupPath, StandardCopyOption.REPLACE_EXISTING); + log.info("Configuration backup created: {} -> {}", filePath.getFileName(), backupPath.getFileName()); + return backupPath; + + } catch (IOException e) { + log.error("Failed to create backup of '{}': {}", filePath, e.getMessage(), e); + throw new IOException("Failed to create backup of '" + filePath + "': " + e.getMessage(), e); + } + } + + @Override + public ConfigFileMetadata getFileMetadata(String fileName) + throws ConfigFileNotFoundException, ConfigFileNotAllowedException { + + validateFileAccess(fileName); + Path filePath = getConfigFilePath(fileName); + + try { + Long size = Files.size(filePath); + LocalDateTime lastModified = LocalDateTime.ofInstant( + Files.getLastModifiedTime(filePath).toInstant(), + ZoneId.systemDefault()); + Boolean readable = Files.isReadable(filePath); + Boolean writable = Files.isWritable(filePath); + + ConfigFileMetadata metadata = new ConfigFileMetadata( + fileName, filePath, size, lastModified, readable, writable); + + log.debug("Retrieved metadata for '{}': size={}, readable={}, writable={}", + fileName, size, readable, writable); + + return metadata; + + } catch (IOException e) { + log.error("Error reading metadata for file '{}': {}", fileName, e.getMessage(), e); + throw new ConfigFileNotFoundException( + "Error reading metadata for file '" + fileName + "': " + e.getMessage(), e); + } + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/itemupdate/ItemFilesMetadataRepair.java b/dspace-api/src/main/java/org/dspace/app/itemupdate/ItemFilesMetadataRepair.java new file mode 100644 index 000000000000..970e8440ccc1 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/itemupdate/ItemFilesMetadataRepair.java @@ -0,0 +1,257 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.itemupdate; + +import java.util.Iterator; +import java.util.List; +import java.util.UUID; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.content.Bundle; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.factory.ClarinServiceFactory; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.CollectionService; +import org.dspace.content.service.ItemService; +import org.dspace.content.service.clarin.ClarinItemService; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.factory.EPersonServiceFactory; + +/** + * Documentation for this class: https://github.com/ufal/clarin-dspace/pull/1243#issue-3236707035 + */ +public class ItemFilesMetadataRepair { + + private static final Logger log = LogManager.getLogger(ItemFilesMetadataRepair.class); + + private ItemFilesMetadataRepair() { + } + + public static void main(String[] args) throws Exception { + log.info("Fixing item files metadata started."); + + Options options = new Options(); + options.addRequiredOption("e", "email", true, "admin email"); + options.addOption("c", "collection", true, "collection UUID"); + options.addOption("i", "item", true, "item UUID"); + options.addOption("d", "dry-run", false, "dry run - with no repair"); + options.addOption("h", "help", false, "help"); + options.addOption("v", "verbose", false, "verbose output"); + + CommandLineParser parser = new DefaultParser(); + try { + CommandLine line = parser.parse(options, args); + if (line.hasOption('h') || !line.hasOption('e')) { + printHelpAndExit(options); + } + String adminEmail = line.getOptionValue('e'); + String collectionUuid = line.getOptionValue('c'); + String itemUuid = line.getOptionValue('i'); + boolean verboseOutput = line.hasOption('v'); + boolean dryRun = line.hasOption('d'); + run(adminEmail, collectionUuid, itemUuid, dryRun, verboseOutput); + } catch (ParseException e) { + System.err.println("Failed to parse command line options: " + e.getMessage()); + printHelpAndExit(options); + } + + log.info("Fixing item files metadata finished."); + } + + private static void run(String adminEmail, + String collectionUuid, + String itemUuid, + boolean dryRun, + boolean verboseOutput) throws Exception { + + System.out.println("ItemFilesMetadataRepair Started.\n"); + + try (Context context = new Context(Context.Mode.READ_WRITE)) { + ItemService itemService = ContentServiceFactory.getInstance().getItemService(); + ClarinItemService clarinItemService = ClarinServiceFactory.getInstance().getClarinItemService(); + + EPerson eperson = EPersonServiceFactory.getInstance().getEPersonService().findByEmail(context, adminEmail); + context.turnOffAuthorisationSystem(); + context.setCurrentUser(eperson); + context.restoreAuthSystemState(); + + String messagePrefix = dryRun ? "Found incorrect files metadata in" : "Updated"; + if (itemUuid != null) { + // fixing only one item + Item item = itemService.find(context, UUID.fromString(itemUuid)); + if (item == null) { + throw new IllegalArgumentException("Item not found with the provided UUID"); + } + boolean updated = updateItem(item, context, clarinItemService, itemService, dryRun, verboseOutput); + if (updated) { + System.out.println(dryRun ? "Files metadata are incorrect." : "Files metadata were updated."); + } else { + System.out.println("Files metadata are correct."); + } + } else if (collectionUuid != null) { + // fixing items in collection + CollectionService collectionService = ContentServiceFactory.getInstance().getCollectionService(); + Collection collection = collectionService.find(context, UUID.fromString(collectionUuid)); + if (collection == null) { + throw new IllegalArgumentException("Invalid Collection UUID"); + } + Iterator itemIterator = itemService.findAllByCollection(context, collection); + Results results = + updateItems(itemIterator, context, clarinItemService, itemService, dryRun, verboseOutput); + System.out.printf("Checked %d items in Collection: \"%s\".\n", + results.getItemsCount(), collection.getName()); + System.out.printf("%s %d items.\n", messagePrefix, results.getUpdatedItemsCount()); + } else { + // fixing all items + Iterator itemIterator = itemService.findAll(context); + Results results = + updateItems(itemIterator, context, clarinItemService, itemService, dryRun, verboseOutput); + System.out.printf("Checked %d items.\n", results.getItemsCount()); + System.out.printf("%s %d items.\n", messagePrefix, results.getUpdatedItemsCount()); + } + context.complete(); + } + + System.out.println("\nItemFilesMetadataRepair Finished"); + } + + private static Results updateItems(Iterator itemIterator, + Context context, + ClarinItemService clarinItemService, + ItemService itemService, + boolean dryRun, + boolean verboseOutput) throws Exception { + int itemsCount = 0; + int updatedItemsCount = 0; + while (itemIterator.hasNext()) { + itemsCount++; + boolean updated = + updateItem(itemIterator.next(), context, clarinItemService, itemService, dryRun, verboseOutput); + if (updated) { + updatedItemsCount++; + } + } + + return new Results(itemsCount, updatedItemsCount); + } + + private static boolean updateItem(Item item, + Context context, + ClarinItemService clarinItemService, + ItemService itemService, + boolean dryRun, + boolean verboseOutput) throws Exception { + boolean updated = false; + + List filesCountValues = + itemService.getMetadata(item, "local", "files", "count", Item.ANY); + List filesSizeValues = + itemService.getMetadata(item, "local", "files", "size", Item.ANY); + List hasFilesValues = + itemService.getMetadata(item, "local", "has", "files", Item.ANY); + + int filesCount = 0; + String filesCountValue = "undefined"; + if (!filesCountValues.isEmpty()) { + filesCountValue = filesCountValues.get(0).getValue(); + try { + filesCount = Integer.parseInt(filesCountValue); + } catch (NumberFormatException ex) { + // filesCount = 0 + } + } + long filesSize = 0; + String filesSizeValue = "undefined"; + if (!filesSizeValues.isEmpty()) { + filesSizeValue = filesSizeValues.get(0).getValue(); + try { + filesSize = Long.parseLong(filesSizeValue); + } catch (NumberFormatException ex) { + // filesSize = 0 + } + } + String hasFiles = hasFilesValues.isEmpty() ? "no" : hasFilesValues.get(0).getValue(); + + List originalBundles = item.getBundles(Constants.CONTENT_BUNDLE_NAME); + if (!CollectionUtils.isEmpty(originalBundles)) { + Bundle bundle = originalBundles.get(0); + boolean hasBitstreams = !CollectionUtils.isEmpty(bundle.getBitstreams()); + if (hasBitstreams && (filesCount == 0 || filesSize == 0 || !"yes".equals(hasFiles))) { + if (verboseOutput) { + String message = "Incorrect metadata: [files.count: %s, files.size: %s, has.files: %s], " + + "in item '%s' with files."; + System.out.printf((message) + "%n", filesCountValue, filesSizeValue, hasFiles, item.getHandle()); + } + if (!dryRun) { + clarinItemService.updateItemFilesMetadata(context, item, bundle); + } + updated = true; + } else if (!hasBitstreams && (filesCount > 0 || filesSize > 0 || "yes".equals(hasFiles))) { + if (verboseOutput) { + String message = "Incorrect metadata: [files.count: %s, files.size: %s, has.files: %s], " + + "in item '%s' without files."; + System.out.printf((message) + "%n", filesCountValue, filesSizeValue, hasFiles, item.getHandle()); + } + if (!dryRun) { + itemService.clearMetadata( + context, item, "local", "has", "files", Item.ANY); + itemService.clearMetadata( + context, item, "local", "files", "count", Item.ANY); + itemService.clearMetadata( + context, item, "local", "files", "size", Item.ANY); + itemService.addMetadata( + context, item, "local", "has", "files", Item.ANY, "no"); + itemService.addMetadata( + context, item, "local", "files", "count", Item.ANY, "0"); + itemService.addMetadata( + context, item, "local", "files", "size", Item.ANY, "0"); + } + updated = true; + } + } + return updated; + } + + private static void printHelpAndExit(Options options) { + // print the help message + HelpFormatter myHelp = new HelpFormatter(); + myHelp.printHelp("dsrun org.dspace.app.itemupdate.ItemFilesMetadataRepair \n", options); + System.exit(0); + } + + private static class Results { + private final int itemsCount; + private final int updatedItemsCount; + + public Results(int itemsCount, int updatedItemsCount) { + this.itemsCount = itemsCount; + this.updatedItemsCount = updatedItemsCount; + } + + public int getItemsCount() { + return itemsCount; + } + + public int getUpdatedItemsCount() { + return updatedItemsCount; + } + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/util/ACE.java b/dspace-api/src/main/java/org/dspace/app/util/ACE.java new file mode 100644 index 000000000000..9627b213dea3 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/util/ACE.java @@ -0,0 +1,170 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.util; + +import java.util.Set; + +import org.apache.logging.log4j.Logger; + +/** + * Class that represents single Access Control Entry + * + * @author Michal Josífko + * Class is copied from the LINDAT/CLARIAH-CZ (https://github.com/ufal/clarin-dspace) and modified by + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ + +public class ACE { + + /** Logger */ + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(ACE.class); + private static final String POLICY_KEYWORD = "policy"; + private static final String POLICY_DENY_KEYWORD = "deny"; + private static final String POLICY_ALLOW_KEYWORD = "allow"; + private static final String ACTION_KEYWORD = "action"; + private static final String ACTION_READ_KEYWORD = "read"; + private static final String ACTION_WRITE_KEYWORD = "write"; + private static final String GRANTEE_TYPE_KEYWORD = "grantee-type"; + private static final String GRANTEE_TYPE_USER_KEYWORD = "user"; + private static final String GRANTEE_TYPE_GROUP_KEYWORD = "group"; + private static final String GRANTEE_ID_KEYWORD = "grantee-id"; + private static final String ANY_KEYWORD = "*"; + public static final int ACTION_READ = 1; + public static final int ACTION_WRITE = 2; + private static final int POLICY_DENY = 1; + private static final int POLICY_ALLOW = 2; + private static final int GRANTEE_TYPE_USER = 1; + private static final int GRANTEE_TYPE_GROUP = 2; + private static final String GRANTEE_ID_ANY = "-1"; + private int policy; + private int action; + private int granteeType; + private String granteeID; + + /** + * Creates new ACE object from given String + * + * @param aceDefinition from the acl definition string + * @return ACE object or null + */ + public static ACE fromString(String aceDefinition) { + ACE ace = null; + String[] aceParts = aceDefinition.split(","); + + int errors = 0; + + int policy = 0; + int action = 0; + int granteeType = 0; + String granteeID = ""; + + for (int i = 0; i < aceParts.length; i++) { + String acePart = aceParts[i]; + String keyValue[] = acePart.split("="); + + if (keyValue.length != 2) { + log.error("Invalid ACE format: " + acePart); + errors++; + continue; + } + + String key = keyValue[0].trim(); + String value = keyValue[1].trim(); + + if (key.equals(POLICY_KEYWORD)) { + if (value.equals(POLICY_DENY_KEYWORD)) { + policy = POLICY_DENY; + } else if (value.equals(POLICY_ALLOW_KEYWORD)) { + policy = POLICY_ALLOW; + } else { + log.error("Invalid ACE policy value: " + value); + errors++; + } + } else if (key.equals(ACTION_KEYWORD)) { + if (value.equals(ACTION_READ_KEYWORD)) { + action = ACTION_READ; + } else if (value.equals(ACTION_WRITE_KEYWORD)) { + action = ACTION_WRITE; + } else { + log.error("Invalid ACE action value: " + value); + errors++; + } + } else if (key.equals(GRANTEE_TYPE_KEYWORD)) { + if (value.equals(GRANTEE_TYPE_USER_KEYWORD)) { + granteeType = GRANTEE_TYPE_USER; + } else if (value.equals(GRANTEE_TYPE_GROUP_KEYWORD)) { + granteeType = GRANTEE_TYPE_GROUP; + } else { + log.error("Invalid ACE grantee type value: " + value); + errors++; + } + } else if (key.equals(GRANTEE_ID_KEYWORD)) { + if (value.equals(ANY_KEYWORD)) { + granteeID = GRANTEE_ID_ANY; + } else { + granteeID = value; + } + } else { + log.error("Invalid ACE keyword: " + key); + errors++; + } + } + if (errors == 0) { + ace = new ACE(policy, action, granteeType, granteeID); + } + return ace; + } + + /** + * Constructor for creating new Access Control Entry + * + * @param policy deny/allow + * @param action read/write + * @param granteeType user/group + * @param granteeID group UUID + */ + private ACE(int policy, int action, int granteeType, String granteeID) { + this.policy = policy; + this.action = action; + this.granteeType = granteeType; + this.granteeID = granteeID; + } + + /** + * Method that checks whether the given inputs match this Access Control Entry + * + * @param userID of the current user + * @param groupIDs where is assigned the current user + * @param action could/couldn't be allowed for the user + * @return + */ + public boolean matches(String userID, Set groupIDs, int action) { + if (this.action == action) { + if (granteeType == ACE.GRANTEE_TYPE_USER) { + if (granteeID.equals(GRANTEE_ID_ANY) || userID.equals(granteeID)) { + return true; + } + } else if (granteeType == ACE.GRANTEE_TYPE_GROUP) { + if (granteeID.equals(GRANTEE_ID_ANY) || groupIDs.contains(granteeID)) { + return true; + } + } + } + return false; + } + + /** + * Convenience method to verify if this entry is allowing the action; + * + * @return the action is allowed or not + */ + public boolean isAllowed() { + return policy == ACE.POLICY_ALLOW; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/util/ACL.java b/dspace-api/src/main/java/org/dspace/app/util/ACL.java new file mode 100644 index 000000000000..625147c20acd --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/util/ACL.java @@ -0,0 +1,142 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.util; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.logging.log4j.Logger; +import org.dspace.authorize.factory.AuthorizeServiceFactory; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Group; +import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.eperson.service.GroupService; + +/** + * Class that represents Access Control List + * + * @author Michal Josífko + * Class is copied from the LINDAT/CLARIAH-CZ (https://github.com/ufal/clarin-dspace) and modified by + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class ACL { + + /** Logger */ + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(ACL.class); + public static final int ACTION_READ = ACE.ACTION_READ; + public static final int ACTION_WRITE = ACE.ACTION_WRITE; + /** + * List of single Access Control Entry + */ + private List acl; + protected AuthorizeService authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService(); + protected GroupService groupService = EPersonServiceFactory.getInstance().getGroupService(); + + /** + * Creates new ACL object from given String + * + * @param aclDefinition of the field from the form definition file + * @return ACL object + */ + public static ACL fromString(String aclDefinition) { + List acl = new ArrayList(); + if (aclDefinition != null) { + String[] aclEntries = aclDefinition.split(";"); + for (int i = 0; i < aclEntries.length; i++) { + String aclEntry = aclEntries[i]; + ACE ace = ACE.fromString(aclEntry); + if (ace != null) { + acl.add(ace); + } + } + } + return new ACL(acl); + } + + /** + * Constructor for creating new Access Control List + * + * @param acl List of ACE + */ + ACL(List acl) { + this.acl = acl; + } + + /** + * Method to verify whether the the given user ID and set of group IDs is + * allowed to perform the given action + * + * @param userID current user + * @param groupIDs where is assigned the current user + * @param action read/write + * @return if user will see the input field + */ + private boolean isAllowedAction(String userID, Set groupIDs, int action) { + for (ACE ace : acl) { + if (ace.matches(userID, groupIDs, action)) { + return ace.isAllowed(); + } + } + return false; + } + + /** + * Convenience method to verify whether the current user is allowed to + * perform given action based on current context + * + * @param c Current context, the user information are loaded from the context + * @param action read/write + * @return if user will see the input field + * @throws SQLException + */ + public boolean isAllowedAction(Context c, int action) { + boolean res = false; + if (acl.isEmpty()) { + // To maintain backwards compatibility allow everything if the ACL + // is empty + return true; + } + try { + if (authorizeService.isAdmin(c)) { + // Admin is always allowed + return true; + } else { + EPerson e = c.getCurrentUser(); + if (e != null) { + UUID userID = e.getID(); + List groups = groupService.allMemberGroups(c, c.getCurrentUser()); + + Set groupIDs = groups.stream().flatMap(group -> Stream.of(group.getID().toString())) + .collect(Collectors.toSet()); + + return isAllowedAction(userID.toString(), groupIDs, action); + } + } + } catch (SQLException e) { + log.error(e); + } + return res; + } + + /** + * Returns true is the ACL is empty set of rules + * + * @return contains some ACE elements + */ + public boolean isEmpty() { + return acl.isEmpty(); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/util/Util.java b/dspace-api/src/main/java/org/dspace/app/util/Util.java index 3a0c368880e4..fa7566fcb5a9 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/Util.java +++ b/dspace-api/src/main/java/org/dspace/app/util/Util.java @@ -514,4 +514,19 @@ public static List differenceInSubmissionFields(Collection fromCollectio return ListUtils.removeAll(fromFieldName, toFieldName); } + + /** + * Format the netId with the IdP/organization, e.g. {@code netId[organization]}. + * Ported from CLARIN-DSpace (used by Shibboleth authentication). + * + * @param netId the user's netId from the IdP + * @param organization the IdP/organization identifier + * @return formatted netId, or {@code null} if netId is blank + */ + public static String formatNetId(String netId, String organization) { + if (StringUtils.isBlank(netId)) { + return null; + } + return netId + "[" + organization + "]"; + } } diff --git a/dspace-api/src/main/java/org/dspace/authenticate/clarin/ClarinShibAuthentication.java b/dspace-api/src/main/java/org/dspace/authenticate/clarin/ClarinShibAuthentication.java new file mode 100644 index 000000000000..aeaf4c6e3a52 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/authenticate/clarin/ClarinShibAuthentication.java @@ -0,0 +1,1384 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.authenticate.clarin; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.authenticate.AuthenticationMethod; +import org.dspace.authenticate.factory.AuthenticateServiceFactory; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataFieldName; +import org.dspace.content.MetadataSchema; +import org.dspace.content.MetadataSchemaEnum; +import org.dspace.content.NonUniqueMetadataException; +import org.dspace.content.clarin.ClarinUserRegistration; +import org.dspace.content.clarin.ClarinVerificationToken; +import org.dspace.content.factory.ClarinServiceFactory; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.MetadataFieldService; +import org.dspace.content.service.MetadataSchemaService; +import org.dspace.content.service.clarin.ClarinUserRegistrationService; +import org.dspace.content.service.clarin.ClarinVerificationTokenService; +import org.dspace.core.Context; +import org.dspace.core.Utils; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Group; +import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.eperson.service.EPersonService; +import org.dspace.eperson.service.GroupService; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; + +/** + * Shibboleth authentication for CLARIN-DSpace + * + * This class is customized ShibAuthentication class. + * + * Shibboleth is a distributed authentication system for securely authenticating + * users and passing attributes about the user from one or more identity + * providers. In the Shibboleth terminology DSpace is a Service Provider which + * receives authentication information and then based upon that provides a + * service to the user. With Shibboleth DSpace will require that you use + * Apache installed with the mod_shib module acting as a proxy for all HTTP + * requests for your servlet container (typically Tomcat). DSpace will receive + * authentication information from the mod_shib module through HTTP headers. + * + * See for more information on installing and configuring a Shibboleth + * Service Provider: + * https://wiki.shibboleth.net/confluence/display/SHIB2/Installation + * + * See the DSpace.cfg or DSpace manual for information on how to configure + * this authentication module. + * + * @author Bruc Liong, MELCOE + * @author Xiang Kevin Li, MELCOE + * @author Scott Phillips + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class ClarinShibAuthentication implements AuthenticationMethod { + /** + * log4j category + */ + private static final Logger log = LogManager.getLogger(ClarinShibAuthentication.class); + + // If the user which are in the login process has email already associated with a different users email. + private boolean isDuplicateUser = false; + + /** + * Additional metadata mappings + **/ + protected Map metadataHeaderMap = null; + + /** + * Shibboleth headers retrieved from the request headers (standard auth) or request attribute (verification token). + */ + ShibHeaders shibheaders; + + /** + * The class with user email and shib headers. + */ + ClarinVerificationToken clarinVerificationToken; + + /** + * Maximum length for eperson metadata fields + **/ + protected final int NAME_MAX_SIZE = 64; + protected final int PHONE_MAX_SIZE = 32; + + /** + * Maximum length for eperson additional metadata fields + **/ + protected final int METADATA_MAX_SIZE = 1024; + + protected EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); + protected GroupService groupService = EPersonServiceFactory.getInstance().getGroupService(); + protected MetadataFieldService metadataFieldService = ContentServiceFactory.getInstance().getMetadataFieldService(); + protected MetadataSchemaService metadataSchemaService = ContentServiceFactory.getInstance() + .getMetadataSchemaService(); + protected ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + protected ClarinUserRegistrationService clarinUserRegistrationService = + ClarinServiceFactory.getInstance().getClarinUserRegistration(); + protected ClarinVerificationTokenService clarinVerificationTokenService = ClarinServiceFactory.getInstance() + .getClarinVerificationTokenService(); + + /** + * Authenticate the given or implicit credentials. This is the heart of the + * authentication method: test the credentials for authenticity, and if + * accepted, attempt to match (or optionally, create) an + * EPerson. If an EPerson is found it is set in + * the Context that was passed. + * + * DSpace supports authentication using NetID, or email address. A user's NetID + * is a unique identifier from the IdP that identifies a particular user. The + * NetID can be of almost any form such as a unique integer, string, or with + * Shibboleth 2.0 you can use "targeted ids". You will need to coordinate with + * your Shibboleth federation or identity provider. There are three ways to + * supply identity information to DSpace: + * + * 1) NetID from Shibboleth Header (best) + * + * The NetID-based method is superior because users may change their email + * address with the identity provider. When this happens DSpace will not be + * able to associate their new address with their old account. + * + * 2) Email address from Shibboleth Header (okay) + * + * In the case where a NetID header is not available or not found DSpace + * will fall back to identifying a user based-upon their email address. + * + * 3) Tomcat's Remote User (worst) + * + * In the event that neither Shibboleth headers are found then as a last + * resort DSpace will look at Tomcat's remote user field. This is the least + * attractive option because Tomcat has no way to supply additional + * attributes about a user. Because of this the autoregister option is not + * supported if this method is used. + * + * Identity Scheme Migration Strategies: + * + * If you are currently using Email based authentication (either 1 or 2) and + * want to upgrade to NetID based authentication then there is an easy path. + * Simply enable Shibboleth to pass the NetID attribute and set the netid-header + * below to the correct value. When a user attempts to log in to DSpace first + * DSpace will look for an EPerson with the passed NetID, however when this + * fails DSpace will fall back to email based authentication. Then DSpace will + * update the user's EPerson account record to set their netid so all future + * authentications for this user will be based upon netid. One thing to note + * is that DSpace will prevent an account from switching NetIDs. If an account + * already has a NetID set and then they try and authenticate with a + * different NetID the authentication will fail. + * + * @param context DSpace context, will be modified (ePerson set) upon success. + * @param username Username (or email address) when method is explicit. Use null + * for implicit method. + * @param password Password for explicit auth, or null for implicit method. + * @param realm Not used by Shibboleth-based authentication + * @param request The HTTP request that started this operation, or null if not + * applicable. + * @return One of: SUCCESS, BAD_CREDENTIALS, CERT_REQUIRED, NO_SUCH_USER, + * BAD_ARGS + *

+ * Meaning:
+ * SUCCESS - authenticated OK.
+ * BAD_CREDENTIALS - user exists, but credentials (e.g. passwd) + * don't match
+ * CERT_REQUIRED - not allowed to login this way without X.509 cert. + *
+ * NO_SUCH_USER - user not found using this method.
+ * BAD_ARGS - user/pw not appropriate for this method + * @throws SQLException if database error + */ + @Override + public int authenticate(Context context, String username, String password, + String realm, HttpServletRequest request) throws SQLException { + // Check if sword compatibility is allowed, and if so see if we can + // authenticate based upon a username and password. This is really helpful + // if your repo uses Shibboleth but you want some accounts to be able use + // sword. This allows this compatibility without installing the password-based + // authentication method which has side effects such as allowing users to login + // with a username and password from the webui. + boolean swordCompatibility = configurationService + .getBooleanProperty("authentication-shibboleth.sword.compatibility", true); + if (swordCompatibility && + username != null && username.length() > 0 && + password != null && password.length() > 0) { + return swordCompatibility(context, username, password, request); + } + + if (request == null) { + log.warn("Unable to authenticate using Shibboleth because the request object is null."); + return BAD_ARGS; + } + // CLARIN + // Log all headers received if debugging is turned on. This is enormously + // helpful when debugging shibboleth related problems. + if (log.isDebugEnabled()) { + log.debug("Starting Shibboleth Authentication"); + } + + // Shib headers could be loaded from the request header or request attribute. The shib headers are in the + // request attribute only if the user is trying to authenticate by `verification token`. + String shibHeadersAttr = (String) request.getAttribute("shib.headers"); + if (StringUtils.isNotEmpty(shibHeadersAttr)) { + shibheaders = new ShibHeaders(shibHeadersAttr); + } else { + shibheaders = new ShibHeaders(request); + } + shibheaders.log_headers(); + + String organization = shibheaders.get_idp(); + if (organization == null) { + log.info("Exiting shibboleth authenticate because no idp set"); + return BAD_ARGS; + } + + // The user e-mail is not stored in the `shibheaders` but in the `clarinVerificationToken`. + // The email was added to the `clarinVerificationToken` in the ClarinShibbolethFilter. + String[] netidHeaders = configurationService.getArrayProperty("authentication-shibboleth.netid-header"); + + // Load the verification token from the request header or from the request parameter. + // This is only set if the user is trying to authenticate with the `verification-token`. + String VERIFICATION_TOKEN = "verification-token"; + String verificationTokenFromRequest = StringUtils.defaultIfBlank(request.getHeader(VERIFICATION_TOKEN), + request.getParameter(VERIFICATION_TOKEN)); + if (StringUtils.isNotEmpty(verificationTokenFromRequest)) { + log.info("Verification token from request header `{}`: {}", VERIFICATION_TOKEN, + verificationTokenFromRequest); + clarinVerificationToken = clarinVerificationTokenService.findByToken(context, verificationTokenFromRequest); + } + // CLARIN + + // Initialize the additional EPerson metadata. + initialize(context); + + // Should we auto register new users. + boolean autoRegister = configurationService.getBooleanProperty("authentication-shibboleth.autoregister", true); + + // Four steps to authenticate a user + try { + // Step 1: Identify User + EPerson eperson = findEPerson(context, request, netidHeaders); + + // Step 2: Register New User, if necessary + if (eperson == null && autoRegister && !isDuplicateUser) { + eperson = registerNewEPerson(context, request, netidHeaders); + } + + if (eperson == null) { + return AuthenticationMethod.NO_SUCH_USER; + } + + // Step 3: Update User's Metadata + updateEPerson(context, request, eperson, netidHeaders); + + // Step 4: Log the user in. + context.setCurrentUser(eperson); + request.getSession().setAttribute("shib.authenticated", true); + AuthenticateServiceFactory.getInstance().getAuthenticationService().initEPerson(context, request, eperson); + + log.info(eperson.getEmail() + " has been authenticated via shibboleth."); + return AuthenticationMethod.SUCCESS; + } catch (Throwable t) { + // Log the error, and undo the authentication before returning a failure. + log.error("Unable to successfully authenticate using shibboleth for user because of " + + "an exception.", t); + context.setCurrentUser(null); + return AuthenticationMethod.NO_SUCH_USER; + } + } + + /** + * Get list of extra groups that user implicitly belongs to. Note that this + * method will be invoked regardless of the authentication status of the + * user (logged-in or not) e.g. a group that depends on the client + * network-address. + * + * DSpace is able to place users into pre-defined groups based upon values + * received from Shibboleth. Using this option you can place all faculty members + * into a DSpace group when the correct affiliation's attribute is provided. + * When DSpace does this they are considered 'special groups', these are really + * groups but the user's membership within these groups is not recorded in the + * database. Each time a user authenticates they are automatically placed within + * the pre-defined DSpace group, so if the user loses their affiliation then the + * next time they login they will no longer be in the group. + * + * Depending upon the shibboleth attributed use in the role-header, it may be + * scoped. Scoped is shibboleth terminology for identifying where an attribute + * originated from. For example a students affiliation may be encoded as + * "student@tamu.edu". The part after the @ sign is the scope, and the preceding + * value is the value. You may use the whole value or only the value or scope. + * Using this you could generate a role for students and one institution + * different than students at another institution. Or if you turn on + * ignore-scope you could ignore the institution and place all students into + * one group. + * + * The values extracted (a user may have multiple roles) will be used to look + * up which groups to place the user into. The groups are defined as + * {@code authentication.shib.role.} which is a comma separated list of + * DSpace groups. + * + * @param context A valid DSpace context. + * @param request The request that started this operation, or null if not + * applicable. + * @return array of EPerson-group IDs, possibly 0-length, but never + * null. + */ + @Override + public List getSpecialGroups(Context context, HttpServletRequest request) { + try { + // User has not successfuly authenticated via shibboleth. + if (request == null || + context.getCurrentUser() == null || + request.getSession().getAttribute("shib.authenticated") == null) { + return Collections.EMPTY_LIST; + } + + // If we have already calculated the special groups then return them. + if (request.getSession().getAttribute("shib.specialgroup") != null) { + log.debug("Returning cached special groups."); + List sessionGroupIds = (List) request.getSession().getAttribute("shib.specialgroup"); + List result = new ArrayList<>(); + for (UUID uuid : sessionGroupIds) { + result.add(groupService.find(context, uuid)); + } + return result; + } + + + List groupIds = new ShibGroup(new ShibHeaders(request), context).get(); + // Cache the special groups, so we don't have to recalculate them again + // for this session. + request.getSession().setAttribute("shib.specialgroup", groupIds); + + List groups = new ArrayList<>(); + for (UUID uuid : groupIds) { + Group foundGroup = groupService.find(context, uuid); + if (Objects.isNull(foundGroup)) { + continue; + } + groups.add(foundGroup); + } + return groups; + } catch (Throwable t) { + log.error("Unable to validate any sepcial groups this user may belong too because of an exception.", t); + return Collections.EMPTY_LIST; + } + } + + + /** + * Indicate whether or not a particular self-registering user can set + * themselves a password in the profile info form. + * + * @param context DSpace context + * @param request HTTP request, in case anything in that is used to decide + * @param email e-mail address of user attempting to register + * @throws SQLException if database error + */ + @Override + public boolean allowSetPassword(Context context, + HttpServletRequest request, String email) throws SQLException { + // don't use password at all + return false; + } + + /** + * Predicate, is this an implicit authentication method. An implicit method + * gets credentials from the environment (such as an HTTP request or even + * Java system properties) rather than the explicit username and password. + * For example, a method that reads the X.509 certificates in an HTTPS + * request is implicit. + * + * @return true if this method uses implicit authentication. + */ + @Override + public boolean isImplicit() { + return false; + } + + /** + * Indicate whether or not a particular user can self-register, based on + * e-mail address. + * + * @param context DSpace context + * @param request HTTP request, in case anything in that is used to decide + * @param username e-mail address of user attempting to register + * @throws SQLException if database error + */ + @Override + public boolean canSelfRegister(Context context, HttpServletRequest request, + String username) throws SQLException { + + // Shibboleth will auto create accounts if configured to do so, but that is not + // the same as self register. Self register means that the user can sign up for + // an account from the web. This is not supported with shibboleth. + return false; + } + + /** + * Initialize a new e-person record for a self-registered new user. + * + * @param context DSpace context + * @param request HTTP request, in case it's needed + * @param eperson newly created EPerson record - email + information from the + * registration form will have been filled out. + * @throws SQLException if database error + */ + @Override + public void initEPerson(Context context, HttpServletRequest request, + EPerson eperson) throws SQLException { + // We don't do anything because all our work is done authenticate and special groups. + } + + /** + * Get login page to which to redirect. Returns URL (as string) to which to + * redirect to obtain credentials (either password prompt or e.g. HTTPS port + * for client cert.); null means no redirect. + *

+ * For Shibboleth, this URL looks like (note 'target' param is URL encoded, but shown as unencoded in this example) + * [shibURL]?target=[dspace.server.url]/api/authn/shibboleth?redirectUrl=[dspace.ui.url] + *

+ * This URL is used by the client to redirect directly to Shibboleth for authentication. The "target" param + * is then the location (in REST API) where Shibboleth redirects back to. The "redirectUrl" is the path/URL in the + * client (e.g. Angular UI) which the REST API redirects the user to (after capturing/storing any auth info from + * Shibboleth). + * @param context DSpace context, will be modified (ePerson set) upon success. + * @param request The HTTP request that started this operation, or null if not + * applicable. + * @param response The HTTP response from the servlet method. + * @return fully-qualified URL or null + */ + @Override + public String loginPageURL(Context context, HttpServletRequest request, HttpServletResponse response) { + // If this server is configured for lazy sessions then use this to + // login, otherwise default to the protected shibboleth url. + + boolean lazySession = configurationService.getBooleanProperty("authentication-shibboleth.lazysession", false); + + if ( lazySession ) { + String shibURL = getShibURL(request); + + // Determine the client redirect URL, where to redirect after authenticating. + String redirectUrl = null; + if (request.getHeader("Referer") != null && StringUtils.isNotBlank(request.getHeader("Referer"))) { + redirectUrl = request.getHeader("Referer"); + } else if (request.getHeader("X-Requested-With") != null + && StringUtils.isNotBlank(request.getHeader("X-Requested-With"))) { + redirectUrl = request.getHeader("X-Requested-With"); + } + + // Determine the server return URL, where shib will send the user after authenticating. + // We need it to trigger DSpace's ShibbolethLoginFilter so we will extract the user's information, + // locally authenticate them & then redirect back to the UI. + String returnURL = configurationService.getProperty("dspace.server.url") + "/api/authn/shibboleth" + + ((redirectUrl != null) ? "?redirectUrl=" + redirectUrl : ""); + + try { + shibURL += "?target=" + URLEncoder.encode(returnURL, "UTF-8"); + } catch (UnsupportedEncodingException uee) { + log.error("Unable to generate lazysession authentication",uee); + } + + log.debug("Redirecting user to Shibboleth initiator: " + shibURL); + + return response.encodeRedirectURL(shibURL); + } else { + // If we are not using lazy sessions rely on the protected URL. + return response.encodeRedirectURL(request.getContextPath() + + "/shibboleth-login"); + } + } + + @Override + public String getName() { + return "shibboleth"; + } + + /** + * Check if Shibboleth plugin is enabled + * @return true if enabled, false otherwise + */ + public static boolean isEnabled() { + final String shibPluginName = new ClarinShibAuthentication().getName(); + boolean shibEnabled = false; + // Loop through all enabled authentication plugins to see if Shibboleth is one of them. + Iterator authenticationMethodIterator = + AuthenticateServiceFactory.getInstance().getAuthenticationService().authenticationMethodIterator(); + while (authenticationMethodIterator.hasNext()) { + if (shibPluginName.equals(authenticationMethodIterator.next().getName())) { + shibEnabled = true; + break; + } + } + return shibEnabled; + } + + /** + * Identify an existing EPerson based upon the shibboleth attributes provided on + * the request object. There are three cases where this can occurr, each as + * a fallback for the previous method. + * + * 1) NetID from Shibboleth Header (best) + * The NetID-based method is superior because users may change their email + * address with the identity provider. When this happens DSpace will not be + * able to associate their new address with their old account. + * CLARIN + * Sometimes if the user with netid exists the epersonService.findByNetid cannot find it. This is happening + * only if the user is authenticated with `verification-token`. This problem is fixed. + * CLARIN + * + * 2) Email address from Shibboleth Header (okay) + * In the case where a NetID header is not available or not found DSpace + * will fall back to identifying a user based upon their email address. + * + * 3) Tomcat's Remote User (worst) + * In the event that neither Shibboleth headers are found then as a last + * resort DSpace will look at Tomcat's remote user field. This is the least + * attractive option because Tomcat has no way to supply additional + * attributes about a user. Because of this the autoregister option is not + * supported if this method is used. + * + * If successful then the identified EPerson will be returned, otherwise null. + * + * @param context The DSpace database context + * @param request The current HTTP Request + * @return The EPerson identified or null. + * @throws SQLException if database error + * @throws AuthorizeException if authorization error + */ + protected EPerson findEPerson(Context context, HttpServletRequest request, String[] netidHeaders) + throws SQLException { + + boolean isUsingTomcatUser = configurationService + .getBooleanProperty("authentication-shibboleth.email-use-tomcat-remote-user"); + String emailHeader = configurationService.getProperty("authentication-shibboleth.email-header"); + + EPerson eperson = null; + boolean foundNetID = false; + boolean foundEmail = false; + boolean foundRemoteUser = false; + + + // 1) First, look for a netid header. + if (netidHeaders != null) { + eperson = findEpersonByNetId(netidHeaders, shibheaders, ePersonService, context, true); + if (eperson != null) { + foundNetID = true; + } + } + + // 2) Second, look for an email header. + if (eperson == null && emailHeader != null) { + String email = getEmailAcceptedOrNull(findSingleAttribute(request, emailHeader)); + if (StringUtils.isEmpty(email) && Objects.nonNull(clarinVerificationToken)) { + email = clarinVerificationToken.getEmail(); + } + + if (email != null) { + foundEmail = true; + email = email.toLowerCase(); + eperson = ePersonService.findByEmail(context, email); + + if (eperson == null) { + log.info( + "Unable to identify EPerson based upon Shibboleth email header: '" + emailHeader + "'='" + + email + "'."); + } else { + log.info( + "Identified EPerson based upon Shibboleth email header: '" + emailHeader + "'='" + + email + "'" + "."); + } + + // The condition `Objects.isNull(clarinVerificationToken)` was added because ePersonService couldn't + // find the eperson by netid when he exists. Otherwise the service find the user correctly + // but in that case when the clarinVerificationToken is not null it cannot find him. Do not know why. + if (eperson != null && eperson.getNetid() != null && Objects.isNull(clarinVerificationToken)) { + // If the user has a netID it has been locked to that netid, don't let anyone else try and steal + // the account. + log.error( + "The identified EPerson based upon Shibboleth email header, '" + emailHeader + "'='" + + email + "', is locked to another netid: '" + eperson.getNetid() + + "'. This might be a possible hacking attempt to steal another users " + + "credentials. If the user's netid has changed you will need to manually " + + "change it to the correct value or unset it in the database."); + this.isDuplicateUser = true; + eperson = null; + } + } + } + + // 3) Last, check to see if tomcat is passing a user. + if (eperson == null && isUsingTomcatUser) { + String email = request.getRemoteUser(); + + if (email != null) { + foundRemoteUser = true; + email = email.toLowerCase(); + eperson = ePersonService.findByEmail(context, email); + + if (eperson == null) { + log.info("Unable to identify EPerson based upon Tomcat's remote user: '" + email + "'."); + } else { + log.info("Identified EPerson based upon Tomcat's remote user: '" + email + "'."); + } + + if (eperson != null && eperson.getNetid() != null) { + // If the user has a netID it has been locked to that netid, don't let anyone else try and steal + // the account. + log.error( + "The identified EPerson based upon Tomcat's remote user, '" + email + "', is locked to " + + "another netid: '" + eperson + .getNetid() + "'. This might be a possible hacking attempt to steal another" + + " users credentials. If the user's netid has changed you will need to manually" + + " change it to the correct value or unset it in the database."); + eperson = null; + } + } + } + + if (!foundNetID && !foundEmail && !foundRemoteUser) { + log.error( + "Shibboleth authentication was not able to find a NetId, Email, or Tomcat Remote user for " + + "which to indentify a user from."); + } + + + return eperson; + } + + /** + * Register a new eperson object. This method is called when no existing user was + * found for the NetID or Email and autoregister is enabled. When these conditions + * are met this method will create a new eperson object. + * + * In order to create a new eperson object there is a minimal set of metadata + * required: Email, First Name, and Last Name. If we don't have access to these + * three pieces of information then we will be unable to create a new eperson + * object, such as the case when Tomcat's Remote User field is used to identify + * a particular user. + * + * Note, that this method only adds the minimal metadata. Any additional metadata + * will need to be added by the updateEPerson method. + * + * @param context The current DSpace database context + * @param request The current HTTP Request + * @return A new eperson object or null if unable to create a new eperson. + * @throws SQLException if database error + * @throws AuthorizeException if authorization error + */ + protected EPerson registerNewEPerson(Context context, HttpServletRequest request, String[] netidHeaders) + throws SQLException, AuthorizeException { + + // Header names + String emailHeader = configurationService.getProperty("authentication-shibboleth.email-header"); + String fnameHeader = configurationService.getProperty("authentication-shibboleth.firstname-header"); + String lnameHeader = configurationService.getProperty("authentication-shibboleth.lastname-header"); + + // CLARIN + String org = shibheaders.get_idp(); + if ( org == null ) { + return null; + } + // CLARIN + + // Header values + String netid = getFirstNetId(netidHeaders); + String email = getEmailAcceptedOrNull(findSingleAttribute(request, emailHeader)); + String fname = Headers.updateValueByCharset(findSingleAttribute(request, fnameHeader)); + String lname = Headers.updateValueByCharset(findSingleAttribute(request, lnameHeader)); + + // If the values are not in the request headers try to retrieve it from `shibheaders`. + if (StringUtils.isEmpty(email) && Objects.nonNull(clarinVerificationToken)) { + email = clarinVerificationToken.getEmail(); + } + if (StringUtils.isEmpty(fname)) { + fname = shibheaders.get_single(fnameHeader); + } + if (StringUtils.isEmpty(lname)) { + lname = shibheaders.get_single(lnameHeader); + } + + if ( email == null ) { + // We require that there be an email, first name, and last name. If we + // don't have at least these three pieces of information then we fail. + String message = "Unable to register new eperson because we are unable to find an email address along " + + "with first and last name for the user.\n"; + message += " NetId Header: '" + Arrays.toString(netidHeaders) + "'='" + netid + "' (Optional) \n"; + message += " Email Header: '" + emailHeader + "'='" + email + "' \n"; + message += " First Name Header: '" + fnameHeader + "'='" + fname + "' \n"; + message += " Last Name Header: '" + lnameHeader + "'='" + lname + "'"; + log.error( String.format( + "Could not identify a user from [%s] - we have not received enough information " + + "(email, netid, eppn, ...). \n\nDetails:\n%s\n\nHeaders received:\n%s", + org, message, request.getHeaderNames().toString()) ); + return null; // TODO should this throw an exception? + } + + // Turn off authorizations to create a new user + context.turnOffAuthorisationSystem(); + EPerson eperson = ePersonService.create(context); + + // Set the minimum attributes for the new eperson + if (netid != null) { + eperson.setNetid(netid); + } + eperson.setEmail(email.toLowerCase()); + if (fname != null) { + eperson.setFirstName(context, fname); + } + if (lname != null) { + eperson.setLastName(context, lname); + } + eperson.setCanLogIn(true); + + // Commit the new eperson + AuthenticateServiceFactory.getInstance().getAuthenticationService().initEPerson(context, request, eperson); + ePersonService.update(context, eperson); + context.dispatchEvents(); + + /* CLARIN + * + * Register User in the CLARIN license database + * + */ + // if no email the registration is postponed after entering and confirming mail + if (Objects.nonNull(email)) { + try { + ClarinUserRegistration clarinUserRegistration = new ClarinUserRegistration(); + clarinUserRegistration.setConfirmation(true); + clarinUserRegistration.setEmail(email); + clarinUserRegistration.setPersonID(eperson.getID()); + clarinUserRegistration.setOrganization(org); + clarinUserRegistrationService.create(context, clarinUserRegistration); + eperson.setCanLogIn(false); + ePersonService.update(context, eperson); + } catch (Exception e) { + throw new AuthorizeException("User has not been added among registred users!") ; + } + } + + /* CLARIN */ + + // Turn authorizations back on. + context.restoreAuthSystemState(); + + if (log.isInfoEnabled()) { + String message = "Auto registered new eperson using Shibboleth-based attributes:"; + if (netid != null) { + message += " NetId: '" + netid + "'\n"; + } + message += " Email: '" + email + "' \n"; + message += " First Name: '" + fname + "' \n"; + message += " Last Name: '" + lname + "'"; + log.info(message); + } + + return eperson; + } + + + /** + * After we successfully authenticated a user, this method will update the user's attributes. The + * user's email, name, or other attribute may have been changed since the last time they + * logged into DSpace. This method will update the database with their most recent information. + * + * This method handles the basic DSpace metadata (email, first name, last name) along with + * additional metadata set using the setMetadata() methods on the eperson object. The + * additional metadata are defined by a mapping created in the dspace.cfg. + * + * @param context The current DSpace database context + * @param request The current HTTP Request + * @param eperson The eperson object to update. + * @throws SQLException if database error + * @throws AuthorizeException if authorization error + */ + protected void updateEPerson(Context context, HttpServletRequest request, EPerson eperson, String[] netidHeaders) + throws SQLException, AuthorizeException { + + // Header names & values + String emailHeader = configurationService.getProperty("authentication-shibboleth.email-header"); + String fnameHeader = configurationService.getProperty("authentication-shibboleth.firstname-header"); + String lnameHeader = configurationService.getProperty("authentication-shibboleth.lastname-header"); + + String netid = getFirstNetId(netidHeaders); + String email = getEmailAcceptedOrNull(findSingleAttribute(request, emailHeader)); + String fname = Headers.updateValueByCharset(findSingleAttribute(request, fnameHeader)); + String lname = Headers.updateValueByCharset(findSingleAttribute(request, lnameHeader)); + + // If the values are not in the request headers try to retrieve it from `shibheaders`. + if (StringUtils.isEmpty(email) && Objects.nonNull(clarinVerificationToken)) { + email = clarinVerificationToken.getEmail(); + } + if (StringUtils.isEmpty(fname)) { + fname = shibheaders.get_single(fnameHeader); + } + if (StringUtils.isEmpty(lname)) { + lname = shibheaders.get_single(lnameHeader); + } + + // Truncate values of parameters that are too big. + if (fname != null && fname.length() > NAME_MAX_SIZE) { + log.warn( + "Truncating eperson's first name because it is longer than " + NAME_MAX_SIZE + ": '" + fname + "'"); + fname = fname.substring(0, NAME_MAX_SIZE); + } + if (lname != null && lname.length() > NAME_MAX_SIZE) { + log.warn("Truncating eperson's last name because it is longer than " + NAME_MAX_SIZE + ": '" + lname + "'"); + lname = lname.substring(0, NAME_MAX_SIZE); + } + + context.turnOffAuthorisationSystem(); + + // 1) Update the minimum metadata + + // Only update the netid if none has been previously set. This can occur when a repo switches + // to netid based authentication. The current users do not have netids and fall back to email-based + // identification but once they login we update their record and lock the account to a particular netid. + if (netid != null && eperson.getNetid() == null) { + eperson.setNetid(netid); + } + // The email could have changed if using netid based lookup. + if (email != null) { + String lowerCaseEmail = email.toLowerCase(); + // Check the email is unique + EPerson epersonByEmail = ePersonService.findByEmail(context, lowerCaseEmail); + if (epersonByEmail != null && !epersonByEmail.getID().equals(eperson.getID())) { + log.error("Unable to update the eperson's email metadata because the email '{}' is already in use.", + lowerCaseEmail); + throw new AuthorizeException("The email address is already in use."); + } else { + eperson.setEmail(email.toLowerCase()); + } + } + if (fname != null) { + eperson.setFirstName(context, fname); + } + if (lname != null) { + eperson.setLastName(context, lname); + } + + if (log.isDebugEnabled()) { + String message = "Updated the eperson's minimal metadata: \n"; + message += " Email Header: '" + emailHeader + "' = '" + email + "' \n"; + message += " First Name Header: '" + fnameHeader + "' = '" + fname + "' \n"; + message += " Last Name Header: '" + fnameHeader + "' = '" + lname + "'"; + log.debug(message); + } + + // 2) Update additional eperson metadata + for (String header : metadataHeaderMap.keySet()) { + + String field = metadataHeaderMap.get(header); + String value = findSingleAttribute(request, header); + if (StringUtils.isEmpty(value)) { + value = shibheaders.get_single(header); + } + + // Truncate values + if (value == null) { + log.warn("Unable to update the eperson's '{}' metadata" + + " because the header '{}' does not exist.", field, header); + continue; + } else if ("phone".equals(field) && value.length() > PHONE_MAX_SIZE) { + log.warn("Truncating eperson phone metadata because it is longer than {}: '{}'", + PHONE_MAX_SIZE, value); + value = value.substring(0, PHONE_MAX_SIZE); + } else if (value.length() > METADATA_MAX_SIZE) { + log.warn("Truncating eperson {} metadata because it is longer than {}: '{}'", + field, METADATA_MAX_SIZE, value); + value = value.substring(0, METADATA_MAX_SIZE); + } + + String[] nameParts = MetadataFieldName.parse(field); + ePersonService.setMetadataSingleValue(context, eperson, + nameParts[0], nameParts[1], nameParts[2], value, null); + log.debug("Updated the eperson's '{}' metadata using header: '{}' = '{}'.", + field, header, value); + } + ePersonService.update(context, eperson); + context.dispatchEvents(); + context.restoreAuthSystemState(); + } + + /** + * Provide password-based authentication to enable sword compatibility. + * + * Sword compatibility will allow this authentication method to work when using + * sword. Sword relies on username and password based authentication and is + * entirely incapable of supporting shibboleth. This option allows you to + * authenticate username and passwords for sword sessions without adding + * another authentication method onto the stack. You will need to ensure that + * a user has a password. One way to do that is to create the user via the + * create-administrator command line command and then edit their permissions. + * + * @param context The DSpace database context + * @param username The username + * @param password The password + * @param request The HTTP Request + * @return A valid DSpace Authentication Method status code. + * @throws SQLException if database error + */ + protected int swordCompatibility(Context context, String username, String password, HttpServletRequest request) + throws SQLException { + + log.debug("Shibboleth Sword compatibility activated."); + EPerson eperson = ePersonService.findByEmail(context, username.toLowerCase()); + + if (eperson == null) { + // lookup failed. + log.error( + "Shibboleth-based password authentication failed for user " + username + + " because no such user exists."); + return NO_SUCH_USER; + } else if (!eperson.canLogIn()) { + // cannot login this way + log.error( + "Shibboleth-based password authentication failed for user " + username + + " because the eperson object is not allowed to login."); + return BAD_ARGS; + } else if (eperson.getRequireCertificate()) { + // this user can only login with x.509 certificate + log.error( + "Shibboleth-based password authentication failed for user " + username + + " because the eperson object requires a certificate to authenticate.."); + return CERT_REQUIRED; + } else if (ePersonService.checkPassword(context, eperson, password)) { + // Password matched + AuthenticateServiceFactory.getInstance().getAuthenticationService().initEPerson(context, request, eperson); + context.setCurrentUser(eperson); + log.info(eperson + .getEmail() + " has been authenticated via shibboleth using password-based sword " + + "compatibility mode."); + return SUCCESS; + } else { + // Passsword failure + log.error( + "Shibboleth-based password authentication failed for user " + username + + " because a bad password was supplied."); + return BAD_CREDENTIALS; + } + + } + + + /** + * Initialize Shibboleth Authentication. + * + * During initalization the mapping of additional eperson metadata will be loaded from the DSpace.cfg + * and cached. While loading the metadata mapping this method will check the EPerson object to see + * if it supports the metadata field. If the field is not supported and autocreate is turned on then + * the field will be automatically created. + * + * It is safe to call this methods multiple times. + * + * @param context context + * @throws SQLException if database error + */ + protected synchronized void initialize(Context context) throws SQLException { + + if (metadataHeaderMap != null) { + return; + } + + + HashMap map = new HashMap<>(); + + String[] mappingString = configurationService.getArrayProperty("authentication-shibboleth.eperson.metadata"); + boolean autoCreate = configurationService + .getBooleanProperty("authentication-shibboleth.eperson.metadata.autocreate", true); + + // Bail out if not set, returning an empty map. + if (mappingString == null || mappingString.length == 0) { + log.debug("No additional eperson metadata mapping found: authentication.shib.eperson.metadata"); + + metadataHeaderMap = map; + return; + } + + log.debug("Loading additional eperson metadata from: 'authentication.shib.eperson.metadata' = '" + StringUtils + .join(mappingString, ",") + "'"); + + + for (String metadataString : mappingString) { + metadataString = metadataString.trim(); + + String[] metadataParts = metadataString.split("=>"); + + if (metadataParts.length != 2) { + log.error("Unable to parse metadat mapping string: '" + metadataString + "'"); + continue; + } + + String header = metadataParts[0].trim(); + String name = metadataParts[1].trim().toLowerCase(); + + // `name` is not just name of the metadata field like `phone` but is like `eperson.phone` and the method + // which find if the metadata field exists doesn't work with name in that type. + String[] schemaAndField = name.split("\\."); + if (schemaAndField.length != 2) { + log.error("Unable to parse schema and field string from name: '" + name + "'"); + continue; + } + + String fieldName = schemaAndField[1]; + boolean valid = checkIfEpersonMetadataFieldExists(context, fieldName); + + if (!valid && autoCreate) { + valid = autoCreateEpersonMetadataField(context, fieldName); + } + + if (valid) { + // The eperson field is fine, we can use it. + log.debug("Loading additional eperson metadata mapping for: '{}' = '{}'", + header, name); + map.put(header, name); + } else { + // The field doesn't exist, and we can't use it. + log.error("Skipping the additional eperson metadata mapping for: '{}' = '{}'" + + " because the field is not supported by the current configuration.", + header, name); + } + } // foreach metadataStringList + + + metadataHeaderMap = map; + } + + /** + * Check if a MetadataField for an eperson is available. + * + * @param metadataName The name of the metadata field. + * @param context context + * @return True if a valid metadata field, otherwise false. + * @throws SQLException if database error + */ + protected synchronized boolean checkIfEpersonMetadataFieldExists(Context context, String metadataName) + throws SQLException { + + if (metadataName == null) { + return false; + } + + MetadataField metadataField = metadataFieldService.findByElement(context, + MetadataSchemaEnum.EPERSON.getName(), metadataName, null); + return metadataField != null; + } + + /** + * Validate Postgres Column Names + */ + protected final String COLUMN_NAME_REGEX = "^[_A-Za-z0-9]+$"; + + /** + * Automatically create a new metadataField for an eperson + * + * @param context context + * @param metadataName The name of the new metadata field. + * @return True if successful, otherwise false. + * @throws SQLException if database error + */ + protected synchronized boolean autoCreateEpersonMetadataField(Context context, String metadataName) + throws SQLException { + + if (metadataName == null) { + return false; + } + + // The phone is a predefined field + if ("phone".equals(metadataName)) { + return true; + } + + if (!metadataName.matches(COLUMN_NAME_REGEX)) { + return false; + } + + MetadataSchema epersonSchema = metadataSchemaService.find(context, "eperson"); + MetadataField metadataField = null; + try { + context.turnOffAuthorisationSystem(); + metadataField = metadataFieldService.create(context, epersonSchema, metadataName, null, null); + } catch (AuthorizeException | NonUniqueMetadataException e) { + log.error(e.getMessage(), e); + return false; + } finally { + context.restoreAuthSystemState(); + } + return metadataField != null; + } + + + /** + * Find a particular Shibboleth header value and return the all values. + * The header name uses a bit of fuzzy logic, so it will first try case + * sensitive, then it will try lowercase, and finally it will try uppercase. + * + * This method will not interpret the header value in any way. + * + * This method will return null if value is empty. + * + * @param request The HTTP request to look for values in. + * @param name The name of the attribute or header + * @return The value of the attribute or header requested, or null if none found. + */ + protected String findAttribute(HttpServletRequest request, String name) { + if (name == null) { + return null; + } + // First try to get the value from the attribute + String value = (String) request.getAttribute(name); + if (StringUtils.isEmpty(value)) { + value = (String) request.getAttribute(name.toLowerCase()); + } + if (StringUtils.isEmpty(value)) { + value = (String) request.getAttribute(name.toUpperCase()); + } + + // Second try to get the value from the header + if (StringUtils.isEmpty(value)) { + value = request.getHeader(name); + } + if (StringUtils.isEmpty(value)) { + value = request.getHeader(name.toLowerCase()); + } + if (StringUtils.isEmpty(value)) { + value = request.getHeader(name.toUpperCase()); + } + + // Added extra check for empty value of an attribute. + // In case that value is Empty, it should not be returned, return 'null' instead. + // This prevents passing empty value to other methods, stops the authentication process + // and prevents creation of 'empty' DSpace EPerson if autoregister == true and it subsequent + // authentication. + if (StringUtils.isEmpty(value)) { + log.debug("ShibAuthentication - attribute " + name + " is empty!"); + return null; + } + + boolean reconvertAttributes = + configurationService.getBooleanProperty( + "authentication-shibboleth.reconvert.attributes", + false); + + if (!StringUtils.isEmpty(value) && reconvertAttributes) { + try { + String inputEncoding = configurationService.getProperty("shibboleth.name.conversion.inputEncoding", + "ISO-8859-1"); + String outputEncoding = configurationService.getProperty("shibboleth.name.conversion.outputEncoding", + "UTF-8"); + + value = new String(value.getBytes(inputEncoding), outputEncoding); + } catch (UnsupportedEncodingException ex) { + log.warn("Failed to reconvert shibboleth attribute (" + + name + ").", ex); + } + } + + return value; + } + + + /** + * Find a particular Shibboleth header value and return the first value. + * The header name uses a bit of fuzzy logic, so it will first try case + * sensitive, then it will try lowercase, and finally it will try uppercase. + * + * Shibboleth attributes may contain multiple values separated by a + * semicolon. This method will return the first value in the attribute. If + * you need multiple values use findMultipleAttributes instead. + * + * If no attribute is found then null is returned. + * + * @param request The HTTP request to look for headers values on. + * @param name The name of the header + * @return The value of the header requested, or null if none found. + */ + public String findSingleAttribute(HttpServletRequest request, String name) { + if (name == null) { + return null; + } + String value = findAttribute(request, name); + + if (value != null) { + value = sortEmailsAndGetFirst(value); + } + return value; + } + + /** + * Find a particular Shibboleth hattributeeader value and return the values. + * The attribute name uses a bit of fuzzy logic, so it will first try case + * sensitive, then it will try lowercase, and finally it will try uppercase. + * + * Shibboleth attributes may contain multiple values separated by a + * semicolon and semicolons are escaped with a backslash. This method will + * split all the attributes into a list and unescape semicolons. + * + * If no attributes are found then null is returned. + * + * @param request The HTTP request to look for headers values on. + * @param name The name of the attribute + * @return The list of values found, or null if none found. + */ + protected List findMultipleAttributes(HttpServletRequest request, String name) { + String values = findAttribute(request, name); + + if (values == null) { + return null; + } + + // Shibboleth attributes are separated by semicolons (and semicolons are + // escaped with a backslash). So here we will scan through the string and + // split on any unescaped semicolons. + List valueList = new ArrayList<>(); + int idx = 0; + do { + idx = values.indexOf(';', idx); + + if (idx == 0) { + // if the string starts with a semicolon just remove it. This will + // prevent an endless loop in an error condition. + values = values.substring(1, values.length()); + + } else if (idx > 0 && values.charAt(idx - 1) == '\\') { + // The attribute starts with an escaped semicolon + idx++; + } else if (idx > 0) { + // First extract the value and store it on the list. + String value = values.substring(0, idx); + value = value.replaceAll("\\\\;", ";"); + valueList.add(value); + + // Next, remove the value from the string and continue to scan. + values = values.substring(idx + 1, values.length()); + idx = 0; + } + } while (idx >= 0); + + // The last attribute will still be left on the values string, put it + // into the list. + if (values.length() > 0) { + values = values.replaceAll("\\\\;", ";"); + valueList.add(values); + } + + return valueList; + } + + private String getShibURL(HttpServletRequest request) { + String shibURL = configurationService.getProperty("authentication-shibboleth.lazysession.loginurl", + "/Shibboleth.sso/Login"); + boolean forceHTTPS = + configurationService.getBooleanProperty("authentication-shibboleth.lazysession.secure", true); + + // Shibboleth url must be absolute + if (shibURL.startsWith("/")) { + String serverUrl = Utils.getBaseUrl(configurationService.getProperty("dspace.server.url")); + shibURL = serverUrl + shibURL; + if ((request.isSecure() || forceHTTPS) && shibURL.startsWith("http://")) { + shibURL = shibURL.replace("http://", "https://"); + } + } + return shibURL; + + } + + @Override + public boolean isUsed(final Context context, final HttpServletRequest request) { + if (request != null && + context.getCurrentUser() != null && + request.getSession().getAttribute("shib.authenticated") != null) { + return true; + } + return false; + } + + @Override + public boolean canChangePassword(Context context, EPerson ePerson, String currentPassword) { + return false; + } + + @Override + public boolean areSpecialGroupsApplicable(Context context, HttpServletRequest request) { + return true; + } + + public String getEmailAcceptedOrNull(String email) { + // no whitespaces in mail + if (StringUtils.isEmpty(email) || Pattern.compile("\\s").matcher(email).find()) { + return null; + } + return email; + } + + /** + * Find an EPerson by a NetID header. The method will go through all the netid headers and try to find a user. + */ + public static EPerson findEpersonByNetId(String[] netidHeaders, ShibHeaders shibheaders, + EPersonService ePersonService, Context context, boolean logAllowed) + throws SQLException { + // Go through all the netid headers and try to find a user. It could be e.g., `eppn`, `persistent-id`,.. + for (String netidHeader : netidHeaders) { + netidHeader = netidHeader.trim(); + String netid = shibheaders.get_single(netidHeader); + if (netid == null) { + continue; + } + + EPerson eperson = ePersonService.findByNetid(context, netid); + + if (eperson == null && logAllowed) { + log.info( + "Unable to identify EPerson based upon Shibboleth netid header: '" + netidHeader + + "'='" + netid + "'."); + } else if (eperson != null) { + log.debug( + "Identified EPerson based upon Shibboleth netid header: '" + netidHeader + "'='" + + netid + "'" + "."); + return eperson; + } + } + return null; + } + + /** + * Sort the email addresses and return the first one. + * @param value The email addresses separated by semicolons. + */ + public static String sortEmailsAndGetFirst(String value) { + // If there are multiple values encoded in the shibboleth attribute + // they are separated by a semicolon, and any semicolons in the + // attribute are escaped with a backslash. + // Step 1: Split the input string into email addresses + List emails = Arrays.stream(value.split("(? email.replaceAll("\\\\;", ";")) // Unescape semicolons + .collect(Collectors.toList()); + + // Step 2: Sort the email list alphabetically + emails.sort(String::compareToIgnoreCase); + + // Step 3: Get the first sorted email + return emails.get(0); + } + + /** + * Get the first netid from the list of netid headers. E.g., eppn, persistent-id,... + * @param netidHeaders list of netid headers loaded from the configuration `authentication-shibboleth.netid-header` + */ + public String getFirstNetId(String[] netidHeaders) { + for (String netidHeader : netidHeaders) { + netidHeader = netidHeader.trim(); + String netid = shibheaders.get_single(netidHeader); + if (netid != null) { + //When creating use first match (eppn before targeted-id) + return netid; + } + } + return null; + } +} + diff --git a/dspace-api/src/main/java/org/dspace/authenticate/clarin/Headers.java b/dspace-api/src/main/java/org/dspace/authenticate/clarin/Headers.java new file mode 100644 index 000000000000..04d4454cbc8c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/authenticate/clarin/Headers.java @@ -0,0 +1,215 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +/* Created for LINDAT/CLARIN */ +package org.dspace.authenticate.clarin; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.services.ConfigurationService; +import org.dspace.utils.DSpace; + +/** + * Helper class for request headers. + * Class is copied from UFAL/CLARIN-DSPACE (https://github.com/ufal/clarin-dspace) and modified by + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class Headers { + + private static final Logger log = LogManager.getLogger(org.dspace.authenticate.clarin.Headers.class); + // variables + // + private static ConfigurationService configurationService = new DSpace().getConfigurationService(); + + private HashMap> headers_ = new HashMap>(); + private String header_separator_ = null; + private static String EMPTY_STRING = ""; + + + // ctors + // + public Headers(HttpServletRequest request, String header_separator ) { + initialise(request, header_separator, null); + } + + public Headers(String shibHeaders, String header_separator ) { + initialise(shibHeaders, header_separator); + } + + public Headers(HttpServletRequest request, String header_separator, List interesting ) { + initialise(request, header_separator, interesting); + } + + public void initialise(HttpServletRequest request, String header_separator, List interesting) { + header_separator_ = header_separator; + // + Enumeration e_keys = request.getHeaderNames(); + while (e_keys.hasMoreElements()) { + String key = (String) e_keys.nextElement(); + if ( interesting != null && !interesting.contains(key) ) { + continue; + } + + List vals = new ArrayList(); + Enumeration e_vals = request.getHeaders(key); + while (e_vals.hasMoreElements()) { + String values = updateValueByCharset((String) e_vals.nextElement()); + vals.addAll( header2values(values) ); + } + + // make it case-insensitive + headers_.put(key.toLowerCase(), vals); + } + } + + public void initialise(String shibHeaders, String header_separator) { + header_separator_ = header_separator; + // + for (String line : shibHeaders.split("\n")) { + String key = " "; + try { + String key_value[] = line.split("=", 2); + key = key_value[0].trim(); + headers_.put(key, List.of(key_value[1])); + } catch (Exception ignore) { + // + } + } + } + + public String toString() { + StringBuilder ret = new StringBuilder(); + for (String header : headers_.keySet()) { + ret.append(header).append(" = ").append(headers_.get(header).toString()).append("\n"); + } + return ret.toString(); + } + + // + // + + public Map> get() { + return headers_; + } + + public List get(String key) { + return headers_.get(key.toLowerCase()); + } + + // helper methods (few things are copied from ShibAuthenetication.java) + // + + private String unescape(String value) { + return value.replaceAll("\\\\" + header_separator_, header_separator_); + } + + + private List header2values(String header) { + // Shibboleth attributes are separated by semicolons (and semicolons are + // escaped with a backslash). So here we will scan through the string and + // split on any unescaped semicolons. + List values = new ArrayList(); + + if ( header == null ) { + return values; + } + + int idx = 0; + do { + idx = header.indexOf(header_separator_,idx); + + if ( idx == 0 ) { + // if the string starts with a semicolon just remove it. This will + // prevent an endless loop in an error condition. + header = header.substring(1,header.length()); + + } else if (idx > 0 && header.charAt(idx - 1) == '\\' ) { + // found an escaped semicolon; move on + idx++; + } else if ( idx > 0) { + // First extract the value and store it on the list. + String value = header.substring(0, idx); + value = unescape(value); + values.add(value); + // Next, remove the value from the string and continue to scan. + header = header.substring(idx + 1, header.length()); + idx = 0; + } + } while (idx >= 0); + + // The last attribute will still be left on the values string, put it + // into the list. + if (header.length() > 0) { + header = unescape(header); + values.add(header); + } + + return values; + } + + + /** + * Convert ISO header value to UTF-8 or return UTF-8 value if it is not ISO. + * @param value ISO/UTF-8 header value String + * @return Converted ISO value to UTF-8 or UTF-8 value from input + */ + public static String updateValueByCharset(String value) { + String inputEncoding = configurationService.getProperty("shibboleth.name.conversion.inputEncoding", + "ISO-8859-1"); + String outputEncoding = configurationService.getProperty("shibboleth.name.conversion.outputEncoding", + "UTF-8"); + + if (StringUtils.isBlank(value)) { + value = EMPTY_STRING; + } + + // If the value is not ISO-8859-1, then it is already UTF-8 + if (!isISOType(value)) { + return value; + } + + try { + // Encode the string to UTF-8 + return new String(value.getBytes(inputEncoding), outputEncoding); + } catch (UnsupportedEncodingException ex) { + log.warn("Cannot convert the value: " + value + " from " + inputEncoding + " to " + outputEncoding + + " because of: " + ex.getMessage()); + return value; + } + } + + /** + * Check if the value is ISO-8859-1 encoded. + * @param value String to check + * @return true if the value is ISO-8859-1 encoded, false otherwise + */ + private static boolean isISOType(String value) { + try { + // Encode the string to ISO-8859-1 + byte[] iso8859Bytes = value.getBytes(StandardCharsets.ISO_8859_1); + + // Decode the bytes back to a string using ISO-8859-1 + String decodedString = new String(iso8859Bytes, StandardCharsets.ISO_8859_1); + + // Compare the original string with the decoded string + return StringUtils.equals(value, decodedString); + } catch (Exception e) { + // An exception occurred, so the input is not ISO-8859-1 + return false; + } + } +} diff --git a/dspace-api/src/main/java/org/dspace/authenticate/clarin/ShibGroup.java b/dspace-api/src/main/java/org/dspace/authenticate/clarin/ShibGroup.java new file mode 100644 index 000000000000..caea45ca7838 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/authenticate/clarin/ShibGroup.java @@ -0,0 +1,307 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.authenticate.clarin; +/* Created for LINDAT/CLARIN */ + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.UUID; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.core.Context; +import org.dspace.eperson.Group; +import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.eperson.service.GroupService; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.utils.DSpace; + +/** + * Try to refactor the Shibboleth mess. + * + * Get groups a user should be put into according to several Shibboleth headers + * and default configuration values. + * + * Class is copied from UFAL/CLARIN-DSPACE (https://github.com/ufal/clarin-dspace) and modified by + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class ShibGroup { + // variables + // + private static final Logger log = LogManager.getLogger(ShibGroup.class); + private ShibHeaders shib_headers_ = null; + private Context context_ = null; + + private static String defaultRoles; + private static String roleHeader; + private static boolean ignoreScope; + private static boolean ignoreValue; + + ConfigurationService configurationService; + GroupService groupService; + // ctor + // + + public ShibGroup(ShibHeaders shib_headers, Context context) { + configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + groupService = EPersonServiceFactory.getInstance().getGroupService(); + + defaultRoles = configurationService.getProperty("authentication-shibboleth.default-roles"); + roleHeader = configurationService.getProperty("authentication-shibboleth.role-header"); + ignoreScope = configurationService + .getBooleanProperty("authentication-shibboleth.role-header.ignore-scope", true); + ignoreValue = configurationService + .getBooleanProperty("authentication-shibboleth.role-header.ignore-value", false); + + shib_headers_ = shib_headers; + context_ = context; + + if (ignoreScope && ignoreValue) { + throw new IllegalStateException( + "Both config parameters for ignoring attributes scope and value are turned on, " + + "this is not a permissable configuration. (Note: ignore-scope defaults to true) " + + "The configuration parameters are: 'authentication.shib.role-header.ignore-scope' " + + "and 'authentication.shib.role-header.ignore-value'"); + } + } + + /** + * This is again a bit messy but the purpose is to find out into which groups an EPerson belongs; hence, + * authorisation part from AAI. + * + * + */ + + public List get() { + try { + log.debug("Starting to determine special groups"); + + // Get afill from `authentication-shibboleth.header.entitlement` and from EmAIL HEADER + /* + * lets be evil and hack the email to the entitlement field + */ + List affiliations = new ArrayList(); + + affiliations.addAll( + get_affilations_from_roles(roleHeader)); + affiliations.addAll( + get_affilations_from_shib_mappings()); + + /* */ + + + // If none affiliation was loaded + if (affiliations.isEmpty()) { + if (defaultRoles != null) { + affiliations = Arrays.asList(defaultRoles.split(",")); + } + log.debug("Failed to find Shibboleth role header, '" + roleHeader + "', " + + "falling back to the default roles: '" + defaultRoles + "'"); + } else { + log.debug("Found Shibboleth role header: '" + roleHeader + "' = '" + affiliations + "'"); + } + + // Loop through each affiliation + // + Set groups = new HashSet(); + if (affiliations != null) { + for ( String affiliation : affiliations) { + // populate the organisation name + affiliation = populate_affiliation(affiliation, ignoreScope, ignoreValue); + // try to get the group names from authentication-shibboleth.cfg + String groupNames = get_group_names_from_affiliation(affiliation); + + if (groupNames == null) { + log.debug("Unable to find role mapping for the value, '" + affiliation + "', " + + "there should be a mapping in the dspace.cfg: authentication.shib.role." + + affiliation + " = "); + continue; + } else { + log.debug("Mapping role affiliation to DSpace group: '" + groupNames + "'"); + } + + // get the group ids + groups.addAll(string2groups(groupNames)); + + } // foreach affiliations + } // if affiliations + + //attribute -> group mapping + //check shibboleth attribute ATTR and put users having value ATTR_VALUE1 and ATTR_VALUE2 to GROUP1 + //users having ATTR_VALUE3 to GROUP2 + //groups must exist + //header.ATTR=ATTR_VALUE1=>GROUP1,ATTR_VALUE2=>GROUP1,ATTR_VALUE3=>GROUP2 + final String lookFor = "authentication-shibboleth.header."; + ConfigurationService configurationService = new DSpace().getConfigurationService(); + Properties allShibbolethProperties = configurationService.getProperties(); + for (String propertyName : allShibbolethProperties.stringPropertyNames()) { + //look for properties in authentication shibboleth that start with "header." + if (propertyName.startsWith(lookFor)) { + String headerName = propertyName.substring(lookFor.length()); + List presentHeaderValues = shib_headers_.get(headerName); + if (!CollectionUtils.isEmpty(presentHeaderValues)) { + //if shibboleth sent any attributes under the headerName + String[] values2groups = configurationService.getPropertyAsType( + propertyName, String[].class); + for (String value2group : values2groups) { + String[] value2groupParts = value2group.split("=>", 2); + String headerValue = value2groupParts[0].trim(); + String assignedGroup = value2groupParts[1].trim(); + if (presentHeaderValues.contains(headerValue)) { + //our configured header value is present so add a group + groups.addAll(string2groups(assignedGroup)); + } + } + } + } + } + + /* + * Default group for shib authenticated users + */ + Group default_group = get_default_group(); + if ( null != default_group ) { + groups.add(default_group.getID()); + } + /* */ + + log.info("Added current EPerson to special groups: " + groups); + // Convert from a Java Set to primitive ArrayList array + return new ArrayList<>(groups); + } catch (Throwable t) { + log.error( + "Unable to validate any special groups this user may belong too because of an exception.",t); + return new ArrayList<>(); + } + } + + // + // + private List get_affilations_from_roles(String roleHeader) { + List roleHeaderValues = shib_headers_.get(roleHeader); + List affiliations = new ArrayList(); + + // Get the Shib supplied affiliation or use the default affiliation + // e.g., we can use 'entitlement' shibboleth header + if (roleHeaderValues != null) { + for (String roleHeaderValue : roleHeaderValues) { + affiliations.addAll(string2values(roleHeaderValue)); + } + } + return affiliations; + } + + private List get_affilations_from_shib_mappings() { + List ret = new ArrayList(); + String organization = shib_headers_.get_idp(); + // Try to get email based on utilities mapping database table + // + if (organization != null) { + String email_header = configurationService.getProperty("authentication-shibboleth.email-header"); + if (email_header != null) { + String email = shib_headers_.get_single(email_header); + if (email != null) { + ret = string2values(email); + } + } + } + if ( ret == null ) { + return new ArrayList(); + } + + return ret; + } + + private String populate_affiliation(String affiliation, boolean ignoreScope, boolean ignoreValue) { + // If we ignore the affilation's scope then strip the scope if it exists. + if (ignoreScope) { + int index = affiliation.indexOf('@'); + if (index != -1) { + affiliation = affiliation.substring(0, index); + } + } + // If we ignore the value, then strip it out so only the scope remains. + if (ignoreValue) { + int index = affiliation.indexOf('@'); + if (index != -1) { + affiliation = affiliation.substring(index + 1, affiliation.length()); + } + } + + return affiliation; + } + + private String get_group_names_from_affiliation(String affiliation) { + String groupNames = configurationService.getProperty( + "authentication-shibboleth.role." + affiliation); + if (groupNames == null || groupNames.trim().length() == 0) { + groupNames = configurationService.getProperty( + "authentication-shibboleth.role." + affiliation.toLowerCase()); + } + return groupNames; + } + + private List string2groups(String groupNames) { + List groups = new ArrayList(); + // Add each group to the list. + String[] names = groupNames.split(","); + for (int i = 0; i < names.length; i++) { + try { + + Group group = groupService.findByName(context_, names[i].trim()); + if (group != null) { + groups.add(group.getID()); + } else { + log.debug("Unable to find group: '" + names[i].trim() + "'"); + } + } catch (SQLException sqle) { + log.error( + "Exception thrown while trying to lookup affiliation role for group name: '" + + names[i].trim() + "'", sqle); + } + } // for each groupNames + return groups; + } + + private Group get_default_group() { + String defaultAuthGroup = configurationService.getProperty( + "authentication-shibboleth.default.auth.group"); + if (defaultAuthGroup != null && defaultAuthGroup.trim().length() != 0) { + try { + Group group = groupService.findByName(context_,defaultAuthGroup.trim()); + if (group != null) { + return group; + } else { + log.debug("Unable to find default group: '" + defaultAuthGroup.trim() + "'"); + } + } catch (SQLException sqle) { + log.error("Exception thrown while trying to lookup shibboleth " + + "default authentication group with name: '" + defaultAuthGroup.trim() + "'",sqle); + } + } + + return null; + } + + // helpers + // + + private static List string2values(String string) { + if ( string == null ) { + return null; + } + return Arrays.asList(string.split(",|;")); + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/authenticate/clarin/ShibHeaders.java b/dspace-api/src/main/java/org/dspace/authenticate/clarin/ShibHeaders.java new file mode 100644 index 000000000000..95f0a95dc9ce --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/authenticate/clarin/ShibHeaders.java @@ -0,0 +1,160 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +/* Created for LINDAT/CLARIN */ +package org.dspace.authenticate.clarin; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.app.util.Util; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; + +/** + * Shibboleth authentication header abstraction for DSpace + * + * Parses all headers in ctor. + * Class is copied from UFAL/CLARIN-DSPACE (https://github.com/ufal/clarin-dspace) and modified by + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class ShibHeaders { + // constants + // + private static final String header_separator_ = ";"; + private String[] netIdHeaders = null; + ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + + // variables + // + private static final Logger log = LogManager.getLogger(ShibHeaders.class); + + private Headers headers_ = null; + + // ctor + // + + public ShibHeaders(HttpServletRequest request, String[] interesting) { + initialise(request, Arrays.asList(interesting)); + } + public ShibHeaders(HttpServletRequest request, String interesting) { + initialise(request, Arrays.asList(interesting)); + } + public ShibHeaders(HttpServletRequest request) { + initialise(request, null); + } + + public ShibHeaders(String shibHeaders) { + initialise(shibHeaders); + } + + // inits + // + + public void initialise(HttpServletRequest request, List interesting) { + headers_ = new Headers(request, header_separator_, interesting); + this.initializeNetIdHeader(); + } + + public void initialise(String shibHeaders) { + headers_ = new Headers(shibHeaders, header_separator_); + this.initializeNetIdHeader(); + } + + // + // + + public String get_idp() { + return get_single("Shib-Identity-Provider"); + } + + // list like interface (few things are copied from ShibAuthenetication.java) + // + + /** + * Find a particular Shibboleth header value and return the all values. + * The header name uses a bit of fuzzy logic, so it will first try case + * sensitive, then it will try lowercase, and finally it will try uppercase. + */ + public List get(String key) { + List values = headers_.get(key); + if (values != null && values.isEmpty()) { + values = null; + } + return values; + } + + /** + * Find a particular Shibboleth header value and return the first value. + * + * Shibboleth attributes may contain multiple values separated by a + * semicolon. This method will return the first value in the attribute. If + * you need multiple values use findMultipleHeaders instead. + */ + public String get_single(String name) { + List values = get(name); + if (values != null && !values.isEmpty()) { + // Format netId + if (ArrayUtils.contains(this.netIdHeaders, name)) { + return Util.formatNetId(values.get(0), this.get_idp()); + } + return values.get(0); + } + return null; + } + + /** + * Get keys which starts with prefix. + * @return + */ + public List get_prefix_keys(String prefix) { + List keys = new ArrayList(); + for (String k : headers_.get().keySet()) { + if (k.toLowerCase().startsWith(prefix)) { + keys.add(k); + } + } + return keys; + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + for ( Map.Entry> i : headers_.get().entrySet() ) { + if (StringUtils.equals("cookie", i.getKey())) { + continue; + } + sb.append(String.format("%s=%s\n", + i.getKey(), StringUtils.join(i.getValue().toArray(), ",") )); + } + return sb.toString(); + } + + // + // + + public void log_headers() { + for ( Map.Entry> i : headers_.get().entrySet() ) { + log.debug(String.format("header:%s=%s", + i.getKey(), StringUtils.join(i.getValue().toArray(), ",") )); + } + } + + private void initializeNetIdHeader() { + this.netIdHeaders = configurationService.getArrayProperty("authentication-shibboleth.netid-header"); + } + + public String[] getNetIdHeaders() { + return this.netIdHeaders; + } +} diff --git a/dspace-api/src/main/java/org/dspace/authorize/AuthorizationBitstreamUtils.java b/dspace-api/src/main/java/org/dspace/authorize/AuthorizationBitstreamUtils.java new file mode 100644 index 000000000000..40f213076df2 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/authorize/AuthorizationBitstreamUtils.java @@ -0,0 +1,214 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.authorize; + +import static org.dspace.content.clarin.ClarinLicense.Confirmation; + +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import jakarta.servlet.http.HttpServletRequest; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.dspace.content.Bitstream; +import org.dspace.content.Item; +import org.dspace.content.clarin.ClarinLicense; +import org.dspace.content.clarin.ClarinLicenseResourceMapping; +import org.dspace.content.service.BitstreamService; +import org.dspace.content.service.clarin.ClarinLicenseResourceMappingService; +import org.dspace.content.service.clarin.ClarinLicenseResourceUserAllowanceService; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.services.model.Request; +import org.dspace.utils.DSpace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Authorize the user if could download the Item's bitstream. + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +@Component +public class AuthorizationBitstreamUtils { + + private static final Logger log = LoggerFactory.getLogger(AuthorizationBitstreamUtils.class); + + @Autowired + ClarinLicenseResourceUserAllowanceService clarinLicenseResourceUserAllowanceService; + @Autowired + ClarinLicenseResourceMappingService clarinLicenseResourceMappingService; + @Autowired + BitstreamService bitstreamService; + + /** + * Check if the current user is authorized to download the bitstream in the three steps: + * 1. If the current user is submitter of the item where the current bitstream is -> the user is authorized. + * 2. If the request contains token which is verified -> the user is authorized. + * 3. If the bitstream license requires confirmation every time or the user didn't fill in required + * metadata for the bitstream's license -> the user is not authorized. + * @param context + * @return + * @throws SQLException + */ + public boolean authorizeBitstream(Context context, Bitstream bitstream) throws SQLException, + AuthorizeException { + if (Objects.isNull(bitstream)) { + return false; + } + if (Objects.isNull(context)) { + return false; + } + + // Load the current user + EPerson currentUser = context.getCurrentUser(); + // Load the current user ID or if the user do not exist set ID to null + UUID userID = null; // user not logged in + if (Objects.nonNull(currentUser)) { + userID = currentUser.getID(); + } + + UUID bitstreamUUID = bitstream.getID(); + // 1. If the current user is submitter of the item where the current bitstream is -> the user is authorized. + if (userIsSubmitter(context, bitstream, currentUser, userID)) { + return true; + } + + // 2. If the request contains token which is verified -> the user is authorized. + if (isTokenVerified(context, bitstreamUUID)) { + return true; + } + + // 3. If the bitstream license requires confirmation every time or the user didn't fill in required + // metadata for the bitstream's license -> the user is not authorized. + return isUserAllowedToAccessTheResource(context, userID, bitstreamUUID); + } + + /** + * Do not allow download for anonymous users. Allow it only if the bitstream has Clarin License and the license has + * confirmation = 3 (allow anonymous). + * + * @param context DSpace context object + * @param bitstreamID downloading Bitstream UUID + * @return if the current user is authorized + */ + public boolean authorizeLicenseWithUser(Context context, UUID bitstreamID) throws SQLException { + // If the current user is null that means that the user is not signed + if (Objects.nonNull(context.getCurrentUser())) { + // User is signed + return true; + } + + // Get ClarinLicenseResourceMapping where the bitstream is mapped with clarin license + List clarinLicenseResourceMappings = + clarinLicenseResourceMappingService.findByBitstreamUUID(context, bitstreamID); + + // Bitstream does not have Clarin License + if (CollectionUtils.isEmpty(clarinLicenseResourceMappings)) { + return true; + } + + // Bitstream should have only one type of the Clarin license, so we could get first record + ClarinLicense clarinLicense = Objects.requireNonNull(clarinLicenseResourceMappings.get(0)).getLicense(); + // ALLOW_ANONYMOUS - Allow download for anonymous users, but with license confirmation + // NOT_REQUIRED - License confirmation is not required + if ((clarinLicense.getConfirmation() == Confirmation.ALLOW_ANONYMOUS) || + (clarinLicense.getConfirmation() == Confirmation.NOT_REQUIRED)) { + return true; + } + return false; + } + + private boolean userIsSubmitter(Context context, Bitstream bitstream, EPerson currentUser, UUID userID) { + try { + // Load Bitstream's Item, the Item contains the Bitstream + Item item = (Item) bitstreamService.getParentObject(context, bitstream); + + // If the Item is submitted by the current user, the submitter is always authorized to access his own + // bitstream + EPerson submitter = null; + if (Objects.nonNull(item)) { + submitter = item.getSubmitter(); + } + + if (Objects.nonNull(submitter) && Objects.nonNull(userID)) { + if (Objects.nonNull(currentUser) && + StringUtils.equals(submitter.getID().toString(), userID.toString())) { + return true; + } + } + } catch (SQLException sqle) { + log.error("Failed to get parent object for bitstream", sqle); + return false; + } catch (ClassCastException ex) { + // parent object is not an Item + // special bitstreams e.g. images of community/collection + return false; + } + + return false; + } + + private boolean isTokenVerified(Context context, UUID bitstreamID) throws DownloadTokenExpiredException, + SQLException { + // Load the current request. + Request currentRequest = new DSpace().getRequestService().getCurrentRequest(); + if (Objects.isNull(currentRequest)) { + return false; + } + + HttpServletRequest request = currentRequest.getHttpServletRequest(); + if (Objects.isNull(request)) { + return false; + } + + // Load the token from the request + String dtoken = null; + try { + dtoken = request.getParameter("dtoken"); + } catch (IllegalStateException e) { + //If the dspace kernel is null (eg. when we get here from OAI) + } catch (Exception e) { + // + } + + if (StringUtils.isBlank(dtoken)) { + return false; + } + + boolean tokenFound = clarinLicenseResourceUserAllowanceService.verifyToken(context, bitstreamID, dtoken); + // Check token + if (tokenFound) { // database token match with url token + return true; + } else { + throw new DownloadTokenExpiredException("The download token is invalid or expires."); + } + } + + /** + * Check if the Clarin License attached to the downloading bitstream requires custom user information and + * check if the current user has filled in that required info in the past. + * @param context DSpace context object + * @param userID UUID of the current user + * @param bitstreamID UUID of the downloading bitstream + */ + private boolean isUserAllowedToAccessTheResource(Context context, UUID userID, UUID bitstreamID) + throws MissingLicenseAgreementException, SQLException { + boolean allowed = clarinLicenseResourceUserAllowanceService + .isUserAllowedToAccessTheResource(context, userID, bitstreamID); + + if (!allowed) { + throw new MissingLicenseAgreementException("Missing license agreement!"); + } + return true; + } +} diff --git a/dspace-api/src/main/java/org/dspace/authorize/DownloadTokenExpiredException.java b/dspace-api/src/main/java/org/dspace/authorize/DownloadTokenExpiredException.java new file mode 100644 index 000000000000..03c9319135fe --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/authorize/DownloadTokenExpiredException.java @@ -0,0 +1,19 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.authorize; + +/** + * If the token for downloading the bitstream with attached Clarin License is expired. + */ +public class DownloadTokenExpiredException extends AuthorizeException { + public static String NAME = "DownloadTokenExpiredException"; + + public DownloadTokenExpiredException(String message) { + super(message); + } +} diff --git a/dspace-api/src/main/java/org/dspace/authorize/MissingLicenseAgreementException.java b/dspace-api/src/main/java/org/dspace/authorize/MissingLicenseAgreementException.java new file mode 100644 index 000000000000..b0d16bcdafca --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/authorize/MissingLicenseAgreementException.java @@ -0,0 +1,19 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.authorize; + +/** + * If the Clarin License which the bitstream is attached to needs the required info which the current user + * hasn't filled in. + */ +public class MissingLicenseAgreementException extends AuthorizeException { + public static String NAME = "MissingLicenseAgreementException"; + public MissingLicenseAgreementException(String message) { + super(message); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/DspaceObjectClarinServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/DspaceObjectClarinServiceImpl.java new file mode 100644 index 000000000000..170d7a4537af --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/DspaceObjectClarinServiceImpl.java @@ -0,0 +1,83 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content; + +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; + +import org.apache.commons.collections4.CollectionUtils; +import org.dspace.content.service.DspaceObjectClarinService; +import org.dspace.content.service.WorkspaceItemService; +import org.dspace.core.Constants; +import org.dspace.core.Context; + +/** + * Additional service implementation for the DspaceObject in Clarin-DSpace. + * + * @author Michaela Paurikova (michaela.paurikova at dataquest.sk) + */ +public class DspaceObjectClarinServiceImpl implements DspaceObjectClarinService { + private WorkspaceItemService workspaceItemService; + @Override + public Community getPrincipalCommunity(Context context, DSpaceObject dso) throws SQLException { + int type = dso.getType(); + // dso is community + if (type == Constants.COMMUNITY) { + return (Community) dso; + } + + Collection collection = this.getCollectionOfDSO(context, dso, type); + // collection doesn't have the community + if (Objects.isNull(collection)) { + return null; + } + + List communities = collection.getCommunities(); + // collection doesn't have the community + if (CollectionUtils.isEmpty(communities)) { + return null; + } + + // principal community is in the first index + return communities.get(0); + } + + /** + * Return the collection where belongs current DSpaceObject + * @param context DSpaceObject contenxt + * @param dso DSpaceObject Collection or Item + * @param type number representation of DSpaceObject type + * @return Collection of the dso + * @throws SQLException database error + */ + private Collection getCollectionOfDSO(Context context, DSpaceObject dso, int type) throws SQLException { + // the dso is Collection + if (type == Constants.COLLECTION) { + return (Collection) dso; + } + + // the dso is not the Item it doesn't have Collection + if (type != Constants.ITEM) { + return null; + } + + Collection collection; + collection = ((Item) dso).getOwningCollection(); + if (Objects.nonNull(collection)) { + return collection; + } + + // the dso doesn't have owning collection try to find the collection from the wi + WorkspaceItem wi = workspaceItemService.findByItem(context, (Item)dso); + if (Objects.isNull(wi)) { + return null; + } + return wi.getCollection(); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/authority/SimpleORCIDAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/SimpleORCIDAuthority.java new file mode 100644 index 000000000000..f76cee9be111 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/authority/SimpleORCIDAuthority.java @@ -0,0 +1,164 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.authority; + +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.external.CachingOrcidRestConnector; +import org.dspace.external.provider.orcid.xml.ExpandedSearchConverter; +import org.dspace.utils.DSpace; + + +/** + * ChoiceAuthority using the ORCID API. + * It uses the orcid as the authority value and thus is simpler to use then the * SolrAuthority. + */ +public class SimpleORCIDAuthority implements ChoiceAuthority { + + private static final Logger log = LogManager.getLogger(SimpleORCIDAuthority.class); + + private String pluginInstanceName; + private final CachingOrcidRestConnector orcidRestConnector = new DSpace().getServiceManager().getServiceByName( + "CachingOrcidRestConnector", CachingOrcidRestConnector.class); + private static final int maxResults = 100; + + /** + * Get all values from the authority that match the preferred value. + * Note that the offering was entered by the user and may contain + * mixed/incorrect case, whitespace, etc so the plugin should be careful + * to clean up user data before making comparisons. + *

+ * Value of a "Name" field will be in canonical DSpace person name format, + * which is "Lastname, Firstname(s)", e.g. "Smith, John Q.". + *

+ * Some authorities with a small set of values may simply return the whole + * set for any sample value, although it's a good idea to set the + * defaultSelected index in the Choices instance to the choice, if any, + * that matches the value. + * + * @param text user's value to match + * @param start choice at which to start, 0 is first. + * @param limit maximum number of choices to return, 0 for no limit. + * @param locale explicit localization key if available, or null + * @return a Choices object (never null). + */ + @Override + public Choices getMatches(String text, int start, int limit, String locale) { + log.debug("getMatches: " + text + ", start: " + start + ", limit: " + limit + ", locale: " + locale); + if (text == null || text.trim().isEmpty()) { + return new Choices(true); + } + + start = Math.max(start, 0); + if (limit < 1 || limit > maxResults) { + limit = maxResults; + } + + ExpandedSearchConverter.Results search = orcidRestConnector.search(text, start, limit); + List choices = search.results().stream() + .map(this::toChoice) + .collect(Collectors.toList()); + + + int confidence = !search.isOk() ? Choices.CF_FAILED : + choices.isEmpty() ? Choices.CF_NOTFOUND : + choices.size() == 1 ? Choices.CF_UNCERTAIN + : Choices.CF_AMBIGUOUS; + int total = search.numFound().intValue(); + return new Choices(choices.toArray(new Choice[0]), start, total, + confidence, total > (start + limit)); + } + + /** + * Get the single "best" match (if any) of a value in the authority + * to the given user value. The "confidence" element of Choices is + * expected to be set to a meaningful value about the circumstances of + * this match. + *

+ * This call is typically used in non-interactive metadata ingest + * where there is no interactive agent to choose from among options. + * + * @param text user's value to match + * @param locale explicit localization key if available, or null + * @return a Choices object (never null) with 1 or 0 values. + */ + @Override + public Choices getBestMatch(String text, String locale) { + log.debug("getBestMatch: " + text); + Choices matches = getMatches(text, 0, 1, locale); + if (matches.values.length != 0 && !matches.values[0].value.equalsIgnoreCase(text)) { + // novalue + matches = new Choices(false); + } + return matches; + } + + /** + * Get the canonical user-visible "label" (i.e. short descriptive text) + * for a key in the authority. Can be localized given the implicit + * or explicit locale specification. + *

+ * This may get called many times while populating a Web page so it should + * be implemented as efficiently as possible. + * + * @param key authority key known to this authority. + * @param locale explicit localization key if available, or null + * @return descriptive label - should always return something, never null. + */ + @Override + public String getLabel(String key, String locale) { + log.debug("getLabel: " + key); + String label = orcidRestConnector.getLabel(key); + return label != null ? label : key; + } + + /** + * Get the instance's particular name. + * Returns the name by which the class was chosen when + * this instance was created. Only works for instances created + * by PluginService, or if someone remembers to call setPluginName. + *

+ * Useful when the implementation class wants to be configured differently + * when it is invoked under different names. + * + * @return name or null if not available. + */ + @Override + public String getPluginInstanceName() { + return pluginInstanceName; + } + + /** + * Set the name under which this plugin was instantiated. + * Not to be invoked by application code, it is + * called automatically by PluginService.getNamedPlugin() + * when the plugin is instantiated. + * + * @param name -- name used to select this class. + */ + @Override + public void setPluginInstanceName(String name) { + this.pluginInstanceName = name; + } + + private Choice toChoice(ExpandedSearchConverter.Result result) { + Choice c = new Choice(result.authority(), result.value(), result.label()); + //add orcid to extras so it's shown + c.extras.put("orcid", result.authority()); + // add the value to extra information only if it is present + //in dspace-angular the extras are keys for translation form.other-information. + result.creditName().ifPresent(val -> c.extras.put("credit-name", val)); + result.otherNames().ifPresent(val -> c.extras.put("other-names", val)); + result.institutionNames().ifPresent(val -> c.extras.put("institution", val)); + + return c; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinFeaturedService.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinFeaturedService.java new file mode 100644 index 000000000000..d55dfbef482c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinFeaturedService.java @@ -0,0 +1,80 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * This is NOT a service. + * This class is representing a featured service in the ref box (item view). The featured services are defined + * in the `clarin-dspace.cfg` file. + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class ClarinFeaturedService { + + public ClarinFeaturedService() { + } + + /** + * The name of the Featured Service e.g. `kontext` + */ + private String name; + + /** + * The URL of the Featured Service, it's where the user will be redirected after clicking on it in the ref box. + */ + private String url; + + /** + * Some description of the Featured service. + */ + private String description; + + /** + * Links for the Featured Services in the more languages. + */ + private List featuredServiceLinks; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getFeaturedServiceLinks() { + if (Objects.isNull(featuredServiceLinks)) { + featuredServiceLinks = new ArrayList<>(); + } + return featuredServiceLinks; + } + + public void setFeaturedServiceLinks(List featuredServiceLinks) { + this.featuredServiceLinks = featuredServiceLinks; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinFeaturedServiceLink.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinFeaturedServiceLink.java new file mode 100644 index 000000000000..a6db134a15d1 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinFeaturedServiceLink.java @@ -0,0 +1,37 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +/** + * This is NOT a service. + * This class is representing a featured service link in the ref box (item view). The featured services are defined + * in the `clarin-dspace.cfg` file. + * This class holds the link for redirecting to the Featured Service in the another language. + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class ClarinFeaturedServiceLink { + private String key; + private String value; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinItemServiceImpl.java new file mode 100644 index 000000000000..018964b4cbf0 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinItemServiceImpl.java @@ -0,0 +1,277 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.apache.logging.log4j.Logger; +import org.dspace.content.Bitstream; +import org.dspace.content.Bundle; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataValue; +import org.dspace.content.dao.clarin.ClarinItemDAO; +import org.dspace.content.service.CollectionService; +import org.dspace.content.service.ItemService; +import org.dspace.content.service.clarin.ClarinItemService; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Service implementation for the Item object. + * This service is enhancement of the ItemService service for Clarin project purposes. + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class ClarinItemServiceImpl implements ClarinItemService { + + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(ClarinItemServiceImpl.class); + private static final String DELIMETER = ","; + private static final String NO_YEAR = "0000"; + + @Autowired + ClarinItemDAO clarinItemDAO; + + @Autowired + CollectionService collectionService; + + @Autowired + ItemService itemService; + + @Override + public List findByBitstreamUUID(Context context, UUID bitstreamUUID) throws SQLException { + return clarinItemDAO.findByBitstreamUUID(context, bitstreamUUID); + } + + @Override + public List findByHandle(Context context, MetadataField metadataField, String handle) throws SQLException { + return clarinItemDAO.findByHandle(context, metadataField, handle); + } + + @Override + public Community getOwningCommunity(Context context, DSpaceObject dso) { + if (Objects.isNull(dso)) { + return null; + } + int type = dso.getType(); + if (Objects.equals(type, Constants.COMMUNITY)) { + return (Community) dso; + } + + Collection owningCollection = null; + if (Objects.equals(type, Constants.COLLECTION)) { + owningCollection = (Collection) dso; + } + + if (Objects.equals(type, Constants.ITEM)) { + owningCollection = ((Item) dso).getOwningCollection(); + } + + if (Objects.isNull(owningCollection)) { + return null; + } + + try { + List communities = owningCollection.getCommunities(); + if (CollectionUtils.isEmpty(communities)) { + log.error("Community list of the owning collection is empty."); + return null; + } + + // First community is the owning community. + Community owningCommunity = communities.get(0); + if (Objects.isNull(owningCommunity)) { + log.error("Owning community is null."); + return null; + } + + return owningCommunity; + } catch (SQLException e) { + log.error("Cannot getOwningCommunity for the Item: " + dso.getID() + ", because: " + e.getSQLState()); + } + + return null; + } + + @Override + public Community getOwningCommunity(Context context, UUID owningCollectionId) throws SQLException { + Collection owningCollection = collectionService.find(context, owningCollectionId); + + if (Objects.isNull(owningCollection)) { + return null; + } + + try { + List communities = owningCollection.getCommunities(); + if (CollectionUtils.isEmpty(communities)) { + log.error("Community list of the owning collection is empty."); + return null; + } + + // First community is the owning community. + Community owningCommunity = communities.get(0); + if (Objects.isNull(owningCommunity)) { + log.error("Owning community is null."); + return null; + } + + return owningCommunity; + } catch (SQLException e) { + log.error("Cannot getOwningCommunity for the Collection: " + owningCollectionId + + ", because: " + e.getSQLState()); + } + return null; + } + + @Override + public void updateItemFilesMetadata(Context context, Item item) throws SQLException { + List originalBundles = itemService.getBundles(item, Constants.CONTENT_BUNDLE_NAME); + if (Objects.nonNull(originalBundles.get(0))) { + updateItemFilesMetadata(context, item, originalBundles.get(0)); + } else { + log.error("Cannot update item files metadata because the ORIGINAL bundle is null."); + } + } + + @Override + public void updateItemFilesMetadata(Context context, Item item, Bundle bundle) throws SQLException { + if (!Objects.equals(bundle.getName(), Constants.CONTENT_BUNDLE_NAME)) { + return; + } + + if (Objects.isNull(item)) { + log.error("Cannot update the item files metadata because the item is null."); + return; + } + + int totalNumberOfFiles = 0; + long totalSizeofFiles = 0; + + /* Add local.has.files metadata */ + boolean hasFiles = false; + List origs = itemService.getBundles(item, Constants.CONTENT_BUNDLE_NAME); + for (Bundle orig : origs) { + if (CollectionUtils.isNotEmpty(orig.getBitstreams())) { + hasFiles = true; + } + for (Bitstream bit : orig.getBitstreams()) { + totalNumberOfFiles ++; + totalSizeofFiles += bit.getSizeBytes(); + } + } + + itemService.clearMetadata(context, item, "local", "has", "files", Item.ANY); + itemService.clearMetadata(context, item, "local", "files", "count", Item.ANY); + itemService.clearMetadata(context, item, "local", "files", "size", Item.ANY); + if ( hasFiles ) { + itemService.addMetadata(context, item, "local", "has", "files", Item.ANY, "yes"); + } else { + itemService.addMetadata(context, item,"local", "has", "files", Item.ANY, "no"); + } + itemService.addMetadata(context, item,"local", "files", "count", Item.ANY, "" + totalNumberOfFiles); + itemService.addMetadata(context, item,"local", "files", "size", Item.ANY, "" + totalSizeofFiles); + } + + @Override + public void updateItemFilesMetadata(Context context, Bitstream bit) throws SQLException { + // Get the Item the bitstream is associated with + Item item = null; + Bundle bundle = null; + List origs = bit.getBundles(); + for (Bundle orig : origs) { + if (!Constants.CONTENT_BUNDLE_NAME.equals(orig.getName())) { + continue; + } + + List items = orig.getItems(); + if (CollectionUtils.isEmpty(items)) { + continue; + } + + item = items.get(0); + bundle = orig; + break; + } + + // It could be null when the bundle name is e.g. `LICENSE` + if (Objects.isNull(item) || Objects.isNull(bundle)) { + return; + } + this.updateItemFilesMetadata(context, item, bundle); + } + + @Override + public void updateItemDatesMetadata(Context context, Item item) throws SQLException { + if (Objects.isNull(context)) { + log.error("Cannot update item dates metadata because the context is null."); + return; + } + + List approximatedDates = + itemService.getMetadata(item, "local", "approximateDate", "issued", Item.ANY, false); + + if (CollectionUtils.isEmpty(approximatedDates) || StringUtils.isBlank(approximatedDates.get(0).getValue())) { + log.debug("Cannot update item dates metadata because the approximate date is empty."); + return; + } + + // Get the approximate date value from the metadata + String approximateDateValue = approximatedDates.get(0).getValue(); + + // Split the approximate date value by the delimeter and get the list of years. + List listOfYearValues = Arrays.asList(approximateDateValue.split(DELIMETER)); + // Trim the list of years - remove leading and trailing whitespaces + listOfYearValues.replaceAll(String::trim); + + try { + // Clear the current `dc.date.issued` metadata + itemService.clearMetadata(context, item, "dc", "date", "issued", Item.ANY); + + // Update the `dc.date.issued` metadata with a new value: `0000` or the last year from the sequence + if (CollectionUtils.isNotEmpty(listOfYearValues) && isListOfNumbers(listOfYearValues)) { + // Take the last year from the list of years and add it to the `dc.date.issued` metadata + itemService.addMetadata(context, item, "dc", "date", "issued", Item.ANY, + getLastNumber(listOfYearValues)); + } else { + // Add the `0000` value to the `dc.date.issued` metadata + itemService.addMetadata(context, item, "dc", "date", "issued", Item.ANY, NO_YEAR); + } + } catch (SQLException e) { + log.error("Cannot remove `dc.date.issued` metadata because: {}", e.getMessage()); + } + } + + public static boolean isListOfNumbers(List values) { + for (String value : values) { + if (!NumberUtils.isCreatable(value)) { + return false; + } + } + return true; + } + + private static String getLastNumber(List values) { + if (CollectionUtils.isEmpty(values)) { + return NO_YEAR; + } + return values.get(values.size() - 1); + } + + +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicense.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicense.java new file mode 100644 index 000000000000..eb540b75e313 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicense.java @@ -0,0 +1,227 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import org.apache.logging.log4j.Logger; +import org.dspace.content.Bitstream; +import org.dspace.core.ReloadableEntity; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + + +/** + * Class representing a clarin license in DSpace. + * Clarin License is license for the bitstreams of the item. The item could have only one type of the Clarin License. + * The Clarin License is selected in the submission process. + * Admin could manage Clarin Licenses in the License Administration page. + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@Entity +@Table(name = "license_definition") +public class ClarinLicense implements ReloadableEntity { + + private static Logger log = org.apache.logging.log4j.LogManager.getLogger(ClarinLicense.class); + + /** + * Required info key word. + */ + public static final String SEND_TOKEN = "SEND_TOKEN"; + public static final String EXTRA_EMAIL = "EXTRA_EMAIL"; + + @Id + @Column(name = "license_id") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "license_definition_license_id_seq") + @SequenceGenerator(name = "license_definition_license_id_seq", sequenceName = "license_definition_license_id_seq", + allocationSize = 1) + private Integer id; + + @ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST}) + @JoinTable( + name = "license_label_extended_mapping", + joinColumns = @JoinColumn(name = "license_id"), + inverseJoinColumns = @JoinColumn(name = "label_id")) + Set clarinLicenseLabels = new HashSet<>(); + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "license", cascade = CascadeType.PERSIST) + private List clarinLicenseResourceMappings = new ArrayList<>(); + + @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST}) + @JoinColumn(name = "user_registration_id") + private ClarinUserRegistration eperson; + + @Column(name = "name") + private String name = null; + + @Column(name = "definition") + private String definition = null; + + @Column(name = "confirmation") + @Enumerated(EnumType.ORDINAL) + // Hibernate 6 maps ORDINAL enums to TINYINT by default; the column is INTEGER (see Lindat + // schema migration), so pin the JDBC type to INTEGER to keep schema validation + upgrades working. + @JdbcTypeCode(SqlTypes.INTEGER) + private Confirmation confirmation = Confirmation.NOT_REQUIRED; + + @Column(name = "required_info") + private String requiredInfo = null; + + public ClarinLicense() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getDefinition() { + return definition; + } + + public void setDefinition(String definition) { + this.definition = definition; + } + + public Confirmation getConfirmation() { + return confirmation == null ? Confirmation.NOT_REQUIRED : confirmation; + } + + public void setConfirmation(Confirmation confirmation) { + this.confirmation = confirmation; + } + + public String getRequiredInfo() { + return requiredInfo; + } + + public void setRequiredInfo(String requiredInfo) { + this.requiredInfo = requiredInfo; + } + + public List getLicenseLabels() { + ClarinLicenseLabel[] output = clarinLicenseLabels.toArray(new ClarinLicenseLabel[] {}); + return Arrays.asList(output); + } + + public void setLicenseLabels(Set clarinLicenseLabels) { + this.clarinLicenseLabels = clarinLicenseLabels; + } + + public List getClarinLicenseResourceMappings() { + return clarinLicenseResourceMappings; + } + + /** + * The bitstream is not removed from the database after deleting the item, but is set as `deleted`. + * Do not count deleted bitstreams for the clarin license. + * @return count of the non deleted bitstream assigned to the current clarin license. + */ + public int getNonDeletedBitstreams() { + int counter = 0; + + for (ClarinLicenseResourceMapping clrm : clarinLicenseResourceMappings) { + Bitstream bitstream = clrm.getBitstream(); + try { + if (bitstream.isDeleted()) { + continue; + } + counter++; + } catch (SQLException e) { + log.error("Cannot find out if the bitstream: " + bitstream.getID() + " is deleted."); + } + } + return counter; + } + + public ClarinLicenseLabel getNonExtendedClarinLicenseLabel() { + for (ClarinLicenseLabel cll : getLicenseLabels()) { + if (!cll.isExtended()) { + return cll; + } + } + return null; + } + + @Override + public Integer getID() { + return id; + } + + public Set getClarinLicenseLabels() { + return clarinLicenseLabels; + } + + public void setClarinLicenseLabels(Set clarinLicenseLabels) { + this.clarinLicenseLabels = clarinLicenseLabels; + } + + public ClarinUserRegistration getEperson() { + return eperson; + } + + public void setEperson(ClarinUserRegistration eperson) { + this.eperson = eperson; + } + + public enum Confirmation { + + // if new Confirmation value is needed, add it to the end of this list, to not break the backward compatibility + NOT_REQUIRED(0), ASK_ONLY_ONCE(1), ASK_ALWAYS(2), ALLOW_ANONYMOUS(3); + + private final int value; + + Confirmation(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static Confirmation getConfirmation(int value) { + return Arrays.stream(values()) + .filter(v -> (v.getValue() == value)) + .findFirst() + .orElseThrow(() -> + new IllegalArgumentException("No license confirmation found for value: " + value)); + } + + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseLabel.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseLabel.java new file mode 100644 index 000000000000..d74cd39d4105 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseLabel.java @@ -0,0 +1,115 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import org.dspace.core.ReloadableEntity; + +/** + * Class representing a clarin license label of the clarin license. The clarin license could have one + * non-extended license label and multiple extended license labels. + * The license label could be defined in the License Administration table. + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +@Entity +@Table(name = "license_label") +public class ClarinLicenseLabel implements ReloadableEntity { + + @Id + @Column(name = "label_id") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "license_label_label_id_seq") + @SequenceGenerator(name = "license_label_label_id_seq", sequenceName = "license_label_label_id_seq", + allocationSize = 1) + private Integer id; + + @Column(name = "label") + private String label = null; + + @Column(name = "title") + private String title = null; + + @Column(name = "is_extended") + private boolean isExtended = false; + + @Column(name = "icon") + private byte[] icon = null; + + @ManyToMany(mappedBy = "clarinLicenseLabels") + List licenses = new ArrayList<>(); + + public ClarinLicenseLabel() { + } + + public void setId(Integer id) { + this.id = id; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public boolean isExtended() { + return isExtended; + } + + public void setExtended(boolean extended) { + isExtended = extended; + } + + public List getLicenses() { + return licenses; + } + + public void setLicenses(List licenses) { + this.licenses = licenses; + } + + public void addLicense(ClarinLicense license) { + if (Objects.isNull(this.licenses)) { + this.licenses = new ArrayList<>(); + } + this.licenses.add(license); + } + + public byte[] getIcon() { + return icon; + } + + public void setIcon(byte[] icon) { + this.icon = icon; + } + + @Override + public Integer getID() { + return id; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseLabelServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseLabelServiceImpl.java new file mode 100644 index 000000000000..faa6f02bc933 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseLabelServiceImpl.java @@ -0,0 +1,109 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.dao.clarin.ClarinLicenseLabelDAO; +import org.dspace.content.service.clarin.ClarinLicenseLabelService; +import org.dspace.core.Context; +import org.dspace.core.LogHelper; +import org.hibernate.ObjectNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Service implementation for the Clarin License Label object. + * This class is responsible for all business logic calls for the Clarin License Label object and is autowired + * by spring. + * This class should never be accessed directly. + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class ClarinLicenseLabelServiceImpl implements ClarinLicenseLabelService { + + private static final Logger log = LoggerFactory.getLogger(ClarinLicenseServiceImpl.class); + + @Autowired + ClarinLicenseLabelDAO clarinLicenseLabelDAO; + + @Autowired + AuthorizeService authorizeService; + + @Override + public ClarinLicenseLabel create(Context context) throws SQLException, AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "You must be an admin to create an CLARIN License Label"); + } + + // Create a table row + ClarinLicenseLabel clarinLicenseLabel = clarinLicenseLabelDAO.create(context, new ClarinLicenseLabel()); + log.info(LogHelper.getHeader(context, "create_clarin_license_label", "clarin_license_label_id=" + + clarinLicenseLabel.getID())); + + return clarinLicenseLabel; + } + + @Override + public ClarinLicenseLabel create(Context context, ClarinLicenseLabel clarinLicenseLabel) throws SQLException, + AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "You must be an admin to create an CLARIN License Label"); + } + + return clarinLicenseLabelDAO.create(context, clarinLicenseLabel); + } + + @Override + public ClarinLicenseLabel find(Context context, int valueId) throws SQLException { + return clarinLicenseLabelDAO.findByID(context, ClarinLicenseLabel.class, valueId); + } + + @Override + public List findAll(Context context) throws SQLException, AuthorizeException { + return clarinLicenseLabelDAO.findAll(context, ClarinLicenseLabel.class); + } + + @Override + public void delete(Context context, ClarinLicenseLabel license) throws SQLException, AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "You must be an admin to create an CLARIN License Label"); + } + + clarinLicenseLabelDAO.delete(context, license); + } + + @Override + public void update(Context context, ClarinLicenseLabel newClarinLicenseLabel) throws SQLException, + AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "You must be an admin to create an CLARIN License Label"); + } + + if (Objects.isNull(newClarinLicenseLabel)) { + throw new IllegalArgumentException("Cannot update licenseLabel because the clarinLicenseLabel is null"); + } + + ClarinLicenseLabel foundClarinLicenseLabel = find(context, newClarinLicenseLabel.getID()); + if (Objects.isNull(foundClarinLicenseLabel)) { + throw new ObjectNotFoundException(newClarinLicenseLabel.getID(), "Cannot update the clarinLicenseLabel " + + "because the licenseLabel wasn't found in the database."); + } + + clarinLicenseLabelDAO.save(context, newClarinLicenseLabel); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseResourceMapping.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseResourceMapping.java new file mode 100644 index 000000000000..a7f13bd19d03 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseResourceMapping.java @@ -0,0 +1,85 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import org.dspace.content.Bitstream; +import org.dspace.core.ReloadableEntity; + +@Entity +@Table(name = "license_resource_mapping") +public class ClarinLicenseResourceMapping implements ReloadableEntity { + + @Id + @Column(name = "mapping_id") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "license_resource_mapping_mapping_id_seq") + @SequenceGenerator(name = "license_resource_mapping_mapping_id_seq", + sequenceName = "license_resource_mapping_mapping_id_seq", + allocationSize = 1) + private Integer id; + + @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST}) + @JoinColumn(name = "license_id") + private ClarinLicense license; + + @OneToOne(cascade = {CascadeType.PERSIST}) + @JoinColumn(name = "bitstream_uuid", referencedColumnName = "uuid") + private Bitstream bitstream; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "licenseResourceMapping", cascade = CascadeType.PERSIST) + private List licenseResourceUserAllowances = new ArrayList<>(); + + @Override + public Integer getID() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Bitstream getBitstream() { + return bitstream; + } + + public void setBitstream(Bitstream bitstream) { + this.bitstream = bitstream; + } + + public ClarinLicense getLicense() { + return license; + } + + public void setLicense(ClarinLicense license) { + this.license = license; + } + + public List getLicenseResourceUserAllowances() { + return licenseResourceUserAllowances; + } + + public void setLicenseResourceUserAllowances(List + licenseResourceUserAllowances) { + this.licenseResourceUserAllowances = licenseResourceUserAllowances; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseResourceMappingServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseResourceMappingServiceImpl.java new file mode 100644 index 000000000000..60391c7ab3ce --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseResourceMappingServiceImpl.java @@ -0,0 +1,269 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import static org.dspace.content.clarin.ClarinLicense.Confirmation; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import jakarta.ws.rs.NotFoundException; +import org.apache.commons.collections4.CollectionUtils; +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.Bitstream; +import org.dspace.content.dao.clarin.ClarinLicenseResourceMappingDAO; +import org.dspace.content.service.BitstreamService; +import org.dspace.content.service.clarin.ClarinLicenseResourceMappingService; +import org.dspace.content.service.clarin.ClarinLicenseResourceUserAllowanceService; +import org.dspace.content.service.clarin.ClarinLicenseService; +import org.dspace.core.Context; +import org.dspace.core.LogHelper; +import org.hibernate.ObjectNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +public class ClarinLicenseResourceMappingServiceImpl implements ClarinLicenseResourceMappingService { + + private static final Logger log = LoggerFactory.getLogger(ClarinLicenseServiceImpl.class); + + @Autowired + ClarinLicenseResourceMappingDAO clarinLicenseResourceMappingDAO; + @Autowired + ClarinLicenseResourceUserAllowanceService clarinLicenseResourceUserAllowanceService; + + @Autowired + ClarinLicenseService clarinLicenseService; + + @Autowired + BitstreamService bitstreamService; + + @Autowired + AuthorizeService authorizeService; + + @Override + public ClarinLicenseResourceMapping create(Context context) throws SQLException { + // Create a table row + ClarinLicenseResourceMapping clarinLicenseResourceMapping = clarinLicenseResourceMappingDAO.create(context, + new ClarinLicenseResourceMapping()); + + log.info(LogHelper.getHeader(context, "create_clarin_license_resource_mapping", + "clarin_license_resource_mapping_id=" + clarinLicenseResourceMapping.getID())); + + return clarinLicenseResourceMapping; + } + + @Override + public ClarinLicenseResourceMapping create(Context context, + ClarinLicenseResourceMapping clarinLicenseResourceMapping) + throws SQLException { + return clarinLicenseResourceMappingDAO.create(context, clarinLicenseResourceMapping); + } + + @Override + public ClarinLicenseResourceMapping create(Context context, Integer licenseId, UUID bitstreamUuid) + throws SQLException { + ClarinLicenseResourceMapping clarinLicenseResourceMapping = new ClarinLicenseResourceMapping(); + ClarinLicense clarinLicense = clarinLicenseService.find(context, licenseId); + if (Objects.isNull(clarinLicense)) { + throw new NotFoundException("Cannot find the license with id: " + licenseId); + } + + Bitstream bitstream = bitstreamService.find(context, bitstreamUuid); + if (Objects.isNull(bitstream)) { + throw new NotFoundException("Cannot find the bitstream with id: " + bitstreamUuid); + } + clarinLicenseResourceMapping.setLicense(clarinLicense); + clarinLicenseResourceMapping.setBitstream(bitstream); + + return clarinLicenseResourceMappingDAO.create(context, clarinLicenseResourceMapping); + } + + @Override + public ClarinLicenseResourceMapping find(Context context, int valueId) throws SQLException { + return clarinLicenseResourceMappingDAO.findByID(context, ClarinLicenseResourceMapping.class, valueId); + } + + @Override + public List findAll(Context context) throws SQLException { + return clarinLicenseResourceMappingDAO.findAll(context, ClarinLicenseResourceMapping.class); + } + + @Override + public List findAllByLicenseId(Context context, Integer licenseId) + throws SQLException { + List mappings = + clarinLicenseResourceMappingDAO.findAll(context, ClarinLicenseResourceMapping.class); + List mappingsByLicenseId = new ArrayList<>(); + for (ClarinLicenseResourceMapping mapping: mappings) { + if (Objects.equals(mapping.getLicense().getID(), licenseId)) { + mappingsByLicenseId.add(mapping); + } + } + return mappingsByLicenseId; + } + + @Override + public void update(Context context, ClarinLicenseResourceMapping newClarinLicenseResourceMapping) + throws SQLException { + if (Objects.isNull(newClarinLicenseResourceMapping)) { + throw new IllegalArgumentException("Cannot update clarin license resource mapping because " + + "the new clarin license resource mapping is null"); + } + + ClarinLicenseResourceMapping foundClarinLicenseResourceMapping = + find(context, newClarinLicenseResourceMapping.getID()); + if (Objects.isNull(foundClarinLicenseResourceMapping)) { + throw new ObjectNotFoundException(newClarinLicenseResourceMapping.getID(), "Cannot update " + + "the license resource mapping because" + + " the clarin license resource mapping wasn't found " + + "in the database."); + } + + clarinLicenseResourceMappingDAO.save(context, newClarinLicenseResourceMapping); + } + + @Override + public void delete(Context context, ClarinLicenseResourceMapping clarinLicenseResourceMapping) + throws SQLException { + clarinLicenseResourceMappingDAO.delete(context, clarinLicenseResourceMapping); + } + + @Override + public void detachLicenses(Context context, Bitstream bitstream) throws SQLException { + List clarinLicenseResourceMappings = + clarinLicenseResourceMappingDAO.findByBitstreamUUID(context, bitstream.getID()); + + if (CollectionUtils.isEmpty(clarinLicenseResourceMappings)) { + log.info("Cannot detach licenses because bitstream with id: " + bitstream.getID() + " is not " + + "attached to any license."); + return; + } + + clarinLicenseResourceMappings.forEach(clarinLicenseResourceMapping -> { + try { + this.delete(context, clarinLicenseResourceMapping); + } catch (SQLException e) { + log.error(e.getMessage()); + } + }); + } + + @Override + public void attachLicense(Context context, ClarinLicense clarinLicense, Bitstream bitstream) throws SQLException { + ClarinLicenseResourceMapping clarinLicenseResourceMapping = this.create(context); + if (Objects.isNull(clarinLicenseResourceMapping)) { + throw new NotFoundException("Cannot create the ClarinLicenseResourceMapping."); + } + if (Objects.isNull(clarinLicense) || Objects.isNull(bitstream)) { + throw new IllegalArgumentException("CLARIN License or Bitstream cannot be null."); + } + + clarinLicenseResourceMapping.setBitstream(bitstream); + clarinLicenseResourceMapping.setLicense(clarinLicense); + + clarinLicenseResourceMappingDAO.save(context, clarinLicenseResourceMapping); + } + + @Override + public List findByBitstreamUUID(Context context, UUID bitstreamID) + throws SQLException { + return clarinLicenseResourceMappingDAO.findByBitstreamUUID(context, bitstreamID); + } + + public List findByBitstreamUUIDs(Context context, List bitstreamIDs) + throws SQLException { + return clarinLicenseResourceMappingDAO.findByBitstreamUUIDs(context, bitstreamIDs); + } + + @Override + public ClarinLicense getLicenseToAgree(Context context, UUID userId, UUID resourceID) throws SQLException { + // Load Clarin License for current bitstream. + List clarinLicenseResourceMappings = + clarinLicenseResourceMappingDAO.findByBitstreamUUID(context, resourceID); + + // Check there is mappings for the clarin license and bitstream + if (CollectionUtils.isEmpty(clarinLicenseResourceMappings)) { + return null; + } + + // Get the first resource mapping, get only fist - there shouldn't b more mappings + ClarinLicenseResourceMapping clarinLicenseResourceMapping = clarinLicenseResourceMappings.get(0); + if (Objects.isNull(clarinLicenseResourceMapping)) { + return null; + } + + // Get Clarin License from resource mapping to get confirmation policies. + ClarinLicense clarinLicenseToAgree = clarinLicenseResourceMapping.getLicense(); + if (Objects.isNull(clarinLicenseToAgree)) { + return null; + } + + // Confirmation states: + // NOT_REQUIRED + // ASK_ONLY_ONCE + // ASK_ALWAYS + // ALLOW_ANONYMOUS + if (clarinLicenseToAgree.getConfirmation() == Confirmation.NOT_REQUIRED) { + return null; + } + + switch (clarinLicenseToAgree.getConfirmation()) { + case ASK_ONLY_ONCE: + // Ask only once - check if the clarin license required info is filled in by the user + if (userFilledInRequiredInfo(context, clarinLicenseResourceMapping, userId)) { + return null; + } + return clarinLicenseToAgree; + case ASK_ALWAYS: + case ALLOW_ANONYMOUS: + return clarinLicenseToAgree; + default: + return null; + } + } + + private boolean userFilledInRequiredInfo(Context context, + ClarinLicenseResourceMapping clarinLicenseResourceMapping, UUID userID) + throws SQLException { + if (Objects.isNull(userID)) { + return false; + } + + // Find all records when the current user fill in some clarin license required info + List clarinLicenseResourceUserAllowances = + null; + try { + clarinLicenseResourceUserAllowances = clarinLicenseResourceUserAllowanceService.findByEPersonId(context, + userID); + } catch (AuthorizeException e) { + log.error("Cannot get the user registration for the user with id: {}", userID, e); + return false; + } + // The user hasn't been filled in any information. + if (CollectionUtils.isEmpty(clarinLicenseResourceUserAllowances)) { + return false; + } + + // The ClarinLicenseResourceMapping.id record is in the ClarinLicenseResourceUserAllowance + // that means the user added some information for the downloading bitstream's license. + for (ClarinLicenseResourceUserAllowance clrua : clarinLicenseResourceUserAllowances) { + int userAllowanceMappingID = clrua.getLicenseResourceMapping().getID(); + int resourceMappingID = clarinLicenseResourceMapping.getID(); + if (Objects.equals(userAllowanceMappingID, resourceMappingID)) { + return true; + } + } + + return false; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseResourceUserAllowance.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseResourceUserAllowance.java new file mode 100644 index 000000000000..b66adb0f9bc0 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseResourceUserAllowance.java @@ -0,0 +1,109 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import org.apache.logging.log4j.Logger; +import org.dspace.core.ReloadableEntity; + +@Entity +@Table(name = "license_resource_user_allowance") +public class ClarinLicenseResourceUserAllowance implements ReloadableEntity { + + private static Logger log = org.apache.logging.log4j.LogManager.getLogger(ClarinLicenseResourceUserAllowance.class); + + @Id + @Column(name = "transaction_id") + @GeneratedValue(strategy = GenerationType.SEQUENCE, + generator = "license_resource_user_allowance_transaction_id_seq") + @SequenceGenerator(name = "license_resource_user_allowance_transaction_id_seq", + sequenceName = "license_resource_user_allowance_transaction_id_seq", + allocationSize = 1) + private Integer id; + + @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST}) + @JoinColumn(name = "user_registration_id") + private ClarinUserRegistration userRegistration; + + @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST}) + @JoinColumn(name = "mapping_id") + private ClarinLicenseResourceMapping licenseResourceMapping; + + @Column(name = "created_on") + private Date createdOn; + + @Column(name = "token") + private String token; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "transaction", cascade = CascadeType.PERSIST) + private List userMetadata = new ArrayList<>(); + + @Override + public Integer getID() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public ClarinUserRegistration getUserRegistration() { + return userRegistration; + } + + public void setUserRegistration(ClarinUserRegistration userRegistration) { + this.userRegistration = userRegistration; + } + + public ClarinLicenseResourceMapping getLicenseResourceMapping() { + return licenseResourceMapping; + } + + public void setLicenseResourceMapping(ClarinLicenseResourceMapping licenseResourceMapping) { + this.licenseResourceMapping = licenseResourceMapping; + } + + public Date getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Date createdOn) { + this.createdOn = createdOn; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public List getUserMetadata() { + return userMetadata; + } + + public void setUserMetadata(List userMetadata) { + this.userMetadata = userMetadata; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseResourceUserAllowanceServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseResourceUserAllowanceServiceImpl.java new file mode 100644 index 000000000000..4af119b0c8cc --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseResourceUserAllowanceServiceImpl.java @@ -0,0 +1,173 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import org.apache.commons.collections4.CollectionUtils; +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.dao.clarin.ClarinLicenseResourceUserAllowanceDAO; +import org.dspace.content.service.clarin.ClarinLicenseResourceMappingService; +import org.dspace.content.service.clarin.ClarinLicenseResourceUserAllowanceService; +import org.dspace.core.Context; +import org.dspace.core.LogHelper; +import org.dspace.eperson.EPerson; +import org.hibernate.ObjectNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +public class ClarinLicenseResourceUserAllowanceServiceImpl implements ClarinLicenseResourceUserAllowanceService { + private static final Logger log = LoggerFactory.getLogger(ClarinLicenseResourceUserAllowanceService.class); + + @Autowired + AuthorizeService authorizeService; + @Autowired + ClarinLicenseResourceUserAllowanceDAO clarinLicenseResourceUserAllowanceDAO; + @Autowired + ClarinLicenseResourceMappingService clarinLicenseResourceMappingService; + + @Override + public ClarinLicenseResourceUserAllowance create(Context context) throws SQLException { + // Create a table row + ClarinLicenseResourceUserAllowance clarinLicenseResourceUserAllowance = + clarinLicenseResourceUserAllowanceDAO.create(context, + new ClarinLicenseResourceUserAllowance()); + + log.info(LogHelper.getHeader(context, "create_clarin_license_resource_user_allowance", + "create_clarin_license_resource_user_allowance_id=" + clarinLicenseResourceUserAllowance.getID())); + + return clarinLicenseResourceUserAllowance; + } + + @Override + public ClarinLicenseResourceUserAllowance find(Context context, int valueId) throws SQLException, + AuthorizeException { + ClarinLicenseResourceUserAllowance clrua = clarinLicenseResourceUserAllowanceDAO.findByID(context, + ClarinLicenseResourceUserAllowance.class, valueId); + + if (Objects.isNull(clrua)) { + return null; + } + this.authorizeClruaAction(context, List.of(clrua)); + return clrua; + } + + @Override + public List findAll(Context context) throws SQLException, AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "You must be an admin to get all clarin license resource user allowances"); + } + + return clarinLicenseResourceUserAllowanceDAO.findAll(context, ClarinLicenseResourceUserAllowance.class); + } + + @Override + public void update(Context context, ClarinLicenseResourceUserAllowance clarinLicenseResourceUserAllowance) + throws SQLException, AuthorizeException { + if (Objects.isNull(clarinLicenseResourceUserAllowance)) { + throw new IllegalArgumentException("Cannot update clarinLicenseResourceUserAllowance because the " + + "new clarinLicenseResourceUserAllowance is null"); + } + + ClarinLicenseResourceUserAllowance foundClrua = find(context, clarinLicenseResourceUserAllowance.getID()); + if (Objects.isNull(foundClrua)) { + throw new ObjectNotFoundException(clarinLicenseResourceUserAllowance.getID(), + "Cannot update the clarinLicenseResourceUserAllowance because the " + + "clarinLicenseResourceUserAllowance wasn't found in the database."); + } + + clarinLicenseResourceUserAllowanceDAO.save(context, clarinLicenseResourceUserAllowance); + } + + @Override + public void delete(Context context, ClarinLicenseResourceUserAllowance clarinLicenseResourceUserAllowance) + throws SQLException, AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "You must be an admin to create an CLARIN license resource user allowance"); + } + clarinLicenseResourceUserAllowanceDAO.delete(context, clarinLicenseResourceUserAllowance); + } + + @Override + public boolean verifyToken(Context context, UUID resourceID, String token) throws SQLException { + List clarinLicenseResourceUserAllowances = + clarinLicenseResourceUserAllowanceDAO.findByTokenAndBitstreamId(context, resourceID, token); + + return CollectionUtils.isNotEmpty(clarinLicenseResourceUserAllowances); + } + + @Override + public boolean isUserAllowedToAccessTheResource(Context context, UUID userId, UUID resourceId) throws SQLException { + ClarinLicense clarinLicenseToAgree = + clarinLicenseResourceMappingService.getLicenseToAgree(context, userId, resourceId); + + // If the list is empty there are none licenses to agree -> the user is authorized. + return Objects.isNull(clarinLicenseToAgree); + } + + @Override + public List findByEPersonId(Context context, UUID userID) throws SQLException, + AuthorizeException { + List clruaList = + clarinLicenseResourceUserAllowanceDAO.findByEPersonId(context, userID); + + this.authorizeClruaAction(context, clruaList); + return clruaList; + } + + @Override + public List findByEPersonIdAndBitstreamId(Context context, UUID userID, + UUID bitstreamID) + throws SQLException, AuthorizeException { + + List clruaList = clarinLicenseResourceUserAllowanceDAO + .findByEPersonIdAndBitstreamId(context, userID, bitstreamID); + + this.authorizeClruaAction(context, clruaList); + + return clruaList; + } + + /** + * Check if the user is authorized to access the Clarin License Resource User Allowance + */ + private void authorizeClruaAction(Context context, List clruaList) + throws SQLException, AuthorizeException { + if (authorizeService.isAdmin(context)) { + return; + } + + if (CollectionUtils.isEmpty(clruaList)) { + return; + } + // Check if the user is the same as from the userRegistration + // Do not allow to get the userRegistration of another user + EPerson currentUser = context.getCurrentUser(); + ClarinLicenseResourceUserAllowance clrua = clruaList.get(0); + + // Check if the userRegistration is not null + if (Objects.isNull(clrua.getUserRegistration())) { + return; + } + + UUID userRegistrationEpersonUUID = clrua.getUserRegistration().getPersonID(); + if (Objects.nonNull(currentUser) && currentUser.getID().equals(userRegistrationEpersonUUID)) { + return; + } + + throw new AuthorizeException("You are not authorized to access the Clarin License Resource User Allowance " + + "because it is not associated with your account."); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseServiceImpl.java new file mode 100644 index 000000000000..9005e34cef19 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinLicenseServiceImpl.java @@ -0,0 +1,220 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; + +import org.apache.commons.collections4.CollectionUtils; +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.Bitstream; +import org.dspace.content.Bundle; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.dao.clarin.ClarinLicenseDAO; +import org.dspace.content.service.ItemService; +import org.dspace.content.service.clarin.ClarinLicenseResourceMappingService; +import org.dspace.content.service.clarin.ClarinLicenseService; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.core.LogHelper; +import org.hibernate.ObjectNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Service implementation for the ClarinLicense object. + * This class is responsible for all business logic calls for the ClarinLicense object and + * is autowired by spring. This class should never be accessed directly. + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class ClarinLicenseServiceImpl implements ClarinLicenseService { + + private static final Logger log = LoggerFactory.getLogger(ClarinLicenseServiceImpl.class); + + @Autowired + ClarinLicenseDAO clarinLicenseDAO; + + @Autowired + AuthorizeService authorizeService; + + @Autowired + ItemService itemService; + + @Autowired + ClarinLicenseService clarinLicenseService; + + @Autowired + ClarinLicenseResourceMappingService clarinLicenseResourceMappingService; + + @Override + public ClarinLicense create(Context context) throws SQLException, AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "You must be an admin to create an CLARIN License"); + } + + // Create a table row + ClarinLicense clarinLicense = clarinLicenseDAO.create(context, new ClarinLicense()); + + log.info(LogHelper.getHeader(context, "create_clarin_license", "clarin_license_id=" + + clarinLicense.getID())); + + return clarinLicense; + } + + @Override + public ClarinLicense create(Context context, ClarinLicense clarinLicense) throws SQLException, AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "You must be an admin to create an CLARIN License"); + } + + return clarinLicenseDAO.create(context, clarinLicense); + } + + @Override + public ClarinLicense find(Context context, int valueId) throws SQLException { + return clarinLicenseDAO.findByID(context, ClarinLicense.class, valueId); + } + + @Override + public ClarinLicense findByName(Context context, String name) throws SQLException { + return clarinLicenseDAO.findByName(context, name); + } + + @Override + public List findByNameLike(Context context, String name) throws SQLException { + return clarinLicenseDAO.findByNameLike(context, name); + } + + @Override + public void addLicenseMetadataToItem(Context context, ClarinLicense clarinLicense, Item item) throws SQLException { + if (Objects.isNull(clarinLicense) || Objects.isNull(item)) { + log.error("Cannot add clarin license to the item metadata because the Item or the CLARIN License is null."); + } + if (Objects.isNull(clarinLicense.getDefinition()) || + Objects.isNull(clarinLicense.getNonExtendedClarinLicenseLabel()) || + Objects.isNull(clarinLicense.getName())) { + log.error("Cannot add clarin license to the item metadata because one of the necessary clairn license" + + "attribute is null: " + + "nonExtendedClarinLicenseLabel: " + clarinLicense.getNonExtendedClarinLicenseLabel() + + ", name: " + clarinLicense.getName() + + ", definition: " + clarinLicense.getDefinition()); + } + itemService.addMetadata(context, item, "dc", "rights", "uri", Item.ANY, + clarinLicense.getDefinition()); + itemService.addMetadata(context, item, "dc", "rights", null, Item.ANY, + clarinLicense.getName()); + itemService.addMetadata(context, item, "dc", "rights", "label", Item.ANY, + clarinLicense.getNonExtendedClarinLicenseLabel().getLabel()); + } + + @Override + public void clearLicenseMetadataFromItem(Context context, Item item) throws SQLException { + itemService.clearMetadata(context, item, "dc", "rights", "holder", Item.ANY); + itemService.clearMetadata(context, item,"dc", "rights", "uri", Item.ANY); + itemService.clearMetadata(context, item, "dc", "rights", null, Item.ANY); + itemService.clearMetadata(context, item, "dc", "rights", "label", Item.ANY); + } + + @Override + public void addClarinLicenseToBitstream(Context context, Item item, Bundle bundle, Bitstream bitstream) { + try { + if (!Objects.equals(bundle.getName(), Constants.CONTENT_BUNDLE_NAME)) { + return; + } + + if (Objects.isNull(item)) { + return; + } + + List dcRights = + itemService.getMetadata(item, "dc", "rights", null, Item.ANY); + List dcRightsUri = + itemService.getMetadata(item, "dc", "rights", "uri", Item.ANY); + + String licenseName = null; + // If the item bitstreams has license + if (CollectionUtils.isNotEmpty(dcRights)) { + if ( dcRights.size() != dcRightsUri.size() ) { + log.warn( String.format("Harvested bitstream [%s / %s] has different length of " + + "dc_rights and dc_rights_uri", bitstream.getName(), bitstream.getHandle())); + licenseName = "unknown"; + } else { + licenseName = Objects.requireNonNull(dcRights.get(0)).getValue(); + } + } + + ClarinLicense clarinLicense = this.clarinLicenseService.findByName(context, licenseName); + // The item bitstreams doesn't have the license + if (Objects.isNull(clarinLicense)) { + log.info("Cannot find clarin license with name: " + licenseName); + return; + } + + // The item bitstreams has the license -> detach old license and attach the new license + List bundles = item.getBundles(Constants.CONTENT_BUNDLE_NAME); + for (Bundle clarinBundle : bundles) { + List bitstreamList = clarinBundle.getBitstreams(); + for (Bitstream bundleBitstream : bitstreamList) { + // in case bitstream ID exists in license table for some reason .. just remove it + this.clarinLicenseResourceMappingService.detachLicenses(context, bundleBitstream); + // add the license to bitstream + this.clarinLicenseResourceMappingService.attachLicense(context, clarinLicense, bundleBitstream); + } + } + + this.clearLicenseMetadataFromItem(context, item); + this.addLicenseMetadataToItem(context, clarinLicense, item); + } catch (SQLException | AuthorizeException e) { + log.error("Something went wrong in the maintenance of clarin license in the bitstream bundle: " + + e.getMessage()); + } + } + + @Override + public List findAll(Context context) throws SQLException, AuthorizeException { + return clarinLicenseDAO.findAll(context, ClarinLicense.class); + } + + + @Override + public void delete(Context context, ClarinLicense clarinLicense) throws SQLException, AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "You must be an admin to delete an CLARIN License"); + } + + clarinLicenseDAO.delete(context, clarinLicense); + } + + @Override + public void update(Context context, ClarinLicense newClarinLicense) throws SQLException, AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "You must be an admin to update an CLARIN License"); + } + + if (Objects.isNull(newClarinLicense)) { + throw new IllegalArgumentException("Cannot update clarin license because the new clarin license is null"); + } + + ClarinLicense foundClarinLicense = find(context, newClarinLicense.getID()); + if (Objects.isNull(foundClarinLicense)) { + throw new ObjectNotFoundException(newClarinLicense.getID(), + "Cannot update the license because the clarin license wasn't found in the database."); + } + + clarinLicenseDAO.save(context, newClarinLicense); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinToken.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinToken.java new file mode 100644 index 000000000000..50e06c68e805 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinToken.java @@ -0,0 +1,119 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import java.util.Objects; +import java.util.UUID; + +import com.nimbusds.jose.JOSEObjectType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import org.dspace.core.ReloadableEntity; +import org.dspace.eperson.EPerson; + +/** + * Entity representing Clarin Tokens. + * + * @author Milan Kuchtiak + */ +@Entity +@Table(name = "clarin_token") +public class ClarinToken implements ReloadableEntity { + + public static final String E_PERSON_ID = "eid"; + public static final String TOKEN_ISSUER = "clarin-dspace"; + public static final String AUTHENTICATION_METHOD = "clarin-token"; + public static final JOSEObjectType TOKEN_TYPE = new JOSEObjectType("CLARIN-JWE-TOKEN"); + public static final int MASKED_TOKEN_SIZE = 15; + public static final int UNMASKED_TOKEN_SIZE = 3; + // this config property is required to be set + public static final String PROPERTY_ENCRYPTION_SECRET = "clarin.token.encryption.secret"; + // this config property is optional, and set to 90 days by default + public static final String PROPERTY_MAX_EXPIRATION_TIME_IN_DAYS = "clarin.token.max.expiration.time.in.days"; + + @Id + @Column(name = "id") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "clarin_token_id_seq") + @SequenceGenerator(name = "clarin_token_id_seq", sequenceName = "clarin_token_id_seq", + allocationSize = 1) + private Integer id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "eperson_id") + private EPerson ePerson; + + @Column(name = "sign_key") + private String signKey; + + public ClarinToken() { + } + + @Override + public Integer getID() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public EPerson getEPerson() { + return ePerson; + } + + public void setEPerson(EPerson ePerson) { + this.ePerson = ePerson; + } + + /** + * Returns a MAC (Message Authentication Code) shared secret key used to sign and verify token + * + * @return sharedSecret value + */ + public String getSignKey() { + return signKey; + } + + public void setSignKey(String signKey) { + this.signKey = signKey; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + ClarinToken that = (ClarinToken) o; + return Objects.equals(id, that.id) && + Objects.equals(ePerson, that.ePerson) && + Objects.equals(signKey, that.signKey); + } + + @Override + public int hashCode() { + UUID ePersonId = (ePerson != null) ? ePerson.getID() : null; + return Objects.hash(id, ePersonId, signKey); + } + + @Override + public String toString() { + return "ClarinToken{" + + "id: " + id + + ", ePerson: " + (ePerson != null ? ePerson.getEmail() : "null") + + ", signKey: " + signKey + + '}'; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinTokenServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinTokenServiceImpl.java new file mode 100644 index 000000000000..feb508d4be26 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinTokenServiceImpl.java @@ -0,0 +1,205 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import java.security.SecureRandom; +import java.sql.SQLException; +import java.text.ParseException; +import java.util.Base64; +import java.util.Date; +import javax.crypto.SecretKey; + +import com.nimbusds.jose.EncryptionMethod; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWEAlgorithm; +import com.nimbusds.jose.JWEHeader; +import com.nimbusds.jose.JWEObject; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.Payload; +import com.nimbusds.jose.crypto.DirectDecrypter; +import com.nimbusds.jose.crypto.DirectEncrypter; +import com.nimbusds.jose.crypto.MACSigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import jakarta.ws.rs.BadRequestException; +import org.dspace.administer.ClarinTokenUtils; +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.dao.clarin.ClarinTokenDAO; +import org.dspace.content.service.clarin.ClarinTokenService; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Service implementation for ClarinToken object. + * This class is responsible for all business logic calls for the ClarinToken object and is autowired + * by spring. + * This class should never be accessed directly. + * + * @author Milan Kuchtiak + */ +public class ClarinTokenServiceImpl implements ClarinTokenService { + + @Autowired + ClarinTokenDAO clarinTokenDAO; + + @Autowired + AuthorizeService authorizeService; + + @Autowired + private ConfigurationService configurationService; + + @Override + public ClarinToken find(Context context, Integer id) throws SQLException { + return clarinTokenDAO.findByID(context, ClarinToken.class, id); + } + + @Override + public String createToken(Context context, EPerson ePerson, Date expirationTime) + throws SQLException, AuthorizeException { + boolean ignoreAuth = context.ignoreAuthorization(); + + String encryptionSecret = configurationService.getProperty(ClarinToken.PROPERTY_ENCRYPTION_SECRET); + + if (encryptionSecret == null) { + throw new RuntimeException("Missing clarin.token.encryption.secret configuration key"); + } + + if (ePerson == null) { + throw new BadRequestException("EPerson must be defined."); + } + + if (!ignoreAuth && context.getCurrentUser() == null) { + throw new AuthorizeException("You must be authenticated user"); + } + + if (!ignoreAuth && !authorizeService.isAdmin(context) && !context.getCurrentUser().equals(ePerson)) { + throw new AuthorizeException("You must be admin user to create clarin token for this User ID"); + } + + SecureRandom random = new SecureRandom(); + byte[] sharedSecretArray = new byte[32]; + random.nextBytes(sharedSecretArray); + + String macSecret = Base64.getEncoder().encodeToString(sharedSecretArray); + + JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() + .issuer(ClarinToken.TOKEN_ISSUER) + .claim(ClarinToken.E_PERSON_ID, ePerson.getID().toString()) + .expirationTime(expirationTime) + .build(); + + SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet); + + // sign JWT token + try { + JWSSigner signer = new MACSigner(macSecret); + signedJWT.sign(signer); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + + // encode JWT token + JWEObject jweObject; + try { + SecretKey aesKey = ClarinTokenUtils.getSecretKeyFromBase64EncodedString(encryptionSecret); + + ClarinToken pat = new ClarinToken(); + pat.setEPerson(ePerson); + pat.setSignKey(macSecret); + + pat = clarinTokenDAO.create(context, pat); + + JWEHeader header = new JWEHeader.Builder(JWEAlgorithm.DIR, EncryptionMethod.A256GCM) + .keyID(String.valueOf(pat.getID())) + .type(ClarinToken.TOKEN_TYPE) + .build(); + jweObject = new JWEObject(header, new Payload(signedJWT)); + jweObject.encrypt(new DirectEncrypter(aesKey)); + + } catch (JOSEException e) { + throw new RuntimeException(e); + } + + return jweObject.serialize(); + } + + @Override + public void delete(Context context, String token) throws SQLException, AuthorizeException { + boolean ignoreAuth = context.ignoreAuthorization(); + + if (!ignoreAuth && context.getCurrentUser() == null) { + throw new AuthorizeException("You must be authenticated user"); + } + + try { + if (ClarinTokenUtils.isClarinToken(token)) { + ClarinToken clarinToken = find(context, ClarinTokenUtils.getTokenId(token)); + if (clarinToken != null) { + + if (!ignoreAuth && + !authorizeService.isAdmin(context) && + !context.getCurrentUser().equals(clarinToken.getEPerson())) { + throw new AuthorizeException("You must be admin user to delete this token"); + } + + clarinTokenDAO.delete(context, clarinToken); + } else { + throw new BadRequestException("This token is not valid."); + } + } + } catch (ParseException ex) { + throw new BadRequestException(ex.getMessage()); + } + } + + @Override + public void delete(Context context, EPerson ePerson) throws SQLException, AuthorizeException { + boolean ignoreAuth = context.ignoreAuthorization(); + if (!ignoreAuth && context.getCurrentUser() == null) { + throw new AuthorizeException("You must be authenticated user"); + } + if (!ignoreAuth && !authorizeService.isAdmin(context) && !context.getCurrentUser().equals(ePerson)) { + throw new AuthorizeException("You must be admin user to delete token for this User ID"); + } + clarinTokenDAO.deleteTokensForEPerson(context, ePerson); + } + + @Override + public void deleteAll(Context context) throws SQLException, AuthorizeException { + boolean ignoreAuth = context.ignoreAuthorization(); + if (!ignoreAuth && !authorizeService.isAdmin(context)) { + throw new AuthorizeException("You must be admin user"); + } + clarinTokenDAO.deleteAll(context); + } + + @Override + public EPerson getEPersonFromClarinToken(Context context, String token) + throws SQLException, ParseException, JOSEException { + JWEObject jweObj = JWEObject.parse(token); + String tokenId = jweObj.getHeader().getKeyID(); + if (tokenId != null) { + ClarinToken clarinToken = find(context, Integer.valueOf(tokenId)); + if (clarinToken != null) { + jweObj.decrypt(new DirectDecrypter( + ClarinTokenUtils.getSecretKeyFromBase64EncodedString( + configurationService.getProperty(ClarinToken.PROPERTY_ENCRYPTION_SECRET)))); + SignedJWT signedJWT = jweObj.getPayload().toSignedJWT(); + if (ClarinTokenUtils.isSignedJWTValid(signedJWT, clarinToken)) { + return clarinToken.getEPerson(); + } + } + } + return null; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinUserMetadata.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinUserMetadata.java new file mode 100644 index 000000000000..a7f8418a2bad --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinUserMetadata.java @@ -0,0 +1,90 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import org.apache.logging.log4j.Logger; +import org.dspace.core.ReloadableEntity; + +@Entity +@Table(name = "user_metadata") +public class ClarinUserMetadata implements ReloadableEntity { + + private static Logger log = org.apache.logging.log4j.LogManager.getLogger(ClarinUserMetadata.class); + @Id + @Column(name = "user_metadata_id") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_metadata_user_metadata_id_seq") + @SequenceGenerator(name = "user_metadata_user_metadata_id_seq", sequenceName = "user_metadata_user_metadata_id_seq", + allocationSize = 1) + private Integer id; + + @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST}) + @JoinColumn(name = "user_registration_id") + private ClarinUserRegistration eperson; + + @Column(name = "metadata_key") + private String metadataKey = null; + + @Column(name = "metadata_value") + private String metadataValue = null; + + @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST}) + @JoinColumn(name = "transaction_id") + private ClarinLicenseResourceUserAllowance transaction; + + @Override + public Integer getID() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public ClarinUserRegistration getEperson() { + return eperson; + } + + public void setEperson(ClarinUserRegistration eperson) { + this.eperson = eperson; + } + + public String getMetadataKey() { + return metadataKey; + } + + public void setMetadataKey(String metadataKey) { + this.metadataKey = metadataKey; + } + + public String getMetadataValue() { + return metadataValue; + } + + public void setMetadataValue(String metadataValue) { + this.metadataValue = metadataValue; + } + + public ClarinLicenseResourceUserAllowance getTransaction() { + return transaction; + } + + public void setTransaction(ClarinLicenseResourceUserAllowance transaction) { + this.transaction = transaction; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinUserMetadataServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinUserMetadataServiceImpl.java new file mode 100644 index 000000000000..af0bc8dc8309 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinUserMetadataServiceImpl.java @@ -0,0 +1,175 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.dao.clarin.ClarinUserMetadataDAO; +import org.dspace.content.service.clarin.ClarinUserMetadataService; +import org.dspace.core.Context; +import org.dspace.core.LogHelper; +import org.dspace.eperson.EPerson; +import org.hibernate.ObjectNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.CollectionUtils; + +public class ClarinUserMetadataServiceImpl implements ClarinUserMetadataService { + private static final Logger log = LoggerFactory.getLogger(ClarinUserMetadataService.class); + + @Autowired + AuthorizeService authorizeService; + @Autowired + ClarinUserMetadataDAO clarinUserMetadataDAO; + + @Override + public ClarinUserMetadata create(Context context) throws SQLException { + // Create a table row + ClarinUserMetadata clarinUserMetadata = clarinUserMetadataDAO.create(context, + new ClarinUserMetadata()); + + log.info(LogHelper.getHeader(context, "create_clarin_user_metadata", + "clarin_user_metadata_id=" + clarinUserMetadata.getID())); + + return clarinUserMetadata; + } + + @Override + public ClarinUserMetadata find(Context context, int valueId) throws SQLException, AuthorizeException { + ClarinUserMetadata clarinUserMetadata = clarinUserMetadataDAO + .findByID(context, ClarinUserMetadata.class, valueId); + + if (Objects.isNull(clarinUserMetadata)) { + return null; + } + this.authorizeClarinUserMetadataAction(context, List.of(clarinUserMetadata)); + return clarinUserMetadata; + } + + @Override + public List findAll(Context context) throws SQLException, AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "You must be an admin to get all clarin user metadata."); + } + return clarinUserMetadataDAO.findAll(context, ClarinUserMetadata.class); + } + + @Override + public void update(Context context, ClarinUserMetadata clarinUserMetadata) throws SQLException, AuthorizeException { + if (Objects.isNull(clarinUserMetadata)) { + throw new IllegalArgumentException("Cannot update user metadata because the new user metadata is null"); + } + + ClarinUserMetadata foundUserMetadata = find(context, clarinUserMetadata.getID()); + if (Objects.isNull(foundUserMetadata)) { + throw new ObjectNotFoundException(clarinUserMetadata.getID(), + "Cannot update the user metadata because the user metadata wasn't found in the database."); + } + + clarinUserMetadataDAO.save(context, clarinUserMetadata); + } + + @Override + public void delete(Context context, ClarinUserMetadata clarinUserMetadata) throws SQLException, AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "You must be an admin to create an Clarin user metadata"); + } + clarinUserMetadataDAO.delete(context, clarinUserMetadata); + } + + @Override + public List findByUserRegistrationAndBitstream(Context context, Integer userRegUUID, + UUID bitstreamUUID, boolean lastTransaction) + throws SQLException, AuthorizeException { + List userMetadata = null; + if (lastTransaction) { + userMetadata = getLastTransactionUserMetadata(clarinUserMetadataDAO + .findByUserRegistrationAndBitstream(context, userRegUUID, bitstreamUUID)); + } else { + userMetadata = clarinUserMetadataDAO.findByUserRegistrationAndBitstream(context, + userRegUUID, bitstreamUUID); + } + + this.authorizeClarinUserMetadataAction(context, userMetadata); + + if (userMetadata == null) { + userMetadata = List.of(); + } + return userMetadata; + } + + private List getLastTransactionUserMetadata(List userMetadataList) { + Integer latestTransactionId = getIdOfLastTransaction(userMetadataList); + if (latestTransactionId == null) { + return userMetadataList; + } + + List filteredUserMetadata = null; + // Filter all user metadata by the last transaction + try { + filteredUserMetadata = userMetadataList.stream() + .filter(clarinUserMetadata -> clarinUserMetadata.getTransaction().getID() + .equals(latestTransactionId)) + .collect(Collectors.toList()); + } catch (Exception e) { + log.error("Error filtering user metadata by the last transaction", e); + } + return filteredUserMetadata; + } + + private Integer getIdOfLastTransaction(List userMetadataList) { + // userMetadataList is filtered by the last transaction - first element is the last transaction + try { + return userMetadataList.get(0).getTransaction().getID(); + } catch (IndexOutOfBoundsException e) { + log.error("No transaction found for the user metadata"); + return null; + } + } + + /** + * Check if the user is admin or if the user is the same as from the userRegistration + */ + private void authorizeClarinUserMetadataAction(Context context, List userMetadata) + throws SQLException, AuthorizeException { + if (authorizeService.isAdmin(context)) { + return; + } + + if (CollectionUtils.isEmpty(userMetadata)) { + return; + } + + // Check if the user is the same as from the userRegistration + // Do not allow to get the userRegistration of another user + EPerson currentUser = context.getCurrentUser(); + ClarinUserMetadata userMetadatum = userMetadata.get(0); + + // Check if the userRegistration is not null + if (Objects.isNull(userMetadatum.getEperson())) { + return; + } + + UUID userRegistrationEpersonUUID = userMetadatum.getEperson().getPersonID(); + if (currentUser.getID().equals(userRegistrationEpersonUUID)) { + return; + } + + throw new AuthorizeException("You are not authorized to access the Clarin User Metadata " + + "because it is not associated with your account."); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinUserRegistration.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinUserRegistration.java new file mode 100644 index 000000000000..36cc2e89ace2 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinUserRegistration.java @@ -0,0 +1,137 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import org.apache.logging.log4j.Logger; +import org.dspace.core.ReloadableEntity; + +@Entity +@Table(name = "user_registration") +public class ClarinUserRegistration implements ReloadableEntity { + + // Anonymous user + public static final String ANONYMOUS_USER_REGISTRATION = "anonymous"; + + // Registered user without organization + public static final String UNKNOWN_USER_REGISTRATION = "Unknown"; + + private static Logger log = org.apache.logging.log4j.LogManager.getLogger(ClarinUserRegistration.class); + + @Id + @Column(name = "user_registration_id") + @GeneratedValue(strategy = GenerationType.SEQUENCE, + generator = "user_registration_user_registration_id_seq") + @SequenceGenerator(name = "user_registration_user_registration_id_seq", + sequenceName = "user_registration_user_registration_id_seq", + allocationSize = 1) + protected Integer id; + + @Column(name = "eperson_id") + private UUID ePersonID = null; + + @Column(name = "email") + private String email = null; + + @Column(name = "organization") + private String organization = null; + + @Column(name = "confirmation") + private boolean confirmation = false; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "eperson", cascade = CascadeType.PERSIST) + private List clarinLicenses = new ArrayList<>(); + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "userRegistration", cascade = CascadeType.PERSIST) + private List licenseResourceUserAllowances = new ArrayList<>(); + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "eperson", cascade = CascadeType.PERSIST) + private List userMetadata = new ArrayList<>(); + + public ClarinUserRegistration() { + } + + public UUID getPersonID() { + return ePersonID; + } + + public void setPersonID(UUID ePersonID) { + this.ePersonID = ePersonID; + } + + public void setId(Integer id) { + this.id = id; + } + + @Override + public Integer getID() { + return id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getOrganization() { + return organization; + } + + public void setOrganization(String organization) { + this.organization = organization; + } + + public boolean isConfirmation() { + return confirmation; + } + + public void setConfirmation(boolean confirmation) { + this.confirmation = confirmation; + } + + public List getClarinLicenses() { + return clarinLicenses; + } + + public List getLicenseResourceUserAllowances() { + return licenseResourceUserAllowances; + } + + public void setClarinLicenses(List clarinLicenses) { + this.clarinLicenses = clarinLicenses; + } + + public void setLicenseResourceUserAllowances(List + licenseResourceUserAllowances) { + this.licenseResourceUserAllowances = licenseResourceUserAllowances; + } + + public List getUserMetadata() { + return userMetadata; + } + + public void setUserMetadata(List userMetadata) { + this.userMetadata = userMetadata; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinUserRegistrationServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinUserRegistrationServiceImpl.java new file mode 100644 index 000000000000..f99b0d218868 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinUserRegistrationServiceImpl.java @@ -0,0 +1,155 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.dao.clarin.ClarinUserRegistrationDAO; +import org.dspace.content.service.clarin.ClarinUserRegistrationService; +import org.dspace.core.Context; +import org.dspace.core.LogHelper; +import org.dspace.eperson.EPerson; +import org.hibernate.ObjectNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.CollectionUtils; + +public class ClarinUserRegistrationServiceImpl implements ClarinUserRegistrationService { + + private static final Logger log = LoggerFactory.getLogger(ClarinUserRegistrationService.class); + + @Autowired + AuthorizeService authorizeService; + @Autowired + ClarinUserRegistrationDAO clarinUserRegistrationDAO; + + @Override + public ClarinUserRegistration create(Context context) throws SQLException, AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "You must be an admin to create a CLARIN user registration"); + } + // Create a table row + ClarinUserRegistration clarinUserRegistration = clarinUserRegistrationDAO.create(context, + new ClarinUserRegistration()); + + log.info(LogHelper.getHeader(context, "create_clarin_user_registration", + "clarin_user_registration_id=" + clarinUserRegistration.getID())); + + return clarinUserRegistration; + } + + @Override + public ClarinUserRegistration create(Context context, + ClarinUserRegistration clarinUserRegistration) throws SQLException, AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "You must be an admin to create a CLARIN user registration"); + } + + return clarinUserRegistrationDAO.create(context, clarinUserRegistration); + } + + @Override + public ClarinUserRegistration find(Context context, int valueId) throws SQLException, AuthorizeException { + ClarinUserRegistration clarinUserRegistration = clarinUserRegistrationDAO + .findByID(context, ClarinUserRegistration.class, valueId); + + if (Objects.isNull(clarinUserRegistration)) { + return null; + } + this.authorizeClarinUserRegistrationAction(context, List.of(clarinUserRegistration)); + return clarinUserRegistration; + } + + @Override + public List findAll(Context context) throws SQLException, AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "You must be an admin to get all CLARIN user registrations"); + } + + return clarinUserRegistrationDAO.findAll(context, ClarinUserRegistration.class); + } + + @Override + public List findByEPersonUUID(Context context, UUID epersonUUID) + throws SQLException, AuthorizeException { + List clarinUserRegistrationList = clarinUserRegistrationDAO + .findByEPersonUUID(context, epersonUUID); + + this.authorizeClarinUserRegistrationAction(context, clarinUserRegistrationList); + + return clarinUserRegistrationList; + } + + @Override + public List findByEmail(Context context, String email) + throws SQLException { + return clarinUserRegistrationDAO.findByEmail(context, email); + } + + @Override + public void delete(Context context, ClarinUserRegistration clarinUserRegistration) + throws SQLException, AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "You must be an admin to create an CLARIN user registration"); + } + clarinUserRegistrationDAO.delete(context, clarinUserRegistration); + } + + @Override + public void update(Context context, ClarinUserRegistration clarinUserRegistration) throws SQLException, + AuthorizeException { + if (Objects.isNull(clarinUserRegistration)) { + throw new IllegalArgumentException("Cannot update ClarinUserRegistration because the object is null"); + } + + ClarinUserRegistration foundUserRegistration = find(context, clarinUserRegistration.getID()); + if (Objects.isNull(foundUserRegistration)) { + throw new ObjectNotFoundException(clarinUserRegistration.getID(), + "Cannot update the ClarinUserRegistration because the object wasn't found in the database."); + } + + clarinUserRegistrationDAO.save(context, clarinUserRegistration); + } + + /** + * Check if the user is admin or if the user is the same as from the userRegistration + */ + private void authorizeClarinUserRegistrationAction(Context context, List + userRegistrationList) + throws SQLException, AuthorizeException { + if (authorizeService.isAdmin(context)) { + return; + } + + if (CollectionUtils.isEmpty(userRegistrationList)) { + return; + } + + // Check if the user is the same as from the userRegistration + // Do not allow to get the userRegistration of another user + EPerson currentUser = context.getCurrentUser(); + ClarinUserRegistration clarinUserRegistration = userRegistrationList.get(0); + UUID userRegistrationEpersonUUID = clarinUserRegistration.getPersonID(); + if (currentUser.getID().equals(userRegistrationEpersonUUID)) { + return; + } + + throw new AuthorizeException("You are not authorized to access the Clarin User Registration " + + "because it is not associated with your account."); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinVerificationToken.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinVerificationToken.java new file mode 100644 index 000000000000..91e3aec14465 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinVerificationToken.java @@ -0,0 +1,105 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import org.dspace.core.ReloadableEntity; + +/** + * If the Shibboleth authentication failed because IdP hasn't sent the SHIB_EMAIL header. + * The user retrieve the verification token to the email for registration and login. + * In the case of the Shibboleth Auth failure the IdP headers are stored as the string into the `shib_headers` column. + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +@Entity +@Table(name = "verification_token") +public class ClarinVerificationToken implements ReloadableEntity { + + @Id + @Column(name = "verification_token_id") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "verification_token_verification_token_id_seq") + @SequenceGenerator(name = "verification_token_verification_token_id_seq", + sequenceName = "verification_token_verification_token_id_seq", + allocationSize = 1) + private Integer id; + + /** + * Value of the Shibboleth `SHIB-NETID` header. + */ + @Column(name = "eperson_netid") + private String ePersonNetID = null; + + /** + * The email filled in by the user. + */ + @Column(name = "email") + private String email = null; + + /** + * In the case of the Shibboleth Auth failure the IdP headers are stored as the string into this column. + */ + @Column(name = "shib_headers") + private String shibHeaders = null; + + /** + * Generated verification token which is sent to the email. + */ + @Column(name = "token") + private String token = null; + + public ClarinVerificationToken() { + } + + public String getShibHeaders() { + return shibHeaders; + } + + public void setShibHeaders(String shibHeaders) { + this.shibHeaders = shibHeaders; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getePersonNetID() { + return ePersonNetID; + } + + public void setePersonNetID(String ePersonNetID) { + this.ePersonNetID = ePersonNetID; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + @Override + public Integer getID() { + return id; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinVerificationTokenServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinVerificationTokenServiceImpl.java new file mode 100644 index 000000000000..0c857bff02f6 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinVerificationTokenServiceImpl.java @@ -0,0 +1,113 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; + +import org.dspace.authenticate.clarin.ShibHeaders; +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.dao.clarin.ClarinVerificationTokenDAO; +import org.dspace.content.service.clarin.ClarinVerificationTokenService; +import org.dspace.core.Context; +import org.dspace.core.LogHelper; +import org.hibernate.ObjectNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Service implementation for the ClarinVerificationToken object. + * This class is responsible for all business logic calls for the ClarinVerificationToken object and + * is autowired by spring. This class should never be accessed directly. + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class ClarinVerificationTokenServiceImpl implements ClarinVerificationTokenService { + + private static final Logger log = LoggerFactory.getLogger(ClarinVerificationTokenServiceImpl.class); + + @Autowired + ClarinVerificationTokenDAO clarinVerificationTokenDAO; + @Autowired + AuthorizeService authorizeService; + + @Override + public ClarinVerificationToken create(Context context) throws SQLException { + ClarinVerificationToken clarinVerificationToken = clarinVerificationTokenDAO.create(context, + new ClarinVerificationToken()); + + log.info(LogHelper.getHeader(context, "create_clarin_verification_token", + "clarin_verification_token_id=" + clarinVerificationToken.getID())); + + return clarinVerificationToken; + } + + @Override + public ClarinVerificationToken find(Context context, int valueId) throws SQLException { + return clarinVerificationTokenDAO.findByID(context, ClarinVerificationToken.class, valueId); + } + + @Override + public List findAll(Context context) throws SQLException, AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "You must be an admin to load all clarin verification tokens."); + } + + return clarinVerificationTokenDAO.findAll(context, ClarinVerificationToken.class); + } + + @Override + public ClarinVerificationToken findByToken(Context context, String token) throws SQLException { + return clarinVerificationTokenDAO.findByToken(context, token); + } + + @Override + public ClarinVerificationToken findByNetID(Context context, String netID) throws SQLException { + return clarinVerificationTokenDAO.findByNetID(context, netID); + } + + @Override + public ClarinVerificationToken findByNetID(Context context, String[] netIdHeaders, ShibHeaders shibHeaders) + throws SQLException { + for (String netidHeader : netIdHeaders) { + String netID = shibHeaders.get_single(netidHeader); + ClarinVerificationToken clarinVerificationToken = clarinVerificationTokenDAO.findByNetID(context, netID); + if (Objects.nonNull(clarinVerificationToken)) { + return clarinVerificationToken; + } + } + return null; + } + + @Override + public void delete(Context context, ClarinVerificationToken clarinVerificationToken) + throws SQLException { + clarinVerificationTokenDAO.delete(context, clarinVerificationToken); + } + + @Override + public void update(Context context, ClarinVerificationToken newClarinVerificationToken) throws SQLException { + if (Objects.isNull(newClarinVerificationToken)) { + throw new IllegalArgumentException("Cannot update clarin verification token because " + + "the new verification token is null"); + } + + ClarinVerificationToken foundClarinVerificationToken = find(context, newClarinVerificationToken.getID()); + if (Objects.isNull(foundClarinVerificationToken)) { + throw new ObjectNotFoundException(newClarinVerificationToken.getID(), + "Cannot update the clarin verification token because the clarin verification token wasn't " + + "found in the database."); + } + + clarinVerificationTokenDAO.save(context, newClarinVerificationToken); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/ClarinWorkspaceItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinWorkspaceItemServiceImpl.java new file mode 100644 index 000000000000..3b51fd68a5be --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/ClarinWorkspaceItemServiceImpl.java @@ -0,0 +1,77 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import java.sql.SQLException; +import java.util.Objects; +import java.util.UUID; + +import org.apache.logging.log4j.Logger; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Collection; +import org.dspace.content.WorkspaceItem; +import org.dspace.content.dao.WorkspaceItemDAO; +import org.dspace.content.service.WorkspaceItemService; +import org.dspace.content.service.clarin.ClarinWorkspaceItemService; +import org.dspace.core.Context; +import org.dspace.core.LogHelper; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Service interface class for the WorkspaceItem object created for Clarin-Dspace import. + * Contains methods needed to import bitstream when dspace5 migrating to dspace7. + * The implementation of this class is autowired by spring. + * This class should never be accessed directly. + * + * @author Michaela Paurikova(michaela.paurikova at dataquest.sk) + */ +public class ClarinWorkspaceItemServiceImpl implements ClarinWorkspaceItemService { + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger( + ClarinWorkspaceItemServiceImpl.class); + @Autowired + private WorkspaceItemService workspaceItemService; + @Autowired + private WorkspaceItemDAO workspaceItemDAO; + + @Override + public WorkspaceItem create(Context context, Collection collection, boolean multipleTitles, boolean publishedBefore, + boolean multipleFiles, Integer stageReached, Integer pageReached, + boolean template, boolean isNewVersion) throws AuthorizeException, SQLException { + + //create empty workspace item with item + // The isNewVersion parameter controls handle creation: + // - If isNewVersion is true, no new handle is created (existing handles are preserved). + // - If isNewVersion is false, a new handle is generated for the item. + WorkspaceItem workspaceItem = workspaceItemService.create(context, collection, false, isNewVersion); + //set workspace item values based on input values + workspaceItem.setPublishedBefore(publishedBefore); + workspaceItem.setMultipleFiles(multipleFiles); + workspaceItem.setMultipleTitles(multipleTitles); + workspaceItem.setPageReached(pageReached); + workspaceItem.setStageReached(stageReached); + return workspaceItem; + } + + @Override + public WorkspaceItem find(Context context, UUID uuid) throws SQLException { + //find workspace item by its UUID + WorkspaceItem workspaceItem = workspaceItemDAO.findByID(context, WorkspaceItem.class, uuid); + + //create log if the workspace item is not found + if (log.isDebugEnabled()) { + if (Objects.nonNull(workspaceItem)) { + log.debug(LogHelper.getHeader(context, "find_workspace_item", + "not_found,workspace_item_uuid=" + uuid)); + } else { + log.debug(LogHelper.getHeader(context, "find_workspace_item", + "workspace_item_uuid=" + uuid)); + } + } + return workspaceItem; + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/MatomoReportSubscription.java b/dspace-api/src/main/java/org/dspace/content/clarin/MatomoReportSubscription.java new file mode 100644 index 000000000000..adda8789b62a --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/MatomoReportSubscription.java @@ -0,0 +1,102 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import java.util.Objects; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import org.dspace.content.Item; +import org.dspace.core.ReloadableEntity; +import org.dspace.eperson.EPerson; + +/** + * Entity representing Matomo report subscriptions. + * + * @author Milan Kuchtiak + */ +@Entity +@Table(name = "matomo_report_registry") +public class MatomoReportSubscription implements ReloadableEntity { + + @Id + @Column(name = "id") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "matomo_report_registry_id_seq") + @SequenceGenerator(name = "matomo_report_registry_id_seq", sequenceName = "matomo_report_registry_id_seq", + allocationSize = 1) + private Integer id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "eperson_id") + private EPerson ePerson; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id") + private Item item; + + public MatomoReportSubscription() { + } + + @Override + public Integer getID() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public EPerson getEPerson() { + return ePerson; + } + + public void setEPerson(EPerson ePerson) { + this.ePerson = ePerson; + } + + public Item getItem() { + return item; + } + + public void setItem(Item item) { + this.item = item; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + MatomoReportSubscription that = (MatomoReportSubscription) o; + return Objects.equals(id, that.id) + && Objects.equals(ePerson.getID(), that.ePerson.getID()) + && Objects.equals(item.getID(), that.item.getID()); + } + + @Override + public int hashCode() { + return Objects.hash(id, ePerson.getID(), item.getID()); + } + + @Override + public String toString() { + return "MatomoReportSubscription{" + + "id=" + id + + ", ePerson=" + ePerson.getName() + + ", item=" + item.getHandle() + + '}'; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/clarin/MatomoReportSubscriptionServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/clarin/MatomoReportSubscriptionServiceImpl.java new file mode 100644 index 000000000000..7f4c2457fc9d --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/clarin/MatomoReportSubscriptionServiceImpl.java @@ -0,0 +1,112 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.clarin; + +import java.sql.SQLException; +import java.util.List; + +import jakarta.ws.rs.BadRequestException; +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.Item; +import org.dspace.content.dao.ItemDAO; +import org.dspace.content.dao.clarin.MatomoReportSubscriptionDAO; +import org.dspace.content.service.clarin.MatomoReportSubscriptionService; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Service implementation for MatomoReportSubscription object. + * This class is responsible for all business logic calls for the MatomoReportSubscription object and is autowired + * by spring. + * This class should never be accessed directly. + * + * @author Milan Kuchtiak + */ +public class MatomoReportSubscriptionServiceImpl implements MatomoReportSubscriptionService { + + @Autowired + MatomoReportSubscriptionDAO matomoReportSubscriptionDAO; + + @Autowired + ItemDAO itemDAO; + + @Autowired + AuthorizeService authorizeService; + + @Override + public MatomoReportSubscription find(Context context, int id) throws SQLException, AuthorizeException { + if (context.getCurrentUser() == null) { + throw new AuthorizeException("You must be authenticated user"); + } + MatomoReportSubscription matomoReportSubscription = + matomoReportSubscriptionDAO.findByID(context, MatomoReportSubscription.class, id); + + if (matomoReportSubscription != null && !authorizeService.isAdmin(context) && + !context.getCurrentUser().getID().equals(matomoReportSubscription.getEPerson().getID())) { + throw new AuthorizeException("You must be admin user to get matomo report subscription for this ID"); + } + return matomoReportSubscription; + } + + @Override + public List findAll(Context context) throws SQLException, AuthorizeException { + if (context.getCurrentUser() == null) { + throw new AuthorizeException("You must be authenticated user"); + } + if (authorizeService.isAdmin(context)) { + return matomoReportSubscriptionDAO.findAll(context, MatomoReportSubscription.class); + } else { + return matomoReportSubscriptionDAO.findByEPersonId(context, context.getCurrentUser().getID()); + } + } + + @Override + public MatomoReportSubscription findByItem(Context context, Item item) throws SQLException, AuthorizeException { + if (context.getCurrentUser() == null) { + throw new AuthorizeException("You must be authenticated user"); + } + return matomoReportSubscriptionDAO.findByItemIdAndCurrentUser(context, item.getID()); + } + + @Override + public MatomoReportSubscription subscribe(Context context, Item item) throws SQLException, AuthorizeException { + MatomoReportSubscription matomoReportSubscription = findByItem(context, item); + + if (matomoReportSubscription != null) { + // already subscribed + return matomoReportSubscription; + } else { + MatomoReportSubscription mrs = new MatomoReportSubscription(); + mrs.setEPerson(context.getCurrentUser()); + mrs.setItem(item); + return matomoReportSubscriptionDAO.create(context, mrs); + } + } + + @Override + public void unsubscribe(Context context, Item item) throws SQLException, AuthorizeException { + if (context.getCurrentUser() == null) { + throw new AuthorizeException("You must be authenticated user"); + } + MatomoReportSubscription matomoReportSubscription = findByItem(context, item); + if (matomoReportSubscription != null) { + matomoReportSubscriptionDAO.delete(context, matomoReportSubscription); + } else { + throw new BadRequestException("Matomo report subscription for this item doesn't exist for this user"); + } + } + + @Override + public boolean isSubscribed(Context context, Item item) throws SQLException, AuthorizeException { + if (context.getCurrentUser() == null) { + throw new AuthorizeException("You must be authenticated user"); + } + return (matomoReportSubscriptionDAO.findByItemIdAndCurrentUser(context, item.getID()) != null); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinItemDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinItemDAO.java new file mode 100644 index 000000000000..ac10470a9d77 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinItemDAO.java @@ -0,0 +1,22 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +import org.dspace.content.Item; +import org.dspace.content.MetadataField; +import org.dspace.core.Context; + +public interface ClarinItemDAO { + List findByBitstreamUUID(Context context, UUID bitstreamUUID) throws SQLException; + + List findByHandle(Context context, MetadataField metadataField, String handle) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinLicenseDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinLicenseDAO.java new file mode 100644 index 000000000000..99147af64e65 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinLicenseDAO.java @@ -0,0 +1,31 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.clarin; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.content.clarin.ClarinLicense; +import org.dspace.core.Context; +import org.dspace.core.GenericDAO; + +/** + * Database Access Object interface class for the Clarin License object. + * The implementation of this class is responsible for all database calls for the Clarin License object + * and is autowired by spring This class should only be accessed from a single service and should never be exposed + * outside the API + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public interface ClarinLicenseDAO extends GenericDAO { + + ClarinLicense findByName(Context context, String name) throws SQLException; + + List findByNameLike(Context context, String name) throws SQLException; + +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinLicenseLabelDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinLicenseLabelDAO.java new file mode 100644 index 000000000000..1abd25b7a96a --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinLicenseLabelDAO.java @@ -0,0 +1,22 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.clarin; + +import org.dspace.content.clarin.ClarinLicenseLabel; +import org.dspace.core.GenericDAO; + +/** + * Database Access Object interface class for the Clarin License Label object. + * The implementation of this class is responsible for all database calls for the Clarin License Label object + * and is autowired by spring This class should only be accessed from a single service and should never be exposed + * outside the API + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public interface ClarinLicenseLabelDAO extends GenericDAO { +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinLicenseResourceMappingDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinLicenseResourceMappingDAO.java new file mode 100644 index 000000000000..f6233643174e --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinLicenseResourceMappingDAO.java @@ -0,0 +1,23 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +import org.dspace.content.clarin.ClarinLicenseResourceMapping; +import org.dspace.core.Context; +import org.dspace.core.GenericDAO; + +public interface ClarinLicenseResourceMappingDAO extends GenericDAO { + + List findByBitstreamUUID(Context context, UUID bitstreamUUID) throws SQLException; + List findByBitstreamUUIDs(Context context, List bitstreamUUIDs) + throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinLicenseResourceUserAllowanceDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinLicenseResourceUserAllowanceDAO.java new file mode 100644 index 000000000000..2fb1433f3db7 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinLicenseResourceUserAllowanceDAO.java @@ -0,0 +1,24 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +import org.dspace.content.clarin.ClarinLicenseResourceUserAllowance; +import org.dspace.core.Context; +import org.dspace.core.GenericDAO; + +public interface ClarinLicenseResourceUserAllowanceDAO extends GenericDAO { + List findByTokenAndBitstreamId(Context context, UUID resourceID, + String token) throws SQLException; + List findByEPersonId(Context context, UUID userID) throws SQLException; + List findByEPersonIdAndBitstreamId(Context context, UUID userID, + UUID bitstreamID) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinTokenDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinTokenDAO.java new file mode 100644 index 000000000000..7481726461c8 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinTokenDAO.java @@ -0,0 +1,31 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.clarin; + +import java.sql.SQLException; + +import org.dspace.content.clarin.ClarinToken; +import org.dspace.core.Context; +import org.dspace.core.GenericDAO; +import org.dspace.eperson.EPerson; + +/** + * Database Access Object interface class for the ClarinToken object. + * The implementation of this class is responsible for all database calls for the ClarinToken object + * and is autowired by spring This class should only be accessed from a single service and should never be exposed + * outside the API + * + * @author Milan Kuchtiak + */ +public interface ClarinTokenDAO extends GenericDAO { + + void deleteAll(Context context) throws SQLException; + + void deleteTokensForEPerson(Context context, EPerson ePerson) throws SQLException; + +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinUserMetadataDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinUserMetadataDAO.java new file mode 100644 index 000000000000..c25b77435d11 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinUserMetadataDAO.java @@ -0,0 +1,21 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +import org.dspace.content.clarin.ClarinUserMetadata; +import org.dspace.core.Context; +import org.dspace.core.GenericDAO; + +public interface ClarinUserMetadataDAO extends GenericDAO { + List findByUserRegistrationAndBitstream(Context context, Integer userRegUUID, + UUID bitstreamUUID) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinUserRegistrationDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinUserRegistrationDAO.java new file mode 100644 index 000000000000..1a6a8b1be5d1 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinUserRegistrationDAO.java @@ -0,0 +1,23 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +import org.dspace.content.clarin.ClarinUserRegistration; +import org.dspace.core.Context; +import org.dspace.core.GenericDAO; + +public interface ClarinUserRegistrationDAO extends GenericDAO { + + List findByEPersonUUID(Context context, UUID epersonUUID) throws SQLException; + + List findByEmail(Context context, String email) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinVerificationTokenDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinVerificationTokenDAO.java new file mode 100644 index 000000000000..50516a4e677e --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/clarin/ClarinVerificationTokenDAO.java @@ -0,0 +1,28 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.clarin; + +import java.sql.SQLException; + +import org.dspace.content.clarin.ClarinVerificationToken; +import org.dspace.core.Context; +import org.dspace.core.GenericDAO; + +/** + * Database Access Object interface class for the ClarinVerificationToken object. + * The implementation of this class is responsible for all database calls for the ClarinVerificationToken object + * and is autowired by spring This class should only be accessed from a single service and should never be exposed + * outside the API + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public interface ClarinVerificationTokenDAO extends GenericDAO { + + ClarinVerificationToken findByToken(Context context, String token) throws SQLException; + ClarinVerificationToken findByNetID(Context context, String netID) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/clarin/MatomoReportSubscriptionDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/clarin/MatomoReportSubscriptionDAO.java new file mode 100644 index 000000000000..ad00cdf5294f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/clarin/MatomoReportSubscriptionDAO.java @@ -0,0 +1,33 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +import org.dspace.content.clarin.MatomoReportSubscription; +import org.dspace.core.Context; +import org.dspace.core.GenericDAO; + +/** + * Database Access Object interface class for the MatomoReportSubscription object. + * The implementation of this class is responsible for all database calls for the MatomoReportSubscription object + * and is autowired by spring This class should only be accessed from a single service and should never be exposed + * outside the API + * + * @author Milan Kuchtiak + */ +public interface MatomoReportSubscriptionDAO extends GenericDAO { + + MatomoReportSubscription findByItemIdAndCurrentUser(Context context, UUID itemId) throws SQLException; + + MatomoReportSubscription findByEPersonIdAndItemId(Context context, UUID ePersonId, UUID itemId) throws SQLException; + + List findByEPersonId(Context context, UUID ePersonId) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinItemDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinItemDAOImpl.java new file mode 100644 index 000000000000..d0e34acb37b6 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinItemDAOImpl.java @@ -0,0 +1,45 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.impl.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +import jakarta.persistence.Query; +import org.dspace.content.Item; +import org.dspace.content.MetadataField; +import org.dspace.content.dao.clarin.ClarinItemDAO; +import org.dspace.core.AbstractHibernateDAO; +import org.dspace.core.Context; + +public class ClarinItemDAOImpl extends AbstractHibernateDAO + implements ClarinItemDAO { + @Override + public List findByBitstreamUUID(Context context, UUID bitstreamUUID) throws SQLException { + Query query = createQuery(context, "SELECT item FROM Item as item join item.bundles bundle " + + "join bundle.bitstreams bitstream WHERE bitstream.id = :bitstreamUUID"); + + query.setParameter("bitstreamUUID", bitstreamUUID); + query.setHint("org.hibernate.cacheable", Boolean.TRUE); + + return list(query); + } + + @Override + public List findByHandle(Context context, MetadataField metadataField, String handle) throws SQLException { + Query query = createQuery(context, "SELECT item FROM Item as item join item.metadata metadata " + + "WHERE metadata.value = :handle AND metadata.metadataField = :metadata_field"); + + query.setParameter("handle", handle); + query.setParameter("metadata_field", metadataField); + query.setHint("org.hibernate.cacheable", Boolean.TRUE); + + return list(query); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinLicenseDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinLicenseDAOImpl.java new file mode 100644 index 000000000000..4938446a48ab --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinLicenseDAOImpl.java @@ -0,0 +1,57 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.impl.clarin; + +import java.sql.SQLException; +import java.util.List; + +import jakarta.persistence.Query; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import org.dspace.content.clarin.ClarinLicense; +import org.dspace.content.clarin.ClarinLicense_; +import org.dspace.content.dao.clarin.ClarinLicenseDAO; +import org.dspace.core.AbstractHibernateDAO; +import org.dspace.core.Context; + +/** + * Hibernate implementation of the Database Access Object interface class for the Clarin License object. + * This class is responsible for all database calls for the Clarin License object and is autowired by spring + * This class should never be accessed directly. + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class ClarinLicenseDAOImpl extends AbstractHibernateDAO implements ClarinLicenseDAO { + protected ClarinLicenseDAOImpl() { + super(); + } + + @Override + public ClarinLicense findByName(Context context, String name) throws SQLException { + Query query = createQuery(context, "SELECT cl " + + "FROM ClarinLicense cl " + + "WHERE cl.name = :name"); + + query.setParameter("name", name); + query.setHint("org.hibernate.cacheable", Boolean.TRUE); + + return singleResult(query); + } + + @Override + public List findByNameLike(Context context, String name) throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, ClarinLicense.class); + Root clarinLicenseRoot = criteriaQuery.from(ClarinLicense.class); + criteriaQuery.select(clarinLicenseRoot); + criteriaQuery.where(criteriaBuilder.like(clarinLicenseRoot.get(ClarinLicense_.name), "%" + name + "%")); + criteriaQuery.orderBy(criteriaBuilder.asc(clarinLicenseRoot.get(ClarinLicense_.name))); + return list(context, criteriaQuery, false, ClarinLicense.class, -1, -1); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinLicenseLabelDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinLicenseLabelDAOImpl.java new file mode 100644 index 000000000000..1bf2179a3935 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinLicenseLabelDAOImpl.java @@ -0,0 +1,26 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.impl.clarin; + +import org.dspace.content.clarin.ClarinLicenseLabel; +import org.dspace.content.dao.clarin.ClarinLicenseLabelDAO; +import org.dspace.core.AbstractHibernateDAO; + +/** + * Hibernate implementation of the Database Access Object interface class for the Clarin License Label object. + * This class is responsible for all database calls for the Clarin License Label object and is autowired by spring + * This class should never be accessed directly. + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class ClarinLicenseLabelDAOImpl extends AbstractHibernateDAO + implements ClarinLicenseLabelDAO { + protected ClarinLicenseLabelDAOImpl() { + super(); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinLicenseResourceMappingDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinLicenseResourceMappingDAOImpl.java new file mode 100644 index 000000000000..9bee9486f313 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinLicenseResourceMappingDAOImpl.java @@ -0,0 +1,74 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.impl.clarin; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import jakarta.persistence.Query; +import org.dspace.content.clarin.ClarinLicenseResourceMapping; +import org.dspace.content.dao.clarin.ClarinLicenseResourceMappingDAO; +import org.dspace.core.AbstractHibernateDAO; +import org.dspace.core.Context; + +public class ClarinLicenseResourceMappingDAOImpl extends AbstractHibernateDAO + implements ClarinLicenseResourceMappingDAO { + protected ClarinLicenseResourceMappingDAOImpl() { + super(); + } + + /** + * Maximum number of UUIDs to include per query batch. + */ + private static final int BATCH_SIZE = 10_000; + + @Override + public List findByBitstreamUUID(Context context, UUID bitstreamUUID) + throws SQLException { + Query query = createQuery(context, "SELECT clrm " + + "FROM ClarinLicenseResourceMapping clrm " + + "WHERE clrm.bitstream.id = :bitstreamUUID"); + + query.setParameter("bitstreamUUID", bitstreamUUID); + query.setHint("org.hibernate.cacheable", Boolean.TRUE); + + return list(query); + } + + @Override + public List findByBitstreamUUIDs(Context context, List bitstreamUUIDs) + throws SQLException { + if (bitstreamUUIDs == null || bitstreamUUIDs.isEmpty()) { + return List.of(); + } + List results = new ArrayList<>(); + + for (int i = 0; i < bitstreamUUIDs.size(); i += BATCH_SIZE) { + int end = Math.min(i + BATCH_SIZE, bitstreamUUIDs.size()); + List batch = bitstreamUUIDs.subList(i, end); + + Query query = createQuery(context, + "SELECT clrm FROM ClarinLicenseResourceMapping clrm " + + "WHERE clrm.bitstream.id IN :bitstreamUUIDs"); + query.setParameter("bitstreamUUIDs", batch); + query.setHint("org.hibernate.cacheable", Boolean.TRUE); + + results.addAll(list(query)); + } + + return results; + } + + @Override + public void delete(Context context, ClarinLicenseResourceMapping clarinLicenseResourceMapping) throws SQLException { + clarinLicenseResourceMapping.setBitstream(null); + super.delete(context, clarinLicenseResourceMapping); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinLicenseResourceUserAllowanceDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinLicenseResourceUserAllowanceDAOImpl.java new file mode 100644 index 000000000000..12544f4f403a --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinLicenseResourceUserAllowanceDAOImpl.java @@ -0,0 +1,81 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.impl.clarin; + +import java.sql.SQLException; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import jakarta.persistence.Query; +import org.dspace.content.clarin.ClarinLicenseResourceUserAllowance; +import org.dspace.content.dao.clarin.ClarinLicenseResourceUserAllowanceDAO; +import org.dspace.core.AbstractHibernateDAO; +import org.dspace.core.Context; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + +public class ClarinLicenseResourceUserAllowanceDAOImpl extends AbstractHibernateDAO + implements ClarinLicenseResourceUserAllowanceDAO { + + @Autowired + ConfigurationService configurationService; + + @Override + public List findByTokenAndBitstreamId(Context context, UUID resourceID, + String token) throws SQLException { + Query query = createQuery(context, "SELECT clrua " + + "FROM ClarinLicenseResourceUserAllowance clrua " + + "WHERE clrua.token = :token AND clrua.licenseResourceMapping.bitstream.id = :resourceID " + + "AND clrua.createdOn >= :notGeneratedBefore"); + + // Token is expired after 30 days by default, the default value could be changed by the value from + // the configuration + int tokenExpirationDays = + configurationService.getIntProperty("bitstream.download.token.expiration.days", 30); + + Calendar cal = Calendar.getInstance(); + cal.setTime(new Date()); + cal.add(Calendar.DAY_OF_MONTH, -tokenExpirationDays); + + query.setParameter("token", token); + query.setParameter("resourceID", resourceID); + query.setParameter("notGeneratedBefore", cal.getTime()); + query.setHint("org.hibernate.cacheable", Boolean.TRUE); + + return list(query); + } + + @Override + public List findByEPersonId(Context context, UUID userID) throws SQLException { + Query query = createQuery(context, "SELECT clrua " + + "FROM ClarinLicenseResourceUserAllowance clrua " + + "WHERE clrua.userRegistration.ePersonID = :userID"); + + query.setParameter("userID", userID); + query.setHint("org.hibernate.cacheable", Boolean.TRUE); + + return list(query); + } + + @Override + public List findByEPersonIdAndBitstreamId(Context context, UUID userID, + UUID bitstreamID) throws SQLException { + Query query = createQuery(context, "SELECT clrua " + + "FROM ClarinLicenseResourceUserAllowance clrua " + + "WHERE clrua.userRegistration.ePersonID = :userID " + + "AND clrua.licenseResourceMapping.bitstream.id = :bitstreamID"); + + query.setParameter("userID", userID); + query.setParameter("bitstreamID", bitstreamID); + query.setHint("org.hibernate.cacheable", Boolean.TRUE); + + return list(query); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinTokenDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinTokenDAOImpl.java new file mode 100644 index 000000000000..b04ca30e000b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinTokenDAOImpl.java @@ -0,0 +1,44 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.impl.clarin; + +import java.sql.SQLException; + +import jakarta.persistence.Query; +import org.dspace.content.clarin.ClarinToken; +import org.dspace.content.dao.clarin.ClarinTokenDAO; +import org.dspace.core.AbstractHibernateDAO; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; + +/** + * Hibernate implementation of the Database Access Object interface class for the ClarinToken object. + * This class is responsible for all database calls for theClarinToken object and is autowired by spring + * This class should never be accessed directly. + * + * @author Milan Kuchtiak + */ +public class ClarinTokenDAOImpl extends AbstractHibernateDAO + implements ClarinTokenDAO { + + @Override + public void deleteTokensForEPerson(Context context, EPerson ePerson) throws SQLException { + Query query = createQuery(context, "DELETE FROM ClarinToken " + + "WHERE ePerson = :ePerson"); + query.setParameter("ePerson", ePerson); + query.executeUpdate(); + context.commit(); + } + + @Override + public void deleteAll(Context context) throws SQLException { + String stringQuery = "DELETE FROM ClarinToken"; + createQuery(context, stringQuery).executeUpdate(); + context.commit(); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinUserMetadataDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinUserMetadataDAOImpl.java new file mode 100644 index 000000000000..466c0ece7b64 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinUserMetadataDAOImpl.java @@ -0,0 +1,44 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.impl.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +import jakarta.persistence.Query; +import org.dspace.content.clarin.ClarinUserMetadata; +import org.dspace.content.dao.clarin.ClarinUserMetadataDAO; +import org.dspace.core.AbstractHibernateDAO; +import org.dspace.core.Context; + +public class ClarinUserMetadataDAOImpl extends AbstractHibernateDAO + implements ClarinUserMetadataDAO { + + protected ClarinUserMetadataDAOImpl() { + super(); + } + + @Override + public List findByUserRegistrationAndBitstream(Context context, Integer userRegUUID, + UUID bitstreamUUID) throws SQLException { + Query query = createQuery(context, "SELECT cum FROM ClarinUserMetadata as cum " + + "JOIN cum.eperson as ur " + + "JOIN cum.transaction as clrua " + + "JOIN clrua.licenseResourceMapping as map " + + "WHERE ur.id = :userRegUUID " + + "AND map.bitstream.id = :bitstreamUUID " + + "ORDER BY clrua.id DESC"); + + query.setParameter("userRegUUID", userRegUUID); + query.setParameter("bitstreamUUID", bitstreamUUID); + query.setHint("org.hibernate.cacheable", Boolean.TRUE); + + return list(query); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinUserRegistrationDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinUserRegistrationDAOImpl.java new file mode 100644 index 000000000000..27aa056e8414 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinUserRegistrationDAOImpl.java @@ -0,0 +1,48 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.impl.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +import jakarta.persistence.Query; +import org.dspace.content.clarin.ClarinUserRegistration; +import org.dspace.content.dao.clarin.ClarinUserRegistrationDAO; +import org.dspace.core.AbstractHibernateDAO; +import org.dspace.core.Context; + +public class ClarinUserRegistrationDAOImpl extends AbstractHibernateDAO + implements ClarinUserRegistrationDAO { + + protected ClarinUserRegistrationDAOImpl() { + super(); + } + + @Override + public List findByEPersonUUID(Context context, UUID epersonUUID) throws SQLException { + Query query = createQuery(context, "SELECT cur FROM ClarinUserRegistration as cur " + + "WHERE cur.ePersonID = :epersonUUID"); + + query.setParameter("epersonUUID", epersonUUID); + query.setHint("org.hibernate.cacheable", Boolean.TRUE); + + return list(query); + } + + @Override + public List findByEmail(Context context, String email) throws SQLException { + Query query = createQuery(context, "SELECT cur FROM ClarinUserRegistration as cur " + + "WHERE cur.email = :email"); + + query.setParameter("email", email); + query.setHint("org.hibernate.cacheable", Boolean.TRUE); + + return list(query); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinVerificationTokenDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinVerificationTokenDAOImpl.java new file mode 100644 index 000000000000..b287d849f5d9 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/ClarinVerificationTokenDAOImpl.java @@ -0,0 +1,56 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.impl.clarin; + +import java.sql.SQLException; + +import jakarta.persistence.Query; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import org.dspace.content.clarin.ClarinVerificationToken; +import org.dspace.content.clarin.ClarinVerificationToken_; +import org.dspace.content.dao.clarin.ClarinVerificationTokenDAO; +import org.dspace.core.AbstractHibernateDAO; +import org.dspace.core.Context; + +/** + * Hibernate implementation of the Database Access Object interface class for the ClarinVerificationToken object. + * This class is responsible for all database calls for the ClarinVerificationToken object and is autowired by spring + * This class should never be accessed directly. + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class ClarinVerificationTokenDAOImpl extends AbstractHibernateDAO + implements ClarinVerificationTokenDAO { + + @Override + public ClarinVerificationToken findByToken(Context context, String token) throws SQLException { + Query query = createQuery(context, "SELECT cvt " + + "FROM ClarinVerificationToken cvt " + + "WHERE cvt.token = :token"); + + query.setParameter("token", token); + query.setHint("org.hibernate.cacheable", Boolean.TRUE); + + return singleResult(query); + } + + @Override + public ClarinVerificationToken findByNetID(Context context, String netID) throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, ClarinVerificationToken.class); + Root clarinVerificationTokenRoot = criteriaQuery.from(ClarinVerificationToken.class); + criteriaQuery.select(clarinVerificationTokenRoot); + criteriaQuery.where(criteriaBuilder.like(clarinVerificationTokenRoot.get(ClarinVerificationToken_.ePersonNetID), + "%" + netID + "%")); + criteriaQuery.orderBy(criteriaBuilder.asc(clarinVerificationTokenRoot. + get(ClarinVerificationToken_.ePersonNetID))); + return singleResult(context, criteriaQuery); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/MatomoReportSubscriptionDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/MatomoReportSubscriptionDAOImpl.java new file mode 100644 index 000000000000..88a2cf72019c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/clarin/MatomoReportSubscriptionDAOImpl.java @@ -0,0 +1,65 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.dao.impl.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +import jakarta.persistence.Query; +import org.dspace.content.clarin.MatomoReportSubscription; +import org.dspace.content.dao.clarin.MatomoReportSubscriptionDAO; +import org.dspace.core.AbstractHibernateDAO; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; + +/** + * Hibernate implementation of the Database Access Object interface class for the MatomoReportSubscription object. + * This class is responsible for all database calls for the MatomoReportSubscription object and is autowired by spring + * This class should never be accessed directly. + * + * @author Milan Kuchtiak + */ +public class MatomoReportSubscriptionDAOImpl extends AbstractHibernateDAO + implements MatomoReportSubscriptionDAO { + protected MatomoReportSubscriptionDAOImpl() { + super(); + } + + @Override + public MatomoReportSubscription findByItemIdAndCurrentUser(Context context, UUID itemId)throws SQLException { + EPerson currentUser = context.getCurrentUser(); + return findByEPersonIdAndItemId(context, currentUser.getID(), itemId); + } + + @Override + public MatomoReportSubscription findByEPersonIdAndItemId(Context context, UUID ePersonId, UUID itemId) + throws SQLException { + Query query = createQuery( + context, + "SELECT m FROM MatomoReportSubscription m WHERE m.ePerson.id = :ePersonId AND m.item.id = :itemId" + ); + query.setParameter("ePersonId", ePersonId); + query.setParameter("itemId", itemId); + query.setHint("org.hibernate.cacheable", Boolean.TRUE); + + return singleResult(query); + } + + @Override + public List findByEPersonId(Context context, UUID ePersonId) throws SQLException { + Query query = createQuery( + context, + "SELECT m FROM MatomoReportSubscription m WHERE m.ePerson.id = :ePersonId" + ); + query.setParameter("ePersonId", ePersonId); + query.setHint("org.hibernate.cacheable", Boolean.TRUE); + + return list(query); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/factory/ClarinServiceFactory.java b/dspace-api/src/main/java/org/dspace/content/factory/ClarinServiceFactory.java new file mode 100644 index 000000000000..844a5aacafe7 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/factory/ClarinServiceFactory.java @@ -0,0 +1,61 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.factory; + +import org.dspace.content.service.clarin.ClarinItemService; +import org.dspace.content.service.clarin.ClarinLicenseLabelService; +import org.dspace.content.service.clarin.ClarinLicenseResourceMappingService; +import org.dspace.content.service.clarin.ClarinLicenseResourceUserAllowanceService; +import org.dspace.content.service.clarin.ClarinLicenseService; +import org.dspace.content.service.clarin.ClarinTokenService; +import org.dspace.content.service.clarin.ClarinUserMetadataService; +import org.dspace.content.service.clarin.ClarinUserRegistrationService; +import org.dspace.content.service.clarin.ClarinVerificationTokenService; +import org.dspace.content.service.clarin.MatomoReportSubscriptionService; +import org.dspace.handle.service.HandleClarinService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.matomo.java.tracking.MatomoTracker; + +/** + * Abstract factory to get services for the clarin package, use ClarinServiceFactory.getInstance() to retrieve an + * implementation + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public abstract class ClarinServiceFactory { + + public abstract ClarinLicenseService getClarinLicenseService(); + + public abstract ClarinLicenseLabelService getClarinLicenseLabelService(); + + public abstract ClarinLicenseResourceMappingService getClarinLicenseResourceMappingService(); + + public abstract HandleClarinService getClarinHandleService(); + + public abstract ClarinUserRegistrationService getClarinUserRegistration(); + + public abstract ClarinUserMetadataService getClarinUserMetadata(); + + public abstract ClarinLicenseResourceUserAllowanceService getClarinLicenseResourceUserAllowance(); + + public abstract ClarinVerificationTokenService getClarinVerificationTokenService(); + + public abstract MatomoTracker getMatomoTracker(); + + public abstract ClarinItemService getClarinItemService(); + + public abstract MatomoReportSubscriptionService getMatomoReportService(); + + public abstract ClarinTokenService getClarinTokenService(); + + public static ClarinServiceFactory getInstance() { + return DSpaceServicesFactory.getInstance().getServiceManager() + .getServiceByName("clarinServiceFactory", ClarinServiceFactory.class); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/content/factory/ClarinServiceFactoryImpl.java b/dspace-api/src/main/java/org/dspace/content/factory/ClarinServiceFactoryImpl.java new file mode 100644 index 000000000000..1fe989b56437 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/factory/ClarinServiceFactoryImpl.java @@ -0,0 +1,127 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.factory; + +import org.dspace.content.service.clarin.ClarinItemService; +import org.dspace.content.service.clarin.ClarinLicenseLabelService; +import org.dspace.content.service.clarin.ClarinLicenseResourceMappingService; +import org.dspace.content.service.clarin.ClarinLicenseResourceUserAllowanceService; +import org.dspace.content.service.clarin.ClarinLicenseService; +import org.dspace.content.service.clarin.ClarinTokenService; +import org.dspace.content.service.clarin.ClarinUserMetadataService; +import org.dspace.content.service.clarin.ClarinUserRegistrationService; +import org.dspace.content.service.clarin.ClarinVerificationTokenService; +import org.dspace.content.service.clarin.MatomoReportSubscriptionService; +import org.dspace.handle.service.HandleClarinService; +import org.matomo.java.tracking.MatomoTracker; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Factory implementation to get services for the clarin package, use ClarinServiceFactory.getInstance() + * to retrieve an implementation + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class ClarinServiceFactoryImpl extends ClarinServiceFactory { + + @Autowired(required = true) + private ClarinLicenseService clarinLicenseService; + + @Autowired(required = true) + private ClarinLicenseLabelService clarinLicenseLabelService; + + @Autowired(required = true) + private ClarinLicenseResourceMappingService clarinLicenseResourceMappingService; + + @Autowired(required = true) + private HandleClarinService handleClarinService; + + @Autowired(required = true) + private ClarinUserRegistrationService clarinUserRegistrationService; + + @Autowired(required = true) + private ClarinUserMetadataService clarinUserMetadataService; + + @Autowired(required = true) + private ClarinLicenseResourceUserAllowanceService clarinLicenseResourceUserAllowanceService; + + @Autowired(required = true) + private ClarinVerificationTokenService clarinVerificationTokenService; + + @Autowired(required = true) + private ClarinItemService clarinItemService; + + @Autowired(required = true) + private MatomoTracker matomoTracker; + + @Autowired(required = true) + private MatomoReportSubscriptionService matomoReportSubscriptionService; + + @Autowired(required = true) + private ClarinTokenService clarinTokenService; + + @Override + public ClarinLicenseService getClarinLicenseService() { + return clarinLicenseService; + } + + @Override + public ClarinLicenseLabelService getClarinLicenseLabelService() { + return clarinLicenseLabelService; + } + + @Override + public ClarinLicenseResourceMappingService getClarinLicenseResourceMappingService() { + return clarinLicenseResourceMappingService; + } + + @Override + public HandleClarinService getClarinHandleService() { + return handleClarinService; + } + + @Override + public ClarinUserRegistrationService getClarinUserRegistration() { + return clarinUserRegistrationService; + } + + @Override + public ClarinUserMetadataService getClarinUserMetadata() { + return clarinUserMetadataService; + } + + @Override + public ClarinLicenseResourceUserAllowanceService getClarinLicenseResourceUserAllowance() { + return clarinLicenseResourceUserAllowanceService; + } + + @Override + public ClarinVerificationTokenService getClarinVerificationTokenService() { + return clarinVerificationTokenService; + } + + @Override + public MatomoTracker getMatomoTracker() { + return matomoTracker; + } + + @Override + public ClarinItemService getClarinItemService() { + return clarinItemService; + } + + @Override + public MatomoReportSubscriptionService getMatomoReportService() { + return matomoReportSubscriptionService; + } + + @Override + public ClarinTokenService getClarinTokenService() { + return clarinTokenService; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/service/DspaceObjectClarinService.java b/dspace-api/src/main/java/org/dspace/content/service/DspaceObjectClarinService.java new file mode 100644 index 000000000000..bad38a3cb0af --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/service/DspaceObjectClarinService.java @@ -0,0 +1,32 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.service; + +import java.sql.SQLException; + +import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; +import org.dspace.core.Context; + +/** + * Additional service interface class of DspaceObjectService for the DspaceObject in Clarin-DSpace. + * + * @author Michaela Paurikova (michaela.paurikova at dataquest.sk) + */ +public interface DspaceObjectClarinService { + + /* Created for LINDAT/CLARIAH-CZ (UFAL) */ + /** + * Retrieve all handle from the registry + * + * @param context DSpace context object + * @return array of handles + * @throws SQLException if database error + */ + public Community getPrincipalCommunity(Context context, DSpaceObject dso) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinItemService.java b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinItemService.java new file mode 100644 index 000000000000..0559b10e1378 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinItemService.java @@ -0,0 +1,105 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.service.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +import org.dspace.content.Bitstream; +import org.dspace.content.Bundle; +import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.MetadataField; +import org.dspace.core.Context; + +/** + * Service interface class for the Item object. + * This service is enhancement of the ItemService service for Clarin project purposes. + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public interface ClarinItemService { + + /** + * Find Item by the BitstreamUUID + * @param context DSpace context object + * @param bitstreamUUID UUID of the finding bitstream + * @return found bitstream or null + * @throws SQLException database error + */ + List findByBitstreamUUID(Context context, UUID bitstreamUUID) throws SQLException; + + /** + * Find Item by the Handle + * @param context DSpace context object + * @param handle String of the finding item + * @return found Item or null + * @throws SQLException database error + */ + List findByHandle(Context context, MetadataField metadataField, String handle) throws SQLException; + + /** + * Get item/collection/community's owning community + * @param context DSpace context object + * @param dso item/collection/community + * @return owning community or null + */ + Community getOwningCommunity(Context context, DSpaceObject dso); + + /** + * Get owning community from the collection with UUID which is passed to the method. + * @param context DSpace context object + * @param owningCollectionId UUID of the collection to get the owning community + * @return owning community or null + * @throws SQLException + */ + Community getOwningCommunity(Context context, UUID owningCollectionId) throws SQLException; + + /** + * Update item's metadata about its files (local.has.files, local.files.size, local.files.count). + * This method doesn't require Item's Bundle to be passed as a parameter. The ORIGINAL bundle is used by default. + * @param context DSpace context object + * @param item Update metadata for this Item + * @throws SQLException + */ + void updateItemFilesMetadata(Context context, Item item) throws SQLException; + + /** + * Update item's metadata about its files (local.has.files, local.files.size, local.files.count). + * @param context DSpace context object + * @param item Update metadata for this Item + * @param bundle Bundle to be used for the metadata update - it if is not the ORIGINAL bundle + * the method will be skipped. + * @throws SQLException + */ + void updateItemFilesMetadata(Context context, Item item, Bundle bundle) throws SQLException; + + /** + * Update item's metadata about its files (local.has.files, local.files.size, local.files.count). + * The Item and Bundle information is taken from the Bitstream object. + * @param context + * @param bit + * @throws SQLException + */ + void updateItemFilesMetadata(Context context, Bitstream bit) throws SQLException; + + /** + * Update item's metadata about its dates (dc.date.issued, local.approximateDate.issued). + * If the local.approximateDate.issued has any approximate value, e.g. 'cca 1938 - 1945' or 'approx. 1995' + * or similar, use 0000 + * If the local.approximateDate.issued has several values, e.g. 1993, 1918, 2021 use the last one: + * `dc.date.issued` = 2021 + * + * @param context DSpace context object + * @param item Update metadata for this Item + */ + void updateItemDatesMetadata(Context context, Item item) throws SQLException; + +} diff --git a/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinLicenseLabelService.java b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinLicenseLabelService.java new file mode 100644 index 000000000000..adb56ecc238d --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinLicenseLabelService.java @@ -0,0 +1,81 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.service.clarin; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.clarin.ClarinLicenseLabel; +import org.dspace.core.Context; + +/** + * Service interface class for the Clarin License Label object. + * The implementation of this class is responsible for all business logic calls for the Clarin License Label object + * and is autowired by spring + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public interface ClarinLicenseLabelService { + + /** + * Create a new clarin license label. Authorization is done inside this method. + * @param context DSpace context object + * @return the newly created clarin license label + * @throws SQLException if database error + * @throws AuthorizeException the user in not admin + */ + ClarinLicenseLabel create(Context context) throws SQLException, AuthorizeException; + + /** + * Create a new clarin license label. Authorization is done inside this method. + * @param context DSpace context object + * @param clarinLicenseLabel new clarin license label object data + * @return the newly created clarin license label + * @throws SQLException if database error + * @throws AuthorizeException the user in not admin + */ + ClarinLicenseLabel create(Context context, ClarinLicenseLabel clarinLicenseLabel) throws SQLException, + AuthorizeException; + + /** + * Find the clarin license label object by id + * @param context DSpace context object + * @param valueId id of the searching clarin license label object + * @return found clarin license label object or null + * @throws SQLException if database error + */ + ClarinLicenseLabel find(Context context, int valueId) throws SQLException; + + /** + * Find all clarin license label objects + * @param context DSpace context object + * @return list of all clarin license label objects + * @throws SQLException if database error + * @throws AuthorizeException the user in not admin + */ + List findAll(Context context) throws SQLException, AuthorizeException; + + /** + * Delete the clarin license label by id. The id is retrieved from passed clarin license label object. + * @param context DSpace context object + * @param clarinLicenseLabel object to delete + * @throws SQLException if database error + * @throws AuthorizeException the user in not admin + */ + void delete(Context context, ClarinLicenseLabel clarinLicenseLabel) throws SQLException, AuthorizeException; + + /** + * Update the clarin license label object by id. The id is retrieved from passed clarin license label object. + * @param context DSpace context object + * @param newClarinLicenseLabel with new clarin license label object values + * @throws SQLException if database error + * @throws AuthorizeException the user is not admin + */ + void update(Context context, ClarinLicenseLabel newClarinLicenseLabel) throws SQLException, AuthorizeException; +} diff --git a/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinLicenseResourceMappingService.java b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinLicenseResourceMappingService.java new file mode 100644 index 000000000000..1523dc7a6c30 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinLicenseResourceMappingService.java @@ -0,0 +1,47 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.service.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Bitstream; +import org.dspace.content.clarin.ClarinLicense; +import org.dspace.content.clarin.ClarinLicenseResourceMapping; +import org.dspace.core.Context; + +public interface ClarinLicenseResourceMappingService { + + ClarinLicenseResourceMapping create(Context context) throws SQLException, AuthorizeException; + ClarinLicenseResourceMapping create(Context context, ClarinLicenseResourceMapping clarinLicenseResourceMapping) + throws SQLException, AuthorizeException; + ClarinLicenseResourceMapping create(Context context, Integer licenseId, UUID bitstreamUuid) + throws SQLException, AuthorizeException; + + ClarinLicenseResourceMapping find(Context context, int valueId) throws SQLException; + List findAll(Context context) throws SQLException; + List findAllByLicenseId(Context context, Integer licenseId) throws SQLException; + + void update(Context context, ClarinLicenseResourceMapping newClarinLicenseResourceMapping) throws SQLException; + + void delete(Context context, ClarinLicenseResourceMapping clarinLicenseResourceMapping) throws SQLException; + + void detachLicenses(Context context, Bitstream bitstream) throws SQLException; + + void attachLicense(Context context, ClarinLicense clarinLicense, Bitstream bitstream) + throws SQLException, AuthorizeException; + + List findByBitstreamUUID(Context context, UUID bitstreamID) throws SQLException; + + List findByBitstreamUUIDs(Context context, List bitstreamIDs) + throws SQLException; + + ClarinLicense getLicenseToAgree(Context context, UUID userId, UUID resourceID) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinLicenseResourceUserAllowanceService.java b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinLicenseResourceUserAllowanceService.java new file mode 100644 index 000000000000..be2a881825e8 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinLicenseResourceUserAllowanceService.java @@ -0,0 +1,33 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.service.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.clarin.ClarinLicenseResourceUserAllowance; +import org.dspace.core.Context; + +public interface ClarinLicenseResourceUserAllowanceService { + ClarinLicenseResourceUserAllowance create(Context context) throws SQLException; + ClarinLicenseResourceUserAllowance find(Context context, int valueId) throws SQLException, AuthorizeException; + List findAll(Context context) throws SQLException, AuthorizeException; + void update(Context context, ClarinLicenseResourceUserAllowance clarinLicenseResourceUserAllowance) + throws SQLException, AuthorizeException; + void delete(Context context, ClarinLicenseResourceUserAllowance clarinLicenseResourceUserAllowance) + throws SQLException, AuthorizeException; + boolean verifyToken(Context context, UUID resourceID, String token) throws SQLException; + boolean isUserAllowedToAccessTheResource(Context context, UUID userId, UUID resourceId) throws SQLException; + List findByEPersonId(Context context, UUID userID) + throws SQLException, AuthorizeException; + List findByEPersonIdAndBitstreamId(Context context, UUID userID, + UUID bitstreamID) + throws SQLException, AuthorizeException; +} diff --git a/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinLicenseService.java b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinLicenseService.java new file mode 100644 index 000000000000..93fbe88df3cb --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinLicenseService.java @@ -0,0 +1,115 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.service.clarin; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Bitstream; +import org.dspace.content.Bundle; +import org.dspace.content.Item; +import org.dspace.content.clarin.ClarinLicense; +import org.dspace.core.Context; + +/** + * Service interface class for the Clarin License object. + * The implementation of this class is responsible for all business logic calls for the Clarin License object + * and is autowired by spring + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public interface ClarinLicenseService { + + /** + * Create a new clarin license. Authorization is done inside this method. + * + * @param context @param context DSpace context object + * @return the newly created clarin license + * @throws SQLException if database error + * @throws AuthorizeException the user in not admin + */ + ClarinLicense create(Context context) throws SQLException, AuthorizeException; + + /** + * Create a new clarin license. Authorization is done inside this method. + * + * @param context DSpace context object + * @param clarinLicense new clarin license object data + * @return the newly created clarin license + * @throws SQLException if database error + * @throws AuthorizeException the user in not admin + */ + ClarinLicense create(Context context, ClarinLicense clarinLicense) throws SQLException, AuthorizeException; + + /** + * Find the clarin license object by id + * + * @param context DSpace context object + * @param valueId id of the searching clarin license object + * @return found clarin license object or null + * @throws SQLException if database error + */ + ClarinLicense find(Context context, int valueId) throws SQLException; + + /** + * Find the Clarin License by the full clarin license name. + * + * @param context DSpace context object + * @param name the full clarin license name + * @return Clarin License with searching name. + * @throws SQLException + */ + ClarinLicense findByName(Context context, String name) throws SQLException; + + /** + * Find the Clarin License by the substring of the clarin license name. + * + * @param context DSpace context object + * @param name substring of the clarin license name + * @return List of clarin licenses which contains searching string in it's name + * @throws SQLException + */ + List findByNameLike(Context context, String name) throws SQLException; + + void addLicenseMetadataToItem(Context context, ClarinLicense clarinLicense, Item item) throws SQLException; + + void clearLicenseMetadataFromItem(Context context, Item item) throws SQLException; + + void addClarinLicenseToBitstream(Context context, Item item, Bundle bundle, Bitstream bitstream); + + /** + * Find all clarin license objects + * + * @param context DSpace context object + * @return list of all clarin license objects + * @throws SQLException if database error + * @throws AuthorizeException the user in not admin + */ + List findAll(Context context) throws SQLException, AuthorizeException; + + /** + * Delete the clarin license by id. The id is retrieved from the passed clarin license object. + * + * @param context DSpace context object + * @param clarinLicense object to delete + * @throws SQLException if database error + * @throws AuthorizeException the user in not admin + */ + void delete(Context context, ClarinLicense clarinLicense) throws SQLException, AuthorizeException; + + /** + * Update the clarin license object by id. The id is retrieved from passed clarin license object. + * + * @param context DSpace context object + * @param newClarinLicense with new clarin license object values + * @throws SQLException if database error + * @throws AuthorizeException the user is not admin + */ + void update(Context context, ClarinLicense newClarinLicense) throws SQLException, AuthorizeException; +} diff --git a/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinTokenService.java b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinTokenService.java new file mode 100644 index 000000000000..c41bffa844ee --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinTokenService.java @@ -0,0 +1,95 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.service.clarin; + +import java.sql.SQLException; +import java.text.ParseException; +import java.util.Date; + +import com.nimbusds.jose.JOSEException; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.clarin.ClarinToken; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; + +/** + * Service interface class for the ClarinTokenService object. + * The implementation of this class is responsible for all business logic calls for the ClarinTokenService object + * and is autowired by spring + * + * @author Milan Kuchtiak + */ +public interface ClarinTokenService { + + /** + * Find the ClarinToken object by id. + * Any user can get ClarinToken object for given ID, to allow JWT authentication service + * to get the userID + sign key for given token, at the moment when nobody is signed to DSpace yet. + * + * @param context DSpace context object + * @param id ID of the searching larinToken object + * @return found ClarinToken object or null + * @throws SQLException if database error + */ + ClarinToken find(Context context, Integer id) throws SQLException; + + /** + * Create new token for ePerson, with given ID, and create ClarinToken object containing shared secret string + * used to verify token. + * + * @param context DSpace context object + * @param ePerson EPerson + * @param expirationTime expiration time when token becomes expired + * @return token string + * @throws SQLException if database error + * @throws AuthorizeException when user is not allowed to create token + */ + String createToken(Context context, EPerson ePerson, Date expirationTime) throws SQLException, AuthorizeException; + + /** + * Delete/Invalidate all clarin tokens for ePerson, with given ID. + * + * @param context DSpace context object + * @param ePerson EPerson + * @throws SQLException if database error + * @throws AuthorizeException when user is not allowed to delete token + */ + void delete(Context context, EPerson ePerson) throws SQLException, AuthorizeException; + + /** + * Delete/Invalidate token. + * + * @param context DSpace context object + * @param token token string + * @throws SQLException if database error + * @throws AuthorizeException when user is not allowed to delete token + */ + void delete(Context context, String token) throws SQLException, AuthorizeException; + + /** + * Delete/Invalidate all clarin tokens. + * + * @param context DSpace context object + * @throws SQLException if database error + * @throws AuthorizeException when user is not admin user + */ + void deleteAll(Context context) throws SQLException, AuthorizeException; + + /** + * Get EPerson object from clarin token. + * + * @param context DSpace context object + * @param token clarin token string + * @return EPerson object + * @throws SQLException if database error occurs + * @throws ParseException if token parse error occurs + * @throws JOSEException if other JOSE error occurs + */ + EPerson getEPersonFromClarinToken(Context context, String token) throws SQLException, ParseException, JOSEException; + +} diff --git a/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinUserMetadataService.java b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinUserMetadataService.java new file mode 100644 index 000000000000..ca2fd1016cad --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinUserMetadataService.java @@ -0,0 +1,30 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.service.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.clarin.ClarinUserMetadata; +import org.dspace.core.Context; + +public interface ClarinUserMetadataService { + + ClarinUserMetadata create(Context context) throws SQLException; + + ClarinUserMetadata find(Context context, int valueId) throws SQLException, AuthorizeException; + List findAll(Context context) throws SQLException, AuthorizeException; + void update(Context context, ClarinUserMetadata clarinUserMetadata) throws SQLException, AuthorizeException; + void delete(Context context, ClarinUserMetadata clarinUserMetadata) throws SQLException, AuthorizeException; + + List findByUserRegistrationAndBitstream(Context context, Integer userRegUUID, + UUID bitstreamUUID, boolean lastTransaction) + throws SQLException, AuthorizeException; +} diff --git a/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinUserRegistrationService.java b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinUserRegistrationService.java new file mode 100644 index 000000000000..0434e3f417a9 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinUserRegistrationService.java @@ -0,0 +1,31 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.service.clarin; + +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.clarin.ClarinUserRegistration; +import org.dspace.core.Context; + +public interface ClarinUserRegistrationService { + ClarinUserRegistration create(Context context) throws SQLException, AuthorizeException; + ClarinUserRegistration create(Context context, + ClarinUserRegistration clarinUserRegistration) throws SQLException, AuthorizeException; + + ClarinUserRegistration find(Context context, int valueId) throws SQLException, AuthorizeException; + List findAll(Context context) throws SQLException, AuthorizeException; + List findByEPersonUUID(Context context, UUID epersonUUID) + throws SQLException, AuthorizeException; + + List findByEmail(Context context, String email) throws SQLException; + void delete(Context context, ClarinUserRegistration clarinUserRegistration) throws SQLException, AuthorizeException; + void update(Context context, ClarinUserRegistration clarinUserRegistration) throws SQLException, AuthorizeException; +} diff --git a/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinVerificationTokenService.java b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinVerificationTokenService.java new file mode 100644 index 000000000000..653a765929f1 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinVerificationTokenService.java @@ -0,0 +1,101 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.service.clarin; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.authenticate.clarin.ShibHeaders; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.clarin.ClarinVerificationToken; +import org.dspace.core.Context; + +/** + * Service interface class for the ClarinVerificationToken object. + * The implementation of this class is responsible for all business logic calls for the ClarinVerificationToken object + * and is autowired by spring + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public interface ClarinVerificationTokenService { + + /** + * Create a new clarin verification token. + * + * @param context @param context DSpace context object + * @return the newly created clarin verification token. + * @throws SQLException if database error + */ + ClarinVerificationToken create(Context context) throws SQLException; + + /** + * Find the clarin verification token object by the id + * + * @param context DSpace context object + * @param valueId id of the searching clarin license object + * @return found clarin verification token object or null + * @throws SQLException if database error + */ + ClarinVerificationToken find(Context context, int valueId) throws SQLException; + + /** + * Find all clarin verification token objects + * @param context DSpace context object + * @return List of the clarin verification token objects or null + * @throws SQLException if database error + * @throws AuthorizeException if the user is not the admin + */ + List findAll(Context context) throws SQLException, AuthorizeException; + + /** + * Find the clarin verification token object by the token + * @param context DSpace context object + * @param token of the searching clarin license object + * @return found clarin verification token object or null + * @throws SQLException if database error + */ + ClarinVerificationToken findByToken(Context context, String token) throws SQLException; + + /** + * Find the clarin verification token object by the token + * @param context DSpace context object + * @param netID of the searching clarin license object + * @return found clarin verification token object or null + * @throws SQLException if database error + */ + ClarinVerificationToken findByNetID(Context context, String netID) throws SQLException; + + /** + * Find the clarin verification token object from the shibboleth headers trying every netId header + * until the object is found + * @param context DSpace context object + * @param netIdHeaders array of the netId headers - values from the configuration + * @param shibHeaders object with the shibboleth headers + * @return found clarin verification token object or null + * @throws SQLException if database error + */ + ClarinVerificationToken findByNetID(Context context, String[] netIdHeaders, ShibHeaders shibHeaders) + throws SQLException; + + /** + * Remove the clarin verification token from DB + * @param context DSpace context object + * @param clarinUserRegistration object to delete + * @throws SQLException if database error + */ + void delete(Context context, ClarinVerificationToken clarinUserRegistration) + throws SQLException; + + /** + * Update the clarin verification token object. The object is found by id then updated + * @param context DSpace context object + * @param newClarinVerificationToken object with fresh data to update + * @throws SQLException if database error + */ + void update(Context context, ClarinVerificationToken newClarinVerificationToken) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinWorkspaceItemService.java b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinWorkspaceItemService.java new file mode 100644 index 000000000000..3d6a8966cbd1 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/service/clarin/ClarinWorkspaceItemService.java @@ -0,0 +1,60 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.service.clarin; + +import java.sql.SQLException; +import java.util.UUID; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Collection; +import org.dspace.content.WorkspaceItem; +import org.dspace.core.Context; + +/** + * Service interface class for the WorkspaceItem object created for Clarin-Dspace import. + * Contains methods needed to import bitstream when dspace5 migrating to dspace7. + * The implementation of this class is autowired by spring. + * + * @author Michaela Paurikova(michaela.paurikova at dataquest.sk) + */ +public interface ClarinWorkspaceItemService { + + /** + * Create a new empty workspace item. + * Set workspace item attributes by its input values. + * @param context context + * @param collection Collection being submitted to + * @param multipleTitles contains multiple titles + * @param publishedBefore published before + * @param multipleFiles contains multiple files + * @param stageReached stage reached + * @param pageReached page reached + * @param template if true, the workspace item starts as a copy + * of the collection's template item + * @param isNewVersion controls handle creation behavior during import operations: + * if false, a new handle is created; + * if true, the existing handle is preserved. + * @return created workspace item + * @throws AuthorizeException if authorization error + * @throws SQLException if database error + */ + public WorkspaceItem create(Context context, Collection collection, + boolean multipleTitles, boolean publishedBefore, + boolean multipleFiles, Integer stageReached, + Integer pageReached, boolean template, boolean isNewVersion) + throws AuthorizeException, SQLException; + + /*** + * Find workspace item by its UUID. + * @param context context + * @param uuid workspace item UUID + * @return found workspace item + * @throws SQLException if database error + */ + public WorkspaceItem find(Context context, UUID uuid) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/content/service/clarin/MatomoReportSubscriptionService.java b/dspace-api/src/main/java/org/dspace/content/service/clarin/MatomoReportSubscriptionService.java new file mode 100644 index 000000000000..bc331109e393 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/service/clarin/MatomoReportSubscriptionService.java @@ -0,0 +1,83 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.service.clarin; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Item; +import org.dspace.content.clarin.MatomoReportSubscription; +import org.dspace.core.Context; + +/** + * Service interface class for the MatomoReportSubscription object. + * The implementation of this class is responsible for all business logic calls for the MatomoReportSubscription object + * and is autowired by spring + * + * @author Milan Kuchtiak + */ +public interface MatomoReportSubscriptionService { + + /** + * Subscribe current user to get Matomo report for the item. + * @param context DSpace context object + * @param item Item to be included in Matomo report + * @return the newly created MatomoReport object + * @throws SQLException if database error + * @throws AuthorizeException the user in not admin + */ + MatomoReportSubscription subscribe(Context context, Item item) throws SQLException, AuthorizeException; + + /** + * Unsubscribe current user from getting the Matomo report for the item. + * @param context DSpace context object + * @param item Item to be excluded from Matomo report + * @throws SQLException if database error + * @throws AuthorizeException the user in not admin + */ + void unsubscribe(Context context, Item item) throws SQLException, AuthorizeException; + + /** + * Check if current user is subscribed to get the Matomo report for the item. + * @param context DSpace context object + * @param item Item to be checked if included in Matomo report + * @throws SQLException if database error + * @throws AuthorizeException the user in not admin + */ + boolean isSubscribed(Context context, Item item) throws SQLException, AuthorizeException; + + /** + * Find the MatomoReportSubscription object by id + * @param context DSpace context object + * @param id id of the searching MatomoReportSubscription object + * @return found MatomoReportSubscription object or null + * @throws SQLException if database error + */ + MatomoReportSubscription find(Context context, int id) throws SQLException, AuthorizeException; + + /** + * Find the MatomoReportSubscription object for the item, for current user. + * @param context DSpace context object + * @param item Item object for which the MatomoReportSubscription is searched for + * @return found MatomoReportSubscription object or null + * @throws SQLException if database error + * @throws AuthorizeException the user in not + */ + MatomoReportSubscription findByItem(Context context, Item item) throws SQLException, AuthorizeException; + + /** + * Find all MatomoReportSubscription objects, only available for admin user. + * @param context DSpace context object + * @return list of all MatomoReportSubscription objects + * @throws SQLException if database error + * @throws AuthorizeException the user in not admin + */ + List findAll(Context context) throws SQLException, AuthorizeException; + +} diff --git a/dspace-api/src/main/java/org/dspace/core/ProvenanceMessageFormatter.java b/dspace-api/src/main/java/org/dspace/core/ProvenanceMessageFormatter.java new file mode 100644 index 000000000000..dd26c33388ac --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/core/ProvenanceMessageFormatter.java @@ -0,0 +1,142 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.core; + +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.ResourcePolicy; +import org.dspace.content.Bitstream; +import org.dspace.content.Collection; +import org.dspace.content.DCDate; +import org.dspace.content.Item; +import org.dspace.content.MetadataField; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.InstallItemService; +import org.dspace.eperson.EPerson; + +/** + * The ProvenanceMessageProvider providing methods to generate provenance messages for DSpace items. + * It loads message templates + * from a JSON file and formats messages based on the context, including user details and timestamps. + * + * @author Michaela Paurikova (dspace at dataquest.sk) + */ +public class ProvenanceMessageFormatter { + private InstallItemService installItemService; + + public ProvenanceMessageFormatter() {} + + public String getMessage(Context context, String messageTemplate, Item item, Object... args) + throws SQLException, AuthorizeException { + // Initialize InstallItemService if it is not initialized. + if (installItemService == null) { + installItemService = ContentServiceFactory.getInstance().getInstallItemService(); + } + String msg = getMessage(context, messageTemplate, args); + msg = msg + "\n" + installItemService.getBitstreamProvenanceMessage(context, item); + return msg; + } + + public String getMessage(Context context, String messageTemplate, Object... args) { + EPerson currentUser = context.getCurrentUser(); + String timestamp = DCDate.getCurrent().toString(); + String details = validateMessageTemplate(messageTemplate, args); + + // Handle null user case + if (currentUser == null) { + return String.format("%s by None on %s", + details, + timestamp); + } + + return String.format("%s by %s (%s) on %s", + details, + currentUser.getFullName(), + currentUser.getEmail(), + timestamp); + } + + public String getMessage(Item item) { + String msg = "Item was in collections:\n"; + List collsList = item.getCollections(); + for (Collection coll : collsList) { + msg = msg + coll.getName() + " (ID: " + coll.getID() + ")\n"; + } + return msg; + } + + public String getMessage(Bitstream bitstream) { + // values of deleted bitstream + String msg = bitstream.getName() + ": " + + bitstream.getSizeBytes() + " bytes, checksum: " + + bitstream.getChecksum() + " (" + + bitstream.getChecksumAlgorithm() + ")\n"; + return msg; + } + + public String getMessage(List resPolicies) { + return resPolicies.stream() + .filter(rp -> rp.getAction() == Constants.READ) + .map(rp -> String.format("[%s, %s, %d, %s, %s, %s, %s]", + rp.getRpName(), rp.getRpType(), rp.getAction(), + rp.getEPerson() != null ? rp.getEPerson().getEmail() : null, + rp.getGroup() != null ? rp.getGroup().getName() : null, + rp.getStartDate() != null ? rp.getStartDate().toString() : null, + rp.getEndDate() != null ? rp.getEndDate().toString() : null)) + .collect(Collectors.joining(";")); + } + + public String getMessage(ResourcePolicy resourcePolicy) { + StringBuilder sb = new StringBuilder(); + sb.append("[Action: ") + .append(Constants.actionText[resourcePolicy.getAction()]); + if (resourcePolicy.getEPerson() != null) { + sb.append(", EPerson: ").append(resourcePolicy.getEPerson().getEmail()); + } + if (resourcePolicy.getGroup() != null) { + sb.append(", Group: ").append(resourcePolicy.getGroup().getName()); + } + if (resourcePolicy.getStartDate() != null) { + sb.append(", Start: ").append(resourcePolicy.getStartDate().toString()); + } + if (resourcePolicy.getEndDate() != null) { + sb.append(", End: ").append(resourcePolicy.getEndDate().toString()); + } + if (resourcePolicy.getRpDescription() != null) { + sb.append(", Description: ").append(resourcePolicy.getRpDescription()); + } + sb.append("]"); + return sb.toString(); + } + + public String getMetadata(String oldMtdKey, String oldMtdValue) { + return oldMtdKey + ": " + oldMtdValue; + } + + public String getMetadataReplacement(String metadataKey, String oldValue, String newValue) { + return String.format("%s [%s -> %s]", metadataKey, + Objects.toString(oldValue, "empty"), + Objects.toString(newValue, "empty")); + } + + public String getMetadataField(MetadataField metadataField) { + return metadataField.toString() + .replace('_', '.'); + } + + private String validateMessageTemplate(String messageTemplate, Object... args) { + if (messageTemplate == null) { + throw new IllegalArgumentException("The provenance message template is null!"); + } + return String.format(messageTemplate, args); + } +} diff --git a/dspace-api/src/main/java/org/dspace/core/ProvenanceMessageTemplates.java b/dspace-api/src/main/java/org/dspace/core/ProvenanceMessageTemplates.java new file mode 100644 index 000000000000..666155ab82d3 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/core/ProvenanceMessageTemplates.java @@ -0,0 +1,41 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.core; + +/** + * The ProvenanceMessageTemplates enum provides message templates for provenance messages. + * + * @author Michaela Paurikova (dspace at dataquest.sk) + */ +public enum ProvenanceMessageTemplates { + ACCESS_CONDITION("Access condition (%s) was added to %s (%s)"), + RESOURCE_POLICIES_REMOVED("Resource policies (%s) of %s (%s) were removed"), + BUNDLE_ADDED("Item was added bitstream to bundle (%s)"), + EDIT_LICENSE("License (%s) was %s"), + MOVE_ITEM("Item was moved from collection (%s) to different collection"), + MAPPED_ITEM("Item was mapped to collection (%s)"), + DELETED_ITEM_FROM_MAPPED("Item was deleted from mapped collection (%s)"), + EDIT_BITSTREAM("Item (%s) was deleted bitstream (%s)"), + ITEM_METADATA("Item metadata (%s) was %s"), + BITSTREAM_METADATA("Item metadata (%s) was %s bitstream (%s)"), + ITEM_REPLACE_SINGLE_METADATA("Item bitstream (%s) metadata (%s) was updated"), + DISCOVERABLE("Item was made %sdiscoverable"), + RESOURCE_POLICY_CREATED("Resource policy created: %s for %s (%s)"), + RESOURCE_POLICY_UPDATED("Resource policy updated: %s for %s (%s)"), + RESOURCE_POLICY_DELETED("Resource policy deleted: %s for %s (%s)"); + + private final String template; + + ProvenanceMessageTemplates(String template) { + this.template = template; + } + + public String getTemplate() { + return template; + } +} diff --git a/dspace-api/src/main/java/org/dspace/core/ProvenanceService.java b/dspace-api/src/main/java/org/dspace/core/ProvenanceService.java new file mode 100644 index 000000000000..8778b03872c5 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/core/ProvenanceService.java @@ -0,0 +1,215 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.core; + +import java.util.List; + +import org.dspace.app.bulkaccesscontrol.model.BulkAccessControlInput; +import org.dspace.authorize.ResourcePolicy; +import org.dspace.content.Bitstream; +import org.dspace.content.Bundle; +import org.dspace.content.Collection; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataValue; + +/** + * The ProvenanceService is responsible for creating provenance metadata for items based on the actions performed. + * + * @author Milan Majchrak (dspace at dataquest.sk) + */ +public interface ProvenanceService { + /** + * Add a provenance message to the item when a new access condition is added + * + * @param context DSpace context object + * @param item item to which the access condition is added + * @param accessControl the access control input + */ + void setItemPolicies(Context context, Item item, BulkAccessControlInput accessControl); + + /** + * Add a provenance message to the item when a read policy is removed + * + * @param context DSpace context object + * @param dso DSpace object from which the read policy is removed + * @param resPolicies list of resource policies that are removed + */ + void removeReadPolicies(Context context, DSpaceObject dso, List resPolicies); + + /** + * Add a provenance message to the item when a bitstream policy is set + * + * @param context DSpace context object + * @param bitstream bitstream to which the policy is set + * @param item item to which the bitstream belongs + * @param accessControl the access control input + */ + void setBitstreamPolicies(Context context, Bitstream bitstream, Item item, + BulkAccessControlInput accessControl); + + /** + * Add a provenance message to the item when a bitstream policy is set + * + * @param context DSpace context object + * @param bitstream to which the policy is set + * @param item to which the bitstream belongs + * @param accConditionsStr the access control input as string + */ + void setBitstreamPolicies(Context context, Bitstream bitstream, Item item, + String accConditionsStr); + + /** + * Add a provenance message to the item when an item's license is edited + * + * @param context DSpace context object + * @param item item to which the license is edited + * @param newLicense true if the license is new, false if it's edited + */ + void updateLicense(Context context, Item item, boolean newLicense); + + /** + * Add a provenance message to the item when it's moved to a collection + * + * @param context DSpace context object + * @param item item that is moved + * @param collection collection to which the item is moved + */ + void moveItem(Context context, Item item, Collection collection); + + /** + * Add a provenance message to the item when it's mapped to a collection + * + * @param context DSpace context object + * @param item item that is mapped + * @param collection collection to which the item is mapped + */ + void mappedItem(Context context, Item item, Collection collection); + + /** + * Add a provenance message to the item when it's deleted from a mapped collection + * + * @param context DSpace context object + * @param item item that is deleted from a mapped collection + * @param collection collection from which the item is deleted + */ + void deletedItemFromMapped(Context context, Item item, Collection collection); + + /** + * Add a provenance message to the item when it's bitstream is deleted + * + * @param context DSpace context object + * @param bitstream deleted bitstream + * @param item item from which the bitstream is deleted + */ + void deleteBitstream(Context context, Bitstream bitstream, Item item); + + /** + * Add a provenance message to the item when metadata is added + * + * @param context DSpace context object + * @param dso DSpace object to which the metadata is added + * @param metadataField metadata field that is added + * @param metadataValue metadata value that is added + */ + void addMetadata(Context context, DSpaceObject dso, MetadataField metadataField, String metadataValue); + + /** + * Add a provenance message to the item when metadata is removed + * + * @param context DSpace context object + * @param dso DSpace object from which the metadata is removed + */ + void removeMetadata(Context context, DSpaceObject dso, String schema, String element, String qualifier); + + /** + * Add a provenance message to the item when metadata is removed at a given index + * + * @param context DSpace context object + * @param dso DSpace object from which the metadata is removed + * @param metadataValues list of metadata values + * @param indexInt index at which the metadata is removed + */ + void removeMetadataAtIndex(Context context, DSpaceObject dso, List metadataValues, + int indexInt); + + /** + * Add a provenance message to the item when metadata is replaced, showing both old and new values + * + * @param context DSpace context object + * @param dso DSpace object to which the metadata is replaced + * @param metadataField metadata field that is replaced + * @param oldMtdVal old metadata value + * @param newMtdVal new metadata value + */ + void replaceMetadata(Context context, DSpaceObject dso, MetadataField metadataField, String oldMtdVal, + String newMtdVal); + + /** + * Add a provenance message to the item when metadata is replaced + * + * @param context DSpace context object + * @param dso DSpace object to which the metadata is replaced + * @param metadataField metadata field that is replaced + * @param oldMtdVal old metadata value + */ + void replaceMetadataSingle(Context context, DSpaceObject dso, MetadataField metadataField, + String oldMtdVal, String newMtdVal); + + /** + * Add a provenance message to the item when metadata is updated + * + * @param context DSpace context object + * @param item item to which the metadata is updated + * @param discoverable true if the item is discoverable, false if it's not + */ + void makeDiscoverable(Context context, Item item, boolean discoverable); + + /** + * Add a provenance message to the item when a bitstream is uploaded + * + * @param context DSpace context object + * @param bundle bundle to which the bitstream is uploaded + */ + void uploadBitstream(Context context, Bundle bundle); + + /** + * Add a provenance message to the item when a resource policy is created + * + * @param context DSpace context object + * @param resourcePolicy the resource policy that was created + */ + void createResourcePolicy(Context context, ResourcePolicy resourcePolicy); + + /** + * Add a provenance message to the item when a resource policy is updated + * + * @param context DSpace context object + * @param resourcePolicy the resource policy that was updated + */ + void updateResourcePolicy(Context context, ResourcePolicy resourcePolicy); + + /** + * Add a provenance message to the item when a resource policy is deleted + * + * @param context DSpace context object + * @param resourcePolicy the resource policy that was deleted + */ + void deleteResourcePolicy(Context context, ResourcePolicy resourcePolicy); + + /** + * Fetch an Item object using a service and return the first Item object from the list. + * Log an error if the list is empty or if there is an SQL error + * + * @param context DSpace context object + * @param bitstream bitstream to which the item is fetched + */ + Item findItemByBitstream(Context context, Bitstream bitstream); + +} diff --git a/dspace-api/src/main/java/org/dspace/core/ProvenanceServiceImpl.java b/dspace-api/src/main/java/org/dspace/core/ProvenanceServiceImpl.java new file mode 100644 index 000000000000..834bb40379f7 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/core/ProvenanceServiceImpl.java @@ -0,0 +1,465 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.core; + +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.app.bulkaccesscontrol.model.AccessCondition; +import org.dspace.app.bulkaccesscontrol.model.BulkAccessControlInput; +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.ResourcePolicy; +import org.dspace.content.Bitstream; +import org.dspace.content.Bundle; +import org.dspace.content.Collection; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataSchemaEnum; +import org.dspace.content.MetadataValue; +import org.dspace.content.clarin.ClarinLicenseResourceMapping; +import org.dspace.content.service.BitstreamService; +import org.dspace.content.service.ItemService; +import org.dspace.content.service.clarin.ClarinItemService; +import org.dspace.content.service.clarin.ClarinLicenseResourceMappingService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * ProvenanceServiceImpl is an implementation of ProvenanceService. + * + * @author Michaela Paurikova (dspace at dataquest.sk) + */ +public class ProvenanceServiceImpl implements ProvenanceService { + private static final Logger log = LogManager.getLogger(ProvenanceServiceImpl.class); + + @Autowired + private ItemService itemService; + @Autowired + private ClarinItemService clarinItemService; + @Autowired + private ClarinLicenseResourceMappingService clarinResourceMappingService; + @Autowired + private BitstreamService bitstreamService; + + private final ProvenanceMessageFormatter messageProvider = new ProvenanceMessageFormatter(); + + public void setItemPolicies(Context context, Item item, BulkAccessControlInput accessControl) { + String resPoliciesStr = extractAccessConditions(accessControl.getItem().getAccessConditions()); + if (StringUtils.isNotBlank(resPoliciesStr)) { + String msg = messageProvider.getMessage(context, ProvenanceMessageTemplates.ACCESS_CONDITION.getTemplate(), + resPoliciesStr, "item", item.getID()); + try { + addProvenanceMetadata(context, item, msg); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when setting item policies.", e); + } + } + } + + public void removeReadPolicies(Context context, DSpaceObject dso, List resPolicies) { + if (resPolicies.isEmpty()) { + return; + } + String resPoliciesStr = messageProvider.getMessage(resPolicies); + try { + if (dso.getType() == Constants.ITEM) { + Item item = (Item) dso; + String msg = messageProvider.getMessage(context, + ProvenanceMessageTemplates.RESOURCE_POLICIES_REMOVED.getTemplate(), + resPoliciesStr.isEmpty() ? "empty" : resPoliciesStr, "item", item.getID()); + addProvenanceMetadata(context, item, msg); + } else if (dso.getType() == Constants.BITSTREAM) { + Bitstream bitstream = (Bitstream) dso; + Item item = findItemByBitstream(context, bitstream); + if (Objects.nonNull(item)) { + String msg = messageProvider.getMessage(context, + ProvenanceMessageTemplates.RESOURCE_POLICIES_REMOVED.getTemplate(), + resPoliciesStr.isEmpty() ? "empty" : resPoliciesStr, "bitstream", bitstream.getID()); + addProvenanceMetadata(context, item, msg); + } + } + } catch (SQLException | AuthorizeException e) { + log.error("Unable to remove read policies from the DSpace object.", e); + } + } + + public void setBitstreamPolicies(Context context, Bitstream bitstream, Item item, + BulkAccessControlInput accessControl) { + String accConditionsStr = extractAccessConditions(accessControl.getBitstream().getAccessConditions()); + if (StringUtils.isNotBlank(accConditionsStr)) { + this.setBitstreamPolicies(context, bitstream, item, accConditionsStr); + } + } + + @Override + public void setBitstreamPolicies(Context context, Bitstream bitstream, Item item, String accConditionsStr) { + String msg = messageProvider.getMessage( + context, + ProvenanceMessageTemplates.ACCESS_CONDITION.getTemplate(), + accConditionsStr, + "bitstream", + bitstream.getID() + ); + + try { + addProvenanceMetadata(context, item, msg); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when setting bitstream policies.", e); + } + } + + public void updateLicense(Context context, Item item, boolean newLicense) { + String oldLicense = null; + + try { + oldLicense = findLicenseInBundles(item, Constants.LICENSE_BUNDLE_NAME, oldLicense, context); + if (oldLicense == null) { + oldLicense = findLicenseInBundles(item, Constants.CONTENT_BUNDLE_NAME, oldLicense, context); + } + + String msg = messageProvider.getMessage(context, ProvenanceMessageTemplates.EDIT_LICENSE.getTemplate(), + item, Objects.isNull(oldLicense) ? "empty" : oldLicense, + !newLicense ? "removed" : Objects.isNull(oldLicense) ? "added" : "updated"); + addProvenanceMetadata(context, item, msg); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when editing Item's license.", e); + } + + } + + public void moveItem(Context context, Item item, Collection collection) { + try { + String msg = messageProvider.getMessage(context, ProvenanceMessageTemplates.MOVE_ITEM.getTemplate(), + item, collection.getID()); + // Update item in DB + // Because a user can move an item without authorization turn off authorization + context.turnOffAuthorisationSystem(); + addProvenanceMetadata(context, item, msg); + context.restoreAuthSystemState(); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when moving an item to a different collection.", + e); + } + } + + public void mappedItem(Context context, Item item, Collection collection) { + String msg = messageProvider.getMessage(context, ProvenanceMessageTemplates.MAPPED_ITEM.getTemplate(), + collection.getID()); + try { + addProvenanceMetadata(context, item, msg); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when mapping an item into a collection.", e); + } + } + + public void deletedItemFromMapped(Context context, Item item, Collection collection) { + String msg = messageProvider.getMessage(context, + ProvenanceMessageTemplates.DELETED_ITEM_FROM_MAPPED.getTemplate(), collection.getID()); + try { + addProvenanceMetadata(context, item, msg); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when deleting an item from a mapped collection.", + e); + } + } + + public void deleteBitstream(Context context, Bitstream bitstream, Item item) { + try { + if (Objects.nonNull(item)) { + String msg = messageProvider.getMessage(context, + ProvenanceMessageTemplates.EDIT_BITSTREAM.getTemplate(), item, item.getID(), + messageProvider.getMessage(bitstream)); + addProvenanceMetadata(context, item, msg); + } + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when deleting a bitstream.", e); + } + } + + public void addMetadata(Context context, DSpaceObject dso, MetadataField metadataField, String metadataValue) { + try { + if (Constants.ITEM == dso.getType()) { + String msg = messageProvider.getMessage(context, ProvenanceMessageTemplates.ITEM_METADATA.getTemplate(), + messageProvider.getMetadata( + messageProvider.getMetadataField(metadataField), metadataValue), "added"); + addProvenanceMetadata(context, (Item) dso, msg); + } + + if (dso.getType() == Constants.BITSTREAM) { + Bitstream bitstream = (Bitstream) dso; + Item item = findItemByBitstream(context, bitstream); + if (Objects.nonNull(item)) { + String msg = messageProvider.getMessage(context, + ProvenanceMessageTemplates.BITSTREAM_METADATA.getTemplate(), item, + messageProvider.getMetadata( + messageProvider.getMetadataField(metadataField), metadataValue), "added by", + messageProvider.getMessage(bitstream)); + addProvenanceMetadata(context, item, msg); + } + } + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when adding metadata to a DSpace object.", e); + } + } + + public void removeMetadata(Context context, DSpaceObject dso, String schema, String element, String qualifier) { + if (dso.getType() != Constants.BITSTREAM) { + return; + } + MetadataField oldMtdKey = null; + String oldMtdValue = null; + List mtd = bitstreamService.getMetadata((Bitstream) dso, schema, element, qualifier, Item.ANY); + if (CollectionUtils.isEmpty(mtd)) { + // Do not add any provenance message when there are no metadata to remove + return; + } + oldMtdKey = mtd.get(0).getMetadataField(); + oldMtdValue = mtd.get(0).getValue(); + Bitstream bitstream = (Bitstream) dso; + try { + Item item = findItemByBitstream(context, bitstream); + if (Objects.nonNull(item)) { + String msg = messageProvider.getMessage(context, + ProvenanceMessageTemplates.BITSTREAM_METADATA.getTemplate(), + messageProvider.getMetadata(messageProvider.getMetadataField(oldMtdKey), oldMtdValue), + "deleted from", messageProvider.getMessage(bitstream)); + addProvenanceMetadata(context, item, msg); + } + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when removing metadata from a dso.", e); + } + + } + + public void removeMetadataAtIndex(Context context, DSpaceObject dso, List metadataValues, + int indexInt) { + if (dso.getType() != Constants.ITEM) { + return; + } + // Remember removed mtd + String oldMtdKey = messageProvider.getMetadataField(metadataValues.get(indexInt).getMetadataField()); + String oldMtdValue = metadataValues.get(indexInt).getValue(); + try { + String msg = messageProvider.getMessage(context, ProvenanceMessageTemplates.ITEM_METADATA.getTemplate(), + messageProvider.getMetadata(oldMtdKey, oldMtdValue), "deleted"); + addProvenanceMetadata(context, (Item) dso, msg); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when removing metadata at a specific index " + + "from a dso", e); + } + } + + public void replaceMetadata(Context context, DSpaceObject dso, MetadataField metadataField, String oldMtdVal, + String newMtdVal) { + if (dso.getType() != Constants.ITEM) { + return; + } + + try { + String msg = messageProvider.getMessage(context, ProvenanceMessageTemplates.ITEM_METADATA.getTemplate(), + messageProvider.getMetadataReplacement( + messageProvider.getMetadataField(metadataField), oldMtdVal, newMtdVal), "updated"); + addProvenanceMetadata(context, (Item) dso, msg); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when replacing metadata in a dso.", e); + } + + } + + public void replaceMetadataSingle(Context context, DSpaceObject dso, MetadataField metadataField, + String oldMtdVal, String newMtdVal) { + if (dso.getType() != Constants.BITSTREAM) { + return; + } + + Bitstream bitstream = (Bitstream) dso; + try { + Item item = findItemByBitstream(context, bitstream); + if (Objects.nonNull(item)) { + String msg = messageProvider.getMessage(context, + ProvenanceMessageTemplates.ITEM_REPLACE_SINGLE_METADATA.getTemplate(), + messageProvider.getMessage(bitstream), + messageProvider.getMetadata(messageProvider.getMetadataField(metadataField), oldMtdVal)); + addProvenanceMetadata(context, item, msg); + } + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when replacing metadata in a item.", e); + } + } + + public void makeDiscoverable(Context context, Item item, boolean discoverable) { + try { + String msg = messageProvider.getMessage(context, ProvenanceMessageTemplates.DISCOVERABLE.getTemplate(), + item, discoverable ? "" : "non-") + messageProvider.getMessage(item); + addProvenanceMetadata(context, item, msg); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when making an item discoverable.", e); + } + } + + public void uploadBitstream(Context context, Bundle bundle) { + Item item = bundle.getItems().get(0); + try { + String msg = messageProvider.getMessage(context, ProvenanceMessageTemplates.BUNDLE_ADDED.getTemplate(), + item, bundle.getID()); + addProvenanceMetadata(context,item, msg); + itemService.update(context, item); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when updating an item's bitstream.", e); + } + } + + private void addProvenanceMetadata(Context context, Item item, String msg) + throws SQLException, AuthorizeException { + itemService.addMetadata(context, item, MetadataSchemaEnum.DC.getName(), + "description", "provenance", "en", msg); + itemService.update(context, item); + } + + private String extractAccessConditions(List accessConditions) { + return accessConditions.stream() + .map(ac -> ac.getName() + + (ac.getStartDate() != null ? " [from: " + ac.getStartDate().toString() + "]" : "") + + (ac.getEndDate() != null ? " [till: " + ac.getEndDate().toString() + "]" : "")) + .collect(Collectors.joining(";")); + } + + public Item findItemByBitstream(Context context, Bitstream bitstream) { + List items = null; + try { + items = clarinItemService.findByBitstreamUUID(context, bitstream.getID()); + } catch (SQLException e) { + log.error("Unable to find item by bitstream (" + bitstream.getID() + " ).", e); + return null; + } + if (items.isEmpty()) { + log.warn("Bitstream (" + bitstream.getID() + ") is not assigned to any item."); + return null; + } + return items.get(0); + } + + @Override + public void createResourcePolicy(Context context, ResourcePolicy resourcePolicy) { + if (Objects.isNull(resourcePolicy.getdSpaceObject())) { + return; + } + + DSpaceObject dso = resourcePolicy.getdSpaceObject(); + String resourcePolicyStr = messageProvider.getMessage(resourcePolicy); + String dsoType = getDSpaceObjectType(dso.getType()); + + try { + if (dso.getType() != Constants.ITEM) { + log.warn("Provenance message for resource policy creation is supported only for items." + + " DSpace object type: " + dsoType); + return; + } + Item item = (Item) dso; + String msg = messageProvider.getMessage(context, + ProvenanceMessageTemplates.RESOURCE_POLICY_CREATED.getTemplate(), + resourcePolicyStr, dsoType, item.getID()); + addProvenanceMetadata(context, item, msg); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when creating resource policy.", e); + } + } + + @Override + public void updateResourcePolicy(Context context, ResourcePolicy resourcePolicy) { + if (Objects.isNull(resourcePolicy.getdSpaceObject())) { + return; + } + + DSpaceObject dso = resourcePolicy.getdSpaceObject(); + String resourcePolicyStr = messageProvider.getMessage(resourcePolicy); + String dsoType = getDSpaceObjectType(dso.getType()); + + try { + if (dso.getType() != Constants.ITEM) { + log.warn("Provenance message for resource policy update is supported only for items. " + + "Current DSpace object type: " + dsoType); + return; + } + Item item = (Item) dso; + String msg = messageProvider.getMessage(context, + ProvenanceMessageTemplates.RESOURCE_POLICY_UPDATED.getTemplate(), + resourcePolicyStr, dsoType, item.getID()); + addProvenanceMetadata(context, item, msg); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when updating resource policy.", e); + } + } + + @Override + public void deleteResourcePolicy(Context context, ResourcePolicy resourcePolicy) { + if (Objects.isNull(resourcePolicy.getdSpaceObject())) { + return; + } + + DSpaceObject dso = resourcePolicy.getdSpaceObject(); + String resourcePolicyStr = messageProvider.getMessage(resourcePolicy); + String dsoType = getDSpaceObjectType(dso.getType()); + + try { + if (dso.getType() != Constants.ITEM) { + log.warn("Provenance message for resource policy deletion is supported only for items. " + + "The current DSpace object type is: " + dsoType); + return; + } + Item item = (Item) dso; + String msg = messageProvider.getMessage(context, + ProvenanceMessageTemplates.RESOURCE_POLICY_DELETED.getTemplate(), + resourcePolicyStr, dsoType, item.getID()); + addProvenanceMetadata(context, item, msg); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when deleting resource policy.", e); + } + } + + private String getDSpaceObjectType(int type) { + switch (type) { + case Constants.ITEM: + return "item"; + case Constants.BITSTREAM: + return "bitstream"; + case Constants.COLLECTION: + return "collection"; + case Constants.COMMUNITY: + return "community"; + case Constants.BUNDLE: + return "bundle"; + default: + return "object"; + } + } + + private String findLicenseInBundles(Item item, String bundleName, String currentLicense, Context context) + throws SQLException { + List bundles = item.getBundles(bundleName); + for (Bundle clarinBundle : bundles) { + List bitstreamList = clarinBundle.getBitstreams(); + for (Bitstream bundleBitstream : bitstreamList) { + if (Objects.isNull(currentLicense)) { + List mappings = + this.clarinResourceMappingService.findByBitstreamUUID(context, bundleBitstream.getID()); + if (CollectionUtils.isNotEmpty(mappings)) { + return mappings.get(0).getLicense().getName(); + } + } + } + } + return currentLicense; + } +} diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/ItemHandleChecker.java b/dspace-api/src/main/java/org/dspace/ctask/general/ItemHandleChecker.java new file mode 100644 index 000000000000..067979c12af0 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/ctask/general/ItemHandleChecker.java @@ -0,0 +1,189 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.ctask.general; + +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Response; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.curate.Curator; +import org.glassfish.jersey.client.ClientProperties; + +/** + * A handle checker that builds upon the BasicLinkChecker to check the Handle URLs. + * + * @author Milan Kuchtiak + */ +public class ItemHandleChecker extends BasicLinkChecker { + + private static final int CONNECTION_TIMEOUT_SEC = 2; + private static final int READ_TIMEOUT_SEC = 3; + + private List ignoredUrls; + + private Map checkedResults; + private Client client; + private String handlePrefix; + + @Override + public void init(Curator curator, String taskId) throws IOException { + super.init(curator, taskId); + client = ClientBuilder.newBuilder() + .connectTimeout(CONNECTION_TIMEOUT_SEC, TimeUnit.SECONDS) + .readTimeout(READ_TIMEOUT_SEC, TimeUnit.SECONDS) + // we want all the locations on the way + .property(ClientProperties.FOLLOW_REDIRECTS, false) + .build(); + String[] ignores = configurationService.getArrayProperty("curate.checklist.ignore"); + ignoredUrls = (ignores == null) ? List.of() : List.of(ignores); + + handlePrefix = configurationService.getProperty("handle.canonical.prefix", "http://hdl.handle.net/"); + + checkedResults = new HashMap<>(); + } + + @Override + protected List getURLs(Item item) { + List ids = itemService.getMetadata(item, "dc", "identifier", "uri", Item.ANY); + List theURLs = new ArrayList<>(); + for (MetadataValue id : ids) { + String url = id.getValue(); + if (url != null && url.startsWith(handlePrefix) && !isIgnoredURL(url)) { + theURLs.add(url); + } + } + return theURLs; + } + + @Override + protected boolean checkURL(String url, StringBuilder results) { + HandleResponse handleResponse = getHandleResponse(url, results); + + return (handleResponse.getFamily() == Response.Status.Family.SUCCESSFUL); + } + + /** + * Checks if given URL should be ignored. + * + * @param url URL to be checked + * @return true if url should be ignored + */ + protected boolean isIgnoredURL(String url) { + return ignoredUrls.stream().anyMatch(url::contains); + } + + private HandleResponse getHandleResponse(String url, StringBuilder results) { + WebTarget target = client.target(url); + + HandleResponse checkedResult = checkedResults.get(url); + if (checkedResult != null) { + return checkedResult; + } + + try (Response response = target.request().head()) { + HandleResponse handleResponse = HandleResponse.fromResponse(response); + appendResults(url, handleResponse, results); + checkedResults.putIfAbsent(url, handleResponse); + if (response.getStatusInfo().getFamily() == Response.Status.Family.REDIRECTION) { + String location; + URI locationUri = response.getLocation(); + if (!locationUri.isAbsolute()) { + // Resolve relative URI against the original URL + location = target.getUri().resolve(locationUri).toString(); + } else { + location = locationUri.toString(); + } + return getHandleResponse(location, results); + } else { + return handleResponse; + } + } catch (Exception ex) { + HandleResponse err; + if (ex.getCause() instanceof SocketTimeoutException) { + err = new HandleResponse(617, Response.Status.Family.OTHER, ex.getMessage()); + } else { + err = new HandleResponse(500, Response.Status.Family.SERVER_ERROR, ex.getMessage()); + } + appendResults(url, err, results); + checkedResults.putIfAbsent(url, err); + return err; + } + } + + private static void appendResults(String url, HandleResponse handleResponse, StringBuilder results) { + switch (handleResponse.getFamily()) { + case SUCCESSFUL: + results.append(" - ").append(url).append(" = ").append(handleResponse.getStatus()) + .append(" - OK\n"); + break; + case REDIRECTION: + results.append(" - ").append(url).append(" = ").append(handleResponse.getStatus()) + .append(" - REDIRECTED\n"); + break; + default: { + results.append(" - ").append(url).append(" = ").append(handleResponse.getStatus()) + .append(" - FAILED\n"); + if (handleResponse.getErrorMessage() != null) { + results.append(" - Error: ").append(handleResponse.getErrorMessage()).append("\n"); + } + } + } + } + + private static final class HandleResponse { + private final int status; + private final Response.Status.Family family; + private final String errorMessage; + + private HandleResponse(int status, Response.Status.Family family) { + this(status, family, null); + } + + private HandleResponse(int status, Response.Status.Family family, String errorMessage) { + this.status = status; + this.family = family; + this.errorMessage = errorMessage; + } + + public int getStatus() { + return status; + } + + public Response.Status.Family getFamily() { + return family; + } + + public String getErrorMessage() { + return errorMessage; + } + + public static HandleResponse fromResponse(Response response) { + return new HandleResponse(response.getStatus(), response.getStatusInfo().getFamily()); + } + + @Override + public String toString() { + return "HandleResponse{" + + "status=" + status + + ", family=" + family + + ", errorMessage='" + errorMessage + "'" + + '}'; + } + } +} diff --git a/dspace-api/src/main/java/org/dspace/discovery/ClarinSolrItemsCommunityIndexPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/ClarinSolrItemsCommunityIndexPlugin.java new file mode 100644 index 000000000000..36e60e3af816 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/discovery/ClarinSolrItemsCommunityIndexPlugin.java @@ -0,0 +1,49 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.discovery; + +import java.util.Objects; + +import org.apache.logging.log4j.Logger; +import org.apache.solr.common.SolrInputDocument; +import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.content.service.clarin.ClarinItemService; +import org.dspace.core.Context; +import org.dspace.discovery.indexobject.IndexableItem; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Plugin for indexing the Items community. It helps search the Item by the community. + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class ClarinSolrItemsCommunityIndexPlugin implements SolrServiceIndexPlugin { + + private static final Logger log = org.apache.logging.log4j.LogManager + .getLogger(ClarinSolrItemsCommunityIndexPlugin.class); + + @Autowired(required = true) + protected ClarinItemService clarinItemService; + + @Override + public void additionalIndex(Context context, IndexableObject indexableObject, SolrInputDocument document) { + if (indexableObject instanceof IndexableItem) { + Item item = ((IndexableItem) indexableObject).getIndexedObject(); + + Community owningCommunity = clarinItemService.getOwningCommunity(context, item); + String communityName = Objects.isNull(owningCommunity) ? " " : owningCommunity.getName(); + + // _keyword and _filter because + // they are needed in order to work as a facet and filter. + document.addField("items_owning_community", communityName); + document.addField("items_owning_community_keyword", communityName); + document.addField("items_owning_community_filter", communityName); + } + } +} diff --git a/dspace-api/src/main/java/org/dspace/discovery/IsoLangCodes.java b/dspace-api/src/main/java/org/dspace/discovery/IsoLangCodes.java new file mode 100644 index 000000000000..6a6d49ded913 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/discovery/IsoLangCodes.java @@ -0,0 +1,117 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +/* Created for LINDAT/CLARIAH-CZ (UFAL) */ +package org.dspace.discovery; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Class is copied from the LINDAT/CLARIAH-CZ (https://github.com/ufal/clarin-dspace/blob + * /si-master-origin/dspace-api/src/main/java/cz/cuni/mff/ufal/IsoLangCodes.java) and modified by + * + * @author Marian Berger (marian.berger at dataquest.sk) + */ +public class IsoLangCodes { + + public static final String LANG_CODES_FILE = "lang_codes.txt"; + + /** + * Language codes in LANG_CODES_FILE are expected in format Language:code. + * Therefore separator is ":". + */ + public static final String LANG_CODE_SEPARATOR = ":"; + /** + * Language codes in LANG_CODES_FILE are expected in format Language:code. + * Therefore there must be 2 parts after separating by ":". + */ + private static final int EXPECTED_PARTS_OF_ISO_LANG_CODE = 2; + + /** + * Class that provides language codes from file LANG_CODES_FILE + */ + private IsoLangCodes() { + } + + /** log4j logger */ + private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j + .LogManager.getLogger(IsoLangCodes.class); + + private static Map isoLanguagesMap = null; + + static { + getLangMap(); + } + + /** + * @return map with language codes and languages. If called for the first time, builds the map. + */ + private static Map getLangMap() { + if (isoLanguagesMap == null) { + synchronized (IsoLangCodes.class) { + isoLanguagesMap = buildMap(); + } + } + return isoLanguagesMap; + } + + /** + * Builds language code map from file LANG_CODES_FILE + * + * + * @return map with language codes and languages + */ + private static Map buildMap() { + Map map = new HashMap(); + final InputStream langCodesInputStream = Thread.currentThread() + .getContextClassLoader().getResourceAsStream(LANG_CODES_FILE); + if (!Objects.nonNull(langCodesInputStream)) { + return map; + } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(langCodesInputStream, + StandardCharsets.UTF_8))) { + String line; + boolean loading = false; + while ((line = reader.readLine()) != null) { + if (!loading) { + if (line.equals("==start==")) { + loading = true; + } + } else { + String[] splitted = line.split(LANG_CODE_SEPARATOR); + if (!(splitted.length == EXPECTED_PARTS_OF_ISO_LANG_CODE)) { + log.warn("Bad string: " + line + " in " + LANG_CODES_FILE); + map.put("", ""); + } else { + map.put(splitted[1], splitted[0]); + } + } + } + } catch (IOException e) { + log.error(e); + } + + return map; + } + + /** + * @param langCode language code + * @return Language for given code + */ + public static String getLangForCode(String langCode) { + return getLangMap().get(langCode); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/external/CachingOrcidRestConnector.java b/dspace-api/src/main/java/org/dspace/external/CachingOrcidRestConnector.java new file mode 100644 index 000000000000..a4d14ae1a859 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/external/CachingOrcidRestConnector.java @@ -0,0 +1,224 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.external; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Optional; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.external.provider.orcid.xml.ExpandedSearchConverter; +import org.dspace.services.ConfigurationService; +import org.dspace.utils.DSpace; +import org.json.JSONObject; +import org.springframework.cache.annotation.Cacheable; + +/** + * A different implementation of the communication with the ORCID API. + * The API returns no-cache headers, we use @Cacheable to cache the labels (id->name) for some time. + * Originally the idea was to reuse the OrcidRestConnector, but in the end that just wraps apache http client. + */ +public class CachingOrcidRestConnector { + private static final Logger log = LogManager.getLogger(CachingOrcidRestConnector.class); + + private String apiURL; + // Access tokens are long-lived ~ 20years, don't bother with refreshing + private volatile String _accessToken; + private final ExpandedSearchConverter converter = new ExpandedSearchConverter(); + + private static final Pattern p = Pattern.compile("^\\p{Alpha}+", Pattern.UNICODE_CHARACTER_CLASS); + private static final String edismaxParams = "&defType=edismax&qf=" + + URLEncoder.encode( "family-name^4.0 credit-name^3.0 other-names^2.0 text", StandardCharsets.UTF_8); + + private final HttpClient httpClient = HttpClient + .newBuilder() + .connectTimeout( Duration.ofSeconds(5)) + .build(); + + /* + * We basically need to obtain the access token only once, but there is no guarantee this will succeed. The + * failure shouldn't be fatal, so we'll try again next time. + */ + private Optional init() { + if (_accessToken == null) { + synchronized (CachingOrcidRestConnector.class) { + if (_accessToken == null) { + log.info("Initializing Orcid connector"); + ConfigurationService configurationService = new DSpace().getConfigurationService(); + String clientSecret = configurationService.getProperty("orcid.application-client-secret"); + String clientId = configurationService.getProperty("orcid.application-client-id"); + String OAUTHUrl = configurationService.getProperty("orcid.token-url"); + + try { + _accessToken = getAccessToken(clientSecret, clientId, OAUTHUrl); + } catch (Exception e) { + log.error("Error during initialization of the Orcid connector", e); + } + } + } + } + return Optional.ofNullable(_accessToken); + } + + /** + * Set the URL of the ORCID API + * @param apiURL + */ + public void setApiURL(String apiURL) { + this.apiURL = apiURL; + } + + /** + * Search the ORCID API + * + * The query is passed to the ORCID API as is, except when it contains just 'unicode letters'. + * In that case, we try to be smart and turn it into edismax query with wildcard. + * + * @param query - the search query + * @param start - initial offset when paging results + * @param limit - maximum number of results to return + * @return the results + */ + public ExpandedSearchConverter.Results search(String query, int start, int limit) { + String extra; + // if query contains just 'unicode letters'; try to be smart and turn it into edismax query with wildcard + if (p.matcher(query).matches()) { + query += " || " + query + "*"; + extra = edismaxParams; + } else { + extra = ""; + } + final String searchPath = String.format("expanded-search?q=%s&start=%s&rows=%s%s", URLEncoder.encode(query, + StandardCharsets.UTF_8), start, limit, extra); + + return init().map(token -> { + try (InputStream inputStream = httpGet(searchPath, token)) { + return converter.convert(inputStream); + } catch (IOException e) { + log.error("Error during search", e); + return ExpandedSearchConverter.ERROR; + } + }).orElse(ExpandedSearchConverter.ERROR); + } + + /** + * Get the label for an ORCID, ideally the name of the person. + * + * Null is: + * - either an error -> won't be cached, + * - or it means no result, which'd be odd provided we get here with a valid orcid -> not caching should be ok + * + * @param orcid the id you are looking for + * @return the label or null in case nothing found/error + */ + @Cacheable(cacheNames = "orcid-labels", unless = "#result == null") + public String getLabel(String orcid) { + log.debug("getLabel: " + orcid); + // in theory, we could use orcid.org/v3.0//personal-details, but didn't want to write another converter + ExpandedSearchConverter.Results search = search("orcid:" + orcid, 0, 1); + if (search.isOk() && search.numFound() > 0) { + return search.results().get(0).label(); + } + return null; + } + + protected String getAccessToken(String clientSecret, String clientId, String OAUTHUrl) { + if (StringUtils.isNotBlank(clientSecret) + && StringUtils.isNotBlank(clientId) + && StringUtils.isNotBlank(OAUTHUrl)) { + String authenticationParameters = + String.format("client_id=%s&client_secret=%s&scope=/read-public&grant_type=client_credentials", + clientId, clientSecret); + + HttpRequest request = HttpRequest.newBuilder() + .uri(java.net.URI.create(OAUTHUrl)) + .POST(HttpRequest.BodyPublishers.ofString(authenticationParameters)) + .timeout(Duration.ofSeconds(5)) + .header("Accept", "application/json") + .header("Content-Type", "application/x-www-form-urlencoded") + .build(); + + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (isSuccess(response)) { + JSONObject responseObject = new JSONObject(response.body()); + return responseObject.getString("access_token"); + } else { + log.error("Error during initialization of the Orcid connector, status code: " + + response.statusCode()); + throw new RuntimeException("Error during initialization of the Orcid connector, status code: " + + response.statusCode()); + } + } catch (IOException | InterruptedException e) { + log.error("Error during initialization of the Orcid connector", e); + throw new RuntimeException(e); + } + } else { + log.error("Missing configuration for Orcid connector"); + throw new RuntimeException("Missing configuration for Orcid connector"); + } + } + + // Package/sub-class visible so tests can stub the HTTP layer (see CachingOrcidRestConnectorTest) + // and avoid hitting the live ORCID sandbox. + protected InputStream httpGet(String path, String accessToken) throws IOException { + String trimmedPath = path.replaceFirst("^/+", "").replaceFirst("/+$", ""); + + String fullPath = apiURL + '/' + trimmedPath; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(fullPath)) + .timeout(Duration.ofSeconds(5)) + .header("Content-Type", "application/vnd.orcid+xml") + .header("Authorization", "Bearer " + accessToken) + .build(); + + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); + if (isSuccess(response)) { + return response.body(); + } else { + log.error("Error in rest connector for path: " + fullPath + ", status code: " + response.statusCode()); + throw new UnexpectedStatusException("Error in rest connector for path: " + + fullPath + ", status code: " + response.statusCode()); + } + } catch (UnexpectedStatusException e) { + throw e; + } catch (IOException | InterruptedException e) { + log.error("Error in rest connector for path: " + fullPath, e); + throw new RuntimeException(e); + } + } + + private boolean isSuccess(HttpResponse response) { + return response.statusCode() >= 200 && response.statusCode() < 300; + } + + private static class UnexpectedStatusException extends IOException { + public UnexpectedStatusException(String message) { + super(message); + } + } + + //Just for testing + protected void forceAccessToken(String accessToken) { + synchronized (CachingOrcidRestConnector.class) { + this._accessToken = accessToken; + } + } +} diff --git a/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/CacheLogger.java b/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/CacheLogger.java new file mode 100644 index 000000000000..061bd4a6d425 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/CacheLogger.java @@ -0,0 +1,25 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.external.provider.orcid.xml; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.ehcache.event.CacheEvent; +import org.ehcache.event.CacheEventListener; + +/** + * A simple logger for cache events + */ +public class CacheLogger implements CacheEventListener { + private static final Logger log = LogManager.getLogger(CacheLogger.class); + @Override + public void onEvent(CacheEvent event) { + log.debug("ORCID Cache Event Type: {} | Key: {} ", + event.getType(), event.getKey()); + } +} diff --git a/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/ExpandedSearchConverter.java b/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/ExpandedSearchConverter.java new file mode 100644 index 000000000000..eebb63e027b0 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/ExpandedSearchConverter.java @@ -0,0 +1,199 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.external.provider.orcid.xml; + +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +import java.io.InputStream; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.Logger; +import org.orcid.jaxb.model.v3.release.search.expanded.ExpandedResult; +import org.orcid.jaxb.model.v3.release.search.expanded.ExpandedSearch; +import org.xml.sax.SAXException; + +/** + * Convert the XML response from the ORCID API to a list of Results + * The conversion here is sort of a layer between the Choice class and the ORCID classes + */ +public class ExpandedSearchConverter extends Converter { + + public static final ExpandedSearchConverter.Results ERROR = + new ExpandedSearchConverter.Results(new ArrayList<>(), 0L, false); + + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(ExpandedSearchConverter.class); + + @Override + public ExpandedSearchConverter.Results convert(InputStream inputStream) { + try { + ExpandedSearch search = (ExpandedSearch) unmarshall(inputStream, ExpandedSearch.class); + long numFound = search.getNumFound(); + return new Results(search.getResults().stream() + .filter(Objects::nonNull) + .filter(result -> isNotBlank(result.getOrcidId())) + .map(ExpandedSearchConverter.Result::new) + .collect(Collectors.toList()), numFound); + } catch (SAXException | URISyntaxException e) { + log.error(e); + } + return ERROR; + } + + + /** + * Keeps the results and their total number + */ + public static final class Results { + private final List results; + private final Long numFound; + + private final boolean ok; + + Results(List results, Long numFound) { + this(results, numFound, true); + } + + Results(List results, Long numFound, boolean ok) { + this.results = results; + this.numFound = numFound; + this.ok = ok; + } + + + /** + * The results + * @return the results as List + */ + public List results() { + return results; + } + + /** + * The total number of results + * @return the number of results + */ + public Long numFound() { + return numFound; + } + + /** + * Whether there were any issues + * @return false if there were issues + */ + public boolean isOk() { + return ok; + } + + @Override + public String toString() { + return "Results[" + + "results=" + results + ", " + + "numFound=" + numFound + ']'; + } + + } + + /** + * Represents a single result + * Taking care of potential null/empty values + */ + public static final class Result { + private final String authority; + private final String value; + private final String label; + private final String creditName; + private final String[] otherNames; + private final String[] institutionNames; + + Result(ExpandedResult result) { + if (isBlank(result.getOrcidId())) { + throw new IllegalArgumentException("OrcidId is required"); + } + final String last = isNotBlank(result.getFamilyNames()) ? result.getFamilyNames() : ""; + final String first = isNotBlank(result.getGivenNames()) ? result.getGivenNames() : ""; + final String maybeComma = isNotBlank(last) && isNotBlank(first) ? ", " : ""; + String displayName = String.format("%s%s%s", last, maybeComma, first); + displayName = isNotBlank(displayName) ? displayName : result.getOrcidId(); + + this.authority = result.getOrcidId(); + this.value = displayName; + this.label = displayName; + + this.creditName = result.getCreditName(); + this.otherNames = result.getOtherNames(); + this.institutionNames = result.getInstitutionNames(); + } + + /** + * The authority value + * @return orcid + */ + public String authority() { + return authority; + } + + /** + * The value to store + * @return the value + */ + public String value() { + return value; + } + + /** + * The label to display + * @return the label + */ + public String label() { + return label; + } + + /** + * Optional extra info - credit name + * @return the credit name + */ + public Optional creditName() { + return Optional.ofNullable(creditName); + } + + /** + * Optional extra info - other names + * @return other names + */ + public Optional otherNames() { + return Optional.ofNullable(otherNames).map(names -> String.join(" | ", names)); + } + + /** + * Optional extra info - institution names + * @return institution names + */ + public Optional institutionNames() { + //joining with newline doesn't seem to matter for ui + return Optional.ofNullable(institutionNames) .map(names -> String.join(" | ", names)); + } + + @Override + public String toString() { + return "Result[" + + "authority=" + authority + ", " + + "value=" + value + ", " + + "label=" + label + ", " + + "creditNames=" + creditName + ", " + + "otherNames=" + Arrays.toString(otherNames) + ", " + + "institutionNames=" + Arrays.toString(institutionNames) + ']'; + } + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/handle/AbstractPIDService.java b/dspace-api/src/main/java/org/dspace/handle/AbstractPIDService.java new file mode 100644 index 000000000000..a1c3c75185c1 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/handle/AbstractPIDService.java @@ -0,0 +1,91 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.handle; + +import java.net.Authenticator; +import java.net.PasswordAuthentication; +import java.util.Map; + +import org.dspace.services.ConfigurationService; +import org.dspace.utils.DSpace; +import org.springframework.stereotype.Component; + +/* Created for LINDAT/CLARIAH-CZ (UFAL) */ +/** + * Abstract class for PID service which manages EPIC handles. + * This class loads the parameters from configuration for calling EPIC API. + * + * @author Michaela Paurikova (michaela.paurikova at dataquest.sk) + */ +@Component +public abstract class AbstractPIDService { + public String PIDServiceURL; + public String PIDServiceUSER; + public String PIDServicePASS; + + private ConfigurationService configurationService = new DSpace().getConfigurationService(); + + class PIDServiceAuthenticator extends Authenticator { + public PasswordAuthentication getPasswordAuthentication() { + return (new PasswordAuthentication(PIDServiceUSER, + PIDServicePASS.toCharArray())); + } + } + + public enum HTTPMethod { + GET, POST, PUT, DELETE + } + + public enum PARAMS { + PID, DATA, COMMAND, REGEX, HEADER + } + + public enum HANDLE_FIELDS { + URL, + TITLE, + REPOSITORY, + SUBMITDATE, + REPORTEMAIL, + DATASETNAME, + DATASETVERSION, + QUERY + } + + public PIDServiceAuthenticator authenticator = null; + + public AbstractPIDService() throws Exception { + PIDServiceURL = configurationService.getProperty("lr.pid.service.url", "lr.pid.service.url"); + PIDServiceUSER = configurationService.getProperty("lr.pid.service.user", "lr.pid.service.user"); + PIDServicePASS = configurationService.getProperty("lr.pid.service.pass", "lr.pid.service.pass"); + if (PIDServiceURL == null || PIDServiceURL.length() == 0) { + throw new Exception("PIDService URL not configured."); + } + authenticator = new PIDServiceAuthenticator(); + Authenticator.setDefault(authenticator); + } + + public abstract String sendPIDCommand(HTTPMethod method, Map params) throws Exception; + + public abstract String resolvePID(String PID) throws Exception; + + public abstract String createPID(Map handleFields, String prefix) throws Exception; + + public abstract String createCustomPID(Map handleFields, + String prefix, String suffix) throws Exception; + + public abstract String modifyPID(String PID, Map handleFields) throws Exception; + + public abstract String deletePID(String PID) throws Exception; + + public abstract String findHandle(Map handleFields, String prefix) throws Exception; + + public abstract boolean supportsCustomPIDs() throws Exception; + + public abstract String whoAmI(String encoding) throws Exception; + +} diff --git a/dspace-api/src/main/java/org/dspace/handle/EpicHandleRestHelper.java b/dspace-api/src/main/java/org/dspace/handle/EpicHandleRestHelper.java new file mode 100644 index 000000000000..1c40127ffda9 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/handle/EpicHandleRestHelper.java @@ -0,0 +1,145 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +package org.dspace.handle; + +import java.net.URI; +import java.net.URISyntaxException; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * Helper class to perform REST calls to ePIC Handle PID Service. + * + * @author Milan Kuchtiak + */ +public class EpicHandleRestHelper { + + private static final Client client = ClientBuilder.newClient(); + + private EpicHandleRestHelper() { + } + + public static Response createHandle(String pidServiceURL, + String prefix, + String subPrefix, + String subSuffix, + String jsonData) { + URI uri; + try { + uri = new URI(pidServiceURL); + } catch (URISyntaxException e) { + return invalidUriResponse(); + } + + WebTarget webTarget = client.target(uri).path(prefix); + if (subPrefix != null) { + webTarget = webTarget.queryParam("prefix", subPrefix); + } + if (subSuffix != null) { + webTarget = webTarget.queryParam("suffix", subSuffix); + } + + return webTarget + .request(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .post(Entity.json(jsonData)); + } + + public static Response updateHandle(String pidServiceURL, String prefix, String suffix, String jsonData) { + URI uri; + try { + uri = new URI(pidServiceURL); + } catch (URISyntaxException e) { + return invalidUriResponse(); + } + + return client.target(uri).path(prefix).path(suffix) + .request(MediaType.APPLICATION_JSON) + .put(Entity.json(jsonData)); + } + + public static Response deleteHandle(String pidServiceURL, String prefix, String suffix) { + URI uri; + try { + uri = new URI(pidServiceURL); + } catch (URISyntaxException e) { + return invalidUriResponse(); + } + + return client.target(uri).path(prefix).path(suffix) + .request() + .delete(); + } + + public static Response searchHandles(String pidServiceURL, + String prefix, + String urlParameter, + Integer page, + Integer limit) { + URI uri; + try { + uri = new URI(pidServiceURL); + } catch (URISyntaxException e) { + return invalidUriResponse(); + } + + WebTarget webTarget = client.target(uri).path(prefix); + webTarget = webTarget.queryParam("URL", urlParameter); + if (page != null) { + webTarget = webTarget.queryParam("page", page); + } + if (limit != null) { + webTarget = webTarget.queryParam("limit", limit); + } + + Invocation.Builder request = webTarget.request().header("Depth", "1"); + + return request.accept(MediaType.APPLICATION_JSON).get(); + } + + public static Response countHandles(String pidServiceURL, String prefix, String urlParameter) { + URI uri; + try { + uri = new URI(pidServiceURL); + } catch (URISyntaxException e) { + return invalidUriResponse(); + } + + WebTarget webTarget = client.target(uri).path(prefix) + .queryParam("URL", urlParameter) + .queryParam("limit", "0"); + + return webTarget.request().accept(MediaType.APPLICATION_JSON).get(); + } + + public static Response getHandle(String pidServiceURL, String prefix, String suffix) { + URI uri; + try { + uri = new URI(pidServiceURL); + } catch (URISyntaxException e) { + return invalidUriResponse(); + } + + return client.target(uri).path(prefix).path(suffix) + .request() + .accept(MediaType.APPLICATION_JSON) + .get(); + } + + private static Response invalidUriResponse() { + return Response.status(400, "invalid ePIC PID Service URL").build(); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java b/dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java new file mode 100644 index 000000000000..352fc52f76a3 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/handle/EpicHandleServiceImpl.java @@ -0,0 +1,235 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +package org.dspace.handle; + +import java.io.IOException; +import java.net.Authenticator; +import java.net.PasswordAuthentication; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; +import org.dspace.handle.service.EpicHandleService; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Service implementation for ePIC handles. + * This class is autowired by spring and should never be accessed directly. + * + * @author Milan Kuchtiak + */ +public class EpicHandleServiceImpl implements EpicHandleService { + private String pidServiceUrl; + private String pidServiceUser; + private String pidServicePassword; + private final ObjectMapper objectMapper; + + @Autowired + protected ConfigurationService configurationService; + + public EpicHandleServiceImpl() { + objectMapper = new ObjectMapper(); + } + + private void initialize() throws IOException { + pidServiceUrl = configurationService.getProperty("lr.pid.service.url"); + if (pidServiceUrl == null || pidServiceUrl.isBlank()) { + throw new IOException("Missing lr.pid.service.url property in DSpace configuration"); + } + pidServiceUser = configurationService.getProperty("lr.pid.service.user"); + pidServicePassword = configurationService.getProperty("lr.pid.service.pass"); + Authenticator authenticator = new EpicHandleServiceAuthenticator(); + Authenticator.setDefault(authenticator); + } + + @Override + public String resolveURLForHandle(String prefix, String suffix) throws IOException { + initialize(); + try (Response response = EpicHandleRestHelper.getHandle(pidServiceUrl, prefix, suffix)) { + if (response.getStatus() == Response.Status.NOT_FOUND.getStatusCode()) { + return null; + } + if (response.getStatus() != Response.Status.OK.getStatusCode()) { + throw new WebApplicationException(response); + } + String jsonResponse = response.readEntity(String.class); + + List epicPidDataList = objectMapper.readValue(jsonResponse, new TypeReference<>() { + }); + + return epicPidDataList.stream() + .filter(epicPidData -> "URL".equals(epicPidData.getType())) + .map(epicPidData -> epicPidData.getParsedData().toString()) + .findFirst().orElse(null); + } + } + + @Override + public List searchHandles(String prefix, String urlQuery, Integer page, Integer limit) throws IOException { + initialize(); + String urlParameter = (urlQuery == null ? "*" : String.format("*%s*", urlQuery)); + + try (Response response = EpicHandleRestHelper.searchHandles(pidServiceUrl, prefix, urlParameter, page, limit)) { + if (response.getStatus() != Response.Status.OK.getStatusCode()) { + throw new WebApplicationException(response); + } + + String jsonResponse = response.readEntity(String.class); + Map> epicPidDataMap = objectMapper.readValue(jsonResponse, new TypeReference<>() { + }); + + List handleList = new ArrayList<>(); + epicPidDataMap.forEach((key, value) -> { + String url = getUrlFromEpicDataList(value); + if (url != null) { + String handleKey = key.startsWith("/handles/") ? key.substring(9) : key; + handleList.add(new Handle(handleKey, url)); + } + }); + return handleList; + } + } + + @Override + public String createHandle(String prefix, String subPrefix, String subSuffix, String url) throws IOException { + initialize(); + String jsonData = getJsonDataForUrl(objectMapper, url).toString(); + try ( Response response = EpicHandleRestHelper.createHandle( + pidServiceUrl, + prefix, + subPrefix, + subSuffix, + jsonData)) { + if (response.getStatus() != Response.Status.CREATED.getStatusCode()) { + throw new WebApplicationException(response); + } + return objectMapper.readValue(response.readEntity(String.class), EpicPid.class).getHandle(); + } + } + + @Override + public String createOrUpdateHandle(String prefix, String suffix, String url) throws IOException { + initialize(); + String jsonData = getJsonDataForUrl(objectMapper, url).toString(); + + try (Response response = EpicHandleRestHelper.updateHandle(pidServiceUrl, prefix, suffix, jsonData)) { + if (response.getStatus() == Response.Status.NO_CONTENT.getStatusCode()) { + return null; + } else if (response.getStatus() == Response.Status.CREATED.getStatusCode()) { + return objectMapper.readValue(response.readEntity(String.class), EpicPid.class).getHandle(); + } else { + throw new WebApplicationException(response); + } + } + } + + @Override + public void deleteHandle(String prefix, String suffix) throws IOException { + initialize(); + try (Response response = EpicHandleRestHelper.deleteHandle(pidServiceUrl, prefix, suffix)) { + if (response.getStatus() != Response.Status.NO_CONTENT.getStatusCode()) { + throw new WebApplicationException(response); + } + } + } + + @Override + public int countHandles(String prefix, String urlQuery) throws IOException { + initialize(); + String urlParameter = urlQuery == null ? "*" : String.format("*%s*", urlQuery); + + try (Response response = EpicHandleRestHelper.countHandles(pidServiceUrl, prefix, urlParameter)) { + if (response.getStatus() != Response.Status.OK.getStatusCode()) { + throw new WebApplicationException(response); + } + + String jsonResponse = response.readEntity(String.class); + List epicPids = objectMapper.readValue(jsonResponse, new TypeReference<>() {}); + return epicPids.size(); + } + } + + private static ArrayNode getJsonDataForUrl(ObjectMapper objectMapper, String url) { + ObjectNode epicPidDataNode = objectMapper.createObjectNode() + .put("type", "URL") + .put("parsed_data", url); + return objectMapper.createArrayNode().add(epicPidDataNode); + } + + private static String getUrlFromEpicDataList(List epicPidDataList) { + return epicPidDataList.stream() + .filter(epicPidData -> "URL".equals(epicPidData.getType())) + .map(epicPidData -> epicPidData.getParsedData().toString()) + .findFirst().orElse(null); + } + + class EpicHandleServiceAuthenticator extends Authenticator { + + @Override + public PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(pidServiceUser, pidServicePassword.toCharArray()); + } + } + + static class EpicPid { + private final String handle; + + @JsonCreator + public EpicPid(@JsonProperty("epic-pid") String handle) { + this.handle = handle; + } + public String getHandle() { + return handle; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static class EpicPidData { + private final int index; + private final String type; + private final Object parsedData; + + @JsonCreator + public EpicPidData(@JsonProperty("idx") int index, + @JsonProperty("type") String type, + @JsonProperty("parsed_data") Object parsedData) { + this.index = index; + this.type = type; + this.parsedData = parsedData; + } + + public int getIndex() { + return index; + } + + public String getType() { + return type; + } + + public Object getParsedData() { + return parsedData; + } + + @Override + public String toString() { + return "EpicPidData[" + index + ", " + type + ", " + parsedData + "]"; + } + } + +} diff --git a/dspace-api/src/main/java/org/dspace/handle/Handle.java b/dspace-api/src/main/java/org/dspace/handle/Handle.java index 29182ad56c89..f50708d99eb3 100644 --- a/dspace-api/src/main/java/org/dspace/handle/Handle.java +++ b/dspace-api/src/main/java/org/dspace/handle/Handle.java @@ -7,6 +7,9 @@ */ package org.dspace.handle; +import java.util.Date; +import java.util.Objects; + import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -51,6 +54,15 @@ public class Handle implements ReloadableEntity { @Column(name = "resource_type_id") private Integer resourceTypeId; + @Column(name = "url") + private String url; + + @Column(name = "dead") + private Boolean dead; + + @Column(name = "dead_since") + private Date deadSince; + /** * Protected constructor, create object using: * {@link org.dspace.handle.service.HandleService#createHandle(Context, DSpaceObject)} @@ -125,4 +137,31 @@ public int hashCode() { .append(resourceTypeId) .toHashCode(); } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public Boolean getDead() { + if (Objects.isNull(dead)) { + return false; + } + return dead; + } + + public void setDead(Boolean dead) { + this.dead = dead; + } + + public Date getDeadSince() { + return deadSince; + } + + public void setDeadSince(Date deadSince) { + this.deadSince = deadSince; + } } diff --git a/dspace-api/src/main/java/org/dspace/handle/HandleClarinServiceImpl.java b/dspace-api/src/main/java/org/dspace/handle/HandleClarinServiceImpl.java new file mode 100644 index 000000000000..3ae43e547053 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/handle/HandleClarinServiceImpl.java @@ -0,0 +1,553 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.handle; + +import static org.dspace.handle.external.ExternalHandleConstants.MAGIC_BEAN; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateFormatUtils; +import org.apache.logging.log4j.Logger; +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.service.CollectionService; +import org.dspace.content.service.CommunityService; +import org.dspace.content.service.ItemService; +import org.dspace.content.service.SiteService; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.core.LogHelper; +import org.dspace.handle.dao.HandleClarinDAO; +import org.dspace.handle.dao.HandleDAO; +import org.dspace.handle.external.HandleRest; +import org.dspace.handle.service.HandleClarinService; +import org.dspace.handle.service.HandleService; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Additional service implementation for the Handle object in Clarin-DSpace. + * + * @author Michaela Paurikova (michaela.paurikova at dataquest.sk) + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class HandleClarinServiceImpl implements HandleClarinService { + + /** + * log4j logger + */ + private static Logger log = org.apache.logging.log4j.LogManager.getLogger(HandleClarinServiceImpl.class); + + @Autowired(required = true) + protected HandleDAO handleDAO; + + @Autowired(required = true) + protected HandleClarinDAO handleClarinDAO; + + protected SiteService siteService; + + @Autowired(required = true) + protected HandleService handleService; + + @Autowired(required = true) + protected ItemService itemService; + + @Autowired(required = true) + protected CollectionService collectionService; + + @Autowired(required = true) + protected CommunityService communityService; + + @Autowired(required = true) + protected ConfigurationService configurationService; + + @Autowired(required = true) + protected AuthorizeService authorizeService; + + static final String PREFIX_DELIMITER = "/"; + static final String PART_IDENTIFIER_DELIMITER = "@"; + + /** + * Protected Constructor + */ + protected HandleClarinServiceImpl() { + } + + @Override + public List findAll(Context context, String sortingColumn) throws SQLException { + return handleClarinDAO.findAll(context, sortingColumn); + } + + @Override + public List findAll(Context context) throws SQLException { + return handleDAO.findAll(context, Handle.class); + } + + @Override + public Handle findByID(Context context, int id) throws SQLException { + return handleDAO.findByID(context, Handle.class, id); + } + + @Override + public Handle findByHandle(Context context, String handle) throws SQLException { + return handleDAO.findByHandle(context, handle); + } + + @Override + public Handle createExternalHandle(Context context, String handleStr, String url, Boolean dead, Date deadSince) + throws SQLException, AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "Only administrators may modify the external handle."); + } + Handle handle = this.createExternalHandle(context, handleStr, url); + handle.setDead(dead); + handle.setDeadSince(deadSince); + this.save(context, handle); + return handle; + } + + @Override + public Handle createExternalHandle(Context context, String handleStr, String url) + throws SQLException, AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "Only administrators may modify the external handle."); + } + + String handleId; + // Do we want to generate the new handleId or use entered handleStr? + if (!(StringUtils.isBlank(handleStr))) { + // We use handleStr entered by use + handleId = handleStr; + } else { + // We generate new handleId + handleId = createId(context); + } + + Handle handle = handleDAO.create(context, new Handle()); + + // Set handleId + handle.setHandle(handleId); + + // When you add null to String, it converts null to "null" + if (!(StringUtils.isBlank(url)) && !Objects.equals(url,"null")) { + handle.setUrl(url); + } else { + throw new RuntimeException("Cannot change url of handle object " + + "- the url has wrong value: 'null' or is blank"); + } + + this.save(context, handle); + + log.debug("Created new external Handle with handle " + handleId); + + return handle; + } + + @Override + public void delete(Context context, Handle handle) throws SQLException, AuthorizeException { + // Check authorisation: Only admins may create DC types + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "Only administrators may modify the handle registry"); + } + // Delete handle + handleDAO.delete(context, handle); + log.info(LogHelper.getHeader(context, "delete_handle", + "handle_id=" + handle.getID())); + } + + @Override + public void save(Context context, Handle handle) throws SQLException, AuthorizeException { + // Check authorisation: Only admins may create DC types + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "Only administrators may modify the handle registry"); + } + // Save handle + handleDAO.save(context, handle); + log.info(LogHelper.getHeader(context, "save_handle", + "handle_id=" + handle.getID() + + "handle=" + handle.getHandle() + + "resourceTypeID=" + handle.getResourceTypeId())); + } + + @Override + public void update(Context context, Handle handleObject, String newHandle, + String newUrl) + throws SQLException, AuthorizeException { + // Check authorisation: Only admins may create DC types + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "Only administrators may modify the handle registry"); + } + + // Set handle only if it is not empty + if (!(StringUtils.isBlank(newHandle))) { + handleObject.setHandle(newHandle); + } else { + throw new RuntimeException("Cannot change handle of handle object " + + "- the handle is empty"); + } + + // Set url only if it is external handle + if (!isInternalResource(handleObject)) { + // When you add null to String, it converts null to "null" + if (!(StringUtils.isBlank(newUrl)) && !Objects.equals(newUrl,"null")) { + handleObject.setUrl(newUrl); + } else { + throw new RuntimeException("Cannot change url of handle object " + + "- the url has wrong value: 'null' or is blank"); + } + } + + this.save(context, handleObject); + + log.info(LogHelper.getHeader(context, "update_handle", + "handle_id=" + handleObject.getID())); + } + + @Override + public void setPrefix(Context context, String newPrefix, String oldPrefix) throws SQLException, + AuthorizeException { + // Check authorisation: Only admins may create DC types + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "Only administrators may modify the handle registry"); + } + // Control, if are new and old prefix entered + if (StringUtils.isBlank(newPrefix) || StringUtils.isBlank(oldPrefix)) { + throw new NullPointerException("Cannot set prefix. Required fields are empty."); + } + // Get handle prefix + String prefix = handleService.getPrefix(); + // Set prefix only if not equal to old prefix + if (Objects.equals(prefix, oldPrefix)) { + // Return value says if set prefix was successful + if (!(configurationService.setProperty("handle.prefix", newPrefix))) { + // Prefix has not changed + throw new RuntimeException("error while trying to set handle prefix"); + } + } else { + throw new RuntimeException("Cannot set prefix. Entered prefix does not match with "); + } + + log.info(LogHelper.getHeader(context, "set_handle_prefix", + "old_prefix=" + oldPrefix + " new_prefix=" + newPrefix)); + } + + /* Created for LINDAT/CLARIAH-CZ (UFAL) */ + @Override + public boolean isInternalResource(Handle handle) { + // In internal handle is not entered url + return (Objects.isNull(handle.getUrl()) || handle.getUrl().isEmpty()); + } + + @Override + public String resolveToURL(Context context, String handleStr) throws SQLException { + // Handle is not entered + if (Objects.isNull(handleStr)) { + throw new IllegalArgumentException("Handle is null"); + } + + // + String partIdentifier = extractPartIdentifier(handleStr); + handleStr = stripPartIdentifier(handleStr); + + // Find handle + Handle handle = handleDAO.findByHandle(context, handleStr); + //Handle was not find + if (Objects.isNull(handle)) { + return null; + } + + String url; + if (isInternalResource(handle)) { + // Internal handle + // Create url for internal handle + String currentUiUrl = configurationService.getProperty("dspace.ui.url"); + url = currentUiUrl.endsWith("/") ? currentUiUrl : currentUiUrl + "/"; + url += "handle/" + handleStr; + } else { + // External handle + url = handle.getUrl(); + } + url = appendPartIdentifierToUrl(url, partIdentifier); + + log.debug("Resolved {} to {}", handle, url); + + return url; + } + + @Override + public DSpaceObject resolveToObject(Context context, String handle) throws IllegalStateException, SQLException { + Handle foundHandle = findByHandle(context, handle); + + if (Objects.isNull(foundHandle)) { + // If this is the Site-wide Handle, return Site object + if (Objects.equals(handle, configurationService.getProperty("handle.prefix") + "/0")) { + return siteService.findSite(context); + } + // Otherwise, return null (i.e. handle not found in DB) + return null; + } + + // Check if handle was allocated previously, but is currently not + // Associated with a DSpaceObject + // (this may occur when 'unbindHandle()' is called for an obj that was removed) + if (Objects.isNull(foundHandle.getResourceTypeId()) || Objects.isNull(foundHandle.getDSpaceObject())) { + // If handle has been unbound, just return null (as this will result in a PageNotFound) + return null; + } + + int handleTypeId = foundHandle.getResourceTypeId(); + UUID resourceID = foundHandle.getDSpaceObject().getID(); + + if (handleTypeId == Constants.ITEM) { + Item item = itemService.find(context, resourceID); + if (log.isDebugEnabled()) { + log.debug("Resolved handle " + handle + " to item " + + (Objects.isNull(item) ? (-1) : item.getID())); + } + + return item; + } else if (handleTypeId == Constants.COLLECTION) { + Collection collection = collectionService.find(context, resourceID); + if (log.isDebugEnabled()) { + log.debug("Resolved handle " + handle + " to collection " + + (Objects.isNull(collection) ? (-1) : collection.getID())); + } + + return collection; + } else if (handleTypeId == Constants.COMMUNITY) { + Community community = communityService.find(context, resourceID); + if (log.isDebugEnabled()) { + log.debug("Resolved handle " + handle + " to community " + + (Objects.isNull(community) ? (-1) : community.getID())); + } + + return community; + } + + throw new IllegalStateException("Unsupported Handle Type " + + Constants.typeText[handleTypeId]); + } + + @Override + public int count(Context context) throws SQLException { + return handleDAO.countRows(context); + } + + /** + * Create id for handle object. + * + * @param context DSpace context object + * @return handle id + * @throws SQLException if database error + */ + private String createId(Context context) throws SQLException { + // Get configured prefix + String handlePrefix = handleService.getPrefix(); + // Get next available suffix (as a Long, since DSpace uses an incrementing sequence) + Long handleSuffix = handleDAO.getNextHandleSuffix(context); + + return handlePrefix + (handlePrefix.endsWith("/") ? "" : "/") + handleSuffix.toString(); + } + + @Override + public List convertHandleWithMagicToExternalHandle(List magicHandles) { + List externalHandles = new ArrayList<>(); + for (org.dspace.handle.Handle handleWithMagic: magicHandles) { + externalHandles.add(new org.dspace.handle.external.Handle(handleWithMagic.getHandle(), + handleWithMagic.getUrl())); + } + + return externalHandles; + } + + @Override + public List convertExternalHandleToHandleRest(List externalHandles) { + List externalHandleRestList = new ArrayList<>(); + for (org.dspace.handle.external.Handle externalHandle: externalHandles) { + HandleRest externalHandleRest = new HandleRest(); + + externalHandleRest.setHandle(externalHandle.getHandle()); + externalHandleRest.setUrl(externalHandle.url); + externalHandleRest.setTitle(externalHandle.title); + externalHandleRest.setSubprefix(externalHandle.subprefix); + externalHandleRest.setReportemail(externalHandle.reportemail); + externalHandleRest.setRepository(externalHandle.repository); + externalHandleRest.setSubmitdate(externalHandle.submitdate); + + externalHandleRestList.add(externalHandleRest); + } + + return externalHandleRestList; + } + + /** + * Returns complete handle made from prefix and suffix + */ + @Override + public String completeHandle(String prefix, String suffix) { + return prefix + PREFIX_DELIMITER + suffix; + } + + /** + * Split handle by prefix delimiter + */ + @Override + public String[] splitHandle(String handle) { + if (Objects.nonNull(handle)) { + return handle.split(PREFIX_DELIMITER); + } + return new String[] { null, null }; + } + + @Override + public List findAllExternalHandles(Context context) throws SQLException { + // fetch all handles which contains `@magicLindat` string from the DB + return handleDAO.findAll(context, Handle.class) + .stream() + .filter(handle -> Objects.nonNull(handle)) + .filter(handle -> Objects.nonNull(handle.getUrl())) + .filter(handle -> handle.getUrl().contains(MAGIC_BEAN)) + .collect(Collectors.toList()); + } + + @Override + public boolean isDead(Context context, String handle) throws SQLException { + String baseHandle = stripPartIdentifier(handle); + Handle foundHandle = handleDAO.findByHandle(context, baseHandle); + return foundHandle.getDead(); + + } + @Override + public String getDeadSince(Context context, String handle) throws SQLException { + String baseHandle = stripPartIdentifier(handle); + Handle foundHandle = handleDAO.findByHandle(context, baseHandle); + Date timestamptz = foundHandle.getDeadSince(); + + return Objects.nonNull(timestamptz) ? DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT. + format(timestamptz) : null; + } + + @Override + public Handle createHandle(Context context, String handleStr) throws SQLException, AuthorizeException { + // Check authorisation: Only admins may create DC types + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "Only administrators may modify the handle registry"); + } + + String handleId; + // Do we want to generate the new handleId or use entered handleStr? + if (StringUtils.isNotBlank(handleStr)) { + // We use handleStr entered by use + handleId = handleStr; + } else { + // We generate new handleId + handleId = createId(context); + } + + Handle handle = handleDAO.create(context, new Handle()); + // Set handleId + handle.setHandle(handleId); + this.save(context, handle); + log.debug("Created new Handle with handle " + handleId); + return handle; + } + + @Override + public Handle findByHandleAndMagicToken(Context context, String handle, String token) throws SQLException { + Handle h = findByHandle(context, handle); + if (Objects.isNull(h) || Objects.isNull(h.getUrl()) || !h.getUrl().contains(MAGIC_BEAN)) { + return null; + } + org.dspace.handle.external.Handle magicHandle = + new org.dspace.handle.external.Handle(h.getHandle(), h.getUrl()); + if (magicHandle.token.equals(token)) { + return h; + } else { + return null; + } + } + + /** + * Strips the part identifier from the handle + * + * @param handle The handle with optional part identifier + * @return The handle without the part identifier + */ + private String stripPartIdentifier(String handle) { + if (Objects.isNull(handle)) { + return null; + } + + String baseHandle; + int pos = handle.indexOf(PART_IDENTIFIER_DELIMITER); + if (pos >= 0) { + baseHandle = handle.substring(0, pos); + } else { + baseHandle = handle; + } + return baseHandle; + } + + /** + * Extracts the part identifier from the handle + * + * @param handle The handle with optional part identifier + * @return part identifier or null + */ + private String extractPartIdentifier(String handle) { + // + if (Objects.isNull(handle)) { + return null; + } + String partIdentifier = null; + int pos = handle.indexOf(PART_IDENTIFIER_DELIMITER); + if (pos >= 0) { + partIdentifier = handle.substring(pos + 1); + } + return partIdentifier; + } + + /** + * Appends the partIdentifier as parameters to the given URL + * + * @param url The URL + * @param partIdentifier Part identifier (can be null or empty) + * @return Final URL with part identifier appended as parameters to the given URL + */ + private static String appendPartIdentifierToUrl(String url, String partIdentifier) { + // + String finalUrl = url; + if (Objects.isNull(finalUrl) || StringUtils.isBlank(partIdentifier)) { + return finalUrl; + } + if (finalUrl.contains("?")) { + finalUrl += '&' + partIdentifier; + } else { + finalUrl += '?' + partIdentifier; + } + return finalUrl; + } +} diff --git a/dspace-api/src/main/java/org/dspace/handle/HandlePlugin.java b/dspace-api/src/main/java/org/dspace/handle/HandlePlugin.java index 3e219b2c3413..43a25f31eb6f 100644 --- a/dspace-api/src/main/java/org/dspace/handle/HandlePlugin.java +++ b/dspace-api/src/main/java/org/dspace/handle/HandlePlugin.java @@ -7,12 +7,15 @@ */ package org.dspace.handle; +import static org.dspace.handle.external.ExternalHandleConstants.DEFAULT_CANONICAL_HANDLE_PREFIX; + import java.sql.SQLException; import java.util.Collections; import java.util.Enumeration; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Objects; import net.cnri.util.StreamTable; import net.handle.hdllib.Encoder; @@ -58,6 +61,13 @@ public class HandlePlugin implements HandleStorage { **/ private static transient DSpaceKernelImpl kernelImpl; + /** + * CLARIN: cached repository name, canonical handle prefix and configuration service, + * used by external handle resolution ({@link org.dspace.handle.external.Handle}). + */ + private static String repositoryName; + private static String canonicalHandlePrefix; + /** * References to DSpace Services **/ @@ -404,4 +414,47 @@ public Enumeration getHandlesForNA(byte[] theNAHandle) } } } + + /** + * CLARIN: the configured repository name ({@code dspace.name}), cached after first lookup. + * Used by external handle (magic URL) resolution. + * + * @return the trimmed repository name, or {@code null} if unavailable + */ + public static String getRepositoryName() { + if (Objects.nonNull(repositoryName)) { + return repositoryName; + } + ConfigurationService cfg = DSpaceServicesFactory.getInstance().getConfigurationService(); + if (Objects.isNull(cfg)) { + return null; + } + String name = cfg.getProperty("dspace.name"); + if (Objects.isNull(name)) { + repositoryName = null; + return repositoryName; + } + repositoryName = name.trim(); + return repositoryName; + } + + /** + * CLARIN: the canonical handle prefix ({@code handle.canonical.prefix}, default + * {@link org.dspace.handle.external.ExternalHandleConstants#DEFAULT_CANONICAL_HANDLE_PREFIX}), + * cached after first lookup. Used by external handle resolution. + * + * @return the canonical handle prefix + */ + public static String getCanonicalHandlePrefix() { + if (Objects.nonNull(canonicalHandlePrefix)) { + return canonicalHandlePrefix; + } + ConfigurationService cfg = DSpaceServicesFactory.getInstance().getConfigurationService(); + if (Objects.isNull(cfg)) { + canonicalHandlePrefix = DEFAULT_CANONICAL_HANDLE_PREFIX; + } else { + canonicalHandlePrefix = cfg.getProperty("handle.canonical.prefix", DEFAULT_CANONICAL_HANDLE_PREFIX); + } + return canonicalHandlePrefix; + } } diff --git a/dspace-api/src/main/java/org/dspace/handle/PIDService.java b/dspace-api/src/main/java/org/dspace/handle/PIDService.java new file mode 100644 index 000000000000..615afbcf12e8 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/handle/PIDService.java @@ -0,0 +1,133 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.handle; + +/* Created for LINDAT/CLARIAH-CZ (UFAL) */ +/** + * Service for PID. + * + * @author Michaela Paurikova (michaela.paurikova at dataquest.sk) + */ +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Random; + +import org.dspace.services.ConfigurationService; +import org.dspace.utils.DSpace; + +public class PIDService { + public static final String SERVICE_TYPE_EPIC = "epic"; + + public static final String SERVICE_TYPE_EPIC2 = "epic2"; + + private static AbstractPIDService pidService = null; + + private static ConfigurationService configurationService = new DSpace().getConfigurationService(); + + private PIDService() { + + } + + private static void initialize() throws Exception { + if (Objects.nonNull(pidService)) { + return; + } + String serviceType = getServiceType(); + String pidServiceClass = null; + if (serviceType.equals(PIDService.SERVICE_TYPE_EPIC2)) { + pidServiceClass = "org.dspace.handle.PIDServiceEPICv2"; + } else { + throw new IllegalArgumentException("Illegal pid.service type"); + } + try { + pidService = (AbstractPIDService)Class.forName(pidServiceClass).getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new Exception(e); + } + } + + public static String getServiceType() { + return configurationService.getProperty("lr.pid.service.type", "lr.pid.service.type"); + } + + /** + * + * @param PID + * @return URL assigned to the PID + * @throws Exception + */ + public static String resolvePID(String PID) throws Exception { + initialize(); + return pidService.resolvePID(PID); + } + + public static String modifyPID(String PID, String URL, Map additionalFields) throws Exception { + initialize(); + Map handleFields = new LinkedHashMap(); + handleFields.put(AbstractPIDService.HANDLE_FIELDS.URL.toString(), URL); + if (null != additionalFields) { + handleFields.putAll(additionalFields); + } + return pidService.modifyPID(PID, handleFields); + } + + public static String createPID(String URL, String prefix) throws Exception { + initialize(); + Map handleFields = new HashMap(); + handleFields.put(AbstractPIDService.HANDLE_FIELDS.URL.toString(), URL); + return pidService.createPID(handleFields, prefix); + } + + public static String createCustomPID(String URL, String prefix, String suffix) throws Exception { + initialize(); + Map handleFields = new HashMap(); + handleFields.put(AbstractPIDService.HANDLE_FIELDS.URL.toString(), URL); + return pidService.createCustomPID(handleFields, prefix, suffix); + } + + public static String findHandle(String URL, String prefix) throws Exception { + initialize(); + Map handleFields = new HashMap(); + handleFields.put(AbstractPIDService.HANDLE_FIELDS.URL.toString(), URL); + return pidService.findHandle(handleFields, prefix); + } + + public static boolean supportsCustomPIDs() throws Exception { + initialize(); + return pidService.supportsCustomPIDs(); + } + + public static String who_am_i(String encoding) throws Exception { + initialize(); + return pidService.whoAmI(encoding); + } + + public static String deletePID(String PID) throws Exception { + initialize(); + return pidService.deletePID(PID); + } + + public static String test_pid(String PID) throws Exception { + who_am_i(null); + // 1. search for pid + // 2. modify it + resolvePID(PID); + Random randomGenerator = new Random(); + int randomInt = randomGenerator.nextInt(10000); + String url = String.format("http://only.testing.mff.cuni.cz/%d", randomInt); + modifyPID(PID, url, null); + String resolved = resolvePID(PID); + if ( resolved.equals(url) ) { + return "testing succesful"; + } else { + return "testing seemed ok but resolving did not return the expected result"; + } + } +} diff --git a/dspace-api/src/main/java/org/dspace/handle/dao/HandleClarinDAO.java b/dspace-api/src/main/java/org/dspace/handle/dao/HandleClarinDAO.java new file mode 100644 index 000000000000..07146c75fb5d --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/handle/dao/HandleClarinDAO.java @@ -0,0 +1,33 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.handle.dao; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.core.Context; +import org.dspace.handle.Handle; + +/** + * Database Access Object interface class for the Handle object. + * The implementation of this class is responsible for the specific database calls for the Handle object + * and is autowired by spring + * This class should only be accessed from a single service and should never be exposed outside of the API + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public interface HandleClarinDAO { + + /** + * Find all Handles following the sorting options + * @param context DSpace context object + * @param sortingColumn sorting option in the specific format e.g. `handle:123456789/111` + * @return List of Handles + */ + List findAll(Context context, String sortingColumn) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/handle/dao/impl/HandleClarinDAOImpl.java b/dspace-api/src/main/java/org/dspace/handle/dao/impl/HandleClarinDAOImpl.java new file mode 100644 index 000000000000..163bf655bb81 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/handle/dao/impl/HandleClarinDAOImpl.java @@ -0,0 +1,102 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.handle.dao.impl; + +import java.sql.SQLException; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.Logger; +import org.dspace.core.AbstractHibernateDAO; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.handle.Handle; +import org.dspace.handle.Handle_; +import org.dspace.handle.dao.HandleClarinDAO; + +/** + * Hibernate implementation of the Database Access Object interface class for the Handle object. + * This class is responsible for specific database calls for the Handle object and is autowired by spring + * This class should never be accessed directly. + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class HandleClarinDAOImpl extends AbstractHibernateDAO implements HandleClarinDAO { + + /** + * The constant for the sorting option `url:external`. + */ + private static final String EXTERNAL = "external"; + + /** + * log4j category + */ + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(HandleClarinDAOImpl.class); + + @Override + public List findAll(Context context, String sortingColumnDef) + throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Handle.class); + Root handleRoot = criteriaQuery.from(Handle.class); + criteriaQuery.select(handleRoot); + + // If the sortingColumnDef is null return all Handles + if (Objects.isNull(sortingColumnDef)) { + return executeCriteriaQuery(context, criteriaQuery, false, -1, -1); + } + + // load sortingColumn + // the sortingColumnDefAsList should have 2 elements + int sortingColumnIndex = 0; + int sortingValueIndex = 1; + String[] sortingColumnDefAsList = sortingColumnDef.split(":"); + if (ArrayUtils.isEmpty(sortingColumnDefAsList) || sortingColumnDefAsList.length < 2) { + return executeCriteriaQuery(context, criteriaQuery, false, -1, -1); + } + + String sortingValue = sortingColumnDefAsList[sortingValueIndex]; + String sortingColumnName = sortingColumnDefAsList[sortingColumnIndex]; + // set up the `where` clause to the criteria query + switch (sortingColumnName) { + case Handle_.RESOURCE_TYPE_ID: + // set the Item resource type as default + Integer sortingValueInt = Constants.ITEM; + try { + sortingValueInt = Integer.parseInt(sortingValue); + } catch (Exception e) { + log.error("Cannot search Handles with sorting option: resourceTypeId because the sorting " + + "definition is wrong. Cannot parse String to Integer because: " + e.getMessage()); + } + criteriaQuery.where(criteriaBuilder.equal(handleRoot.get(Handle_.resourceTypeId), sortingValueInt)); + break; + case Handle_.URL: + if (StringUtils.equals(sortingValue, EXTERNAL)) { + criteriaQuery.where(criteriaBuilder.isNotNull(handleRoot.get(Handle_.url))); + } else { + criteriaQuery.where(criteriaBuilder.isNull(handleRoot.get(Handle_.url))); + } + break; + default: + criteriaQuery.where(criteriaBuilder.like(handleRoot.get(Handle_.handle), sortingValue + "%")); + break; + } + + // orderBy + List orderList = new LinkedList<>(); + orderList.add(criteriaBuilder.desc(handleRoot.get(Handle_.handle))); + + return list(context, criteriaQuery, false, Handle.class, -1, -1); + } +} diff --git a/dspace-api/src/main/java/org/dspace/handle/external/ExternalHandleConstants.java b/dspace-api/src/main/java/org/dspace/handle/external/ExternalHandleConstants.java new file mode 100644 index 000000000000..e9b7949144b7 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/handle/external/ExternalHandleConstants.java @@ -0,0 +1,20 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.handle.external; + +/** + * Constants for the external handles. + */ +public final class ExternalHandleConstants { + public static final String MAGIC_BEAN = "@magicLindat@"; + + public static final String DEFAULT_CANONICAL_HANDLE_PREFIX = "http://hdl.handle.net/"; + + private ExternalHandleConstants() { + } +} diff --git a/dspace-api/src/main/java/org/dspace/handle/external/Handle.java b/dspace-api/src/main/java/org/dspace/handle/external/Handle.java new file mode 100644 index 000000000000..fbd67c8f5cc2 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/handle/external/Handle.java @@ -0,0 +1,133 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.handle.external; + +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.dspace.handle.external.ExternalHandleConstants.MAGIC_BEAN; + +import java.util.Objects; +import java.util.UUID; + +import org.dspace.handle.HandlePlugin; + +/** + * The external Handle which contains the url with the `@magicLindat` string. That string is parsed to the + * attributes. + * Created by + * @author okosarko on 13.10.15. + * Modified by + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class Handle { + + private String handle; + public String url; + public String title; + public String repository; + public String submitdate; + public String reportemail; + public String subprefix; + public String datasetName; + public String datasetVersion; + public String query; + public String token; + + public Handle() { + + } + + public Handle(String handle, String url, String title, String repository, String submitdate, String reportemail, + String datasetName, String datasetVersion, String query, String token, String subprefix) { + this.handle = handle; + this.url = url; + this.title = title; + this.repository = repository; + this.submitdate = submitdate; + this.reportemail = reportemail; + this.datasetName = datasetName; + this.datasetVersion = datasetVersion; + this.query = query; + this.token = token; + this.subprefix = subprefix; + } + + /** + * Constructor which parse the magicURL to the attributes + * @param handle + * @param magicURL + */ + public Handle(String handle, String magicURL) { + this.handle = handle; + //similar to HandlePlugin + String[] splits = magicURL.split(MAGIC_BEAN,10); + this.url = splits[splits.length - 1]; + this.title = splits[1]; + this.repository = splits[2]; + this.submitdate = splits[3]; + this.reportemail = splits[4]; + if (isNotBlank(splits[5])) { + this.datasetName = splits[5]; + } + if (isNotBlank(splits[6])) { + this.datasetVersion = splits[6]; + } + if (isNotBlank(splits[7])) { + this.query = splits[7]; + } + if (isNotBlank(splits[8])) { + this.token = splits[8]; + } + this.subprefix = handle.split("/",2)[1].split("-",2)[0]; + } + + /** + * Generate new token and combine the properties into the url with `@magicLindat` string + * @return url with the `@magicLindat` string + */ + public String generateMagicUrl() { + return generateMagicUrl(this.title, this.submitdate, this.reportemail, this.datasetName, this.datasetVersion, + this.query, this.url); + } + + /** + * Generate new token and combine the params into the url with `@magicLindat` string + * @return url with the `@magicLindat` string + */ + private static String generateMagicUrl(String title, String submitdate, String reportemail, String datasetName, + String datasetVersion, String query, String url) { + String magicURL = ""; + String token = UUID.randomUUID().toString(); + String[] magicURLProps = new String[] {title, HandlePlugin.getRepositoryName(), submitdate, reportemail, + datasetName, datasetVersion, query, token, url}; + for (String part : magicURLProps) { + if (isBlank(part)) { + //optional dataset etc... + part = ""; + } + magicURL += MAGIC_BEAN + part; + } + return magicURL; + } + + /** + * It the `handle` attribute is null return the CanonicalHandlePrefix + * @return `handle` attribute value or the CanonicalHandlePrefix loaded from the configuration + */ + public String getHandle() { + return Objects.isNull(handle) ? null : HandlePlugin.getCanonicalHandlePrefix() + handle; + } + + /** + * Remove the CanonicalHandlePrefix from the `handle` attribute + * @param handle + */ + public void setHandle(String handle) { + this.handle = handle.replace(HandlePlugin.getCanonicalHandlePrefix(),""); + } +} diff --git a/dspace-api/src/main/java/org/dspace/handle/external/HandleRest.java b/dspace-api/src/main/java/org/dspace/handle/external/HandleRest.java new file mode 100644 index 000000000000..630ca62ea653 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/handle/external/HandleRest.java @@ -0,0 +1,81 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.handle.external; + +/** + * The `external/Handle` REST Resource + */ +public class HandleRest { + + private String handle; + private String url; + private String title; + private String repository; + private String submitdate; + private String reportemail; + private String subprefix; + + public HandleRest() { + } + + public String getHandle() { + return handle; + } + + public void setHandle(String handle) { + this.handle = handle; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getRepository() { + return repository; + } + + public void setRepository(String repository) { + this.repository = repository; + } + + public String getSubmitdate() { + return submitdate; + } + + public void setSubmitdate(String submitdate) { + this.submitdate = submitdate; + } + + public String getReportemail() { + return reportemail; + } + + public void setReportemail(String reportemail) { + this.reportemail = reportemail; + } + + public String getSubprefix() { + return subprefix; + } + + public void setSubprefix(String subprefix) { + this.subprefix = subprefix; + } +} diff --git a/dspace-api/src/main/java/org/dspace/handle/factory/HandleClarinServiceFactory.java b/dspace-api/src/main/java/org/dspace/handle/factory/HandleClarinServiceFactory.java new file mode 100644 index 000000000000..525ab469ffe7 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/handle/factory/HandleClarinServiceFactory.java @@ -0,0 +1,30 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.handle.factory; + +import org.dspace.handle.service.EpicHandleService; +import org.dspace.handle.service.HandleClarinService; +import org.dspace.services.factory.DSpaceServicesFactory; + +/** + * Abstract factory to get services for the handle package, use HandleClarinServiceFactory.getInstance() to retrieve an + * implementation + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public abstract class HandleClarinServiceFactory { + + public abstract HandleClarinService getHandleClarinService(); + + public abstract EpicHandleService getEpicHandleService(); + + public static HandleClarinServiceFactory getInstance() { + return DSpaceServicesFactory.getInstance().getServiceManager() + .getServiceByName("handleClarinServiceFactory", HandleClarinServiceFactory.class); + } +} diff --git a/dspace-api/src/main/java/org/dspace/handle/factory/HandleClarinServiceFactoryImpl.java b/dspace-api/src/main/java/org/dspace/handle/factory/HandleClarinServiceFactoryImpl.java new file mode 100644 index 000000000000..da0ed123ce77 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/handle/factory/HandleClarinServiceFactoryImpl.java @@ -0,0 +1,37 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.handle.factory; + +import org.dspace.handle.service.EpicHandleService; +import org.dspace.handle.service.HandleClarinService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Factory implementation to get services for the handle package, use HandleClarinServiceFactory.getInstance() + * to retrieve an implementation + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + */ +public class HandleClarinServiceFactoryImpl extends HandleClarinServiceFactory { + + @Autowired(required = true) + private HandleClarinService handleClarinService; + + @Autowired + private EpicHandleService epicHandleService; + + @Override + public HandleClarinService getHandleClarinService() { + return handleClarinService; + } + + @Override + public EpicHandleService getEpicHandleService() { + return epicHandleService; + } +} diff --git a/dspace-api/src/main/java/org/dspace/handle/service/EpicHandleService.java b/dspace-api/src/main/java/org/dspace/handle/service/EpicHandleService.java new file mode 100644 index 000000000000..76a4a5c6da08 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/handle/service/EpicHandleService.java @@ -0,0 +1,124 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.handle.service; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +/** + * Interface to help with + * ePIC handles REST API. + * + * @author Milan Kuchtiak + */ +public interface EpicHandleService { + + /** + * Returns the URL for handle, or null if handle cannot be found. + * + * @param prefix The handle prefix + * @param suffix The handle suffix + * @return The handle URL or null + * @throws IOException If request to ePIC handle server fails + */ + String resolveURLForHandle(String prefix, String suffix) throws IOException; + + /** + * Creates new handle with unique suffix. The suffix is prefixed with subPrefix, and suffixed with subSuffix. + * Returns the handle created or throws Exception. + * + * @param prefix The handle prefix + * @param subPrefix The handle subPrefix, or null + * @param subSuffix The handle subSuffix, or null + * @param url url associated with the handle (required) + * @return The full handle String (prefix/suffix) + * @throws IOException If request to ePIC handle server fails + */ + String createHandle(String prefix, String subPrefix, String subSuffix, String url) throws IOException; + + /** + * Creates new handle with given prefix/suffix or updates handle when this handle already exists. + * Returns the handle when handle is created or null when handle is updated, or throws Exception. + * + * @param prefix The handle prefix + * @param suffix The handle suffix + * @param url url associated with the handle (required) + * @return The full handle String (prefix/suffix) when handle is created, or null when + * existing handle is updated + * @throws IOException If request to ePIC handle server fails + */ + String createOrUpdateHandle(String prefix, String suffix, String url) throws IOException; + + /** + * Returns no content or throws Exception + * + * @param prefix The handle prefix + * @param suffix The handle suffix + * @throws IOException If request to ePIC handle server fails + */ + void deleteHandle(String prefix, String suffix) throws IOException; + + /** + * Search handles satisfying the urlQuery + * + * @param prefix The handle prefix + * @param urlQuery part of URL used to search handles, e.g. "www.test.com" + * @param page the one based offset to start searching from (default is 1 - first page) + * @param limit sets the limit for response (default is 1000, limit = 0 - all items will be returned) + * @return list of handles satisfying the URL query + * @throws IOException If request to ePIC handle server fails + */ + List searchHandles(String prefix, String urlQuery, Integer page, Integer limit) throws IOException; + + /** Count handles by URL query + * + * @param prefix The handle prefix + * @param urlQuery URL query used to filter handles, when null all handles containing URL part are counted + * @return handles count + */ + int countHandles(String prefix, String urlQuery) throws IOException; + + class Handle { + private final String handle; + private final String url; + + public Handle(String handle, String url) { + this.handle = handle; + this.url = url; + } + + public String getHandle() { + return handle; + } + + public String getUrl() { + return url; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + Handle h = (Handle) o; + return Objects.equals(handle, h.handle) && Objects.equals(url, h.url); + } + + @Override + public int hashCode() { + return Objects.hash(handle, url); + } + + @Override + public String toString() { + return "Handle[" + handle + " -> " + url + "]"; + } + } + +} diff --git a/dspace-api/src/main/java/org/dspace/handle/service/HandleClarinService.java b/dspace-api/src/main/java/org/dspace/handle/service/HandleClarinService.java new file mode 100644 index 000000000000..69e404dc4028 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/handle/service/HandleClarinService.java @@ -0,0 +1,266 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.handle.service; + +import java.sql.SQLException; +import java.util.Date; +import java.util.List; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.DSpaceObject; +import org.dspace.core.Context; +import org.dspace.handle.Handle; +import org.dspace.handle.external.HandleRest; + +/** + * Additional service interface class of HandleService for the Handle object in Clarin-DSpace. + * + * @author Michaela Paurikova (michaela.paurikova at dataquest.sk) + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + * @author Peter Breton modified for LINDAT/CLARIN + */ +public interface HandleClarinService { + + /** + * Find all Handles following the sorting options + * @param context DSpace context object + * @param sortingColumn sorting option in the specific format e.g. `handle:123456789/111` + * @return List of Handles + */ + List findAll(Context context, String sortingColumn) throws SQLException; + + /** + * Retrieve all handle from the registry + * + * @param context DSpace context object + * @return array of handles + * @throws SQLException if database error + */ + public List findAll(Context context) throws SQLException; + + /** + * Find the handle corresponding to the given numeric ID. The ID is + * a database key internal to DSpace. + * + * @param context DSpace context object + * @param id the handle ID + * @return the handle object + * @throws SQLException if database error + */ + public Handle findByID(Context context, int id) throws SQLException; + + /** + * Find the handle corresponding to the given string handle. + * + * @param context DSpace context object + * @param handle string handle + * @return the handle object + * @throws SQLException if database error + */ + public Handle findByHandle(Context context, String handle) throws SQLException; + + /** + * Creates a new external handle. + * External handle has to have entered URL. + * + * @param context DSpace context object + * @param handleStr String + * @param url String + * @return new Handle + * @throws SQLException if database error + * @throws AuthorizeException if authorization error + */ + public Handle createExternalHandle(Context context, String handleStr, String url) + throws SQLException, AuthorizeException; + + /** + * Creates a new external Handle with the given handle string, URL, and optional dead status and date. + * Only administrators are authorized to create external handles. + * The created handle is saved and returned. + * + * @param context the DSpace context + * @param handleStr the string representation of the handle + * @param url the URL to associate with the handle + * @param dead whether the handle is marked as dead + * @param deadSince the date since the handle has been dead + * @return the newly created Handle + * @throws SQLException if a database error occurs + * @throws AuthorizeException if the current user is not an administrator + */ + public Handle createExternalHandle(Context context, String handleStr, String url, Boolean dead, Date deadSince) + throws SQLException, AuthorizeException; + + /** + * Delete the handle. + * + * @param context DSpace context object + * @param handle handle + * @throws SQLException if database error + * @throws AuthorizeException if authorization error + */ + public void delete(Context context, Handle handle) throws SQLException, AuthorizeException; + + /** + * Save the metadata field in the database. + * + * @param context dspace context + * @param handle handle + * @throws SQLException if database error + * @throws AuthorizeException if authorization error + */ + public void save(Context context, Handle handle) + throws SQLException, AuthorizeException; + + /** + * Update handle and url in handle object. + * It is not possible to update external handle to internal handle or + * external handle to internal handle. + * + * @param context DSpace context object + * @param newHandle new handle + * @param newUrl new url + * @throws SQLException if database error + * @throws AuthorizeException if authorization error + */ + public void update(Context context, Handle handleObject, String newHandle, + String newUrl) + throws SQLException, AuthorizeException; + + /** + * Set handle prefix. + * + * @param context DSpace context object + * @param newPrefix new prefix + * @throws SQLException if database error + * @throws AuthorizeException if authorization error + */ + public void setPrefix(Context context, String newPrefix, String oldPrefix) throws SQLException, AuthorizeException; + + /* Created for LINDAT/CLARIAH-CZ (UFAL) */ + /** + * Control, if handle is internal resource. + * + * @param handle handle object + * @return boolean + */ + public boolean isInternalResource(Handle handle); + + /** + * Return the local URL for internal handle, + * saved url for external handle + * and null if handle cannot be found. + * + * @param context DSpace context + * @param handleStr The handle + * @return The URL + * @throws SQLException If a database error occurs + */ + public String resolveToURL(Context context, String handleStr) throws SQLException; + /** + * Return the object which handle maps to (Item, Collection, Community), or null. This is the object + * itself, not a URL which points to it. + * + * @param context DSpace context + * @param handle The handle to resolve + * @return The object which handle maps to, or null if handle is not mapped + * to any object. + * @throws IllegalStateException If handle was found but is not bound to an object + * @throws SQLException If a database error occurs + */ + public DSpaceObject resolveToObject(Context context, String handle) throws IllegalStateException, SQLException; + + /** + * Return the number of entries in the handle table. + * @param context + * @return number of rows in the handle table + * @throws SQLException + */ + int count(Context context) throws SQLException; + + /** + * Create the external handles from the list of handles with magic URL + * + * @param magicHandles handles with `@magicLindat` string in the URL + * @return List of External Handles + */ + public List convertHandleWithMagicToExternalHandle(List magicHandles); + + /** + * Convert external.Handles to the external.HandleRest object + * + * @param externalHandles + * @return List of Handle Rest + */ + public List convertExternalHandleToHandleRest(List externalHandles); + + /** + * Join the prefix and suffix with the delimiter + * + * @param prefix of the handle + * @param suffix of the handle + * @return the Handle string which is joined the prefix and suffix with delimiter + */ + public String completeHandle(String prefix, String suffix); + + /** + * Returns prefix/suffix or null/null. + * + * @param handle Prefix of the handle + */ + public String[] splitHandle(String handle); + + /** + * Retrieve all external handle from the registry. The external handle has `@magicLindat` string in the URL. + * + * @param context DSpace context object + * @return array of external handles + * @throws SQLException if database error + */ + public List findAllExternalHandles(Context context) throws SQLException; + + /** + * Returns the Handle `dead` column value from the database. + * + * @param context DSpace context object + * @param handle handle of Handle object + * @return Handle `dead` column value from the database + * @throws SQLException if database error + */ + public boolean isDead(Context context, String handle) throws SQLException; + + /** + * Return the date when was the Handle set as dead + * @param context DSpace context object + * @param handle of the Handle object + * @return Date in the String format + * @throws SQLException if database error + */ + public String getDeadSince(Context context, String handle) throws SQLException; + + /** + * Create handle without dspace object. + * This method is created for migration purposes. + * @param context context + * @param handle handle of Handle object + * @return created Handle + * @throws SQLException if database error + * @throws AuthorizeException if authorization error + */ + public Handle createHandle(Context context, String handle) throws SQLException, AuthorizeException; + + /** + * Returns a handle entity matching the provided `prefix/suffix` but only when the "magic url" + * contains the provided token. + * @param context + * @param handle prefix/suffix + * @param token the automatically generated part of the magic URL + * @return Handle entity or null (if the handle is not found or the "magic url" does not contain the provided token) + * @throws SQLException + */ + Handle findByHandleAndMagicToken(Context context, String handle, String token) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/identifier/ClarinVersionedDOIIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/ClarinVersionedDOIIdentifierProvider.java new file mode 100644 index 000000000000..bd17bd5a4883 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/identifier/ClarinVersionedDOIIdentifierProvider.java @@ -0,0 +1,271 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.identifier; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.core.Context; +import org.dspace.identifier.doi.DOIConnector; +import org.dspace.identifier.doi.DOIIdentifierException; +import org.dspace.services.ConfigurationService; +import org.dspace.versioning.VersionHistory; +import org.dspace.versioning.service.VersionHistoryService; +import org.dspace.versioning.service.VersioningService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * This class is copied from the VersionedDOIIdentifierProvider. The main difference is that was removed code + * where is created the handle based on the history. + * + * @author Milan Majchrak (milan.majchrak at dataquest.sk) + * @author Marsa Haoua + * @author Pascal-Nicolas Becker (dspace at pascal dash becker dot de) + */ +public class ClarinVersionedDOIIdentifierProvider extends DOIIdentifierProvider { + /** + * log4j category + */ + private static final Logger log = LogManager.getLogger(VersionedDOIIdentifierProvider.class); + + protected DOIConnector connector; + + static final char DOT = '.'; + protected static final String pattern = "\\d+\\" + String.valueOf(DOT) + "\\d+"; + + @Autowired(required = true) + protected VersioningService versioningService; + @Autowired(required = true) + protected VersionHistoryService versionHistoryService; + + @Override + public String mint(Context context, DSpaceObject dso) + throws IdentifierException { + if (!(dso instanceof Item)) { + throw new IdentifierException("Currently only Items are supported for DOIs."); + } + Item item = (Item) dso; + + VersionHistory history = null; + try { + history = versionHistoryService.findByItem(context, item); + } catch (SQLException ex) { + throw new RuntimeException("A problem occured while accessing the database.", ex); + } + + String doi = null; + try { + doi = getDOIByObject(context, dso); + if (doi != null) { + return doi; + } + } catch (SQLException ex) { + log.error("Error while attemping to retrieve information about a DOI for " + + contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso) + + " with ID " + dso.getID() + ".", ex); + throw new RuntimeException("Error while attempting to retrieve " + + "information about a DOI for " + + contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso) + + " with ID " + dso.getID() + ".", ex); + } + + // TODO do not return DOI based on the history + // check whether we have a DOI in the metadata and if we have to remove it + String metadataDOI = getDOIOutOfObject(dso); + if (metadataDOI != null) { + // check whether doi and version number matches + String bareDOI = getBareDOI(metadataDOI); + int versionNumber; + try { + versionNumber = versionHistoryService.getVersion(context, history, item).getVersionNumber(); + } catch (SQLException ex) { + throw new RuntimeException(ex); + } + String versionedDOI = bareDOI; + if (versionNumber > 1) { + versionedDOI = bareDOI + .concat(String.valueOf(DOT)) + .concat(String.valueOf(versionNumber)); + } + if (!metadataDOI.equalsIgnoreCase(versionedDOI)) { + log.debug("Will remove DOI " + metadataDOI + + " from item metadata, as it should become " + versionedDOI + "."); + // remove old versioned DOIs + try { + removePreviousVersionDOIsOutOfObject(context, item, metadataDOI); + } catch (AuthorizeException ex) { + throw new RuntimeException( + "Trying to remove an old DOI from a versioned item, but wasn't authorized to.", ex); + } + } else { + log.debug("DOI " + doi + " matches version number " + versionNumber + "."); + // ensure DOI exists in our database as well and return. + // this also checks that the doi is not assigned to another dso already. + try { + loadOrCreateDOI(context, dso, versionedDOI); + } catch (SQLException ex) { + log.error( + "A problem with the database connection occurd while processing DOI " + + versionedDOI + ".", ex); + throw new RuntimeException("A problem with the database connection occured.", ex); + } + return versionedDOI; + } + } + + try { + doi = loadOrCreateDOI(context, dso, null).getDoi(); + } catch (SQLException ex) { + log.error("SQLException while creating a new DOI: ", ex); + throw new IdentifierException(ex); + } + return doi; + } + + @Override + public void register(Context context, DSpaceObject dso, String identifier) + throws IdentifierException { + if (!(dso instanceof Item)) { + throw new IdentifierException("Currently only Items are supported for DOIs."); + } + Item item = (Item) dso; + + if (StringUtils.isEmpty(identifier)) { + identifier = mint(context, dso); + } + String doiIdentifier = doiService.formatIdentifier(identifier); + + DOI doi = null; + + // search DOI in our db + try { + doi = loadOrCreateDOI(context, dso, doiIdentifier); + } catch (SQLException ex) { + log.error("Error in databse connection: " + ex.getMessage(), ex); + throw new RuntimeException("Error in database conncetion.", ex); + } + + if (DELETED.equals(doi.getStatus()) || + TO_BE_DELETED.equals(doi.getStatus())) { + throw new DOIIdentifierException("You tried to register a DOI that " + + "is marked as DELETED.", DOIIdentifierException.DOI_IS_DELETED); + } + + // Check status of DOI + if (IS_REGISTERED.equals(doi.getStatus())) { + return; + } + + String metadataDOI = getDOIOutOfObject(dso); + if (!StringUtils.isEmpty(metadataDOI) + && !metadataDOI.equalsIgnoreCase(doiIdentifier)) { + // remove doi of older version from the metadata + try { + removePreviousVersionDOIsOutOfObject(context, item, metadataDOI); + } catch (AuthorizeException ex) { + throw new RuntimeException( + "Trying to remove an old DOI from a versioned item, but wasn't authorized to.", ex); + } + } + + // change status of DOI + doi.setStatus(TO_BE_REGISTERED); + try { + doiService.update(context, doi); + } catch (SQLException ex) { + log.warn("SQLException while changing status of DOI {} to be registered.", ex); + throw new RuntimeException(ex); + } + } + + protected String getBareDOI(String identifier) + throws DOIIdentifierException { + doiService.formatIdentifier(identifier); + String doiPrefix = DOI.SCHEME.concat(getPrefix()) + .concat(String.valueOf(SLASH)) + .concat(getNamespaceSeparator()); + String doiPostfix = identifier.substring(doiPrefix.length()); + if (doiPostfix.matches(pattern) && doiPostfix.lastIndexOf(DOT) != -1) { + return doiPrefix.concat(doiPostfix.substring(0, doiPostfix.lastIndexOf(DOT))); + } + // if the pattern does not match, we are already working on a bare handle. + return identifier; + } + + protected String getDOIPostfix(String identifier) + throws DOIIdentifierException { + + String doiPrefix = DOI.SCHEME.concat(getPrefix()).concat(String.valueOf(SLASH)).concat(getNamespaceSeparator()); + String doiPostfix = null; + if (null != identifier) { + doiPostfix = identifier.substring(doiPrefix.length()); + } + return doiPostfix; + } + + void removePreviousVersionDOIsOutOfObject(Context c, Item item, String oldDoi) + throws IdentifierException, AuthorizeException { + if (StringUtils.isEmpty(oldDoi)) { + throw new IllegalArgumentException("Old DOI must be neither empty nor null!"); + } + + String bareDoi = getBareDOI(doiService.formatIdentifier(oldDoi)); + String bareDoiRef = doiService.DOIToExternalForm(bareDoi); + + List identifiers = itemService + .getMetadata(item, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, Item.ANY); + // We have to remove all DOIs referencing previous versions. To do that, + // we store all identifiers we do not know in an array list, clear + // dc.identifier.uri and add the safed identifiers. + // The list of identifiers to safe won't get larger then the number of + // existing identifiers. + ArrayList newIdentifiers = new ArrayList<>(identifiers.size()); + boolean changed = false; + for (MetadataValue identifier : identifiers) { + if (!StringUtils.startsWithIgnoreCase(identifier.getValue(), bareDoiRef)) { + newIdentifiers.add(identifier.getValue()); + } else { + changed = true; + } + } + // reset the metadata if neccessary. + if (changed) { + try { + itemService.clearMetadata(c, item, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, Item.ANY); + itemService.addMetadata(c, item, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, newIdentifiers); + itemService.update(c, item); + } catch (SQLException ex) { + throw new RuntimeException("A problem with the database connection occured.", ex); + } + } + } + + @Override + @Autowired(required = true) + public void setDOIConnector(DOIConnector connector) { + super.setDOIConnector(connector); + this.connector = connector; + } + + @Override + @Autowired(required = true) + public void setConfigurationService(ConfigurationService configurationService) { + super.setConfigurationService(configurationService); + this.configurationService = configurationService; + } + +} + diff --git a/dspace-api/src/main/java/org/dspace/util/FileInfo.java b/dspace-api/src/main/java/org/dspace/util/FileInfo.java new file mode 100644 index 000000000000..fa9e75a06f6e --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/util/FileInfo.java @@ -0,0 +1,48 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.util; + +import java.util.Hashtable; +/** + * This class is used to store the information about a file or a directory + * + * @author longtv + */ +public class FileInfo { + + public String name; + public String content; + public String size; + public boolean isDirectory; + + public Hashtable sub = null; + + public FileInfo(String name, String content, String size, boolean isDirectory, Hashtable sub) { + this.name = name; + this.content = content; + this.size = size; + this.isDirectory = isDirectory; + this.sub = sub; + } + + public FileInfo(String name) { + this.name = name; + sub = new Hashtable(); + isDirectory = true; + } + public FileInfo(String content, boolean isDirectory) { + this.content = content; + this.isDirectory = isDirectory; + } + + public FileInfo(String name, String size) { + this.name = name; + this.size = size; + isDirectory = false; + } +} diff --git a/dspace-api/src/main/java/org/dspace/util/FileTreeViewGenerator.java b/dspace-api/src/main/java/org/dspace/util/FileTreeViewGenerator.java new file mode 100644 index 000000000000..b5a731b0d08f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/util/FileTreeViewGenerator.java @@ -0,0 +1,102 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.util; + +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +/** + * Generate a tree view of the file in a bitstream + * + * @author longtv + */ +public class FileTreeViewGenerator { + private FileTreeViewGenerator () { + } + + public static List parse(String data) throws ParserConfigurationException, IOException, SAXException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(new InputSource(new StringReader(data))); + Element rootElement = document.getDocumentElement(); + NodeList nl = rootElement.getChildNodes(); + FileInfo root = new FileInfo("root"); + if (nl.getLength() > 0) { + Node n = nl.item(0); + do { + String fileInfo = n.getFirstChild().getTextContent(); + String f[] = fileInfo.split("\\|"); + String fileName = ""; + String path = f[0]; + + long size = getSize(f); + + if (!path.endsWith("/")) { + fileName = path.substring(path.lastIndexOf('/') + 1); + if (path.lastIndexOf('/') != -1) { + path = path.substring(0, path.lastIndexOf('/')); + } else { + path = ""; + } + } + FileInfo current = root; + for (String p : path.split("/")) { + if (current.sub.containsKey(p)) { + current = current.sub.get(p); + } else { + FileInfo temp = new FileInfo(p); + current.sub.put(p, temp); + current = temp; + } + } + if (!fileName.isEmpty()) { + FileInfo temp = new FileInfo(fileName, humanReadableFileSize(size)); + current.sub.put(fileName, temp); + } + } while ((n = n.getNextSibling()) != null); + } + return new ArrayList<>(root.sub.values()); + } + + public static String humanReadableFileSize(long bytes) { + int thresh = 1024; + if (Math.abs(bytes) < thresh) { + return bytes + " B"; + } + String units[] = {"kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}; + int u = -1; + do { + bytes /= thresh; + ++u; + } while (Math.abs(bytes) >= thresh && u < units.length - 1); + return bytes + " " + units[u]; + } + + private static long getSize(String[] f) { + if (f.length > 1) { + try { + return Long.parseLong(f[1]); + } catch (NumberFormatException e) { + // Malformed size, default to 0 + return 0L; + } + } + return 0L; + } +} diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.2_2022.07.28__Upgrade_to_Lindat_Clarin_schema.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.2_2022.07.28__Upgrade_to_Lindat_Clarin_schema.sql new file mode 100644 index 000000000000..529577b1b800 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.2_2022.07.28__Upgrade_to_Lindat_Clarin_schema.sql @@ -0,0 +1,413 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +-- HANDLE TABLE +ALTER TABLE handle ADD url varchar(2048); +ALTER TABLE handle ADD dead BOOL; +ALTER TABLE handle ADD dead_since TIMESTAMP; + +-- MetadataField table +-- Because of metashareSchema +ALTER TABLE metadatafieldregistry ALTER COLUMN element TYPE VARCHAR(128); + +-- LICENSES +-- +-- Name: license_definition; Type: TABLE; Schema: public; Owner: dspace; Tablespace: +-- + +CREATE TABLE license_definition ( + license_id integer NOT NULL, + name varchar(256), + definition varchar(256), + user_registration_id integer, + label_id integer, + created_on timestamp, + confirmation integer DEFAULT 0, + required_info varchar(256) +); + +-- +-- Name: license_definition_license_id_seq; Type: SEQUENCE; Schema: public; Owner: dspace +-- + +CREATE SEQUENCE license_definition_license_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + +-- +-- Name: license_definition_license_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dspace +-- + +-- +-- Name: license_label; Type: TABLE; Schema: public; Owner: dspace; Tablespace: +-- + +CREATE TABLE license_label ( + label_id integer NOT NULL, + label varchar(5), + title varchar(180), + is_extended boolean DEFAULT false, + icon bytea +); + + +-- +-- Name: license_label_extended_mapping; Type: TABLE; Schema: public; Owner: dspace; Tablespace: +-- + +CREATE TABLE license_label_extended_mapping ( + mapping_id integer NOT NULL, + license_id integer, + label_id integer +); + +-- +-- Name: license_label_extended_mapping_mapping_id_seq; Type: SEQUENCE; Schema: public; Owner: dspace +-- + +CREATE SEQUENCE license_label_extended_mapping_mapping_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +-- +-- Name: license_label_extended_mapping_mapping_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dspace +-- + +-- +-- Name: license_label_extended_mapping_mapping_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dspace +-- + +-- +-- Name: license_label_label_id_seq; Type: SEQUENCE; Schema: public; Owner: dspace +-- + +CREATE SEQUENCE license_label_label_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +-- Name: license_label_label_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dspace + + + +-- +-- Name: license_label_label_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dspace +-- + +-- +-- Name: license_resource_mapping; Type: TABLE; Schema: public; Owner: dspace; Tablespace: +-- + +CREATE TABLE license_resource_mapping ( + mapping_id integer NOT NULL, + bitstream_uuid uuid, + license_id integer +); + + +-- +-- Name: license_resource_mapping_mapping_id_seq; Type: SEQUENCE; Schema: public; Owner: dspace +-- + +CREATE SEQUENCE license_resource_mapping_mapping_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +-- +-- Name: license_resource_mapping_mapping_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dspace +-- + + +-- +-- Name: license_resource_mapping_mapping_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dspace +-- + +-- +-- Name: license_resource_user_allowance; Type: TABLE; Schema: public; Owner: dspace; Tablespace: +-- + +CREATE TABLE license_resource_user_allowance ( + transaction_id integer NOT NULL, + user_registration_id integer, + mapping_id integer, + created_on timestamp, + token varchar(256) +); + +-- +-- Name: license_resource_user_allowance_transaction_id_seq; Type: SEQUENCE; Schema: public; Owner: dspace +-- + +CREATE SEQUENCE license_resource_user_allowance_transaction_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + +-- +-- Name: user_registration; Type: TABLE; Schema: public; Owner: dspace; Tablespace: +-- + +CREATE TABLE user_registration ( + user_registration_id integer NOT NULL, + eperson_id UUID, + email varchar(256), + organization varchar(256), + confirmation boolean DEFAULT true +); + +CREATE SEQUENCE user_registration_user_registration_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + +-- +-- Name: user_metadata; Type: TABLE; Schema: public; Owner: dspace; Tablespace: +-- + +CREATE TABLE user_metadata ( + user_metadata_id integer NOT NULL, + user_registration_id integer, + metadata_key character varying(64), + metadata_value character varying(256), + transaction_id integer +); + +-- +-- Name: user_metadata_user_metadata_id_seq; Type: SEQUENCE; Schema: public; Owner: dspace +-- + +CREATE SEQUENCE user_metadata_user_metadata_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +CREATE TABLE verification_token ( + verification_token_id integer NOT NULL, + shib_headers varchar(2048), + eperson_netid varchar(256), + token varchar(256), + email varchar(256) +); + +CREATE SEQUENCE verification_token_verification_token_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +ALTER TABLE verification_token ALTER COLUMN verification_token_id SET DEFAULT nextval('verification_token_verification_token_id_seq'); + +-- +-- Name: license_id; Type: DEFAULT; Schema: public; Owner: dspace +-- + +ALTER TABLE license_definition ALTER COLUMN license_id SET DEFAULT nextval('license_definition_license_id_seq'); + + +-- +-- Name: label_id; Type: DEFAULT; Schema: public; Owner: dspace +-- + +ALTER TABLE license_label ALTER COLUMN label_id SET DEFAULT nextval('license_label_label_id_seq'); + + +-- +-- Name: mapping_id; Type: DEFAULT; Schema: public; Owner: dspace +-- + +ALTER TABLE license_label_extended_mapping ALTER COLUMN mapping_id SET DEFAULT nextval('license_label_extended_mapping_mapping_id_seq'); + + +-- +-- Name: mapping_id; Type: DEFAULT; Schema: public; Owner: dspace +-- + +ALTER TABLE license_resource_mapping ALTER COLUMN mapping_id SET DEFAULT nextval('license_resource_mapping_mapping_id_seq'); + +-- +-- Name: transaction_id; Type: DEFAULT; Schema: public; Owner: dspace +-- + +ALTER TABLE license_resource_user_allowance ALTER COLUMN transaction_id SET DEFAULT nextval('license_resource_user_allowance_transaction_id_seq'); + +-- +-- Name: user_metadata_id; Type: DEFAULT; Schema: public; Owner: dspace +-- + +ALTER TABLE user_metadata ALTER COLUMN user_metadata_id SET DEFAULT nextval('user_metadata_user_metadata_id_seq'); + +-- +-- Name: user_registration_id; Type: DEFAULT; Schema: public; Owner: dspace +-- + +--ALTER TABLE user_registration ALTER COLUMN eperson_id SET DEFAULT nextval('user_registration_eperson_id_seq'); + +-- +-- Name: license_definition_pkey; Type: CONSTRAINT; Schema: public; Owner: dspace; Tablespace: +-- + +ALTER TABLE license_definition + ADD CONSTRAINT license_definition_pkey PRIMARY KEY (license_id); + + +-- +-- Name: license_label_extended_mapping_pkey; Type: CONSTRAINT; Schema: public; Owner: dspace; Tablespace: +-- + +ALTER TABLE license_label_extended_mapping + ADD CONSTRAINT license_label_extended_mapping_pkey PRIMARY KEY (mapping_id); + + +-- +-- Name: license_label_pkey; Type: CONSTRAINT; Schema: public; Owner: dspace; Tablespace: +-- + +ALTER TABLE license_label + ADD CONSTRAINT license_label_pkey PRIMARY KEY (label_id); + + +-- +-- Name: license_resource_mapping_pkey; Type: CONSTRAINT; Schema: public; Owner: dspace; Tablespace: +-- + +ALTER TABLE license_resource_mapping + ADD CONSTRAINT license_resource_mapping_pkey PRIMARY KEY (mapping_id); + + +-- +-- Name: license_resource_user_allowance_pkey; Type: CONSTRAINT; Schema: public; Owner: dspace; Tablespace: +-- + +ALTER TABLE license_resource_user_allowance + ADD CONSTRAINT license_resource_user_allowance_pkey PRIMARY KEY (transaction_id); + +-- +-- Name: user_registration_pkey; Type: CONSTRAINT; Schema: public; Owner: dspace; Tablespace: +-- + +ALTER TABLE user_registration + ADD CONSTRAINT user_registration_pkey PRIMARY KEY (user_registration_id); + +-- +-- Name: user_metadata_pkey; Type: CONSTRAINT; Schema: public; Owner: dspace; Tablespace: +-- + +ALTER TABLE user_metadata + ADD CONSTRAINT user_metadata_pkey PRIMARY KEY (user_metadata_id); + +ALTER TABLE verification_token + ADD CONSTRAINT verification_token_pkey PRIMARY KEY (verification_token_id); + +-- +-- Name: license_definition_license_label_extended_mapping_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE license_label_extended_mapping + ADD CONSTRAINT license_definition_license_label_extended_mapping_fk FOREIGN KEY (license_id) REFERENCES license_definition(license_id) ON DELETE CASCADE; + + +-- +-- Name: license_definition_license_resource_mapping_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE license_resource_mapping + ADD CONSTRAINT license_definition_license_resource_mapping_fk FOREIGN KEY (license_id) REFERENCES license_definition(license_id) ON DELETE CASCADE; + + +ALTER TABLE license_resource_mapping + ADD CONSTRAINT bitstream_license_resource_mapping_fk FOREIGN KEY (bitstream_uuid) REFERENCES bitstream(uuid) ON DELETE CASCADE; + +-- +-- Name: license_label_license_definition_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +--ALTER TABLE license_definition +-- ADD CONSTRAINT license_label_license_definition_fk FOREIGN KEY (label_id) REFERENCES license_label(label_id); + + +-- +-- Name: license_label_license_label_extended_mapping_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE license_label_extended_mapping + ADD CONSTRAINT license_label_license_label_extended_mapping_fk FOREIGN KEY (label_id) REFERENCES license_label(label_id) ON DELETE CASCADE; + + +-- +-- Name: license_resource_mapping_license_resource_user_allowance_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE license_resource_user_allowance + ADD CONSTRAINT license_resource_mapping_license_resource_user_allowance_fk FOREIGN KEY (mapping_id) REFERENCES license_resource_mapping(mapping_id) ON UPDATE CASCADE ON DELETE CASCADE; + +-- +-- Name: user_registration_license_definition_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE license_definition + ADD CONSTRAINT user_registration_license_definition_fk FOREIGN KEY (user_registration_id) REFERENCES user_registration(user_registration_id); + +-- +-- Name: user_registration_license_resource_user_allowance_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE license_resource_user_allowance + ADD CONSTRAINT user_registration_license_resource_user_allowance_fk FOREIGN KEY (user_registration_id) REFERENCES user_registration(user_registration_id); + +-- +-- Name: license_resource_user_allowance_user_metadata_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE user_metadata + ADD CONSTRAINT license_resource_user_allowance_user_metadata_fk FOREIGN KEY (transaction_id) REFERENCES license_resource_user_allowance(transaction_id) ON UPDATE CASCADE ON DELETE CASCADE; + +-- +-- Name: user_registration_user_metadata_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE user_metadata + ADD CONSTRAINT user_registration_user_metadata_fk FOREIGN KEY (user_registration_id) REFERENCES user_registration(user_registration_id); + +ALTER TABLE eperson + ALTER COLUMN netid TYPE character varying(256); + +ALTER TABLE eperson + ALTER COLUMN email TYPE character varying(256); + +ALTER TABLE metadatafieldregistry + ALTER COLUMN element TYPE character varying(128); + +ALTER TABLE handle + ALTER COLUMN url TYPE character varying(8192); + +ALTER TABLE eperson ADD welcome_info varchar(30); + +ALTER TABLE eperson ADD can_edit_submission_metadata BOOL; diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2024.01.25__insert_checksum_result.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2024.01.25__insert_checksum_result.sql new file mode 100644 index 000000000000..612810b01ca8 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2024.01.25__insert_checksum_result.sql @@ -0,0 +1,14 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +insert into checksum_results +values +( + 'CHECKSUM_SYNC_NO_MATCH', + 'The checksum value from S3 is not matching the checksum value from the local file system' +); \ No newline at end of file diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2024.08.05__Added_Preview_Tables.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2024.08.05__Added_Preview_Tables.sql new file mode 100644 index 000000000000..068f80f9430a --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2024.08.05__Added_Preview_Tables.sql @@ -0,0 +1,78 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +-- +-- Name: previewcontent; Type: TABLE; Schema: public; Owner: dspace; Tablespace: +-- + +CREATE TABLE previewcontent ( + previewcontent_id integer NOT NULL, + bitstream_id uuid NOT NULL, + name varchar(2000), + content varchar(2000), + isDirectory boolean DEFAULT false, + size varchar(256) +); + +-- +-- Name: previewcontent_previewcontent_id_seq; Type: SEQUENCE; Schema: public; Owner: dspace +-- + +CREATE SEQUENCE previewcontent_previewcontent_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + +ALTER TABLE previewcontent ALTER COLUMN previewcontent_id SET DEFAULT nextval('previewcontent_previewcontent_id_seq'); + +-- +-- Name: previewcontent_pkey; Type: CONSTRAINT; Schema: public; Owner: dspace; Tablespace: +-- + +ALTER TABLE previewcontent + ADD CONSTRAINT previewcontent_pkey PRIMARY KEY (previewcontent_id); + +-- +-- Name: previewcontent_bitstream_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE previewcontent + ADD CONSTRAINT previewcontent_bitstream_fk FOREIGN KEY (bitstream_id) REFERENCES bitstream(uuid) ON DELETE CASCADE; + +-- +-- Name: preview2preview; Type: TABLE; Schema: public; Owner: dspace; Tablespace: +-- + +CREATE TABLE preview2preview ( + parent_id integer NOT NULL, + child_id integer NOT NULL, + name varchar(2000) +); + +-- +-- Name: preview2preview_pkey; Type: CONSTRAINT; Schema: public; Owner: dspace; Tablespace: +-- + +ALTER TABLE preview2preview + ADD CONSTRAINT preview2preview_pkey PRIMARY KEY (parent_id, child_id); + +-- +-- Name: preview2preview_parent_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE preview2preview + ADD CONSTRAINT preview2preview_parent_fk FOREIGN KEY (parent_id) REFERENCES previewcontent(previewcontent_id) ON DELETE CASCADE; + +-- +-- Name: preview2preview_child_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE preview2preview + ADD CONSTRAINT preview2preview_child_fk FOREIGN KEY (child_id) REFERENCES previewcontent(previewcontent_id) ON DELETE CASCADE; diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2024.09.30__Add_share_token_to_workspaceitem.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2024.09.30__Add_share_token_to_workspaceitem.sql new file mode 100644 index 000000000000..af472c74f97b --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2024.09.30__Add_share_token_to_workspaceitem.sql @@ -0,0 +1,9 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +ALTER TABLE workspaceitem ADD share_token varchar(32); diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.06.03__Create_table_report_result.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.06.03__Create_table_report_result.sql new file mode 100644 index 000000000000..61e858e40076 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.06.03__Create_table_report_result.sql @@ -0,0 +1,19 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +CREATE SEQUENCE report_result_id_seq START WITH 1 INCREMENT BY 1; + +CREATE TABLE report_result ( + report_result_id INTEGER NOT NULL DEFAULT NEXTVAL('report_result_id_seq') PRIMARY KEY, + type VARCHAR(256), + value TEXT, + executor_id UUID, + args TEXT, + last_modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (executor_id) REFERENCES EPerson(uuid) ON DELETE SET NULL +); diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.06.09__Added_Indexes_To_Preview_Tables.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.06.09__Added_Indexes_To_Preview_Tables.sql new file mode 100644 index 000000000000..b0f95661c0c1 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.06.09__Added_Indexes_To_Preview_Tables.sql @@ -0,0 +1,25 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +-- =================================================================== +-- PERFORMANCE INDEXES +-- =================================================================== + +-- +-- Index to speed up queries filtering previewcontent by bitstream_id, +-- used in hasPreview() and getPreview() JOIN with bitstream table. +-- +CREATE INDEX idx_previewcontent_bitstream_id +ON previewcontent (bitstream_id); + +-- +-- Index to optimize NOT EXISTS subquery in getPreview(), +-- checking for existence of child_id in preview2preview. +-- +CREATE INDEX idx_preview2preview_child_id +ON preview2preview (child_id); \ No newline at end of file diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.07.29__Matomo_report_registry_table.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.07.29__Matomo_report_registry_table.sql new file mode 100644 index 000000000000..de8d18f9d972 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.07.29__Matomo_report_registry_table.sql @@ -0,0 +1,24 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +----------------------------------------------------------------------------------- +-- Create table for MatomoReportSubscription entity +----------------------------------------------------------------------------------- + +CREATE SEQUENCE matomo_report_registry_id_seq; + +CREATE TABLE matomo_report_registry +( + id INTEGER NOT NULL, + eperson_id UUID NOT NULL, + item_id UUID NOT NULL, + CONSTRAINT matomo_report_registry_pkey PRIMARY KEY (id), + CONSTRAINT matomo_report_registry_eperson_id_fkey FOREIGN KEY (eperson_id) REFERENCES eperson (uuid) ON DELETE CASCADE, + CONSTRAINT matomo_report_registry_item_id_fkey FOREIGN KEY (item_id) REFERENCES item (uuid) ON DELETE CASCADE, + CONSTRAINT matomo_report_registry_unique UNIQUE(eperson_id, item_id) +); diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.09.18__Clarin_token.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.09.18__Clarin_token.sql new file mode 100644 index 000000000000..7338a7ed7d81 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.09.18__Clarin_token.sql @@ -0,0 +1,20 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +----------------------------------------------------------------------------------- +-- Create table for clarin token entity +----------------------------------------------------------------------------------- +CREATE SEQUENCE clarin_token_id_seq; + +CREATE TABLE clarin_token +( + id INTEGER PRIMARY KEY, + eperson_id UUID NOT NULL, + sign_key VARCHAR2(50) NOT NULL, + CONSTRAINT clarin_token_eperson_id_fkey FOREIGN KEY (eperson_id) REFERENCES eperson (uuid) ON DELETE CASCADE +); diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.10.30__7z_bitstream_format.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.10.30__7z_bitstream_format.sql new file mode 100644 index 000000000000..eedbbb1ff2f1 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.10.30__7z_bitstream_format.sql @@ -0,0 +1,12 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +----------------------------------------------------------------------------------- +-- Do nothing, but force migration to load /dspace/config/registries/bitstream-format.xml file +-- to register new application/x-7z-compressed mime-type +----------------------------------------------------------------------------------- \ No newline at end of file diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.2_2022.07.28__Upgrade_to_Lindat_Clarin_schema.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.2_2022.07.28__Upgrade_to_Lindat_Clarin_schema.sql new file mode 100644 index 000000000000..d056e15b947d --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.2_2022.07.28__Upgrade_to_Lindat_Clarin_schema.sql @@ -0,0 +1,490 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +-- HANDLE TABLE +ALTER TABLE handle ADD url varchar; +ALTER TABLE handle ADD dead BOOL; +ALTER TABLE handle ADD dead_since TIMESTAMP WITH TIME ZONE; + +-- MetadataField table +-- Because of metashareSchema +ALTER TABLE metadatafieldregistry ALTER COLUMN element TYPE VARCHAR(128); + +-- LICENSES +-- +-- Name: license_definition; Type: TABLE; Schema: public; Owner: dspace; Tablespace: +-- + +CREATE TABLE license_definition ( + license_id integer NOT NULL, + name varchar(256), + definition varchar(256), + user_registration_id integer, + label_id integer, + created_on timestamp, + confirmation integer DEFAULT 0, + required_info varchar(256) +); + +ALTER TABLE public.license_definition OWNER TO dspace; + +-- +-- Name: license_definition_license_id_seq; Type: SEQUENCE; Schema: public; Owner: dspace +-- + +CREATE SEQUENCE license_definition_license_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + +ALTER TABLE public.license_definition_license_id_seq OWNER TO dspace; + +-- +-- Name: license_definition_license_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dspace +-- + +ALTER SEQUENCE license_definition_license_id_seq OWNED BY license_definition.license_id; + +-- +-- Name: license_label; Type: TABLE; Schema: public; Owner: dspace; Tablespace: +-- + +CREATE TABLE license_label ( + label_id integer NOT NULL, + label varchar(5), + title varchar(180), + icon bytea, + is_extended boolean DEFAULT false +); + + +ALTER TABLE public.license_label OWNER TO dspace; + +-- +-- Name: license_label_extended_mapping; Type: TABLE; Schema: public; Owner: dspace; Tablespace: +-- + +CREATE TABLE license_label_extended_mapping ( + mapping_id integer NOT NULL, + license_id integer, + label_id integer +); + +ALTER TABLE public.license_label_extended_mapping OWNER TO dspace; + +-- +-- Name: license_label_extended_mapping_mapping_id_seq; Type: SEQUENCE; Schema: public; Owner: dspace +-- + +CREATE SEQUENCE license_label_extended_mapping_mapping_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +ALTER TABLE public.license_label_extended_mapping_mapping_id_seq OWNER TO dspace; + +-- +-- Name: license_label_extended_mapping_mapping_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dspace +-- + +ALTER SEQUENCE license_label_extended_mapping_mapping_id_seq OWNED BY license_label_extended_mapping.mapping_id; + + +-- +-- Name: license_label_extended_mapping_mapping_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dspace +-- + +SELECT pg_catalog.setval('license_label_extended_mapping_mapping_id_seq', 991137, true); + +-- +-- Name: license_label_label_id_seq; Type: SEQUENCE; Schema: public; Owner: dspace +-- + +CREATE SEQUENCE license_label_label_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +ALTER TABLE public.license_label_label_id_seq OWNER TO dspace; + +-- +-- Name: license_label_label_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dspace +-- + +ALTER SEQUENCE license_label_label_id_seq OWNED BY license_label.label_id; + + +-- +-- Name: license_label_label_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dspace +-- + +SELECT pg_catalog.setval('license_label_label_id_seq', 19, true); + +-- +-- Name: license_resource_mapping; Type: TABLE; Schema: public; Owner: dspace; Tablespace: +-- + +CREATE TABLE license_resource_mapping ( + mapping_id integer NOT NULL, + bitstream_uuid uuid, + license_id integer +); + + +ALTER TABLE public.license_resource_mapping OWNER TO dspace; + +-- +-- Name: license_resource_mapping_mapping_id_seq; Type: SEQUENCE; Schema: public; Owner: dspace +-- + +CREATE SEQUENCE license_resource_mapping_mapping_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +ALTER TABLE public.license_resource_mapping_mapping_id_seq OWNER TO dspace; + +-- +-- Name: license_resource_mapping_mapping_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dspace +-- + +ALTER SEQUENCE license_resource_mapping_mapping_id_seq OWNED BY license_resource_mapping.mapping_id; + + +-- +-- Name: license_resource_mapping_mapping_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dspace +-- + +SELECT pg_catalog.setval('license_resource_mapping_mapping_id_seq', 1382, true); + + +-- +-- Name: license_resource_user_allowance; Type: TABLE; Schema: public; Owner: dspace; Tablespace: +-- + +CREATE TABLE license_resource_user_allowance ( + transaction_id integer NOT NULL, + user_registration_id integer, + mapping_id integer, + created_on timestamp, + token varchar(256) +); + +ALTER TABLE public.license_resource_user_allowance OWNER TO dspace; + +-- +-- Name: license_resource_user_allowance_transaction_id_seq; Type: SEQUENCE; Schema: public; Owner: dspace +-- + +CREATE SEQUENCE license_resource_user_allowance_transaction_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + +ALTER TABLE public.license_resource_user_allowance_transaction_id_seq OWNER TO dspace; + +-- +-- Name: license_resource_user_allowance_transaction_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dspace +-- + +ALTER SEQUENCE license_resource_user_allowance_transaction_id_seq OWNED BY license_resource_user_allowance.transaction_id; + +-- +-- Name: license_resource_user_allowance_transaction_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dspace +-- + +SELECT pg_catalog.setval('license_resource_user_allowance_transaction_id_seq', 241, true); +-- +-- Name: user_registration; Type: TABLE; Schema: public; Owner: dspace; Tablespace: +-- + +CREATE TABLE user_registration ( + user_registration_id integer NOT NULL, + eperson_id UUID, + email character varying(256), + organization character varying(256), + confirmation boolean DEFAULT true +); + +ALTER TABLE public.user_registration OWNER TO dspace; + +CREATE SEQUENCE user_registration_user_registration_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + +ALTER TABLE public.user_registration_user_registration_id_seq OWNER TO dspace; + +-- +-- Name: user_registration_user_registration_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dspace +-- + +--ALTER SEQUENCE user_registration_user_registration_id_seq OWNED BY user_registration.eperson_id; + +-- +---- Name: user_metadata; Type: TABLE; Schema: public; Owner: dspace; Tablespace: +---- + +CREATE TABLE user_metadata ( + user_metadata_id integer NOT NULL, + user_registration_id integer, + metadata_key character varying(64), + metadata_value character varying(256), + transaction_id integer +); + + +ALTER TABLE public.user_metadata OWNER TO dspace; + +-- +-- Name: user_metadata_user_metadata_id_seq; Type: SEQUENCE; Schema: public; Owner: dspace +-- + +CREATE SEQUENCE user_metadata_user_metadata_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +ALTER TABLE public.user_metadata_user_metadata_id_seq OWNER TO dspace; + +-- +-- Name: user_metadata_user_metadata_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dspace +-- + +ALTER SEQUENCE user_metadata_user_metadata_id_seq OWNED BY user_metadata.user_metadata_id; + + +-- +-- Name: user_metadata_user_metadata_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dspace +-- + +SELECT pg_catalog.setval('user_metadata_user_metadata_id_seq', 68, true); + + +-- Name: license_id; Type: DEFAULT; Schema: public; Owner: dspace + + +CREATE TABLE verification_token ( + verification_token_id integer NOT NULL, + eperson_netid varchar(256), + shib_headers varchar(2048), + token varchar(256), + email varchar(256) +); + +CREATE SEQUENCE verification_token_verification_token_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +ALTER TABLE ONLY verification_token ALTER COLUMN verification_token_id SET DEFAULT nextval('verification_token_verification_token_id_seq'::regclass); + +ALTER TABLE ONLY license_definition ALTER COLUMN license_id SET DEFAULT nextval('license_definition_license_id_seq'::regclass); + + +-- +-- Name: label_id; Type: DEFAULT; Schema: public; Owner: dspace +-- + +ALTER TABLE ONLY license_label ALTER COLUMN label_id SET DEFAULT nextval('license_label_label_id_seq'::regclass); + + +-- +-- Name: mapping_id; Type: DEFAULT; Schema: public; Owner: dspace +-- + +ALTER TABLE ONLY license_label_extended_mapping ALTER COLUMN mapping_id SET DEFAULT nextval('license_label_extended_mapping_mapping_id_seq'::regclass); + + +-- +-- Name: mapping_id; Type: DEFAULT; Schema: public; Owner: dspace +-- + +ALTER TABLE ONLY license_resource_mapping ALTER COLUMN mapping_id SET DEFAULT nextval('license_resource_mapping_mapping_id_seq'::regclass); + + +-- +-- Name: transaction_id; Type: DEFAULT; Schema: public; Owner: dspace +-- + +ALTER TABLE ONLY license_resource_user_allowance ALTER COLUMN transaction_id SET DEFAULT nextval('license_resource_user_allowance_transaction_id_seq'::regclass); + +-- +-- Name: user_metadata_id; Type: DEFAULT; Schema: public; Owner: dspace +-- + +ALTER TABLE ONLY user_metadata ALTER COLUMN user_metadata_id SET DEFAULT nextval('user_metadata_user_metadata_id_seq'::regclass); + +-- +-- Name: user_registration_id; Type: DEFAULT; Schema: public; Owner: dspace +-- + +--ALTER TABLE ONLY user_registration ALTER COLUMN eperson_id SET DEFAULT nextval('user_registration_user_registration_id_seq'::regclass); + +-- +-- Name: license_definition_pkey; Type: CONSTRAINT; Schema: public; Owner: dspace; Tablespace: +-- + +ALTER TABLE ONLY license_definition + ADD CONSTRAINT license_definition_pkey PRIMARY KEY (license_id); + + +-- +-- Name: license_label_extended_mapping_pkey; Type: CONSTRAINT; Schema: public; Owner: dspace; Tablespace: +-- + +ALTER TABLE ONLY license_label_extended_mapping + ADD CONSTRAINT license_label_extended_mapping_pkey PRIMARY KEY (mapping_id); + + +-- +-- Name: license_label_pkey; Type: CONSTRAINT; Schema: public; Owner: dspace; Tablespace: +-- + +ALTER TABLE ONLY license_label + ADD CONSTRAINT license_label_pkey PRIMARY KEY (label_id); + + +-- +-- Name: license_resource_mapping_pkey; Type: CONSTRAINT; Schema: public; Owner: dspace; Tablespace: +-- + +ALTER TABLE ONLY license_resource_mapping + ADD CONSTRAINT license_resource_mapping_pkey PRIMARY KEY (mapping_id); + + +-- +-- Name: license_resource_user_allowance_pkey; Type: CONSTRAINT; Schema: public; Owner: dspace; Tablespace: +-- + +ALTER TABLE ONLY license_resource_user_allowance + ADD CONSTRAINT license_resource_user_allowance_pkey PRIMARY KEY (transaction_id); + + +CREATE UNIQUE INDEX license_definition_license_id_key ON license_definition USING btree (name); + + +-- +-- Name: license_definition_license_label_extended_mapping_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE ONLY license_label_extended_mapping + ADD CONSTRAINT license_definition_license_label_extended_mapping_fk FOREIGN KEY (license_id) REFERENCES license_definition(license_id) ON DELETE CASCADE; + + +-- +-- Name: license_definition_license_resource_mapping_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE ONLY license_resource_mapping + ADD CONSTRAINT license_definition_license_resource_mapping_fk FOREIGN KEY (license_id) REFERENCES license_definition(license_id) ON DELETE CASCADE; + +ALTER TABLE ONLY license_resource_mapping + ADD CONSTRAINT bitstream_license_resource_mapping_fk FOREIGN KEY (bitstream_uuid) REFERENCES bitstream(uuid) ON DELETE CASCADE; + +-- +-- Name: license_label_license_definition_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +--ALTER TABLE ONLY license_definition +-- ADD CONSTRAINT license_label_license_definition_fk FOREIGN KEY (label_id) REFERENCES license_label(label_id); + + +-- +-- Name: license_label_license_label_extended_mapping_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE ONLY license_label_extended_mapping + ADD CONSTRAINT license_label_license_label_extended_mapping_fk FOREIGN KEY (label_id) REFERENCES license_label(label_id) ON DELETE CASCADE; + + +-- +-- Name: license_resource_mapping_license_resource_user_allowance_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE ONLY license_resource_user_allowance + ADD CONSTRAINT license_resource_mapping_license_resource_user_allowance_fk FOREIGN KEY (mapping_id) REFERENCES license_resource_mapping(mapping_id) ON UPDATE CASCADE ON DELETE CASCADE; + +-- +-- Name: user_registration_pkey; Type: CONSTRAINT; Schema: public; Owner: dspace; Tablespace: +-- + +ALTER TABLE ONLY user_registration + ADD CONSTRAINT user_registration_pkey PRIMARY KEY (user_registration_id); + +ALTER TABLE verification_token + ADD CONSTRAINT verification_token_pkey PRIMARY KEY (verification_token_id); + +-- +-- Name: user_registration_license_definition_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE ONLY license_definition + ADD CONSTRAINT user_registration_license_definition_fk FOREIGN KEY (user_registration_id) REFERENCES user_registration(user_registration_id); +-- +-- Name: user_registration_license_resource_user_allowance_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE ONLY license_resource_user_allowance + ADD CONSTRAINT user_registration_license_resource_user_allowance_fk FOREIGN KEY (user_registration_id) REFERENCES user_registration(user_registration_id); + +-- +-- Name: user_metadata_pkey; Type: CONSTRAINT; Schema: public; Owner: dspace; Tablespace: +-- + +ALTER TABLE ONLY user_metadata + ADD CONSTRAINT user_metadata_pkey PRIMARY KEY (user_metadata_id); + +-- +-- Name: license_resource_user_allowance_user_metadata_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE ONLY user_metadata + ADD CONSTRAINT license_resource_user_allowance_user_metadata_fk FOREIGN KEY (transaction_id) REFERENCES license_resource_user_allowance(transaction_id) ON UPDATE CASCADE ON DELETE CASCADE; + +-- +-- Name: user_registration_user_metadata_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE ONLY user_metadata + ADD CONSTRAINT user_registration_user_metadata_fk FOREIGN KEY (user_registration_id) REFERENCES user_registration(user_registration_id); + +ALTER TABLE eperson + ALTER COLUMN netid TYPE character varying(256); + +ALTER TABLE eperson + ALTER COLUMN email TYPE character varying(256); + +ALTER TABLE metadatafieldregistry + +ALTER COLUMN element TYPE character varying(128); + +ALTER TABLE eperson ADD welcome_info varchar(30); + +ALTER TABLE eperson ADD can_edit_submission_metadata BOOL; diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2024.01.25__insert_checksum_result.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2024.01.25__insert_checksum_result.sql new file mode 100644 index 000000000000..612810b01ca8 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2024.01.25__insert_checksum_result.sql @@ -0,0 +1,14 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +insert into checksum_results +values +( + 'CHECKSUM_SYNC_NO_MATCH', + 'The checksum value from S3 is not matching the checksum value from the local file system' +); \ No newline at end of file diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2024.08.05__Added_Preview_Tables.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2024.08.05__Added_Preview_Tables.sql new file mode 100644 index 000000000000..57919fbfa8e6 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2024.08.05__Added_Preview_Tables.sql @@ -0,0 +1,88 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +-- +-- Name: previewcontent; Type: TABLE; Schema: public; Owner: dspace; Tablespace: +-- + +CREATE TABLE previewcontent ( + previewcontent_id integer NOT NULL, + bitstream_id uuid NOT NULL, + name varchar(2000), + content varchar(2000), + isDirectory boolean DEFAULT false, + size varchar(256) +); + +ALTER TABLE public.previewcontent OWNER TO dspace; + +-- +-- Name: previewcontent_previewcontent_id_seq; Type: SEQUENCE; Schema: public; Owner: dspace +-- + +CREATE SEQUENCE previewcontent_previewcontent_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + +ALTER TABLE public.previewcontent_previewcontent_id_seq OWNER TO dspace; + +-- +-- Name: previewcontent_previewcontent_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: dspace +-- + +ALTER SEQUENCE previewcontent_previewcontent_id_seq OWNED BY previewcontent.previewcontent_id; + +-- +-- Name: previewcontent_pkey; Type: CONSTRAINT; Schema: public; Owner: dspace; Tablespace: +-- + +ALTER TABLE ONLY previewcontent + ADD CONSTRAINT previewcontent_pkey PRIMARY KEY (previewcontent_id); + +-- +-- Name: previewcontent_bitstream_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE ONLY previewcontent + ADD CONSTRAINT previewcontent_bitstream_fk FOREIGN KEY (bitstream_id) REFERENCES bitstream(uuid) ON DELETE CASCADE; + +-- +-- Name: preview2preview; Type: TABLE; Schema: public; Owner: dspace; Tablespace: +-- + +CREATE TABLE preview2preview ( + parent_id integer NOT NULL, + child_id integer NOT NULL, + name varchar(2000) +); + +ALTER TABLE public.preview2preview OWNER TO dspace; + +-- +-- Name: preview2preview_pkey; Type: CONSTRAINT; Schema: public; Owner: dspace; Tablespace: +-- + +ALTER TABLE preview2preview + ADD CONSTRAINT preview2preview_pkey PRIMARY KEY (parent_id, child_id); + +-- +-- Name: preview2preview_parent_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE preview2preview + ADD CONSTRAINT preview2preview_parent_fk FOREIGN KEY (parent_id) REFERENCES previewcontent(previewcontent_id) ON DELETE CASCADE; + +-- +-- Name: preview2preview_child_fk; Type: FK CONSTRAINT; Schema: public; Owner: dspace +-- + +ALTER TABLE preview2preview + ADD CONSTRAINT preview2preview_child_fk FOREIGN KEY (child_id) REFERENCES previewcontent(previewcontent_id) ON DELETE CASCADE; diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2024.09.30__Add_share_token_to_workspaceitem.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2024.09.30__Add_share_token_to_workspaceitem.sql new file mode 100644 index 000000000000..af472c74f97b --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2024.09.30__Add_share_token_to_workspaceitem.sql @@ -0,0 +1,9 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +ALTER TABLE workspaceitem ADD share_token varchar(32); diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2024.10.25__insert_default_licenses.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2024.10.25__insert_default_licenses.sql new file mode 100644 index 000000000000..bb51f7d3b731 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2024.10.25__insert_default_licenses.sql @@ -0,0 +1,202 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +---- +---- Default License Definitions with Associated License Labels and Mappings +---- NOTE: Do NOT use this file if your repository already contains licenses. +---- +-- +---- +---- Data for Name: license_definition; Type: TABLE DATA; Schema: public; Owner: dspace +---- +-- +---- Insert data into tables only if the tables (license_definition, license_label, license_label_extended_mapping) +---- are empty +--DO $$ +--BEGIN +-- -- Check if the 'license_definition' table is empty +-- PERFORM 1 FROM public.license_definition LIMIT 1; +-- IF NOT FOUND THEN +-- -- Check if the 'license_label' table is empty +-- PERFORM 1 FROM public.license_label LIMIT 1; +-- IF NOT FOUND THEN +-- -- Check if the 'license_label_extended_mapping' table is empty +-- PERFORM 1 FROM public.license_label_extended_mapping LIMIT 1; +-- IF NOT FOUND THEN +-- -- All three tables are empty, so insert data +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (1, 'GNU General Public Licence, version 3', 'http://opensource.org/licenses/GPL-3.0', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (2, 'GNU General Public License, version 2', 'http://www.gnu.org/licenses/gpl-2.0.html', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (3, 'The MIT License (MIT)', 'http://opensource.org/licenses/mit-license.php', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (4, 'Artistic License 2.0', 'http://opensource.org/licenses/Artistic-2.0', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (5, 'Artistic License (Perl) 1.0', 'http://opensource.org/licenses/Artistic-Perl-1.0', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (6, 'Attribution-NonCommercial-NoDerivs 3.0 Unported (CC BY-NC-ND 3.0)', 'http://creativecommons.org/licenses/by-nc-nd/3.0/', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (7, 'BSD 2-Clause "Simplified" or "FreeBSD" license', 'http://opensource.org/licenses/BSD-2-Clause', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (8, 'BSD 3-Clause "New" or "Revised" license', 'http://opensource.org/licenses/BSD-3-Clause', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (9, 'Attribution-NonCommercial 3.0 Unported (CC BY-NC 3.0)', 'http://creativecommons.org/licenses/by-nc/3.0/', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (10, 'Attribution-NonCommercial-ShareAlike 3.0 Unported (CC BY-NC-SA 3.0)', 'http://creativecommons.org/licenses/by-nc-sa/3.0/', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (11, 'Attribution-NoDerivs 3.0 Unported (CC BY-ND 3.0)', 'http://creativecommons.org/licenses/by-nd/3.0/', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (12, 'Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)', 'http://creativecommons.org/licenses/by-sa/3.0/', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (13, 'Creative Commons - Attribution 3.0 Unported (CC BY 3.0)', 'http://creativecommons.org/licenses/by/3.0/', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (14, 'Public Domain Dedication (CC Zero)', 'http://creativecommons.org/publicdomain/zero/1.0/', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (22, 'Apache License 2.0', 'http://opensource.org/licenses/Apache-2.0', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (24, 'Affero General Public License 1 (AGPL-1.0)', 'http://www.affero.org/oagpl.html', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (25, 'Affero General Public License 3 (AGPL-3.0)', 'http://opensource.org/licenses/AGPL-3.0', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (26, 'Common Development and Distribution License (CDDL-1.0)', 'http://opensource.org/licenses/CDDL-1.0', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (27, 'Eclipse Public License 1.0 (EPL-1.0)', 'http://opensource.org/licenses/EPL-1.0', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (28, 'GNU General Public License 2 or later (GPL-2.0)', 'http://opensource.org/licenses/GPL-2.0', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (29, 'GNU Library or "Lesser" General Public License 2.1 (LGPL-2.1)', 'http://opensource.org/licenses/LGPL-2.1', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (30, 'GNU Library or "Lesser" General Public License 2.1 or later (LGPL-2.1)', 'http://opensource.org/licenses/LGPL-2.1', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (31, 'GNU Library or "Lesser" General Public License 3.0 (LGPL-3.0)', 'http://opensource.org/licenses/LGPL-3.0', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (32, 'Mozilla Public License 2.0', 'http://opensource.org/licenses/MPL-2.0', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (33, 'Open Data Commons Attribution License (ODC-By)', 'http://opendatacommons.org/licenses/by/summary/', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (34, 'Open Data Commons Open Database License (ODbL)', 'http://opendatacommons.org/licenses/odbl/summary/', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (35, 'Open Data Commons Public Domain Dedication and License (PDDL)', 'http://opendatacommons.org/licenses/pddl/summary/', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (36, 'Public Domain Mark (PD)', 'http://creativecommons.org/publicdomain/mark/1.0/', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (37, 'Creative Commons - Attribution 4.0 International (CC BY 4.0)', 'http://creativecommons.org/licenses/by/4.0/', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (38, 'Creative Commons - Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)', 'http://creativecommons.org/licenses/by-sa/4.0/', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (39, 'Creative Commons - Attribution-NoDerivatives 4.0 International (CC BY-ND 4.0)', 'http://creativecommons.org/licenses/by-nd/4.0/', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (40, 'Creative Commons - Attribution-NonCommercial 4.0 International (CC BY-NC 4.0)', 'http://creativecommons.org/licenses/by-nc/4.0/', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (41, 'Creative Commons - Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)', 'http://creativecommons.org/licenses/by-nc-sa/4.0/', NULL, NULL, NULL, 0, NULL); +-- INSERT INTO public.license_definition (license_id, name, definition, user_registration_id, label_id, created_on, confirmation, required_info) VALUES (42, 'Creative Commons - Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0)', 'http://creativecommons.org/licenses/by-nc-nd/4.0/', NULL, NULL, NULL, 0, NULL); +-- +-- -- +-- -- Data for Name: license_label; Type: TABLE DATA; Schema: public; Owner: dspace +-- -- +-- +-- INSERT INTO public.license_label (label_id, label, title, icon, is_extended) VALUES (20, 'PUB', 'Publicly Available', NULL, false); +-- INSERT INTO public.license_label (label_id, label, title, icon, is_extended) VALUES (21, 'ACA', 'Academic Use', '\x89504e470d0a1a0a0000000d494844520000003500000016080600000060ded4e8000000097048597300000b1300000b1301009a9c1800000e256943435050686f746f73686f70204943432070726f66696c65000078dad5976750135c97c74f0a49e848ef448a204d7a514120201d218054a524a199841002084a13055190a28822a0f4a248095d407a551101b1a0d81ebb8f05441031fbc17df77d6676f6c3ceec97fd7dfacf9973e79c39e7cedcfb0710ec0c6230a84821001a9dc524da5ae1bd7d7cf1d847c00302200e1ab02d8814cdb074757582ff91d5fb80000098d50a6230a8f0bf43804c89260120340120921c4da201208a01c09ac460b20010cf00e06e1c8bc1024036018028d3dbc71700390c00a2a17ff40300100dfea33f028028d383480040010008c4924259002821008c309d1c4e07e05606c09893c282c800826400d0a4d122c90082b900a016fc8fb3a1ffd07ffa040000314224359289772258e30941d4f06066108b4286ff6368d4987fd54300001f85eee90e006a00200b0488042a440213f0e00404b0063c102008a8100ec1c08420600105c82cca61160000219211cf0c0f0d63e12d190c2a45136f4f27696be2f5747475e0ff13de3ebef83f6a851f10008008fff5ef987f3c80411800fac8bf630c5e806b5f00a4ff91a7ee06205f0bc06e27c53063ff3563c46d803f77e53f412100900020053ed0899045e4226590eda840b402fa19572fa6015b822be6aee7b9c9fb806f4560aba0a7d0f12d23c23f444dc458e26d12eb5286d20c9956d955791d85087cf5d6792511650b95a06df9aa5d6a6fd44103af49d0f2d60edf71442747b742ef8a7eb941a361b35191f179933cd3c49de45d4ebbf5cca4ccc1fce59edb167d96b556058434eb4336ae7b0d6de5ecc0ee8dfdbc43a7e379a72467928bf53e255784eb73b71e62917b9c879be7764fcefe59af1aef241f575f65df6f7ec3fe670e781d5439f877407be0b1208760c1e07ba47cb23b4594f220e47ca8679848d854f8c9089b43e8437d54164d8bf6825e11e9cb10620c47c5330da221fa05eb564c53ecb938c66162bc5e825cc2fa914747d989f949b1c9c4148d545ceaeb63fd69178e479e704c974fff9171e7645126f594f96981d34fb2dab253cf38e588e72ce5d6e431f3cdcf22cf8e9dcb29f0382f797eb1b0e4c2818bf2179f16155ef22b162fbe579253ea7299f7f2f8958c3242d9eff2a60a5aa56ae55f55c5d50ed59f6a926b856acbeaf4eafaea9dea97ae1ebaba762dbd41b8a1ecfa8eebbd8d4e8d8f9b429a569bd35b445baeb0b5d983adfb5a9fb645b5437b4e8742474b27a173be2ba28bd37df686ea8dde1e9f9e95de937dca7d8337836ffeeebf326033f07e306768cfd0dbe1c211a751d4e8cdb1b471e284f2247272796aedb6e49da8e9040010001d2041157c47b820d8485d642fca1bcd831ee72ac4c46129b8406e12cf11de5cbeebfc0f049142c65b42850b44a6c470e2161234c9cb5213d2ebb29a72eef2690a2df8fb5b7f2a892a6ba9386fa3aa26aae56caf52efd118d27ca2f546fbf98e4f3a3f75d7f556f4970dbe1bae1abd327e6572df747467c7aedadd4566d9e647f7845bb85a1a58491250844fd673361d7b2fd9a6d985da5b3b6c73e473fce034ea5ceb92becfdfd5c84dc8ed3d71dcbdd423dad369bffcfe2f5e23de053e145f633facdfacff9503f483bb02b8031e06d6063182f790b849b3e4524a68884ec846e870d8e970cf08c5885787d8d42334029d877e37f202831c65c294894647bf654dc4d4c69e8ca31df688374a9048583e327fb42fb134e94872400a2155e518fad8cbb4a9e3d527b2d21919ee270d33c533574fdd3fdd91559a9d7ce6608e592e3e0f97f7317ffa6ccbb97305acf33e859617142f222ffe553479a9a5b8b824a1d4ffb2c315d532c1b2f5f2c58abecacaaaeceaf81a4aad439d563db6fef6d5d3d7ec1bd00d5dd7631ab51bdf375535fbb708b64cb2d35a4d5a3fb655b47b77f0740c74c674a9773dedceba6171e3474f632fa54fb26ff6667abf59ffca40fd60e090ccd083e1f323c451fed1a9b133e36e1312137393c553945b5ab7566f0fddc99b0ebe6b3cc333f3f45eebec99b943f3f6f79517300bef1ecc3cbcf048efbff65f03eb0877441752035987d2438da0c95cfc5cc39864ac258e07f79cbb87a78837858fce4f16f017dc2fe4b98528ec2d724034482c423c5e2243324faa44ba45664076546e41febdc257fc9a2242895719a722bc4d50954f4d7a3bb73a5a03a1b1acf9516b517b6247bb4e896e8a1e459f60a06d88337c61d46f5c6672dc94bcd36a97ccaeefbbe7cddacdcfeda15bd8596a5821ac16093dd6c536717b5d6cb5ec78ed5edbf73b5c746438ed7596755e7599da77c5f5b09b035199b8ea3ee151ea19b5dfca4bd8eb9537db27c5d7dd4fceef8d7feb81d4836e010a016f039b82e2836d4882a487e40a0a354437e47b68575872b84b8448c4c2a1126a384d9df685de1999c4708a128e7ac4ac8aa6b3cc63b863e662cbe3e8874de2d1f1330925476847cd12f9131793ae2627a410531553578e8da7151f8f3d619b2e9bbe9271eb647966fc29e269f52c64d6a36cf699829cc85cbb3c7cde7afefdb39de70a0a98e71d0a552fa02f2c5dec2d2abd1453ec5562582a7a19ae20cbf8ca852b442b25aa64aa156a146a65ea24ebc5ae8a5e136910bd2ed628d924d32cd322c696685568536bd7ead0ea34e8b2ea76bb11d413dd9bde577273ac7f6960634879d8612461b479ecdd84f264e054f9adb53beed34d3342f7e26697e73317441eb01f452dea3fd95cda78817e65fbfaeabb9d1f777ece5eee5c6fe47000fefc1900003046001702007ce400dc6c01b276036c1b0490e00170e507f03005a4b62820be0f02c22ae71fefc74ef085a3500203f0128146a8219c100cc4394417620989416a233d9189c82ae45de43a4a09e5864a44d5a31ea27168137418ba087d870bc565c245e3aae07a8c11c3b8624e63c6b038ac2df604760cc78fdb8f2bc2bde4d6e266710ff388f19079d8bc58de00de4e3e613e1adf38bf0aff69fe65012f816e4115c112213ea123425fb6d0b77c12a6097f116189ac8ba689f189e58a4b88574a684b744b3a4a2e49c54af34857ca58ca3c9465c889c875c9fbc8ff52a8c4efc3ffde5aaf7840495869443951c544e5c7b62ed564b5bddb79b73f54afd488d774d052d3e6d25eda31a253ab7b4a2f4e9f62e06fe86eb4cfd8c664afa9fd4e975d81bbc3cde2cc8fed29b3e8b25cb4fadb5ad8c6746fa86dbe5dbd7d9543ae63aa538233c325765f946b941b8548778ff448f6ccda9fed55e87ddd67d077c6efb53fe7a07c8059a057d0b1e072d2040515a21b4a0e2b0a9f3d04542bda49fa5d864c5438b32dfa774c606cfb61fe787ac2dda3868915c95b5292523969d4e39fd269191f33c34e7dce6264afe51ccf53cb6f39e758305f9874d1f6924709f5f289b28e8ae96a74ad497dd4b5aaeb2f9ba5d8216da51d6fba77f744f74d0ff00f058d748c23267d6e5dbef361c66c3673fec603e1476e8ba94f879f7d7b29f197dd1be2bbd80f499f123fc77cf55d565879bd7a714d73bd6683fb1771f3e0ef140e0700742104cec3087c45c823ec112cc465c424621589473a21139035c839140aa5870a42e5a34650eb682d7420fa3cfa361796cb8a2b91ab87eb27c614138fe9c502d61a9b89bd8793c585e13ab9b9b97db9af72ffe6f1e269e6e5e5a5f00ef0a9f0a5f2bde077e4ef16c00b6408ac0a860b3e1172131ad9b2734bafb0a1709bc82e9101513bd119313fb1d7e231125889524913c9fb520c695ee97a19679915d9223973b90ff2a50a8e0a6bf8a6ad118a0a8a8f95ca94835554553e6e6b524d51dbb75d6efb0ff53b1ad59a27b542b46d77e8e848eb627457f45eeadf331832ec376a35669bb04d5b7676edeadb3d6a3667fe6ccf7b4bb4950041d5dadcc67f6f826da15d837d8b43bde355a746e72e97b17d33ae0b6e4bc4af1e084ff9fd5a5e26de9e3e2cdf4cbf72ff9b079e076002e5831c832348d964366531141b66141e1271f1d020f51b5d3d92c42889ba1d0dac5d31b1b1cd71dfe24d12a28e341cfd91b427393d65ea187f9acbf1b213df32b44f9eca5c382d9fc5cc9ecac1e746e70d9e153c175c3058b8f5425191c5a50f25459703cac4ca672bb3aa9d6b95ebde5c6d6cc86a0c6c3666cbb6aeb42f767675b7f694f4a5f69307fd86778f1a8ce327456ef1df5e9b5e9a19996d9faf5d38fd90fad8e189f992f8b36f2fe65e35bc3ef676ff7bf90f1f3eddf89cf1d57959606572b560cdf927ff46cf269dc30100538883365841e823a2102d8815a421f230b2178544d9a2b251f7d10a682aba930bc3e5c555c7b58171c1546138586f6c134e08178dbbc76dc05dc283e561f22cf17af00ef399f175f26bf2b3050c043a0409825342078596b79c12d614be25122b2a293a299628ae23fe5ca2509228252ab5205d20e323ab24fb4dae5b3e4f8184d7dfcabbf5a3e2a852a572864ae4363f552735f3eddaea1a1a8a9a4a5af2da6a3bb4748c75edf4fcf4230d720c6b8ca68d7f982aecb4dd15bdbbc1ecf51e790b7fcb4b564bd69a36c97be7eca4ed590eb34e2ace675c3eba12ddaeb94b79247abef5b2f7eef255f4cb3f803b9810b01c1412fc88ec4e990e7509bb19617da88f66416f675844f547ef618dc5bac4cdc60724bc3f1a9fb8969c912a73ace5b8ed89e90cd2c9f553c5597ad97339cc3cbefcee73c482cdc2e28bc245b1971e97b8960e5c312aabab90af2cafde56535fa75dcfbe66dcd0d368d134d262cf7edec66c5fed4ce8dabc71b657a5afb65f7de0da90f670e3a8ced8b50981c9ac5bbcb799775ede759b699a1598a3ce4f2ca83c487978ffb1e4a2f7930b4f179e893d27bec87879fdd5d05f8f5f73de8abd537aaff861eb47854f727f4bfdbdf9f9e997beaf25dfc296759797579abfd356f1abf33f62d6f8d62ad70dd66ffe74fc39b961ba51fb4be2d7d15fe39b629bc19bd737377edbfecefdfd90a3c4a172da391c80e8107dbd3f0e434a0e80e70b87f36e1f8000016093c4e1ac9971381b2f0130750067c3fff85000008c1040712600c05073ed7ff341ff017c3ece4feba5845a000000206348524d0000592b0000595f0000deb900008399000070e20000c0e400002f4b00001978d02b64290000042c4944415478dad498594c5c5518c77ff7323bcb40ca32436929ed80d2c5071393425994362969215553496c6dd3343e92b8251a1f348d892f7d50138dc697be9214ab95124b09d0326030b46a84a235b294916518d619665866e6dee3c3c4a1bcc89c312afe93efe5de73fff7ffffbe73cff9ee518410c431d5e1c2ebaec2ebae26f47b01db1da9bb26705475e3a872937f6cf8cfcb4adcd48da79a3105d3c82daac55909f6d26def09ff2f30dd03beb136c26941eaefbeb061aaf3b90fc9010a2b5e657d00d60721eaddfea60c0e301f02f31330defb11b3c0d1af5e4bb9f4d2be2a166fd4b337ff3cfe26088f821604c1f60f2d18d3bbd20759aec3cc3c9843a44f2aa2bdee630ad31ba13bf10c897fb11a8acce06a185ffec44060d8852117c2124f0b79570b0195cc3481aa4a3eab48e8324d42c0e732a0874dac79404f2e7da7dfcee6e1b4814b2ffba9ab58dd346a3da2f0d9b534ae77db08ae2a988c507b789537ce2c936addfcc2bfe24918ba0774d5a402108d82ae241e1aa0c1d0b09187d30654155afa7782b5207e0f0ddebf62a7a9dd464db985cbefeca1fe780e2d3d563eb8b61b8431311e195dd1686cfd88994a6ea2b7f6da301be1d993b95cfddac784bf80022601f0cc18f8a6cf426d958577df2a85e014358fcde1cc4ca5b8d4099919303bb8254f3288554a261bba023a44c270abdf4cf993664e1dcf420868b91385141be830369502c0c1833910f2826f00825e2e548c72649f17966712e291d6163715950c4da1e7272b81159563cfe451e20851e810b4b6cfa35b0b405358f0c74cd96c29b0ba049ab211f3a3b03c9b108fb4b6f8f4138ae40709ad7d1600aedf5aa4ad2dc4ea9a8e6f29c2772376caed2a0e7b6c21989b8f40448b09047e183651bc3342ba35b60a6ec58322929c7e8f663181585852f8f6672316b3c2c8d82a43630a51618c755b1dcb60cda6c419c56880ce9e45a2584083f92595c64fed9cb99c8d1086847864b56d544a97cb44db3d2b9aaef0deeb851c7f3a13740d80b38dbfd1dde72770c1c90edb1ca78f4468ea5ee1f9378dd41e4aa36bc044380aa74e3a515c0768fbbc7f4b9e0c7d2ec94a45e4a2f59e05b3112acb32c1330483b761f03627f6cf130eebdcbc9b02aa91576afd9cafb3b214d0b9d2610583898b0d3bb878361fe62712e391d4166b68afeeed244baf914a8500f69781d5063fde89671880bc3db0ab043c0fc0e7014541e417b3647192956586b5102ccec0d448ac33d98ae7fb76b9322daa5d8af8e2f19ba4aed722ed2a09188d1089fc93cd1f84cc6d06325cc3847f0555935afd9282169517a94a8cd75320c335ac52d4d04c980ea9bd407ab3fe1b21a32b4c07450dcd065ce7dc8c7f791fff808a4a0dff57e874613f701fd739f723bff3654daccde6a2881aa976ffbf861020942e2c393eeafb5edc7c460130d5e962a6a702afbb9ae0f8ee6d6f28add083a3aa9bbcca5ef28fc60f5efe180026b7b96a2d80c8d60000000049454e44ae426082', false); +-- INSERT INTO public.license_label (label_id, label, title, icon, is_extended) VALUES (22, 'RES', 'Restricted Use', NULL, false); +-- INSERT INTO public.license_label (label_id, label, title, icon, is_extended) VALUES (23, 'BY', 'Attribution Required', '\x89504e470d0a1a0a0000000d4948445200000040000000400806000000aa6971de0000001974455874536f6674776172650041646f626520496d616765526561647971c9653c000004374944415478daec5bd171e23010154cfe4d07d081e9c0a402e800ae024a800e4c07d001e9c04e05261598abc05001a7e7d1ddf88824af644931096f66c70cd842fbb45aed4aeb17e617632e332e532113213a9c85e48deb6ff64088b9a45c4a2e3747528a36e33e2bbee45238545a2585f8af5e295e06505c66155f4a04e677f6058acb2c22f8d458f740f17bd9d8283230bc3fe272e0b2203f10456c369bb1e9745a5f2793492d52f77f3ed7723a9d589ee7b55caf5793fee5a26f571fa31e9938b9e572793b1e8fb7ae401b688b1369e21b9c4f093458513ab0d96c6e6559de5ca3aaaaba6d2211954b1248cacfe7732f8acb8880458422216a5be230222e4cdd14599651aca11309ad733e8ee35b5114c69ddfeff7b5c52449520b3ee33b1b6b401f083e21b22160dfa63c3a600a9df9e2371b10a6446613dd39571ed6d236776d2c8a48c2c624c2ab5c2b0fc08b5356115b1048f8e40f861202765c465266c6e33a38198d46bdccc60e8703e303a4bda58d804417e5bdbdbdf556f97fa1201f20449f0a4cef13a87b02b6aa27d334adc3d92ea090d795603c8f81d240a963a29a3b58aa5c0531bab51bbfd9fa17437f204da38fae3db32ab69791e03aa06a21bb90797ea76b332592e3d3aaf6faf8ec032dab4e4ccaef43c4f73ef3060d01699380c2c7dcc7d4314863a5d3a2ebf4d3f882b219f34b6fb289cf9b408cdf75a7076d74f5399af6c743b16f2fc562b1e8b4245d2e97ceeb7ad736a083262e980d4570f0794d4c92de073d54602b4e8189d202340f3d1c34015c6d01a3ef4e409b0548e951eddc3e2234ba4c86160f7d2702d890fd703c097812f024e049801438a4fc2ed0e902024e3f9880f30bf20dd5e662d76870b7dbfd97ccbcbebe929ecbb2ccd91e2180e3761d01b92c1f00011e6370dbd0d53501271448ccb948b751793aedb4238301ad1ec3f5ffc28a148516aba1b0002970d0f0e8c0e86baa4c7210705539c296fdf58700fc906afeb3460166904d5146dc060bb4359e36e300e5506fb7db871e7d8df97f9adfde0f46425a40cbe8973246bc1f8d852460bd5e1b1f8dd53108f3706e1f9a009c34317dc98c1209f350bd119280b6035846a82f56d606a1e12eab826f0208455305c579e214c14b898c6f026c4a645498330f4552be0820164f1a1753a76d24984e071f04106b059565726dd9096203e50121cedc102e53b337ea7dd44c1471fe6ab5621f1f1fdadb44b66b55414eaa1077b1449a02c515be4b658d488019faaaf4b8af394060c602578c93df15c079becb9aa2bf80bf2156897b7b8d26d2e50bb2f01905165daabef02cda208e7853797271f4c082082c27462922aa37e10021d89d513943383fec21fe7d65e6fdfdddb46fc8f07e85c83413467c832490542276098aa82d5608244766f93e802bc4ec6bde1fcc8425f606898993ec38e2bd52fc1e63b1c7e8f23de242b43976ddd941003f01973f15d7115394e4dc85ae17b15d7f12d7abaf0efe1160005144fe5e042ce31f0000000049454e44ae426082', true); +-- INSERT INTO public.license_label (label_id, label, title, icon, is_extended) VALUES (24, 'SA', 'Share Alike', '\x89504e470d0a1a0a0000000d4948445200000040000000400806000000aa6971de0000001974455874536f6674776172650041646f626520496d616765526561647971c9653c000004ab4944415478dadc5b8d75a24010461bc00eb403ed403bd00e4c075e07da81d781d7014905980af02a402b502be0f6e3418ee4606766990572f3de2426c2b2f3b333dfcc2ea3c02f4d0d2f0a5e199e156ca38be187e173f1197c0bbe1141e8a3e1c470a6c44931e67cc8826f0dc78a42db94b11d92e04bc36907827fe5b46f454c3bb238c5711f4b039abf0f40f82aef5d041909af0f0dff34fcc2be210c83d56a152c168bfcf76c36cbb98eaed76bce97cb25389fcf393f9f4fc9fc903936869f3eac1e4a22fb76bbcda228cada12c6c0584691922039f7b1de13aee0699a66da74bfdfb3fd7ecf55c45d53092167bd2f974b2f82d729024aee4a09a4dbc322a7d329eb9ae238e678436b2558859f4ea7599224595f046f98cfe79c9810ba087fb40d8c0763027d137349c452e1d7df41f82a3194b057097a43145ea004563c38d9029eabf0c810c7e3315bafd7b5eb16ffc3774875ae71851113c8a5b0b469d0656288d6489152688b00eb925da004223b580ba8c6e20696914e64b7dbb5c6f850049428553a5145caac0fb7f2909e442c3500110f6abd206aba4162011fc25761b6d25248eab07eeb878210c87c96bd124fc0b5dc8cb06bba5082ef11b4baa8fdb91e4978c19184bc88de1242c0a2268f09d5a53afccdadf6f01c6e3ab6c482b40a7c6a2f92a4218ef5b13ca8897363087729a09f6019676a85bd12d043ad7d098264e4f2fc7b2e59c6cab3c15ec3fd298b49bb431c8fe28e6931cebe11fc48a22dd6af96b52431859ba100c19ba0f1d8fc98d481023430293a1c0ef9752f2f748f14d76d361b51494a5d8fa62987d0906da0bc3beb9cfe24a0075e20ad258800c68e5390c53246fd179ac8cf457806a61761026f0aa094e02a3c37b80e42014d4a682b3c4701dc60dd74ff58ab773e994cf2a06494f0b12384bf2d016830a4e201554f0086d0ea16531e8014378825e083287ca111031a97003629fb26ad39d8c61917677006a9000ed0e100368b2cd7717120c91965958870341a3532679275f4fafa6afdbe0cb81461bbdda680b3f0a67ff1e46ce63a01abf56fb75b6beb13c6bc58cb6149cf5f2b5895c481d8dc4c4395c3a146094b556e927e0067fb9b5bae1399646a6d89a18ee692a5e4fca4045b9125d8fb677b14a725a6d214e57471aa753c1406214ac6260aa7a728318ca429aad216e77881c64e11772949dae2d68d1109ac75d907e4b2a4b822ac9f8ab6c624fd415f3b43d2ca92d897dc8a3747b94547b520d2129e0a9e5a9ba3eadbe3509ae07c5fe3264a97dbe3ea0724caf37ddc085f063adce3f22c62f9259ca3b20046d7a66e31f037a0251a20522a8fc096c761abcd14344e00a9cbe3b42e842ef2dbdb9bb5416cf8f77f77488a09a0c487a98f9a81a9e7b38271e0481115a4a4458e76c788217cea7a50927d425c1aa935889961ba392f5c2e892ebc015667620cf513e309b750f1717e18f1865b296a0b5f5542c4cde5b012b6b8db640bdc8b3184a832a9d6f9daafcc94e7090e921b801d90dfc1c8f94dad2c6084c7e3f18117dedfdfa573fb65f887af5766be42e621bd34752f7a1a9d524861858e3892b8bc2f6fe8e3fdc1b878f660682909922d2d3e28c1eb4e9dee02fd97a7773e5c7dd4419c58057f5f9f9f149fad4563f0f9f5f9b3cfa8fe478001008252cd9d372775a60000000049454e44ae426082', true); +-- INSERT INTO public.license_label (label_id, label, title, icon, is_extended) VALUES (25, 'NC', 'Noncommercial', '\x89504e470d0a1a0a0000000d4948445200000040000000400806000000aa6971de0000001974455874536f6674776172650041646f626520496d616765526561647971c9653c000005024944415478dad45b3d76da401016760a77a24c876f20a5730737801b403a3a8e00398192130027c0a53bf009049d3be113002750f6535679b2bcff3f923cefcdc37991969dd1ce37df8c865ee0570644474463aa8f544572a67aa87cbe075f4822a209d18c68ee4833ba66d465c3a744538746f334a5dfd529c3b3060c679d8a561d81f8deb76038eb441887c6bde17d0ba22f0a80c695300c83a7a7a7e0f1f1df12b7dbcd74a9ef44e7f4ef57dd9b7bbafb26ba213ad13174329904711c173a1a8d84d71f0e87e07c3e179fd0f777ad0470a459e7e6e3c887aa20478ccea7d3699ea6696e2b5863b158146b2a86c4c547b688e8c252c397cb657eb95c72d78235b1b6a2239c3a41c9f8f178ecc570962370229a7242284b717822bbdd2e6f5af6fb7d3e180cbc3a411af35114e55996e56d094ec37038547142689205d64467dcb888a202a5fbfdbe91774b942f65369bfd4f89ba827bb7dbad2c3bfcd06577c2276f1bef00b4ea9a38d23682ac2339094b96a1771c86f7dbd793f7259bcd26204e105db262e101cb01309e691d019d4e1a5f7502c104e12532070c792c0f8ceef9f9b9b3c697823de2417124ae1750f70c0f3191683e9f17606323a0b8d820144fabcaffafd76bf0f6f6565c03279b3afae1e1a1a0dc02508413feb0fe6328231700bff57a6d4467419474aa3cd3ef2a4542969860b153dd1c08882ae5851136e52ebecb2443606f02da9cb2905f7b7365d1c32343b6c657d58470d5536d4da37a7d6fb54138a2faa4b06157c69b8682e41424550738ebe5819a62c30ac4c4abf10a0429ab727e2e10f9686321ae61581543707aea753f36efa29f20d84b912fc7bcf82e8f3236a2d190b08ae7b2dc85ba12c1de8b6cb0e4d5f7ac868442092ad5a6459082933bda43fbcc16e2f8c3bf414c56ab554154c8f115b12da9606d10a1a644d0878cb900a8927b718d423d2ec4822449bc7792b04f11105ae7dd12274c1d811845ccfb6aae4852b2bb38d56c5c72f9848b6e725d1a71409d05da002642cb65afb17107b8c4893a67f8520ea8e384697894ef1c4c71a27507b48d139d71405b38a1ed8026fbfdc009dd86890e4ec8d2a031116a90b058e1848c0831871cb0501be2aa02ade204d826e7ba3db718427cb98a739d1729ae4b70d82148c309b71c764586aac753d6dc9085800d4e88cae1d0473786d70f2c0ba02ac6e0a8ca5223ee735177b01a225c20acf7046c8db7d13a2639e01399525354371d4a5a50460a5cf0c02712a5b6b8495f4e80ba46a94d95f569d61d91f28b1193f21427c7a608b219c0c03d12c0ccb45e8dd9a444139657323b1b3179351688263f5d10233803eb40eba703a186d071d10c91a4d34c361196bb0c85a6264414df067d7afaf5f98093683a04dd557485bb2a78c58e3d0ac66e312bb4954d88609484692516c6d82bbea8abc69f4e27e13c95ca88cc4d341d862fc01775c9098ac6afe809ff20df3817bfd21b56222760d2c374b4adfeb2c2741d45e3318ff7cb64fdb58ca4b4d137a8b24e0506988a06259d4c88b7d13b50649bcee685a54e301d65f1d866773a31aefc5b016cce872370dc35cae02cf0f40bb3b50e8fc731b569ae82d480126bd6145a31df3370c292971d44e3b5406a203d5e8de3b38efa2058a51e8fc762225582ecbc49d09f4da4de61d0ce4fe544f13e6e9a7f84b4a9d0b6f13b9b34e742a2a09ddf0feee949ec8c0c75a64d2d9f78a70c674d9d2e02b7bf234ee99a03d79bed358013233a8c84cf7e3998241094ac57cadf8ff4f3e66b837f051800dd4b1284b44b8df00000000049454e44ae426082', true); +-- INSERT INTO public.license_label (label_id, label, title, icon, is_extended) VALUES (26, 'ReD', 'Redeposit Modified', NULL, true); +-- INSERT INTO public.license_label (label_id, label, title, icon, is_extended) VALUES (27, 'ND', 'No Derivative Works', '\x89504e470d0a1a0a0000000d4948445200000040000000400806000000aa6971de0000001974455874536f6674776172650041646f626520496d616765526561647971c9653c000003bd4944415478daec5be191b230100dcefdc70eb003b502ec403bd00eb403ed403bd00e382b002b800ef0ab00ad802f8f89339e9784040910bc37b3a33348645f369bcdeee210b3f0a84c98cca88c98c89050b95189d877c83f6211a0f49e4a4c25af496236e6b8cb8a2fa984352a2d2363d925c57d2a69038abf4ada36115e43335e26611b4b03cc671d50fe59b6551471347fef5239505929dfe0ba64369b91c964527c8e46a34278b85eaf85244942a2282ae47ebfeb3c1f768e0595bb895977753cfb72b9cc8320c8df05c6c05894481d273936b1de6355c5d334cdeb469665f976bb552522ab93045765bdfbbe6f44711e1120b929124acd1e33723c1ef3a61186a18a35bc4d825479cff3f2388ef3b6006b188fc72a3ec1ada2fc5e3630fe180fd036149744a8abfcdc06e59fa140c2b616a7d745e5354850f2074799c3ebaaf28a3ea17429f83206db74783a2494ec0ed20394f0708320c416608b2c3945eacd3eccca3694f803ae1504a21bc0a86d28590a312fd617c6f6b602cb567547588b7ed8447cdf921520d023034600f77c4f0f38c2b3bb0d180e8764b158882e2f1e04b82c6dfd0babd58ad80e09019859cf6161ef37ef17d4840a1675806cce66b331a6d0e17028b24bba9620c82cadbe44b30ff3d7551eb8dd6ee472b9182300e3eb02a9b8f3f9ccb58201abd8706fea0b24bacc40c0b0ef0448964c6101dcab367b7f0d5d46830a37f58980a22e90f32ed03882f4098ec32f810cc887e38f804f27e0abee0151db3b9d4ec61e18e179dd0ebad653604936e66da9929b802ea2f1b00412d14cf605125dae20801b5ca334dd17e080262320d2bcc93a482633112e01c1e9a97704382c21c25d064110c8120ac2e3aa49ebc1c146e7988e67994ea7b2a448016e15783e9fe7b643921eff511ff8f8a4e8b7c84676bb9db56b1fe9334993d5af684d5818b1a126a839fbdcf298b03486fe1fdbb05eaf2b154885c5d1fd7edffbe2a8d40ac88794c7813e3748c42aced3da1619c42da4861619c0aa2629c54e31ed66ead236b92e04498abd8261d5582220255da26d364fc0292b289f928a8d920f7f50da24dd46ff10b6e5265a659549c04c34610d98750466a4858e71a57679786213f102fc8d629778edca3f9310a8262f314be8227f67b7c0bd184371c69ff77a4fb9625481086c275a4744ba3c8a6a3304c90c51e519999b474205df2bf419e08487ee0c23afccbc86cc5d7a692a63398d46e196c50a0d49a063f2a6aca18df70743f6df9d81afe324df9cf14e29fe0a8fadc7ba5f9e5e9b3075a7013f0197ff787d7e48042d39cf996cf2f3f5f9c8a457ff2fc0009a2c4cd8368e16c80000000049454e44ae426082', true); +-- INSERT INTO public.license_label (label_id, label, title, icon, is_extended) VALUES (28, 'Inf', 'Inform Before Use', NULL, true); +-- INSERT INTO public.license_label (label_id, label, title, icon, is_extended) VALUES (29, 'CC', 'Distributed under Creative Commons', '\x89504e470d0a1a0a0000000d4948445200000040000000400806000000aa6971de0000001974455874536f6674776172650041646f626520496d616765526561647971c9653c000004404944415478daec5b8b919b3010c5d700d701ee00776077e074e074e04905a403d2814ba004ec0a7007beabc07605448f810bc348bb2b217ee7eccc4eee2e42b0ff9fb40a868548e14ee1a6c6758d145c153e149eeb9f819fc1822056982abc292c3d6151ef19cd99f083c2dc23d126cceb77cd06f69ea52dc5dbd48c884692b84423e229d4fd3e03e2db98b810b2b25c1f2afca3f0a7f881300c76bb5db0d96caa7fd7eb75853af8f8f8a8f07abd06e7f3b9c2e7f369f37d881c3f143e87907a587b6391440e8743996559d917b007f6b2f40df124c42b6997499294f7fbbdf40dd8137be31d0226dc7d324144fc7ebf2f6fb75b39348011428df0c204967848c487aadb429ee7126de8cd0432ccc5713c8ad4296dc037087c42e8427cca113f84adbb80c0247297ec6e11c45b3021b1b1fbfb9288b76042dc4bf5a3289a2df10d303e81358588e2605114e5dc010262a2c3c1c9eb2309590a20443251c14efa50aba501e30fb45a70323d008e2e0d909f10a690eb3cbf76f176bb2d970a305b420ba26e7dff6da4dfd6028201c73603b2beb60fef9ba669a531baf08962495a3380e9b0613ca7d34848569a8213bea0683340bbe8743a895e8275c232b562aa299c82893a06f6894c603ab14755236c4d0b24498f65b3e2ab82ec3201bf4b99d86dbc70403c8f94bfca919d9c1f54deb58707621b3506a375ea2ec5e3f1487e27cc8faa0f3217f5621c4cf552ec818f3349166b005843a5dfd807489907e51388685085436dc383735826d5d7a977b76e6fb7ce8036ea0d7fd3d5542e5211996111b8e6fd26a99a1c67a3e6dd9e6197a0b6e429894a08176aabfe3f2800735c9eb3d124f8179f60fade379756d1e3f1d0fe5d49c57a2fcc01748039c218f016cc14dedfdf5f9b01262d7b19068c056f3ed5f372b99092d3d9bb692fcc055d7c870b780d83540285d085f0d60e95a66c92ea3f36711d114452147161d07b22a48bcfdd6c0c04e21dd4c7e912215dcd807554ddc2254283a6c240539edfe4f054418567b10efb504ca73481a85972633124e905301d17b62c6ea4c6b4af58e4ca7682c1c924e5b06eb8e25a0e4b7a16c4be7b2f0d11a898f4e3297b0513a40d119886a416a0d2f6765f70f09698d463732d31f816a9606c5a62dfb229ca4c89d2ff6df1ef3c1861a45f98cef85a3727e60a549b8d1a90bef47094d482a59882a0c3cc9e2f4e6dfaf90b3b2051fc3f222384ed2b1f926a20719df18d6df382f439776d9664dc786b4ac70801080e4a16ae0725c5e7841173c73609e15c729cf3c2ed0ecf18315e20f5694e8c37b5c3108c00e116670706bb3390491b15cd1cb0cf616a3c0b55b71c9d5bd9fcca8111880ebfad6e5745d1d7b519205ae1ddd157fbba4c7365e6f3d3fabe24aef3fc1a639eb00da6b92a47d9fb7eeca14ac81da91f09d33e61ce074436bec12366c1ccaed2465453c5a3aa9f8299df210eeb9233f32cedc310aabe1ac961ee827fd7e7b9930f988a62c27aad7fbe0cf9717f051800f651ef6054e44bf60000000049454e44ae426082', true); +-- INSERT INTO public.license_label (label_id, label, title, icon, is_extended) VALUES (30, 'ZERO', 'No Copyright', '\x89504e470d0a1a0a0000000d4948445200000040000000400806000000aa6971de0000001974455874536f6674776172650041646f626520496d616765526561647971c9653c000004704944415478daec5b8d71ea300c362c001b840dc206b0016f03d880be09e04d001bd04e001b0426081b00130426c8f39726776d706cc9719cf44777baf6dac489e44f3f96949e689602c953c9e39c4739ebe82cf92ef998ff0ebe892f44a1e48de48be4d411c7f99a4197059fe72f9a36cc71feacced0ccf16e53f9d2b62200c7a805c1cb1ce566e7959692930e08ff91573682f498d70f246f252fc8370c06623a9d663c1e8fc56834ca5845f7fb5d9ccfe78c8fc763c68fc783f37e881c7f243f9ad8f501c7c9cde7f374bfdfa775298ee36c2da9488e6f085b111e2fb95aadd2244952d78435b1365111894b2590849fcd66e9e572499b26280288f0a504a3f0d8111750e75214451434d4568236cc8561e865d77568984c2614250c6c84df98846fc2d66d88601231370c22bb3b5426fb619885a8e170c8d66a11e6aed7eba7bf2334224c826d68b15888b7b737dd256bc9ffa8769fb8dcf9c283074160745eb8c63692109010d6823e5e8efb62705614c155cfc2bd5c32f884887294ad5c0049098776bb5ded14176b70d166880e732baf0f58fa16de5609408e2153e4ed3eec9e43088d8cd435a56498dc706bf0074a14ecaa6ee0da222136b3196b3a348548e5f92b535c87f0fbb4ab100a4c450b772360b69af5827239cbc9432979fa66b3b1f217589b6b8ad4fac1de15ecb8c27351c30dc39a0df9941d3af1be38149962bb2d6a0ae61ebc0cef949d1126551770b5bd5c2ed9e1cc00d327c633b8a4f131b37edeb87822097f76ae8f1c5f47288b95e9e5e5c5e933a8cfcd695c69ffdcc407c4853fd5f6cbcc254d3488fa55ad2a8dd62a0b9a3a521542d7ebf573ad3d0832f4d57916439661bf8001e5855dc2ff703888d3e9f474ddebebabf148cc35038d2ce3bec54d4e4865fbd87928caa6ce60a900d16fa3a5845dbedd6ecabffb26ef0a80fdaa765f86b7c651d709056cb7dba76e0fba472a87f8ed1480dd8702544a716df79d5400a05fde7d145851d06c8bfa2e332e53e852556d55886802793a8a5d1c3a2827416aadc15450b129cc569d088180ab0b04706d188eaf6af7cb3d83bacfd2c872870294ff45e3834ba614b6ec0f54610fc2ab72049b67101470ece743054f8434959b7353bb3ac8f7ab4e8126c5db748e346b9eb5f540d70511cabaa6260ad737a19741a90bee5d94c3a98eb08a502e33dddb5449cc6b515455dca408cf2d8a1a4ae31b52599c5b18fdaa65f19fd61889bf7d6bcc50a09dff36476ddae35c53f88aed71ed800416e6ce08f81e9080b90a8b59a11f3b22f33b2465630a0512b8e6d0d4aca001f6a4d9207654102d4e897eccf30966150bcb41c9c21f18e78411737d0f4d1ab23cbff3c285f3f28106440602e4db99182f52d6261401c119297663df0c449c388e135e9d616adc8b35987904cbe67b168a40746035f551012a3e9901237c96c31d4263f993195d69acaaef22f9af8f72fa4474eba3a924cf5dbcd2c0942b78e24d9d30e78242d1cef7833bd1b14f6927baa28a43a8774e7095692c1da3629f1f639d43bde7411953f13e86839f43513192f3b18c2fde3f9f3fe7bf9f9a7cc1ff020c00b8b38128c9ee68580000000049454e44ae426082', true); +-- INSERT INTO public.license_label (label_id, label, title, icon, is_extended) VALUES (31, 'GPLv3', 'GNU General Public License, version 3.0', '\x89504e470d0a1a0a0000000d4948445200000020000000200804000000d973b27f00000002624b47440000aa8d2332000000097048597300000dd700000dd70142289b78000000097670416700000020000000200087fa9c9d000004e54944415448c77dd5d96fd4e715c6f1cffc663cf6d8c62bdb00b62103b6716d106002c62d04255237acd2566aa4a4adaaaa7f01bd6f2e2ab554b9897a59a98bbaa4a9aa4895825a2ab2101a08e0408bcd6a8cc0061b30d878b03d335edf5ec47128697bee5ee93ccf7b2ecef93e319fad948c1d766b55af0c53065d74da3937e49f6e8e3df54edae280176c2ca95a96ac88a550900d9333f971fdde76448f99ff6dd0e045df4936ae4feeb04ba3554a91735f9fb33e726b66a6cfeffcc9c07f995b64bbd763d94c38144e848761213c59f3e16138110e854c8865fdd176d167e5fbbc9b9a3b108e85a9104208e3e166980e21845008f7c26c082184c9702c7485d49c77edfbc422be68b0cd4fcaf67e3b7e48ad01b7dcf077d76d95c4a85f199675cd800acf125dab9fdda0c7dd4f0d1abc92facacbf1970cba6741d68831ad9a4448b8e5b23c4a64ddd72219f5d5cf56f948f66383623f88beffa5d4cbee6ab6d99871092d3a9480847a299139799b640c5a2b1fdda80fe3bacdc7b1c38f36d6fdd0a45669c7957ad656eb8cba8f94bcac5a2d9a95f8a7159adcd2e272f168da194309a5ba8a377d5ddab08dde93b65791111f98526456ad017129e53ab549e8f6452b257ccdcf374d77b918d9e8f90dc903a6a4e58c6b5364c43155ba1cd4e18cb7e475a8f08e7119a5ee5b6ece01cf243d6f5364672cb3539359c50a22298f9d50a3d5843beea8d065c40519291f9a9534a3d88c26ed6219ed091da9aa5d6a149b9211f30f13c6f03783266c96f5c88c537a341bf15793b67a28526d9737ab721d91b665c94609351e48f902a62da8b5c6944885981171b3e28262cbecb4ca5d958a34aa48da12a9abb20a0d726eaab75ba443a7325f96f607539ed3ac47c16e9126ad068d6a14b35a257509e5a54a516dab0f4d7bac588b22cbcd98d4eff3f61b326f5cad3addeeb96a9b954829a53c211613434cab22175cf1821279efd9e0255b3ca34a95ef7acb294dceb8e29b368bf0b12e21972fcf2f9e458b6a8fad5570ce80061b3478c7512dda5df0a62679ab6416f73f2f4f2e61687ce5c8e245cdeb35eb9ae386bc68a531311fe8356ebd2e77641db47671c17920cb50c2a5c76dfd89fd62c8b9ec913b7698b14ea54bc61cb05ed6468d6af46b5e9207fdb2732ec5a5e73b57a69e5382b80ddae4b47b24618d65ae88f9aa6675fa1cb5c7ba25843df66bdde3e1b791b30b37bb5d5fbcbbb4b4245af51853a6dd4dbd265cd5a7a0ec0902f63b6be196eec835ef5f9f3ba2b044d56a431a553969da1abb5df5be1b46d4492fc90b8eb83ee7b8ab7133a6e7f68dd6ec54b748d9b80b1ad4396dcc6aab4dbbeca14a1dd62f1974fb99e19b5ed517c703d5a3edb9a27655609951d7345af08631ad92068df886e625920efaa9e3b9f04b6f988d63d670681cd81462db9522b2ca0391352eeab55acca0727b942eca47bde6f55078dbabee7ec2c45143d36d97d2d3b1269548aa57adcc8471a79d7756a7560970db6b7e11b2e7bce2fc93541e325c68ea4d0fc7d659212e2e6e415cb911ff92b4dc7a95669c77d8ef43f6bc1f7bd7c27f2643c27e47e3b96de170e809938b81321d2e87a3a127dc0e63a1271c0edb4222efa8fd8be33c156d9166dff3adf2bacdd11ebb64ac508c8231fd4e3be5cac2e46d7ff61b573efdfde9702db7c7417be375e5e53551ad32e48c1a5b98989cbfed84bf3865f2ffa533546bd3a9d3e7ac5682827b2e39e9a45e8f9e6efe379cddf27e00b2b0830000002574455874646174653a63726561746500323031302d30312d31395432313a33353a34372d30373a3030be1641b70000002574455874646174653a6d6f6469667900323031302d30312d31395432313a33353a34372d30373a3030cf4bf90b0000001974455874536f667477617265007777772e696e6b73636170652e6f72679bee3c1a0000000049454e44ae426082', true); +-- INSERT INTO public.license_label (label_id, label, title, icon, is_extended) VALUES (32, 'GPLv2', 'GNU General Public License, version 2.0', '\x89504e470d0a1a0a0000000d4948445200000020000000200804000000d973b27f00000002624b47440000aa8d2332000000097048597300000dd700000dd70142289b78000000097670416700000020000000200087fa9c9d000003fc4944415448c795955b6c936518c77f5ffbf1ad5f5b4ab7216c7127d846066c333bb1c112173c5c6036b30b41259198188877265c301363d44412d42c9a70a131124d8c12e38d0968c8300b07290293c8d898e28cb08aa056a05bd76e6bfbfdbdd8a97420f37ff5bec9f3ffbfcff3bccfc160216cca69a0856a4af001e38c30c0f7fcc0af24b28d8dacbb452ded3c468527b8d40a1836304154b1a9c46d86f996c3f433756f81529ee6396b4d99d540336b58891788f32797394b1f57a6a62ef3295f70f52e7ee3a29ecf8d68b976eb84224a2b1369457442bb552e23ca41ea712da4b7d16ba7dad5a398ee85987ad42e3b452f6dd912f5f4fa523b352447ff054743da295f8a5eeaef8cfda09ddaa5b01683b076c94e7190d2597a0e5dae68bb86164597a42175c815e56572a605363050a1a392d29ad0c47d8298c651558801368089978e9cca4e9af9895e463058c5a3acc6e11c3f22c060394d94f2373d8c524f236e36d2c9fecac90e06a09650954eeaa41e91572eb9e4d3365dd6a45e915fb63cf22857cfe8aace6b9d7c7a559392a4ef5425423ce4a2c9286f229ffd1c23cd2a0a18e710874892244e9a62f2b8cd371cc76182384904c05a9a30ca6974b1d10eb630cc310c9ee233dea3854a441a8002dee16d0a89f133498c8cc20dd28c1d64a3494dc05acd056e91cfb3345343094b28c102600945801fcdbd3c5f796b0858f15a93e220b9fc41927ccab8ce216240218f03708b8f8810268722ccace22d6019378a4dfc5e3c248125588cf016bf63d04c13060637f91007833a5a21cb071b2ff84d0c03132f0609e2f8584b8a3053081036d5047990add47261a67d67f3307d3289c7fd694ab08870894edee563bae75e59c1ebd4e263d95cef4c328a05e490200171936bb757dca48142aef03e13388448ce0998145294313c1c8e700d03832dac6414ae990c8ed6fc626e650b0738493f0e637858871f0787344e46d40e70894b002ca792688a4193d38927cee46de32552f410c5621d1dbc401eb994513cf39dd3d3ae684ece8d451f89514e1b34f2416dc3011af98b8bdcc0a2942a0238fc46180feb593a431a63706ea6bab9ce5e2e9ee745584ab79d7c4d894537b3244dea0dd949baa7d5db18ae56e87f0984542d86699bf6c7cb9beef1edbab268fa556d977b9cbd7867f353c5e180b3479145d123daa380c3d754656e8736ce059d2e8ddc973ea22e051dced176e74e31e9a02fe0ecd0d9998171f7d49dd50e051cfa7872416f61b29923ee789df6a9ff2ebb21a67eed539dcc0447d83c4fcf74c34515cfb3cd5fbcd6b58966ca79801c609208c39c21c490130bf3259f30345fa0d9cbd5cf263a79d85decf7e7b9f2f10171fee1a633164b8739c15784886512b2050072a9a19556d653800798e006839ce21417b9956dfc2f6ec53a03c4e284690000002574455874646174653a63726561746500323031302d30312d31395432313a33353a34372d30373a3030be1641b70000002574455874646174653a6d6f6469667900323031302d30312d31395432313a33353a34372d30373a3030cf4bf90b0000001974455874536f667477617265007777772e696e6b73636170652e6f72679bee3c1a0000000049454e44ae426082', true); +-- INSERT INTO public.license_label (label_id, label, title, icon, is_extended) VALUES (33, 'BSD', 'BSD', '\x89504e470d0a1a0a0000000d4948445200000020000000200806000000737a7af400000006624b4744000000000000f943bb7f000000097048597300000dd700000dd70142289b78000000097670416700000020000000200087fa9c9d000008c34944415458c3ad976b6c54651ac77f67ce9999ceadf442e96d5aa8564a0b95562a140a5b16f012244182bb62a29b266b4cc06c4282f1839744dd88624cc028f10388ae06814d40651502ac5c1a2d852d5b7a675aea3230b45c5a3a6da7a773aeef7ee8880501d1f84fce87c9799ff7ffbccf7f9ee7fc5f89bb4312300528036603d3817c2090783f04848136e024703af15bfba58da55f78ef4a902d0596005325494a73b95c2eb7dbed703a9d00188681a669b6aeeb9a10e21ad0091c02f601ed80f15b12c803fe043c0314a7a5a5b90b0b0b99397326454545e4e6e6e2f7fb0188c56244221142a1104d4d4d747575118d46e309f24f81dd40e42eab8d049403db81684a4a8a78ecb1c7c4b66ddb447b7bbb181c1c149665899b619aa68846a3a2b5b5556cd9b2453cfae8a32239395900d7807f00f7df45c57100d5c0b7b22c9ba5a5a5e2830f3e10172e5c10b66d8bbb856559221c0e8b4d9b3689929212e170380ce0205095e0b82d2a8023b22c9b8b172f16070f1e149aa6dd35f1cd88c7e362dfbe7da2baba5a381c0e339144d9edc80b807fcab26c2e59b244343434fcaa53df0eb66d8bfafa7ab170e14221cbb2999036ff66f224e06560a8bcbc5c1c3e7cf877211f2fc9810307446969a900a2c08b807b7c02f3808ef4f474b175eb56a11b86d08787457f73b3e8adad15d1ce4e61e9fa0d9b6ad1a818ecea12b1484458e364b24d538c5ebd2a62918888f7f509db34c7d66b9af8f0c30f456a6aaa005a80070164c00f3ce77038962e5bb64c5ebb762d6e4da3f5fdf7697ceb2dce7efe39bdb5b5b8fc7e92efbb0f87a230d0d141d33befd0b67933170f1d428b46094c9e8ce2f5d273f428a713713d478ea00d0ce0cdcec6939a4a4e4e0e6d6d6d8442a114a00f382127dae36f99999979eb5e78818ab232ba3efb8cc6b7dfc6d675dca9a944cf9c61a0ad8d8c59b370280aa75e7f9dce4f3f451f1e66f8dc397a8f1dc39391417261218d6fbec9b92fbfc4181961b0bb9bc8a143a8910869a5a5a4e7e763db36b5b5b5b2aaaa2ee0b80cac0056cc9933c7bb7af56a92749da677dfc588c598bd7e3d25ab57234912178f1c21909f8fa569b46ddeccc4b23266bdfa2a5955554892c4c4f2723c93267166eb567cc120951b3690f7f0c318b118170e1c0020abb2125f6a2ac78f1f271c0ebb81330a50a528ca848a071f243b3b9b584707b17098896565041f7984a4f474262f5bc6d99d3b19eaee0649c25455a6ac58c1bdab56212c8bc2279f44f1f918b978115bd7f1e7e793fd873fe00c04489f3993132fbec80f7bf6107ce82172abaa78e08107f8eebbef522ccb9ae70066783c1ea5a4b818b7cb85353a8a6518b8d2d2509292003055156159385c2e1c4e274208b4fe7e2c4d439265dce9e9c889b54863c34ed83600130a0b995a53831d8fd373f4282e59a6b8a404b7dbad00331420cfebf59293939398850e2460b8bb9bf0d75f234912ddbb7661eb3aa9d3a7935a5282272383b33b76907ccf3d4c59b10267e29b70ebc12e91316b16818202fa9b9ab0464608e6e6e2f57a255555f315c0e776bbf1f97c37c4f57eff3dfd2d2d0821d0a351148f07637898d4e2620a57ada279e3464ebefc32c3e130453535f882c1dbe6e09a30015f5e1e433ffc80118be1f7fb71bbdd007e05902449424a940e21c626535a1a29d3a681c381adeb0c767612dab68d8cd9b32979fe792459a6e3a38f38bd610303eded94bff4d275c97e56048703d9ed469826c2b2aecb04480aa06a9a96acaaeaf5000164575753f1da6b285e2fb661d0bd6b17cd1b377261ff7e32e7cca174ed5a528a8b69d9b489735f7d859294c4b4679f1dbff975589a8676ed1a8acf879c9484aaaa188601a03a801e5555b974e9d20d41ce40007f5e1efefc7c92efbd9729cb97e30b06897674a00f0de10c042858b992aaf7de236bde3c22df7e4b7f53d34f951c87d8850b0c9d3d4b7241018adf4f6f4f0f8903471c40dbe8e8a8150a85304cf386409190034076bb915d2e6cc3c0d67510024992482f2b23b3aa0a5355517b7a7e8a492462c5e39cdfb78f787f3f59f3e661c932674221e2f1b809b42b409d61188b1b1a1a52ae5cb982fbc736b22c6c5d1f7b4c93aba74ea1f6f6e20b06e9deb50bcfa449e42c5a84158f136d6f4752149cc9c9489284adeb98b118b6ae73fe9b6f087dfc3169a5a5e42e5ac4e5ab57f9efa95398a63904d429c071e07c737373cae9a62666a7a7230941dfa95334ae5f8fc3e5c254557a8f1dc3d234264c9dcad91d3b180e87c9acac0449a2f7d831d267ce24b5a40480bec6464ebef20ae6c80897ebea507c3ea6af59837fca14febd772fadadad00e780130a63beadb6b7b777fa9eddbbe569cf3c833310e0eae9d30c7474dcf09fb8efa9a728aaa9c1150810fae41322870f836d13282860fa9a35a4cd9881272b8b685717ffdbb307d9e36162793925cf3d47fed2a5f40f0cb07bf76e2e5fbe6c02478090c4984f5b046cc9c9c92978e78d3798e570303096251220290a29c5c504972cc1170c620c0fd3d7d8487f5313b6ae935e5e4ed6dcb9488ac2e5fa7a063b3b9180a48c0cd2eebf1f7f5e1e36b063c70ed6ad5bc7952b57ce027f056a7f3ca00fd800a80b162c1027ebeb85a969d71f4bd384b88541b12d4bd8b730a8c2b67fb6beaeae4e5456560a6004f83be0bdb95b4a80fd4ea7d37ee28927c49950e87773446d6d6d62f9f2e54251141bd80b14dd7260017f041a5c2e97bd72e54a71e2c40961261ccd6f816118a2aeae4e3cfef8e3c2e974da403d309f3bd87305580e34389d4e7bfefcf962e7ce9da2bfbfff5793f7f5f589eddbb78bb973e78e275f9ae0b823146021b01f50b3b3b3454d4d8dd8bb77af387ffefc1d6d7a3c1e17e170587cf1c517e2e9a79f169999993f6afe2f60016316f06765bf151c099dfe02fc5996e5c9d9d9d97269692915151514151591959585cfe7430881aaaaf4f4f4100a85686868a0a5a5854b972e99b66d9f0376019f31765f14779bc0f8ee989390a51ac8773a9d018fc7a378bd5edc6ef7d8e75ad7191919617474d44c4cb8307014f80af80fa0de8ee017ef6a094c4874492563167e3a9093489044992f02adc0f709bdcf30766dbf23fe0f708f107b7e1ffe120000002574455874646174653a63726561746500323031302d30312d31395432313a33353a34372d30373a3030be1641b70000002574455874646174653a6d6f6469667900323031302d30312d31395432313a33353a34372d30373a3030cf4bf90b0000001974455874536f667477617265007777772e696e6b73636170652e6f72679bee3c1a0000000049454e44ae426082', true); +-- INSERT INTO public.license_label (label_id, label, title, icon, is_extended) VALUES (34, 'MIT', 'The MIT License', '\x89504e470d0a1a0a0000000d4948445200000020000000200804000000d973b27f00000002624b47440000aa8d2332000000097048597300000dd700000dd70142289b78000000097670416700000020000000200087fa9c9d000003c64944415448c78d954b4c547714c67ff7313338033e50288332764420bcaa54a882582498b4b18d34e96bd336eebaedaa9b2ebbeaca2e9a74d1b44935b4606a1b6da2a98dad8b1651d0d80a54c40a835625508b3c8619e672bf2e18601844f856ff7f72beef7fce3de77cd76039327896ddbc403921b2807122f47085eb4488a7071b69772fe51ce610c546b6d7eb333d4082b83b13d7236ef13367e925b1b240016ff22ea5d9be9deca284ad640293dca38f3fe8672c462fc739c5bd27e48d41152d8c6dd42bfa4abd7aac59cdc3d198baf5855ed67af188af796e59e6983470c1722af599eecad59330ab883e5599cc04e7d98fb954a09a5f2da749e715d7d310d35935c87438cfee547a98939673485d2bbc9d0a571d3a28cba185d062db3e62bc4abfa454fd34ccea27558a313ec4372750c75f9bf5a5126ba24bd28c3ed72671831ab0c9a4d92c6ce200bdccb09e301e66b9cb2826213631c41821e23c48a9d8623f0d9c2ed6117aa09ace3c7da78bdaa7b08e6850d288de5758656ad5a43e50a5bed731ed54583b54a81d0aab426dfa4639e212bb4cf6122ea79a69220c70950830c2550618601c976106984c4edc287ff3180303931acaa0901a93fdf6861af27031b099e036628861ec94465bbc4d1bdff22a3e8e7292e33492cff3581ba9b3a9586797e2034cf298a68f1803c4d8c67f2955070932432e0605ec014094e2b3a31526057ef2175621877e46e9c34b51faa8e122402839f95bf11b844c023e02c9902d6ce32e77b84d2e5b93812b23131f649a1846723344163b19a5930861b2571530000c93689c6852c04b11312e3042117e56439404444dee4779b8a0598887df48508c7715ba784014ee99f44ccff62d98cc367298248b10c62a25c4b949cca1d7a43d31d1c50806426c613b104c7e422d7b7511c35cc319a7dde412437f721d8b0c3c0428c14f21d958f8b0012f1958c9023d0b67b846370c72d96282c2a93d5eb391626a292687325ea2183fa5d492472655d4b11930f053493d4160946374386ae51418347127a83639493b993795f9dba2c9cc9f133aa15cd1cf8b73d904f88468bdaeacd10d5cb56b9f98e2e3c55e9771cee3bea19b6b12e851b36c973394a40e55235d5ef7755d96f3547242ed7a4d1e970eea975abb4d335d1eb75eadfa7745faa85a543b473f8c9d3e193607394734a8a33aa3a1347b8f29a21ff48e9e1153fcc881856e2e49c3a484f778cbda1eb42aa9a6843c028828f7e9a38b1b3c74dc41da38c1adc5994aff4505d84b330d843c59eb6c3f3ec40c534c3bce38112e729acee4eead2000b08132f6514739f9048029fea19bdfe9e026e3e9c1ff030b55eb0b90e9adfb0000002574455874646174653a63726561746500323031302d30312d31395432313a33353a34372d30373a3030be1641b70000002574455874646174653a6d6f6469667900323031302d30312d31395432313a33353a34372d30373a3030cf4bf90b0000001974455874536f667477617265007777772e696e6b73636170652e6f72679bee3c1a0000000049454e44ae426082', true); +-- INSERT INTO public.license_label (label_id, label, title, icon, is_extended) VALUES (35, 'OSI', 'The Open Source Initiative ', '\x89504e470d0a1a0a0000000d4948445200000028000000240806000000fb3c781600000424694343504943432050726f66696c65000038118555df6fdb54143e896f52a4163f205847878ac5af55535bb91b1aadc6064993a5ed4a16a5e9d82a24e43a3789a91b07dbe9b6aa4f7b813706fc0140d9030f483c210d06627bd9f6c0b44953872aaa49487be8c40f2126ed0555e1bb76622753c45cf5facb39df39e73be75edb443d5f69b59a19558896abae9dcf249593a716949e4d8ad2b3d44b03d4abe94e2d91cbcd122ec115f7ceebe11d8a08cbed91eefe4ef623bf7a8bdcd189224fc06e151d7d19f80c51ccd46bb64b14bf07fbf869b706dcf31cf0d3360402ab02977d9c1278d1c7273d4e213f098ed02aeb15ad08bc063cbcd8662fb7615f0318c893e1556e1bba226691b3ad926172cfe12f8f71b731ff0f2e9b75f4ec5d8358fb9ca5b963b80f89de2bf654be893fd7b5f41cf04bb05fafb949617f05f88ffad27c02781f51f4a9927d74dee7475f5fad14de06de057bd170a70b4dfb6a75317b1c18b1d1f525eb98c82338d7756712b3a41780ef56f8b4d863e891a8c85369e061e0c14a7daa995f9a7156e684ddcbb35a99cc02238f64bfa7cde48007803fb4adbca805cdd23a3733a216f24b576b6eaea941daae9a59510bfd32993b5e8fc8296dbb95c2941fcb0eba76a119cb164ac6d1e9267fad667a6711dad805bb9e17da909fddd2ec74061879d83fbc3a2fe6061cdb5dd45262b6a3c047e84444234e162d62d5a94a3ba4509e3294c4bd46363c2532c88485c3cb6131224fd2126cdd79398fe3c7848cb217bd2da251a53bc7af70bfc9b1583f53d901fc1f62b3ec301b6713a4b037d89bec084bc13ac10e050a726d3a152ad37d28f5f3bc4f7554163a4e50f2fc407d288851ced9ef1afacd8ffe869ab04b2bf4234fd031345bed13200713687537d23ea76b6b3fec0e3cca06bbf8ceedbe6b6b74fc71f38ddd8b6dc736b06ec6b6c2f8d8afb12dfc6d52023b607a8a96a1caf076c20978231d3d5c01d3250deb6fe059d0da52dab1a3a5eaf981d02326c13fc83eccd2b9e1d0aafea2fea96ea85fa817d4df3b6a84193ba6247d2a7d2bfd287d277d2ffd4c8a7459ba22fd245d95be912e0539bbefbd9f25d87baf5f6113dd8a5d68d56b9f3527534eca7be417e594fcbcfcb23c1bb014b95f1e93a7e4bdf0ec09f6cd0cfd1dbd18740ab36a4db57b2df10418340f25069d06d7c654c584ab741659db9f93a65236c8c6d8f423a7765c9ce5968a783a9e8a274889ef8b4fc4c7e23302b79ebcf85ef826b0a603f5fe9313303a3ae06d2c4c25c833e29d3a715645f40a749bd0cd5d7e06df0ea249ab76d636ca1557d9afaaaf29097ccab8325dd5478715cd3415cfe5283677b8bdc28ba324be83228ee841defbbe4576dd0c6dee5b4487ffc23beb56685ba8137ded10f5bf1ada86f04e7ce633a28b07f5babde2e7a348e40691533ab0dffb1de94be2dd74b7d17880f755cf27443b1f371affae371a3b5f22ff16d165f33f590071786c1b2b13000000097048597300000b1300000b1301009a9c18000009ae494441545809bd586b6c14d715be33b3b3e3b5bd7eadbd181b5c1b080e71e40670c02950d92502d407aa9ada4514aaa4519b1f55faab9192082946156afab351aa4a11ea9f2a4de36daba0264da9a80c22d084d8450d260a8ff0f6738d5fbb5eefceeeccf4fbeeceac97e00527917ae19b7be7de73cffdceb9e79eb96bc5711cf1658b82021d8ae8025822824aa1facb2bf749855fe021497509b5a3a543018f0c09e5ab5184a2741eecf41d3f7fdc717a1d2b7fec73b569e4e7053ca5810e89e5e662d12220e4c27fc7584f8fca39f97d4b6dcb45966a91f45a4787e6f4f5d163f091b2b676cbcadd465d608b1ed49b145d2dc3263b76ca9a4e4f9b9793c38913e31f0cff0d646e4af94ec5e7f4496f2f75c9ac1796222dc9f540fe25c7467b79fd8ea603256b82fb8b9b8241bdc210aa5f25e3ac2adb11b6690973322512576627e29762af8df45dff3588ce2adddd9ad3dbbbe42d5f9207f3c95534847684b6d71eaedc58b3d257ee27215bee33482d1410055fa182b12354732229a63f8c0e8ebd71e3c939676e40e95640726971b93482aed5a1967077a863f99b559bc242f1a9a695b27c20471a8b165076609c0defdab669ebb74f8dce8ebd33b43b36327542e9ecc476674365d1c96ee77d4fb1e2c64d5959d9b6dabd8daf576d0639454959c98c819a5e5ad0ef39d1ed42450f6a56d2d234434b556fad2d83df2298b70ddb7dc1d3bda0e0eed63d0942114f5e067569e39ee6572b37857d60675aa6652824464292144f33daf0965cc2f6bc9a654a5978db500dcd0c3d16ae494fa57e03b95d3c30d0ad628dec3c39f9ce0723a570e9ea9263cbdaeb7f52fe48a8155b65d9694bcf9193a3c8379a6aa97ecdd6fcaa2a616856962c5893232b781bdbec53033e51de5ab533f46038abbca3e39e1c0a7ad0f59e853ab0faa987f617d5160bc7b26daca72d58849535d58607b4d45042a4a2f3297ac4a82dd68d9a22f082472d9bc73bfb1ff18ab9667163d01f7ca4f249b4238c43ce29e4c5820485ebbd606570a33f1c58af68d826d3c1138b713bf92039d352a7ce44a33303d15fce9c1d3be1085dab6a0f7fabbcadfaf98af5d52542556d6183a44357222433b6c69454b4bc780b88ad05b18bb94f24d57ea6142498f5bf10a55fad6c2b0a0738cd42a069f25030e0106fd83575fabf9373970f0f3e81854ee6e93e5bac145fd47ea1bd59d65a85230c59261eda062f3262fd5545e535adb5ad9873518061a1b2e8fec33225d21b9181abe10ba1216ea03417c8e4a7fa14c74a6498885f27397e77792adbdada742e967012bd89ebf1bf3ba62d655db7b3825d8aed2bd5855e6d3452b6303d5a759fa2f89420b737bbadae30768a092e339711e9447a90bdb1e198c253d9dfdf9f6e7b264bd28aa73f468a9107449ef285b51c555785aa6b4176455a5aa88e317057b92f41613929e78eaf0474d00b7029729bd0746d25b5ce18335217170ad60565942a86af9ef1060fd36d0b85f65a0ece8f95f23a2123e778ef5ebd2841297cb047aacc2432b7ec942d905a724bb0c5057c415d041a4af780d38a4baf5c4af11306c54adf4bf2646e08d405bead16e1e263c913e2ad09c242b5126961cd646eb1b3ebfcf99cee9c90db5894a01c7327256f24ce9a9349462092b4b7d168d8b606b7d838a92b1bbbd6feb5b67d45bbfcbec2b3cb36d7ed6cdab7ee4f65ada12066f070611d5a05cd3c5c082d5c24cca933e3e7b85604ff0a95c204dd495317a3675263f3d7a840d5142ee6ea42e24d5baa5e699835dbeb1ff52f2b7a550e28a238f095e0e19aceba07b4129fc9b422c9491fc1751a8206de37a3f3ff498b6cfcba377057ef9d55618211014b116a8e3331f7e9ec9ff179e217231b29f91b623b9a359f1156c27a9baa21308793fb2eee847c4100a293f2a8691b74a8c9d184885d988d4036dd8d8b084719bba8ef2a050962b2233ab35f8de163d77f3bf3d164148745477a497b5e445ce2880a6deeca6c7cf8d8b55e4ffbe491d137e6aec5b89d3eac8ceb18c9c17b7ecdb4e1d1d9739383e3ef0fbd42f94824bbbd723d4f415e5d902065e4c79cd722c7b936f541f4f9d827d3f4808e85335c1029c8611a498e248ee3fd637a9cf3e2227e3a3592e817199903f929642e4c63c83f3d3021a64e450f34ffb83910ee6caac59805d0cf8b967b1294338e1f97b7dfdbe7467f7ffbe4d8216c37f21796d355a6082d85ed8a5f88fd45cab66df4c91bb3e3a4e6afc68ff0460dca0a3d47c3660727c5e4e9f167a6ae4e1c890f259fd574d15cf778d3aeba9dabbe5ebf63d56e6f9bbd9a3aef4b90d66182941b3d79e3c0f8d1a117a6faa342641c038b2a891bf15b20ffae24b86ac016d188f4e2c87b37df4a5c8fc561886627337e5c565313476f3d1d1d187e8db2f83cdf542d27a6aace06e4843d8e626faadedd5c2af5f4c8a895cd82df6239ea3ec091bf43e48d63f4f4cd974b2b2a3e34bf933a54dc50bad91c9dff07c6c7948318c735deb31e7d838dddcdefcd7e34b96bee72ec9f43912b2f9a8e39e0e9f5f932a72d474f238e7b612c6e14191fb282348ebf7b3c3919bc50c6777a8984595390b54cbc79e3bae811ee511646685df847b59d0d2d729c3f45b373384fcad46d6dd85ab5baea071cff8c8cfca9e1f5e7d7ee9a72be9c231faec27cc1fc362779ef68dfa5dc23e4c92c567b862d36e6f5e5ebf674a2ce79a404ed2d003fe0f4e0726055dee4b5787f98ef28654699b1a6a4b184a7307f175a30b6ce9531f40a7d7d757335f5d1ab86dbcfbb1b77ca00ca008ef3da1570c71f443becb66522c64953cad1c9df09f5c005803794fd00929938059c019e06c8ee04d00e5096e32f43d9fbd0f153b4b9a8095c06360235c03830035c825c1fe47e86f66d6083dbcf8341c2978029600dc0bf52bc05f9b3b48ce53180823b002eb01720913dc07ae057c03c700bd80790dcb3c0bf801f022c0f03f4c83b0089166181e75053df37012fd7d1630f0057011a1e73e5f8257a02f804603c7f0390ccbd3a0141a694043a483c8ad769bcf3cf1cb4e853e018f06f80060c03d78087009643000d7d0a6800fa01167a8f84653e75db49d4f432c9d3709609806dd67f0466816cc08304e3ed77400ae06492f92e701db801d0aa4e6004380774002f00edc03618f273e8f83eda9c4fef309ee9856980db3c003c0a44811500f58e02278117818b00c9519673e8eda3d07b0575ee90ac449bd6739bf6017f003601a5104425760124510990841f0801abdd717aad0b206154d28b7b50cb83867a1df035601940877807612dda7b01ae4f9ddf03b6033af5c89401eb594bad18e0a1617c35a0eb39f79d79692179b233af40fe0b8d2f611e7f5cb849341b77f2ef7a58bb09687539331e1580278d605be645b74d7268e6123bb796329c47796f9cfd04fba5cebc79520efdde3ab9bc2b3d88815c8155396fe6b77302ffe7c6ff00610e5ffd6a8201470000000049454e44ae426082', true); +-- +-- +-- -- +-- -- Data for Name: license_label_extended_mapping; Type: TABLE DATA; Schema: public; Owner: dspace +-- -- +-- +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991138, 1, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991139, 1, 31); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991140, 2, 32); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991141, 2, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991142, 3, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991143, 3, 34); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991144, 4, 35); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991145, 4, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991146, 5, 35); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991147, 5, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991148, 6, 23); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991149, 6, 25); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991150, 6, 27); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991151, 6, 29); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991152, 6, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991153, 7, 33); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991154, 7, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991155, 7, 23); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991156, 8, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991157, 8, 33); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991158, 8, 23); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991159, 9, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991160, 9, 23); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991161, 9, 29); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991162, 9, 25); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991163, 10, 23); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991164, 10, 25); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991165, 10, 29); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991166, 10, 24); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991167, 10, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991168, 11, 29); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991169, 11, 23); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991170, 11, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991171, 11, 27); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991172, 12, 24); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991173, 12, 23); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991174, 12, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991175, 12, 29); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991176, 13, 29); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991177, 13, 23); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991178, 13, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991179, 14, 29); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991180, 14, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991181, 14, 30); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991204, 22, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991206, 24, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991207, 25, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991208, 26, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991209, 27, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991210, 28, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991211, 29, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991212, 30, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991213, 31, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991214, 32, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991215, 33, 23); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991216, 33, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991217, 34, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991218, 35, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991219, 36, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991220, 37, 29); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991221, 37, 23); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991222, 37, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991223, 38, 29); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991224, 38, 24); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991225, 38, 23); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991226, 38, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991227, 39, 27); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991228, 39, 29); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991229, 39, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991230, 39, 23); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991231, 40, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991232, 40, 25); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991233, 40, 29); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991234, 40, 23); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991235, 41, 25); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991236, 41, 29); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991237, 41, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991238, 41, 24); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991239, 41, 23); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991240, 42, 20); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991241, 42, 27); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991242, 42, 25); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991243, 42, 23); +-- INSERT INTO public.license_label_extended_mapping (mapping_id, license_id, label_id) VALUES (991244, 42, 29); +-- +-- +-- -- +-- -- Name: license_definition_license_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dspace +-- -- +-- +-- PERFORM pg_catalog.setval('public.license_definition_license_id_seq', 85, true); +-- +-- +-- -- +-- -- Name: license_label_extended_mapping_mapping_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dspace +-- -- +-- +-- PERFORM pg_catalog.setval('public.license_label_extended_mapping_mapping_id_seq', 991244, true); +-- +-- +-- -- +-- -- Name: license_label_label_id_seq; Type: SEQUENCE SET; Schema: public; Owner: dspace +-- -- +-- +-- PERFORM pg_catalog.setval('public.license_label_label_id_seq', 35, true); +-- +-- END IF; -- End of license_label_extended_mapping check +-- END IF; -- End of license_label check +-- END IF; -- End of license_definition check +--END $$; \ No newline at end of file diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.06.03__Create_table_report_result.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.06.03__Create_table_report_result.sql new file mode 100644 index 000000000000..e23f5735bd27 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.06.03__Create_table_report_result.sql @@ -0,0 +1,31 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +CREATE TABLE report_result ( + report_result_id integer NOT NULL PRIMARY KEY, + type varchar(256), + value TEXT, + executor_id UUID REFERENCES EPerson(uuid) ON DELETE SET NULL, + args TEXT, + last_modified TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- +-- Name: report_result_id_seq; Type: SEQUENCE; Schema: public; Owner: dspace +-- + +CREATE SEQUENCE report_result_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + +ALTER TABLE report_result + ALTER COLUMN report_result_id + SET DEFAULT nextval('report_result_id_seq'); \ No newline at end of file diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.06.09__Added_Indexes_To_Preview_Tables.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.06.09__Added_Indexes_To_Preview_Tables.sql new file mode 100644 index 000000000000..b0f95661c0c1 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.06.09__Added_Indexes_To_Preview_Tables.sql @@ -0,0 +1,25 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +-- =================================================================== +-- PERFORMANCE INDEXES +-- =================================================================== + +-- +-- Index to speed up queries filtering previewcontent by bitstream_id, +-- used in hasPreview() and getPreview() JOIN with bitstream table. +-- +CREATE INDEX idx_previewcontent_bitstream_id +ON previewcontent (bitstream_id); + +-- +-- Index to optimize NOT EXISTS subquery in getPreview(), +-- checking for existence of child_id in preview2preview. +-- +CREATE INDEX idx_preview2preview_child_id +ON preview2preview (child_id); \ No newline at end of file diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.07.29__Matomo_report_registry_table.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.07.29__Matomo_report_registry_table.sql new file mode 100644 index 000000000000..198e39a8847b --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.07.29__Matomo_report_registry_table.sql @@ -0,0 +1,24 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +----------------------------------------------------------------------------------- +-- Create table for MatomoReportSubscription +----------------------------------------------------------------------------------- + +CREATE SEQUENCE matomo_report_registry_id_seq; + +CREATE TABLE matomo_report_registry +( + id INTEGER NOT NULL, + eperson_id UUID NOT NULL, + item_id UUID NOT NULL, + CONSTRAINT matomo_report_registry_pkey PRIMARY KEY (id), + CONSTRAINT matomo_report_registry_eperson_id_fkey FOREIGN KEY (eperson_id) REFERENCES eperson (uuid) ON DELETE CASCADE, + CONSTRAINT matomo_report_registry_item_id_fkey FOREIGN KEY (item_id) REFERENCES item (uuid) ON DELETE CASCADE, + CONSTRAINT matomo_report_registry_unique UNIQUE(eperson_id, item_id) +); \ No newline at end of file diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.09.18__Clarin_token.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.09.18__Clarin_token.sql new file mode 100644 index 000000000000..f57d27b71882 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.09.18__Clarin_token.sql @@ -0,0 +1,20 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +----------------------------------------------------------------------------------- +-- Create table for clarin token entity +----------------------------------------------------------------------------------- +CREATE SEQUENCE clarin_token_id_seq; + +CREATE TABLE clarin_token +( + id INTEGER PRIMARY KEY, + eperson_id UUID NOT NULL, + sign_key VARCHAR(50) NOT NULL, + CONSTRAINT clarin_token_eperson_id_fkey FOREIGN KEY (eperson_id) REFERENCES eperson (uuid) ON DELETE CASCADE +); \ No newline at end of file diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.10.30__7z_bitstream_format.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.10.30__7z_bitstream_format.sql new file mode 100644 index 000000000000..eedbbb1ff2f1 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.10.30__7z_bitstream_format.sql @@ -0,0 +1,12 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +----------------------------------------------------------------------------------- +-- Do nothing, but force migration to load /dspace/config/registries/bitstream-format.xml file +-- to register new application/x-7z-compressed mime-type +----------------------------------------------------------------------------------- \ No newline at end of file diff --git a/dspace/config/hibernate.cfg.xml b/dspace/config/hibernate.cfg.xml index da84fc788676..bbe193838636 100644 --- a/dspace/config/hibernate.cfg.xml +++ b/dspace/config/hibernate.cfg.xml @@ -86,6 +86,19 @@ + + + + + + +