diff --git a/mdm-core/README.md b/mdm-core/README.md index 95c76d23ab..3f629f4dee 100644 --- a/mdm-core/README.md +++ b/mdm-core/README.md @@ -72,6 +72,7 @@ Controller: folder | PUT | /api/folders/${id} | Action: update | GET | /api/folders/${id} | Action: show | GET | /api/folders/${id}/export | Action: export + | POST | /api/folders/${id}/import | Action: import Controller: importer | GET | /api/importer/parameters/${ns}?/${name}?/${version}? | Action: parameters diff --git a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/UrlMappings.groovy b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/UrlMappings.groovy index 0212cb4229..2d97473bd8 100644 --- a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/UrlMappings.groovy +++ b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/UrlMappings.groovy @@ -87,6 +87,10 @@ class UrlMappings { get "/export/$exporterNamespace/$exporterName/$exporterVersion"(controller: 'folder', action: 'exportFolder') } + group '/folders', { + post "/import/$importerNamespace/$importerName/$importerVersion"(controller: 'folder', action: 'importFolder') + } + '/versionedFolders'(resources: 'versionedFolder', excludes: DEFAULT_EXCLUDES) { '/folders'(resources: 'folder', excludes: DEFAULT_EXCLUDES) diff --git a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/container/FolderController.groovy b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/container/FolderController.groovy index c5b185f55c..65d4dc45ff 100644 --- a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/container/FolderController.groovy +++ b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/container/FolderController.groovy @@ -17,10 +17,15 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.container +import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiException +import uk.ac.ox.softeng.maurodatamapper.core.container.provider.importer.FolderImporterProviderService +import uk.ac.ox.softeng.maurodatamapper.core.container.provider.importer.parameter.FolderImporterProviderServiceParameters import uk.ac.ox.softeng.maurodatamapper.core.controller.EditLoggingController +import uk.ac.ox.softeng.maurodatamapper.core.importer.ImporterService import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.provider.MauroDataMapperServiceProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.exporter.ExporterProviderService +import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.ImporterProviderService import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.search.SearchParams import uk.ac.ox.softeng.maurodatamapper.core.search.SearchService import uk.ac.ox.softeng.maurodatamapper.hibernate.search.PaginatedHibernateSearchResult @@ -28,8 +33,11 @@ import uk.ac.ox.softeng.maurodatamapper.security.SecurityPolicyManagerService import grails.gorm.transactions.Transactional import grails.web.http.HttpHeaders +import grails.web.mime.MimeType import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired +import org.springframework.validation.Errors +import org.springframework.web.multipart.support.AbstractMultipartHttpServletRequest import static org.springframework.http.HttpStatus.CREATED import static org.springframework.http.HttpStatus.NO_CONTENT @@ -41,6 +49,7 @@ class FolderController extends EditLoggingController { static responseFormats = ['json', 'xml'] MauroDataMapperServiceProviderService mauroDataMapperServiceProviderService + ImporterService importerService FolderService folderService SearchService mdmCoreSearchService VersionedFolderService versionedFolderService @@ -185,11 +194,54 @@ class FolderController extends EditLoggingController { log.info("Exporting Folder using ${exporter.displayName}") ByteArrayOutputStream outputStream = exporter.exportDomain(currentUser, params.folderId) if (!outputStream) return errorResponse(UNPROCESSABLE_ENTITY, 'Folder could not be exported') - log.info('Export complete') + log.info('Single Folder Export complete') render(file: outputStream.toByteArray(), fileName: "${instance.label}.${exporter.fileExtension}", contentType: exporter.fileType) } + @Transactional + def importFolder() throws ApiException { + FolderImporterProviderService importer = + mauroDataMapperServiceProviderService.findImporterProvider(params.importerNamespace, params.importerName, params.importerVersion) as FolderImporterProviderService + if (!importer) return notFound(ImporterProviderService, "${params.importerNamespace}:${params.importerName}:${params.importerVersion}") + + FolderImporterProviderServiceParameters importerProviderServiceParameters = request.contentType.startsWith(MimeType.MULTIPART_FORM.name) + ? importerService.extractImporterProviderServiceParameters(importer, request as AbstractMultipartHttpServletRequest) + : importerService.extractImporterProviderServiceParameters(importer, request) + + Errors errors = importerService.validateParameters(importerProviderServiceParameters, importer.importerProviderServiceParametersClass) + if (errors.hasErrors()) { + transactionStatus.setRollbackOnly() + return respond(errors) + } + + if (!currentUserSecurityPolicyManager.userCanCreateResourceId(resource, null, Folder, importerProviderServiceParameters.parentFolderId)) { + if (!currentUserSecurityPolicyManager.userCanReadSecuredResourceId(Folder, importerProviderServiceParameters.parentFolderId)) { + return notFound(Folder, importerProviderServiceParameters.parentFolderId) + } + return forbiddenDueToPermissions() + } + + Folder folder = importer.importDomain(currentUser, importerProviderServiceParameters) as Folder + if (!folder) { + transactionStatus.setRollbackOnly() + return errorResponse(UNPROCESSABLE_ENTITY, 'No folder imported') + } + + folder.parentFolder = folderService.get(importerProviderServiceParameters.parentFolderId) + folderService.validate(folder) + if (folder.hasErrors()) { + transactionStatus.setRollbackOnly() + return respond(folder.errors) + } + + log.debug('No errors in imported folder') + log.info('Single Folder Import complete') + + saveResource(folder) + saveResponse(folder) + } + @Override protected Folder queryForResource(Serializable id) { folderService.get(id) @@ -239,7 +291,6 @@ class FolderController extends EditLoggingController { namespace: hasProperty('namespace') ? this.namespace : null)) respond instance, [status: OK, view: 'update', model: [userSecurityPolicyManager: currentUserSecurityPolicyManager, folder: instance]] } - } } diff --git a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/container/FolderInterceptor.groovy b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/container/FolderInterceptor.groovy index 1613bcb0c4..53191dfda2 100644 --- a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/container/FolderInterceptor.groovy +++ b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/container/FolderInterceptor.groovy @@ -72,6 +72,13 @@ class FolderInterceptor extends SecurableResourceInterceptor { return true } + if (actionName == 'importFolder') { + if (!currentUserSecurityPolicyManager.userCanEditSecuredResourceId(Folder, id)) { + return forbiddenOrNotFound(false, Folder, id) + } + return true + } + checkActionAuthorisationOnSecuredResource(Folder, getId(), true) } } diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/FolderService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/FolderService.groovy index dd39d408de..d076c74959 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/FolderService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/FolderService.groovy @@ -362,7 +362,6 @@ class FolderService extends ContainerService { throw new ApiNotYetImplementedException('MSXX', 'Folder permission copying') } log.warn('Permission copying is not yet implemented') - } log.debug('Validating and saving copy') setFolderRefinesFolder(copiedFolder, original, copier) @@ -645,4 +644,13 @@ class FolderService extends ContainerService { if (parentCache) parentCache.addDiffCache(folder.path, fDiffCache) fDiffCache } + + void checkImportedFolderAssociations(User importingUser, Folder folder) { + folder.checkPath() + folder.createdBy = importingUser.emailAddress + folder.validate() + checkFacetsAfterImportingMultiFacetAware(folder) + folder.childFolders?.each { checkImportedFolderAssociations(importingUser, it) } + log.debug('Folder associations checked') + } } diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/FolderJsonImporterService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/FolderJsonImporterService.groovy new file mode 100644 index 0000000000..dd9a3fcb76 --- /dev/null +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/FolderJsonImporterService.groovy @@ -0,0 +1,60 @@ +/* + * Copyright 2020 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.container.provider.importer + +import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException +import uk.ac.ox.softeng.maurodatamapper.core.container.Folder +import uk.ac.ox.softeng.maurodatamapper.core.container.provider.importer.parameter.FolderFileImporterProviderServiceParameters +import uk.ac.ox.softeng.maurodatamapper.core.traits.provider.importer.JsonImportMapping +import uk.ac.ox.softeng.maurodatamapper.security.User + +import groovy.util.logging.Slf4j + +@Slf4j +class FolderJsonImporterService extends DataBindFolderImporterProviderService implements JsonImportMapping { + + @Override + String getDisplayName() { + 'JSON Folder Importer' + } + + @Override + String getVersion() { + '1.0' + } + + @Override + Boolean canImportMultipleDomains() { + false + } + + @Override + Folder importFolder(User currentUser, byte[] content) { + log.debug('Parsing in file content using JsonSlurper') + Map folder = slurpAndClean(content).folder + if (!folder) throw new ApiBadRequestException('JIS03', 'Cannot import JSON as folder is not present') + + log.debug('Importing Folder map') + bindMapToFolder(currentUser, new HashMap(folder)) + } + + @Override + List importFolders(User currentUser, byte[] content) { + throw new ApiBadRequestException('FBIP04', "${name} cannot import multiple Folders") + } +} diff --git a/mdm-core/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/FolderFunctionalSpec.groovy b/mdm-core/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/FolderFunctionalSpec.groovy index d54d472149..01d2bef427 100644 --- a/mdm-core/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/FolderFunctionalSpec.groovy +++ b/mdm-core/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/FolderFunctionalSpec.groovy @@ -23,9 +23,13 @@ import uk.ac.ox.softeng.maurodatamapper.test.functional.ResourceFunctionalSpec import grails.gorm.transactions.Rollback import grails.testing.mixin.integration.Integration import grails.testing.spock.RunOnce +import grails.web.mime.MimeType import groovy.util.logging.Slf4j import io.micronaut.core.type.Argument import io.micronaut.http.HttpStatus +import spock.lang.Shared + +import static uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress.getFUNCTIONAL_TEST /** *
@@ -42,6 +46,7 @@ import io.micronaut.http.HttpStatus
  *  |   POST   | /api/folders/${folderId}/search   | Action: search
  *
  *  |   GET    | /api/folders/${folderId}/export    | Action: export
+ *  |   POST   | /api/folders/${folderId}/import    | Action: import
  * 
* @see uk.ac.ox.softeng.maurodatamapper.core.container.FolderController */ @@ -49,10 +54,17 @@ import io.micronaut.http.HttpStatus @Slf4j class FolderFunctionalSpec extends ResourceFunctionalSpec { + @Shared + UUID parentFolderId + @RunOnce @Rollback def setup() { - assert Folder.count() == 0 + log.debug('Check and setup test data') + sessionFactory.currentSession.flush() + parentFolderId = new Folder(label: 'Parent Functional Test Folder', createdBy: FUNCTIONAL_TEST).save(flush: true).id + assert parentFolderId + assert Folder.count() == 1 } @Override @@ -94,6 +106,14 @@ class FolderFunctionalSpec extends ResourceFunctionalSpec { }''' } + @Override + void verifyR1EmptyIndexResponse() { + verifyResponse(HttpStatus.OK, response) + assert response.body().count == 1 + assert response.body().items.size() == 1 + assert response.body().items[0].label == 'Parent Functional Test Folder' + } + void 'Test the save action fails when using the same label persists an instance'() { given: List createdIds = [] @@ -544,4 +564,120 @@ class FolderFunctionalSpec extends ResourceFunctionalSpec { cleanup: ids.reverseEach { cleanUpData(it) } } + + void 'FE06 : test import Folder JSON'() { + when: 'The save action is executed with valid data' + POST('', validJson) + + then: 'The response is correct' + response.status == HttpStatus.CREATED + response.body().id + + when: 'The export action is executed with a valid instance' + String id = response.body().id + GET("${id}/export/uk.ac.ox.softeng.maurodatamapper.core.container.provider.exporter/FolderJsonExporterService/1.0", STRING_ARG) + + then: 'The response is correct' + verifyResponse(HttpStatus.OK, jsonCapableResponse) + + when: 'The delete action is executed with a valid instance' + String exportedJson = jsonCapableResponse.body() + DELETE(getDeleteEndpoint(id)) + + then: 'The response is correct' + response.status == HttpStatus.NO_CONTENT + + when: 'The import action is executed with valid data' + POST('import/uk.ac.ox.softeng.maurodatamapper.core.container.provider.importer/FolderJsonImporterService/1.0', [ + parentFolderId: parentFolderId.toString(), + importFile : [ + fileName : 'FT Import', + fileType : MimeType.JSON_API.name, + fileContents: exportedJson.bytes.toList() + ] + ], STRING_ARG) + + then: 'The response is correct' + verifyJsonResponse(HttpStatus.CREATED, '''{ + "id": "${json-unit.matches:id}", + "label": "Functional Test Folder", + "lastUpdated": "${json-unit.matches:offsetDateTime}", + "domainType": "Folder", + "hasChildFolders": false, + "readableByEveryone": false, + "readableByAuthenticatedUsers": false, + "availableActions": [ + "delete", + "show", + "update" + ] +}''') + } + + void 'FE07 : test import Folder with child Folders JSON'() { + given: + List responseStatuses = [] + List ids = [] + Closure saveResponse = { -> + responseStatuses << response.status + ids << response.body()?.id + } + + when: 'The save actions are executed with valid data' + POST('', validJson) + saveResponse() + POST("${ids[0]}/folders", [label: 'Functional Test Folder 2']) + saveResponse() + POST("${ids[0]}/folders", [label: 'Functional Test Folder 3']) + saveResponse() + POST("${ids[2]}/folders", [label: 'Functional Test Folder 4']) + saveResponse() + + then: 'The responses are correct' + responseStatuses.every { it == HttpStatus.CREATED } + ids.every() + + when: 'The export action is executed with a valid instance' + GET("${ids[0]}/export/uk.ac.ox.softeng.maurodatamapper.core.container.provider.exporter/FolderJsonExporterService/1.0", STRING_ARG) + + then: 'The response is correct' + verifyResponse(HttpStatus.OK, jsonCapableResponse) + + when: 'The delete action is executed with a valid instance' + String exportedJson = jsonCapableResponse.body() + responseStatuses.clear() + ids.reverseEach { + DELETE(getDeleteEndpoint(it)) + responseStatuses << response.status + } + + then: 'The response is correct' + responseStatuses.every { it == HttpStatus.NO_CONTENT } + + when: 'The import action is executed with valid data' + POST('import/uk.ac.ox.softeng.maurodatamapper.core.container.provider.importer/FolderJsonImporterService/1.0', [ + parentFolderId: parentFolderId.toString(), + importFile : [ + fileName : 'FT Import', + fileType : MimeType.JSON_API.name, + fileContents: exportedJson.bytes.toList() + ] + ], STRING_ARG) + + then: 'The response is correct' + verifyJsonResponse(HttpStatus.CREATED, '''{ + "id": "${json-unit.matches:id}", + "label": "Functional Test Folder", + "lastUpdated": "${json-unit.matches:offsetDateTime}", + "domainType": "Folder", + "hasChildFolders": true, + "readableByEveryone": false, + "readableByAuthenticatedUsers": false, + "availableActions": [ + "delete", + "show", + "update" + ] +}''') + } } diff --git a/mdm-core/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/FolderJsonImporterServiceSpec.groovy b/mdm-core/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/FolderJsonImporterServiceSpec.groovy new file mode 100644 index 0000000000..c10b4e47b2 --- /dev/null +++ b/mdm-core/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/FolderJsonImporterServiceSpec.groovy @@ -0,0 +1,167 @@ +/* + * Copyright 2020 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.container.provider.importer + +import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException +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.container.test.provider.BaseFolderImporterServiceSpec +import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation +import uk.ac.ox.softeng.maurodatamapper.core.facet.Rule + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Integration +@Rollback +class FolderJsonImporterServiceSpec extends BaseFolderImporterServiceSpec { + + FolderJsonImporterService folderJsonImporterService + + @Override + FolderImporterProviderService getImporterService() { + folderJsonImporterService + } + + @Override + String getImportType() { + 'json' + } + + // TODO: FI01 : Test import null Folder + + void 'FI02 : test import invalid Folder'() { + when: 'given empty content' + importFolder(''.bytes) + + then: + ApiBadRequestException exception = thrown(ApiBadRequestException) + exception.errorCode == CANNOT_IMPORT_EMPTY_FILE_CODE + + when: 'given an empty JSON map' + importFolder('{}'.bytes) + + then: + exception = thrown(ApiBadRequestException) + exception.errorCode == CANNOT_IMPORT_JSON_CODE + } + + void 'FI03 : test import Folder'() { + when: + Folder folder = importFolder('emptyFolder') + + then: + folder.tap { + id + label == 'Test Folder' + lastUpdated + domainType == 'Folder' + } + } + + void 'FI04 : test import empty Folder'() { + when: + Folder folder = importFolder('emptyFolder') + + then: + !folder.childFolders + !folderService.findAllModelsInFolder(folder) + } + + void 'FI05 : test import Folder with description'() { + expect: + importFolder('folderIncDescription').description == 'Test Folder description' + } + + void 'FI06 : test import Folder with metadata'() { + when: + Folder folder = importFolder('folderIncMetadata') + + then: + folder.metadata.size() == 3 + folder.metadata.tap { + find { it.namespace == 'test.com' && it.key == 'mdk1' && it.value == 'mdv1' } + find { it.namespace == 'test.com/simple' && it.key == 'mdk1' && it.value == 'mdv1' } + find { it.namespace == 'test.com/simple' && it.key == 'mdk2' && it.value == 'mdv2' } + } + } + + void 'FI07 : test import Folder with annotations'() { + when: + Folder folder = importFolder('folderIncAnnotations') + + then: + folder.annotations.size() == 2 + folder.annotations.eachWithIndex { Annotation it, int i -> + it.createdBy == StandardEmailAddress.INTEGRATION_TEST + it.label == "Test Annotation ${i}" + it.description == "Test Annotation ${i} description" + } + } + + void 'FI08 : test import Folder with rules'() { + when: + Folder folder = importFolder('folderIncRules') + + then: + folder.rules.size() == 2 + folder.rules.eachWithIndex { Rule it, int i -> + it.createdBy == StandardEmailAddress.INTEGRATION_TEST + it.name == "Test Rule ${i}" + it.description == "Test Rule ${i} description" + } + } + + void 'FI09 : test import Folder with child Folders'() { + when: + Folder folder = importFolder('folderIncEmptyChildFolder') + + then: + folder.childFolders.size() == 1 + folder.childFolders.find { it.label == 'Empty Child Folder' && !it.childFolders } + + when: + folder = importFolder('folderIncChildFolders') + + then: + folder.childFolders.size() == 2 + folder.childFolders.find { it.label == 'Empty Child Folder' && !it.childFolders } + folder.childFolders.find { it.label == 'Child Folder with Facets and Own Child Folder' }.tap { + metadata.size() == 3 + metadata.tap { + find { it.namespace == 'test.com' && it.key == 'mdk1' && it.value == 'mdv1' } + find { it.namespace == 'test.com/simple' && it.key == 'mdk1' && it.value == 'mdv1' } + find { it.namespace == 'test.com/simple' && it.key == 'mdk2' && it.value == 'mdv2' } + } + annotations.size() == 2 + annotations.eachWithIndex { Annotation annotation, int i -> + annotation.createdBy == StandardEmailAddress.INTEGRATION_TEST + annotation.label == "Test Annotation ${i}" + annotation.description == "Test Annotation ${i} description" + } + rules.size() == 2 + rules.eachWithIndex { Rule rule, int i -> + rule.createdBy == StandardEmailAddress.INTEGRATION_TEST + rule.name == "Test Rule ${i}" + rule.description == "Test Rule ${i} description" + } + childFolders.size() == 1 + childFolders.find { it.label == 'Inner Child Folder' && !it.childFolders } + } + } +} diff --git a/mdm-core/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/test/provider/BaseFolderImporterServiceSpec.groovy b/mdm-core/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/test/provider/BaseFolderImporterServiceSpec.groovy new file mode 100644 index 0000000000..f3794c94da --- /dev/null +++ b/mdm-core/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/test/provider/BaseFolderImporterServiceSpec.groovy @@ -0,0 +1,98 @@ +/* + * Copyright 2020 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.container.test.provider + +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.provider.importer.FolderImporterProviderService +import uk.ac.ox.softeng.maurodatamapper.core.container.provider.importer.parameter.FolderFileImporterProviderServiceParameters +import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.FileParameter +import uk.ac.ox.softeng.maurodatamapper.test.integration.BaseIntegrationSpec + +import grails.testing.spock.RunOnce +import grails.util.BuildSettings +import groovy.util.logging.Slf4j +import org.springframework.beans.factory.annotation.Autowired +import spock.lang.Shared + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + +@Slf4j +abstract class BaseFolderImporterServiceSpec extends BaseIntegrationSpec { + + static final String CANNOT_IMPORT_EMPTY_FILE_CODE = 'FBIP02' + static final String CANNOT_IMPORT_JSON_CODE = 'JIS03' + + @Autowired + FolderService folderService + + @Shared + Path resourcesPath + + @Shared + FolderFileImporterProviderServiceParameters basicParameters + + abstract FolderImporterProviderService getImporterService() + + abstract String getImportType() + + Folder getTestFolder() { + folder + } + + @Override + def setupSpec() { + basicParameters = new FolderFileImporterProviderServiceParameters() + } + + @RunOnce + @Override + def setup() { + resourcesPath = Paths.get(BuildSettings.BASE_DIR.absolutePath, 'src', 'integration-test', 'resources', importType) + basicParameters.importFile = null + } + + @Override + void setupDomainData() { + } + + byte[] loadTestFile(String filename) { + Path testFilePath = resourcesPath.resolve("${filename}.${importType}").toAbsolutePath() + assert Files.exists(testFilePath) + Files.readAllBytes(testFilePath) + } + + Folder importFolder(byte[] bytes) { + log.trace('Importing:\n {}', new String(bytes)) + basicParameters.importFile = new FileParameter(fileContents: bytes) + + Folder imported = importerService.importDomain(admin, basicParameters) as Folder + imported.parentFolder = testFolder + check(imported) + folderService.save(imported) + // sessionFactory.currentSession.flush() + log.debug('Folder saved') + folderService.get(imported.id) + } + + Folder importFolder(String filename) { + importFolder(loadTestFile(filename)) + } +} diff --git a/mdm-core/src/integration-test/resources/url-mappings/tracked_endpoints.txt b/mdm-core/src/integration-test/resources/url-mappings/tracked_endpoints.txt index 102242adea..2fd03128b0 100644 --- a/mdm-core/src/integration-test/resources/url-mappings/tracked_endpoints.txt +++ b/mdm-core/src/integration-test/resources/url-mappings/tracked_endpoints.txt @@ -68,6 +68,7 @@ | PUT | /api/folders/${folderId}/folders/${id} | update | | GET | /api/folders/${folderId}/folders/${id} | show | | GET | /api/folders/${folderId}/export/${exporterNamespace}/${exporterName}/${exporterVersion} | exportFolder | +| GET | /api/folders/${folderId}/import/${exporterNamespace}/${exporterName}/${exporterVersion} | importFolder | | PUT | /api/folders/${folderId}/folder/${destinationFolderId} | changeFolder | | POST | /api/folders | save | | GET | /api/folders | index | diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/ContainerImporterProviderService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/ContainerImporterProviderService.groovy new file mode 100644 index 0000000000..c2137fc7b3 --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/ContainerImporterProviderService.groovy @@ -0,0 +1,31 @@ +/* + * Copyright 2020 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.container.provider.importer + +import uk.ac.ox.softeng.maurodatamapper.core.model.Container +import uk.ac.ox.softeng.maurodatamapper.core.model.ContainerService +import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.ImporterProviderService +import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.ImporterProviderServiceParameters + +import groovy.transform.CompileStatic + +@CompileStatic +abstract class ContainerImporterProviderService extends ImporterProviderService { + + abstract ContainerService getContainerService() +} diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/DataBindFolderImporterProviderService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/DataBindFolderImporterProviderService.groovy new file mode 100644 index 0000000000..edceba22dd --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/DataBindFolderImporterProviderService.groovy @@ -0,0 +1,75 @@ +/* + * Copyright 2020 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.container.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.core.container.Folder +import uk.ac.ox.softeng.maurodatamapper.core.container.provider.importer.parameter.FolderFileImporterProviderServiceParameters +import uk.ac.ox.softeng.maurodatamapper.security.User + +import grails.web.databinding.DataBindingUtils +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.springframework.core.GenericTypeResolver + +@Slf4j +@CompileStatic +abstract class DataBindFolderImporterProviderService

extends FolderImporterProviderService

{ + + abstract Folder importFolder(User currentUser, byte[] content) + + abstract List importFolders(User currentUser, byte[] content) + + @Override + Class

getImporterProviderServiceParametersClass() { + (Class

) GenericTypeResolver.resolveTypeArgument(getClass(), DataBindFolderImporterProviderService) + } + + @Override + Folder importDomain(User currentUser, FolderFileImporterProviderServiceParameters params) { + checkImportParams(currentUser, params) + importFolder(currentUser, params.importFile.fileContents) + } + + @Override + List importDomains(User currentUser, FolderFileImporterProviderServiceParameters params) { + checkImportParams(currentUser, params) + importFolders(currentUser, params.importFile.fileContents) + } + + Folder bindMapToFolder(User currentUser, Map folderMap) { + if (!folderMap) throw new ApiBadRequestException('FBIP03', 'No FolderMap supplied to import') + + Folder folder = new Folder() + log.debug('Binding map to new Folder instance') + DataBindingUtils.bindObjectToInstance(folder, folderMap, null, importBlacklistedProperties, null) + + log.debug('Fixing bound Folder') + folderService.checkImportedFolderAssociations(currentUser, folder) + + log.debug('Binding complete') + folder + } + + private void checkImportParams(User currentUser, FolderFileImporterProviderServiceParameters params) { + if (!currentUser) throw new ApiUnauthorizedException('FBIP01', 'User must be logged in to import folder') + if (params.importFile.fileContents.size() == 0) throw new ApiBadRequestException('FBIP02', 'Cannot import empty file') + log.info("Importing ${params.importFile.fileName} as ${currentUser.emailAddress}") + } +} diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/FolderImporterProviderService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/FolderImporterProviderService.groovy new file mode 100644 index 0000000000..824151a17e --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/FolderImporterProviderService.groovy @@ -0,0 +1,43 @@ +/* + * Copyright 2020 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.container.provider.importer + +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.provider.importer.parameter.FolderImporterProviderServiceParameters +import uk.ac.ox.softeng.maurodatamapper.core.provider.ProviderType + +import groovy.transform.CompileStatic +import org.springframework.beans.factory.annotation.Autowired + +@CompileStatic +abstract class FolderImporterProviderService

extends ContainerImporterProviderService { + + @Autowired + FolderService folderService + + @Override + FolderService getContainerService() { + folderService + } + + @Override + String getProviderType() { + "Folder${ProviderType.IMPORTER.name}" + } +} diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/parameter/FolderFileImporterProviderServiceParameters.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/parameter/FolderFileImporterProviderServiceParameters.groovy new file mode 100644 index 0000000000..79293359d9 --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/parameter/FolderFileImporterProviderServiceParameters.groovy @@ -0,0 +1,39 @@ +/* + * Copyright 2020 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.container.provider.importer.parameter + +import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.FileParameter +import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.config.ImportGroupConfig +import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.config.ImportParameterConfig + +import groovy.transform.CompileStatic + +@CompileStatic +class FolderFileImporterProviderServiceParameters extends FolderImporterProviderServiceParameters { + + @ImportParameterConfig( + displayName = 'File', + description = 'The file containing the data to be imported', + order = -1, + group = @ImportGroupConfig( + name = 'Source', + order = -1 + ) + ) + FileParameter importFile +} diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/parameter/FolderImporterProviderServiceParameters.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/parameter/FolderImporterProviderServiceParameters.groovy new file mode 100644 index 0000000000..92e643b59c --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/provider/importer/parameter/FolderImporterProviderServiceParameters.groovy @@ -0,0 +1,37 @@ +/* + * Copyright 2020 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.container.provider.importer.parameter + +import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.ImporterProviderServiceParameters +import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.config.ImportGroupConfig +import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.config.ImportParameterConfig + +import groovy.transform.CompileStatic + +@CompileStatic +class FolderImporterProviderServiceParameters implements ImporterProviderServiceParameters { + @ImportParameterConfig( + displayName = 'Parent Folder', + description = 'The parent Folder into which the Folder should be imported.', + order = 0, + group = @ImportGroupConfig( + name = 'Folder', + order = 0 + )) + UUID parentFolderId +} 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..23e7a496db 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 @@ -18,18 +18,17 @@ package uk.ac.ox.softeng.maurodatamapper.core.provider.importer import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException -import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem 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 - extends MauroDataMapperService { +abstract class ImporterProviderService extends MauroDataMapperService { abstract D importDomain(User currentUser, T params) diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/MultiFacetAwareService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/MultiFacetAwareService.groovy index 916dea05f8..f449c5c547 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/MultiFacetAwareService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/MultiFacetAwareService.groovy @@ -128,18 +128,21 @@ trait MultiFacetAwareService { if (multiFacetAware.metadata) { multiFacetAware.metadata.each { it.multiFacetAwareItemId = multiFacetAware.id + it.multiFacetAwareItemDomainType = it.domainType it.createdBy = it.createdBy ?: multiFacetAware.createdBy } } if (multiFacetAware.rules) { multiFacetAware.rules.each { it.multiFacetAwareItemId = multiFacetAware.id + it.multiFacetAwareItemDomainType = it.domainType it.createdBy = it.createdBy ?: multiFacetAware.createdBy } } if (multiFacetAware.annotations) { multiFacetAware.annotations.each { it.multiFacetAwareItemId = multiFacetAware.id + it.multiFacetAwareItemDomainType = it.domainType it.createdBy = it.createdBy ?: multiFacetAware.createdBy } }