diff --git a/mdm-core/grails-app/conf/db/migration/core/V5_1_2__rename_file_type_to_content_type.sql b/mdm-core/grails-app/conf/db/migration/core/V5_1_2__rename_file_type_to_content_type.sql new file mode 100644 index 0000000000..3d2b2989b4 --- /dev/null +++ b/mdm-core/grails-app/conf/db/migration/core/V5_1_2__rename_file_type_to_content_type.sql @@ -0,0 +1,9 @@ +UPDATE core.domain_export +SET export_content_type = export_file_type +WHERE export_content_type IS NULL; + +ALTER TABLE core.domain_export + ALTER COLUMN export_content_type SET NOT NULL; + +ALTER TABLE core.domain_export + DROP COLUMN export_file_type; \ No newline at end of file diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/async/DomainExport.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/async/DomainExport.groovy index f073c79e36..5df10c43a4 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/async/DomainExport.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/async/DomainExport.groovy @@ -38,7 +38,6 @@ class DomainExport implements MdmDomain { MdmDomain exportedDomain byte[] exportData String exportFileName - String exportFileType String exportContentType String exporterNamespace @@ -55,7 +54,6 @@ class DomainExport implements MdmDomain { exporterNamespace blank: false exporterName blank: false exportFileName blank: false - exportFileType blank: false exportContentType nullable: true, blank: false } @@ -94,8 +92,7 @@ class DomainExport implements MdmDomain { this.exporterNamespace = exporterProviderService.namespace this.exporterName = exporterProviderService.name this.exporterVersion = exporterProviderService.sortableVersion() - this.exportFileType = exporterProviderService.fileType - this.exportContentType = exporterProviderService.producesContentType + this.exportContentType = exporterProviderService.contentType } Map getDownloadLinkParams() { diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/provider/exporter/FolderJsonExporterService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/provider/exporter/FolderJsonExporterService.groovy index 3ce0d37112..6174bbe636 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/provider/exporter/FolderJsonExporterService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/provider/exporter/FolderJsonExporterService.groovy @@ -32,6 +32,8 @@ import org.springframework.beans.factory.annotation.Autowired @CompileStatic class FolderJsonExporterService extends FolderExporterProviderService implements TemplateBasedExporter { + public static final CONTENT_TYPE = 'application/mauro.folder+json' + @Autowired JsonViewTemplateEngine templateEngine @@ -45,19 +47,14 @@ class FolderJsonExporterService extends FolderExporterProviderService implements '1.0' } - @Override - String getFileType() { - 'text/json' - } - @Override String getFileExtension() { 'json' } @Override - String getProducesContentType() { - 'application/mdm+json' + String getContentType() { + CONTENT_TYPE } @Override @@ -73,7 +70,7 @@ class FolderJsonExporterService extends FolderExporterProviderService implements @Override ByteArrayOutputStream exportFolder(User currentUser, Folder folder, Map parameters) throws ApiException { ExportMetadata exportMetadata = new ExportMetadata(this, currentUser.firstName, currentUser.lastName) - exportModel(new ExportModel(folder, 'folder', version, exportMetadata), fileType) + exportModel(new ExportModel(folder, 'folder', version, exportMetadata), contentType) } @Override diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/importer/ImporterService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/importer/ImporterService.groovy index f43cf5a066..ea4faae080 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/importer/ImporterService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/importer/ImporterService.groovy @@ -31,6 +31,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.Importe import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.config.ImportParameterConfig import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.importer.ImportParameterGroup import uk.ac.ox.softeng.maurodatamapper.security.User +import uk.ac.ox.softeng.maurodatamapper.traits.domain.MdmDomain import grails.validation.ValidationErrors import grails.web.databinding.DataBinder @@ -57,12 +58,15 @@ class ImporterService implements DataBinder { @Autowired MessageSource messageSource - public > List importDomains( + @Autowired(required = false) + Set importerProviderServices + + public > List importDomains( User currentUser, T importer, P importParams) { importer.importDomains(currentUser, importParams).findAll() } - public > M importDomain( + public > M importDomain( User currentUser, T importer, P importParams) { M model = importer.importDomain(currentUser, importParams) @@ -212,4 +216,35 @@ class ImporterService implements DataBinder { importerProviderServiceParameters } + public > List findImporterProviderServicesByContentType( + String contentType) { + importerProviderServices.findAll {it.handlesContentType(contentType)}.sort() + } + + public > T findImporterProviderServiceByContentType( + String contentType) { + findImporterProviderServicesByContentType(contentType).first() + } + + public > T findImporterProviderServiceByContentType( + String namespace, String name, String version, String contentType) { + findImporterProviderServices(namespace, name, version).findAll {it.handlesContentType(contentType)}.sort().first() + } + + public > List findImporterProviderServices(String namespace, + String name, + String version) { + if (version) { + importerProviderServices.findAll { + it.namespace.equalsIgnoreCase(namespace) && + it.name.equalsIgnoreCase(name) && + it.version.equalsIgnoreCase(version) + }.sort() + } else { + importerProviderServices.findAll { + it.namespace.equalsIgnoreCase(namespace) && + it.name.equalsIgnoreCase(name) + }.sort() + } + } } \ No newline at end of file diff --git a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/importer/ImporterProviderServiceData.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/importer/ImporterProviderServiceData.groovy new file mode 100644 index 0000000000..d69e7902c1 --- /dev/null +++ b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/importer/ImporterProviderServiceData.groovy @@ -0,0 +1,30 @@ +/* + * Copyright 2020-2022 University of Oxford and Health and Social Care Information Centre, also known as NHS Digital + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package uk.ac.ox.softeng.maurodatamapper.core.rest.transport.importer + +import grails.validation.Validateable + +class ImporterProviderServiceData implements Validateable { + String name + String namespace + String version + + static constraints = { + version nullable: true, blank: false + } +} diff --git a/mdm-core/grails-app/views/domainExport/_domainExport.gson b/mdm-core/grails-app/views/domainExport/_domainExport.gson index 2e0f7a52e6..9e29cdde66 100644 --- a/mdm-core/grails-app/views/domainExport/_domainExport.gson +++ b/mdm-core/grails-app/views/domainExport/_domainExport.gson @@ -24,7 +24,6 @@ json { export { fileName domainExport.exportFileName - fileType domainExport.exportFileType contentType domainExport.exportContentType fileSize domainExport.exportFileSize } diff --git a/mdm-core/grails-app/views/mauroDataMapperServiceProvider/_exporterProviderService.gson b/mdm-core/grails-app/views/mauroDataMapperServiceProvider/_exporterProviderService.gson index c1df2275ab..a73ccc913e 100644 --- a/mdm-core/grails-app/views/mauroDataMapperServiceProvider/_exporterProviderService.gson +++ b/mdm-core/grails-app/views/mauroDataMapperServiceProvider/_exporterProviderService.gson @@ -8,6 +8,6 @@ model { json { fileExtension exporterProviderService.fileExtension - fileType exporterProviderService.fileType + contentType exporterProviderService.contentType canExportMultipleDomains exporterProviderService.canExportMultipleDomains() } \ No newline at end of file diff --git a/mdm-core/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/core/async/DomainExportFunctionalSpec.groovy b/mdm-core/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/core/async/DomainExportFunctionalSpec.groovy index 4ba932ae10..4fde5e8548 100644 --- a/mdm-core/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/core/async/DomainExportFunctionalSpec.groovy +++ b/mdm-core/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/core/async/DomainExportFunctionalSpec.groovy @@ -89,7 +89,7 @@ class DomainExportFunctionalSpec extends BaseFunctionalSpec { } @Override - String getFileType() { + String getContentType() { grails.web.mime.MimeType.JSON.name } @@ -157,8 +157,7 @@ class DomainExportFunctionalSpec extends BaseFunctionalSpec { }, "export": { "fileName": "test.json", - "fileType": "application/json", - "contentType": null, + "contentType": "application/json", "fileSize": 20 }, "exportedOn": "${json-unit.matches:offsetDateTime}", diff --git a/mdm-core/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/core/provider/MauroDataMapperServiceProviderFunctionalSpec.groovy b/mdm-core/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/core/provider/MauroDataMapperServiceProviderFunctionalSpec.groovy index fb4793b724..b875aa5b09 100644 --- a/mdm-core/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/core/provider/MauroDataMapperServiceProviderFunctionalSpec.groovy +++ b/mdm-core/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/core/provider/MauroDataMapperServiceProviderFunctionalSpec.groovy @@ -59,7 +59,7 @@ class MauroDataMapperServiceProviderFunctionalSpec extends BaseFunctionalSpec { ], "providerType": "FolderExporter", "fileExtension": "json", - "fileType": "text/json", + "contentType": "application/mauro.folder+json", "canExportMultipleDomains": false } ]''') diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/provider/exporter/ExporterProviderService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/provider/exporter/ExporterProviderService.groovy index 9dff629bb5..b48087b41c 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/provider/exporter/ExporterProviderService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/provider/exporter/ExporterProviderService.groovy @@ -66,15 +66,11 @@ abstract class ExporterProviderService extends MauroDataMapperService { abstract String getFileExtension() - abstract String getFileType() - /** * MIME type produced by ExporterProviderService * @return MIME type */ - String getProducesContentType() { - null - } + abstract String getContentType() @Override String getProviderType() { diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/provider/importer/ImporterProviderService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/provider/importer/ImporterProviderService.groovy index f1bf687881..32e7b1456c 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/provider/importer/ImporterProviderService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/provider/importer/ImporterProviderService.groovy @@ -23,12 +23,13 @@ import uk.ac.ox.softeng.maurodatamapper.core.provider.ProviderType import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.ImporterProviderServiceParameters import uk.ac.ox.softeng.maurodatamapper.provider.MauroDataMapperService import uk.ac.ox.softeng.maurodatamapper.security.User +import uk.ac.ox.softeng.maurodatamapper.traits.domain.MdmDomain import groovy.transform.CompileStatic import org.springframework.core.GenericTypeResolver @CompileStatic -abstract class ImporterProviderService +abstract class ImporterProviderService extends MauroDataMapperService { abstract D importDomain(User currentUser, T params) @@ -37,6 +38,8 @@ abstract class ImporterProviderService getImportBlacklistedProperties() { ['id', 'domainType', 'lastUpdated'] } diff --git a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/importer/ImporterControllerSpec.groovy b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/importer/ImporterControllerSpec.groovy index 19cae49725..68785c2049 100644 --- a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/importer/ImporterControllerSpec.groovy +++ b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/importer/ImporterControllerSpec.groovy @@ -230,5 +230,10 @@ otherwise you could get an error.''', String getVersion() { '1.0' } + + @Override + Boolean handlesContentType(String contentType) { + contentType.equalsIgnoreCase('application/mauro.test') + } } } \ No newline at end of file diff --git a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/importer/ImporterServiceSpec.groovy b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/importer/ImporterServiceSpec.groovy index 2a4c1d0208..321619824c 100644 --- a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/importer/ImporterServiceSpec.groovy +++ b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/importer/ImporterServiceSpec.groovy @@ -201,6 +201,11 @@ otherwise you could get an error.''', null } + @Override + Boolean handlesContentType(String contentType) { + contentType.equalsIgnoreCase('application/mauro.test') + } + @Override String getDisplayName() { 'Test Importer' diff --git a/mdm-plugin-dataflow/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowController.groovy b/mdm-plugin-dataflow/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowController.groovy index 3f3018d346..aec21bb853 100644 --- a/mdm-plugin-dataflow/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowController.groovy +++ b/mdm-plugin-dataflow/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowController.groovy @@ -99,7 +99,7 @@ class DataFlowController extends EditLoggingController { return errorResponse(UNPROCESSABLE_ENTITY, 'DataFlow could not be exported') } - render(file: outputStream.toByteArray(), fileName: "${instance.label}.${exporter.fileExtension}", contentType: exporter.fileType) + render(file: outputStream.toByteArray(), fileName: "${instance.label}.${exporter.fileExtension}", contentType: exporter.contentType) } def exportDataFlows() { @@ -128,7 +128,7 @@ class DataFlowController extends EditLoggingController { return errorResponse(UNPROCESSABLE_ENTITY, 'DataFlows could not be exported') } - render(file: outputStream.toByteArray(), fileName: "DataFlows.${exporter.fileExtension}", contentType: exporter.fileType) + render(file: outputStream.toByteArray(), fileName: "DataFlows.${exporter.fileExtension}", contentType: exporter.contentType) } private DataFlowImporterProviderService findImporter() { diff --git a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/provider/exporter/DataFlowJsonExporterService.groovy b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/provider/exporter/DataFlowJsonExporterService.groovy index e5bc725022..7b9480de51 100644 --- a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/provider/exporter/DataFlowJsonExporterService.groovy +++ b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/provider/exporter/DataFlowJsonExporterService.groovy @@ -33,6 +33,8 @@ import org.springframework.beans.factory.annotation.Autowired */ class DataFlowJsonExporterService extends DataFlowExporterProviderService implements TemplateBasedExporter { + public static final CONTENT_TYPE = 'application/mauro.dataflow+json' + @Autowired JsonViewTemplateEngine templateEngine @@ -42,13 +44,8 @@ class DataFlowJsonExporterService extends DataFlowExporterProviderService implem } @Override - String getFileType() { - 'text/json' - } - - @Override - String getProducesContentType() { - 'application/mdm+json' + String getContentType() { + CONTENT_TYPE } @Override @@ -69,7 +66,7 @@ class DataFlowJsonExporterService extends DataFlowExporterProviderService implem @Override ByteArrayOutputStream exportDataFlow(User currentUser, DataFlow dataFlow, Map parameters) throws ApiException { ExportMetadata exportMetadata = new ExportMetadata(this, currentUser.firstName, currentUser.lastName) - exportModel new ExportModel(dataFlow, 'dataFlow', version, exportMetadata), fileType + exportModel new ExportModel(dataFlow, 'dataFlow', version, exportMetadata), contentType } @Override diff --git a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/provider/exporter/DataFlowXmlExporterService.groovy b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/provider/exporter/DataFlowXmlExporterService.groovy index 2fea3e48e2..d2f0007334 100644 --- a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/provider/exporter/DataFlowXmlExporterService.groovy +++ b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/provider/exporter/DataFlowXmlExporterService.groovy @@ -33,6 +33,8 @@ import org.springframework.beans.factory.annotation.Autowired */ class DataFlowXmlExporterService extends DataFlowExporterProviderService implements TemplateBasedExporter { + public static final CONTENT_TYPE = 'application/mauro.dataflow+xml' + @Autowired MarkupViewTemplateEngine templateEngine @@ -42,13 +44,8 @@ class DataFlowXmlExporterService extends DataFlowExporterProviderService impleme } @Override - String getFileType() { - 'text/xml' - } - - @Override - String getProducesContentType() { - 'application/mdm+xml' + String getContentType() { + CONTENT_TYPE } @Override @@ -69,7 +66,7 @@ class DataFlowXmlExporterService extends DataFlowExporterProviderService impleme @Override ByteArrayOutputStream exportDataFlow(User currentUser, DataFlow dataFlow, Map parameters) throws ApiException { ExportMetadata exportMetadata = new ExportMetadata(this, currentUser.firstName, currentUser.lastName) - exportModel new ExportModel(dataFlow, 'dataFlow', version, 'gml', exportMetadata), fileType + exportModel new ExportModel(dataFlow, 'dataFlow', version, 'gml', exportMetadata), contentType } @Override diff --git a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/provider/importer/DataFlowJsonImporterService.groovy b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/provider/importer/DataFlowJsonImporterService.groovy index 40ee0e192e..368601268b 100644 --- a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/provider/importer/DataFlowJsonImporterService.groovy +++ b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/provider/importer/DataFlowJsonImporterService.groovy @@ -21,6 +21,7 @@ import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiUnauthorizedException import uk.ac.ox.softeng.maurodatamapper.core.traits.provider.importer.JsonImportMapping import uk.ac.ox.softeng.maurodatamapper.dataflow.DataFlow +import uk.ac.ox.softeng.maurodatamapper.dataflow.provider.exporter.DataFlowJsonExporterService import uk.ac.ox.softeng.maurodatamapper.dataflow.provider.importer.parameter.DataFlowFileImporterProviderServiceParameters import uk.ac.ox.softeng.maurodatamapper.security.User @@ -40,6 +41,11 @@ class DataFlowJsonImporterService extends DataBindDataFlowImporterProviderServic '4.0' } + @Override + Boolean handlesContentType(String contentType) { + contentType.equalsIgnoreCase(DataFlowJsonExporterService.CONTENT_TYPE) + } + @Override DataFlow importDataFlow(User currentUser, byte[] content) { if (!currentUser) throw new ApiUnauthorizedException('JIS01', 'User must be logged in to import model') diff --git a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/provider/importer/DataFlowXmlImporterService.groovy b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/provider/importer/DataFlowXmlImporterService.groovy index 8e6ae7b7fd..45a74dee64 100644 --- a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/provider/importer/DataFlowXmlImporterService.groovy +++ b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/provider/importer/DataFlowXmlImporterService.groovy @@ -21,6 +21,7 @@ package uk.ac.ox.softeng.maurodatamapper.dataflow.provider.importer import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiUnauthorizedException import uk.ac.ox.softeng.maurodatamapper.dataflow.DataFlow +import uk.ac.ox.softeng.maurodatamapper.dataflow.provider.exporter.DataFlowXmlExporterService import uk.ac.ox.softeng.maurodatamapper.dataflow.provider.importer.parameter.DataFlowFileImporterProviderServiceParameters import uk.ac.ox.softeng.maurodatamapper.security.User @@ -57,6 +58,11 @@ class DataFlowXmlImporterService extends DataBindDataFlowImporterProviderService true } + @Override + Boolean handlesContentType(String contentType) { + contentType.equalsIgnoreCase(DataFlowXmlExporterService.CONTENT_TYPE) + } + @Override DataFlow importDataFlow(User currentUser, byte[] content) { if (!currentUser) throw new ApiUnauthorizedException('XIS01', 'User must be logged in to import model') diff --git a/mdm-plugin-dataflow/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowFunctionalSpec.groovy b/mdm-plugin-dataflow/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowFunctionalSpec.groovy index a2385d09d0..8bd3aa0a6b 100644 --- a/mdm-plugin-dataflow/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowFunctionalSpec.groovy +++ b/mdm-plugin-dataflow/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowFunctionalSpec.groovy @@ -196,7 +196,7 @@ class DataFlowFunctionalSpec extends ResourceFunctionalSpec { ], "providerType": "DataFlowExporter", "fileExtension": "xml", - "fileType": "text/xml", + "contentType": "application/mauro.dataflow+xml", "canExportMultipleDomains": false }, { @@ -210,7 +210,7 @@ class DataFlowFunctionalSpec extends ResourceFunctionalSpec { ], "providerType": "DataFlowExporter", "fileExtension": "json", - "fileType": "text/json", + "contentType": "application/mauro.dataflow+json", "canExportMultipleDomains": false } ]''' diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/exporter/DataModelJsonExporterService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/exporter/DataModelJsonExporterService.groovy index 3f81f39e26..5aa3349e6c 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/exporter/DataModelJsonExporterService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/exporter/DataModelJsonExporterService.groovy @@ -29,6 +29,8 @@ import org.springframework.beans.factory.annotation.Autowired class DataModelJsonExporterService extends DataModelExporterProviderService implements TemplateBasedExporter { + public static final CONTENT_TYPE = 'application/mauro.datamodel+json' + @Autowired JsonViewTemplateEngine templateEngine @@ -42,19 +44,14 @@ class DataModelJsonExporterService extends DataModelExporterProviderService impl '3.1' } - @Override - String getFileType() { - 'text/json' - } - @Override String getFileExtension() { 'json' } @Override - String getProducesContentType() { - 'application/mdm+json' + String getContentType() { + CONTENT_TYPE } @Override @@ -70,12 +67,12 @@ class DataModelJsonExporterService extends DataModelExporterProviderService impl @Override ByteArrayOutputStream exportDataModel(User currentUser, DataModel dataModel, Map parameters) throws ApiException { ExportMetadata exportMetadata = new ExportMetadata(this, currentUser.firstName, currentUser.lastName) - exportModel(new ExportModel(dataModel, 'dataModel', version, exportMetadata), fileType) + exportModel(new ExportModel(dataModel, 'dataModel', version, exportMetadata), contentType) } @Override ByteArrayOutputStream exportDataModels(User currentUser, List dataModels, Map parameters) throws ApiException { ExportMetadata exportMetadata = new ExportMetadata(this, currentUser.firstName, currentUser.lastName) - exportModel(new ExportModel(dataModels, 'dataModel', 'dataModels', version, exportMetadata), fileType) + exportModel(new ExportModel(dataModels, 'dataModel', 'dataModels', version, exportMetadata), contentType) } } diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/exporter/DataModelXmlExporterService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/exporter/DataModelXmlExporterService.groovy index 2dc248a9d1..1209371b02 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/exporter/DataModelXmlExporterService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/exporter/DataModelXmlExporterService.groovy @@ -32,6 +32,8 @@ import org.springframework.beans.factory.annotation.Autowired */ class DataModelXmlExporterService extends DataModelExporterProviderService implements TemplateBasedExporter { + public static final CONTENT_TYPE = 'application/mauro.datamodel+xml' + @Autowired MarkupViewTemplateEngine templateEngine @@ -45,19 +47,14 @@ class DataModelXmlExporterService extends DataModelExporterProviderService imple '5.1' } - @Override - String getFileType() { - 'text/xml' - } - @Override String getFileExtension() { 'xml' } @Override - String getProducesContentType() { - 'application/mdm+xml' + String getContentType() { + CONTENT_TYPE } @Override @@ -73,12 +70,12 @@ class DataModelXmlExporterService extends DataModelExporterProviderService imple @Override ByteArrayOutputStream exportDataModel(User currentUser, DataModel dataModel, Map parameters) throws ApiException { ExportMetadata exportMetadata = new ExportMetadata(this, currentUser.firstName, currentUser.lastName) - exportModel(new ExportModel(dataModel, 'dataModel', version, '4.1', 'gml', exportMetadata), fileType) + exportModel(new ExportModel(dataModel, 'dataModel', version, '4.1', 'gml', exportMetadata), contentType) } @Override ByteArrayOutputStream exportDataModels(User currentUser, List dataModels, Map parameters) throws ApiException { ExportMetadata exportMetadata = new ExportMetadata(this, currentUser.firstName, currentUser.lastName) - exportModel(new ExportModel(dataModels, 'dataModel', 'dataModels', version, '4.1', 'gml', exportMetadata), fileType) + exportModel(new ExportModel(dataModels, 'dataModel', 'dataModels', version, '4.1', 'gml', exportMetadata), contentType) } } diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/importer/DataModelJsonImporterService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/importer/DataModelJsonImporterService.groovy index 30fcb077b9..c30a20033b 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/importer/DataModelJsonImporterService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/importer/DataModelJsonImporterService.groovy @@ -21,6 +21,7 @@ import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiUnauthorizedException import uk.ac.ox.softeng.maurodatamapper.core.traits.provider.importer.JsonImportMapping import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel +import uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter.DataModelJsonExporterService import uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer.parameter.DataModelFileImporterProviderServiceParameters import uk.ac.ox.softeng.maurodatamapper.security.User @@ -44,6 +45,11 @@ class DataModelJsonImporterService extends DataBindDataModelImporterProviderServ true } + @Override + Boolean handlesContentType(String contentType) { + contentType.equalsIgnoreCase(DataModelJsonExporterService.CONTENT_TYPE) + } + @Override DataModel importDataModel(User currentUser, byte[] content) { if (!currentUser) throw new ApiUnauthorizedException('JIS01', 'User must be logged in to import model') diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/importer/DataModelXmlImporterService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/importer/DataModelXmlImporterService.groovy index c970822c53..64f872d477 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/importer/DataModelXmlImporterService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/importer/DataModelXmlImporterService.groovy @@ -21,6 +21,7 @@ import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiUnauthorizedException import uk.ac.ox.softeng.maurodatamapper.core.traits.provider.importer.XmlImportMapping import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel +import uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter.DataModelXmlExporterService import uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer.parameter.DataModelFileImporterProviderServiceParameters import uk.ac.ox.softeng.maurodatamapper.security.User @@ -55,6 +56,11 @@ class DataModelXmlImporterService extends DataBindDataModelImporterProviderServi true } + @Override + Boolean handlesContentType(String contentType) { + contentType.equalsIgnoreCase(DataModelXmlExporterService.CONTENT_TYPE) + } + @Override DataModel importDataModel(User currentUser, byte[] content) { if (!currentUser) throw new ApiUnauthorizedException('XIS01', 'User must be logged in to import model') diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelFunctionalSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelFunctionalSpec.groovy index 419511fa68..5df11a14ef 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelFunctionalSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelFunctionalSpec.groovy @@ -209,7 +209,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec implemen "allowsExtraMetadataKeys": true, "canExportMultipleDomains": true, "version": "${json-unit.matches:version}", - "fileType": "text/xml" + "contentType": "application/mauro.datamodel+xml" }, { "providerType": "DataModelExporter", @@ -223,7 +223,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec implemen "allowsExtraMetadataKeys": true, "canExportMultipleDomains": true, "version": "${json-unit.matches:version}", - "fileType": "text/json" + "contentType": "application/mauro.datamodel+json" } ]''' } @@ -2894,8 +2894,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec implemen }, "export": { "fileName": "Complex Test DataModel.json", - "fileType": "text/json", - "contentType": "application/mdm+json", + "contentType": "application/mauro.datamodel+json", "fileSize": "${json-unit.any-number}" }, "exportedOn": "${json-unit.matches:offsetDateTime}", diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/MauroDataMapperServiceProviderFunctionalSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/MauroDataMapperServiceProviderFunctionalSpec.groovy index 9bcabddfd9..707ddac92e 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/MauroDataMapperServiceProviderFunctionalSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/MauroDataMapperServiceProviderFunctionalSpec.groovy @@ -59,7 +59,7 @@ class MauroDataMapperServiceProviderFunctionalSpec extends BaseFunctionalSpec { ], "providerType": "DataModelExporter", "fileExtension": "json", - "fileType": "text/json", + "contentType": "application/mauro.datamodel+json", "canExportMultipleDomains": true }, { @@ -73,7 +73,7 @@ class MauroDataMapperServiceProviderFunctionalSpec extends BaseFunctionalSpec { ], "providerType": "DataModelExporter", "fileExtension": "xml", - "fileType": "text/xml", + "contentType": "application/mauro.datamodel+xml", "canExportMultipleDomains": true }, { @@ -87,7 +87,7 @@ class MauroDataMapperServiceProviderFunctionalSpec extends BaseFunctionalSpec { ], "providerType": "FolderExporter", "fileExtension": "json", - "fileType": "text/json", + "contentType": "application/mauro.folder+json", "canExportMultipleDomains": false } ]''') diff --git a/mdm-plugin-federation/grails-app/assets/xsd/newerPublishedModels_1.0.xsd b/mdm-plugin-federation/grails-app/assets/xsd/newerPublishedModels_1.0.xsd index 32581d6fa2..32d38f2109 100644 --- a/mdm-plugin-federation/grails-app/assets/xsd/newerPublishedModels_1.0.xsd +++ b/mdm-plugin-federation/grails-app/assets/xsd/newerPublishedModels_1.0.xsd @@ -34,7 +34,6 @@ - diff --git a/mdm-plugin-federation/grails-app/assets/xsd/publishedModels_1.0.xsd b/mdm-plugin-federation/grails-app/assets/xsd/publishedModels_1.0.xsd index 01eb8c2f85..18f21a41a7 100644 --- a/mdm-plugin-federation/grails-app/assets/xsd/publishedModels_1.0.xsd +++ b/mdm-plugin-federation/grails-app/assets/xsd/publishedModels_1.0.xsd @@ -34,7 +34,6 @@ - diff --git a/mdm-plugin-federation/grails-app/conf/db/migration/federation/V5_1_0__add_subscribed_catalogue_type_and_convert_subscribed_catalogue_id.sql b/mdm-plugin-federation/grails-app/conf/db/migration/federation/V5_1_0__add_subscribed_catalogue_type_and_convert_subscribed_catalogue_id.sql new file mode 100644 index 0000000000..c4028a9c3a --- /dev/null +++ b/mdm-plugin-federation/grails-app/conf/db/migration/federation/V5_1_0__add_subscribed_catalogue_type_and_convert_subscribed_catalogue_id.sql @@ -0,0 +1,8 @@ +ALTER TABLE federation.subscribed_catalogue + ADD subscribed_catalogue_type VARCHAR(255) NOT NULL DEFAULT 'MAURO_JSON'; + +ALTER TABLE federation.subscribed_model + ALTER COLUMN subscribed_model_id TYPE TEXT USING subscribed_model_id::TEXT; + +ALTER TABLE federation.subscribed_model + ALTER COLUMN subscribed_model_type DROP NOT NULL; \ No newline at end of file diff --git a/mdm-plugin-federation/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedCatalogueController.groovy b/mdm-plugin-federation/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedCatalogueController.groovy index c62276f325..9e1fdb12e8 100644 --- a/mdm-plugin-federation/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedCatalogueController.groovy +++ b/mdm-plugin-federation/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedCatalogueController.groovy @@ -60,6 +60,10 @@ class SubscribedCatalogueController extends EditLoggingController types() { + respond SubscribedCatalogueType.labels() + } + /** * Read available models from the subscribed catalogue and return as json. * @@ -79,7 +83,7 @@ class SubscribedCatalogueController extends EditLoggingController { @@ -50,17 +53,32 @@ class SubscribedModelController extends EditLoggingController { } @Transactional - @Override - def save() { + def federate(SubscribedModelFederationParams subscribedModelFederationParams) { if (handleReadOnly()) return - def instance = createResource() + if (subscribedModelFederationParams.hasErrors() || !subscribedModelFederationParams.validate()) { + transactionStatus.setRollbackOnly() + respond subscribedModelFederationParams.errors // STATUS CODE 422 + return + } + + // Validate nested command object separately + if (subscribedModelFederationParams.importerProviderService && + (subscribedModelFederationParams.importerProviderService.hasErrors() || !subscribedModelFederationParams.importerProviderService.validate())) { + transactionStatus.setRollbackOnly() + respond subscribedModelFederationParams.importerProviderService.errors // STATUS CODE 422 + return + } + + SubscribedModel instance = subscribedModelFederationParams.subscribedModel + instance.subscribedCatalogue = subscribedCatalogueService.get(params.subscribedCatalogueId) + instance.createdBy = currentUser.emailAddress if (response.isCommitted()) return if (!validateResource(instance, 'create')) return - def federationResult = subscribedModelService.federateSubscribedModel(instance, currentUserSecurityPolicyManager) + def federationResult = subscribedModelService.federateSubscribedModel(subscribedModelFederationParams, currentUserSecurityPolicyManager) if (federationResult instanceof Errors) { transactionStatus.setRollbackOnly() respond federationResult, view: 'create' // STATUS CODE 422 @@ -104,7 +122,7 @@ class SubscribedModelController extends EditLoggingController { currentUserSecurityPolicyManager = securityPolicyManagerService.addSecurityForSecurableResource( subscribedModel, currentUser, - subscribedModel.subscribedModelId.toString()) + subscribedModel.subscribedModelId) } subscribedModel } diff --git a/mdm-plugin-federation/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedModelInterceptor.groovy b/mdm-plugin-federation/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedModelInterceptor.groovy index fb1b5a6399..dd67da0a4c 100644 --- a/mdm-plugin-federation/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedModelInterceptor.groovy +++ b/mdm-plugin-federation/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedModelInterceptor.groovy @@ -33,13 +33,12 @@ class SubscribedModelInterceptor extends SecurableResourceInterceptor { @Override void checkIds() { Utils.toUuid(params, 'id') - Utils.toUuid(params, 'subscribedModelId') Utils.toUuid(params, 'subscribedCatalogueId') } @Override UUID getId() { - params.subscribedModelId ?: params.id + params.id } boolean before() { @@ -47,7 +46,7 @@ class SubscribedModelInterceptor extends SecurableResourceInterceptor { if (currentUserSecurityPolicyManager.isAuthenticated()) { return actionName == 'index' || actionName == 'show' || - actionName == 'save' || + actionName == 'federate' || actionName == 'newerVersions' || currentUserSecurityPolicyManager.isApplicationAdministrator() ?: forbiddenDueToNotApplicationAdministrator() } diff --git a/mdm-plugin-federation/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/federation/UrlMappings.groovy b/mdm-plugin-federation/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/federation/UrlMappings.groovy index 378c1d56bc..7b8b32c3b3 100644 --- a/mdm-plugin-federation/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/federation/UrlMappings.groovy +++ b/mdm-plugin-federation/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/federation/UrlMappings.groovy @@ -18,6 +18,7 @@ package uk.ac.ox.softeng.maurodatamapper.federation import static uk.ac.ox.softeng.maurodatamapper.core.web.mapping.UrlMappingActions.DEFAULT_EXCLUDES +import static uk.ac.ox.softeng.maurodatamapper.core.web.mapping.UrlMappingActions.DEFAULT_EXCLUDES_AND_NO_SAVE class UrlMappings { @@ -32,9 +33,13 @@ class UrlMappings { '/subscribedCatalogues'(resources: 'subscribedCatalogue', excludes: DEFAULT_EXCLUDES) { get '/testConnection'(controller: 'subscribedCatalogue', action: 'testConnection') } + group '/subscribedCatalogues', { + get '/types'(controller: 'subscribedCatalogue', action: 'types') + } } group '/subscribedCatalogues', { + get '/types'(controller: 'subscribedCatalogue', action: 'types') get '/'(controller: 'subscribedCatalogue', action: 'index') { openAccess = true } @@ -45,7 +50,8 @@ class UrlMappings { get '/testConnection'(controller: 'subscribedCatalogue', action: 'testConnection') get '/publishedModels'(controller: 'subscribedCatalogue', action: 'publishedModels') get "/publishedModels/$publishedModelId/newerVersions"(controller: 'subscribedCatalogue', action: 'newerVersions') - '/subscribedModels'(resources: 'subscribedModel', excludes: DEFAULT_EXCLUDES) + '/subscribedModels'(resources: 'subscribedModel', excludes: DEFAULT_EXCLUDES_AND_NO_SAVE) + post '/subscribedModels'(controller: 'subscribedModel', action: 'federate') get "/subscribedModels/$id/newerVersions"(controller: 'subscribedModel', action: 'newerVersions') } } diff --git a/mdm-plugin-federation/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/federation/publish/PublishController.groovy b/mdm-plugin-federation/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/federation/publish/PublishController.groovy index 1e0b557f1b..eec0e5ceff 100644 --- a/mdm-plugin-federation/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/federation/publish/PublishController.groovy +++ b/mdm-plugin-federation/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/federation/publish/PublishController.groovy @@ -57,14 +57,14 @@ class PublishController extends RestfulController implements Resourceless def newerVersions() { List publishedModels = publishService.findAllPublishedReadableModels(currentUserSecurityPolicyManager) - UUID modelId = Utils.toUuid(params.publishedModelId) - PublishedModel publishedModel = publishedModels.find({it.modelId == modelId}) + PublishedModel publishedModel = publishedModels.find({it.modelId == params.publishedModelId}) + String modelId = params.publishedModelId if (!publishedModel) { - return notFound(PublishedModel, params.publishedModelId) + return notFound(PublishedModel, modelId) } List newerPublishedModels = - publishService.findPublishedSupersedingModels(publishedModels, publishedModel.modelType, modelId, currentUserSecurityPolicyManager) + publishService.findPublishedSupersedingModels(publishedModels, publishedModel.modelType, Utils.toUuid(modelId), currentUserSecurityPolicyManager) respond(newerPublishedModels: newerPublishedModels) } } diff --git a/mdm-plugin-federation/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedCatalogue.groovy b/mdm-plugin-federation/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedCatalogue.groovy index eddb1df833..f819806e3b 100644 --- a/mdm-plugin-federation/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedCatalogue.groovy +++ b/mdm-plugin-federation/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedCatalogue.groovy @@ -24,6 +24,7 @@ import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CallableConstra import uk.ac.ox.softeng.maurodatamapper.security.SecurableResource import uk.ac.ox.softeng.maurodatamapper.traits.domain.MdmDomain +import grails.databinding.BindUsing import grails.gorm.DetachedCriteria import org.apache.commons.validator.routines.UrlValidator @@ -39,15 +40,19 @@ class SubscribedCatalogue implements MdmDomain, SecurableResource, EditHistoryAw Boolean readableByEveryone Boolean readableByAuthenticatedUsers - //Refresh period is assumed to be in units of days + // Refresh period is assumed to be in units of days Integer refreshPeriod - //The last time that we checked the catalogue for models to export + // The last time that we checked the catalogue for models to export OffsetDateTime lastRead - //HTTP read connection timeout in minutes + // HTTP read connection timeout in minutes Integer connectionTimeout + // Connection type to remote catalogue + @BindUsing({obj, source -> SubscribedCatalogueType.findFromMap(source)}) + SubscribedCatalogueType subscribedCatalogueType + static hasMany = [ subscribedModels: SubscribedModel ] diff --git a/mdm-plugin-federation/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedModel.groovy b/mdm-plugin-federation/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedModel.groovy index f992db10bd..b1b3518a97 100644 --- a/mdm-plugin-federation/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedModel.groovy +++ b/mdm-plugin-federation/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedModel.groovy @@ -30,7 +30,7 @@ class SubscribedModel implements MdmDomain, SecurableResource, EditHistoryAware UUID id //The ID of the model on the remote (subscribed) catalogue - UUID subscribedModelId + String subscribedModelId String subscribedModelType //The folder that the model should be imported into UUID folderId @@ -47,7 +47,7 @@ class SubscribedModel implements MdmDomain, SecurableResource, EditHistoryAware subscribedCatalogue nullable: false folderId nullable: false subscribedModelId nullable: false, unique: 'subscribedCatalogue' // Should prevent subscribing to the same modelId from same catalogue - subscribedModelType blank: false + subscribedModelType nullable: true, blank: false lastRead nullable: true localModelId nullable: true path nullable: true @@ -90,7 +90,7 @@ class SubscribedModel implements MdmDomain, SecurableResource, EditHistoryAware by().eq('subscribedCatalogue.id', subscribedCatalogueId) } - static DetachedCriteria bySubscribedCatalogueIdAndSubscribedModelId(UUID subscribedCatalogueId, UUID subscribedModelId) { + static DetachedCriteria bySubscribedCatalogueIdAndSubscribedModelId(UUID subscribedCatalogueId, String subscribedModelId) { bySubscribedCatalogueId(subscribedCatalogueId).eq('subscribedModelId', subscribedModelId) } @@ -98,7 +98,7 @@ class SubscribedModel implements MdmDomain, SecurableResource, EditHistoryAware bySubscribedCatalogueId(subscribedCatalogueId).idEq(id) } - static DetachedCriteria bySubscribedModelId(UUID subscribedModelId) { + static DetachedCriteria bySubscribedModelId(String subscribedModelId) { by() .eq('subscribedModelId', subscribedModelId) } diff --git a/mdm-plugin-federation/grails-app/i18n/messages.properties b/mdm-plugin-federation/grails-app/i18n/messages.properties index c4285d9c1b..e1949413c3 100644 --- a/mdm-plugin-federation/grails-app/i18n/messages.properties +++ b/mdm-plugin-federation/grails-app/i18n/messages.properties @@ -24,5 +24,7 @@ resource.api.feeds.all.href.title=Mauro Data Mapper - All Models invalid.subscribedmodel.folderid.no.permissions=Invalid folderId for subscribed model, user does not have the necessary permissions invalid.subscribedmodel.export=Could not export SubscribedModel from SubscribedCatalogue invalid.subscribedmodel.import=Could not import SubscribedModel into local Catalogue +invalid.subscribedmodel.import.format.unsupported=Could not import SubscribedModel into local Catalogue, no importer and available content type found with given parameters +invalid.subscribedmodel.import.link.notfound=Could not import SubscribedModel into local Catalogue, no published link found for URL [{0}] and/or content type [{1}] invalid.subscribedmodel.federate.exception=Could not federate SubscribedModel into local Catalogue due to [{0}] invalid.subscribedmodel.import.already.exists=Model from authority [{0}] with label [{1}] and version [{2}] already exists in catalogue \ No newline at end of file diff --git a/mdm-plugin-federation/grails-app/services/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedCatalogueService.groovy b/mdm-plugin-federation/grails-app/services/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedCatalogueService.groovy index c7a729a7f1..cd48136368 100644 --- a/mdm-plugin-federation/grails-app/services/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedCatalogueService.groovy +++ b/mdm-plugin-federation/grails-app/services/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedCatalogueService.groovy @@ -20,16 +20,13 @@ package uk.ac.ox.softeng.maurodatamapper.federation import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiException import uk.ac.ox.softeng.maurodatamapper.core.authority.Authority import uk.ac.ox.softeng.maurodatamapper.core.authority.AuthorityService -import uk.ac.ox.softeng.maurodatamapper.core.traits.provider.importer.XmlImportMapping import uk.ac.ox.softeng.maurodatamapper.core.traits.service.AnonymisableService +import uk.ac.ox.softeng.maurodatamapper.federation.converter.SubscribedCatalogueConverter import uk.ac.ox.softeng.maurodatamapper.federation.web.FederationClient import uk.ac.ox.softeng.maurodatamapper.security.basic.AnonymousUser -import uk.ac.ox.softeng.maurodatamapper.util.Utils -import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.core.GrailsApplication import grails.gorm.transactions.Transactional -import grails.rest.Link import grails.util.Environment import groovy.util.logging.Slf4j import io.micronaut.http.client.HttpClientConfiguration @@ -38,14 +35,13 @@ import io.micronaut.http.codec.MediaTypeCodecRegistry import org.springframework.beans.factory.annotation.Autowired import java.time.Duration -import java.time.OffsetDateTime -import java.time.format.DateTimeFormatter @Transactional @Slf4j -class SubscribedCatalogueService implements XmlImportMapping, AnonymisableService { +class SubscribedCatalogueService implements AnonymisableService { - public static final String LINK_RELATIONSHIP_ALTERNATE = 'alternate' + @Autowired + Set subscribedCatalogueConverters @Autowired HttpClientConfiguration httpClientConfiguration @@ -79,27 +75,30 @@ class SubscribedCatalogueService implements XmlImportMapping, AnonymisableServic void verifyConnectionToSubscribedCatalogue(SubscribedCatalogue subscribedCatalogue) { try { - Map catalogueModels = getFederationClientForSubscribedCatalogue(subscribedCatalogue).withCloseable {client -> - client.getSubscribedCatalogueModels(subscribedCatalogue.apiKey) - } - if (!catalogueModels.containsKey('publishedModels') || !catalogueModels.authority) { + def (Authority subscribedAuthority, List publishedModels) = listPublishedModelsWithAuthority(subscribedCatalogue) + + // Check that the remote catalogue has a name (Authority label), which is mandatory for both Mauro JSON and Atom XML catalogues + // Check that the publishedModels list exists, however this may be empty + if (!subscribedAuthority.label || publishedModels == null) { subscribedCatalogue.errors.reject('invalid.subscription.url.response', [subscribedCatalogue.url].toArray(), 'Invalid subscription to catalogue at [{0}], response from catalogue is invalid') } + Authority thisAuthority = authorityService.defaultAuthority // Under prod mode dont let a connection to ourselves exist if (Environment.current == Environment.PRODUCTION) { - if (catalogueModels.authority.label == thisAuthority.label && catalogueModels.authority.url == thisAuthority.url) { + if (subscribedAuthority.label == thisAuthority.label && subscribedAuthority.url == thisAuthority.url) { subscribedCatalogue.errors.reject( 'invalid.subscription.url.authority', [subscribedCatalogue.url, - catalogueModels.authority.label, - catalogueModels.authority.url].toArray(), + subscribedAuthority.label, + subscribedAuthority.url].toArray(), 'Invalid subscription to catalogue at [{0}] as it has the same Authority as this instance [{1}:{2}]') } } + } catch (ApiException exception) { subscribedCatalogue.errors.reject('invalid.subscription.url.connection', [subscribedCatalogue.url, exception.message].toArray(), @@ -124,50 +123,46 @@ class SubscribedCatalogueService implements XmlImportMapping, AnonymisableServic * */ List listPublishedModels(SubscribedCatalogue subscribedCatalogue) { - Map subscribedCatalogueModels = getFederationClientForSubscribedCatalogue(subscribedCatalogue).withCloseable {client -> - client.getSubscribedCatalogueModels(subscribedCatalogue.apiKey) + getFederationClientForSubscribedCatalogue(subscribedCatalogue).withCloseable {client -> + getConverterForSubscribedCatalogue(subscribedCatalogue).getPublishedModels(client, subscribedCatalogue) } - if (subscribedCatalogueModels.publishedModels.isEmpty()) return [] - - (subscribedCatalogueModels.publishedModels as List>).collect {pm -> - new PublishedModel().tap { - modelId = Utils.toUuid(pm.modelId) - title = pm.title // for compatibility with remote catalogue versions prior to 4.12 - if (pm.label) modelLabel = pm.label - if (pm.version) modelVersion = Version.from(pm.version) - modelType = pm.modelType - lastUpdated = OffsetDateTime.parse(pm.lastUpdated, DateTimeFormatter.ISO_OFFSET_DATE_TIME) - dateCreated = OffsetDateTime.parse(pm.dateCreated, DateTimeFormatter.ISO_OFFSET_DATE_TIME) - datePublished = OffsetDateTime.parse(pm.datePublished, DateTimeFormatter.ISO_OFFSET_DATE_TIME) - author = pm.author - description = pm.description - if (pm.links) links = pm.links.collect {link -> new Link(LINK_RELATIONSHIP_ALTERNATE, link.url).tap {contentType = link.contentType}} - } - }.sort() } - List> getAvailableExportersForResourceType(SubscribedCatalogue subscribedCatalogue, String urlResourceType) { + Tuple2> listPublishedModelsWithAuthority(SubscribedCatalogue subscribedCatalogue) { getFederationClientForSubscribedCatalogue(subscribedCatalogue).withCloseable {client -> - client.getAvailableExporters(subscribedCatalogue.apiKey, urlResourceType) + getConverterForSubscribedCatalogue(subscribedCatalogue).getAuthorityAndPublishedModels(client, subscribedCatalogue) } } - Map getVersionLinksForModel(SubscribedCatalogue subscribedCatalogue, String urlModelType, UUID modelId) { + Authority getAuthority(SubscribedCatalogue subscribedCatalogue) { getFederationClientForSubscribedCatalogue(subscribedCatalogue).withCloseable {client -> - client.getVersionLinksForModel(subscribedCatalogue.apiKey, urlModelType, modelId) + getConverterForSubscribedCatalogue(subscribedCatalogue).getAuthority(client, subscribedCatalogue) } } - Map getNewerPublishedVersionsForPublishedModel(SubscribedCatalogue subscribedCatalogue, UUID modelId) { - getFederationClientForSubscribedCatalogue(subscribedCatalogue).withCloseable {client -> - client.getNewerPublishedVersionsForPublishedModel(subscribedCatalogue.apiKey, modelId) + Map getVersionLinksForModel(SubscribedCatalogue subscribedCatalogue, String urlModelType, String publishedModelId) { + if (subscribedCatalogue.subscribedCatalogueType == SubscribedCatalogueType.MAURO_JSON) { + getFederationClientForSubscribedCatalogue(subscribedCatalogue).withCloseable {client -> + client.getVersionLinksForModel(subscribedCatalogue.apiKey, urlModelType, publishedModelId) + } + } else { + [:] } } - String getStringResourceExport(SubscribedCatalogue subscribedCatalogue, String urlResourceType, UUID resourceId, Map exporterInfo) { + Map getNewerPublishedVersionsForPublishedModel(SubscribedCatalogue subscribedCatalogue, String publishedModelId) { + if (subscribedCatalogue.subscribedCatalogueType == SubscribedCatalogueType.MAURO_JSON) { + getFederationClientForSubscribedCatalogue(subscribedCatalogue).withCloseable {client -> + client.getNewerPublishedVersionsForPublishedModel(subscribedCatalogue.apiKey, publishedModelId) + } + } else { + [:] + } + } + + byte[] getBytesResourceExport(SubscribedCatalogue subscribedCatalogue, String resourceUrl) { getFederationClientForSubscribedCatalogue(subscribedCatalogue).withCloseable {client -> - client.getStringResourceExport(subscribedCatalogue.apiKey, urlResourceType, - resourceId, exporterInfo) + client.getBytesResourceExport(subscribedCatalogue.apiKey, resourceUrl) } } @@ -181,31 +176,8 @@ class SubscribedCatalogueService implements XmlImportMapping, AnonymisableServic mediaTypeCodecRegistry) } - /** - * An ID in the atom feed looks like tag:host,MINTED_DATE:uuid - * So get everything after the last : - * - * @param url A tag ID / URL - * @return A UUID as a string - */ - private static String extractUuidFromTagUri(String url) { - final String separator = ':' - int lastPos = url.lastIndexOf(separator) - - return url.substring(lastPos + 1) - } - - /** - * An ID in the atom feed looks like urn:uuid:{model.id}* So get everything after the last : - * - * @param url A urn url - * @return A UUID as a string - */ - private static String extractUuidFromUrn(String url) { - final String separator = ':' - int lastPos = url.lastIndexOf(separator) - - return url.substring(lastPos + 1) + private SubscribedCatalogueConverter getConverterForSubscribedCatalogue(SubscribedCatalogue subscribedCatalogue) { + subscribedCatalogueConverters.find {it.handles(subscribedCatalogue.subscribedCatalogueType)} } void anonymise(String createdBy) { diff --git a/mdm-plugin-federation/grails-app/services/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedModelService.groovy b/mdm-plugin-federation/grails-app/services/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedModelService.groovy index 4740afaef9..61742ba61d 100644 --- a/mdm-plugin-federation/grails-app/services/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedModelService.groovy +++ b/mdm-plugin-federation/grails-app/services/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedModelService.groovy @@ -23,20 +23,26 @@ import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException import uk.ac.ox.softeng.maurodatamapper.core.authority.Authority import uk.ac.ox.softeng.maurodatamapper.core.container.Folder import uk.ac.ox.softeng.maurodatamapper.core.container.FolderService +import uk.ac.ox.softeng.maurodatamapper.core.container.VersionedFolder import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType +import uk.ac.ox.softeng.maurodatamapper.core.importer.ImporterService import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelService -import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.ModelImporterProviderService +import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.ImporterProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.FileParameter import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.ModelImporterProviderServiceParameters import uk.ac.ox.softeng.maurodatamapper.core.traits.service.AnonymisableService +import uk.ac.ox.softeng.maurodatamapper.core.traits.service.MdmDomainService +import uk.ac.ox.softeng.maurodatamapper.federation.rest.transport.SubscribedModelFederationParams import uk.ac.ox.softeng.maurodatamapper.security.SecurableResourceService import uk.ac.ox.softeng.maurodatamapper.security.SecurityPolicyManagerService import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.security.basic.AnonymousUser +import uk.ac.ox.softeng.maurodatamapper.traits.domain.MdmDomain import grails.gorm.transactions.Transactional +import grails.rest.Link import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired @@ -47,10 +53,11 @@ import java.time.OffsetDateTime class SubscribedModelService implements SecurableResourceService, AnonymisableService { @Autowired(required = false) - List modelServices + List domainServices SubscribedCatalogueService subscribedCatalogueService FolderService folderService + ImporterService importerService @Autowired(required = false) SecurityPolicyManagerService securityPolicyManagerService @@ -65,19 +72,14 @@ class SubscribedModelService implements SecurableResourceService list(Map pagination = [:]) { SubscribedModel.list(pagination) } @@ -122,69 +124,102 @@ class SubscribedModelService implements SecurableResourceService exportLinks = getExportLinksForSubscribedModel(subscribedModel) + + if (!exportLinks.find {link -> + (!subscribedModelFederationParams.url || subscribedModelFederationParams.url == link.href) && + (!subscribedModelFederationParams.contentType || subscribedModelFederationParams.contentType == link.contentType) + }) { + log.debug('No published link found for specified URL and/or content type') + subscribedModel.errors.reject('invalid.subscribedmodel.import.link.notfound', + [subscribedModelFederationParams.url, subscribedModelFederationParams.contentType].toArray(), + 'Could not import SubscribedModel into local Catalogue, no published link found for URL [{0}] and/or content type [{1}]') return subscribedModel.errors } - //Get a ModelService to handle the domain type we are dealing with - ModelService modelService = modelServices.find {it.handles(subscribedModel.subscribedModelType)} - ModelImporterProviderService modelImporterProviderService = modelService.getJsonModelImporterProviderService() + ImporterProviderService importerProviderService + Link exportLink + if (subscribedModelFederationParams.importerProviderService?.namespace && subscribedModelFederationParams.importerProviderService?.name) { + exportLink = exportLinks.find {link -> + (!subscribedModelFederationParams.url || subscribedModelFederationParams.url == link.href) && + (!subscribedModelFederationParams.contentType || subscribedModelFederationParams.contentType == link.contentType) && + (importerProviderService = + importerService.findImporterProviderServiceByContentType(subscribedModelFederationParams.importerProviderService.namespace, + subscribedModelFederationParams.importerProviderService.name, + subscribedModelFederationParams.importerProviderService.version, link.contentType)) + } + } else { + exportLink = exportLinks.find {link -> + (!subscribedModelFederationParams.url || subscribedModelFederationParams.url == link.href) && + (!subscribedModelFederationParams.contentType || subscribedModelFederationParams.contentType == link.contentType) && + (importerProviderService = importerService.findImporterProviderServiceByContentType(link.contentType)) + } + } + + if (!importerProviderService) { + log.debug('No ImporterProviderService found for any published content type and given parameters') + subscribedModel.errors.reject('invalid.subscribedmodel.import.format.unsupported', + 'Could not import SubscribedModel into local Catalogue, no importer and available content type found with given parameters') + return subscribedModel.errors + } - //Import the model - ModelImporterProviderServiceParameters parameters = - modelImporterProviderService.createNewImporterProviderServiceParameters() as ModelImporterProviderServiceParameters + ModelImporterProviderServiceParameters parameters = importerService.createNewImporterProviderServiceParameters(importerProviderService) if (parameters.hasProperty('importFile')?.type != FileParameter) { - throw new ApiInternalException('MSXX', "Assigned JSON importer ${modelImporterProviderService.class.simpleName} " + - 'for model cannot import file content') + throw new ApiInternalException('SMS01', "Importer ${importerProviderService.class.simpleName} " + + 'for model cannot import file content') } - parameters.importFile = new FileParameter(fileContents: exportedJson.getBytes()) + byte[] resourceBytes = exportSubscribedModelFromSubscribedCatalogue(subscribedModel, exportLink) + + parameters.importFile = new FileParameter(fileContents: resourceBytes) parameters.folderId = folder.id parameters.finalised = true parameters.useDefaultAuthority = false - Model model = modelImporterProviderService.importDomain(userSecurityPolicyManager.user, parameters) + MdmDomain model = importerService.importDomain(userSecurityPolicyManager.user, importerProviderService, parameters) if (!model) { subscribedModel.errors.reject('invalid.subscribedmodel.import', 'Could not import SubscribedModel into local Catalogue') return subscribedModel.errors + } else if (!Model.isAssignableFrom(model.class) && !VersionedFolder.isAssignableFrom(model.class)) { + throw new ApiInternalException('SMS02', "Domain type ${model.domainType} cannot be imported") } - log.debug('Importing model {}, version {} from authority {}', model.label, model.modelVersion, model.authority) - if (modelService.countByAuthorityAndLabelAndVersion(model.authority, model.label, model.modelVersion)) { + log.debug('Importing domain {}, version {} from authority {}', model.label, model.modelVersion, model.authority) + MdmDomainService domainService = domainServices.find {it.handles(model.domainType)} + if (domainService.respondsTo('countByAuthorityAndLabelAndVersion') && domainService.countByAuthorityAndLabelAndVersion(model.authority, model.label, model.modelVersion)) { subscribedModel.errors.reject('invalid.subscribedmodel.import.already.exists', [model.authority, model.label, model.modelVersion].toArray(), 'Model from authority [{0}] with label [{1}] and version [{2}] already exists in catalogue') return subscribedModel.errors } - + if (!model.hasProperty('folder')) { + throw new ApiInternalException('SMS03', "Domain type ${model.domainType} cannot be imported into a Folder") + } model.folder = folder - modelService.validate(model) + domainService.validate(model) if (model.hasErrors()) { return model.errors } - log.debug('No errors in imported model') - - Model savedModel = modelService.saveModelWithContent(model) - log.debug('Saved model') + MdmDomain savedModel + if (domainService.respondsTo('saveModelWithContent')) { + savedModel = domainService.saveModelWithContent(model) + } else { + savedModel = domainService.save(model) + } + log.debug('Saved domain') if (securityPolicyManagerService) { log.debug('add security to saved model') @@ -193,16 +228,18 @@ class SubscribedModelService implements SecurableResourceService> exporters = subscribedCatalogueService.getAvailableExportersForResourceType(subscribedCatalogue, modelService.getUrlResourceName()) - - //Find a json exporter - Map exporterMap = exporters.find {it.fileType == 'text/json' && it.name == modelService.getJsonModelExporterProviderService().name} + private List getExportLinksForSubscribedModel(SubscribedModel subscribedModel) { + List sourcePublishedModels = subscribedCatalogueService.listPublishedModels(subscribedModel.subscribedCatalogue) + .findAll {pm -> pm.modelId == subscribedModel.subscribedModelId} + .sort {pm -> pm.lastUpdated} // Atom feeds may allow multiple versions of an entry with the same ID - //Can't use DataBindingUtils because of a clash with grails 'version' property - if (!exporterMap) { - throw new ApiBadRequestException('SMSXX', 'Cannot export model from subscribed catalogue as no JSON exporter available') + if (sourcePublishedModels) { + sourcePublishedModels.last().links + } else { + null } - exporterMap } void anonymise(String createdBy) { diff --git a/mdm-plugin-federation/grails-app/services/uk/ac/ox/softeng/maurodatamapper/federation/publish/PublishService.groovy b/mdm-plugin-federation/grails-app/services/uk/ac/ox/softeng/maurodatamapper/federation/publish/PublishService.groovy index 5cc45ba577..94c08ad040 100644 --- a/mdm-plugin-federation/grails-app/services/uk/ac/ox/softeng/maurodatamapper/federation/publish/PublishService.groovy +++ b/mdm-plugin-federation/grails-app/services/uk/ac/ox/softeng/maurodatamapper/federation/publish/PublishService.groovy @@ -52,7 +52,7 @@ class PublishService { links = modelService.getExporterProviderServices().sort().collect {exporterProviderService -> new Link(LINK_RELATIONSHIP_ALTERNATE, modelUrl + '/export/' + exporterProviderService.namespace + '/' + exporterProviderService.name + '/' + exporterProviderService.version).tap { - contentType = exporterProviderService.producesContentType + contentType = exporterProviderService.contentType } } } @@ -84,7 +84,7 @@ class PublishService { List newerPublishedModels = [] newerVersionTree.findAll {it -> Model model = it.versionAware - model.id != modelId && publishedModels.find {pm -> pm.modelId == model.id} + model.id != modelId && publishedModels.find {pm -> pm.modelId == model.id.toString()} }.each {vtm -> newerPublishedModels << getPublishedModel(vtm.versionAware, modelService).tap {previousModelId = vtm.parentVersionTreeModel.versionAware.id} } diff --git a/mdm-plugin-federation/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/federation/converter/AtomSubscribedCatalogueConverter.groovy b/mdm-plugin-federation/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/federation/converter/AtomSubscribedCatalogueConverter.groovy new file mode 100644 index 0000000000..e485b6187b --- /dev/null +++ b/mdm-plugin-federation/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/federation/converter/AtomSubscribedCatalogueConverter.groovy @@ -0,0 +1,61 @@ +/* + * Copyright 2020-2022 University of Oxford and Health and Social Care Information Centre, also known as NHS Digital + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package uk.ac.ox.softeng.maurodatamapper.federation.converter + +import uk.ac.ox.softeng.maurodatamapper.core.authority.Authority +import uk.ac.ox.softeng.maurodatamapper.federation.PublishedModel +import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedCatalogue +import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedCatalogueType +import uk.ac.ox.softeng.maurodatamapper.federation.web.FederationClient +import uk.ac.ox.softeng.maurodatamapper.util.Utils + +import grails.rest.Link +import groovy.xml.slurpersupport.GPathResult + +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter + +class AtomSubscribedCatalogueConverter implements SubscribedCatalogueConverter { + @Override + boolean handles(SubscribedCatalogueType type) { + type == SubscribedCatalogueType.ATOM + } + + @Override + Tuple2> getAuthorityAndPublishedModels(FederationClient federationClient, SubscribedCatalogue subscribedCatalogue) { + GPathResult subscribedCatalogueModelsFeed = federationClient.getSubscribedCatalogueModelsFromAtomFeed(subscribedCatalogue.apiKey) + + Authority subscribedAuthority = new Authority(label: subscribedCatalogueModelsFeed.author.name.text(), url: subscribedCatalogueModelsFeed.author.uri.text()) + + List publishedModels = subscribedCatalogueModelsFeed.entry.collect {entry -> + new PublishedModel().tap { + modelId = entry.id + modelLabel = entry.title + if (entry.updated.text()) lastUpdated = OffsetDateTime.parse(entry.updated.text(), DateTimeFormatter.ISO_OFFSET_DATE_TIME) + if (entry.published.text()) datePublished = OffsetDateTime.parse(entry.published.text(), DateTimeFormatter.ISO_OFFSET_DATE_TIME) + author = entry.author.name ?: subscribedCatalogueModelsFeed.author.name + description = entry.summary + links = entry.link.collect {link -> + new Link(LINK_RELATIONSHIP_ALTERNATE, link.@href.text()).tap {contentType = link.@type} + } + } + } + + return new Tuple2(subscribedAuthority, publishedModels) + } +} \ No newline at end of file diff --git a/mdm-plugin-federation/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/federation/converter/MauroJsonSubscribedCatalogueConverter.groovy b/mdm-plugin-federation/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/federation/converter/MauroJsonSubscribedCatalogueConverter.groovy new file mode 100644 index 0000000000..4b89ca710f --- /dev/null +++ b/mdm-plugin-federation/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/federation/converter/MauroJsonSubscribedCatalogueConverter.groovy @@ -0,0 +1,62 @@ +/* + * Copyright 2020-2022 University of Oxford and Health and Social Care Information Centre, also known as NHS Digital + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package uk.ac.ox.softeng.maurodatamapper.federation.converter + +import uk.ac.ox.softeng.maurodatamapper.core.authority.Authority +import uk.ac.ox.softeng.maurodatamapper.federation.PublishedModel +import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedCatalogue +import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedCatalogueType +import uk.ac.ox.softeng.maurodatamapper.federation.web.FederationClient +import uk.ac.ox.softeng.maurodatamapper.util.Utils +import uk.ac.ox.softeng.maurodatamapper.version.Version + +import grails.rest.Link + +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter + +class MauroJsonSubscribedCatalogueConverter implements SubscribedCatalogueConverter { + @Override + boolean handles(SubscribedCatalogueType type) { + type == SubscribedCatalogueType.MAURO_JSON + } + + @Override + Tuple2> getAuthorityAndPublishedModels(FederationClient federationClient, SubscribedCatalogue subscribedCatalogue) { + Map subscribedCatalogueModels = federationClient.getSubscribedCatalogueModels(subscribedCatalogue.apiKey) + + Authority subscribedAuthority = new Authority(label: subscribedCatalogueModels.authority.label, url: subscribedCatalogueModels.authority.url) + + List publishedModels = (subscribedCatalogueModels.publishedModels as List>).collect {pm -> + new PublishedModel().tap { + modelId = pm.modelId + modelLabel = pm.label + modelVersion = Version.from(pm.version) + modelType = pm.modelType + lastUpdated = OffsetDateTime.parse(pm.lastUpdated, DateTimeFormatter.ISO_OFFSET_DATE_TIME) + dateCreated = OffsetDateTime.parse(pm.dateCreated, DateTimeFormatter.ISO_OFFSET_DATE_TIME) + datePublished = OffsetDateTime.parse(pm.datePublished, DateTimeFormatter.ISO_OFFSET_DATE_TIME) + author = pm.author + description = pm.description + if (pm.links) links = pm.links.collect {link -> new Link(LINK_RELATIONSHIP_ALTERNATE, link.url).tap {contentType = link.contentType}} + } + } + + return new Tuple2(subscribedAuthority, publishedModels) + } +} diff --git a/mdm-plugin-federation/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/federation/converter/SubscribedCatalogueConverter.groovy b/mdm-plugin-federation/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/federation/converter/SubscribedCatalogueConverter.groovy new file mode 100644 index 0000000000..07f4128e85 --- /dev/null +++ b/mdm-plugin-federation/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/federation/converter/SubscribedCatalogueConverter.groovy @@ -0,0 +1,40 @@ +/* + * Copyright 2020-2022 University of Oxford and Health and Social Care Information Centre, also known as NHS Digital + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package uk.ac.ox.softeng.maurodatamapper.federation.converter + +import uk.ac.ox.softeng.maurodatamapper.core.authority.Authority +import uk.ac.ox.softeng.maurodatamapper.federation.PublishedModel +import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedCatalogue +import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedCatalogueType +import uk.ac.ox.softeng.maurodatamapper.federation.web.FederationClient + +trait SubscribedCatalogueConverter { + static final String LINK_RELATIONSHIP_ALTERNATE = 'alternate' + + abstract boolean handles(SubscribedCatalogueType type) + + abstract Tuple2> getAuthorityAndPublishedModels(FederationClient federationClient, SubscribedCatalogue subscribedCatalogue) + + Authority getAuthority(FederationClient federationClient, SubscribedCatalogue subscribedCatalogue) { + getAuthorityAndPublishedModels(federationClient, subscribedCatalogue).v1 + } + + List getPublishedModels(FederationClient federationClient, SubscribedCatalogue subscribedCatalogue) { + getAuthorityAndPublishedModels(federationClient, subscribedCatalogue).v2 + } +} \ No newline at end of file diff --git a/mdm-plugin-federation/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/federation/rest/transport/SubscribedModelFederationParams.groovy b/mdm-plugin-federation/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/federation/rest/transport/SubscribedModelFederationParams.groovy new file mode 100644 index 0000000000..ad646da465 --- /dev/null +++ b/mdm-plugin-federation/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/federation/rest/transport/SubscribedModelFederationParams.groovy @@ -0,0 +1,36 @@ +/* + * Copyright 2020-2022 University of Oxford and Health and Social Care Information Centre, also known as NHS Digital + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package uk.ac.ox.softeng.maurodatamapper.federation.rest.transport + +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.importer.ImporterProviderServiceData +import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedModel + +import grails.validation.Validateable + +class SubscribedModelFederationParams implements Validateable { + SubscribedModel subscribedModel + String url + String contentType + ImporterProviderServiceData importerProviderService + + static constraints = { + url nullable: true, blank: false + contentType nullable: true, blank: false + importerProviderService nullable: true + } +} diff --git a/mdm-plugin-federation/grails-app/views/publishedModel/_publishedModel.gml b/mdm-plugin-federation/grails-app/views/publishedModel/_publishedModel.gml index 8492265159..f5f1552a89 100644 --- a/mdm-plugin-federation/grails-app/views/publishedModel/_publishedModel.gml +++ b/mdm-plugin-federation/grails-app/views/publishedModel/_publishedModel.gml @@ -9,13 +9,12 @@ PublishedModel pm = publishedModel as PublishedModel publishedModel { modelId pm.modelId - title pm.title label pm.modelLabel version pm.modelVersion - modelType pm.modelType + if (pm.modelType) modelType pm.modelType lastUpdated OffsetDateTimeConverter.toString(pm.lastUpdated) - dateCreated OffsetDateTimeConverter.toString(pm.dateCreated) - datePublished OffsetDateTimeConverter.toString(pm.datePublished) + if (pm.dateCreated) dateCreated OffsetDateTimeConverter.toString(pm.dateCreated) + if (pm.datePublished) datePublished OffsetDateTimeConverter.toString(pm.datePublished) if (pm.author) author pm.author if (pm.description) description pm.description if (pm.previousModelId) previousModelId pm.previousModelId diff --git a/mdm-plugin-federation/grails-app/views/publishedModel/_publishedModel.gson b/mdm-plugin-federation/grails-app/views/publishedModel/_publishedModel.gson index 468e677e3f..673d8a4927 100644 --- a/mdm-plugin-federation/grails-app/views/publishedModel/_publishedModel.gson +++ b/mdm-plugin-federation/grails-app/views/publishedModel/_publishedModel.gson @@ -6,13 +6,12 @@ model { json { modelId publishedModel.modelId - title publishedModel.title label publishedModel.modelLabel version publishedModel.modelVersion - modelType publishedModel.modelType + if (publishedModel.modelType) modelType publishedModel.modelType lastUpdated publishedModel.lastUpdated - dateCreated publishedModel.dateCreated - datePublished publishedModel.datePublished + if (publishedModel.dateCreated) dateCreated publishedModel.dateCreated + if (publishedModel.datePublished) datePublished publishedModel.datePublished if (publishedModel.author) author publishedModel.author if (publishedModel.description) description publishedModel.description if (publishedModel.previousModelId) previousModelId publishedModel.previousModelId diff --git a/mdm-plugin-federation/grails-app/views/subscribedCatalogue/_subscribedCatalogue.gson b/mdm-plugin-federation/grails-app/views/subscribedCatalogue/_subscribedCatalogue.gson index 90acdddfe7..7927e54cf4 100644 --- a/mdm-plugin-federation/grails-app/views/subscribedCatalogue/_subscribedCatalogue.gson +++ b/mdm-plugin-federation/grails-app/views/subscribedCatalogue/_subscribedCatalogue.gson @@ -8,6 +8,7 @@ json { id subscribedCatalogue.id url subscribedCatalogue.url label subscribedCatalogue.label + subscribedCatalogueType subscribedCatalogue.subscribedCatalogueType.label if (subscribedCatalogue.description) { description subscribedCatalogue.description diff --git a/mdm-plugin-federation/grails-app/views/subscribedCatalogue/_subscribedCatalogue_full.gson b/mdm-plugin-federation/grails-app/views/subscribedCatalogue/_subscribedCatalogue_full.gson index 46818d416e..a989ef477b 100644 --- a/mdm-plugin-federation/grails-app/views/subscribedCatalogue/_subscribedCatalogue_full.gson +++ b/mdm-plugin-federation/grails-app/views/subscribedCatalogue/_subscribedCatalogue_full.gson @@ -8,6 +8,7 @@ json { id subscribedCatalogue.id url subscribedCatalogue.url label subscribedCatalogue.label + subscribedCatalogueType subscribedCatalogue.subscribedCatalogueType.label if (subscribedCatalogue.description) { description subscribedCatalogue.description diff --git a/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/AtomSubscribedCatalogueFunctionalSpec.groovy b/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/AtomSubscribedCatalogueFunctionalSpec.groovy new file mode 100644 index 0000000000..48f09570ee --- /dev/null +++ b/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/AtomSubscribedCatalogueFunctionalSpec.groovy @@ -0,0 +1,175 @@ +/* + * Copyright 2020-2022 University of Oxford and Health and Social Care Information Centre, also known as NHS Digital + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package uk.ac.ox.softeng.maurodatamapper.federation + +import uk.ac.ox.softeng.maurodatamapper.federation.test.BaseSubscribedCatalogueFunctionalSpec + +import grails.testing.mixin.integration.Integration +import groovy.util.logging.Slf4j + +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter + +import static io.micronaut.http.HttpStatus.CREATED +import static io.micronaut.http.HttpStatus.NO_CONTENT +import static io.micronaut.http.HttpStatus.OK + +@Integration +@Slf4j +class AtomSubscribedCatalogueFunctionalSpec extends BaseSubscribedCatalogueFunctionalSpec { + //note: using a groovy string like "http://localhost:$serverPort/" causes the url to be stripped when saving + @Override + Map getValidJson() { + [ + url : "http://localhost:$serverPort/api/feeds/all".toString(), + apiKey : '67421316-66a5-4830-9156-b1ba77bba5d1', + label : 'Functional Test Label', + subscribedCatalogueType: 'Atom', + description : 'Functional Test Description', + refreshPeriod : 7 + ] + } + + @Override + Map getInvalidJson() { + [ + url : 'wibble', + apiKey: '67421316-66a5-4830-9156-b1ba77bba5d1' + ] + } + + //note: any-string on the Url is a workaround after the previous note + @Override + String getExpectedShowJson() { + '''{ + "id": "${json-unit.matches:id}", + "url": "${json-unit.any-string}", + "label": 'Functional Test Label', + "subscribedCatalogueType": 'Atom', + "description": 'Functional Test Description', + "refreshPeriod": 7, + "apiKey": "67421316-66a5-4830-9156-b1ba77bba5d1" +}''' + } + + @Override + String getExpectedOpenAccessShowJson() { + '''{ + "id": "${json-unit.matches:id}", + "url": "${json-unit.any-string}", + "label": 'Functional Test Label', + "subscribedCatalogueType": 'Atom', + "description": 'Functional Test Description', + "refreshPeriod": 7 +}''' + } + + @Override + String getExpectedOpenAccessIndexJson() { + '''{ + "count": 1, + "items": [ + { + "id": "${json-unit.matches:id}", + "url": "${json-unit.any-string}", + "label": "Functional Test Label", + "subscribedCatalogueType": 'Atom', + "description": "Functional Test Description", + "refreshPeriod": 7 + } + ] +}''' + } + + void 'P01 : Test the publishedModels endpoint'() { + given: + POST('', getValidJson()) + verifyResponse(CREATED, response) + String subscribedCatalogueId = responseBody().id + + when: + GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels", MAP_ARG, true) + + then: + verifyResponse OK, response + verifyBaseJsonResponse(responseBody(), true) + responseBody().items.size() == 1 + + and: + verifyJsonPublishedModel(responseBody().items.find {it.label == 'Finalised Example Test DataModel 1.0.0'}, 'dataModels', getDataModelExporters()) + + cleanup: + DELETE(subscribedCatalogueId) + verifyResponse NO_CONTENT, response + } + + void 'N01 : Test the newerVersions endpoint (with no newer versions)'() { + given: + POST('', getValidJson()) + verifyResponse(CREATED, response) + String subscribedCatalogueId = responseBody().id + + when: + GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels/${finalisedSimpleDataModelId}/newerVersions", MAP_ARG, true) + + then: 'newer versions not supported using Atom catalogues' + verifyResponse OK, response + !responseBody() + + cleanup: + DELETE(subscribedCatalogueId) + verifyResponse NO_CONTENT, response + } + + void 'N02 : Test the newerVersions endpoint (with newer versions)'() { + given: + Tuple tuple = getNewerDataModelIds() + POST('', getValidJson()) + verifyResponse(CREATED, response) + String subscribedCatalogueId = responseBody().id + + when: + GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels/${finalisedSimpleDataModelId}/newerVersions", MAP_ARG, true) + + then: 'newer versions not supported using Atom catalogues' + verifyResponse OK, response + !responseBody() + + cleanup: + DELETE("dataModels/${tuple.v1}?permanent=true", MAP_ARG, true) + verifyResponse NO_CONTENT, response + DELETE("dataModels/${tuple.v2}?permanent=true", MAP_ARG, true) + verifyResponse NO_CONTENT, response + DELETE(subscribedCatalogueId) + verifyResponse NO_CONTENT, response + } + + private void verifyJsonPublishedModel(Map publishedModel, String modelEndpoint, Map exporters) { + assert publishedModel + assert publishedModel.modelId ==~ /urn:uuid:\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/ + assert publishedModel.label + assert OffsetDateTime.parse(publishedModel.datePublished, DateTimeFormatter.ISO_OFFSET_DATE_TIME) + assert OffsetDateTime.parse(publishedModel.lastUpdated, DateTimeFormatter.ISO_OFFSET_DATE_TIME) + assert publishedModel.links.each {link -> + assert link.contentType + String exporterUrl = exporters.get(link.contentType) + assert link.url ==~ /http:\/\/localhost:$serverPort\/api\/$modelEndpoint\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/export\\/$exporterUrl/ + } + } +} + diff --git a/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/MauroJsonSubscribedCatalogueFunctionalSpec.groovy b/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/MauroJsonSubscribedCatalogueFunctionalSpec.groovy new file mode 100644 index 0000000000..c7bafdca68 --- /dev/null +++ b/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/MauroJsonSubscribedCatalogueFunctionalSpec.groovy @@ -0,0 +1,186 @@ +/* + * Copyright 2020-2022 University of Oxford and Health and Social Care Information Centre, also known as NHS Digital + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package uk.ac.ox.softeng.maurodatamapper.federation + +import uk.ac.ox.softeng.maurodatamapper.federation.test.BaseSubscribedCatalogueFunctionalSpec +import uk.ac.ox.softeng.maurodatamapper.version.Version + +import grails.testing.mixin.integration.Integration +import groovy.util.logging.Slf4j + +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter + +import static io.micronaut.http.HttpStatus.CREATED +import static io.micronaut.http.HttpStatus.NO_CONTENT +import static io.micronaut.http.HttpStatus.OK + +@Integration +@Slf4j +class MauroJsonSubscribedCatalogueFunctionalSpec extends BaseSubscribedCatalogueFunctionalSpec { + //note: using a groovy string like "http://localhost:$serverPort/" causes the url to be stripped when saving + @Override + Map getValidJson() { + [ + url : "http://localhost:$serverPort".toString(), + apiKey : '67421316-66a5-4830-9156-b1ba77bba5d1', + label : 'Functional Test Label', + subscribedCatalogueType: 'Mauro JSON', + description : 'Functional Test Description', + refreshPeriod : 7 + ] + } + + @Override + Map getInvalidJson() { + [ + url : 'wibble', + apiKey: '67421316-66a5-4830-9156-b1ba77bba5d1' + ] + } + + //note: any-string on the Url is a workaround after the previous note + @Override + String getExpectedShowJson() { + '''{ + "id": "${json-unit.matches:id}", + "url": "${json-unit.any-string}", + "label": 'Functional Test Label', + "subscribedCatalogueType": 'Mauro JSON', + "description": 'Functional Test Description', + "refreshPeriod": 7, + "apiKey": "67421316-66a5-4830-9156-b1ba77bba5d1" +}''' + } + + @Override + String getExpectedOpenAccessShowJson() { + '''{ + "id": "${json-unit.matches:id}", + "url": "${json-unit.any-string}", + "label": 'Functional Test Label', + "subscribedCatalogueType": 'Mauro JSON', + "description": 'Functional Test Description', + "refreshPeriod": 7 +}''' + } + + @Override + String getExpectedOpenAccessIndexJson() { + '''{ + "count": 1, + "items": [ + { + "id": "${json-unit.matches:id}", + "url": "${json-unit.any-string}", + "label": "Functional Test Label", + "subscribedCatalogueType": 'Mauro JSON', + "description": "Functional Test Description", + "refreshPeriod": 7 + } + ] +}''' + } + + void 'P01 : Test the publishedModels endpoint'() { + given: + POST('', getValidJson()) + verifyResponse(CREATED, response) + String subscribedCatalogueId = responseBody().id + + when: + GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels", MAP_ARG, true) + + then: + verifyResponse OK, response + verifyBaseJsonResponse(responseBody(), true) + responseBody().items.size() == 1 + + and: + verifyJsonPublishedModel(responseBody().items.find {it.label == 'Finalised Example Test DataModel' && it.version == '1.0.0'}, 'DataModel', 'dataModels', getDataModelExporters()) + + cleanup: + DELETE(subscribedCatalogueId) + verifyResponse NO_CONTENT, response + } + + void 'N01 : Test the newerVersions endpoint (with no newer versions)'() { + given: + POST('', getValidJson()) + verifyResponse(CREATED, response) + String subscribedCatalogueId = responseBody().id + + when: + GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels/${finalisedSimpleDataModelId}/newerVersions", MAP_ARG, true) + + then: + verifyResponse OK, response + verifyBaseNewerVersionsJsonResponse(responseBody(), false) + + cleanup: + DELETE(subscribedCatalogueId) + verifyResponse NO_CONTENT, response + } + + void 'N02 : Test the newerVersions endpoint (with newer versions)'() { + given: + Tuple tuple = getNewerDataModelIds() + POST('', getValidJson()) + verifyResponse(CREATED, response) + String subscribedCatalogueId = responseBody().id + + when: + GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels/${finalisedSimpleDataModelId}/newerVersions", MAP_ARG, true) + + then: + verifyResponse OK, response + verifyBaseNewerVersionsJsonResponse(responseBody(), true) + responseBody().newerPublishedModels.size() == 2 + + and: + verifyJsonPublishedModel(responseBody().newerPublishedModels.find {it.label == 'Finalised Example Test DataModel' && it.version == '2.0.0'}, 'DataModel', 'dataModels', getDataModelExporters(), + true) + verifyJsonPublishedModel(responseBody().newerPublishedModels.find {it.label == 'Finalised Example Test DataModel' && it.version == '3.0.0'}, 'DataModel', 'dataModels', getDataModelExporters(), + true) + + cleanup: + DELETE("dataModels/${tuple.v1}?permanent=true", MAP_ARG, true) + verifyResponse NO_CONTENT, response + DELETE("dataModels/${tuple.v2}?permanent=true", MAP_ARG, true) + verifyResponse NO_CONTENT, response + DELETE(subscribedCatalogueId) + verifyResponse NO_CONTENT, response + } + + private void verifyJsonPublishedModel(Map publishedModel, String modelType, String modelEndpoint, Map exporters, boolean newerVersion = false) { + assert publishedModel + assert publishedModel.modelId ==~ /\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/ + assert publishedModel.label + assert Version.from(publishedModel.version) + assert publishedModel.modelType == modelType + assert OffsetDateTime.parse(publishedModel.datePublished, DateTimeFormatter.ISO_OFFSET_DATE_TIME) + assert OffsetDateTime.parse(publishedModel.lastUpdated, DateTimeFormatter.ISO_OFFSET_DATE_TIME) + assert OffsetDateTime.parse(publishedModel.dateCreated, DateTimeFormatter.ISO_OFFSET_DATE_TIME) + assert publishedModel.links.each {link -> + assert link.contentType + String exporterUrl = exporters.get(link.contentType) + assert link.url ==~ /http:\/\/localhost:$serverPort\/api\/$modelEndpoint\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/export\\/$exporterUrl/ + } + if (newerVersion) assert publishedModel.previousModelId ==~ /\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/ + } +} diff --git a/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedModelFunctionalSpec.groovy b/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedModelFunctionalSpec.groovy index 5e4c2659f0..2787ecccc7 100644 --- a/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedModelFunctionalSpec.groovy +++ b/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedModelFunctionalSpec.groovy @@ -41,11 +41,12 @@ import spock.lang.Shared */ @Integration @Slf4j -// Requires a connection to the CD environment, if this connection is not available +// Requires a connection to the CD environment, running a version providing the /types endpoint @Requires({ - String url = 'https://modelcatalogue.cs.ox.ac.uk/continuous-deployment' - HttpURLConnection connection = url.toURL().openConnection() as HttpURLConnection + String url = 'https://modelcatalogue.cs.ox.ac.uk/continuous-deployment/api/admin/subscribedCatalogues/types' + HttpURLConnection connection = (url).toURL().openConnection() as HttpURLConnection connection.setRequestMethod('GET') + connection.setRequestProperty('apiKey', '720e60bc-3993-48d4-a17e-c3a13f037c7e') connection.connect() connection.getResponseCode() == 200 }) @@ -54,6 +55,9 @@ class SubscribedModelFunctionalSpec extends BaseFunctionalSpec { @Shared UUID subscribedCatalogueId + @Shared + UUID atomSubscribedCatalogueId + @Shared UUID folderId @@ -67,11 +71,21 @@ class SubscribedModelFunctionalSpec extends BaseFunctionalSpec { subscribedCatalogueId = new SubscribedCatalogue(url: 'https://modelcatalogue.cs.ox.ac.uk/continuous-deployment', apiKey: '720e60bc-3993-48d4-a17e-c3a13f037c7e', - label: 'Functional Test Label', + label: 'Functional Test Subscribed Catalogue (Mauro JSON)', + subscribedCatalogueType: SubscribedCatalogueType.MAURO_JSON, description: 'Functional Test Description', refreshPeriod: 7, createdBy: StandardEmailAddress.FUNCTIONAL_TEST).save(flush: true).id assert subscribedCatalogueId + + atomSubscribedCatalogueId = new SubscribedCatalogue(url: 'https://modelcatalogue.cs.ox.ac.uk/continuous-deployment/api/feeds/all', + apiKey: '720e60bc-3993-48d4-a17e-c3a13f037c7e', + label: 'Functional Test Subscribed Catalogue (Atom)', + subscribedCatalogueType: SubscribedCatalogueType.ATOM, + description: 'Functional Test Description', + refreshPeriod: 7, + createdBy: StandardEmailAddress.FUNCTIONAL_TEST).save(flush: true).id + assert atomSubscribedCatalogueId } @Transactional @@ -89,24 +103,68 @@ class SubscribedModelFunctionalSpec extends BaseFunctionalSpec { "${getResourcePath()}" } - String createNewItem(Map model) { - POST(getSavePath(), model, MAP_ARG, true) + String getSavePathForAtom() { + "subscribedCatalogues/${getAtomSubscribedCatalogueId()}/subscribedModels" + } + + String createNewItem(Map model, String savePath = null) { + POST(savePath ?: getSavePath(), model, MAP_ARG, true) verifyResponse(HttpStatus.CREATED, response) response.body().id } Map getValidJson() { [ - subscribedModelId : '427d1243-4f89-46e8-8f8f-8424890b5083', - folderId : getFolderId(), - subscribedModelType: 'DataModel' + subscribedModel: [ + subscribedModelId: '427d1243-4f89-46e8-8f8f-8424890b5083', + folderId : getFolderId() + ] + ] + } + + Map getValidJsonWithParams() { + [ + subscribedModel : [ + subscribedModelId: '427d1243-4f89-46e8-8f8f-8424890b5083', + folderId : getFolderId() + ], + contentType : 'application/mauro.datamodel+xml', + importerProviderService: [ + name : 'DataModelXmlImporterService', + namespace: 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer' + ] + ] + } + + Map getValidJsonForAtom() { + [ + subscribedModel: [ + subscribedModelId: 'urn:uuid:427d1243-4f89-46e8-8f8f-8424890b5083', + folderId : getFolderId() + ] + ] + } + + Map getValidJsonForAtomWithParams() { + [ + subscribedModel : [ + subscribedModelId: 'urn:uuid:427d1243-4f89-46e8-8f8f-8424890b5083', + folderId : getFolderId() + ], + contentType : 'application/mauro.datamodel+xml', + importerProviderService: [ + name : 'DataModelXmlImporterService', + namespace: 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer' + ] ] } Map getInvalidJson() { [ - subscribedModelId: null, - folderId: getFolderId() + subscribedModel: [ + subscribedModelId: null, + folderId : getFolderId() + ] ] } @@ -133,19 +191,29 @@ class SubscribedModelFunctionalSpec extends BaseFunctionalSpec { } @Transactional - void 'R2 : Test the save action correctly persists an instance'() { + void 'R2a : Test the save action correctly persists an instance (for #catalogueType)'() { + given: + String savePath + Map validJson + if (SubscribedCatalogueType.findForLabel(catalogueType) == SubscribedCatalogueType.MAURO_JSON) { + savePath = getSavePath() + validJson = getValidJson() + } else { + savePath = getSavePathForAtom() + validJson = getValidJsonForAtom() + } + when: 'The save action is executed with no content' log.debug('No content save') - POST(getSavePath(), [:], MAP_ARG, true) + POST(savePath, [:], MAP_ARG, true) then: 'The response is correct' verifyResponse HttpStatus.UNPROCESSABLE_ENTITY, response - response.body().total >= 1 - response.body().errors.size() == response.body().total + response.body().errors.first().message == 'Property [subscribedModel] of class [class uk.ac.ox.softeng.maurodatamapper.federation.rest.transport.SubscribedModelFederationParams] cannot be null' when: 'The save action is executed with invalid data' log.debug('Invalid content save') - POST(getSavePath(), invalidJson, MAP_ARG, true) + POST(savePath, invalidJson, MAP_ARG, true) then: 'The response is correct' verifyResponse HttpStatus.UNPROCESSABLE_ENTITY, response @@ -154,7 +222,7 @@ class SubscribedModelFunctionalSpec extends BaseFunctionalSpec { when: 'The save action is executed with valid data' log.debug('Valid content save') - createNewItem(validJson) + createNewItem(validJson, savePath) then: 'The response is correct' verifyResponse HttpStatus.CREATED, response @@ -162,10 +230,44 @@ class SubscribedModelFunctionalSpec extends BaseFunctionalSpec { String localModelId = response.body().localModelId cleanup: - DELETE(id) + DELETE(savePath + '/' + id, MAP_ARG, true) + assert response.status() == HttpStatus.NO_CONTENT + DELETE("dataModels/${localModelId}?permanent=true", MAP_ARG, true) + assert response.status() == HttpStatus.NO_CONTENT + + where: + catalogueType << SubscribedCatalogueType.labels() + } + + @Transactional + void 'R2b : Test the save action with parameters correctly persists an instance (for #catalogueType)'() { + given: + String savePath + Map validJson + if (SubscribedCatalogueType.findForLabel(catalogueType) == SubscribedCatalogueType.MAURO_JSON) { + savePath = getSavePath() + validJson = getValidJsonWithParams() + } else { + savePath = getSavePathForAtom() + validJson = getValidJsonForAtomWithParams() + } + + when: 'The save action is executed with valid data with parameters' + createNewItem(validJson, savePath) + + then: 'The response is correct' + verifyResponse HttpStatus.CREATED, response + String id = response.body().id + String localModelId = response.body().localModelId + + cleanup: + DELETE(savePath + '/' + id, MAP_ARG, true) assert response.status() == HttpStatus.NO_CONTENT DELETE("dataModels/${localModelId}?permanent=true", MAP_ARG, true) assert response.status() == HttpStatus.NO_CONTENT + + where: + catalogueType << SubscribedCatalogueType.labels() } void 'R3 : Test the index action with content'() { diff --git a/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/atom/FeedFunctionalSpec.groovy b/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/atom/FeedFunctionalSpec.groovy index 335d4a0a2a..d9b3ae01f3 100644 --- a/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/atom/FeedFunctionalSpec.groovy +++ b/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/atom/FeedFunctionalSpec.groovy @@ -192,8 +192,8 @@ class FeedFunctionalSpec extends BaseFunctionalSpec implements XmlValidator { private static Map getDataModelExporters() { [ - 'application/mdm+json': 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/3.1', - 'application/mdm+xml' : 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelXmlExporterService/5.1' + 'application/mauro.datamodel+json': 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/3.1', + 'application/mauro.datamodel+xml' : 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelXmlExporterService/5.1' ] } } diff --git a/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/publish/PublishFunctionalSpec.groovy b/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/publish/PublishFunctionalSpec.groovy index ca03478b61..a6d82894ea 100644 --- a/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/publish/PublishFunctionalSpec.groovy +++ b/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/publish/PublishFunctionalSpec.groovy @@ -138,7 +138,7 @@ class PublishFunctionalSpec extends BaseFunctionalSpec implements XmlValidator { responseBody().publishedModels.size() == 1 and: - verifyJsonPublishedModel(responseBody().publishedModels.find {it.title == 'FunctionalTest DataModel 1.0.0'}, 'DataModel', 'dataModels', getDataModelExporters()) + verifyJsonPublishedModel(responseBody().publishedModels.find {it.label == 'FunctionalTest DataModel' && it.version == '1.0.0'}, 'DataModel', 'dataModels', getDataModelExporters()) when: Map publishedModel = responseBody().publishedModels.first() @@ -162,7 +162,7 @@ class PublishFunctionalSpec extends BaseFunctionalSpec implements XmlValidator { result.publishedModels.children().size() == 1 and: - verifyXmlPublishedModel(result.publishedModels.publishedModel.find {it.title == 'FunctionalTest DataModel 1.0.0'}, 'DataModel', 'dataModels', getDataModelExporters()) + verifyXmlPublishedModel(result.publishedModels.publishedModel.find {it.label == 'FunctionalTest DataModel' && it.version == '1.0.0'}, 'DataModel', 'dataModels', getDataModelExporters()) when: GPathResult publishedModel = result.publishedModels.publishedModel.first() @@ -210,9 +210,9 @@ class PublishFunctionalSpec extends BaseFunctionalSpec implements XmlValidator { responseBody().newerPublishedModels.size() == 2 and: - verifyJsonPublishedModel(responseBody().newerPublishedModels.find {it.title == 'FunctionalTest DataModel 2.0.0'}, 'DataModel', 'dataModels', getDataModelExporters(), + verifyJsonPublishedModel(responseBody().newerPublishedModels.find {it.label == 'FunctionalTest DataModel' && it.version == '2.0.0'}, 'DataModel', 'dataModels', getDataModelExporters(), true) - verifyJsonPublishedModel(responseBody().newerPublishedModels.find {it.title == 'FunctionalTest DataModel 3.0.0'}, 'DataModel', 'dataModels', getDataModelExporters(), + verifyJsonPublishedModel(responseBody().newerPublishedModels.find {it.label == 'FunctionalTest DataModel' && it.version == '3.0.0'}, 'DataModel', 'dataModels', getDataModelExporters(), true) responseBody().newerPublishedModels.each {publishedModel -> assert publishedModel.description == 'Some random desc' @@ -238,9 +238,9 @@ class PublishFunctionalSpec extends BaseFunctionalSpec implements XmlValidator { result.newerPublishedModels.children().size() == 2 and: - verifyXmlPublishedModel(result.newerPublishedModels.publishedModel.find {it.title == 'FunctionalTest DataModel 2.0.0'}, 'DataModel', 'dataModels', + verifyXmlPublishedModel(result.newerPublishedModels.publishedModel.find {it.label == 'FunctionalTest DataModel' && it.version == '2.0.0'}, 'DataModel', 'dataModels', getDataModelExporters(), true) - verifyXmlPublishedModel(result.newerPublishedModels.publishedModel.find {it.title == 'FunctionalTest DataModel 3.0.0'}, 'DataModel', 'dataModels', + verifyXmlPublishedModel(result.newerPublishedModels.publishedModel.find {it.label == 'FunctionalTest DataModel' && it.version == '3.0.0'}, 'DataModel', 'dataModels', getDataModelExporters(), true) result.newerPublishedModels.publishedModel.each {publishedModel -> assert publishedModel.description == 'Some random desc' @@ -261,7 +261,6 @@ class PublishFunctionalSpec extends BaseFunctionalSpec implements XmlValidator { assert publishedModel.modelId ==~ /\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/ assert publishedModel.label assert Version.from(publishedModel.version) - assert publishedModel.title == publishedModel.label + ' ' + publishedModel.version assert publishedModel.modelType == modelType assert OffsetDateTime.parse(publishedModel.datePublished, DateTimeFormatter.ISO_OFFSET_DATE_TIME) assert OffsetDateTime.parse(publishedModel.lastUpdated, DateTimeFormatter.ISO_OFFSET_DATE_TIME) @@ -279,7 +278,6 @@ class PublishFunctionalSpec extends BaseFunctionalSpec implements XmlValidator { assert publishedModel.modelId.text() ==~ /\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/ assert publishedModel.label.text() assert Version.from(publishedModel.version.text()) - assert publishedModel.title == publishedModel.label.text() + ' ' + publishedModel.version.text() assert publishedModel.modelType == modelType assert OffsetDateTime.parse(publishedModel.datePublished.text(), DateTimeFormatter.ISO_OFFSET_DATE_TIME) assert OffsetDateTime.parse(publishedModel.lastUpdated.text(), DateTimeFormatter.ISO_OFFSET_DATE_TIME) @@ -352,8 +350,8 @@ class PublishFunctionalSpec extends BaseFunctionalSpec implements XmlValidator { private static Map getDataModelExporters() { [ - 'application/mdm+json': 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/3.1', - 'application/mdm+xml' : 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelXmlExporterService/5.1' + 'application/mauro.datamodel+json': 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/3.1', + 'application/mauro.datamodel+xml' : 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelXmlExporterService/5.1' ] } } diff --git a/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedCatalogueFunctionalSpec.groovy b/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/test/BaseSubscribedCatalogueFunctionalSpec.groovy similarity index 50% rename from mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedCatalogueFunctionalSpec.groovy rename to mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/test/BaseSubscribedCatalogueFunctionalSpec.groovy index fe6fc8899f..34772c5f6b 100644 --- a/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedCatalogueFunctionalSpec.groovy +++ b/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/test/BaseSubscribedCatalogueFunctionalSpec.groovy @@ -15,13 +15,14 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package uk.ac.ox.softeng.maurodatamapper.federation +package uk.ac.ox.softeng.maurodatamapper.federation.test +import uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress import uk.ac.ox.softeng.maurodatamapper.core.container.Folder import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.bootstrap.BootstrapModels +import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedCatalogue import uk.ac.ox.softeng.maurodatamapper.test.functional.ResourceFunctionalSpec -import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Transactional import grails.testing.mixin.integration.Integration @@ -32,8 +33,6 @@ import spock.lang.Shared import java.time.OffsetDateTime import java.time.format.DateTimeFormatter -import static uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress.FUNCTIONAL_TEST - import static io.micronaut.http.HttpStatus.CREATED import static io.micronaut.http.HttpStatus.NO_CONTENT import static io.micronaut.http.HttpStatus.OK @@ -46,6 +45,7 @@ import static io.micronaut.http.HttpStatus.OK * | PUT | /api/admin/subscribedCatalogues/${id} | Action: update | * | GET | /api/admin/subscribedCatalogues/${id} | Action: show | * | GET | /api/admin/subscribedCatalogues/${subscribedCatalogueId}/testConnection | Action: testConnection | + * | GET | /api/subscribedCatalogues | Action: types | * | GET | /api/subscribedCatalogues | Action: index | * | GET | /api/subscribedCatalogues/${id} | Action: show | * | GET | /api/subscribedCatalogues/${subscribedCatalogueId}/testConnection | Action: testConnection | @@ -54,7 +54,7 @@ import static io.micronaut.http.HttpStatus.OK */ @Integration @Slf4j -class SubscribedCatalogueFunctionalSpec extends ResourceFunctionalSpec { +abstract class BaseSubscribedCatalogueFunctionalSpec extends ResourceFunctionalSpec { @Shared UUID finalisedSimpleDataModelId @@ -68,7 +68,7 @@ class SubscribedCatalogueFunctionalSpec extends ResourceFunctionalSpec exporters, boolean newerVersion = false) { - assert publishedModel - assert publishedModel.modelId ==~ /\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/ - assert publishedModel.label - assert Version.from(publishedModel.version) - assert publishedModel.title == publishedModel.label + ' ' + publishedModel.version - assert publishedModel.modelType == modelType - assert OffsetDateTime.parse(publishedModel.datePublished, DateTimeFormatter.ISO_OFFSET_DATE_TIME) - assert OffsetDateTime.parse(publishedModel.lastUpdated, DateTimeFormatter.ISO_OFFSET_DATE_TIME) - assert OffsetDateTime.parse(publishedModel.dateCreated, DateTimeFormatter.ISO_OFFSET_DATE_TIME) - assert publishedModel.links.each {link -> - assert link.contentType - String exporterUrl = exporters.get(link.contentType) - assert link.url ==~ /http:\/\/localhost:$serverPort\/api\/$modelEndpoint\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/export\\/$exporterUrl/ - } - if (newerVersion) assert publishedModel.previousModelId ==~ /\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/ + verifyJsonResponse OK, '''[ + "Atom", + "Mauro JSON" + ]''' } - private void verifyBaseJsonResponse(Map responseBody, boolean expectEntries) { + protected void verifyBaseJsonResponse(Map responseBody, boolean expectEntries) { if (expectEntries) { assert responseBody.items.size() > 0 assert responseBody.items.size() == responseBody.count @@ -318,7 +193,7 @@ class SubscribedCatalogueFunctionalSpec extends ResourceFunctionalSpec responseBody, boolean expectEntries) { + protected void verifyBaseNewerVersionsJsonResponse(Map responseBody, boolean expectEntries) { assert OffsetDateTime.parse(responseBody.lastUpdated, DateTimeFormatter.ISO_OFFSET_DATE_TIME) if (expectEntries) { @@ -328,10 +203,10 @@ class SubscribedCatalogueFunctionalSpec extends ResourceFunctionalSpec getDataModelExporters() { + protected static Map getDataModelExporters() { [ - 'application/mdm+json': 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/3.1', - 'application/mdm+xml' : 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelXmlExporterService/5.1' + 'application/mauro.datamodel+json': 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/3.1', + 'application/mauro.datamodel+xml' : 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelXmlExporterService/5.1' ] } } diff --git a/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/test/BaseSubscribedModelServiceIntegrationSpec.groovy b/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/test/BaseSubscribedModelServiceIntegrationSpec.groovy index c61a81a5cd..8e8a7b3901 100644 --- a/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/test/BaseSubscribedModelServiceIntegrationSpec.groovy +++ b/mdm-plugin-federation/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/federation/test/BaseSubscribedModelServiceIntegrationSpec.groovy @@ -19,7 +19,7 @@ package uk.ac.ox.softeng.maurodatamapper.federation.test import uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress import uk.ac.ox.softeng.maurodatamapper.core.container.Folder -import uk.ac.ox.softeng.maurodatamapper.core.model.Model +import uk.ac.ox.softeng.maurodatamapper.core.importer.ImporterService import uk.ac.ox.softeng.maurodatamapper.core.model.ModelService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.ModelImporterProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.FileParameter @@ -27,15 +27,19 @@ import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.ModelIm import uk.ac.ox.softeng.maurodatamapper.federation.PublishedModel import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedCatalogue import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedCatalogueService +import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedCatalogueType import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedModel import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedModelService import uk.ac.ox.softeng.maurodatamapper.test.integration.BaseIntegrationSpec +import uk.ac.ox.softeng.maurodatamapper.traits.domain.MdmDomain import uk.ac.ox.softeng.maurodatamapper.util.Utils +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Rollback import grails.testing.mixin.integration.Integration import groovy.json.JsonSlurper import groovy.util.logging.Slf4j +import org.springframework.beans.factory.annotation.Autowired import java.nio.charset.Charset import java.time.OffsetDateTime @@ -43,7 +47,10 @@ import java.time.OffsetDateTime @Slf4j @Integration @Rollback -abstract class BaseSubscribedModelServiceIntegrationSpec extends BaseIntegrationSpec { +abstract class BaseSubscribedModelServiceIntegrationSpec extends BaseIntegrationSpec { + + @Autowired + ImporterService importerService PublishedModel availableModelVersion1 PublishedModel availableModelVersion2 @@ -77,12 +84,14 @@ abstract class BaseSubscribedModelServiceIntegrationSpec extend subscribedCatalogue = new SubscribedCatalogue(url: 'http://remote.example.com', apiKey: UUID.randomUUID(), label: 'Test Remote Catalogue', + subscribedCatalogueType: SubscribedCatalogueType.MAURO_JSON, createdBy: StandardEmailAddress.ADMIN) checkAndSave(subscribedCatalogue) subscribedCatalogue2 = new SubscribedCatalogue(url: 'http://remote2.example.com', apiKey: UUID.randomUUID(), label: 'Test Remote Catalogue 2', + subscribedCatalogueType: SubscribedCatalogueType.MAURO_JSON, createdBy: editor.emailAddress) checkAndSave(subscribedCatalogue2) @@ -90,29 +99,29 @@ abstract class BaseSubscribedModelServiceIntegrationSpec extend //Note: ID is hardcoded because we are mocking an external input rather than a domain created locally. //Don't need to save AvailableModel availableModelVersion1 = new PublishedModel(modelId: Utils.toUuid('c8023de6-5329-4b8b-8a1b-27c2abeaffcd'), - title: 'Remote Model 1.0.0', + modelLabel: 'Remote Model', + modelVersion: Version.from('1.0.0'), description: 'Remote Model Description', modelType: getModelType(), lastUpdated: OffsetDateTime.now()) availableModelVersion2 = new PublishedModel(modelId: Utils.toUuid('d8023de6-5329-4b8b-8a1b-27c2abeaffcd'), - title: 'Remote Model 2.0.0', + modelLabel: 'Remote Model', + modelVersion: Version.from('2.0.0'), description: 'Remote Model Description', modelType: getModelType(), lastUpdated: OffsetDateTime.now()) - subscribedModelVersion1 = new SubscribedModel(subscribedModelId: Utils.toUuid('c8023de6-5329-4b8b-8a1b-27c2abeaffcd'), + subscribedModelVersion1 = new SubscribedModel(subscribedModelId: 'c8023de6-5329-4b8b-8a1b-27c2abeaffcd', folderId: getFolder().id, subscribedCatalogue: subscribedCatalogue, - createdBy: StandardEmailAddress.ADMIN, - subscribedModelType: getModelType()) + createdBy: StandardEmailAddress.ADMIN) checkAndSave(subscribedModelVersion1) - subscribedModelVersion2 = new SubscribedModel(subscribedModelId: Utils.toUuid('d8023de6-5329-4b8b-8a1b-27c2abeaffcd'), + subscribedModelVersion2 = new SubscribedModel(subscribedModelId: 'd8023de6-5329-4b8b-8a1b-27c2abeaffcd', folderId: getFolder().id, subscribedCatalogue: subscribedCatalogue, - createdBy: editor.emailAddress, - subscribedModelType: getModelType()) + createdBy: editor.emailAddress) checkAndSave(subscribedModelVersion2) } @@ -124,33 +133,37 @@ abstract class BaseSubscribedModelServiceIntegrationSpec extend when: 'imported versions 1 a model' ModelImporterProviderService modelImporterProviderService = modelService.getJsonModelImporterProviderService() - ModelImporterProviderServiceParameters parameters1 = (modelImporterProviderService.createNewImporterProviderServiceParameters() as + ModelImporterProviderServiceParameters parameters1 = (importerService.createNewImporterProviderServiceParameters(modelImporterProviderService) as ModelImporterProviderServiceParameters).tap { importFile = new FileParameter(fileContents: getRemoteModelVersion1Json().getBytes()) folderId = getFolder().id finalised = true + useDefaultAuthority = false } - K importedModelVersion1 = modelImporterProviderService.importDomain(getAdmin(), parameters1) as K + K importedModelVersion1 = importerService.importDomain(getAdmin(), modelImporterProviderService, parameters1) as K importedModelVersion1.folder = folder checkAndSave(importedModelVersion1) subscribedModelVersion1.lastRead = OffsetDateTime.now() subscribedModelVersion1.localModelId = importedModelVersion1.id + subscribedModelVersion1.subscribedModelType = importedModelVersion1.domainType checkAndSave(subscribedModelVersion1) and: 'import version 2 of a model' - ModelImporterProviderServiceParameters parameters2 = (modelImporterProviderService.createNewImporterProviderServiceParameters() as + ModelImporterProviderServiceParameters parameters2 = (importerService.createNewImporterProviderServiceParameters(modelImporterProviderService) as ModelImporterProviderServiceParameters).tap { importFile = new FileParameter(fileContents: getRemoteModelVersion2Json().getBytes()) folderId = getFolder().id finalised = true + useDefaultAuthority = false } - K importedModelVersion2 = modelImporterProviderService.importDomain(getAdmin(), parameters2) as K + K importedModelVersion2 = importerService.importDomain(getAdmin(), modelImporterProviderService, parameters2) as K importedModelVersion2.folder = folder checkAndSave(importedModelVersion2) subscribedModelVersion2.lastRead = OffsetDateTime.now() subscribedModelVersion2.localModelId = importedModelVersion2.id + subscribedModelVersion2.subscribedModelType = importedModelVersion2.domainType checkAndSave(subscribedModelVersion2) then: 'there are no version links' diff --git a/mdm-plugin-federation/src/integration-test/resources/url-mappings/tracked_endpoints.txt b/mdm-plugin-federation/src/integration-test/resources/url-mappings/tracked_endpoints.txt index 2d2127d1e4..22e88d47d0 100644 --- a/mdm-plugin-federation/src/integration-test/resources/url-mappings/tracked_endpoints.txt +++ b/mdm-plugin-federation/src/integration-test/resources/url-mappings/tracked_endpoints.txt @@ -7,13 +7,15 @@ | DELETE | /api/admin/subscribedCatalogues/${id} | delete | | PUT | /api/admin/subscribedCatalogues/${id} | update | | GET | /api/admin/subscribedCatalogues/${id} | show | +| GET | /api/admin/subscribedCatalogues/types | types | | GET | /api/subscribedCatalogues/${subscribedCatalogueId}/publishedModels/${publishedModelId}/newerVersions | newerVersions | | GET | /api/subscribedCatalogues/${subscribedCatalogueId}/publishedModels | publishedModels | | GET | /api/subscribedCatalogues/${subscribedCatalogueId}/testConnection | testConnection | | GET | /api/subscribedCatalogues | index | +| GET | /api/subscribedCatalogues/types | types | | GET | /api/subscribedCatalogues/${subscribedCatalogueId} | show | | GET | /api/subscribedCatalogues/${subscribedCatalogueId}/subscribedModels/${id}/newerVersions | newerVersions | -| POST | /api/subscribedCatalogues/${subscribedCatalogueId}/subscribedModels | save | +| POST | /api/subscribedCatalogues/${subscribedCatalogueId}/subscribedModels | federate | | GET | /api/subscribedCatalogues/${subscribedCatalogueId}/subscribedModels | index | | DELETE | /api/subscribedCatalogues/${subscribedCatalogueId}/subscribedModels/${id} | delete | | PUT | /api/subscribedCatalogues/${subscribedCatalogueId}/subscribedModels/${id} | update | diff --git a/mdm-plugin-federation/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/federation/MdmPluginFederationGrailsPlugin.groovy b/mdm-plugin-federation/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/federation/MdmPluginFederationGrailsPlugin.groovy index a535aa5962..43be4dcd51 100644 --- a/mdm-plugin-federation/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/federation/MdmPluginFederationGrailsPlugin.groovy +++ b/mdm-plugin-federation/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/federation/MdmPluginFederationGrailsPlugin.groovy @@ -17,10 +17,11 @@ */ package uk.ac.ox.softeng.maurodatamapper.federation - import uk.ac.ox.softeng.maurodatamapper.federation.gorm.mapping.MdmPluginFederationSchemaMappingContext import uk.ac.ox.softeng.maurodatamapper.federation.rest.render.MdmAtomPublishedModelCollectionRenderer import uk.ac.ox.softeng.maurodatamapper.federation.rest.render.MdmOpmlSubscribedCatalogueCollectionRenderer +import uk.ac.ox.softeng.maurodatamapper.federation.converter.MauroJsonSubscribedCatalogueConverter +import uk.ac.ox.softeng.maurodatamapper.federation.converter.AtomSubscribedCatalogueConverter import grails.plugins.Plugin @@ -85,6 +86,12 @@ The federation domain, services and controllers for the Mauro Data Mapper backen } opmlSubscribedCatalogueRenderer(MdmOpmlSubscribedCatalogueCollectionRenderer, SubscribedCatalogue) { } + + /* + * Define converters for JSON and Atom Subscribed Catalogues + */ + mauroJsonSubscribedCatalogueConverter MauroJsonSubscribedCatalogueConverter + atomSubscribedCatalogueConverter AtomSubscribedCatalogueConverter } } } diff --git a/mdm-plugin-federation/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/federation/PublishedModel.groovy b/mdm-plugin-federation/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/federation/PublishedModel.groovy index 9d568c4317..8aa050d760 100644 --- a/mdm-plugin-federation/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/federation/PublishedModel.groovy +++ b/mdm-plugin-federation/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/federation/PublishedModel.groovy @@ -26,7 +26,7 @@ import java.util.regex.Pattern class PublishedModel implements Comparable { - UUID modelId + String modelId String modelLabel Version modelVersion String description @@ -57,17 +57,6 @@ class PublishedModel implements Comparable { "${modelLabel} ${modelVersion}" } - void setTitle(String label) { - Pattern titleVersionPattern = ~/ $Version.VERSION_PATTERN$/ - String version = label.find(titleVersionPattern) - if (version) { - modelVersion = Version.from(version.trim()) - modelLabel = (label - version).trim() - } else { - modelLabel = label - } - } - String getDescription() { description == title ? null : description } diff --git a/mdm-plugin-federation/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedCatalogueType.groovy b/mdm-plugin-federation/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedCatalogueType.groovy new file mode 100644 index 0000000000..c1957f5b6c --- /dev/null +++ b/mdm-plugin-federation/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/federation/SubscribedCatalogueType.groovy @@ -0,0 +1,46 @@ +/* + * Copyright 2020-2022 University of Oxford and Health and Social Care Information Centre, also known as NHS Digital + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package uk.ac.ox.softeng.maurodatamapper.federation + +enum SubscribedCatalogueType { + MAURO_JSON('Mauro JSON'), + ATOM('Atom') + + String label + + SubscribedCatalogueType(String label) { + this.label = label + } + + static List labels() { + values().collect { it.label }.sort() + } + + static SubscribedCatalogueType findForLabel(String label) { + String convert = label?.toUpperCase()?.replaceAll(/ /, '_') + try { + return valueOf(convert) + } catch (Exception ignored) {} + null + } + + static SubscribedCatalogueType findFromMap(def map) { + map['subscribedCatalogueType'] instanceof SubscribedCatalogueType ? map['subscribedCatalogueType'] as SubscribedCatalogueType : + findForLabel(map['subscribedCatalogueType'] as String) + } +} \ No newline at end of file diff --git a/mdm-plugin-federation/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/federation/web/FederationClient.groovy b/mdm-plugin-federation/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/federation/web/FederationClient.groovy index 37f1f4c3e4..0c27aa39a5 100644 --- a/mdm-plugin-federation/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/federation/web/FederationClient.groovy +++ b/mdm-plugin-federation/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/federation/web/FederationClient.groovy @@ -20,7 +20,9 @@ package uk.ac.ox.softeng.maurodatamapper.federation.web import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiException import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException +import uk.ac.ox.softeng.maurodatamapper.core.traits.provider.importer.XmlImportMapping import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedCatalogue +import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedCatalogueType import edu.umd.cs.findbugs.annotations.SuppressFBWarnings import groovy.util.logging.Slf4j @@ -39,6 +41,7 @@ import io.micronaut.http.client.netty.DefaultHttpClient import io.micronaut.http.client.netty.ssl.NettyClientSslBuilder import io.micronaut.http.codec.MediaTypeCodecRegistry import io.micronaut.http.exceptions.HttpException +import io.micronaut.http.uri.DefaultUriBuilder import io.micronaut.http.uri.UriBuilder import io.netty.channel.MultithreadEventLoopGroup import io.netty.util.concurrent.DefaultThreadFactory @@ -50,13 +53,16 @@ import java.util.concurrent.ThreadFactory * @since 14/04/2021 */ @Slf4j -@SuppressFBWarnings(value = 'UPM_UNCALLED_PRIVATE_METHOD', justification = 'Methods exist for future programming') +@SuppressFBWarnings(value = 'UPM_UNCALLED_PRIVATE_METHOD', justification = 'Calls to methods with optional params not detected') class FederationClient implements Closeable { static final String API_KEY_HEADER = 'apiKey' private HttpClient client private String hostUrl private String contextPath + private HttpClientConfiguration httpClientConfiguration + private NettyClientSslBuilder nettyClientSslBuilder + private MediaTypeCodecRegistry mediaTypeCodecRegistry FederationClient(SubscribedCatalogue subscribedCatalogue, ApplicationContext applicationContext) { this(subscribedCatalogue, @@ -83,16 +89,22 @@ class FederationClient implements Closeable { ThreadFactory threadFactory, NettyClientSslBuilder nettyClientSslBuilder, MediaTypeCodecRegistry mediaTypeCodecRegistry) { + this.httpClientConfiguration = httpClientConfiguration + this.nettyClientSslBuilder = nettyClientSslBuilder + this.mediaTypeCodecRegistry = mediaTypeCodecRegistry + hostUrl = subscribedCatalogue.url // The http client resolves using URI.resolve which ignores anything in the url path, // therefore we need to make sure its part of the context path. URI hostUri = hostUrl.toURI() - if (hostUri.path && !hostUri.path.endsWith('api')) { + if (subscribedCatalogue.subscribedCatalogueType == SubscribedCatalogueType.MAURO_JSON) { String path = hostUri.path.endsWith('/') ? hostUri.path : "${hostUri.path}/" - this.contextPath = "${path}api" + if (!path.endsWith('/api/')) path = path + 'api/' + this.contextPath = path } else { - this.contextPath = 'api' + this.contextPath = hostUri.path } + client = new DefaultHttpClient(LoadBalancer.fixed(hostUrl.toURL().toURI()), httpClientConfiguration, this.contextPath, @@ -111,7 +123,7 @@ class FederationClient implements Closeable { GPathResult getSubscribedCatalogueModelsFromAtomFeed(UUID apiKey) { // Currently we use the ATOM feed which is XML and the micronaut client isnt designed to decode XML - retrieveXmlDataFromClient(UriBuilder.of('feeds/all'), apiKey) + retrieveXmlDataFromClient(UriBuilder.of(''), apiKey) } Map getSubscribedCatalogueModels(UUID apiKey) { @@ -122,23 +134,16 @@ class FederationClient implements Closeable { retrieveListFromClient(UriBuilder.of(urlResourceType).path('providers/exporters'), apiKey) } - Map getVersionLinksForModel(UUID apiKey, String urlModelResourceType, UUID modelId) { - retrieveMapFromClient(UriBuilder.of(urlModelResourceType).path(modelId.toString()).path('versionLinks'), apiKey) + Map getVersionLinksForModel(UUID apiKey, String urlModelResourceType, String publishedModelId) { + retrieveMapFromClient(UriBuilder.of(urlModelResourceType).path(publishedModelId).path('versionLinks'), apiKey) } - Map getNewerPublishedVersionsForPublishedModel(UUID apiKey, UUID modelId) { - retrieveMapFromClient(UriBuilder.of('published/models').path(modelId.toString()).path('newerVersions'), apiKey) + Map getNewerPublishedVersionsForPublishedModel(UUID apiKey, String publishedModelId) { + retrieveMapFromClient(UriBuilder.of('published/models').path(publishedModelId).path('newerVersions'), apiKey) } - String getStringResourceExport(UUID apiKey, String urlResourceType, UUID resourceId, Map exporterInfo) { - retrieveStringFromClient(UriBuilder.of(urlResourceType) - .path(resourceId.toString()) - .path('export') - .path(exporterInfo.namespace) - .path(exporterInfo.name) - .path(exporterInfo.version), - apiKey - ) + byte[] getBytesResourceExport(UUID apiKey, String resourceUrl) { + retrieveBytesFromClient(UriBuilder.of(resourceUrl), apiKey) } private GPathResult retrieveXmlDataFromClient(UriBuilder uriBuilder, UUID apiKey, Map params = [:]) { @@ -187,6 +192,17 @@ class FederationClient implements Closeable { } } + private byte[] retrieveBytesFromClient(UriBuilder uriBuilder, UUID apiKey, Map params = [:]) { + try { + client.toBlocking().retrieve(HttpRequest.GET(uriBuilder.expand(params)) + .header(API_KEY_HEADER, apiKey.toString()), + Argument.of(byte[])) as byte[] + } + catch (HttpException ex) { + handleHttpException(ex, getFullUrl(uriBuilder, params)) + } + } + private static void handleHttpException(HttpException ex, String fullUrl) throws ApiException { if (ex instanceof HttpClientResponseException) { if (ex.status == HttpStatus.NOT_FOUND) { @@ -203,7 +219,7 @@ class FederationClient implements Closeable { } private String getFullUrl(UriBuilder uriBuilder, Map params) { - String path = uriBuilder.build().toString() - UriBuilder.of(hostUrl).path(contextPath).path(path).expand(params).toString() + String path = uriBuilder.expand(params).toString() + hostUrl.toURI().resolve(UriBuilder.of(contextPath).path(path).build()).toString() } } diff --git a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/exporter/ReferenceDataJsonExporterService.groovy b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/exporter/ReferenceDataJsonExporterService.groovy index c49516b180..6c9eaed5ad 100644 --- a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/exporter/ReferenceDataJsonExporterService.groovy +++ b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/exporter/ReferenceDataJsonExporterService.groovy @@ -30,6 +30,8 @@ import org.springframework.beans.factory.annotation.Autowired class ReferenceDataJsonExporterService extends ReferenceDataModelExporterProviderService implements TemplateBasedExporter { + public static final CONTENT_TYPE = 'application/mauro.referencedatamodel+json' + @Autowired JsonViewTemplateEngine templateEngine @@ -39,13 +41,8 @@ class ReferenceDataJsonExporterService extends ReferenceDataModelExporterProvide } @Override - String getFileType() { - 'text/json' - } - - @Override - String getProducesContentType() { - 'application/mdm+json' + String getContentType() { + CONTENT_TYPE } @Override @@ -66,7 +63,7 @@ class ReferenceDataJsonExporterService extends ReferenceDataModelExporterProvide @Override ByteArrayOutputStream exportReferenceDataModel(User currentUser, ReferenceDataModel referenceDataModel, Map parameters) throws ApiException { ExportMetadata exportMetadata = new ExportMetadata(this, currentUser.firstName, currentUser.lastName) - exportModel new ExportModel(referenceDataModel, 'referenceDataModel', version, exportMetadata), fileType + exportModel new ExportModel(referenceDataModel, 'referenceDataModel', version, exportMetadata), contentType } @Override diff --git a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/exporter/ReferenceDataXmlExporterService.groovy b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/exporter/ReferenceDataXmlExporterService.groovy index 206927e1c3..4edde1a5d4 100644 --- a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/exporter/ReferenceDataXmlExporterService.groovy +++ b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/exporter/ReferenceDataXmlExporterService.groovy @@ -30,6 +30,8 @@ import org.springframework.beans.factory.annotation.Autowired class ReferenceDataXmlExporterService extends ReferenceDataModelExporterProviderService implements TemplateBasedExporter { + public static final CONTENT_TYPE = 'application/mauro.referencedatamodel+xml' + @Autowired MarkupViewTemplateEngine templateEngine @@ -39,13 +41,8 @@ class ReferenceDataXmlExporterService extends ReferenceDataModelExporterProvider } @Override - String getFileType() { - 'text/xml' - } - - @Override - String getProducesContentType() { - 'application/mdm+xml' + String getContentType() { + CONTENT_TYPE } @Override @@ -66,7 +63,7 @@ class ReferenceDataXmlExporterService extends ReferenceDataModelExporterProvider @Override ByteArrayOutputStream exportReferenceDataModel(User currentUser, ReferenceDataModel referenceDataModel, Map parameters) throws ApiException { ExportMetadata exportMetadata = new ExportMetadata(this, currentUser.firstName, currentUser.lastName) - exportModel(new ExportModel(referenceDataModel, 'referenceDataModel', version, 'gml', exportMetadata), fileType) + exportModel(new ExportModel(referenceDataModel, 'referenceDataModel', version, 'gml', exportMetadata), contentType) } @Override diff --git a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/importer/ReferenceDataCsvImporterService.groovy b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/importer/ReferenceDataCsvImporterService.groovy index 1a2ecfc3f4..fd86f31d02 100644 --- a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/importer/ReferenceDataCsvImporterService.groovy +++ b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/importer/ReferenceDataCsvImporterService.groovy @@ -60,6 +60,11 @@ class ReferenceDataCsvImporterService extends ReferenceDataModelImporterProvider false } + @Override + Boolean handlesContentType(String contentType) { + contentType.equalsIgnoreCase('application/mauro.referencedatamodel+csv') + } + @Override List importModels(User currentUser, ReferenceDataModelFileImporterProviderServiceParameters params) { throw new ApiBadRequestException('FBIP04', "${getName()} cannot import multiple Reference Data Models") diff --git a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/importer/ReferenceDataJsonImporterService.groovy b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/importer/ReferenceDataJsonImporterService.groovy index 7cc05b50eb..11141d76db 100644 --- a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/importer/ReferenceDataJsonImporterService.groovy +++ b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/importer/ReferenceDataJsonImporterService.groovy @@ -21,6 +21,7 @@ package uk.ac.ox.softeng.maurodatamapper.referencedata.provider.importer import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiUnauthorizedException import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModel +import uk.ac.ox.softeng.maurodatamapper.referencedata.provider.exporter.ReferenceDataJsonExporterService import uk.ac.ox.softeng.maurodatamapper.referencedata.provider.importer.parameter.ReferenceDataModelFileImporterProviderServiceParameters import uk.ac.ox.softeng.maurodatamapper.security.User @@ -43,6 +44,11 @@ class ReferenceDataJsonImporterService '4.0' } + @Override + Boolean handlesContentType(String contentType) { + contentType.equalsIgnoreCase(ReferenceDataJsonExporterService.CONTENT_TYPE) + } + @Override ReferenceDataModel importReferenceDataModel(User currentUser, byte[] content) { if (!currentUser) throw new ApiUnauthorizedException('JIS01', 'User must be logged in to import model') diff --git a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/importer/ReferenceDataXmlImporterService.groovy b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/importer/ReferenceDataXmlImporterService.groovy index 0bcc14e428..c6a42a7e9e 100644 --- a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/importer/ReferenceDataXmlImporterService.groovy +++ b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/importer/ReferenceDataXmlImporterService.groovy @@ -21,6 +21,7 @@ import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiUnauthorizedException import uk.ac.ox.softeng.maurodatamapper.core.traits.provider.importer.XmlImportMapping import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModel +import uk.ac.ox.softeng.maurodatamapper.referencedata.provider.exporter.ReferenceDataXmlExporterService import uk.ac.ox.softeng.maurodatamapper.referencedata.provider.importer.parameter.ReferenceDataModelFileImporterProviderServiceParameters import uk.ac.ox.softeng.maurodatamapper.security.User @@ -49,6 +50,11 @@ class ReferenceDataXmlImporterService false } + @Override + Boolean handlesContentType(String contentType) { + contentType.equalsIgnoreCase(ReferenceDataXmlExporterService.CONTENT_TYPE) + } + @Override ReferenceDataModel importReferenceDataModel(User currentUser, byte[] content) { if (!currentUser) throw new ApiUnauthorizedException('XIS01', 'User must be logged in to import model') diff --git a/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelFunctionalSpec.groovy b/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelFunctionalSpec.groovy index 98024a71a1..1d923b9e05 100644 --- a/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelFunctionalSpec.groovy +++ b/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelFunctionalSpec.groovy @@ -189,7 +189,7 @@ class ReferenceDataModelFunctionalSpec extends ResourceFunctionalSpec parameters) throws ApiException { ExportMetadata exportMetadata = new ExportMetadata(this, currentUser.firstName, currentUser.lastName) - exportModel(new ExportModel(codeSet, 'codeSet', version, exportMetadata), fileType) + exportModel(new ExportModel(codeSet, 'codeSet', version, exportMetadata), contentType) } @Override ByteArrayOutputStream exportCodeSets(User currentUser, List codeSets, Map parameters) throws ApiException { ExportMetadata exportMetadata = new ExportMetadata(this, currentUser.firstName, currentUser.lastName) - exportModel(new ExportModel(codeSets, 'codeSet', 'codeSets', version, exportMetadata), fileType) + exportModel(new ExportModel(codeSets, 'codeSet', 'codeSets', version, exportMetadata), contentType) } } diff --git a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/exporter/CodeSetXmlExporterService.groovy b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/exporter/CodeSetXmlExporterService.groovy index 46224fb3fb..f094cc1731 100644 --- a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/exporter/CodeSetXmlExporterService.groovy +++ b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/exporter/CodeSetXmlExporterService.groovy @@ -32,6 +32,8 @@ import org.springframework.beans.factory.annotation.Autowired */ class CodeSetXmlExporterService extends CodeSetExporterProviderService implements TemplateBasedExporter { + public static final CONTENT_TYPE = 'application/mauro.codeset+xml' + @Autowired MarkupViewTemplateEngine templateEngine @@ -45,19 +47,14 @@ class CodeSetXmlExporterService extends CodeSetExporterProviderService implement '5.0' } - @Override - String getFileType() { - 'text/xml' - } - @Override String getFileExtension() { 'xml' } @Override - String getProducesContentType() { - 'application/mdm+xml' + String getContentType() { + CONTENT_TYPE } @Override @@ -78,12 +75,12 @@ class CodeSetXmlExporterService extends CodeSetExporterProviderService implement @Override ByteArrayOutputStream exportCodeSet(User currentUser, CodeSet codeSet, Map parameters) throws ApiException { ExportMetadata exportMetadata = new ExportMetadata(this, currentUser.firstName, currentUser.lastName) - exportModel(new ExportModel(codeSet, 'codeSet', version, '4.0', 'gml', exportMetadata), fileType) + exportModel(new ExportModel(codeSet, 'codeSet', version, '4.0', 'gml', exportMetadata), contentType) } @Override ByteArrayOutputStream exportCodeSets(User currentUser, List codeSets, Map parameters) throws ApiException { ExportMetadata exportMetadata = new ExportMetadata(this, currentUser.firstName, currentUser.lastName) - exportModel(new ExportModel(codeSets, 'codeSet', 'codeSets', version, '4.0', 'gml', exportMetadata), fileType) + exportModel(new ExportModel(codeSets, 'codeSet', 'codeSets', version, '4.0', 'gml', exportMetadata), contentType) } } diff --git a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/exporter/TerminologyJsonExporterService.groovy b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/exporter/TerminologyJsonExporterService.groovy index dbab432c17..4b44372630 100644 --- a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/exporter/TerminologyJsonExporterService.groovy +++ b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/exporter/TerminologyJsonExporterService.groovy @@ -29,6 +29,8 @@ import org.springframework.beans.factory.annotation.Autowired class TerminologyJsonExporterService extends TerminologyExporterProviderService implements TemplateBasedExporter { + public static final CONTENT_TYPE = 'application/mauro.terminology+json' + @Autowired JsonViewTemplateEngine templateEngine @@ -42,19 +44,14 @@ class TerminologyJsonExporterService extends TerminologyExporterProviderService '4.0' } - @Override - String getFileType() { - 'text/json' - } - @Override String getFileExtension() { 'json' } @Override - String getProducesContentType() { - 'application/mdm+json' + String getContentType() { + CONTENT_TYPE } @Override @@ -70,12 +67,12 @@ class TerminologyJsonExporterService extends TerminologyExporterProviderService @Override ByteArrayOutputStream exportTerminology(User currentUser, Terminology terminology, Map parameters) throws ApiException { ExportMetadata exportMetadata = new ExportMetadata(this, currentUser.firstName, currentUser.lastName) - exportModel(new ExportModel(terminology, 'terminology', version, exportMetadata), fileType) + exportModel(new ExportModel(terminology, 'terminology', version, exportMetadata), contentType) } @Override ByteArrayOutputStream exportTerminologies(User currentUser, List terminologies, Map parameters) throws ApiException { ExportMetadata exportMetadata = new ExportMetadata(this, currentUser.firstName, currentUser.lastName) - exportModel(new ExportModel(terminologies, 'terminology', 'terminologies', version, exportMetadata), fileType) + exportModel(new ExportModel(terminologies, 'terminology', 'terminologies', version, exportMetadata), contentType) } } diff --git a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/exporter/TerminologyXmlExporterService.groovy b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/exporter/TerminologyXmlExporterService.groovy index 5969311b98..d8be95f67c 100644 --- a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/exporter/TerminologyXmlExporterService.groovy +++ b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/exporter/TerminologyXmlExporterService.groovy @@ -32,6 +32,8 @@ import org.springframework.beans.factory.annotation.Autowired */ class TerminologyXmlExporterService extends TerminologyExporterProviderService implements TemplateBasedExporter { + public static final CONTENT_TYPE = 'application/mauro.terminology+xml' + @Autowired MarkupViewTemplateEngine templateEngine @@ -45,19 +47,14 @@ class TerminologyXmlExporterService extends TerminologyExporterProviderService i '5.0' } - @Override - String getFileType() { - 'text/xml' - } - @Override String getFileExtension() { 'xml' } @Override - String getProducesContentType() { - 'application/mdm+xml' + String getContentType() { + CONTENT_TYPE } @Override @@ -78,12 +75,12 @@ class TerminologyXmlExporterService extends TerminologyExporterProviderService i @Override ByteArrayOutputStream exportTerminology(User currentUser, Terminology terminology, Map parameters) throws ApiException { ExportMetadata exportMetadata = new ExportMetadata(this, currentUser.firstName, currentUser.lastName) - exportModel(new ExportModel(terminology, 'terminology', version, '4.0', 'gml', exportMetadata), fileType) + exportModel(new ExportModel(terminology, 'terminology', version, '4.0', 'gml', exportMetadata), contentType) } @Override ByteArrayOutputStream exportTerminologies(User currentUser, List terminologies, Map parameters) throws ApiException { ExportMetadata exportMetadata = new ExportMetadata(this, currentUser.firstName, currentUser.lastName) - exportModel(new ExportModel(terminologies, 'terminology', 'terminologies', version, '4.0', 'gml', exportMetadata), fileType) + exportModel(new ExportModel(terminologies, 'terminology', 'terminologies', version, '4.0', 'gml', exportMetadata), contentType) } } diff --git a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/importer/CodeSetJsonImporterService.groovy b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/importer/CodeSetJsonImporterService.groovy index 378bcad71f..924354faf6 100644 --- a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/importer/CodeSetJsonImporterService.groovy +++ b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/importer/CodeSetJsonImporterService.groovy @@ -22,6 +22,7 @@ import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiUnauthorizedException import uk.ac.ox.softeng.maurodatamapper.core.traits.provider.importer.JsonImportMapping import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.terminology.CodeSet +import uk.ac.ox.softeng.maurodatamapper.terminology.provider.exporter.CodeSetJsonExporterService import uk.ac.ox.softeng.maurodatamapper.terminology.provider.importer.parameter.CodeSetFileImporterProviderServiceParameters import groovy.util.logging.Slf4j @@ -44,6 +45,11 @@ class CodeSetJsonImporterService extends DataBindCodeSetImporterProviderService< true } + @Override + Boolean handlesContentType(String contentType) { + contentType.equalsIgnoreCase(CodeSetJsonExporterService.CONTENT_TYPE) + } + @Override CodeSet importCodeSet(User currentUser, byte[] content) { if (!currentUser) throw new ApiUnauthorizedException('JIS01', 'User must be logged in to import model') diff --git a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/importer/CodeSetXmlImporterService.groovy b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/importer/CodeSetXmlImporterService.groovy index fb4f481c7b..79aac987ec 100644 --- a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/importer/CodeSetXmlImporterService.groovy +++ b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/provider/importer/CodeSetXmlImporterService.groovy @@ -22,6 +22,7 @@ import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiUnauthorizedException import uk.ac.ox.softeng.maurodatamapper.core.traits.provider.importer.XmlImportMapping import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.terminology.CodeSet +import uk.ac.ox.softeng.maurodatamapper.terminology.provider.exporter.CodeSetXmlExporterService import uk.ac.ox.softeng.maurodatamapper.terminology.provider.importer.parameter.CodeSetFileImporterProviderServiceParameters import groovy.util.logging.Slf4j @@ -49,6 +50,11 @@ class CodeSetXmlImporterService extends DataBindCodeSetImporterProviderService implements X "knownMetadataKeys": [], "providerType": "CodeSetExporter", "fileExtension": "json", - "fileType": "text/json", + "contentType": "application/mauro.codeset+json", "canExportMultipleDomains": true }, { @@ -1266,7 +1266,7 @@ class CodeSetFunctionalSpec extends ResourceFunctionalSpec implements X "knownMetadataKeys": [], "providerType": "CodeSetExporter", "fileExtension": "xml", - "fileType": "text/xml", + "contentType": "application/mauro.codeset+xml", "canExportMultipleDomains": true } ]''' diff --git a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyFunctionalSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyFunctionalSpec.groovy index 1c1e3fe55f..809a36b1c8 100644 --- a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyFunctionalSpec.groovy +++ b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyFunctionalSpec.groovy @@ -1366,7 +1366,7 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec impl "knownMetadataKeys": [], "providerType": "TerminologyExporter", "fileExtension": "json", - "fileType": "text/json", + "contentType": "application/mauro.terminology+json", "canExportMultipleDomains": true }, { @@ -1378,7 +1378,7 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec impl "knownMetadataKeys": [], "providerType": "TerminologyExporter", "fileExtension": "xml", - "fileType": "text/xml", + "contentType": "application/mauro.terminology+xml", "canExportMultipleDomains": true } ]''' diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/async/DomainExportFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/async/DomainExportFunctionalSpec.groovy index f4738b246f..1b1b65603e 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/async/DomainExportFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/async/DomainExportFunctionalSpec.groovy @@ -131,8 +131,7 @@ class DomainExportFunctionalSpec extends UserAccessWithoutUpdatingFunctionalSpec }, "export": { "fileName": "Complex Test DataModel.json", - "fileType": "text/json", - "contentType": "application/mdm+json", + "contentType": "application/mauro.datamodel+json", "fileSize": "${json-unit.any-number}" }, "exportedOn": "${json-unit.matches:offsetDateTime}", diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/provider/MauroDataMapperServiceProviderFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/provider/MauroDataMapperServiceProviderFunctionalSpec.groovy index 5347d2ad45..0147fb7d01 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/provider/MauroDataMapperServiceProviderFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/provider/MauroDataMapperServiceProviderFunctionalSpec.groovy @@ -88,7 +88,7 @@ class MauroDataMapperServiceProviderFunctionalSpec extends FunctionalSpec { ], "providerType": "CodeSetExporter", "fileExtension": "xml", - "fileType": "text/xml", + "contentType": "application/mauro.codeset+xml", "canExportMultipleDomains": true }, { @@ -102,7 +102,7 @@ class MauroDataMapperServiceProviderFunctionalSpec extends FunctionalSpec { ], "providerType": "ReferenceDataModelExporter", "fileExtension": "json", - "fileType": "text/json", + "contentType": "application/mauro.referencedatamodel+json", "canExportMultipleDomains": false }, { @@ -116,7 +116,7 @@ class MauroDataMapperServiceProviderFunctionalSpec extends FunctionalSpec { ], "providerType": "DataModelExporter", "fileExtension": "xml", - "fileType": "text/xml", + "contentType": "application/mauro.datamodel+xml", "canExportMultipleDomains": true }, { @@ -130,7 +130,7 @@ class MauroDataMapperServiceProviderFunctionalSpec extends FunctionalSpec { ], "providerType": "ReferenceDataModelExporter", "fileExtension": "xml", - "fileType": "text/xml", + "contentType": "application/mauro.referencedatamodel+xml", "canExportMultipleDomains": false }, { @@ -144,7 +144,7 @@ class MauroDataMapperServiceProviderFunctionalSpec extends FunctionalSpec { ], "providerType": "CodeSetExporter", "fileExtension": "json", - "fileType": "text/json", + "contentType": "application/mauro.codeset+json", "canExportMultipleDomains": true }, { @@ -158,7 +158,7 @@ class MauroDataMapperServiceProviderFunctionalSpec extends FunctionalSpec { ], "providerType": "DataModelExporter", "fileExtension": "json", - "fileType": "text/json", + "contentType": "application/mauro.datamodel+json", "canExportMultipleDomains": true }, { @@ -172,7 +172,7 @@ class MauroDataMapperServiceProviderFunctionalSpec extends FunctionalSpec { ], "providerType": "TerminologyExporter", "fileExtension": "json", - "fileType": "text/json", + "contentType": "application/mauro.terminology+json", "canExportMultipleDomains": true }, { @@ -186,7 +186,7 @@ class MauroDataMapperServiceProviderFunctionalSpec extends FunctionalSpec { ], "providerType": "TerminologyExporter", "fileExtension": "xml", - "fileType": "text/xml", + "contentType": "application/mauro.terminology+xml", "canExportMultipleDomains": true }, { @@ -200,7 +200,7 @@ class MauroDataMapperServiceProviderFunctionalSpec extends FunctionalSpec { ], "providerType": "DataFlowExporter", "fileExtension": "json", - "fileType": "text/json", + "contentType": "application/mauro.dataflow+json", "canExportMultipleDomains": false }, { @@ -214,7 +214,7 @@ class MauroDataMapperServiceProviderFunctionalSpec extends FunctionalSpec { ], "providerType": "DataFlowExporter", "fileExtension": "xml", - "fileType": "text/xml", + "contentType": "application/mauro.dataflow+xml", "canExportMultipleDomains": false }, { @@ -228,7 +228,7 @@ class MauroDataMapperServiceProviderFunctionalSpec extends FunctionalSpec { ], "providerType": "FolderExporter", "fileExtension": "json", - "fileType": "text/json", + "contentType": "application/mauro.folder+json", "canExportMultipleDomains": false } ]''') diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/datamodel/DataModelFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/datamodel/DataModelFunctionalSpec.groovy index 09be4901c8..abe5287ef9 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/datamodel/DataModelFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/datamodel/DataModelFunctionalSpec.groovy @@ -433,7 +433,7 @@ class DataModelFunctionalSpec extends ModelUserAccessPermissionChangingAndVersio "knownMetadataKeys": [], "providerType": "DataModelExporter", "fileExtension": "json", - "fileType": "text/json", + "contentType": "application/mauro.datamodel+json", "canExportMultipleDomains": true }, { @@ -445,7 +445,7 @@ class DataModelFunctionalSpec extends ModelUserAccessPermissionChangingAndVersio "knownMetadataKeys": [], "providerType": "DataModelExporter", "fileExtension": "xml", - "fileType": "text/xml", + "contentType": "application/mauro.datamodel+xml", "canExportMultipleDomains": true } ]''' @@ -1083,8 +1083,7 @@ class DataModelFunctionalSpec extends ModelUserAccessPermissionChangingAndVersio }, "export": { "fileName": "Functional Test DataModel.json", - "fileType": "text/json", - "contentType": "application/mdm+json", + "contentType": "application/mauro.datamodel+json", "fileSize": "${json-unit.any-number}" }, "exportedOn": "${json-unit.matches:offsetDateTime}", diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/AtomSubscribedCatalogueFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/AtomSubscribedCatalogueFunctionalSpec.groovy new file mode 100644 index 0000000000..b9599c714b --- /dev/null +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/AtomSubscribedCatalogueFunctionalSpec.groovy @@ -0,0 +1,342 @@ +/* + * Copyright 2020-2022 University of Oxford and Health and Social Care Information Centre, also known as NHS Digital + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package uk.ac.ox.softeng.maurodatamapper.testing.functional.federation + +import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedCatalogueType +import uk.ac.ox.softeng.maurodatamapper.testing.functional.test.BaseSubscribedCatalogueFunctionalSpec + +import grails.testing.mixin.integration.Integration +import groovy.util.logging.Slf4j + +import java.time.LocalDate +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter + +import static uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress.getADMIN + +import static io.micronaut.http.HttpStatus.CREATED +import static io.micronaut.http.HttpStatus.NO_CONTENT +import static io.micronaut.http.HttpStatus.OK + +@Integration +@Slf4j +class AtomSubscribedCatalogueFunctionalSpec extends BaseSubscribedCatalogueFunctionalSpec { + @Override + Map getValidJson() { + [ + url : subscribedCatalogueUrl, + apiKey : UUID.randomUUID().toString(), + label : 'Functional Test Label', + subscribedCatalogueType: subscribedCatalogueType.label, + description : 'Functional Test Description', + refreshPeriod : 7 + ] + } + + @Override + Map getInvalidJson() { + [ + url : 'wibble', + apiKey: '67421316-66a5-4830-9156-b1ba77bba5d1' + ] + } + + @Override + Map getValidUpdateJson() { + [ + description: 'Functional Test Description Updated' + ] + } + + @Override + String getExpectedShowJson() { + """{ + "apiKey": "\${json-unit.matches:id}", + "description": "Functional Test Description", + "id": "\${json-unit.matches:id}", + "label": "Functional Test Label", + "subscribedCatalogueType": "$subscribedCatalogueType.label", + "refreshPeriod": 7, + "url": "$subscribedCatalogueUrl" +}""" + } + + @Override + String getExpectedOpenAccessShowJson() { + """{ + "description": "Functional Test Description", + "id": "\${json-unit.matches:id}", + "label": "Functional Test Label", + "subscribedCatalogueType": "$subscribedCatalogueType.label", + "refreshPeriod": 7, + "url": "$subscribedCatalogueUrl" +}""" + } + + @Override + String getExpectedIndexJson() { + """{ + "count": 1, + "items": [ + { + "id": "\${json-unit.matches:id}", + "url": "$subscribedCatalogueUrl", + "label": "Functional Test Label", + "subscribedCatalogueType": "$subscribedCatalogueType.label", + "description": "Functional Test Description", + "refreshPeriod": 7 + } + ] +}""" + } + + @Override + SubscribedCatalogueType getSubscribedCatalogueType() { + SubscribedCatalogueType.ATOM + } + + @Override + String getSubscribedCatalogueUrl() { + "http://localhost:$serverPort/api/feeds/all".toString() + } + + /** + * Test the publishedModels endpoint. This would be on a remote host, but in this functional test + * we use the localhost. Test setup and execution is as follows: + * 1. Login as Admin and create an API Key for Admin + * 2. Subscribe to the local catalogue (in real life this would be remote), specifying the API key created above + * 3. Get the local /publishedModels endpoint. In real life this would connect to the Atom feed on the remote, + * but here we use the local. + * 4. Cleanup + */ + void 'A07a : Test the publishedModels endpoint'() { + + given: + Map apiKeyJson = [ + name : 'Functional Test', + expiryDate: LocalDate.now().plusDays(5).format(DateTimeFormatter.ISO_LOCAL_DATE) + ] + + when: + loginAdmin() + POST("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys", apiKeyJson, MAP_ARG, true) + + then: + verifyResponse CREATED, response + String apiKey = responseBody().apiKey + + when: + //note: using a groovy string like "http://localhost:$serverPort/" causes the url to be stripped when saving + Map subscriptionJson = [ + url : subscribedCatalogueUrl, + apiKey : apiKey, + label : 'Functional Test Label', + subscribedCatalogueType: subscribedCatalogueType.label, + description : 'Functional Test Description', + refreshPeriod : 7 + ] + POST('', subscriptionJson) + + then: + verifyResponse CREATED, response + String subscribedCatalogueId = responseBody().id + + when: + GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels", MAP_ARG, true) + + then: + verifyResponse(OK, response) + verifyBaseJsonResponse(responseBody(), true) + responseBody().items.size() == 3 + + and: + verifyJsonPublishedModel(responseBody().items.find {it.label == 'Finalised Example Test DataModel 1.0.0'}, 'dataModels', + getDataModelExporters()) + verifyJsonPublishedModel(responseBody().items.find {it.label == 'Simple Test CodeSet 1.0.0'}, 'codeSets', getCodeSetExporters()) + verifyJsonPublishedModel(responseBody().items.find {it.label == 'Complex Test CodeSet 1.0.0'}, 'codeSets', getCodeSetExporters()) + + cleanup: + DELETE("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys/${apiKey}", MAP_ARG, true) + removeValidIdObject(subscribedCatalogueId) + cleanUpRoles(subscribedCatalogueId) + } + + void 'A07b : Test the publishedModels endpoint (without API key)'() { + given: + loginAdmin() + + when: + Map subscriptionJson = [ + url : subscribedCatalogueUrl, + apiKey : '', + label : 'Functional Test Label', + subscribedCatalogueType: subscribedCatalogueType.label, + description : 'Functional Test Description', + refreshPeriod : 7 + ] + POST('', subscriptionJson) + + then: + verifyResponse CREATED, response + String subscribedCatalogueId = responseBody().id + + when: + GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels", MAP_ARG, true) + + then: + verifyResponse(OK, response) + verifyBaseJsonResponse(responseBody(), false) + + cleanup: + removeValidIdObject(subscribedCatalogueId) + cleanUpRoles(subscribedCatalogueId) + } + + void 'A08a : Test the newerVersions endpoint (with no newer versions)'() { + given: + Map apiKeyJson = [ + name : 'Functional Test', + expiryDate: LocalDate.now().plusDays(5).format(DateTimeFormatter.ISO_LOCAL_DATE) + ] + loginAdmin() + POST("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys", apiKeyJson, MAP_ARG, true) + verifyResponse CREATED, response + String apiKey = responseBody().apiKey + Map subscriptionJson = [ + url : subscribedCatalogueUrl, + apiKey : apiKey, + label : 'Functional Test Label', + subscribedCatalogueType: subscribedCatalogueType.label, + description : 'Functional Test Description', + refreshPeriod : 7 + ] + POST('', subscriptionJson) + verifyResponse CREATED, response + String subscribedCatalogueId = responseBody().id + + when: + String finalisedDataModelId = getFinalisedDataModelId() + GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels/${finalisedDataModelId}/newerVersions", MAP_ARG, true) + + then: + verifyResponse OK, response + !responseBody() + + cleanup: + DELETE("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys/${apiKey}", MAP_ARG, true) + verifyResponse NO_CONTENT, response + removeValidIdObject(subscribedCatalogueId) + cleanUpRoles(subscribedCatalogueId) + } + + void 'A08b : Test the newerVersions endpoint (with newer versions and API key)'() { + given: + String finalisedDataModelId = getFinalisedDataModelId() + Tuple tuple = getNewerDataModelIds() + String newerPublicId = tuple.v1 + String newerId = tuple.v2 + Map apiKeyJson = [ + name : 'Functional Test', + expiryDate: LocalDate.now().plusDays(5).format(DateTimeFormatter.ISO_LOCAL_DATE) + ] + loginAdmin() + POST("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys", apiKeyJson, MAP_ARG, true) + verifyResponse CREATED, response + String apiKey = responseBody().apiKey + Map subscriptionJson = [ + url : subscribedCatalogueUrl, + apiKey : apiKey, + label : 'Functional Test Label', + subscribedCatalogueType: subscribedCatalogueType.label, + description : 'Functional Test Description', + refreshPeriod : 7 + ] + POST('', subscriptionJson) + verifyResponse CREATED, response + String subscribedCatalogueId = responseBody().id + + when: + GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels/${finalisedDataModelId}/newerVersions", MAP_ARG, true) + + then: + verifyResponse OK, response + !responseBody() + + cleanup: + DELETE("dataModels/${newerId}?permanent=true", MAP_ARG, true) + verifyResponse NO_CONTENT, response + DELETE("dataModels/${newerPublicId}?permanent=true", MAP_ARG, true) + verifyResponse NO_CONTENT, response + DELETE("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys/${apiKey}", MAP_ARG, true) + verifyResponse NO_CONTENT, response + removeValidIdObject(subscribedCatalogueId) + cleanUpRoles(subscribedCatalogueId) + } + + void 'A08c : Test the newerModels endpoint (with newer models, without API key)'() { + given: + String finalisedDataModelId = getFinalisedDataModelId() + Tuple tuple = getNewerDataModelIds() + String newerPublicId = tuple.v1 + String newerId = tuple.v2 + loginAdmin() + Map subscriptionJson = [ + url : subscribedCatalogueUrl, + apiKey : '', + label : 'Functional Test Label', + subscribedCatalogueType: subscribedCatalogueType.label, + description : 'Functional Test Description', + refreshPeriod : 7 + ] + POST('', subscriptionJson) + verifyResponse CREATED, response + String subscribedCatalogueId = responseBody().id + PUT("dataModels/${finalisedDataModelId}/readByEveryone", [:], MAP_ARG, true) + verifyResponse OK, response + + when: + GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels/${finalisedDataModelId}/newerVersions", MAP_ARG, true) + + then: + verifyResponse OK, response + !responseBody() + + cleanup: + DELETE("dataModels/$newerPublicId?permanent=true", MAP_ARG, true) + verifyResponse NO_CONTENT, response + DELETE("dataModels/$newerId?permanent=true", MAP_ARG, true) + verifyResponse NO_CONTENT, response + DELETE("dataModels/$finalisedDataModelId/readByEveryone", MAP_ARG, true) + verifyResponse OK, response + removeValidIdObject(subscribedCatalogueId) + cleanUpRoles(subscribedCatalogueId) + } + + private void verifyJsonPublishedModel(Map publishedModel, String modelEndpoint, Map exporters) { + assert publishedModel + assert publishedModel.modelId ==~ /urn:uuid:\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/ + assert publishedModel.label + assert OffsetDateTime.parse(publishedModel.datePublished, DateTimeFormatter.ISO_OFFSET_DATE_TIME) + assert OffsetDateTime.parse(publishedModel.lastUpdated, DateTimeFormatter.ISO_OFFSET_DATE_TIME) + assert publishedModel.links.each {link -> + assert link.contentType + String exporterUrl = exporters.get(link.contentType) + assert link.url ==~ /http:\/\/localhost:$serverPort\/api\/$modelEndpoint\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/export\\/$exporterUrl/ + } + } +} diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/MauroJsonSubscribedCatalogueFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/MauroJsonSubscribedCatalogueFunctionalSpec.groovy new file mode 100644 index 0000000000..5881f6186a --- /dev/null +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/MauroJsonSubscribedCatalogueFunctionalSpec.groovy @@ -0,0 +1,359 @@ +/* + * Copyright 2020-2022 University of Oxford and Health and Social Care Information Centre, also known as NHS Digital + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package uk.ac.ox.softeng.maurodatamapper.testing.functional.federation + +import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedCatalogueType +import uk.ac.ox.softeng.maurodatamapper.testing.functional.test.BaseSubscribedCatalogueFunctionalSpec +import uk.ac.ox.softeng.maurodatamapper.version.Version + +import grails.testing.mixin.integration.Integration +import groovy.util.logging.Slf4j + +import java.time.LocalDate +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter + +import static uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress.getADMIN + +import static io.micronaut.http.HttpStatus.CREATED +import static io.micronaut.http.HttpStatus.NO_CONTENT +import static io.micronaut.http.HttpStatus.OK + +@Integration +@Slf4j +class MauroJsonSubscribedCatalogueFunctionalSpec extends BaseSubscribedCatalogueFunctionalSpec { + @Override + Map getValidJson() { + [ + url : subscribedCatalogueUrl, + apiKey : UUID.randomUUID().toString(), + label : 'Functional Test Label', + subscribedCatalogueType: subscribedCatalogueType.label, + description : 'Functional Test Description', + refreshPeriod : 7 + ] + } + + @Override + Map getInvalidJson() { + [ + url : 'wibble', + apiKey: '67421316-66a5-4830-9156-b1ba77bba5d1' + ] + } + + @Override + Map getValidUpdateJson() { + [ + description: 'Functional Test Description Updated' + ] + } + + @Override + String getExpectedShowJson() { + """{ + "apiKey": "\${json-unit.matches:id}", + "description": "Functional Test Description", + "id": "\${json-unit.matches:id}", + "label": "Functional Test Label", + "subscribedCatalogueType": "$subscribedCatalogueType.label", + "refreshPeriod": 7, + "url": "$subscribedCatalogueUrl" +}""" + } + + @Override + String getExpectedOpenAccessShowJson() { + """{ + "description": "Functional Test Description", + "id": "\${json-unit.matches:id}", + "label": "Functional Test Label", + "subscribedCatalogueType": "$subscribedCatalogueType.label", + "refreshPeriod": 7, + "url": "$subscribedCatalogueUrl" +}""" + } + + @Override + String getExpectedIndexJson() { + """{ + "count": 1, + "items": [ + { + "id": "\${json-unit.matches:id}", + "url": "$subscribedCatalogueUrl", + "label": "Functional Test Label", + "subscribedCatalogueType": "$subscribedCatalogueType.label", + "description": "Functional Test Description", + "refreshPeriod": 7 + } + ] +}""" + } + + @Override + SubscribedCatalogueType getSubscribedCatalogueType() { + SubscribedCatalogueType.MAURO_JSON + } + + @Override + String getSubscribedCatalogueUrl() { + "http://localhost:$serverPort".toString() + } + + /** + * Test the publishedModels endpoint. This would be on a remote host, but in this functional test + * we use the localhost. Test setup and execution is as follows: + * 1. Login as Admin and create an API Key for Admin + * 2. Subscribe to the local catalogue (in real life this would be remote), specifying the API key created above + * 3. Get the local /publishedModels endpoint. In real life this would connect to /api/published/models on the remote, + * but here we use the local. + * 4. Cleanup + */ + void 'A07a : Test the publishedModels endpoint'() { + + given: + Map apiKeyJson = [ + name : 'Functional Test', + expiryDate: LocalDate.now().plusDays(5).format(DateTimeFormatter.ISO_LOCAL_DATE) + ] + + when: + loginAdmin() + POST("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys", apiKeyJson, MAP_ARG, true) + + then: + verifyResponse CREATED, response + String apiKey = responseBody().apiKey + + when: + //note: using a groovy string like "http://localhost:$serverPort/" causes the url to be stripped when saving + Map subscriptionJson = [ + url : subscribedCatalogueUrl, + apiKey : apiKey, + label : 'Functional Test Label', + subscribedCatalogueType: subscribedCatalogueType.label, + description : 'Functional Test Description', + refreshPeriod : 7 + ] + POST('', subscriptionJson) + + then: + verifyResponse CREATED, response + String subscribedCatalogueId = responseBody().id + + when: + GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels", MAP_ARG, true) + + then: + verifyResponse(OK, response) + verifyBaseJsonResponse(responseBody(), true) + responseBody().items.size() == 3 + + and: + verifyJsonPublishedModel(responseBody().items.find {it.label == 'Finalised Example Test DataModel' && it.version == '1.0.0'}, 'DataModel', 'dataModels', + getDataModelExporters()) + verifyJsonPublishedModel(responseBody().items.find {it.label == 'Simple Test CodeSet' && it.version == '1.0.0'}, 'CodeSet', 'codeSets', getCodeSetExporters()) + verifyJsonPublishedModel(responseBody().items.find {it.label == 'Complex Test CodeSet' && it.version == '1.0.0'}, 'CodeSet', 'codeSets', getCodeSetExporters()) + + cleanup: + DELETE("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys/${apiKey}", MAP_ARG, true) + removeValidIdObject(subscribedCatalogueId) + cleanUpRoles(subscribedCatalogueId) + } + + void 'A07b : Test the publishedModels endpoint (without API key)'() { + given: + loginAdmin() + + when: + Map subscriptionJson = [ + url : subscribedCatalogueUrl, + apiKey : '', + label : 'Functional Test Label', + subscribedCatalogueType: subscribedCatalogueType.label, + description : 'Functional Test Description', + refreshPeriod : 7 + ] + POST('', subscriptionJson) + + then: + verifyResponse CREATED, response + String subscribedCatalogueId = responseBody().id + + when: + GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels", MAP_ARG, true) + + then: + verifyResponse(OK, response) + verifyBaseJsonResponse(responseBody(), false) + + cleanup: + removeValidIdObject(subscribedCatalogueId) + cleanUpRoles(subscribedCatalogueId) + } + + void 'A08a : Test the newerVersions endpoint (with no newer versions)'() { + given: + Map apiKeyJson = [ + name : 'Functional Test', + expiryDate: LocalDate.now().plusDays(5).format(DateTimeFormatter.ISO_LOCAL_DATE) + ] + loginAdmin() + POST("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys", apiKeyJson, MAP_ARG, true) + verifyResponse CREATED, response + String apiKey = responseBody().apiKey + Map subscriptionJson = [ + url : subscribedCatalogueUrl, + apiKey : apiKey, + label : 'Functional Test Label', + subscribedCatalogueType: subscribedCatalogueType.label, + description : 'Functional Test Description', + refreshPeriod : 7 + ] + POST('', subscriptionJson) + verifyResponse CREATED, response + String subscribedCatalogueId = responseBody().id + + when: + String finalisedDataModelId = getFinalisedDataModelId() + GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels/${finalisedDataModelId}/newerVersions", MAP_ARG, true) + + then: + verifyResponse OK, response + verifyBaseNewerVersionsJsonResponse(responseBody(), false) + + cleanup: + DELETE("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys/${apiKey}", MAP_ARG, true) + verifyResponse NO_CONTENT, response + removeValidIdObject(subscribedCatalogueId) + cleanUpRoles(subscribedCatalogueId) + } + + void 'A08b : Test the newerVersions endpoint (with newer versions and API key)'() { + given: + String finalisedDataModelId = getFinalisedDataModelId() + Tuple tuple = getNewerDataModelIds() + String newerPublicId = tuple.v1 + String newerId = tuple.v2 + Map apiKeyJson = [ + name : 'Functional Test', + expiryDate: LocalDate.now().plusDays(5).format(DateTimeFormatter.ISO_LOCAL_DATE) + ] + loginAdmin() + POST("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys", apiKeyJson, MAP_ARG, true) + verifyResponse CREATED, response + String apiKey = responseBody().apiKey + Map subscriptionJson = [ + url : subscribedCatalogueUrl, + apiKey : apiKey, + label : 'Functional Test Label', + subscribedCatalogueType: subscribedCatalogueType.label, + description : 'Functional Test Description', + refreshPeriod : 7 + ] + POST('', subscriptionJson) + verifyResponse CREATED, response + String subscribedCatalogueId = responseBody().id + + when: + GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels/${finalisedDataModelId}/newerVersions", MAP_ARG, true) + + then: + verifyResponse OK, response + verifyBaseNewerVersionsJsonResponse(responseBody(), true) + responseBody().newerPublishedModels.size() == 2 + + and: + verifyJsonPublishedModel(responseBody().newerPublishedModels.find {it.label == 'Finalised Example Test DataModel' && it.version == '2.0.0'}, 'DataModel', 'dataModels', + getDataModelExporters(), true) + verifyJsonPublishedModel(responseBody().newerPublishedModels.find {it.label == 'Finalised Example Test DataModel' && it.version == '3.0.0'}, 'DataModel', 'dataModels', + getDataModelExporters(), true) + + cleanup: + DELETE("dataModels/${newerId}?permanent=true", MAP_ARG, true) + verifyResponse NO_CONTENT, response + DELETE("dataModels/${newerPublicId}?permanent=true", MAP_ARG, true) + verifyResponse NO_CONTENT, response + DELETE("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys/${apiKey}", MAP_ARG, true) + verifyResponse NO_CONTENT, response + removeValidIdObject(subscribedCatalogueId) + cleanUpRoles(subscribedCatalogueId) + } + + void 'A08c : Test the newerModels endpoint (with newer models, without API key)'() { + given: + String finalisedDataModelId = getFinalisedDataModelId() + Tuple tuple = getNewerDataModelIds() + String newerPublicId = tuple.v1 + String newerId = tuple.v2 + loginAdmin() + Map subscriptionJson = [ + url : subscribedCatalogueUrl, + apiKey : '', + label : 'Functional Test Label', + subscribedCatalogueType: subscribedCatalogueType.label, + description : 'Functional Test Description', + refreshPeriod : 7 + ] + POST('', subscriptionJson) + verifyResponse CREATED, response + String subscribedCatalogueId = responseBody().id + PUT("dataModels/${finalisedDataModelId}/readByEveryone", [:], MAP_ARG, true) + verifyResponse OK, response + + when: + GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels/${finalisedDataModelId}/newerVersions", MAP_ARG, true) + + then: + verifyResponse OK, response + verifyBaseNewerVersionsJsonResponse(responseBody(), true) + responseBody().newerPublishedModels.size() == 1 + + and: + verifyJsonPublishedModel(responseBody().newerPublishedModels.find {it.label == 'Finalised Example Test DataModel' && it.version == '2.0.0'}, 'DataModel', 'dataModels', + getDataModelExporters(), true) + + cleanup: + DELETE("dataModels/$newerPublicId?permanent=true", MAP_ARG, true) + verifyResponse NO_CONTENT, response + DELETE("dataModels/$newerId?permanent=true", MAP_ARG, true) + verifyResponse NO_CONTENT, response + DELETE("dataModels/$finalisedDataModelId/readByEveryone", MAP_ARG, true) + verifyResponse OK, response + removeValidIdObject(subscribedCatalogueId) + cleanUpRoles(subscribedCatalogueId) + } + + private void verifyJsonPublishedModel(Map publishedModel, String modelType, String modelEndpoint, Map exporters, boolean newerVersion = false) { + assert publishedModel + assert publishedModel.modelId ==~ /\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/ + assert publishedModel.label + assert Version.from(publishedModel.version) + assert publishedModel.modelType == modelType + assert OffsetDateTime.parse(publishedModel.datePublished, DateTimeFormatter.ISO_OFFSET_DATE_TIME) + assert OffsetDateTime.parse(publishedModel.lastUpdated, DateTimeFormatter.ISO_OFFSET_DATE_TIME) + assert OffsetDateTime.parse(publishedModel.dateCreated, DateTimeFormatter.ISO_OFFSET_DATE_TIME) + assert publishedModel.links.each {link -> + assert link.contentType + String exporterUrl = exporters.get(link.contentType) + assert link.url ==~ /http:\/\/localhost:$serverPort\/api\/$modelEndpoint\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/export\\/$exporterUrl/ + } + if (newerVersion) assert publishedModel.previousModelId ==~ /\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/ + } +} diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/SubscribedModelFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/SubscribedModelFunctionalSpec.groovy index 7799ea98d4..9c65e9e708 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/SubscribedModelFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/SubscribedModelFunctionalSpec.groovy @@ -21,14 +21,17 @@ import uk.ac.ox.softeng.maurodatamapper.core.container.Folder import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.bootstrap.BootstrapModels import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedCatalogue +import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedCatalogueType import uk.ac.ox.softeng.maurodatamapper.security.authentication.ApiKey import uk.ac.ox.softeng.maurodatamapper.security.role.SecurableResourceGroupRole import uk.ac.ox.softeng.maurodatamapper.testing.functional.FunctionalSpec import uk.ac.ox.softeng.maurodatamapper.util.Utils +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Transactional import grails.testing.mixin.integration.Integration import grails.testing.spock.RunOnce +import groovy.json.JsonSlurper import groovy.util.logging.Slf4j import spock.lang.Requires import spock.lang.Shared @@ -58,11 +61,12 @@ import static io.micronaut.http.HttpStatus.UNPROCESSABLE_ENTITY */ @Integration @Slf4j -// Requires a connection to the CD environment, if this connection is not available +// Requires a connection to the CD environment, running a version providing the /types endpoint @Requires({ - String url = 'https://modelcatalogue.cs.ox.ac.uk/continuous-deployment' - HttpURLConnection connection = url.toURL().openConnection() as HttpURLConnection + String url = 'https://modelcatalogue.cs.ox.ac.uk/continuous-deployment/api/admin/subscribedCatalogues/types' + HttpURLConnection connection = (url).toURL().openConnection() as HttpURLConnection connection.setRequestMethod('GET') + connection.setRequestProperty('apiKey', '720e60bc-3993-48d4-a17e-c3a13f037c7e') connection.connect() connection.getResponseCode() == 200 }) @@ -71,6 +75,9 @@ class SubscribedModelFunctionalSpec extends FunctionalSpec { @Shared UUID subscribedCatalogueId + @Shared + UUID atomSubscribedCatalogueId + @Shared UUID adminApiKey @@ -88,12 +95,22 @@ class SubscribedModelFunctionalSpec extends FunctionalSpec { subscribedCatalogueId = new SubscribedCatalogue(url: 'https://modelcatalogue.cs.ox.ac.uk/continuous-deployment', apiKey: '720e60bc-3993-48d4-a17e-c3a13f037c7e', - label: 'Functional Test Label', + label: 'Functional Test Subscribed Catalogue (Mauro JSON)', + subscribedCatalogueType: SubscribedCatalogueType.MAURO_JSON, description: 'Functional Test Description', refreshPeriod: 7, createdBy: FUNCTIONAL_TEST).save(flush: true).id assert subscribedCatalogueId + atomSubscribedCatalogueId = new SubscribedCatalogue(url: 'https://modelcatalogue.cs.ox.ac.uk/continuous-deployment/api/feeds/all', + apiKey: '720e60bc-3993-48d4-a17e-c3a13f037c7e', + label: 'Functional Test Subscribed Catalogue (Atom)', + subscribedCatalogueType: SubscribedCatalogueType.ATOM, + description: 'Functional Test Description', + refreshPeriod: 7, + createdBy: FUNCTIONAL_TEST).save(flush: true).id + assert atomSubscribedCatalogueId + } @Transactional @@ -114,6 +131,10 @@ class SubscribedModelFunctionalSpec extends FunctionalSpec { "subscribedCatalogues/${getSubscribedCatalogueId()}/subscribedModels" } + String getResourcePathForAtom() { + "subscribedCatalogues/${getAtomSubscribedCatalogueId()}/subscribedModels" + } + String getValidId() { loginCreator() POST('', validJson) @@ -123,10 +144,14 @@ class SubscribedModelFunctionalSpec extends FunctionalSpec { id } - void removeValidIdObjects(String id, String localModelId = null) { + void removeValidIdObjects(String id, String localModelId = null, boolean cleanEndpoint = false) { loginCreator() if (id) { - DELETE(id) + if (cleanEndpoint) { + DELETE(id, MAP_ARG, true) + } else { + DELETE(id) + } verifyResponse NO_CONTENT, response } if (localModelId) { @@ -154,16 +179,28 @@ class SubscribedModelFunctionalSpec extends FunctionalSpec { Map getValidJson() { [ - subscribedModelId : '427d1243-4f89-46e8-8f8f-8424890b5083', - folderId : getFolderId(), - subscribedModelType: 'DataModel' + subscribedModel: [ + subscribedModelId: '427d1243-4f89-46e8-8f8f-8424890b5083', + folderId : getFolderId() + ] + ] + } + + Map getValidJsonForAtom() { + [ + subscribedModel: [ + subscribedModelId: 'urn:uuid:427d1243-4f89-46e8-8f8f-8424890b5083', + folderId : getFolderId() + ] ] } Map getInvalidJson() { [ - subscribedModelId: null, - folderId : getFolderId() + subscribedModel: [ + subscribedModelId: null, + folderId : getFolderId() + ] ] } @@ -171,7 +208,7 @@ class SubscribedModelFunctionalSpec extends FunctionalSpec { * Logged in as editor testing */ - void 'E02 : Test the show and index action correctly renders an instance for set user (as editor)'() { + void 'E01 : Test the show and index action correctly renders an instance for set user (as editor)'() { given: String id = getValidId() loginEditor() @@ -198,7 +235,7 @@ class SubscribedModelFunctionalSpec extends FunctionalSpec { * Logged out testing */ - void 'L02 : Test the show and index action does not render an instance for set user (not logged in)'() { + void 'L01 : Test the show and index action does not render an instance for set user (not logged in)'() { given: String id = getValidId() @@ -223,7 +260,7 @@ class SubscribedModelFunctionalSpec extends FunctionalSpec { /** * Testing when logged in as a no access/authenticated user */ - void 'N02 : Test the show and index action correctly renders an instance for set user (as no access/authenticated)'() { + void 'N01 : Test the show and index action correctly renders an instance for set user (as no access/authenticated)'() { given: String id = getValidId() loginAuthenticated() @@ -249,7 +286,7 @@ class SubscribedModelFunctionalSpec extends FunctionalSpec { /** * Testing when logged in as a reader only user */ - void 'R02 : Test the show and index action correctly renders an instance for set user (as reader)'() { + void 'R01 : Test the show and index action correctly renders an instance for set user (as reader)'() { given: String id = getValidId() loginReader() @@ -276,7 +313,7 @@ class SubscribedModelFunctionalSpec extends FunctionalSpec { * Logged in as admin testing */ - void 'A02 : Test the show action correctly renders an instance for set user (as admin)'() { + void 'A01 : Test the show action correctly renders an instance for set user (as admin)'() { given: String id = getValidId() loginAdmin() @@ -297,24 +334,34 @@ class SubscribedModelFunctionalSpec extends FunctionalSpec { * Logged in as editor testing */ - void 'E03 : Test the save action is ok (as editor)'() { + void 'E02 : Test the save action is ok (as editor) (for #catalogueType)'() { given: loginContainerAdmin() + String savePath + Map validJson + if (SubscribedCatalogueType.findForLabel(catalogueType) == SubscribedCatalogueType.MAURO_JSON) { + savePath = getResourcePath() + validJson = getValidJson() + } else { + savePath = getResourcePathForAtom() + validJson = getValidJsonForAtom() + } + when: 'The save action is executed with no content' - POST('', [:]) + POST(savePath, [:], MAP_ARG, true) then: 'The response is correct' verifyResponse UNPROCESSABLE_ENTITY, response when: 'The save action is executed with invalid data' - POST('', invalidJson) + POST(savePath, invalidJson, MAP_ARG, true) then: 'The response is correct' verifyResponse UNPROCESSABLE_ENTITY, response when: 'The save action is executed with valid data' - POST('', validJson) + POST(savePath, validJson, MAP_ARG, true) then: 'The response is correct' verifyResponse CREATED, response @@ -322,11 +369,14 @@ class SubscribedModelFunctionalSpec extends FunctionalSpec { String localModelId = responseBody().localModelId cleanup: - removeValidIdObjects(id, localModelId) + removeValidIdObjects(savePath + '/' + id, localModelId, true) cleanUpRoles(id, localModelId) + + where: + catalogueType << SubscribedCatalogueType.labels() } - void 'E04 : Test the delete action is forbidden (as editor)'() { + void 'E03 : Test the delete action is forbidden (as editor)'() { given: String id = getValidId() String localModelId = getSubscribedModelLocalModelId(id) @@ -347,29 +397,41 @@ class SubscribedModelFunctionalSpec extends FunctionalSpec { * Logged out testing */ - void 'L03 : Test the save action is not found (as not logged in)'() { + void 'L02 : Test the save action is not found (as not logged in) (for #catalogueType)'() { given: + String savePath + Map validJson + if (SubscribedCatalogueType.findForLabel(catalogueType) == SubscribedCatalogueType.MAURO_JSON) { + savePath = getResourcePath() + validJson = getValidJson() + } else { + savePath = getResourcePathForAtom() + validJson = getValidJsonForAtom() + } when: 'The save action is executed with no content' - POST('', [:]) + POST(savePath, [:], MAP_ARG, true) then: 'The response is not found' verifyResponse NOT_FOUND, response when: 'The save action is executed with invalid data' - POST('', invalidJson) + POST(savePath, invalidJson, MAP_ARG, true) then: 'The response is not found' verifyResponse NOT_FOUND, response when: 'The save action is executed with valid data' - POST('', validJson) + POST(savePath, validJson, MAP_ARG, true) then: 'The response is not found' verifyResponse NOT_FOUND, response + + where: + catalogueType << SubscribedCatalogueType.labels() } - void 'L04 : Test the delete action is not found (as not logged in)'() { + void 'L03 : Test the delete action is not found (as not logged in)'() { given: String id = getValidId() String localModelId = getSubscribedModelLocalModelId(id) @@ -389,32 +451,45 @@ class SubscribedModelFunctionalSpec extends FunctionalSpec { * Testing when logged in as a no access/authenticated user */ - void 'N03 : Test the save action is ok (as authenticated)'() { + void 'N02 : Test the save action is ok (as authenticated) (for #catalogueType)'() { given: loginAuthenticated() + String savePath + Map validJson + if (SubscribedCatalogueType.findForLabel(catalogueType) == SubscribedCatalogueType.MAURO_JSON) { + savePath = getResourcePath() + validJson = getValidJson() + } else { + savePath = getResourcePathForAtom() + validJson = getValidJsonForAtom() + } + when: 'The save action is executed with no content' - POST('', [:]) + POST(savePath, [:], MAP_ARG, true) then: 'The response is correct' verifyResponse UNPROCESSABLE_ENTITY, response when: 'The save action is executed with invalid data' - POST('', invalidJson) + POST(savePath, invalidJson, MAP_ARG, true) then: 'The response is correct' verifyResponse UNPROCESSABLE_ENTITY, response when: 'The save action is executed with valid data' - POST('', validJson) + POST(savePath, validJson, MAP_ARG, true) then: 'The response is correct' verifyResponse UNPROCESSABLE_ENTITY, response responseBody().total == 1 responseBody().errors.contains([message: 'Invalid folderId for subscribed model, user does not have the necessary permissions']) + + where: + catalogueType << SubscribedCatalogueType.labels() } - void 'N04 : Test the delete action is forbidden (as authenticated)'() { + void 'N03 : Test the delete action is forbidden (as authenticated)'() { given: String id = getValidId() String localModelId = getSubscribedModelLocalModelId(id) @@ -435,32 +510,45 @@ class SubscribedModelFunctionalSpec extends FunctionalSpec { * Testing when logged in as a reader only user */ - void 'R03a : Test the save action is forbidden (as reader)'() { + void 'R02 : Test the save action is forbidden (as reader) (for #catalogueType)'() { given: loginReader() + String savePath + Map validJson + if (SubscribedCatalogueType.findForLabel(catalogueType) == SubscribedCatalogueType.MAURO_JSON) { + savePath = getResourcePath() + validJson = getValidJson() + } else { + savePath = getResourcePathForAtom() + validJson = getValidJsonForAtom() + } + when: 'The save action is executed with no content' - POST('', [:]) + POST(savePath, [:], MAP_ARG, true) then: 'The response is correct' verifyResponse UNPROCESSABLE_ENTITY, response when: 'The save action is executed with invalid data' - POST('', invalidJson) + POST(savePath, invalidJson, MAP_ARG, true) then: 'The response is correct' verifyResponse UNPROCESSABLE_ENTITY, response when: 'The save action is executed with valid data' - POST('', validJson) + POST(savePath, validJson, MAP_ARG, true) then: 'The response is correct' verifyResponse UNPROCESSABLE_ENTITY, response responseBody().total == 1 responseBody().errors.contains([message: 'Invalid folderId for subscribed model, user does not have the necessary permissions']) + + where: + catalogueType << SubscribedCatalogueType.labels() } - void 'R04 : Test the delete action is forbidden (as reader)'() { + void 'R03 : Test the delete action is forbidden (as reader)'() { given: String id = getValidId() String localModelId = getSubscribedModelLocalModelId(id) @@ -482,24 +570,34 @@ class SubscribedModelFunctionalSpec extends FunctionalSpec { * This proves that admin users can mess with items created by other users */ - void 'A03 : Test the save action is ok (as admin)'() { + void 'A02 : Test the save action is ok (as admin) (for #catalogueType)'() { given: loginAdmin() + String savePath + Map validJson + if (SubscribedCatalogueType.findForLabel(catalogueType) == SubscribedCatalogueType.MAURO_JSON) { + savePath = getResourcePath() + validJson = getValidJson() + } else { + savePath = getResourcePathForAtom() + validJson = getValidJsonForAtom() + } + when: 'The save action is executed with no content' - POST('', [:]) + POST(savePath, [:], MAP_ARG, true) then: 'The response is correct' verifyResponse UNPROCESSABLE_ENTITY, response when: 'The save action is executed with invalid data' - POST('', invalidJson) + POST(savePath, invalidJson, MAP_ARG, true) then: 'The response is correct' verifyResponse UNPROCESSABLE_ENTITY, response when: 'The save action is executed with valid data' - POST('', validJson) + POST(savePath, validJson, MAP_ARG, true) then: 'The response is correct' verifyResponse CREATED, response @@ -507,11 +605,14 @@ class SubscribedModelFunctionalSpec extends FunctionalSpec { String localModelId = responseBody().localModelId cleanup: - removeValidIdObjects(id, localModelId) + removeValidIdObjects(savePath + '/' + id, localModelId, true) cleanUpRoles(id, localModelId) + + where: + catalogueType << SubscribedCatalogueType.labels() } - void 'A04 : Test the delete action is ok (as admin)'() { + void 'A03 : Test the delete action is ok (as admin)'() { given: String id = getValidId() String localModelId = getSubscribedModelLocalModelId(id) @@ -528,12 +629,22 @@ class SubscribedModelFunctionalSpec extends FunctionalSpec { cleanUpRoles(id) } - void 'A05 : Test the save action with attempted federation (as admin)'() { + void 'A04 : Test the save action with attempted federation (as admin) (for #catalogueType)'() { given: loginAdmin() + String savePath + Map validJson + if (SubscribedCatalogueType.findForLabel(catalogueType) == SubscribedCatalogueType.MAURO_JSON) { + savePath = getResourcePath() + validJson = getValidJson() + } else { + savePath = getResourcePathForAtom() + validJson = getValidJsonForAtom() + } + when: 'The save action is executed with valid data' - POST('', validJson) + POST(savePath, validJson, MAP_ARG, true) then: 'The response is correct' verifyResponse CREATED, response @@ -541,17 +652,20 @@ class SubscribedModelFunctionalSpec extends FunctionalSpec { String localModelId = responseBody().localModelId when: 'The save action is executed with existing published model id' - POST('', validJson) + POST(savePath, validJson, MAP_ARG, true) then: 'The response is unprocessable as this model is already subscribed' verifyResponse UNPROCESSABLE_ENTITY, response log.debug('responseBody().errors.first().message={}', responseBody().errors.first().message) responseBody().errors.first().message == 'Property [subscribedModelId] of class [class uk.ac.ox.softeng.maurodatamapper.federation.SubscribedModel] with value [' + - getValidJson().subscribedModelId + '] must be unique' + validJson.subscribedModel.subscribedModelId + '] must be unique' cleanup: - removeValidIdObjects(id, localModelId) + removeValidIdObjects(savePath + '/' + id, localModelId, true) cleanUpRoles(id, localModelId) + + where: + catalogueType << SubscribedCatalogueType.labels() } @Transactional diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/atom/FeedFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/atom/FeedFunctionalSpec.groovy index 80ee80dc69..993a4b561d 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/atom/FeedFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/atom/FeedFunctionalSpec.groovy @@ -162,15 +162,15 @@ class FeedFunctionalSpec extends FunctionalSpec implements XmlComparer { private static Map getDataModelExporters() { [ - 'application/mdm+json': 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/3.1', - 'application/mdm+xml' : 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelXmlExporterService/5.1' + 'application/mauro.datamodel+json': 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/3.1', + 'application/mauro.datamodel+xml' : 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelXmlExporterService/5.1' ] } private static Map getCodeSetExporters() { [ - 'application/mdm+json': 'uk.ac.ox.softeng.maurodatamapper.terminology.provider.exporter/CodeSetJsonExporterService/4.0', - 'application/mdm+xml' : 'uk.ac.ox.softeng.maurodatamapper.terminology.provider.exporter/CodeSetXmlExporterService/5.0' + 'application/mauro.codeset+json': 'uk.ac.ox.softeng.maurodatamapper.terminology.provider.exporter/CodeSetJsonExporterService/4.0', + 'application/mauro.codeset+xml' : 'uk.ac.ox.softeng.maurodatamapper.terminology.provider.exporter/CodeSetXmlExporterService/5.0' ] } } diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/publish/PublishFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/publish/PublishFunctionalSpec.groovy index 09e5d5b713..d140290ff0 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/publish/PublishFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/publish/PublishFunctionalSpec.groovy @@ -116,9 +116,9 @@ class PublishFunctionalSpec extends FunctionalSpec implements XmlComparer { responseBody().publishedModels.size() == 3 and: - verifyJsonPublishedModel(responseBody().publishedModels.find {it.title == 'Simple Test CodeSet 1.0.0'}, 'CodeSet', 'codeSets', getCodeSetExporters()) - verifyJsonPublishedModel(responseBody().publishedModels.find {it.title == 'Complex Test CodeSet 1.0.0'}, 'CodeSet', 'codeSets', getCodeSetExporters()) - verifyJsonPublishedModel(responseBody().publishedModels.find {it.title == 'Finalised Example Test DataModel 1.0.0'}, 'DataModel', 'dataModels', + verifyJsonPublishedModel(responseBody().publishedModels.find {it.label == 'Simple Test CodeSet' && it.version == '1.0.0'}, 'CodeSet', 'codeSets', getCodeSetExporters()) + verifyJsonPublishedModel(responseBody().publishedModels.find {it.label == 'Complex Test CodeSet' && it.version == '1.0.0'}, 'CodeSet', 'codeSets', getCodeSetExporters()) + verifyJsonPublishedModel(responseBody().publishedModels.find {it.label == 'Finalised Example Test DataModel' && it.version == '1.0.0'}, 'DataModel', 'dataModels', getDataModelExporters()) } @@ -135,9 +135,9 @@ class PublishFunctionalSpec extends FunctionalSpec implements XmlComparer { result.publishedModels.children().size() == 3 and: - verifyXmlPublishedModel(result.publishedModels.publishedModel.find {it.title == 'Simple Test CodeSet 1.0.0'}, 'CodeSet', 'codeSets', getCodeSetExporters()) - verifyXmlPublishedModel(result.publishedModels.publishedModel.find {it.title == 'Complex Test CodeSet 1.0.0'}, 'CodeSet', 'codeSets', getCodeSetExporters()) - verifyXmlPublishedModel(result.publishedModels.publishedModel.find {it.title == 'Finalised Example Test DataModel 1.0.0'}, 'DataModel', 'dataModels', + verifyXmlPublishedModel(result.publishedModels.publishedModel.find {it.label == 'Simple Test CodeSet' && it.version == '1.0.0'}, 'CodeSet', 'codeSets', getCodeSetExporters()) + verifyXmlPublishedModel(result.publishedModels.publishedModel.find {it.label == 'Complex Test CodeSet' && it.version == '1.0.0'}, 'CodeSet', 'codeSets', getCodeSetExporters()) + verifyXmlPublishedModel(result.publishedModels.publishedModel.find {it.label == 'Finalised Example Test DataModel' && it.version == '1.0.0'}, 'DataModel', 'dataModels', getDataModelExporters()) } @@ -237,9 +237,9 @@ class PublishFunctionalSpec extends FunctionalSpec implements XmlComparer { responseBody().newerPublishedModels.size() == 2 and: - verifyJsonPublishedModel(responseBody().newerPublishedModels.find {it.title == 'Finalised Example Test DataModel 2.0.0'}, 'DataModel', 'dataModels', + verifyJsonPublishedModel(responseBody().newerPublishedModels.find {it.label == 'Finalised Example Test DataModel' && it.version == '2.0.0'}, 'DataModel', 'dataModels', getDataModelExporters(), true) - verifyJsonPublishedModel(responseBody().newerPublishedModels.find {it.title == 'Finalised Example Test DataModel 3.0.0'}, 'DataModel', 'dataModels', + verifyJsonPublishedModel(responseBody().newerPublishedModels.find {it.label == 'Finalised Example Test DataModel' && it.version == '3.0.0'}, 'DataModel', 'dataModels', getDataModelExporters(), true) cleanup: @@ -266,9 +266,9 @@ class PublishFunctionalSpec extends FunctionalSpec implements XmlComparer { result.newerPublishedModels.children().size() == 2 and: - verifyXmlPublishedModel(result.newerPublishedModels.publishedModel.find {it.title == 'Finalised Example Test DataModel 2.0.0'}, 'DataModel', 'dataModels', + verifyXmlPublishedModel(result.newerPublishedModels.publishedModel.find {it.label == 'Finalised Example Test DataModel' && it.version == '2.0.0'}, 'DataModel', 'dataModels', getDataModelExporters(), true) - verifyXmlPublishedModel(result.newerPublishedModels.publishedModel.find {it.title == 'Finalised Example Test DataModel 3.0.0'}, 'DataModel', 'dataModels', + verifyXmlPublishedModel(result.newerPublishedModels.publishedModel.find {it.label == 'Finalised Example Test DataModel' && it.version == '3.0.0'}, 'DataModel', 'dataModels', getDataModelExporters(), true) cleanup: @@ -284,7 +284,6 @@ class PublishFunctionalSpec extends FunctionalSpec implements XmlComparer { assert publishedModel.modelId ==~ /\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/ assert publishedModel.label assert Version.from(publishedModel.version) - assert publishedModel.title == publishedModel.label + ' ' + publishedModel.version assert publishedModel.modelType == modelType assert OffsetDateTime.parse(publishedModel.datePublished, DateTimeFormatter.ISO_OFFSET_DATE_TIME) assert OffsetDateTime.parse(publishedModel.lastUpdated, DateTimeFormatter.ISO_OFFSET_DATE_TIME) @@ -302,7 +301,6 @@ class PublishFunctionalSpec extends FunctionalSpec implements XmlComparer { assert publishedModel.modelId.text() ==~ /\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/ assert publishedModel.label.text() assert Version.from(publishedModel.version.text()) - assert publishedModel.title == publishedModel.label.text() + ' ' + publishedModel.version.text() assert publishedModel.modelType == modelType assert OffsetDateTime.parse(publishedModel.datePublished.text(), DateTimeFormatter.ISO_OFFSET_DATE_TIME) assert OffsetDateTime.parse(publishedModel.lastUpdated.text(), DateTimeFormatter.ISO_OFFSET_DATE_TIME) @@ -375,15 +373,15 @@ class PublishFunctionalSpec extends FunctionalSpec implements XmlComparer { private static Map getDataModelExporters() { [ - 'application/mdm+json': 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/3.1', - 'application/mdm+xml' : 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelXmlExporterService/5.1' + 'application/mauro.datamodel+json': 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/3.1', + 'application/mauro.datamodel+xml' : 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelXmlExporterService/5.1' ] } private static Map getCodeSetExporters() { [ - 'application/mdm+json': 'uk.ac.ox.softeng.maurodatamapper.terminology.provider.exporter/CodeSetJsonExporterService/4.0', - 'application/mdm+xml' : 'uk.ac.ox.softeng.maurodatamapper.terminology.provider.exporter/CodeSetXmlExporterService/5.0' + 'application/mauro.codeset+json': 'uk.ac.ox.softeng.maurodatamapper.terminology.provider.exporter/CodeSetJsonExporterService/4.0', + 'application/mauro.codeset+xml' : 'uk.ac.ox.softeng.maurodatamapper.terminology.provider.exporter/CodeSetXmlExporterService/5.0' ] } } diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/referencedata/ReferenceDataModelFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/referencedata/ReferenceDataModelFunctionalSpec.groovy index b649558b07..e173256fcd 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/referencedata/ReferenceDataModelFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/referencedata/ReferenceDataModelFunctionalSpec.groovy @@ -245,7 +245,7 @@ class ReferenceDataModelFunctionalSpec extends ModelUserAccessPermissionChanging ], "providerType": "ReferenceDataModelExporter", "fileExtension": "json", - "fileType": "text/json", + "contentType": "application/mauro.referencedatamodel+json", "canExportMultipleDomains": false }, { @@ -259,7 +259,7 @@ class ReferenceDataModelFunctionalSpec extends ModelUserAccessPermissionChanging ], "providerType": "ReferenceDataModelExporter", "fileExtension": "xml", - "fileType": "text/xml", + "contentType": "application/mauro.referencedatamodel+xml", "canExportMultipleDomains": false } ]''' diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/terminology/CodeSetFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/terminology/CodeSetFunctionalSpec.groovy index b90787c7d1..45ab2e50fd 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/terminology/CodeSetFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/terminology/CodeSetFunctionalSpec.groovy @@ -244,7 +244,7 @@ class CodeSetFunctionalSpec extends ModelUserAccessPermissionChangingAndVersioni ], "providerType": "CodeSetExporter", "fileExtension": "xml", - "fileType": "text/xml", + "contentType": "application/mauro.codeset+xml", "canExportMultipleDomains": true }, { @@ -258,7 +258,7 @@ class CodeSetFunctionalSpec extends ModelUserAccessPermissionChangingAndVersioni ], "providerType": "CodeSetExporter", "fileExtension": "json", - "fileType": "text/json", + "contentType": "application/mauro.codeset+json", "canExportMultipleDomains": true } ]''' diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/terminology/TerminologyFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/terminology/TerminologyFunctionalSpec.groovy index e6014b496c..9a491736bb 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/terminology/TerminologyFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/terminology/TerminologyFunctionalSpec.groovy @@ -230,7 +230,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers "knownMetadataKeys": [], "providerType": "TerminologyExporter", "fileExtension": "json", - "fileType": "text/json", + "contentType": "application/mauro.terminology+json", "canExportMultipleDomains": true }, { @@ -242,7 +242,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers "knownMetadataKeys": [], "providerType": "TerminologyExporter", "fileExtension": "xml", - "fileType": "text/xml", + "contentType": "application/mauro.terminology+xml", "canExportMultipleDomains": true } ]''' diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/SubscribedCatalogueFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/test/BaseSubscribedCatalogueFunctionalSpec.groovy similarity index 60% rename from mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/SubscribedCatalogueFunctionalSpec.groovy rename to mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/test/BaseSubscribedCatalogueFunctionalSpec.groovy index 1b75d4c819..e849c7a529 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/federation/SubscribedCatalogueFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/test/BaseSubscribedCatalogueFunctionalSpec.groovy @@ -15,10 +15,12 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package uk.ac.ox.softeng.maurodatamapper.testing.functional.federation +package uk.ac.ox.softeng.maurodatamapper.testing.functional.test +import uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter.DataModelJsonExporterService +import uk.ac.ox.softeng.maurodatamapper.federation.SubscribedCatalogueType import uk.ac.ox.softeng.maurodatamapper.security.role.SecurableResourceGroupRole import uk.ac.ox.softeng.maurodatamapper.testing.functional.FunctionalSpec import uk.ac.ox.softeng.maurodatamapper.util.Utils @@ -51,6 +53,7 @@ import static io.micronaut.http.HttpStatus.UNPROCESSABLE_ENTITY * | PUT | /api/admin/subscribedCatalogues/${id} | Action: update | * | GET | /api/admin/subscribedCatalogues/${id} | Action: show | * | GET | /api/admin/subscribedCatalogues/${subscribedCatalogueId}/testConnection | Action: testConnection | + * | GET | /api/subscribedCatalogues | Action: types | * | GET | /api/subscribedCatalogues | Action: index | * | GET | /api/subscribedCatalogues/${id} | Action: show | * | GET | /api/subscribedCatalogues/${subscribedCatalogueId}/testConnection | Action: testConnection | @@ -59,16 +62,29 @@ import static io.micronaut.http.HttpStatus.UNPROCESSABLE_ENTITY */ @Integration @Slf4j -class SubscribedCatalogueFunctionalSpec extends FunctionalSpec { +abstract class BaseSubscribedCatalogueFunctionalSpec extends FunctionalSpec { + + abstract Map getValidJson() + + abstract Map getInvalidJson() + + abstract Map getValidUpdateJson() + + abstract String getExpectedShowJson() + + abstract String getExpectedOpenAccessShowJson() + + abstract String getExpectedIndexJson() + + abstract SubscribedCatalogueType getSubscribedCatalogueType() + + abstract String getSubscribedCatalogueUrl() @Override String getResourcePath() { 'admin/subscribedCatalogues' } - @Autowired - DataModelJsonExporterService dataModelJsonExporterService - String getValidId() { loginAdmin() POST('', validJson) @@ -119,74 +135,15 @@ class SubscribedCatalogueFunctionalSpec extends FunctionalSpec { void cleanUpRoles(String... ids) { log.info('Cleaning up roles and groups') log.debug('Cleaning up {} roles for ids {}', SecurableResourceGroupRole.count(), ids) - SecurableResourceGroupRole.bySecurableResourceIds(ids.collect { Utils.toUuid(it) }).deleteAll() + SecurableResourceGroupRole.bySecurableResourceIds(ids.collect {Utils.toUuid(it)}).deleteAll() safeSessionFlush() } - Map getValidJson() { - [ - url : "http://localhost:$serverPort".toString(), - apiKey : UUID.randomUUID().toString(), - label : 'Functional Test Label', - description : 'Functional Test Description', - refreshPeriod: 7 - ] - } - - Map getInvalidJson() { - [ - url : 'wibble', - apiKey: '67421316-66a5-4830-9156-b1ba77bba5d1' - ] - } - - Map getValidUpdateJson() { - [ - description: 'Functional Test Description Updated' - ] - } - - String getExpectedShowJson() { - """{ - "apiKey": "\${json-unit.matches:id}", - "description": "Functional Test Description", - "id": "\${json-unit.matches:id}", - "label": "Functional Test Label", - "refreshPeriod": 7, - "url": "http://localhost:$serverPort" -}""" - } - - String getExpectedOpenAccessShowJson() { - """{ - "description": "Functional Test Description", - "id": "\${json-unit.matches:id}", - "label": "Functional Test Label", - "refreshPeriod": 7, - "url": "http://localhost:$serverPort" -}""" - } - - String getExpectedIndexJson() { - """{ - "count": 1, - "items": [ - { - "id": "\${json-unit.matches:id}", - "url": "http://localhost:$serverPort", - "label": "Functional Test Label", - "description": "Functional Test Description", - "refreshPeriod": 7 - } - ] -}""" - } - /* * Logged in as editor testing */ - void 'E02 : Test the open access show and index actions render and admin actions are forbidden (as editor)'() { + void 'E01 : Test the open access show and index actions render and admin actions are forbidden (as editor)'() { given: String id = getValidId() loginEditor() @@ -220,11 +177,25 @@ class SubscribedCatalogueFunctionalSpec extends FunctionalSpec { cleanUpRoles(id) } + void 'E02 : Test the types action renders (as editor)'() { + given: + loginEditor() + + when: + GET('types', STRING_ARG) + + then: + verifyJsonResponse OK, '''[ + "Atom", + "Mauro JSON" + ]''' + } + /* * Logged out testing */ - void 'L02 : Test the show and index actions do not render an instance for set user (not logged in)'() { + void 'L01 : Test the show and index actions do not render an instance for set user (not logged in)'() { given: String id = getValidId() @@ -257,11 +228,7 @@ class SubscribedCatalogueFunctionalSpec extends FunctionalSpec { cleanUpRoles(id) } - /* - * Logged out testing - */ - - void 'L02a : Test the index action for format opml responds when not logged in'() { + void 'L01O : Test the index action for format opml responds when not logged in'() { given: String id = getValidId() @@ -278,10 +245,18 @@ class SubscribedCatalogueFunctionalSpec extends FunctionalSpec { cleanUpRoles(id) } + void 'L02 : Test the types action does not render (when not logged in)'() { + when: + GET('types') + + then: + verifyResponse NOT_FOUND, response + } + /** * Testing when logged in as a no access/authenticated user */ - void 'N02 : Test the open access show and index actions render and admin actions are forbidden (as no access/authenticated)'() { + void 'N01 : Test the open access show and index actions render and admin actions are forbidden (as no access/authenticated)'() { given: String id = getValidId() loginAuthenticated() @@ -315,10 +290,24 @@ class SubscribedCatalogueFunctionalSpec extends FunctionalSpec { cleanUpRoles(id) } + void 'N02 : Test the types action renders (as no access/authenticated)'() { + given: + loginAuthenticated() + + when: + GET('types', STRING_ARG) + + then: + verifyJsonResponse OK, '''[ + "Atom", + "Mauro JSON" + ]''' + } + /** * Testing when logged in as a reader only user */ - void 'R02 : Test the open access show and index actions render and admin actions are forbidden (as reader)'() { + void 'R01 : Test the open access show and index actions render and admin actions are forbidden (as reader)'() { given: String id = getValidId() loginReader() @@ -352,11 +341,25 @@ class SubscribedCatalogueFunctionalSpec extends FunctionalSpec { cleanUpRoles(id) } + void 'R02 : Test the types action renders (as reader)'() { + given: + loginReader() + + when: + GET('types', STRING_ARG) + + then: + verifyJsonResponse OK, '''[ + "Atom", + "Mauro JSON" + ]''' + } + /* * Logged in as admin testing */ - void 'A02 : Test the show and index actions correctly render (as admin)'() { + void 'A01 : Test the show and index actions correctly render (as admin)'() { given: String id = getValidId() loginAdmin() @@ -390,6 +393,20 @@ class SubscribedCatalogueFunctionalSpec extends FunctionalSpec { cleanUpRoles(id) } + void 'A02 : Test the types action renders (as admin)'() { + given: + loginAdmin() + + when: + GET('types', STRING_ARG) + + then: + verifyJsonResponse OK, '''[ + "Atom", + "Mauro JSON" + ]''' + } + /* * Logged in as editor testing */ @@ -417,7 +434,7 @@ class SubscribedCatalogueFunctionalSpec extends FunctionalSpec { verifyResponse FORBIDDEN, response } - void 'E03b : Test the save action is forbidden when using PUT (as editor)'() { + void 'E04b : Test the save action is forbidden when using PUT (as editor)'() { given: String id = getValidId() loginEditor() @@ -439,7 +456,7 @@ class SubscribedCatalogueFunctionalSpec extends FunctionalSpec { cleanUpRoles(id) } - void 'E04 : Test the delete action is forbidden (as editor)'() { + void 'E05 : Test the delete action is forbidden (as editor)'() { given: String id = getValidId() loginEditor() @@ -480,7 +497,7 @@ class SubscribedCatalogueFunctionalSpec extends FunctionalSpec { verifyResponse NOT_FOUND, response } - void 'L03b : Test the save action is not found when using PUT (as not logged in)'() { + void 'L03b : Test the save action is not found when using PUT (as not logged in)'() { given: String id = getValidId() @@ -748,17 +765,7 @@ class SubscribedCatalogueFunctionalSpec extends FunctionalSpec { cleanUpRoles(id) } - /** - * Test the publishedModels endpoint. This would be on a remote host, but in this functional test - * we use the localhost. Test setup and execution is as follows: - * 1. Login as Admin and create an API Key for Admin - * 2. Subscribe to the local catalogue (in real life this would be remote), specifying the API key created above - * 3. Get the local /publishedModels endpoint. In real life this would connect to /api/published/models on the remote, - * but here we use the local. - * 4. Cleanup - */ - void 'A05 : Test the publishedModels endpoint'() { - + void 'A05 : Test the testConnection action'() { given: Map apiKeyJson = [ name : 'Functional Test', @@ -767,228 +774,20 @@ class SubscribedCatalogueFunctionalSpec extends FunctionalSpec { when: loginAdmin() - POST("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys", apiKeyJson, MAP_ARG, true) + POST("catalogueUsers/${getUserByEmailAddress(StandardEmailAddress.ADMIN).id}/apiKeys", apiKeyJson, MAP_ARG, true) then: verifyResponse CREATED, response String apiKey = responseBody().apiKey when: - //note: using a groovy string like "http://localhost:$serverPort/" causes the url to be stripped when saving Map subscriptionJson = [ - url : "http://localhost:$serverPort/".toString(), - apiKey : apiKey, - label : 'Functional Test Label', - description : 'Functional Test Description', - refreshPeriod: 7 - ] - POST('', subscriptionJson) - - then: - verifyResponse CREATED, response - String subscribedCatalogueId = responseBody().id - - when: - GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels", MAP_ARG,true) - - then: - verifyResponse(OK, response) - verifyBaseJsonResponse(responseBody(), true) - responseBody().items.size() == 3 - - and: - verifyJsonPublishedModel(responseBody().items.find {it.title == 'Finalised Example Test DataModel 1.0.0'}, 'DataModel', 'dataModels', - getDataModelExporters()) - verifyJsonPublishedModel(responseBody().items.find {it.title == 'Simple Test CodeSet 1.0.0'}, 'CodeSet', 'codeSets', getCodeSetExporters()) - verifyJsonPublishedModel(responseBody().items.find {it.title == 'Complex Test CodeSet 1.0.0'}, 'CodeSet', 'codeSets', getCodeSetExporters()) - - cleanup: - DELETE("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys/${apiKey}", MAP_ARG, true) - removeValidIdObject(subscribedCatalogueId) - cleanUpRoles(subscribedCatalogueId) - } - - void 'A06 : Test the publishedModels endpoint (without API key)'() { - given: - loginAdmin() - - when: - Map subscriptionJson = [ - url : "http://localhost:$serverPort/".toString(), - apiKey : '', - label : 'Functional Test Label', - description : 'Functional Test Description', - refreshPeriod: 7 - ] - POST('', subscriptionJson) - - then: - verifyResponse CREATED, response - String subscribedCatalogueId = responseBody().id - - when: - GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels", MAP_ARG, true) - - then: - verifyResponse(OK, response) - verifyBaseJsonResponse(responseBody(), false) - - cleanup: - removeValidIdObject(subscribedCatalogueId) - cleanUpRoles(subscribedCatalogueId) - } - - void 'A07 : Test the newerModels endpoint (with no newer models)'() { - given: - Map apiKeyJson = [ - name : 'Functional Test', - expiryDate: LocalDate.now().plusDays(5).format(DateTimeFormatter.ISO_LOCAL_DATE) - ] - loginAdmin() - POST("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys", apiKeyJson, MAP_ARG, true) - verifyResponse CREATED, response - String apiKey = responseBody().apiKey - Map subscriptionJson = [ - url : "http://localhost:$serverPort/".toString(), - apiKey : apiKey, - label : 'Functional Test Label', - description : 'Functional Test Description', - refreshPeriod: 7 - ] - POST('', subscriptionJson) - verifyResponse CREATED, response - String subscribedCatalogueId = responseBody().id - - when: - String finalisedDataModelId = getFinalisedDataModelId() - GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels/${finalisedDataModelId}/newerVersions", MAP_ARG, true) - - then: - verifyResponse OK, response - verifyBaseNewerVersionsJsonResponse(responseBody(), false) - - cleanup: - DELETE("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys/${apiKey}", MAP_ARG, true) - verifyResponse NO_CONTENT, response - removeValidIdObject(subscribedCatalogueId) - cleanUpRoles(subscribedCatalogueId) - } - - void 'A08 : Test the newerModels endpoint (with newer models and API key)'() { - given: - String finalisedDataModelId = getFinalisedDataModelId() - Tuple tuple = getNewerDataModelIds() - String newerPublicId = tuple.v1 - String newerId = tuple.v2 - Map apiKeyJson = [ - name : 'Functional Test', - expiryDate: LocalDate.now().plusDays(5).format(DateTimeFormatter.ISO_LOCAL_DATE) - ] - loginAdmin() - POST("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys", apiKeyJson, MAP_ARG, true) - verifyResponse CREATED, response - String apiKey = responseBody().apiKey - Map subscriptionJson = [ - url : "http://localhost:$serverPort/".toString(), - apiKey : apiKey, - label : 'Functional Test Label', - description : 'Functional Test Description', - refreshPeriod: 7 - ] - POST('', subscriptionJson) - verifyResponse CREATED, response - String subscribedCatalogueId = responseBody().id - - when: - GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels/${finalisedDataModelId}/newerVersions", MAP_ARG, true) - - then: - verifyResponse OK, response - verifyBaseNewerVersionsJsonResponse(responseBody(), true) - responseBody().newerPublishedModels.size() == 2 - - and: - verifyJsonPublishedModel(responseBody().newerPublishedModels.find {it.title == 'Finalised Example Test DataModel 2.0.0'}, 'DataModel', 'dataModels', - getDataModelExporters(), true) - verifyJsonPublishedModel(responseBody().newerPublishedModels.find {it.title == 'Finalised Example Test DataModel 3.0.0'}, 'DataModel', 'dataModels', - getDataModelExporters(), true) - - cleanup: - DELETE("dataModels/${newerId}?permanent=true", MAP_ARG, true) - verifyResponse NO_CONTENT, response - DELETE("dataModels/${newerPublicId}?permanent=true", MAP_ARG, true) - verifyResponse NO_CONTENT, response - DELETE("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys/${apiKey}", MAP_ARG, true) - verifyResponse NO_CONTENT, response - removeValidIdObject(subscribedCatalogueId) - cleanUpRoles(subscribedCatalogueId) - } - - void 'A09 : Test the newerModels endpoint (with newer models, without API key)'() { - given: - String finalisedDataModelId = getFinalisedDataModelId() - Tuple tuple = getNewerDataModelIds() - String newerPublicId = tuple.v1 - String newerId = tuple.v2 - loginAdmin() - Map subscriptionJson = [ - url : "http://localhost:$serverPort/".toString(), - apiKey : '', - label : 'Functional Test Label', - description : 'Functional Test Description', - refreshPeriod: 7 - ] - POST('', subscriptionJson) - verifyResponse CREATED, response - String subscribedCatalogueId = responseBody().id - PUT("dataModels/${finalisedDataModelId}/readByEveryone", [:], MAP_ARG, true) - verifyResponse OK, response - - when: - GET("subscribedCatalogues/${subscribedCatalogueId}/publishedModels/${finalisedDataModelId}/newerVersions", MAP_ARG, true) - - then: - verifyResponse OK, response - verifyBaseNewerVersionsJsonResponse(responseBody(), true) - responseBody().newerPublishedModels.size() == 1 - - and: - verifyJsonPublishedModel(responseBody().newerPublishedModels.find {it.title == 'Finalised Example Test DataModel 2.0.0'}, 'DataModel', 'dataModels', - getDataModelExporters(), true) - - cleanup: - DELETE("dataModels/$newerPublicId?permanent=true", MAP_ARG, true) - verifyResponse NO_CONTENT, response - DELETE("dataModels/$newerId?permanent=true", MAP_ARG, true) - verifyResponse NO_CONTENT, response - DELETE("dataModels/$finalisedDataModelId/readByEveryone", MAP_ARG, true) - verifyResponse OK, response - removeValidIdObject(subscribedCatalogueId) - cleanUpRoles(subscribedCatalogueId) - } - - void 'A10 : Test the testConnection action'() { - given: - Map apiKeyJson = [ - name : 'Functional Test', - expiryDate: LocalDate.now().plusDays(5).format(DateTimeFormatter.ISO_LOCAL_DATE) - ] - - when: - loginAdmin() - POST("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys", apiKeyJson, MAP_ARG, true) - - then: - verifyResponse CREATED, response - String apiKey = responseBody().apiKey - - when: - Map subscriptionJson = [ - url : "http://localhost:$serverPort/".toString(), - apiKey : apiKey, - label : 'Functional Test Label', - description : 'Functional Test Description', - refreshPeriod: 7 + url : subscribedCatalogueUrl, + apiKey : apiKey, + label : 'Functional Test Label', + subscribedCatalogueType: subscribedCatalogueType.label, + description : 'Functional Test Description', + refreshPeriod : 7 ] POST('', subscriptionJson) @@ -1009,23 +808,24 @@ class SubscribedCatalogueFunctionalSpec extends FunctionalSpec { verifyJsonResponse OK, null cleanup: - DELETE("catalogueUsers/${getUserByEmailAddress(ADMIN).id}/apiKeys/${apiKey}", MAP_ARG, true) + DELETE("catalogueUsers/${getUserByEmailAddress(StandardEmailAddress.ADMIN).id}/apiKeys/${apiKey}", MAP_ARG, true) verifyResponse NO_CONTENT, response removeValidIdObject(subscribedCatalogueId) cleanUpRoles(subscribedCatalogueId) } - void 'A11 : Test the testConnection action (without API key)'() { + void 'A06 : Test the testConnection action (without API key)'() { given: loginAdmin() when: Map subscriptionJson = [ - url : "http://localhost:$serverPort/".toString(), - apiKey : '', - label : 'Functional Test Label', - description : 'Functional Test Description', - refreshPeriod: 7 + url : subscribedCatalogueUrl, + apiKey : '', + label : 'Functional Test Label', + subscribedCatalogueType: subscribedCatalogueType.label, + description : 'Functional Test Description', + refreshPeriod : 7 ] POST('', subscriptionJson) @@ -1050,25 +850,7 @@ class SubscribedCatalogueFunctionalSpec extends FunctionalSpec { cleanUpRoles(subscribedCatalogueId) } - private void verifyJsonPublishedModel(Map publishedModel, String modelType, String modelEndpoint, Map exporters, boolean newerVersion = false) { - assert publishedModel - assert publishedModel.modelId ==~ /\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/ - assert publishedModel.label - assert Version.from(publishedModel.version) - assert publishedModel.title == publishedModel.label + ' ' + publishedModel.version - assert publishedModel.modelType == modelType - assert OffsetDateTime.parse(publishedModel.datePublished, DateTimeFormatter.ISO_OFFSET_DATE_TIME) - assert OffsetDateTime.parse(publishedModel.lastUpdated, DateTimeFormatter.ISO_OFFSET_DATE_TIME) - assert OffsetDateTime.parse(publishedModel.dateCreated, DateTimeFormatter.ISO_OFFSET_DATE_TIME) - assert publishedModel.links.each {link -> - assert link.contentType - String exporterUrl = exporters.get(link.contentType) - assert link.url ==~ /http:\/\/localhost:$serverPort\/api\/$modelEndpoint\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/export\\/$exporterUrl/ - } - if (newerVersion) assert publishedModel.previousModelId ==~ /\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/ - } - - private void verifyBaseJsonResponse(Map responseBody, boolean expectEntries) { + protected void verifyBaseJsonResponse(Map responseBody, boolean expectEntries) { if (expectEntries) { assert responseBody.items.size() > 0 assert responseBody.items.size() == responseBody.count @@ -1078,7 +860,7 @@ class SubscribedCatalogueFunctionalSpec extends FunctionalSpec { } } - private void verifyBaseNewerVersionsJsonResponse(Map responseBody, boolean expectEntries) { + protected void verifyBaseNewerVersionsJsonResponse(Map responseBody, boolean expectEntries) { assert OffsetDateTime.parse(responseBody.lastUpdated, DateTimeFormatter.ISO_OFFSET_DATE_TIME) if (expectEntries) { @@ -1088,17 +870,17 @@ class SubscribedCatalogueFunctionalSpec extends FunctionalSpec { } } - private static Map getDataModelExporters() { + protected static Map getDataModelExporters() { [ - 'application/mdm+json': 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/3.1', - 'application/mdm+xml' : 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelXmlExporterService/5.1' + 'application/mauro.datamodel+json': 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/3.1', + 'application/mauro.datamodel+xml' : 'uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelXmlExporterService/5.1' ] } - private static Map getCodeSetExporters() { + protected static Map getCodeSetExporters() { [ - 'application/mdm+json': 'uk.ac.ox.softeng.maurodatamapper.terminology.provider.exporter/CodeSetJsonExporterService/4.0', - 'application/mdm+xml' : 'uk.ac.ox.softeng.maurodatamapper.terminology.provider.exporter/CodeSetXmlExporterService/5.0' + 'application/mauro.codeset+json': 'uk.ac.ox.softeng.maurodatamapper.terminology.provider.exporter/CodeSetJsonExporterService/4.0', + 'application/mauro.codeset+xml' : 'uk.ac.ox.softeng.maurodatamapper.terminology.provider.exporter/CodeSetXmlExporterService/5.0' ] } }