From 7eb003ce7924c08ad7b3452017e5c0a96b7b1ab8 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Thu, 1 Jul 2021 19:00:29 +0100 Subject: [PATCH 01/82] Next snapshot --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3f1b87a295..37111d2433 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Core Info -version=4.7.0 +version=4.8.0-SNAPSHOT group=uk.ac.ox.softeng.maurodatamapper # Gradle gradleVersion=6.7.1 From c154213aee66ed135e4834e7f39f096beafac930 Mon Sep 17 00:00:00 2001 From: Anthony Williams Date: Thu, 17 Jun 2021 16:49:33 +0100 Subject: [PATCH 02/82] add rules to existing merge functionality plus tests in data element spec --- .../maurodatamapper/core/facet/Rule.groovy | 4 +- .../core/facet/rule/RuleRepresentation.groovy | 3 +- .../core/model/CatalogueItem.groovy | 3 + .../datamodel/item/DataElementSpec.groovy | 55 ++++++++++++++++++- 4 files changed, 61 insertions(+), 4 deletions(-) diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy index 6fef14a9e4..357df716b0 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy @@ -36,6 +36,7 @@ class Rule implements MultiFacetItemAware, CreatorAware, Diffable { String name String description + Set ruleRepresentations static hasMany = [ ruleRepresentations: RuleRepresentation @@ -92,11 +93,12 @@ class Rule implements MultiFacetItemAware, CreatorAware, Diffable { .rightHandSide(obj.id.toString(), obj) .appendString('name', this.name, obj.name) .appendString('description', this.description, obj.description) + .appendList(RuleRepresentation, 'ruleRepresentations', this.ruleRepresentations, obj.ruleRepresentations) } @Override String getDiffIdentifier() { - "${this.name}.${this.description}" + "${this.name}" } static DetachedCriteria by() { diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy index fc33931db4..dbf47bf751 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy @@ -87,12 +87,11 @@ class RuleRepresentation implements Diffable, EditHistoryAwa .leftHandSide(id.toString(), this) .rightHandSide(obj.id.toString(), obj) .appendString('language', this.language, obj.language) - .appendString('representation', this.representation, obj.representation) } @Override String getDiffIdentifier() { - "${this.language}.${this.representation}" + "${this.language}" } static DetachedCriteria by() { diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy index b939581b46..055f1693dc 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy @@ -23,6 +23,8 @@ import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTree import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata +import uk.ac.ox.softeng.maurodatamapper.core.facet.Rule +import uk.ac.ox.softeng.maurodatamapper.core.facet.rule.RuleRepresentation import uk.ac.ox.softeng.maurodatamapper.core.model.container.CatalogueItemClassifierAware import uk.ac.ox.softeng.maurodatamapper.core.model.facet.MultiFacetAware import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.EditHistoryAware @@ -101,6 +103,7 @@ trait CatalogueItem implements InformationAware, EditHistory .appendString('description', lhs.description, rhs.description) .appendList(Metadata, 'metadata', lhs.metadata, rhs.metadata) .appendList(Annotation, 'annotations', lhs.annotations, rhs.annotations) + .appendList(Rule, 'rule', lhs.rules, rhs.rules) } static DetachedCriteria withCatalogueItemFilter(DetachedCriteria criteria, Map filters) { diff --git a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementSpec.groovy b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementSpec.groovy index 0a8d300d49..abff35a1fc 100644 --- a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementSpec.groovy +++ b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementSpec.groovy @@ -17,7 +17,8 @@ */ package uk.ac.ox.softeng.maurodatamapper.datamodel.item - +import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff +import uk.ac.ox.softeng.maurodatamapper.core.facet.Rule import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.DataType @@ -144,4 +145,56 @@ class DataElementSpec extends ModelItemSpec implements DomainUnitTe checkAndSave(dataClass) checkAndSave(other) } + + void 'DER01: test diffing DE rules with identical rules'() { + DataElement a = new DataElement(label: 'Functional Data Element', dataType: dataType).addToRules(name: 'rule 1') + .addToRules(new Rule(name: 'rule 2').addToRuleRepresentations(language: 'd', representation: 'a+b')) + DataElement b = new DataElement(label: 'Functional Data Element', dataType: dataType).addToRules(name: 'rule 1') + .addToRules(new Rule(name: 'rule 2').addToRuleRepresentations(language: 'd', representation: 'a+b')) + + when: + ObjectDiff diff = a.diff(b) + + then: + diff.objectsAreIdentical() + } + + void 'DER02: test diffing DE rules with different rule names'() { + DataElement a = new DataElement(label: 'Functional Data Element', dataType: dataType).addToRules(name: 'rule 1') + .addToRules(new Rule(name: 'rule 2').addToRuleRepresentations(language: 'd', representation: 'a+b')) + DataElement b = new DataElement(label: 'Functional Data Element', dataType: dataType).addToRules(name: 'rule 1') + .addToRules(new Rule(name: 'rule 3').addToRuleRepresentations(language: 'd', representation: 'a+b')) + + when: + ObjectDiff diff = a.diff(b) + + then: + diff.numberOfDiffs == 2 + } + + void 'DER03: test diffing DE rules with different languages rules'() { + DataElement a = new DataElement(label: 'Functional Data Element', dataType: dataType).addToRules(name: 'rule 1') + .addToRules(new Rule(name: 'rule 2').addToRuleRepresentations(language: 'd', representation: 'a+b')) + DataElement b = new DataElement(label: 'Functional Data Element', dataType: dataType).addToRules(name: 'rule 1') + .addToRules(new Rule(name: 'rule 2').addToRuleRepresentations(language: 'e', representation: 'a+b')) + + when: + ObjectDiff diff = a.diff(b) + + then: + diff.numberOfDiffs == 2 + } + + void 'DER04: test diffing DE rules with different rules representations'() { + DataElement a = new DataElement(label: 'Functional Data Element', dataType: dataType).addToRules(name: 'rule 1') + .addToRules(new Rule(name: 'rule 2').addToRuleRepresentations(language: 'd', representation: 'a+b')) + DataElement b = new DataElement(label: 'Functional Data Element', dataType: dataType).addToRules(name: 'rule 1') + .addToRules(new Rule(name: 'rule 2').addToRuleRepresentations(language: 'd', representation: 'a+e')) + + when: + ObjectDiff diff = a.diff(b) + + then: + diff.objectsAreIdentical() + } } From 9e657cbd0c25c01265bc9a216391071bbcda868e Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Tue, 29 Jun 2021 14:42:54 +0100 Subject: [PATCH 03/82] Fix the broken tests due to rules being added to diffs --- ...ImportAndDefaultExporterServiceSpec.groovy | 7 +- ...enceDataImporterExporterServiceSpec.groovy | 3 +- ...enceDataImporterExporterServiceSpec.groovy | 5 +- ...ImportAndDefaultExporterServiceSpec.groovy | 22 +- .../datamodel/DataModelFunctionalSpec.groovy | 537 +++++++++--------- .../TerminologyFunctionalSpec.groovy | 227 ++++---- run-all-tests.sh | 3 +- 7 files changed, 428 insertions(+), 376 deletions(-) diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy index ac6a30d393..7673724449 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy @@ -244,7 +244,12 @@ abstract class DataBindImportAndDefaultExporterServiceSpec> E2E Tests <<" ./gradlew --build-cache -Dgradle.test.package=referencedata :mdm-testing-functional:integrationTest ./gradlew --build-cache -Dgradle.test.package=federation :mdm-testing-functional:integrationTest echo ">> Root Test Report <<" -./gradlew --build-cache rootTestReport jacocoTestReport +./gradlew --build-cache rootTestReport +#./gradlew --build-cache jacocoTestReport #./gradlew --build-cache staticCodeAnalysis \ No newline at end of file From f8aab5411eb8cafe082b80d05eb9588a62f2ec9a Mon Sep 17 00:00:00 2001 From: Aaron Forshaw Date: Fri, 16 Jul 2021 15:53:58 +0100 Subject: [PATCH 04/82] feature/gh-108 List all DEs on a DM --- .../datamodel/UrlMappings.groovy | 2 ++ .../item/DataElementController.groovy | 9 ++++- .../item/DataElementFunctionalSpec.groovy | 34 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/mdm-plugin-datamodel/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/datamodel/UrlMappings.groovy b/mdm-plugin-datamodel/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/datamodel/UrlMappings.groovy index f28866121d..6c8cc6188b 100644 --- a/mdm-plugin-datamodel/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/datamodel/UrlMappings.groovy +++ b/mdm-plugin-datamodel/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/datamodel/UrlMappings.groovy @@ -108,6 +108,8 @@ class UrlMappings { get '/allDataClasses'(controller: 'dataClass', action: 'all') + get '/dataElements'(controller: 'dataElement', action: 'index') + /** * DataTypes */ diff --git a/mdm-plugin-datamodel/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementController.groovy b/mdm-plugin-datamodel/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementController.groovy index a647b8d2ea..92580241ce 100644 --- a/mdm-plugin-datamodel/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementController.groovy +++ b/mdm-plugin-datamodel/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementController.groovy @@ -121,8 +121,9 @@ class DataElementController extends CatalogueItemController { @Override protected List listAllReadableResources(Map params) { - params.sort = params.sort ?: ['idx': 'asc', 'label': 'asc'] + if (params.dataTypeId) { + params.sort = params.sort ?: ['idx': 'asc', 'label': 'asc'] if (!dataTypeService.findByDataModelIdAndId(params.dataModelId, params.dataTypeId)) { notFound(params.dataTypeId) return null @@ -131,6 +132,12 @@ class DataElementController extends CatalogueItemController { } if (params.all) removePaginationParameters() + if (!params.dataClassId) { + params.sort = params.sort ?: ['dataClass.idx': 'asc', 'depth': 'asc', 'idx': 'asc'] + return dataElementService.findAllByDataModelId(params.dataModelId, params) + } + + params.sort = params.sort ?: ['idx': 'asc', 'label': 'asc'] if (!dataClassService.findByDataModelIdAndId(params.dataModelId, params.dataClassId)) { notFound(params.dataClassId) return null diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/datamodel/item/DataElementFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/datamodel/item/DataElementFunctionalSpec.groovy index f23cb05084..e776ac9aea 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/datamodel/item/DataElementFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/datamodel/item/DataElementFunctionalSpec.groovy @@ -33,6 +33,7 @@ import io.micronaut.http.HttpStatus /** *
  * Controller: dataElement
+ *  |  GET     | /api/dataModels/${dataModelId}/dataElements                                   | Action: index
  *  |  POST    | /api/dataModels/${dataModelId}/dataClasses/${dataClassId}/dataElements        | Action: save
  *  |  GET     | /api/dataModels/${dataModelId}/dataClasses/${dataClassId}/dataElements        | Action: index
  *  |  DELETE  | /api/dataModels/${dataModelId}/dataClasses/${dataClassId}/dataElements/${id}  | Action: delete
@@ -305,6 +306,39 @@ class DataElementFunctionalSpec extends UserAccessAndCopyingInDataModelsFunction
         assert body.dataType.breadcrumbs[0].finalised == false
     }
 
+    void 'test getting all DataElements for a DataModel'() {
+        given:
+        String endpoint = "/api/dataModels/${getComplexDataModelId()}/dataElements"
+
+        when: 'not logged in'
+        def response = GET(endpoint, MAP_ARG, true)
+
+        then:
+        verifyResponse HttpStatus.NOT_FOUND, response
+
+        when: 'logged in as reader'
+        loginReader()
+        response = GET(endpoint, MAP_ARG, true)
+
+        then:
+        verifyResponse HttpStatus.OK, response
+        responseBody().count == 3
+        responseBody().items[0].domainType == 'DataElement'
+        responseBody().items[1].domainType == 'DataElement'
+        responseBody().items[2].domainType == 'DataElement'
+
+        when: 'logged in as writer'
+        loginEditor()
+        response = GET(endpoint, MAP_ARG, true)
+
+        then:
+        verifyResponse HttpStatus.OK, response
+        responseBody().count == 3
+        responseBody().items[0].domainType == 'DataElement'
+        responseBody().items[1].domainType == 'DataElement'
+        responseBody().items[2].domainType == 'DataElement'
+    }
+
     /*
     void 'test getting all DataElements for a known DataType'() {
         given:

From 56466f3acd88eb6979af9a162571e7f2f3f55ea3 Mon Sep 17 00:00:00 2001
From: Oliver Freeman 
Date: Tue, 15 Jun 2021 09:03:26 +0100
Subject: [PATCH 05/82] Move mergeDiff methods into its own class

---
 .../core/diff/bidirectional/MergeDiff.groovy  | 178 ++++++++++++++++++
 1 file changed, 178 insertions(+)
 create mode 100644 mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/MergeDiff.groovy

diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/MergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/MergeDiff.groovy
new file mode 100644
index 0000000000..6a993f047d
--- /dev/null
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/MergeDiff.groovy
@@ -0,0 +1,178 @@
+package uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional
+
+
+import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable
+
+import static uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ArrayDiff.isArrayDiff
+import static uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.FieldDiff.isFieldDiff
+
+class MergeDiff extends ObjectDiff {
+
+    MergeDiff(Class targetClass) {
+        super(targetClass)
+    }
+
+
+    /**
+     * Filters an ObjectDiff of two {@code Diffable}s based on the differences of each of the Diffables to their common ancestor. See MC-9228
+     * for details on filtering criteria.
+     * @param leftMergeDiff ObjectDiff between left Diffable and commonAncestor Diffable
+     * @param rightMergeDiff ObjectDiff between right Diffable and commonAncestor Diffable
+     * @return this ObjectDiff with f
+     */
+    @SuppressWarnings('GroovyVariableNotAssigned')
+    ObjectDiff mergeDiff(ObjectDiff leftMergeDiff, ObjectDiff rightMergeDiff) {
+        List existingDiffs = new ArrayList<>(this.diffs)
+
+        List leftMergeDiffFieldNames = leftMergeDiff.diffs.fieldName
+        List rightMergeDiffFieldNames = rightMergeDiff.diffs.fieldName
+
+        this.diffs = existingDiffs.collect { diff ->
+
+            if (diff.fieldName in leftMergeDiffFieldNames && diff.fieldName in rightMergeDiffFieldNames) {
+                if (isArrayDiff(diff)) {
+                    return updateArrayMergeDiffPresentOnBothSides(diff as ArrayDiff,
+                                                                  leftMergeDiff.diffs.find { it.fieldName == diff.fieldName } as ArrayDiff,
+                                                                  rightMergeDiff.diffs.find { it.fieldName == diff.fieldName } as ArrayDiff)
+                }
+                if (isFieldDiff(diff)) {
+                    return updateFieldMergeDiffPresentOnBothSides(diff,
+                                                                  rightMergeDiff.diffs.find { it.fieldName == diff.fieldName })
+                }
+            }
+
+            // An entire Diffable type can not be present on the right, so there will be no merge conflicts
+            if (diff.fieldName in leftMergeDiffFieldNames) {
+                if (isArrayDiff(diff)) {
+                    return updateArrayMergeDiffPresentOnOneSide(diff as ArrayDiff,
+                                                                leftMergeDiff.diffs.find { it.fieldName == diff.fieldName } as ArrayDiff)
+                }
+                if (isFieldDiff(diff)) {
+                    return updateFieldMergeDiffPresentOnOneSide(diff as FieldDiff)
+                }
+            }
+            // If not in LHS then dont add
+            null
+        }.findAll() // Strip null values
+        this
+    }
+
+    ArrayDiff updateArrayMergeDiffPresentOnBothSides(ArrayDiff arrayDiff, ArrayDiff leftArrayDiff, ArrayDiff rightArrayDiff) {
+
+        arrayDiff.created = findAllCreatedMergeDiffs(arrayDiff.created, leftArrayDiff)
+        arrayDiff.deleted = findAllDeletedMergeDiffs(arrayDiff.deleted, leftArrayDiff, rightArrayDiff)
+        arrayDiff.modified = findAllModifiedMergeDiffs(arrayDiff.modified, leftArrayDiff, rightArrayDiff)
+        arrayDiff
+    }
+
+    FieldDiff updateFieldMergeDiffPresentOnBothSides(FieldDiff diff, FieldDiff rightFieldDiff) {
+        diff.isMergeConflict = true
+        diff.commonAncestorValue = rightFieldDiff.left
+        diff
+    }
+
+    ArrayDiff updateArrayMergeDiffPresentOnOneSide(ArrayDiff arrayDiff, ArrayDiff leftArrayDiff) {
+        arrayDiff.deleted.each { it.isMergeConflict = false }
+        arrayDiff.created.each { it.isMergeConflict = false }
+
+        arrayDiff.modified.each { objDiff ->
+            def diffIdentifier = objDiff.right.diffIdentifier
+            def leftObjDiff = leftArrayDiff.modified.find { it.left.diffIdentifier == diffIdentifier } as ObjectDiff
+            // call recursively
+            objDiff = objDiff.mergeDiff(leftObjDiff, new ObjectDiff<>())
+            objDiff.isMergeConflict = false
+        }
+        arrayDiff
+    }
+
+    FieldDiff updateFieldMergeDiffPresentOnOneSide(FieldDiff fieldDiff) {
+        fieldDiff.isMergeConflict = false
+        fieldDiff
+    }
+
+    Collection findAllCreatedMergeDiffs(Collection created, ArrayDiff leftArrayDiff) {
+        created.collect { MergeWrapper wrapper ->
+            def diffIdentifier = wrapper.value.diffIdentifier
+            if (diffIdentifier in leftArrayDiff.created.value.diffIdentifier) {
+                // top created, left created
+                wrapper.isMergeConflict = false
+                return wrapper
+            }
+            if (diffIdentifier in leftArrayDiff.modified.left.diffIdentifier) {
+                // top created, left modified
+                wrapper.isMergeConflict = true
+                wrapper.commonAncestorValue = leftArrayDiff.left.find { it.diffIdentifier == diffIdentifier }
+                return wrapper
+            }
+            null
+        }.findAll()
+    }
+
+    Collection findAllDeletedMergeDiffs(Collection deleted, ArrayDiff leftArrayDiff, ArrayDiff rightArrayDiff) {
+        deleted.collect { MergeWrapper wrapper ->
+            def diffIdentifier = wrapper.value.diffIdentifier
+            if (diffIdentifier in rightArrayDiff.modified.left.diffIdentifier) {
+                // top deleted, right modified
+                wrapper.isMergeConflict = true
+                wrapper.commonAncestorValue = rightArrayDiff.left.find { it.diffIdentifier == diffIdentifier }
+                return wrapper
+            } else if (diffIdentifier in leftArrayDiff.deleted.value.diffIdentifier) {
+                // top deleted, right not modified, left deleted
+                wrapper.isMergeConflict = false
+                return wrapper
+            }
+            null
+        }.findAll()
+    }
+
+    Collection findAllModifiedMergeDiffs(Collection modified, ArrayDiff leftArrayDiff, ArrayDiff rightArrayDiff) {
+        modified.collect { ObjectDiff objDiff ->
+            def diffIdentifier = objDiff.right.diffIdentifier
+            if (diffIdentifier in leftArrayDiff.created.value.diffIdentifier) {
+                return updateModifiedObjectMergeDiffCreatedOnOneSide(objDiff)
+            }
+
+            if (diffIdentifier in leftArrayDiff.modified.left.diffIdentifier) {
+                if (diffIdentifier in rightArrayDiff.modified.left.diffIdentifier) {
+                    // top modified, left modified, right modified
+                    return createModifiedObjectMergeDiffModifiedOnBothSides(objDiff,
+                                                                            leftArrayDiff.modified.
+                                                                                find { it.left.diffIdentifier == diffIdentifier } as ObjectDiff,
+                                                                            rightArrayDiff.modified.
+                                                                                find { it.left.diffIdentifier == diffIdentifier } as ObjectDiff,
+                                                                            rightArrayDiff.left.find { it.diffIdentifier == diffIdentifier }
+                    )
+                }
+                // top modified, left modified, right not modified
+                return updateModifiedObjectMergeDiffPresentOnOneSide(objDiff)
+            }
+            null
+        }.findAll()
+    }
+
+    ObjectDiff updateModifiedObjectMergeDiffCreatedOnOneSide(ObjectDiff objectDiff) {
+        // top modified, right created, (left also created)
+        objectDiff.diffs.each {
+            it.isMergeConflict = true
+            it.commonAncestorValue = null
+        }
+        objectDiff.isMergeConflict = true
+        objectDiff.commonAncestorValue = null
+        return objectDiff
+    }
+
+    ObjectDiff createModifiedObjectMergeDiffModifiedOnBothSides(ObjectDiff objectDiff, ObjectDiff leftObjDiff, ObjectDiff rightObjDiff,
+                                                                Object commonAncestorValue) {
+        // call recursively
+        ObjectDiff mergeDiff = objectDiff.mergeDiff(leftObjDiff, rightObjDiff)
+        mergeDiff.isMergeConflict = true
+        mergeDiff.commonAncestorValue = commonAncestorValue
+        return mergeDiff
+    }
+
+    ObjectDiff updateModifiedObjectMergeDiffPresentOnOneSide(ObjectDiff objectDiff) {
+        objectDiff.diffs.each { it.isMergeConflict = false }
+        objectDiff.isMergeConflict = false
+        objectDiff
+    }
+}

From 7feeed514d6164ce2f47fe09668bc4bd49b0f559 Mon Sep 17 00:00:00 2001
From: Oliver Freeman 
Date: Tue, 15 Jun 2021 15:34:47 +0100
Subject: [PATCH 06/82] Rework the basic Diff objects structure to try and make
 it cleaner to track whats happening when we attempt a merge

---
 .../core/facet/Annotation.groovy              |   2 +-
 .../core/facet/Metadata.groovy                |   2 +-
 .../maurodatamapper/core/facet/Rule.groovy    |   4 +-
 .../core/facet/rule/RuleRepresentation.groovy |   4 +-
 .../views/arrayDiff/_arrayDiff.gson           |   2 +-
 .../views/fieldDiff/_fieldDiff.gson           |   2 +-
 .../views/mergeWrapper/_mergeWrapper.gson     |   4 +-
 .../views/objectDiff/_objectDiff.gson         |   2 +-
 .../maurodatamapper/core/diff/Diff.groovy     |  37 +-
 .../core/diff/DiffBuilder.groovy              |  38 ++
 .../maurodatamapper/core/diff/Diffable.groovy |   2 +
 .../core/diff/Mergeable.groovy                |  27 --
 .../core/diff/ObjectDiff.groovy               | 348 ------------------
 .../diff/{ => bidirectional}/ArrayDiff.groovy |  54 +--
 .../bidirectional/BiDirectionalDiff.groovy    |  46 +++
 .../diff/{ => bidirectional}/FieldDiff.groovy |  37 +-
 .../core/diff/bidirectional/ObjectDiff.groovy | 210 +++++++++++
 .../diff/unidirectional/CreationDiff.groovy   |  35 ++
 .../diff/unidirectional/DeletionDiff.groovy   |  35 ++
 .../UniDirectionalDiff.groovy}                |  30 +-
 .../core/model/CatalogueItem.groovy           |   2 +-
 .../maurodatamapper/core/model/Model.groovy   |   6 +-
 .../core/util/test/BasicModel.groovy          |   2 +-
 .../core/util/test/BasicModelItem.groovy      |   2 +-
 .../maurodatamapper/dataflow/DataFlow.groovy  |   2 +-
 .../component/DataClassComponent.groovy       |   2 +-
 .../component/DataElementComponent.groovy     |   2 +-
 .../dataflow/DataFlowService.groovy           |   2 +-
 ...ImportAndDefaultExporterServiceSpec.groovy |   9 +-
 .../datamodel/DataModel.groovy                |   2 +-
 .../datamodel/item/DataClass.groovy           |   2 +-
 .../datamodel/item/DataElement.groovy         |   2 +-
 .../item/datatype/EnumerationType.groovy      |   2 +-
 .../item/datatype/ModelDataType.groovy        |   2 +-
 .../item/datatype/PrimitiveType.groovy        |   2 +-
 .../item/datatype/ReferenceType.groovy        |   2 +-
 .../enumeration/EnumerationValue.groovy       |   2 +-
 .../grails-app/views/dataModel/diff.gson      |   2 +-
 .../grails-app/views/dataModel/mergeDiff.gson |   2 +-
 .../DataModelServiceIntegrationSpec.groovy    |   2 +-
 ...ImportAndDefaultExporterServiceSpec.groovy |   2 +-
 .../referencedata/ReferenceDataModel.groovy   |   4 +-
 .../item/ReferenceDataElement.groovy          |   2 +-
 .../item/ReferenceDataValue.groovy            |   2 +-
 .../datatype/ReferenceEnumerationType.groovy  |   2 +-
 .../datatype/ReferencePrimitiveType.groovy    |   2 +-
 .../ReferenceEnumerationValue.groovy          |   2 +-
 .../views/referenceDataModel/diff.gson        |   2 +-
 .../views/referenceDataModel/mergeDiff.gson   |   2 +-
 ...enceDataImporterExporterServiceSpec.groovy |   2 +-
 ...enceDataImporterExporterServiceSpec.groovy |   2 +-
 .../provider/BaseImporterExporterSpec.groovy  |  16 +-
 .../terminology/CodeSet.groovy                |   2 +-
 .../terminology/Terminology.groovy            |   2 +-
 .../terminology/item/Term.groovy              |   2 +-
 .../item/TermRelationshipType.groovy          |   2 +-
 .../item/term/TermRelationship.groovy         |   2 +-
 .../grails-app/views/codeSet/diff.gson        |   2 +-
 .../grails-app/views/codeSet/mergeDiff.gson   |   2 +-
 .../grails-app/views/terminology/diff.gson    |   2 +-
 .../views/terminology/mergeDiff.gson          |   2 +-
 ...nCodeSetImporterExporterServiceSpec.groovy |   2 +-
 ...lCodeSetImporterExporterServiceSpec.groovy |   2 +-
 ...ImportAndDefaultExporterServiceSpec.groovy |   2 +-
 .../security/test/BasicModel.groovy           |   2 +-
 .../security/test/BasicModelItem.groovy       |   2 +-
 .../test/unit/core/CatalogueItemSpec.groovy   |  10 +-
 .../test/unit/core/ModelSpec.groovy           |   4 +-
 68 files changed, 537 insertions(+), 517 deletions(-)
 create mode 100644 mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy
 delete mode 100644 mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Mergeable.groovy
 delete mode 100644 mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/ObjectDiff.groovy
 rename mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/{ => bidirectional}/ArrayDiff.groovy (53%)
 create mode 100644 mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/BiDirectionalDiff.groovy
 rename mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/{ => bidirectional}/FieldDiff.groovy (65%)
 create mode 100644 mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy
 create mode 100644 mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/CreationDiff.groovy
 create mode 100644 mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/DeletionDiff.groovy
 rename mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/{MergeWrapper.groovy => unidirectional/UniDirectionalDiff.groovy} (62%)

diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy
index 0a79f9c032..961e6af97a 100644
--- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy
+++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy
@@ -18,7 +18,7 @@
 package uk.ac.ox.softeng.maurodatamapper.core.facet
 
 import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.InformationAwareConstraints
 import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.InformationAware
 import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.MultiFacetItemAware
diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy
index b30d6da55d..2b704d8eb1 100644
--- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy
+++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy
@@ -18,7 +18,7 @@
 package uk.ac.ox.softeng.maurodatamapper.core.facet
 
 import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.MultiFacetItemAware
 import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CallableConstraints
 import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CreatorAwareConstraints
diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy
index 357df716b0..8da0ec5499 100644
--- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy
+++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy
@@ -18,7 +18,7 @@
 package uk.ac.ox.softeng.maurodatamapper.core.facet
 
 import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.rule.RuleRepresentation
 import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.MultiFacetItemAware
 import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CallableConstraints
@@ -130,5 +130,5 @@ class Rule implements MultiFacetItemAware, CreatorAware, Diffable {
 
     static DetachedCriteria byName(String name) {
         new DetachedCriteria(Rule).eq('name', name)
-    }    
+    }
 }
\ No newline at end of file
diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy
index dbf47bf751..c601c1103c 100644
--- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy
+++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy
@@ -18,7 +18,7 @@
 package uk.ac.ox.softeng.maurodatamapper.core.facet.rule
 
 import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Rule
 import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.EditHistoryAware
 import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CallableConstraints
@@ -60,7 +60,7 @@ class RuleRepresentation implements Diffable, EditHistoryAwa
     }
 
     /**
-     * Force language to be trimmed and lower case so that e.g. 'SQL' and ' sql' are treated as the same. 
+     * Force language to be trimmed and lower case so that e.g. 'SQL' and ' sql' are treated as the same.
      */
     void setLanguage(String language) {
         this.language = language?.trim()?.toLowerCase()
diff --git a/mdm-core/grails-app/views/arrayDiff/_arrayDiff.gson b/mdm-core/grails-app/views/arrayDiff/_arrayDiff.gson
index 8fe2006284..38a688976a 100644
--- a/mdm-core/grails-app/views/arrayDiff/_arrayDiff.gson
+++ b/mdm-core/grails-app/views/arrayDiff/_arrayDiff.gson
@@ -1,4 +1,4 @@
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ArrayDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ArrayDiff
 
 model {
     ArrayDiff arrayDiff
diff --git a/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson b/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson
index 424e2af57e..5baaaca9da 100644
--- a/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson
+++ b/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson
@@ -1,4 +1,4 @@
-import uk.ac.ox.softeng.maurodatamapper.core.diff.FieldDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.FieldDiff
 
 model {
     FieldDiff fieldDiff
diff --git a/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson b/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson
index 27f347b4eb..a024cb841e 100644
--- a/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson
+++ b/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson
@@ -1,7 +1,7 @@
-import uk.ac.ox.softeng.maurodatamapper.core.diff.MergeWrapper
+import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.UniDirectionalDiff
 
 model {
-    MergeWrapper mergeWrapper
+    UniDirectionalDiff mergeWrapper
 
 }
 
diff --git a/mdm-core/grails-app/views/objectDiff/_objectDiff.gson b/mdm-core/grails-app/views/objectDiff/_objectDiff.gson
index 943a425fea..707c8f36d9 100644
--- a/mdm-core/grails-app/views/objectDiff/_objectDiff.gson
+++ b/mdm-core/grails-app/views/objectDiff/_objectDiff.gson
@@ -1,4 +1,4 @@
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata
 import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem
 import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.InformationAware
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diff.groovy
index 5a19161711..fcd25f5fc4 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diff.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diff.groovy
@@ -17,10 +17,22 @@
  */
 package uk.ac.ox.softeng.maurodatamapper.core.diff
 
-abstract class Diff extends Mergeable {
+import groovy.transform.CompileStatic
 
-    T left
-    T right
+@CompileStatic
+abstract class Diff {
+
+    T value
+    Boolean mergeConflict
+    T commonAncestorValue
+    Class targetClass
+
+    protected Diff(Class targetClass) {
+        this.targetClass = targetClass
+        mergeConflict = false
+    }
+
+    abstract Integer getNumberOfDiffs()
 
     String getDiffType() {
         getClass().simpleName
@@ -33,25 +45,22 @@ abstract class Diff extends Mergeable {
 
         Diff diff = (Diff) o
 
-        if (left != diff.left) return false
-        if (right != diff.right) return false
+        if (value != diff.value) return false
 
         return true
     }
 
-    Diff leftHandSide(T lhs) {
-        this.left = lhs
-        this
+    boolean objectsAreIdentical() {
+        !getNumberOfDiffs()
     }
 
-    Diff rightHandSide(T rhs) {
-        this.right = rhs
+    Diff commonAncestor(T ca) {
+        this.commonAncestorValue = ca
         this
     }
 
-    boolean objectsAreIdentical() {
-        !getNumberOfDiffs()
+    Diff asMergeConflict() {
+        this.mergeConflict = true
+        this
     }
-
-    abstract Integer getNumberOfDiffs()
 }
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy
new file mode 100644
index 0000000000..285fac91d8
--- /dev/null
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy
@@ -0,0 +1,38 @@
+package uk.ac.ox.softeng.maurodatamapper.core.diff
+
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ArrayDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.FieldDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.MergeDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.CreationDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.DeletionDiff
+
+import groovy.transform.CompileStatic
+
+@CompileStatic
+class DiffBuilder {
+
+    static  ArrayDiff arrayDiff(Class arrayClass) {
+        new ArrayDiff(arrayClass as Class>)
+    }
+
+    static  FieldDiff fieldDiff(Class fieldClass) {
+        new FieldDiff(fieldClass)
+    }
+
+    static  ObjectDiff objectDiff(Class objectClass) {
+        new ObjectDiff(objectClass)
+    }
+
+    static  MergeDiff mergeDiff(Class objectClass) {
+        new MergeDiff(objectClass)
+    }
+
+    static  CreationDiff creationDiff(Class objectClass) {
+        new CreationDiff(objectClass)
+    }
+
+    static  DeletionDiff deletionDiff(Class objectClass) {
+        new DeletionDiff(objectClass)
+    }
+}
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diffable.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diffable.groovy
index 86c43e7a60..03b8dce1a0 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diffable.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diffable.groovy
@@ -17,6 +17,8 @@
  */
 package uk.ac.ox.softeng.maurodatamapper.core.diff
 
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
+
 import grails.compiler.GrailsCompileStatic
 
 @GrailsCompileStatic
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Mergeable.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Mergeable.groovy
deleted file mode 100644
index da57381759..0000000000
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Mergeable.groovy
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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.diff
-
-import grails.compiler.GrailsCompileStatic
-
-@GrailsCompileStatic
-abstract class Mergeable {
-
-    Boolean isMergeConflict
-    T commonAncestorValue
-}
\ No newline at end of file
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/ObjectDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/ObjectDiff.groovy
deleted file mode 100644
index f66b2f74be..0000000000
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/ObjectDiff.groovy
+++ /dev/null
@@ -1,348 +0,0 @@
-/*
- * 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.diff
-
-import uk.ac.ox.softeng.maurodatamapper.core.api.exception.ApiDiffException
-
-import groovy.transform.stc.ClosureParams
-import groovy.transform.stc.SimpleType
-
-import java.time.OffsetDateTime
-
-class ObjectDiff extends Diff {
-
-    List diffs
-
-    String leftIdentifier
-    String rightIdentifier
-
-    private ObjectDiff() {
-        diffs = []
-    }
-
-    @Override
-    boolean equals(o) {
-        if (this.is(o)) return true
-        if (getClass() != o.class) return false
-        if (!super.equals(o)) return false
-
-        ObjectDiff objectDiff = (ObjectDiff) o
-
-        if (leftIdentifier != objectDiff.leftIdentifier) return false
-        if (rightIdentifier != objectDiff.rightIdentifier) return false
-        if (diffs != objectDiff.diffs) return false
-
-        return true
-    }
-
-    @Override
-    String toString() {
-        int numberOfDiffs = getNumberOfDiffs()
-        if (!numberOfDiffs) return "${leftIdentifier} == ${rightIdentifier}"
-        "${leftIdentifier} <> ${rightIdentifier} :: ${numberOfDiffs} differences\n  ${diffs.collect {it.toString()}.join('\n  ')}"
-    }
-
-    @Override
-    Integer getNumberOfDiffs() {
-        diffs?.sum {it.getNumberOfDiffs()} as Integer ?: 0
-    }
-
-    ObjectDiff leftHandSide(String leftId, T lhs) {
-        leftHandSide(lhs)
-        this.leftIdentifier = leftId
-        this
-    }
-
-    ObjectDiff rightHandSide(String rightId, T rhs) {
-        rightHandSide(rhs)
-        this.rightIdentifier = rightId
-        this
-    }
-
-    ObjectDiff appendNumber(final String fieldName, final Number lhs, final Number rhs) throws ApiDiffException {
-        append(FieldDiff.builder(Number), fieldName, lhs, rhs)
-    }
-
-    ObjectDiff appendBoolean(final String fieldName, final Boolean lhs, final Boolean rhs) throws ApiDiffException {
-        append(FieldDiff.builder(Boolean), fieldName, lhs, rhs)
-    }
-
-    ObjectDiff appendString(final String fieldName, final String lhs, final String rhs) throws ApiDiffException {
-        append(FieldDiff.builder(String), fieldName, clean(lhs), clean(rhs))
-    }
-
-    ObjectDiff appendOffsetDateTime(final String fieldName, final OffsetDateTime lhs, final OffsetDateTime rhs) throws ApiDiffException {
-        append(FieldDiff.builder(OffsetDateTime), fieldName, lhs, rhs)
-    }
-
-    def  ObjectDiff appendList(Class diffableClass, String fieldName,
-                                                      Collection lhs, Collection rhs)
-        throws ApiDiffException {
-
-        validateFieldNameNotNull(fieldName)
-
-        // If no lhs or rhs then nothing to compare
-        if (!lhs && !rhs) return this
-
-        ArrayDiff diff = ArrayDiff.builder(diffableClass)
-            .fieldName(fieldName)
-            .leftHandSide(lhs)
-            .rightHandSide(rhs)
-
-
-        // If no lhs then all rhs have been created/added
-        if (!lhs) {
-            return append(diff.created(rhs))
-        }
-
-        // If no rhs then all lhs have been deleted/removed
-        if (!rhs) {
-            return append(diff.deleted(lhs))
-        }
-
-        Collection deleted = []
-        Collection modified = []
-
-        // Assume all rhs have been created new
-        List created = new ArrayList<>(rhs)
-
-        Map lhsMap = lhs.collectEntries {[it.getDiffIdentifier(), it]}
-        Map rhsMap = rhs.collectEntries {[it.getDiffIdentifier(), it]}
-
-        // Work through each lhs object and compare to rhs object
-        lhsMap.each {di, lObj ->
-            K rObj = rhsMap[di]
-            if (rObj) {
-                // If robj then it exists and has not been created
-                created.remove(rObj)
-                ObjectDiff od = lObj.diff(rObj)
-                // If not equal then objects have been modified
-                if (!od.objectsAreIdentical()) {
-                    modified.add(od)
-                }
-            } else {
-                // If no robj then object has been deleted from lhs
-                deleted.add(lObj)
-            }
-        }
-
-        if (created || deleted || modified) {
-            append(diff.created(created)
-                       .deleted(deleted)
-                       .modified(modified))
-        }
-        this
-    }
-
-    def  ObjectDiff append(FieldDiff fieldDiff, String fieldName, K lhs, K rhs) {
-        validateFieldNameNotNull(fieldName)
-        if (lhs == null && rhs == null) {
-            return this
-        }
-        if (lhs != rhs) {
-            append(fieldDiff.fieldName(fieldName).leftHandSide(lhs).rightHandSide(rhs))
-        }
-        this
-    }
-
-    ObjectDiff append(FieldDiff fieldDiff) {
-        diffs.add(fieldDiff)
-        this
-    }
-
-    /**
-     * Filters an ObjectDiff of two {@code Diffable}s based on the differences of each of the Diffables to their common ancestor. See MC-9228
-     * for details on filtering criteria.
-     * @param leftMergeDiff ObjectDiff between left Diffable and commonAncestor Diffable
-     * @param rightMergeDiff ObjectDiff between right Diffable and commonAncestor Diffable
-     * @return this ObjectDiff with f
-     */
-    @SuppressWarnings('GroovyVariableNotAssigned')
-    ObjectDiff mergeDiff(ObjectDiff leftMergeDiff, ObjectDiff rightMergeDiff) {
-        List existingDiffs = new ArrayList<>(this.diffs)
-
-        List leftMergeDiffFieldNames = leftMergeDiff.diffs.fieldName
-        List rightMergeDiffFieldNames = rightMergeDiff.diffs.fieldName
-
-        this.diffs = existingDiffs.collect {diff ->
-
-            if (diff.fieldName in leftMergeDiffFieldNames && diff.fieldName in rightMergeDiffFieldNames) {
-                if (ArrayDiff.isArrayDiff(diff)) {
-                    return updateArrayMergeDiffPresentOnBothSides(diff as ArrayDiff,
-                                                                  leftMergeDiff.diffs.find {it.fieldName == diff.fieldName} as ArrayDiff,
-                                                                  rightMergeDiff.diffs.find {it.fieldName == diff.fieldName} as ArrayDiff)
-                }
-                if (FieldDiff.isFieldDiff(diff)) {
-                    return updateFieldMergeDiffPresentOnBothSides(diff,
-                                                                  rightMergeDiff.diffs.find {it.fieldName == diff.fieldName})
-                }
-            }
-
-            // An entire Diffable type can not be present on the right, so there will be no merge conflicts
-            if (diff.fieldName in leftMergeDiffFieldNames) {
-                if (ArrayDiff.isArrayDiff(diff)) {
-                    return updateArrayMergeDiffPresentOnOneSide(diff as ArrayDiff,
-                                                                leftMergeDiff.diffs.find {it.fieldName == diff.fieldName} as ArrayDiff)
-                }
-                if (FieldDiff.isFieldDiff(diff)) {
-                    return updateFieldMergeDiffPresentOnOneSide(diff as FieldDiff)
-                }
-            }
-            // If not in LHS then dont add
-            null
-        }.findAll() // Strip null values
-        this
-    }
-
-    ArrayDiff updateArrayMergeDiffPresentOnOneSide(ArrayDiff arrayDiff, ArrayDiff leftArrayDiff) {
-        arrayDiff.deleted.each {it.isMergeConflict = false}
-        arrayDiff.created.each {it.isMergeConflict = false}
-
-        arrayDiff.modified.each {objDiff ->
-            def diffIdentifier = objDiff.right.diffIdentifier
-            def leftObjDiff = leftArrayDiff.modified.find {it.left.diffIdentifier == diffIdentifier} as ObjectDiff
-            // call recursively
-            objDiff = objDiff.mergeDiff(leftObjDiff, new ObjectDiff<>())
-            objDiff.isMergeConflict = false
-        }
-        arrayDiff
-    }
-
-    FieldDiff updateFieldMergeDiffPresentOnOneSide(FieldDiff fieldDiff) {
-        fieldDiff.isMergeConflict = false
-        fieldDiff
-    }
-
-    FieldDiff updateFieldMergeDiffPresentOnBothSides(FieldDiff diff, FieldDiff rightFieldDiff) {
-        diff.isMergeConflict = true
-        diff.commonAncestorValue = rightFieldDiff.left
-        diff
-    }
-
-    ArrayDiff updateArrayMergeDiffPresentOnBothSides(ArrayDiff arrayDiff, ArrayDiff leftArrayDiff, ArrayDiff rightArrayDiff) {
-
-        arrayDiff.created = findAllCreatedMergeDiffs(arrayDiff.created, leftArrayDiff)
-        arrayDiff.deleted = findAllDeletedMergeDiffs(arrayDiff.deleted, leftArrayDiff, rightArrayDiff)
-        arrayDiff.modified = findAllModifiedMergeDiffs(arrayDiff.modified, leftArrayDiff, rightArrayDiff)
-        arrayDiff
-    }
-
-    Collection findAllCreatedMergeDiffs(Collection created, ArrayDiff leftArrayDiff) {
-        created.collect {MergeWrapper wrapper ->
-            def diffIdentifier = wrapper.value.diffIdentifier
-            if (diffIdentifier in leftArrayDiff.created.value.diffIdentifier) {
-                // top created, left created
-                wrapper.isMergeConflict = false
-                return wrapper
-            }
-            if (diffIdentifier in leftArrayDiff.modified.left.diffIdentifier) {
-                // top created, left modified
-                wrapper.isMergeConflict = true
-                wrapper.commonAncestorValue = leftArrayDiff.left.find {it.diffIdentifier == diffIdentifier}
-                return wrapper
-            }
-            null
-        }.findAll()
-    }
-
-    Collection findAllDeletedMergeDiffs(Collection deleted, ArrayDiff leftArrayDiff, ArrayDiff rightArrayDiff) {
-        deleted.collect {MergeWrapper wrapper ->
-            def diffIdentifier = wrapper.value.diffIdentifier
-            if (diffIdentifier in rightArrayDiff.modified.left.diffIdentifier) {
-                // top deleted, right modified
-                wrapper.isMergeConflict = true
-                wrapper.commonAncestorValue = rightArrayDiff.left.find {it.diffIdentifier == diffIdentifier}
-                return wrapper
-            } else if (diffIdentifier in leftArrayDiff.deleted.value.diffIdentifier) {
-                // top deleted, right not modified, left deleted
-                wrapper.isMergeConflict = false
-                return wrapper
-            }
-            null
-        }.findAll()
-    }
-
-    Collection findAllModifiedMergeDiffs(Collection modified, ArrayDiff leftArrayDiff, ArrayDiff rightArrayDiff) {
-        modified.collect {ObjectDiff objDiff ->
-            def diffIdentifier = objDiff.right.diffIdentifier
-            if (diffIdentifier in leftArrayDiff.created.value.diffIdentifier) {
-                return updateModifiedObjectMergeDiffCreatedOnOneSide(objDiff)
-            }
-
-            if (diffIdentifier in leftArrayDiff.modified.left.diffIdentifier) {
-                if (diffIdentifier in rightArrayDiff.modified.left.diffIdentifier) {
-                    // top modified, left modified, right modified
-                    return createModifiedObjectMergeDiffModifiedOnBothSides(objDiff,
-                                                                            leftArrayDiff.modified.find {it.left.diffIdentifier == diffIdentifier} as ObjectDiff,
-                                                                            rightArrayDiff.modified.find {it.left.diffIdentifier == diffIdentifier} as ObjectDiff,
-                                                                            rightArrayDiff.left.find {it.diffIdentifier == diffIdentifier}
-                    )
-                }
-                // top modified, left modified, right not modified
-                return updateModifiedObjectMergeDiffPresentOnOneSide(objDiff)
-            }
-            null
-        }.findAll()
-    }
-
-    ObjectDiff updateModifiedObjectMergeDiffCreatedOnOneSide(ObjectDiff objectDiff) {
-        // top modified, right created, (left also created)
-        objectDiff.diffs.each {
-            it.isMergeConflict = true
-            it.commonAncestorValue = null
-        }
-        objectDiff.isMergeConflict = true
-        objectDiff.commonAncestorValue = null
-        return objectDiff
-    }
-
-    ObjectDiff createModifiedObjectMergeDiffModifiedOnBothSides(ObjectDiff objectDiff, ObjectDiff leftObjDiff, ObjectDiff rightObjDiff, Object commonAncestorValue) {
-        // call recursively
-        ObjectDiff mergeDiff = objectDiff.mergeDiff(leftObjDiff, rightObjDiff)
-        mergeDiff.isMergeConflict = true
-        mergeDiff.commonAncestorValue = commonAncestorValue
-        return mergeDiff
-    }
-
-    ObjectDiff updateModifiedObjectMergeDiffPresentOnOneSide(ObjectDiff objectDiff) {
-        objectDiff.diffs.each {it.isMergeConflict = false}
-        objectDiff.isMergeConflict = false
-        objectDiff
-    }
-
-    FieldDiff find(@DelegatesTo(List) @ClosureParams(value = SimpleType,
-        options = 'uk.ac.ox.softeng.maurodatamapper.core.diff.FieldDiff') Closure closure) {
-        diffs.find closure
-    }
-
-
-    private static void validateFieldNameNotNull(final String fieldName) throws ApiDiffException {
-        if (!fieldName) {
-            throw new ApiDiffException('OD01', 'Field name cannot be null or blank')
-        }
-    }
-
-    static String clean(String s) {
-        s?.trim() ?: null
-    }
-
-    static  ObjectDiff builder(Class objectClass) {
-        new ObjectDiff()
-    }
-}
-
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/ArrayDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ArrayDiff.groovy
similarity index 53%
rename from mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/ArrayDiff.groovy
rename to mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ArrayDiff.groovy
index 1dc6ae289c..04e81a87ce 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/ArrayDiff.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ArrayDiff.groovy
@@ -15,48 +15,64 @@
  *
  * SPDX-License-Identifier: Apache-2.0
  */
-package uk.ac.ox.softeng.maurodatamapper.core.diff
+package uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional
 
-class ArrayDiff extends FieldDiff> {
+import uk.ac.ox.softeng.maurodatamapper.core.diff.Diff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable
+import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.CreationDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.DeletionDiff
 
-    Collection> created
-    Collection> deleted
-    Collection> modified
+class ArrayDiff extends FieldDiff> {
 
-    private ArrayDiff() {
+    Collection> created
+    Collection> deleted
+    Collection> modified
+
+    ArrayDiff(Class> targetClass) {
+        super(targetClass)
         created = []
         deleted = []
         modified = []
     }
 
-    ArrayDiff created(Collection created) {
-        this.created = created.collect { new MergeWrapper(it) }
+    ArrayDiff created(Collection created) {
+        this.created = created.collect { new CreationDiff(it) }
         this
     }
 
-    ArrayDiff deleted(Collection deleted) {
-        this.deleted = deleted.collect { new MergeWrapper(it) }
+    ArrayDiff deleted(Collection deleted) {
+        this.deleted = deleted.collect { new DeletionDiff(it) }
         this
     }
 
-    ArrayDiff modified(Collection> modified) {
+    ArrayDiff modified(Collection> modified) {
         this.modified = modified
         this
     }
 
+    ArrayDiff withCreatedDiffs(Collection> created) {
+        this.created = created
+        this
+    }
+
+    ArrayDiff withDeletedDiffs(Collection> deleted) {
+        this.deleted = deleted
+        this
+    }
+
     @Override
-    ArrayDiff fieldName(String fieldName) {
-        super.fieldName(fieldName) as ArrayDiff
+    ArrayDiff fieldName(String fieldName) {
+        super.fieldName(fieldName) as ArrayDiff
     }
 
     @Override
-    ArrayDiff leftHandSide(Collection lhs) {
-        super.leftHandSide(lhs) as ArrayDiff
+    ArrayDiff leftHandSide(Collection lhs) {
+        super.leftHandSide(lhs) as ArrayDiff
     }
 
     @Override
-    ArrayDiff rightHandSide(Collection rhs) {
-        super.rightHandSide(rhs) as ArrayDiff
+    ArrayDiff rightHandSide(Collection rhs) {
+        super.rightHandSide(rhs) as ArrayDiff
     }
 
     @Override
@@ -80,10 +96,6 @@ class ArrayDiff extends FieldDiff> {
         stringBuilder.toString()
     }
 
-    static  ArrayDiff builder(Class arrayClass) {
-        new ArrayDiff()
-    }
-
     static boolean isArrayDiff(Diff diff) {
         diff.diffType == ArrayDiff.simpleName
     }
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/BiDirectionalDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/BiDirectionalDiff.groovy
new file mode 100644
index 0000000000..c96c827460
--- /dev/null
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/BiDirectionalDiff.groovy
@@ -0,0 +1,46 @@
+package uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional
+
+import uk.ac.ox.softeng.maurodatamapper.core.diff.Diff
+
+import groovy.transform.CompileStatic
+
+@CompileStatic
+abstract class BiDirectionalDiff extends Diff {
+
+    B right
+
+    protected BiDirectionalDiff(Class targetClass) {
+        super(targetClass)
+    }
+
+    @Override
+    boolean equals(o) {
+        if (this.is(o)) return true
+        if (getClass() != o.class) return false
+
+        BiDirectionalDiff diff = (BiDirectionalDiff) o
+
+        if (left != diff.left) return false
+        if (right != diff.right) return false
+
+        return true
+    }
+
+    BiDirectionalDiff leftHandSide(B lhs) {
+        this.left = lhs
+        this
+    }
+
+    BiDirectionalDiff rightHandSide(B rhs) {
+        this.right = rhs
+        this
+    }
+
+    void setLeft(B left) {
+        this.value = left
+    }
+
+    B getLeft() {
+        this.value
+    }
+}
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/FieldDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/FieldDiff.groovy
similarity index 65%
rename from mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/FieldDiff.groovy
rename to mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/FieldDiff.groovy
index 2b19e1da00..cf07035ff8 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/FieldDiff.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/FieldDiff.groovy
@@ -15,17 +15,19 @@
  *
  * SPDX-License-Identifier: Apache-2.0
  */
-package uk.ac.ox.softeng.maurodatamapper.core.diff
+package uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional
 
-class FieldDiff extends Diff {
+import uk.ac.ox.softeng.maurodatamapper.core.diff.Diff
+
+class FieldDiff extends BiDirectionalDiff {
 
     String fieldName
 
-    FieldDiff() {
+    FieldDiff(Class targetClass) {
+        super(targetClass)
     }
 
-
-    FieldDiff fieldName(String fieldName) {
+    FieldDiff fieldName(String fieldName) {
         this.fieldName = fieldName
         this
     }
@@ -36,7 +38,7 @@ class FieldDiff extends Diff {
         if (getClass() != o.class) return false
         if (!super.equals(o)) return false
 
-        FieldDiff fieldDiff = (FieldDiff) o
+        FieldDiff fieldDiff = (FieldDiff) o
 
         if (fieldName != fieldDiff.fieldName) return false
 
@@ -49,22 +51,29 @@ class FieldDiff extends Diff {
     }
 
     @Override
-    FieldDiff leftHandSide(T lhs) {
-        super.leftHandSide(lhs) as FieldDiff
+    FieldDiff leftHandSide(F lhs) {
+        super.leftHandSide(lhs) as FieldDiff
     }
 
     @Override
-    FieldDiff rightHandSide(T rhs) {
-        super.rightHandSide(rhs) as FieldDiff
+    FieldDiff rightHandSide(F rhs) {
+        super.rightHandSide(rhs) as FieldDiff
     }
 
     @Override
-    String toString() {
-        "${fieldName} :: ${left?.toString()} <> ${right?.toString()}"
+    FieldDiff commonAncestor(F ca) {
+        super.commonAncestor(ca) as FieldDiff
     }
 
-    static  FieldDiff builder(Class fieldClass) {
-        new FieldDiff()
+    @Override
+    FieldDiff asMergeConflict() {
+        this.mergeConflict = true
+        this
+    }
+
+    @Override
+    String toString() {
+        "${fieldName} :: ${left?.toString()} <> ${right?.toString()}"
     }
 
     static boolean isFieldDiff(Diff diff) {
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy
new file mode 100644
index 0000000000..4a5c247b04
--- /dev/null
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy
@@ -0,0 +1,210 @@
+/*
+ * 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.diff.bidirectional
+
+import uk.ac.ox.softeng.maurodatamapper.core.api.exception.ApiDiffException
+import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable
+
+import groovy.transform.stc.ClosureParams
+import groovy.transform.stc.SimpleType
+
+import java.time.OffsetDateTime
+
+import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.arrayDiff
+import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.fieldDiff
+
+/*
+Always in relation to the lhs
+ */
+
+class ObjectDiff extends BiDirectionalDiff {
+
+    List diffs
+
+    String leftIdentifier
+    String rightIdentifier
+    String path
+
+    ObjectDiff(Class targetClass) {
+        super(targetClass)
+        diffs = []
+    }
+
+    @Override
+    boolean equals(o) {
+        if (this.is(o)) return true
+        if (getClass() != o.class) return false
+        if (!super.equals(o)) return false
+
+        ObjectDiff objectDiff = (ObjectDiff) o
+
+        if (leftIdentifier != objectDiff.leftIdentifier) return false
+        if (rightIdentifier != objectDiff.rightIdentifier) return false
+        if (diffs != objectDiff.diffs) return false
+
+        return true
+    }
+
+    @Override
+    String toString() {
+        int numberOfDiffs = getNumberOfDiffs()
+        if (!numberOfDiffs) return "${leftIdentifier} == ${rightIdentifier}"
+        "${leftIdentifier} <> ${rightIdentifier} :: ${numberOfDiffs} differences\n  ${diffs.collect { it.toString() }.join('\n  ')}"
+    }
+
+    @Override
+    Integer getNumberOfDiffs() {
+        diffs?.sum { it.getNumberOfDiffs() } as Integer ?: 0
+    }
+
+    O getRight() {
+        right
+    }
+
+    O getLeft() {
+        right
+    }
+
+    ObjectDiff leftHandSide(String leftId, O lhs) {
+        leftHandSide(lhs)
+        this.leftIdentifier = leftId
+        this
+    }
+
+    ObjectDiff rightHandSide(String rightId, O rhs) {
+        rightHandSide(rhs)
+        this.rightIdentifier = rightId
+        this
+    }
+
+    ObjectDiff appendNumber(final String fieldName, final Number lhs, final Number rhs) throws ApiDiffException {
+        append(fieldDiff(Number), fieldName, lhs, rhs)
+    }
+
+    ObjectDiff appendBoolean(final String fieldName, final Boolean lhs, final Boolean rhs) throws ApiDiffException {
+        append(fieldDiff(Boolean), fieldName, lhs, rhs)
+    }
+
+    ObjectDiff appendString(final String fieldName, final String lhs, final String rhs) throws ApiDiffException {
+        append(fieldDiff(String), fieldName, clean(lhs), clean(rhs))
+    }
+
+    ObjectDiff appendOffsetDateTime(final String fieldName, final OffsetDateTime lhs, final OffsetDateTime rhs) throws ApiDiffException {
+        append(fieldDiff(OffsetDateTime), fieldName, lhs, rhs)
+    }
+
+    def  ObjectDiff appendList(Class diffableClass, String fieldName,
+                                                      Collection lhs, Collection rhs) throws ApiDiffException {
+
+        validateFieldNameNotNull(fieldName)
+
+        // If no lhs or rhs then nothing to compare
+        if (!lhs && !rhs) return this
+
+        ArrayDiff diff = arrayDiff(diffableClass)
+            .fieldName(fieldName)
+            .leftHandSide(lhs)
+            .rightHandSide(rhs)
+
+
+        // If no lhs then all rhs have been created/added
+        if (!lhs) {
+            return append(diff.created(rhs))
+        }
+
+        // If no rhs then all lhs have been deleted/removed
+        if (!rhs) {
+            return append(diff.deleted(lhs))
+        }
+
+        Collection deleted = []
+        Collection modified = []
+
+        // Assume all rhs have been created new
+        List created = new ArrayList<>(rhs)
+
+        Map lhsMap = lhs.collectEntries { [it.getDiffIdentifier(), it] }
+        Map rhsMap = rhs.collectEntries { [it.getDiffIdentifier(), it] }
+
+        // Work through each lhs object and compare to rhs object
+        lhsMap.each { di, lObj ->
+            K rObj = rhsMap[di]
+            if (rObj) {
+                // If robj then it exists and has not been created
+                created.remove(rObj)
+                ObjectDiff od = lObj.diff(rObj)
+                // If not equal then objects have been modified
+                if (!od.objectsAreIdentical()) {
+                    modified.add(od)
+                }
+            } else {
+                // If no robj then object has been deleted from lhs
+                deleted.add(lObj)
+            }
+        }
+
+        if (created || deleted || modified) {
+            append(diff.created(created)
+                       .deleted(deleted)
+                       .modified(modified))
+        }
+        this
+    }
+
+    def  ObjectDiff append(FieldDiff fieldDiff, String fieldName, K lhs, K rhs) {
+        validateFieldNameNotNull(fieldName)
+        if (lhs == null && rhs == null) {
+            return this
+        }
+        if (lhs != rhs) {
+            append(fieldDiff.fieldName(fieldName).leftHandSide(lhs).rightHandSide(rhs))
+        }
+        this
+    }
+
+    ObjectDiff append(FieldDiff fieldDiff) {
+        diffs.add(fieldDiff)
+        this
+    }
+
+    FieldDiff find(@DelegatesTo(List) @ClosureParams(value = SimpleType,
+        options = 'uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.FieldDiff') Closure closure) {
+        diffs.find closure
+    }
+
+
+    private static void validateFieldNameNotNull(final String fieldName) throws ApiDiffException {
+        if (!fieldName) {
+            throw new ApiDiffException('OD01', 'Field name cannot be null or blank')
+        }
+    }
+
+    static String clean(String s) {
+        s?.trim() ?: null
+    }
+
+    /**
+     * @use DiffBuilder.objectDiff* @param objectClass
+     * @return
+     */
+    @Deprecated
+    static  ObjectDiff builder(Class objectClass) {
+        new ObjectDiff()
+    }
+}
+
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/CreationDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/CreationDiff.groovy
new file mode 100644
index 0000000000..98f17cf8b1
--- /dev/null
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/CreationDiff.groovy
@@ -0,0 +1,35 @@
+package uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional
+
+import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable
+
+import groovy.transform.CompileStatic
+
+@CompileStatic
+class CreationDiff extends UniDirectionalDiff {
+
+    CreationDiff(Class targetClass) {
+        super(targetClass)
+    }
+
+    CreationDiff(C created) {
+        this(created.getClass() as Class)
+        this.value = created
+    }
+
+    CreationDiff created(C object) {
+        this.value = object
+        this
+    }
+
+    @Override
+    CreationDiff commonAncestor(C ca) {
+        this.commonAncestorValue = ca
+        this
+    }
+
+    @Override
+    CreationDiff asMergeConflict() {
+        this.mergeConflict = true
+        this
+    }
+}
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/DeletionDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/DeletionDiff.groovy
new file mode 100644
index 0000000000..207c02c6c4
--- /dev/null
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/DeletionDiff.groovy
@@ -0,0 +1,35 @@
+package uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional
+
+import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable
+
+import groovy.transform.CompileStatic
+
+@CompileStatic
+class DeletionDiff extends UniDirectionalDiff {
+
+    DeletionDiff(Class targetClass) {
+        super(targetClass)
+    }
+
+    DeletionDiff(D created) {
+        this(created.getClass() as Class)
+        this.value = created
+    }
+
+    DeletionDiff deleted(D object) {
+        this.value = object
+        this
+    }
+
+    @Override
+    DeletionDiff commonAncestor(D ca) {
+        this.commonAncestorValue = ca
+        this
+    }
+
+    @Override
+    DeletionDiff asMergeConflict() {
+        this.mergeConflict = true
+        this
+    }
+}
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/MergeWrapper.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/UniDirectionalDiff.groovy
similarity index 62%
rename from mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/MergeWrapper.groovy
rename to mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/UniDirectionalDiff.groovy
index fbe7bbd0bf..38e8e4dfc1 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/MergeWrapper.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/UniDirectionalDiff.groovy
@@ -15,29 +15,31 @@
  *
  * SPDX-License-Identifier: Apache-2.0
  */
-package uk.ac.ox.softeng.maurodatamapper.core.diff
+package uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional
 
-class MergeWrapper extends Mergeable {
-    T value
+import uk.ac.ox.softeng.maurodatamapper.core.diff.Diff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable
 
-    MergeWrapper(T value) {
-        this.value = value
-    }
-
-    @Override
-    boolean equals(o) {
-        if (this.is(o)) return true
-        if (getClass() != o.class) return false
+import groovy.transform.CompileStatic
 
-        MergeWrapper diff = (MergeWrapper) o
+@CompileStatic
+abstract class UniDirectionalDiff extends Diff {
 
-        if (value != diff.value) return false
+    protected UniDirectionalDiff(Class targetClass) {
+        super(targetClass)
+    }
 
-        return true
+    String getValueIdentifier() {
+        value.diffIdentifier
     }
 
     @Override
     String toString() {
         value.toString()
     }
+
+    @Override
+    Integer getNumberOfDiffs() {
+        1
+    }
 }
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy
index 055f1693dc..31944fe2e8 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy
@@ -19,7 +19,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.model
 
 
 import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTree
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Model.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Model.groovy
index dc4f7ec28a..9eff3653af 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Model.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Model.groovy
@@ -20,7 +20,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.model
 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.diff.Diffable
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.model.facet.VersionLinkAware
 import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.VersionAware
 import uk.ac.ox.softeng.maurodatamapper.security.SecurableResource
@@ -136,7 +136,7 @@ trait Model extends CatalogueItem implements SecurableRes
     static  DetachedCriteria byLabelAndBranchNameAndFinalisedAndLatestModelVersion(String label, String branchName) {
         byLabelAndFinalisedAndLatestModelVersion(label)
         .eq('branchName', branchName)
-    }    
+    }
 
     static  DetachedCriteria byLabelAndNotFinalised(String label) {
         byLabel(label)
@@ -151,5 +151,5 @@ trait Model extends CatalogueItem implements SecurableRes
     static  DetachedCriteria byLabelAndBranchNameAndNotFinalised(String label, String branchName) {
         byLabelAndNotFinalised(label)
         .eq('branchName', branchName)
-    }       
+    }
 }
diff --git a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/BasicModel.groovy b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/BasicModel.groovy
index 6b801c8cae..f621c81819 100644
--- a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/BasicModel.groovy
+++ b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/BasicModel.groovy
@@ -18,7 +18,7 @@
 package uk.ac.ox.softeng.maurodatamapper.core.util.test
 
 import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTree
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata
diff --git a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/BasicModelItem.groovy b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/BasicModelItem.groovy
index 6c3f6dee00..11c0c9916f 100644
--- a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/BasicModelItem.groovy
+++ b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/BasicModelItem.groovy
@@ -18,7 +18,7 @@
 package uk.ac.ox.softeng.maurodatamapper.core.util.test
 
 import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata
 import uk.ac.ox.softeng.maurodatamapper.core.facet.ReferenceFile
diff --git a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlow.groovy b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlow.groovy
index 7b2386727b..2fb55da8b4 100644
--- a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlow.groovy
+++ b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlow.groovy
@@ -18,7 +18,7 @@
 package uk.ac.ox.softeng.maurodatamapper.dataflow
 
 import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata
 import uk.ac.ox.softeng.maurodatamapper.core.facet.ReferenceFile
diff --git a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponent.groovy b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponent.groovy
index e6b8dc606a..8675a8f559 100644
--- a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponent.groovy
+++ b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponent.groovy
@@ -18,7 +18,7 @@
 package uk.ac.ox.softeng.maurodatamapper.dataflow.component
 
 import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata
 import uk.ac.ox.softeng.maurodatamapper.core.facet.ReferenceFile
diff --git a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponent.groovy b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponent.groovy
index d47028e755..796b90ca39 100644
--- a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponent.groovy
+++ b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponent.groovy
@@ -18,7 +18,7 @@
 package uk.ac.ox.softeng.maurodatamapper.dataflow.component
 
 import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata
 import uk.ac.ox.softeng.maurodatamapper.core.facet.ReferenceFile
diff --git a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowService.groovy b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowService.groovy
index 5284160cd7..5c53b84ba0 100644
--- a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowService.groovy
+++ b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowService.groovy
@@ -19,7 +19,7 @@ package uk.ac.ox.softeng.maurodatamapper.dataflow
 
 import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException
 import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkType
 import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItemService
 import uk.ac.ox.softeng.maurodatamapper.core.path.PathService
diff --git a/mdm-plugin-dataflow/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy b/mdm-plugin-dataflow/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy
index f40d3fffe2..dc17490b6f 100644
--- a/mdm-plugin-dataflow/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy
+++ b/mdm-plugin-dataflow/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy
@@ -18,7 +18,7 @@
 package uk.ac.ox.softeng.maurodatamapper.dataflow.test.provider
 
 import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.provider.exporter.ExporterProviderService
 import uk.ac.ox.softeng.maurodatamapper.dataflow.DataFlow
 import uk.ac.ox.softeng.maurodatamapper.dataflow.DataFlowService
@@ -31,10 +31,7 @@ import org.springframework.beans.factory.annotation.Autowired
 import spock.lang.Stepwise
 import spock.lang.Unroll
 
-import com.google.common.base.CaseFormat
 import java.nio.charset.Charset
-import java.nio.file.Files
-import java.nio.file.Path
 
 /**
  * @since 11/01/2021
@@ -138,14 +135,14 @@ abstract class DataBindImportAndDefaultExporterServiceSpec, ReferenceSummaryM
 
     int countReferenceDataElementsByLabel(String label) {
         this.referenceDataElements?.count { it.label == label } ?: 0
-    }    
+    }
 
     @Override
     String getEditLabel() {
diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataElement.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataElement.groovy
index 2bc164effa..a8da1a7e98 100644
--- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataElement.groovy
+++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataElement.groovy
@@ -18,7 +18,7 @@
 package uk.ac.ox.softeng.maurodatamapper.referencedata.item
 
 import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata
 import uk.ac.ox.softeng.maurodatamapper.core.facet.ReferenceFile
diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValue.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValue.groovy
index 47acab878a..c34c16b5f8 100644
--- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValue.groovy
+++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValue.groovy
@@ -18,7 +18,7 @@
 package uk.ac.ox.softeng.maurodatamapper.referencedata.item
 
 import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModel
 import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware
 import uk.ac.ox.softeng.maurodatamapper.util.Utils
diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferenceEnumerationType.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferenceEnumerationType.groovy
index aa3d251a0c..90757e13ea 100644
--- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferenceEnumerationType.groovy
+++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferenceEnumerationType.groovy
@@ -17,7 +17,7 @@
  */
 package uk.ac.ox.softeng.maurodatamapper.referencedata.item.datatype
 
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem
 import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.IndexedSiblingAware
 import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.validator.UniqueValuesValidator
diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferencePrimitiveType.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferencePrimitiveType.groovy
index f1a822de8a..5b4fad7055 100644
--- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferencePrimitiveType.groovy
+++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferencePrimitiveType.groovy
@@ -17,7 +17,7 @@
  */
 package uk.ac.ox.softeng.maurodatamapper.referencedata.item.datatype
 
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 
 import grails.gorm.DetachedCriteria
 import grails.rest.Resource
diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/enumeration/ReferenceEnumerationValue.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/enumeration/ReferenceEnumerationValue.groovy
index 1a49ec00fa..d0a9ccf4fb 100644
--- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/enumeration/ReferenceEnumerationValue.groovy
+++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/enumeration/ReferenceEnumerationValue.groovy
@@ -18,7 +18,7 @@
 package uk.ac.ox.softeng.maurodatamapper.referencedata.item.datatype.enumeration
 
 import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata
 import uk.ac.ox.softeng.maurodatamapper.core.facet.ReferenceFile
diff --git a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/diff.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/diff.gson
index 91d8d36c34..770157d4d6 100644
--- a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/diff.gson
+++ b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/diff.gson
@@ -1,4 +1,4 @@
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModel
 
 model {
diff --git a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/mergeDiff.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/mergeDiff.gson
index 5a9466efbd..2a86644a5d 100644
--- a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/mergeDiff.gson
+++ b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/mergeDiff.gson
@@ -1,4 +1,4 @@
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModel
 
 model {
diff --git a/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/JsonReferenceDataImporterExporterServiceSpec.groovy b/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/JsonReferenceDataImporterExporterServiceSpec.groovy
index 506ac4fc3f..25b2ddf46a 100644
--- a/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/JsonReferenceDataImporterExporterServiceSpec.groovy
+++ b/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/JsonReferenceDataImporterExporterServiceSpec.groovy
@@ -19,7 +19,7 @@ package uk.ac.ox.softeng.maurodatamapper.referencedata.provider
 
 import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException
 import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModel
 import uk.ac.ox.softeng.maurodatamapper.referencedata.provider.exporter.ReferenceDataJsonExporterService
diff --git a/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/XmlReferenceDataImporterExporterServiceSpec.groovy b/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/XmlReferenceDataImporterExporterServiceSpec.groovy
index b9718d8c83..97060c5bd0 100644
--- a/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/XmlReferenceDataImporterExporterServiceSpec.groovy
+++ b/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/XmlReferenceDataImporterExporterServiceSpec.groovy
@@ -19,7 +19,7 @@ package uk.ac.ox.softeng.maurodatamapper.referencedata.provider
 
 import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException
 import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModel
 import uk.ac.ox.softeng.maurodatamapper.referencedata.provider.exporter.ReferenceDataXmlExporterService
diff --git a/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/test/provider/BaseImporterExporterSpec.groovy b/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/test/provider/BaseImporterExporterSpec.groovy
index cfe6d26859..5e524ab19e 100644
--- a/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/test/provider/BaseImporterExporterSpec.groovy
+++ b/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/test/provider/BaseImporterExporterSpec.groovy
@@ -19,7 +19,7 @@
 
 import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException
 import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.ImporterProviderService
 import uk.ac.ox.softeng.maurodatamapper.core.provider.exporter.ExporterProviderService
@@ -229,7 +229,7 @@ abstract class BaseImporterExporterSpec extends BaseReferenceDataModelIntegratio
 
         then:
         ann.description == 'test annotation 1 description'
-        ann.label == 'test annotation 1 label'          
+        ann.label == 'test annotation 1 label'
 
         when:
         String exported = exportModel(rdm.id)
@@ -266,7 +266,7 @@ abstract class BaseImporterExporterSpec extends BaseReferenceDataModelIntegratio
         !rdm.referenceDataTypes
         !rdm.referenceDataElements
         !rdm.referenceDataValues
-         
+
 
         when:
         String exported = exportModel(rdm.id)
@@ -303,7 +303,7 @@ abstract class BaseImporterExporterSpec extends BaseReferenceDataModelIntegratio
         !rdm.referenceDataTypes
         !rdm.referenceDataElements
         !rdm.referenceDataValues
-         
+
 
         when:
         String exported = exportModel(rdm.id)
@@ -335,23 +335,23 @@ abstract class BaseImporterExporterSpec extends BaseReferenceDataModelIntegratio
         rdm.authority.url == 'http://localhost'
         !rdm.aliases
         !rdm.annotations
-        
+
         //Metadata
         rdm.metadata.size() == 3
 
         //Classifiers
         rdm.classifiers.size() == 1
         rdm.classifiers[0].label == "An imported classifier"
-        
+
         //Reference Data Types
         rdm.referenceDataTypes.size() == 2
-        
+
         //Reference Data Elements
         rdm.referenceDataElements.size() == 2
 
         //Reference Data Values (100 rows of 2 columns)
         rdm.referenceDataValues.size() == 200
-         
+
 
         when:
         String exported = exportModel(rdm.id)
diff --git a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSet.groovy b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSet.groovy
index 75badccb45..1928cbb8c1 100644
--- a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSet.groovy
+++ b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSet.groovy
@@ -19,7 +19,7 @@ package uk.ac.ox.softeng.maurodatamapper.terminology
 
 import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier
 import uk.ac.ox.softeng.maurodatamapper.core.container.Folder
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata
 import uk.ac.ox.softeng.maurodatamapper.core.facet.ReferenceFile
diff --git a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/Terminology.groovy b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/Terminology.groovy
index e6ad75c242..f2debe0550 100644
--- a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/Terminology.groovy
+++ b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/Terminology.groovy
@@ -19,7 +19,7 @@ package uk.ac.ox.softeng.maurodatamapper.terminology
 
 import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier
 import uk.ac.ox.softeng.maurodatamapper.core.container.Folder
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTree
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata
diff --git a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/Term.groovy b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/Term.groovy
index 87a7250af0..e6dd333e23 100644
--- a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/Term.groovy
+++ b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/Term.groovy
@@ -18,7 +18,7 @@
 package uk.ac.ox.softeng.maurodatamapper.terminology.item
 
 import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTree
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata
diff --git a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermRelationshipType.groovy b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermRelationshipType.groovy
index 94638f5a28..8740bf0ae6 100644
--- a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermRelationshipType.groovy
+++ b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermRelationshipType.groovy
@@ -18,7 +18,7 @@
 package uk.ac.ox.softeng.maurodatamapper.terminology.item
 
 import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata
 import uk.ac.ox.softeng.maurodatamapper.core.facet.ReferenceFile
diff --git a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/term/TermRelationship.groovy b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/term/TermRelationship.groovy
index d053137948..ae7043cb43 100644
--- a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/term/TermRelationship.groovy
+++ b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/term/TermRelationship.groovy
@@ -19,7 +19,7 @@ package uk.ac.ox.softeng.maurodatamapper.terminology.item.term
 
 
 import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata
 import uk.ac.ox.softeng.maurodatamapper.core.facet.ReferenceFile
diff --git a/mdm-plugin-terminology/grails-app/views/codeSet/diff.gson b/mdm-plugin-terminology/grails-app/views/codeSet/diff.gson
index f0637f2adf..8547ecc933 100644
--- a/mdm-plugin-terminology/grails-app/views/codeSet/diff.gson
+++ b/mdm-plugin-terminology/grails-app/views/codeSet/diff.gson
@@ -1,4 +1,4 @@
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.terminology.CodeSet
 
 model {
diff --git a/mdm-plugin-terminology/grails-app/views/codeSet/mergeDiff.gson b/mdm-plugin-terminology/grails-app/views/codeSet/mergeDiff.gson
index 0fcb90cd72..f40e575bf9 100644
--- a/mdm-plugin-terminology/grails-app/views/codeSet/mergeDiff.gson
+++ b/mdm-plugin-terminology/grails-app/views/codeSet/mergeDiff.gson
@@ -1,4 +1,4 @@
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.terminology.CodeSet
 
 model {
diff --git a/mdm-plugin-terminology/grails-app/views/terminology/diff.gson b/mdm-plugin-terminology/grails-app/views/terminology/diff.gson
index 2c0425e1ef..cf2f190780 100644
--- a/mdm-plugin-terminology/grails-app/views/terminology/diff.gson
+++ b/mdm-plugin-terminology/grails-app/views/terminology/diff.gson
@@ -1,4 +1,4 @@
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.terminology.Terminology
 
 model {
diff --git a/mdm-plugin-terminology/grails-app/views/terminology/mergeDiff.gson b/mdm-plugin-terminology/grails-app/views/terminology/mergeDiff.gson
index 4341b7e87f..0c9690bb29 100644
--- a/mdm-plugin-terminology/grails-app/views/terminology/mergeDiff.gson
+++ b/mdm-plugin-terminology/grails-app/views/terminology/mergeDiff.gson
@@ -1,4 +1,4 @@
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.terminology.Terminology
 
 model {
diff --git a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/provider/JsonCodeSetImporterExporterServiceSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/provider/JsonCodeSetImporterExporterServiceSpec.groovy
index 06b8f7c196..09b9f2c3d8 100644
--- a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/provider/JsonCodeSetImporterExporterServiceSpec.groovy
+++ b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/provider/JsonCodeSetImporterExporterServiceSpec.groovy
@@ -19,7 +19,7 @@ package uk.ac.ox.softeng.maurodatamapper.terminology.provider
 
 import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException
 import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.terminology.CodeSet
 import uk.ac.ox.softeng.maurodatamapper.terminology.item.Term
diff --git a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/provider/XmlCodeSetImporterExporterServiceSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/provider/XmlCodeSetImporterExporterServiceSpec.groovy
index 5405f3d447..efbf1318b5 100644
--- a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/provider/XmlCodeSetImporterExporterServiceSpec.groovy
+++ b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/provider/XmlCodeSetImporterExporterServiceSpec.groovy
@@ -19,7 +19,7 @@ package uk.ac.ox.softeng.maurodatamapper.terminology.provider
 
 import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException
 import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.terminology.CodeSet
 import uk.ac.ox.softeng.maurodatamapper.terminology.item.Term
diff --git a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/test/provider/DataBindTerminologyImportAndDefaultExporterServiceSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/test/provider/DataBindTerminologyImportAndDefaultExporterServiceSpec.groovy
index c452457d8d..0f53dfded1 100644
--- a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/test/provider/DataBindTerminologyImportAndDefaultExporterServiceSpec.groovy
+++ b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/test/provider/DataBindTerminologyImportAndDefaultExporterServiceSpec.groovy
@@ -18,7 +18,7 @@
 package uk.ac.ox.softeng.maurodatamapper.terminology.test.provider
 
 import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.terminology.Terminology
 import uk.ac.ox.softeng.maurodatamapper.terminology.TerminologyService
 import uk.ac.ox.softeng.maurodatamapper.terminology.provider.exporter.TerminologyExporterProviderService
diff --git a/mdm-security/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/security/test/BasicModel.groovy b/mdm-security/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/security/test/BasicModel.groovy
index ece5deecdc..4ec2725b64 100644
--- a/mdm-security/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/security/test/BasicModel.groovy
+++ b/mdm-security/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/security/test/BasicModel.groovy
@@ -18,7 +18,7 @@
 package uk.ac.ox.softeng.maurodatamapper.security.test
 
 import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTree
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata
diff --git a/mdm-security/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/security/test/BasicModelItem.groovy b/mdm-security/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/security/test/BasicModelItem.groovy
index 3dbed0a3b9..3737a922bc 100644
--- a/mdm-security/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/security/test/BasicModelItem.groovy
+++ b/mdm-security/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/security/test/BasicModelItem.groovy
@@ -18,7 +18,7 @@
 package uk.ac.ox.softeng.maurodatamapper.security.test
 
 import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata
 import uk.ac.ox.softeng.maurodatamapper.core.facet.ReferenceFile
diff --git a/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/unit/core/CatalogueItemSpec.groovy b/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/unit/core/CatalogueItemSpec.groovy
index 7c2abd32d6..dc65e934ea 100644
--- a/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/unit/core/CatalogueItemSpec.groovy
+++ b/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/unit/core/CatalogueItemSpec.groovy
@@ -20,8 +20,8 @@ package uk.ac.ox.softeng.maurodatamapper.test.unit.core
 import uk.ac.ox.softeng.maurodatamapper.core.authority.Authority
 import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier
 import uk.ac.ox.softeng.maurodatamapper.core.container.Folder
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ArrayDiff
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ArrayDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTree
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Edit
@@ -566,12 +566,12 @@ abstract class CatalogueItemSpec extends CreatorAwareSp
         given:
         setValidDomainValues()
         domain.label = 'new\nlabel'
-        
+
 
         when:
         checkAndSave(domain)
 
         then:
-        domain.label == getExpectedNewlineLabel()       
-    }    
+        domain.label == getExpectedNewlineLabel()
+    }
 }
diff --git a/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/unit/core/ModelSpec.groovy b/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/unit/core/ModelSpec.groovy
index 1459336d91..8d5bc6dc40 100644
--- a/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/unit/core/ModelSpec.groovy
+++ b/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/unit/core/ModelSpec.groovy
@@ -18,8 +18,8 @@
 package uk.ac.ox.softeng.maurodatamapper.test.unit.core
 
 import uk.ac.ox.softeng.maurodatamapper.core.container.Folder
-import uk.ac.ox.softeng.maurodatamapper.core.diff.FieldDiff
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.FieldDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.model.Model
 import uk.ac.ox.softeng.maurodatamapper.util.Version
 

From b70beb64c4f034bc46153498ac10899b4cd4ea6f Mon Sep 17 00:00:00 2001
From: Oliver Freeman 
Date: Thu, 17 Jun 2021 11:16:29 +0100
Subject: [PATCH 07/82] Improvements on the diff classes for DSL

---
 .../views/fieldDiff/_fieldDiff.gson           |  6 ++---
 .../views/mergeWrapper/_mergeWrapper.gson     |  6 ++---
 .../maurodatamapper/core/diff/Diff.groovy     |  4 ++++
 .../core/diff/DiffBuilder.groovy              | 23 ++++++++++++++++++-
 .../core/diff/bidirectional/ArrayDiff.groovy  | 16 +++++++++++--
 .../core/diff/bidirectional/FieldDiff.groovy  |  3 +--
 .../core/diff/bidirectional/ObjectDiff.groovy | 23 ++++++++++---------
 .../diff/unidirectional/CreationDiff.groovy   | 12 ++--------
 .../diff/unidirectional/DeletionDiff.groovy   | 12 ++--------
 9 files changed, 63 insertions(+), 42 deletions(-)

diff --git a/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson b/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson
index 5baaaca9da..295d30aba4 100644
--- a/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson
+++ b/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson
@@ -8,9 +8,9 @@ json("${fieldDiff.fieldName}") {
     left fieldDiff.getLeft()
     right fieldDiff.getRight()
 
-    if (fieldDiff.isMergeConflict != null) {
-        isMergeConflict fieldDiff.isMergeConflict
-        if (fieldDiff.isMergeConflict) commonAncestorValue fieldDiff.commonAncestorValue
+    if (fieldDiff.mergeConflict != null) {
+        isMergeConflict fieldDiff.mergeConflict
+        if (fieldDiff.mergeConflict) commonAncestorValue fieldDiff.commonAncestorValue
     }
 
 }
diff --git a/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson b/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson
index a024cb841e..b7129a4e50 100644
--- a/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson
+++ b/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson
@@ -7,8 +7,8 @@ model {
 
 json {
     value tmpl.'/diffable/diffable'(mergeWrapper.value)
-    if (mergeWrapper.isMergeConflict != null) {
-        isMergeConflict mergeWrapper.isMergeConflict
-        if (mergeWrapper.isMergeConflict) commonAncestorValue tmpl.'/diffable/diffable'(mergeWrapper.commonAncestorValue)
+    if (mergeWrapper.mergeConflict != null) {
+        isMergeConflict mergeWrapper.mergeConflict
+        if (mergeWrapper.mergeConflict) commonAncestorValue tmpl.'/diffable/diffable'(mergeWrapper.commonAncestorValue)
     }
 }
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diff.groovy
index fcd25f5fc4..25fcd00575 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diff.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diff.groovy
@@ -54,6 +54,10 @@ abstract class Diff {
         !getNumberOfDiffs()
     }
 
+    Boolean isMergeConflict() {
+        mergeConflict
+    }
+
     Diff commonAncestor(T ca) {
         this.commonAncestorValue = ca
         this
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy
index 285fac91d8..04b054cf75 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy
@@ -12,14 +12,28 @@ import groovy.transform.CompileStatic
 @CompileStatic
 class DiffBuilder {
 
-    static  ArrayDiff arrayDiff(Class arrayClass) {
+    static  ArrayDiff arrayDiff(Class arrayClass) {
         new ArrayDiff(arrayClass as Class>)
     }
 
+    static  ArrayDiff arrayDiff(ArrayDiff original) {
+        arrayDiff(original.targetClass.arrayType() as Class)
+            .fieldName(original.fieldName)
+            .leftHandSide(original.left)
+            .rightHandSide(original.right)
+    }
+
     static  FieldDiff fieldDiff(Class fieldClass) {
         new FieldDiff(fieldClass)
     }
 
+    static  FieldDiff fieldDiff(FieldDiff original) {
+        fieldDiff(original.targetClass)
+            .fieldName(original.fieldName)
+            .leftHandSide(original.left)
+            .rightHandSide(original.right)
+    }
+
     static  ObjectDiff objectDiff(Class objectClass) {
         new ObjectDiff(objectClass)
     }
@@ -28,6 +42,13 @@ class DiffBuilder {
         new MergeDiff(objectClass)
     }
 
+    static  MergeDiff mergeDiff(ObjectDiff objectDiff, K commonAncestor) {
+        mergeDiff(objectDiff.targetClass)
+            .leftHandSide(objectDiff.left)
+            .rightHandSide(objectDiff.right)
+            .commonAncestor(commonAncestor)
+    }
+
     static  CreationDiff creationDiff(Class objectClass) {
         new CreationDiff(objectClass)
     }
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ArrayDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ArrayDiff.groovy
index 04e81a87ce..0781b4de2a 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ArrayDiff.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ArrayDiff.groovy
@@ -22,6 +22,13 @@ import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable
 import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.CreationDiff
 import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.DeletionDiff
 
+import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.creationDiff
+import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.deletionDiff
+
+/**
+ * Note the same object cannot exist in more than one of created, deleted, modified.
+ * These collections are mutually exclusive
+ */
 class ArrayDiff extends FieldDiff> {
 
     Collection> created
@@ -36,12 +43,12 @@ class ArrayDiff extends FieldDiff> {
     }
 
     ArrayDiff created(Collection created) {
-        this.created = created.collect { new CreationDiff(it) }
+        this.created = created.collect { creationDiff(it.class as Class).created(it) }
         this
     }
 
     ArrayDiff deleted(Collection deleted) {
-        this.deleted = deleted.collect { new DeletionDiff(it) }
+        this.deleted = deleted.collect { deletionDiff(it.class as Class).deleted(it) }
         this
     }
 
@@ -75,6 +82,11 @@ class ArrayDiff extends FieldDiff> {
         super.rightHandSide(rhs) as ArrayDiff
     }
 
+    @Override
+    ArrayDiff commonAncestor(Collection ca) {
+        super.commonAncestor(ca) as ArrayDiff
+    }
+
     @Override
     Integer getNumberOfDiffs() {
         created.size() + deleted.size() + ((modified.sum { it.getNumberOfDiffs() } ?: 0) as Integer)
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/FieldDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/FieldDiff.groovy
index cf07035ff8..a63e1e081a 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/FieldDiff.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/FieldDiff.groovy
@@ -67,8 +67,7 @@ class FieldDiff extends BiDirectionalDiff {
 
     @Override
     FieldDiff asMergeConflict() {
-        this.mergeConflict = true
-        this
+       super.asMergeConflict() as FieldDiff
     }
 
     @Override
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy
index 4a5c247b04..ef2c2899fd 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy
@@ -72,23 +72,25 @@ class ObjectDiff extends BiDirectionalDiff {
         diffs?.sum { it.getNumberOfDiffs() } as Integer ?: 0
     }
 
-    O getRight() {
-        right
+    @Deprecated
+    ObjectDiff leftHandSide(String leftId, O lhs) {
+        leftHandSide(lhs)
     }
 
-    O getLeft() {
-        right
+    @Deprecated
+    ObjectDiff rightHandSide(String rightId, O rhs) {
+        rightHandSide(rhs)
     }
 
-    ObjectDiff leftHandSide(String leftId, O lhs) {
-        leftHandSide(lhs)
-        this.leftIdentifier = leftId
+    ObjectDiff leftHandSide(O lhs) {
+        super.leftHandSide(lhs)
+        this.leftIdentifier = lhs.diffIdentifier
         this
     }
 
-    ObjectDiff rightHandSide(String rightId, O rhs) {
-        rightHandSide(rhs)
-        this.rightIdentifier = rightId
+    ObjectDiff rightHandSide(O rhs) {
+        super.rightHandSide(rhs)
+        this.rightIdentifier = rhs.diffIdentifier
         this
     }
 
@@ -187,7 +189,6 @@ class ObjectDiff extends BiDirectionalDiff {
         diffs.find closure
     }
 
-
     private static void validateFieldNameNotNull(final String fieldName) throws ApiDiffException {
         if (!fieldName) {
             throw new ApiDiffException('OD01', 'Field name cannot be null or blank')
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/CreationDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/CreationDiff.groovy
index 98f17cf8b1..4b3ce59605 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/CreationDiff.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/CreationDiff.groovy
@@ -11,25 +11,17 @@ class CreationDiff extends UniDirectionalDiff {
         super(targetClass)
     }
 
-    CreationDiff(C created) {
-        this(created.getClass() as Class)
-        this.value = created
-    }
-
     CreationDiff created(C object) {
         this.value = object
         this
     }
 
-    @Override
     CreationDiff commonAncestor(C ca) {
-        this.commonAncestorValue = ca
-        this
+        super.commonAncestor(ca) as CreationDiff
     }
 
     @Override
     CreationDiff asMergeConflict() {
-        this.mergeConflict = true
-        this
+        super.asMergeConflict() as CreationDiff
     }
 }
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/DeletionDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/DeletionDiff.groovy
index 207c02c6c4..8b4c501c2d 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/DeletionDiff.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/DeletionDiff.groovy
@@ -11,25 +11,17 @@ class DeletionDiff extends UniDirectionalDiff {
         super(targetClass)
     }
 
-    DeletionDiff(D created) {
-        this(created.getClass() as Class)
-        this.value = created
-    }
-
     DeletionDiff deleted(D object) {
         this.value = object
         this
     }
 
-    @Override
     DeletionDiff commonAncestor(D ca) {
-        this.commonAncestorValue = ca
-        this
+        super.commonAncestor(ca) as DeletionDiff
     }
 
     @Override
     DeletionDiff asMergeConflict() {
-        this.mergeConflict = true
-        this
+        super.asMergeConflict() as DeletionDiff
     }
 }

From b6af8d2efa7a1d8464799a07fe3e621b88a0cd16 Mon Sep 17 00:00:00 2001
From: Oliver Freeman 
Date: Thu, 17 Jun 2021 11:17:41 +0100
Subject: [PATCH 08/82] Rename the parameters for merging in the service and
 controller to make it clearer which direction we're going in

---
 .../core/controller/ModelController.groovy     | 12 +++++++-----
 .../core/model/ModelService.groovy             | 18 +++++++++++-------
 2 files changed, 18 insertions(+), 12 deletions(-)

diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy
index b8bb6f8ab5..891ffdc0c8 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy
@@ -23,7 +23,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.authority.AuthorityService
 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.VersionedFolderService
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.exporter.ExporterService
 import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints
 import uk.ac.ox.softeng.maurodatamapper.core.importer.ImporterService
@@ -264,11 +264,13 @@ abstract class ModelController extends CatalogueItemController<
 
     def mergeDiff() {
 
-        T left = queryForResource params[alternateParamsIdKey]
-        if (!left) return notFound(params[alternateParamsIdKey])
+        // Test branch
+        T left = queryForResource params.otherModelId
+        if (!left) return notFound(params.otherModelId)
 
-        T right = queryForResource params.otherModelId
-        if (!right) return notFound(params.otherModelId)
+        // Main branch
+        T right = queryForResource params[alternateParamsIdKey]
+        if (!right) return notFound(params[alternateParamsIdKey])
 
         respond modelService.getMergeDiffForModels(left, right)
     }
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy
index 559b44fc06..aded114797 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy
@@ -23,7 +23,9 @@ import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiNotYetImplementedExcept
 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.container.Folder
-import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.MergeDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
 import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTreeService
 import uk.ac.ox.softeng.maurodatamapper.core.facet.EditService
 import uk.ac.ox.softeng.maurodatamapper.core.facet.EditTitle
@@ -511,14 +513,16 @@ abstract class ModelService extends CatalogueItemService imp
         findLatestFinalisedModelByLabel(label)?.modelVersion ?: Version.from('0.0.0')
     }
 
-    ObjectDiff getMergeDiffForModels(K leftModel, K rightModel) {
-        K commonAncestor = findCommonAncestorBetweenModels(leftModel, rightModel)
+    MergeDiff getMergeDiffForModels(K sourceModel, K targetModel) {
+        K commonAncestor = findCommonAncestorBetweenModels(sourceModel, targetModel)
 
-        ObjectDiff left = commonAncestor.diff(leftModel)
-        ObjectDiff right = commonAncestor.diff(rightModel)
-        ObjectDiff top = rightModel.diff(leftModel)
+        ObjectDiff caDiffSource = commonAncestor.diff(sourceModel)
+        ObjectDiff caDiffTarget = commonAncestor.diff(targetModel)
+        ObjectDiff sourceDiffTarget = sourceModel.diff(targetModel)
 
-        top.mergeDiff(left, right)
+        DiffBuilder
+            .mergeDiff(sourceDiffTarget, commonAncestor)
+            .diff(sourceDiffTarget, caDiffTarget, caDiffSource) as MergeDiff
     }
 
     @Override

From a6622c32f794fc9c4793405069cf8ea280c46ead Mon Sep 17 00:00:00 2001
From: Oliver Freeman 
Date: Thu, 17 Jun 2021 11:20:19 +0100
Subject: [PATCH 09/82] Add more information to the merge diff test to allow
 simpler verification on results

---
 .../DataModelServiceIntegrationSpec.groovy    | 381 +++++++++++-------
 1 file changed, 227 insertions(+), 154 deletions(-)

diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy
index 984d68667c..40bb0d1c67 100644
--- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy
+++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy
@@ -17,7 +17,13 @@
  */
 package uk.ac.ox.softeng.maurodatamapper.datamodel
 
+import uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ArrayDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.FieldDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.MergeDiff
 import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff
+import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata
+import uk.ac.ox.softeng.maurodatamapper.core.facet.MetadataService
 import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType
 import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints
 import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.MergeFieldDiffData
@@ -51,6 +57,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec {
     DataModel simpleDataModel
     DataModelService dataModelService
     DataClassService dataClassService
+    MetadataService metadataService
 
     @Override
     void setupDomainData() {
@@ -284,17 +291,17 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec {
         newDocVersion.edits.size() == 1
 
         and: 'new version of link between old and new version'
-        newDocVersion.versionLinks.any {it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_DOCUMENTATION_VERSION_OF}
+        newDocVersion.versionLinks.any { it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_DOCUMENTATION_VERSION_OF }
 
         and:
-        dataModel.dataTypes.every {odt ->
+        dataModel.dataTypes.every { odt ->
             newDocVersion.dataTypes.any {
                 it.label == odt.label &&
                 it.id != odt.id &&
                 it.domainType == odt.domainType
             }
         }
-        dataModel.dataClasses.every {odc ->
+        dataModel.dataClasses.every { odc ->
             newDocVersion.dataClasses.any {
                 int idcs = it.dataClasses?.size() ?: 0
                 int odcs = odc.dataClasses?.size() ?: 0
@@ -351,17 +358,17 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec {
         newDocVersion.edits.size() == 1
 
         and: 'new version of link between old and new version'
-        newDocVersion.versionLinks.any {it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_DOCUMENTATION_VERSION_OF}
+        newDocVersion.versionLinks.any { it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_DOCUMENTATION_VERSION_OF }
 
         and:
-        dataModel.dataTypes.every {odt ->
+        dataModel.dataTypes.every { odt ->
             newDocVersion.dataTypes.any {
                 it.label == odt.label &&
                 it.id != odt.id &&
                 it.domainType == odt.domainType
             }
         }
-        dataModel.dataClasses.every {odc ->
+        dataModel.dataClasses.every { odc ->
             newDocVersion.dataClasses.any {
                 int idcs = it.dataClasses?.size() ?: 0
                 int odcs = odc.dataClasses?.size() ?: 0
@@ -459,14 +466,14 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec {
         }
 
         and:
-        dataModel.dataTypes.every {odt ->
+        dataModel.dataTypes.every { odt ->
             newVersion.dataTypes.any {
                 it.label == odt.label &&
                 it.id != odt.id &&
                 it.domainType == odt.domainType
             }
         }
-        dataModel.dataClasses.every {odc ->
+        dataModel.dataClasses.every { odc ->
             newVersion.dataClasses.any {
                 int idcs = it.dataClasses?.size() ?: 0
                 int odcs = odc.dataClasses?.size() ?: 0
@@ -522,17 +529,17 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec {
 
 
         and: 'link between old and new version'
-        newVersion.versionLinks.any {it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_FORK_OF}
+        newVersion.versionLinks.any { it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_FORK_OF }
 
         and:
-        dataModel.dataTypes.every {odt ->
+        dataModel.dataTypes.every { odt ->
             newVersion.dataTypes.any {
                 it.label == odt.label &&
                 it.id != odt.id &&
                 it.domainType == odt.domainType
             }
         }
-        dataModel.dataClasses.every {odc ->
+        dataModel.dataClasses.every { odc ->
             newVersion.dataClasses.any {
                 int idcs = it.dataClasses?.size() ?: 0
                 int odcs = odc.dataClasses?.size() ?: 0
@@ -629,17 +636,17 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec {
         newBranch.edits.size() == 1
 
         and: 'new version of link between old and new version'
-        newBranch.versionLinks.any {it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_MODEL_VERSION_OF}
+        newBranch.versionLinks.any { it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_MODEL_VERSION_OF }
 
         and:
-        dataModel.dataTypes.every {odt ->
+        dataModel.dataTypes.every { odt ->
             newBranch.dataTypes.any {
                 it.label == odt.label &&
                 it.id != odt.id &&
                 it.domainType == odt.domainType
             }
         }
-        def missing = dataModel.dataClasses.findAll {odc ->
+        def missing = dataModel.dataClasses.findAll { odc ->
             !newBranch.dataClasses.find {
                 int idcs = it.dataClasses?.size() ?: 0
                 int odcs = odc.dataClasses?.size() ?: 0
@@ -700,17 +707,17 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec {
         newBranch.edits.size() == 1
 
         and: 'new version of link between old and new version'
-        newBranch.versionLinks.any {it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_MODEL_VERSION_OF}
+        newBranch.versionLinks.any { it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_MODEL_VERSION_OF }
 
         and:
-        dataModel.dataTypes.every {odt ->
+        dataModel.dataTypes.every { odt ->
             newBranch.dataTypes.any {
                 it.label == odt.label &&
                 it.id != odt.id &&
                 it.domainType == odt.domainType
             }
         }
-        dataModel.dataClasses.every {odc ->
+        dataModel.dataClasses.every { odc ->
             newBranch.dataClasses.any {
                 int idcs = it.dataClasses?.size() ?: 0
                 int odcs = odc.dataClasses?.size() ?: 0
@@ -929,8 +936,8 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec {
 
         then:
         availableBranches.size() == 2
-        availableBranches.each {it.id in [draftModel.id, testModel.id]}
-        availableBranches.each {it.label == dataModel.label}
+        availableBranches.each { it.id in [draftModel.id, testModel.id] }
+        availableBranches.each { it.label == dataModel.label }
     }
 
 
@@ -940,20 +947,22 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec {
 
         when:
         DataModel dataModel = dataModelService.get(id)
-        dataModel.addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteLeftOnly'))
-            .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteRightOnly'))
-            .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifyLeftOnly'))
-            .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifyRightOnly'))
-            .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteAndDelete'))
-            .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteAndModify'))
-            .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifyAndDelete'))
-            .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifyAndModifyReturningNoDifference'))
-            .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifyAndModifyReturningDifference'))
+        dataModel.author = 'john'
+        dataModel.addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteSourceOnly'))
+            .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteTargetOnly'))
+            .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifySourceOnly'))
+            .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifyTargetOnly'))
+            .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteBoth'))
+            .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteSourceAndModifyTarget'))
+            .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifySourceAndDeleteTarget'))
+            .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifyBothReturningNoDifference'))
+            .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifyBothReturningDifference'))
         dataModel.addToDataClasses(
             new DataClass(createdByUser: admin, label: 'existingClass')
-                .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteLeftOnlyFromExistingClass'))
-                .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteRightOnlyFromExistingClass'))
-        )
+                .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteSourceOnlyFromExistingClass'))
+                .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteTargetOnlyFromExistingClass'))
+        ).addToMetadata(namespace: 'test', key: 'deleteSourceOnly', value: 'deleteSourceOnly')
+            .addToMetadata(namespace: 'test', key: 'modifySourceOnly', value: 'modifySourceOnly')
         dataModelService.finaliseModel(dataModel, admin, null, null, null)
         checkAndSave(dataModel)
 
@@ -961,130 +970,194 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec {
         dataModel.branchName == VersionAwareConstraints.DEFAULT_BRANCH_NAME
 
         when:
-        UUID draftId = createAndSaveNewBranchModel(VersionAwareConstraints.DEFAULT_BRANCH_NAME, dataModel)
+        UUID rightMainId = createAndSaveNewBranchModel(VersionAwareConstraints.DEFAULT_BRANCH_NAME, dataModel)
 
-        dataClassService.delete(dataClassService.findByDataModelIdAndLabel(draftId, 'deleteRightOnlyFromExistingClass'))
-        dataClassService.delete(dataClassService.findByDataModelIdAndLabel(draftId, 'deleteRightOnly'))
-        dataClassService.delete(dataClassService.findByDataModelIdAndLabel(draftId, 'deleteAndDelete'))
-        dataClassService.delete(dataClassService.findByDataModelIdAndLabel(draftId, 'modifyAndDelete'))
+        dataClassService.delete(dataClassService.findByDataModelIdAndLabel(rightMainId, 'deleteTargetOnlyFromExistingClass'))
+        dataClassService.delete(dataClassService.findByDataModelIdAndLabel(rightMainId, 'deleteTargetOnly'))
+        dataClassService.delete(dataClassService.findByDataModelIdAndLabel(rightMainId, 'deleteBoth'))
+        dataClassService.delete(dataClassService.findByDataModelIdAndLabel(rightMainId, 'modifySourceAndDeleteTarget'))
 
-        checkAndSave dataClassService.findByDataModelIdAndLabel(draftId, 'modifyRightOnly').tap {
+        checkAndSave dataClassService.findByDataModelIdAndLabel(rightMainId, 'modifyTargetOnly').tap {
             description = 'Description'
         }
-        checkAndSave dataClassService.findByDataModelIdAndLabel(draftId, 'deleteAndModify').tap {
+        checkAndSave dataClassService.findByDataModelIdAndLabel(rightMainId, 'deleteSourceAndModifyTarget').tap {
             description = 'Description'
         }
-        checkAndSave dataClassService.findByDataModelIdAndLabel(draftId, 'modifyAndModifyReturningNoDifference').tap {
+        checkAndSave dataClassService.findByDataModelIdAndLabel(rightMainId, 'modifyBothReturningNoDifference').tap {
             description = 'Description'
         }
-        checkAndSave dataClassService.findByDataModelIdAndLabel(draftId, 'modifyAndModifyReturningDifference').tap {
-            description = 'DescriptionRight'
+        checkAndSave dataClassService.findByDataModelIdAndLabel(rightMainId, 'modifyBothReturningDifference').tap {
+            description = 'DescriptionTarget'
         }
 
-        checkAndSave dataClassService.findByDataModelIdAndLabel(draftId, 'existingClass')
-                         .addToDataClasses(new DataClass(createdByUser: admin, label: 'addRightToExistingClass'))
+        checkAndSave dataClassService.findByDataModelIdAndLabel(rightMainId, 'existingClass')
+                         .addToDataClasses(new DataClass(createdByUser: admin, label: 'addTargetToExistingClass'))
 
-        DataModel draftModel = dataModelService.get(draftId)
-
-        checkAndSave new DataClass(createdByUser: admin, label: 'rightParentDataClass', dataModel: draftModel)
-                         .addToDataClasses(new DataClass(createdByUser: admin, label: 'rightChildDataClass', dataModel: draftModel))
-        checkAndSave new DataClass(createdByUser: admin, label: 'addRightOnly', dataModel: draftModel)
-        checkAndSave new DataClass(createdByUser: admin, label: 'addAndAddReturningNoDifference', dataModel: draftModel)
-        checkAndSave new DataClass(createdByUser: admin, label: 'addAndAddReturningDifference', description: 'right', dataModel: draftModel)
+        DataModel draftModel = dataModelService.get(rightMainId)
+        draftModel.author = 'dick'
+        checkAndSave new DataClass(createdByUser: admin, label: 'targetParentDataClass', dataModel: draftModel)
+                         .addToDataClasses(new DataClass(createdByUser: admin, label: 'targetChildDataClass', dataModel: draftModel))
+        checkAndSave new DataClass(createdByUser: admin, label: 'addTargetOnly', dataModel: draftModel)
+        checkAndSave new DataClass(createdByUser: admin, label: 'addBothReturningNoDifference', dataModel: draftModel)
+        checkAndSave new DataClass(createdByUser: admin, label: 'addBothReturningDifference', description: 'target', dataModel: draftModel)
 
-        checkAndSave dataModelService.get(draftId).tap {
-            description = 'DescriptionRight'
+        checkAndSave dataModelService.get(rightMainId).tap {
+            description = 'DescriptionTarget'
         }
 
         sessionFactory.currentSession.flush()
         sessionFactory.currentSession.clear()
 
-        UUID testId = createAndSaveNewBranchModel('test', dataModel)
+        UUID leftTestId = createAndSaveNewBranchModel('test', dataModel)
 
-        dataClassService.delete(dataClassService.findByDataModelIdAndLabel(testId, 'deleteLeftOnlyFromExistingClass'))
-        dataClassService.delete(dataClassService.findByDataModelIdAndLabel(testId, 'deleteLeftOnly'))
-        dataClassService.delete(dataClassService.findByDataModelIdAndLabel(testId, 'deleteAndDelete'))
-        dataClassService.delete(dataClassService.findByDataModelIdAndLabel(testId, 'deleteAndModify'))
+        dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteSourceOnlyFromExistingClass'))
+        dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteSourceOnly'))
+        dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteBoth'))
+        dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteSourceAndModifyTarget'))
+        metadataService.delete(metadataService.findAllByMultiFacetAwareItemId(leftTestId).find { it.key == 'deleteSourceOnly' })
 
-        checkAndSave dataClassService.findByDataModelIdAndLabel(testId, 'modifyLeftOnly').tap {
+        checkAndSave dataClassService.findByDataModelIdAndLabel(leftTestId, 'modifySourceOnly').tap {
             description = 'Description'
         }
-        checkAndSave dataClassService.findByDataModelIdAndLabel(testId, 'modifyAndDelete').tap {
+        checkAndSave dataClassService.findByDataModelIdAndLabel(leftTestId, 'modifySourceAndDeleteTarget').tap {
             description = 'Description'
         }
-        checkAndSave dataClassService.findByDataModelIdAndLabel(testId, 'modifyAndModifyReturningNoDifference').tap {
+        checkAndSave dataClassService.findByDataModelIdAndLabel(leftTestId, 'modifyBothReturningNoDifference').tap {
             description = 'Description'
         }
-        checkAndSave dataClassService.findByDataModelIdAndLabel(testId, 'modifyAndModifyReturningDifference').tap {
-            description = 'DescriptionLeft'
+        checkAndSave dataClassService.findByDataModelIdAndLabel(leftTestId, 'modifyBothReturningDifference').tap {
+            description = 'DescriptionSource'
         }
 
-        checkAndSave dataClassService.findByDataModelIdAndLabel(testId, 'existingClass')
-                         .addToDataClasses(new DataClass(createdByUser: admin, label: 'addLeftToExistingClass'))
+        checkAndSave dataClassService.findByDataModelIdAndLabel(leftTestId, 'existingClass')
+                         .addToDataClasses(new DataClass(createdByUser: admin, label: 'addSourceToExistingClass'))
 
-        DataModel testModel = dataModelService.get(testId)
+        DataModel testModel = dataModelService.get(leftTestId)
+        testModel.organisation = 'under test'
+        testModel.author = 'harry'
+        checkAndSave new DataClass(createdByUser: admin, label: 'targetParentDataClass', dataModel: testModel)
+                         .addToDataClasses(new DataClass(createdByUser: admin, label: 'targetChildDataClass', dataModel: testModel))
 
-        checkAndSave new DataClass(createdByUser: admin, label: 'leftParentDataClass', dataModel: testModel)
-                         .addToDataClasses(new DataClass(createdByUser: admin, label: 'leftChildDataClass', dataModel: testModel))
+        checkAndSave new DataClass(createdByUser: admin, label: 'addSourceOnly', dataModel: testModel)
+        checkAndSave new DataClass(createdByUser: admin, label: 'addBothReturningNoDifference', dataModel: testModel)
+        checkAndSave new DataClass(createdByUser: admin, label: 'addBothReturningDifference', description: 'source', dataModel: testModel)
+        checkAndSave new PrimitiveType(createdBy: StandardEmailAddress.ADMIN, label: 'addSourceOnlyOnlyChangeInArray', dataModel: testModel)
+
+
+        checkAndSave metadataService.findAllByMultiFacetAwareItemId(leftTestId).find { it.key == 'modifySourceOnly' }.tap {
+            value = 'altered'
+        }
 
-        checkAndSave new DataClass(createdByUser: admin, label: 'addLeftOnly', dataModel: testModel)
-        checkAndSave new DataClass(createdByUser: admin, label: 'addAndAddReturningNoDifference', dataModel: testModel)
-        checkAndSave new DataClass(createdByUser: admin, label: 'addAndAddReturningDifference', description: 'left', dataModel: testModel)
 
         sessionFactory.currentSession.flush()
         sessionFactory.currentSession.clear()
 
-        DataModel draft = dataModelService.get(draftId)
-        DataModel test = dataModelService.get(testId)
+        DataModel rightMain = dataModelService.get(rightMainId)
+        DataModel leftTest = dataModelService.get(leftTestId)
 
-        def mergeDiff = dataModelService.getMergeDiffForModels(test, draft)
+        MergeDiff mergeDiff = dataModelService.getMergeDiffForModels(leftTest, rightMain)
 
         then:
-        mergeDiff.class == ObjectDiff
         mergeDiff.diffs
-        mergeDiff.numberOfDiffs == 11
-        mergeDiff.diffs.fieldName as Set == ['branchName', 'dataClasses'] as Set
-        def branchNameDiff = mergeDiff.diffs.find {it.fieldName == 'branchName'}
-        branchNameDiff.left == VersionAwareConstraints.DEFAULT_BRANCH_NAME
-        branchNameDiff.right == 'test'
-        !branchNameDiff.isMergeConflict
-        def dataClassesDiff = mergeDiff.diffs.find {it.fieldName == 'dataClasses'}
-        dataClassesDiff.created.size == 3
-        dataClassesDiff.deleted.size == 2
-        dataClassesDiff.modified.size == 4
-        dataClassesDiff.created.value.label as Set == ['addLeftOnly', 'leftParentDataClass', 'modifyAndDelete'] as Set
-        !dataClassesDiff.created.find {it.value.label == 'addLeftOnly'}.isMergeConflict
-        !dataClassesDiff.created.find {it.value.label == 'addLeftOnly'}.commonAncestorValue
-        !dataClassesDiff.created.find {it.value.label == 'leftParentDataClass'}.isMergeConflict
-        !dataClassesDiff.created.find {it.value.label == 'leftParentDataClass'}.commonAncestorValue
-        dataClassesDiff.created.find {it.value.label == 'modifyAndDelete'}.isMergeConflict
-        dataClassesDiff.created.find {it.value.label == 'modifyAndDelete'}.commonAncestorValue
-        dataClassesDiff.deleted.value.label as Set == ['deleteAndModify', 'deleteLeftOnly'] as Set
-        dataClassesDiff.deleted.find {it.value.label == 'deleteAndModify'}.isMergeConflict
-        dataClassesDiff.deleted.find {it.value.label == 'deleteAndModify'}.commonAncestorValue
-        !dataClassesDiff.deleted.find {it.value.label == 'deleteLeftOnly'}.isMergeConflict
-        !dataClassesDiff.deleted.find {it.value.label == 'deleteLeftOnly'}.commonAncestorValue
-        dataClassesDiff.modified.left.diffIdentifier as Set == ['existingClass', 'modifyAndModifyReturningDifference', 'modifyLeftOnly',
+        //        mergeDiff.numberOfDiffs == 11
+
+        when: 'branch name is a non-conflicting diff'
+        FieldDiff stringFieldDiff = mergeDiff.find { it.fieldName == 'branchName' }
+
+        then:
+        stringFieldDiff.left == 'test'
+        stringFieldDiff.right == VersionAwareConstraints.DEFAULT_BRANCH_NAME
+        stringFieldDiff.commonAncestorValue == VersionAwareConstraints.DEFAULT_BRANCH_NAME
+        !stringFieldDiff.isMergeConflict()
+
+        when: 'organisation is a non-conflicting change'
+        stringFieldDiff = mergeDiff.find { it.fieldName == 'organisation' }
+
+        then:
+        stringFieldDiff.left == 'under test'
+        stringFieldDiff.right == null
+        stringFieldDiff.commonAncestorValue == null
+        !stringFieldDiff.isMergeConflict()
+
+        when: 'author is a conflicting change'
+        stringFieldDiff = mergeDiff.find { it.fieldName == 'author' }
+
+        then:
+        stringFieldDiff.left == 'harry'
+        stringFieldDiff.right == 'dick'
+        stringFieldDiff.commonAncestorValue == 'john'
+        stringFieldDiff.isMergeConflict()
+
+        when: 'single array change in datatypes'
+        ArrayDiff dataTypeDiff = mergeDiff.find { it.fieldName == 'dataTypes' } as ArrayDiff
+
+        then:
+        dataTypeDiff.deleted.isEmpty()
+        dataTypeDiff.modified.isEmpty()
+        dataTypeDiff.created.size() == 1
+        dataTypeDiff.created.first().valueIdentifier == 'addSourceOnlyOnlyChangeInArray'
+        !dataTypeDiff.created.first().isMergeConflict()
+        !dataTypeDiff.created.first().commonAncestorValue
+
+        when: 'metadata has array diffs'
+        ArrayDiff metadataDiff = mergeDiff.find { it.fieldName == 'metadata' } as ArrayDiff
+
+        then:
+        metadataDiff.left.size() == 1
+        metadataDiff.right.size() == 2
+        metadataDiff.commonAncestorValue.size() == 2
+        metadataDiff.created.isEmpty()
+        metadataDiff.deleted.size() == 1
+        metadataDiff.deleted.first().valueIdentifier == 'test.deleteSourceOnly'
+        metadataDiff.modified.size() == 1
+        metadataDiff.modified.first().leftIdentifier == 'test.modifySourceOnly'
+        metadataDiff.modified.first().diffs.size() == 1
+        metadataDiff.modified.first().diffs.first().fieldName == 'value'
+        metadataDiff.modified.first().diffs.first().left == 'altered'
+        metadataDiff.modified.first().diffs.first().right == 'modifySourceOnly'
+        metadataDiff.modified.first().diffs.first().commonAncestorValue == 'modifySourceOnly'
+        !metadataDiff.modified.first().diffs.first().isMergeConflict()
+
+
+        and: 'array diffs on the dataclass list'
+        ArrayDiff dataClassesDiff = mergeDiff.diffs.find { it.fieldName == 'dataClasses' } as ArrayDiff
+        dataClassesDiff.created.size() == 3
+        dataClassesDiff.deleted.size() == 2
+        //        dataClassesDiff.modified.size() == 4
+
+        dataClassesDiff.created.value.label as Set == ['addSourceOnly', 'leftParentDataClass', 'modifyAndDelete'] as Set
+        !dataClassesDiff.created.find { it.value.label == 'addSourceOnly' }.isMergeConflict()
+        !dataClassesDiff.created.find { it.value.label == 'addSourceOnly' }.commonAncestorValue
+        !dataClassesDiff.created.find { it.value.label == 'leftParentDataClass' }.isMergeConflict()
+        !dataClassesDiff.created.find { it.value.label == 'leftParentDataClass' }.commonAncestorValue
+        dataClassesDiff.created.find { it.value.label == 'modifyAndDelete' }.isMergeConflict()
+        dataClassesDiff.created.find { it.value.label == 'modifyAndDelete' }.commonAncestorValue
+        dataClassesDiff.deleted.value.label as Set == ['deleteAndModify', 'deleteSourceOnly'] as Set
+        dataClassesDiff.deleted.find { it.value.label == 'deleteAndModify' }.isMergeConflict()
+        dataClassesDiff.deleted.find { it.value.label == 'deleteAndModify' }.commonAncestorValue
+        !dataClassesDiff.deleted.find { it.value.label == 'deleteSourceOnly' }.isMergeConflict()
+        !dataClassesDiff.deleted.find { it.value.label == 'deleteSourceOnly' }.commonAncestorValue
+        dataClassesDiff.modified.left.diffIdentifier as Set == ['existingClass', 'modifyAndModifyReturningDifference', 'modifySourceOnly',
                                                                 'addAndAddReturningDifference'] as Set
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'modifyAndModifyReturningDifference'}.diffs[0].fieldName == 'description'
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'modifyAndModifyReturningDifference'}.isMergeConflict
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'modifyAndModifyReturningDifference'}.commonAncestorValue
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'existingClass'}.diffs[0].fieldName == 'dataClasses'
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'existingClass'}.isMergeConflict
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'existingClass'}.commonAncestorValue
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'existingClass'}.diffs[0].created[0].value.label == 'addLeftToExistingClass'
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'existingClass'}.diffs[0].deleted[0].value.label ==
-        'deleteLeftOnlyFromExistingClass'
-        !dataClassesDiff.modified.find {it.left.diffIdentifier == 'existingClass'}.diffs[0].created[0].isMergeConflict
-        !dataClassesDiff.modified.find {it.left.diffIdentifier == 'existingClass'}.diffs[0].created[0].commonAncestorValue
-        !dataClassesDiff.modified.find {it.left.diffIdentifier == 'existingClass'}.diffs[0].deleted[0].isMergeConflict
-        !dataClassesDiff.modified.find {it.left.diffIdentifier == 'existingClass'}.diffs[0].deleted[0].commonAncestorValue
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'addAndAddReturningDifference'}.diffs[0].fieldName == 'description'
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'addAndAddReturningDifference'}.diffs[0].isMergeConflict
-        !dataClassesDiff.modified.find {it.left.diffIdentifier == 'addAndAddReturningDifference'}.diffs[0].commonAncestorValue
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'modifyLeftOnly'}.diffs[0].fieldName == 'description'
-        !dataClassesDiff.modified.find {it.left.diffIdentifier == 'modifyLeftOnly'}.diffs[0].isMergeConflict
-        !dataClassesDiff.modified.find {it.left.diffIdentifier == 'modifyLeftOnly'}.diffs[0].commonAncestorValue
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyAndModifyReturningDifference' }.diffs[0].fieldName == 'description'
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyAndModifyReturningDifference' }.isMergeConflict()
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyAndModifyReturningDifference' }.commonAncestorValue
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].fieldName == 'dataClasses'
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.isMergeConflict()
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.commonAncestorValue
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].created[0].value.label == 'addSourceToExistingClass'
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].deleted[0].value.label ==
+        'deleteSourceOnlyFromExistingClass'
+        !dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].created[0].isMergeConflict()
+        !dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].created[0].commonAncestorValue
+        !dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].deleted[0].isMergeConflict()
+        !dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].deleted[0].commonAncestorValue
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'addAndAddReturningDifference' }.diffs[0].fieldName == 'description'
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'addAndAddReturningDifference' }.diffs[0].isMergeConflict()
+        !dataClassesDiff.modified.find { it.left.diffIdentifier == 'addAndAddReturningDifference' }.diffs[0].commonAncestorValue
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifySourceOnly' }.diffs[0].fieldName == 'description'
+        !dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifySourceOnly' }.diffs[0].isMergeConflict()
+        !dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifySourceOnly' }.diffs[0].commonAncestorValue
     }
 
     void 'DMSM02 : test merging diff into draft model'() {
@@ -1181,47 +1254,47 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec {
         mergeDiff.diffs
         mergeDiff.numberOfDiffs == 12
         mergeDiff.diffs.fieldName as Set == ['branchName', 'dataClasses', 'description'] as Set
-        def branchNameDiff = mergeDiff.diffs.find {it.fieldName == 'branchName'}
+        def branchNameDiff = mergeDiff.diffs.find { it.fieldName == 'branchName' }
         branchNameDiff.left == VersionAwareConstraints.DEFAULT_BRANCH_NAME
         branchNameDiff.right == 'test'
         !branchNameDiff.isMergeConflict
-        def dataClassesDiff = mergeDiff.diffs.find {it.fieldName == 'dataClasses'}
+        def dataClassesDiff = mergeDiff.diffs.find { it.fieldName == 'dataClasses' }
         dataClassesDiff.created.size == 3
         dataClassesDiff.deleted.size == 2
         dataClassesDiff.modified.size == 4
         dataClassesDiff.created.value.label as Set == ['addLeftOnly', 'leftParentDataClass', 'modifyAndDelete'] as Set
-        !dataClassesDiff.created.find {it.value.label == 'addLeftOnly'}.isMergeConflict
-        !dataClassesDiff.created.find {it.value.label == 'addLeftOnly'}.commonAncestorValue
-        !dataClassesDiff.created.find {it.value.label == 'leftParentDataClass'}.isMergeConflict
-        !dataClassesDiff.created.find {it.value.label == 'leftParentDataClass'}.commonAncestorValue
-        dataClassesDiff.created.find {it.value.label == 'modifyAndDelete'}.isMergeConflict
-        dataClassesDiff.created.find {it.value.label == 'modifyAndDelete'}.commonAncestorValue
+        !dataClassesDiff.created.find { it.value.label == 'addLeftOnly' }.isMergeConflict
+        !dataClassesDiff.created.find { it.value.label == 'addLeftOnly' }.commonAncestorValue
+        !dataClassesDiff.created.find { it.value.label == 'leftParentDataClass' }.isMergeConflict
+        !dataClassesDiff.created.find { it.value.label == 'leftParentDataClass' }.commonAncestorValue
+        dataClassesDiff.created.find { it.value.label == 'modifyAndDelete' }.isMergeConflict
+        dataClassesDiff.created.find { it.value.label == 'modifyAndDelete' }.commonAncestorValue
         dataClassesDiff.deleted.value.label as Set == ['deleteAndModify', 'deleteLeftOnly'] as Set
-        dataClassesDiff.deleted.find {it.value.label == 'deleteAndModify'}.isMergeConflict
-        dataClassesDiff.deleted.find {it.value.label == 'deleteAndModify'}.commonAncestorValue
-        !dataClassesDiff.deleted.find {it.value.label == 'deleteLeftOnly'}.isMergeConflict
-        !dataClassesDiff.deleted.find {it.value.label == 'deleteLeftOnly'}.commonAncestorValue
+        dataClassesDiff.deleted.find { it.value.label == 'deleteAndModify' }.isMergeConflict
+        dataClassesDiff.deleted.find { it.value.label == 'deleteAndModify' }.commonAncestorValue
+        !dataClassesDiff.deleted.find { it.value.label == 'deleteLeftOnly' }.isMergeConflict
+        !dataClassesDiff.deleted.find { it.value.label == 'deleteLeftOnly' }.commonAncestorValue
         dataClassesDiff.modified.left.diffIdentifier as Set == ['existingClass', 'modifyAndModifyReturningDifference', 'modifyLeftOnly',
                                                                 'addAndAddReturningDifference'] as Set
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'modifyAndModifyReturningDifference'}.diffs[0].fieldName == 'description'
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'modifyAndModifyReturningDifference'}.isMergeConflict
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'modifyAndModifyReturningDifference'}.commonAncestorValue
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'existingClass'}.diffs[0].fieldName == 'dataClasses'
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'existingClass'}.isMergeConflict
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'existingClass'}.commonAncestorValue
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'existingClass'}.diffs[0].created[0].value.label == 'addLeftToExistingClass'
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'existingClass'}.diffs[0].deleted[0].value.label ==
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyAndModifyReturningDifference' }.diffs[0].fieldName == 'description'
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyAndModifyReturningDifference' }.isMergeConflict
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyAndModifyReturningDifference' }.commonAncestorValue
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].fieldName == 'dataClasses'
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.isMergeConflict
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.commonAncestorValue
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].created[0].value.label == 'addLeftToExistingClass'
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].deleted[0].value.label ==
         'deleteLeftOnlyFromExistingClass'
-        !dataClassesDiff.modified.find {it.left.diffIdentifier == 'existingClass'}.diffs[0].created[0].isMergeConflict
-        !dataClassesDiff.modified.find {it.left.diffIdentifier == 'existingClass'}.diffs[0].created[0].commonAncestorValue
-        !dataClassesDiff.modified.find {it.left.diffIdentifier == 'existingClass'}.diffs[0].deleted[0].isMergeConflict
-        !dataClassesDiff.modified.find {it.left.diffIdentifier == 'existingClass'}.diffs[0].deleted[0].commonAncestorValue
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'addAndAddReturningDifference'}.diffs[0].fieldName == 'description'
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'addAndAddReturningDifference'}.diffs[0].isMergeConflict
-        !dataClassesDiff.modified.find {it.left.diffIdentifier == 'addAndAddReturningDifference'}.diffs[0].commonAncestorValue
-        dataClassesDiff.modified.find {it.left.diffIdentifier == 'modifyLeftOnly'}.diffs[0].fieldName == 'description'
-        !dataClassesDiff.modified.find {it.left.diffIdentifier == 'modifyLeftOnly'}.diffs[0].isMergeConflict
-        !dataClassesDiff.modified.find {it.left.diffIdentifier == 'modifyLeftOnly'}.diffs[0].commonAncestorValue
+        !dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].created[0].isMergeConflict
+        !dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].created[0].commonAncestorValue
+        !dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].deleted[0].isMergeConflict
+        !dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].deleted[0].commonAncestorValue
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'addAndAddReturningDifference' }.diffs[0].fieldName == 'description'
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'addAndAddReturningDifference' }.diffs[0].isMergeConflict
+        !dataClassesDiff.modified.find { it.left.diffIdentifier == 'addAndAddReturningDifference' }.diffs[0].commonAncestorValue
+        dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyLeftOnly' }.diffs[0].fieldName == 'description'
+        !dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyLeftOnly' }.diffs[0].isMergeConflict
+        !dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyLeftOnly' }.diffs[0].commonAncestorValue
 
         when:
         DataClass addLeftOnly = dataClassService.findByDataModelIdAndLabel(testId, 'addLeftOnly')
@@ -1327,10 +1400,10 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec {
         mergedModel.dataClasses.label as Set == ['existingClass', 'modifyAndModifyReturningDifference', 'modifyLeftOnly', 'sdmclass',
                                                  'addAndAddReturningDifference', 'addLeftOnly', 'modifyAndDelete', 'addLeftToExistingClass',
                                                  'addRightToExistingClass'] as Set
-        mergedModel.dataClasses.find {it.label == 'existingClass'}.dataClasses.label as Set == ['addRightToExistingClass',
-                                                                                                'addLeftToExistingClass'] as Set
-        mergedModel.dataClasses.find {it.label == 'modifyAndModifyReturningDifference'}.description == 'DescriptionLeft'
-        mergedModel.dataClasses.find {it.label == 'modifyLeftOnly'}.description == 'Description'
+        mergedModel.dataClasses.find { it.label == 'existingClass' }.dataClasses.label as Set == ['addRightToExistingClass',
+                                                                                                  'addLeftToExistingClass'] as Set
+        mergedModel.dataClasses.find { it.label == 'modifyAndModifyReturningDifference' }.description == 'DescriptionLeft'
+        mergedModel.dataClasses.find { it.label == 'modifyLeftOnly' }.description == 'Description'
     }
 
 
@@ -1467,7 +1540,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec {
         invalid.errors.errorCount == 1
         invalid.errors.globalErrorCount == 0
         invalid.errors.fieldErrorCount == 1
-        invalid.errors.fieldErrors.any {it.field == 'dataClasses[0].label'}
+        invalid.errors.fieldErrors.any { it.field == 'dataClasses[0].label' }
 
         cleanup:
         GormUtils.outputDomainErrors(messageSource, invalid)
@@ -1589,9 +1662,9 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec {
         results.size() == 3
 
         when:
-        DataElementSimilarityResult childRes = results.find {it.source.label == 'child'}
-        DataElementSimilarityResult ele1Res = results.find {it.source.label == 'ele1'}
-        DataElementSimilarityResult ele2Res = results.find {it.source.label == 'element2'}
+        DataElementSimilarityResult childRes = results.find { it.source.label == 'child' }
+        DataElementSimilarityResult ele1Res = results.find { it.source.label == 'ele1' }
+        DataElementSimilarityResult ele2Res = results.find { it.source.label == 'element2' }
 
         then:
         ele1Res

From b023dd527f5ece8b38460bcb36438c6f4eca8950 Mon Sep 17 00:00:00 2001
From: Oliver Freeman 
Date: Thu, 17 Jun 2021 11:21:11 +0100
Subject: [PATCH 10/82] MergeDiff class partially rewritten

Preserving changes before starting anew with a simpler approach where we just use the 2 diffs between the common ancestor and the source and target
---
 .../core/diff/bidirectional/MergeDiff.groovy  | 348 ++++++++++++------
 1 file changed, 227 insertions(+), 121 deletions(-)

diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/MergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/MergeDiff.groovy
index 6a993f047d..7e2aa9b66d 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/MergeDiff.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/MergeDiff.groovy
@@ -2,177 +2,283 @@ package uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional
 
 
 import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable
+import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.CreationDiff
+import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.DeletionDiff
 
+import groovy.util.logging.Slf4j
+
+import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.arrayDiff
+import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.creationDiff
+import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.deletionDiff
+import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.fieldDiff
+import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.mergeDiff
 import static uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ArrayDiff.isArrayDiff
 import static uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.FieldDiff.isFieldDiff
 
+/**
+ * Holds the filtered result of 3 diffs which provides a unified diff of the changes which one object has made which another object has not made
+ * based off a common ancestor
+ *
+ * The final diff should be the intent of the changes to merge the LHS/source INTO the LHS/target
+ *
+ * 
+ *  LHS --- sourceTargetObjectDiff / top --->  RHS
+ *    ^                                     ^
+ *     \                                   /
+ *   caSourceObjectDiff / source     caTargetObjectDiff / target
+ *           \                           /
+ *            \                         /
+ *                  commonAncestor
+ * 
+ * + * + */ +@Slf4j class MergeDiff extends ObjectDiff { MergeDiff(Class targetClass) { super(targetClass) } + MergeDiff leftHandSide(M lhs) { + super.leftHandSide(lhs) as MergeDiff + } + + MergeDiff rightHandSide(M rhs) { + super.rightHandSide(rhs) as MergeDiff + } + + MergeDiff commonAncestor(M ca) { + super.commonAncestor(ca) as MergeDiff + } /** * Filters an ObjectDiff of two {@code Diffable}s based on the differences of each of the Diffables to their common ancestor. See MC-9228 * for details on filtering criteria. - * @param leftMergeDiff ObjectDiff between left Diffable and commonAncestor Diffable - * @param rightMergeDiff ObjectDiff between right Diffable and commonAncestor Diffable + * + * The resulting MergeDiff should display all the actual differences including merge conflicts with the intent of merging the LHS/source into the + * RHS/target. + * + * @param sourceTargetObjectDiff Object diff between LHS and RHS + * @param caSourceObjectDiff ObjectDiff between common ancestor and LHS + * @param caTargetObjectDiff ObjectDiff between common ancestor and RHS * @return this ObjectDiff with f */ @SuppressWarnings('GroovyVariableNotAssigned') - ObjectDiff mergeDiff(ObjectDiff leftMergeDiff, ObjectDiff rightMergeDiff) { - List existingDiffs = new ArrayList<>(this.diffs) + MergeDiff diff(ObjectDiff sourceTargetObjectDiff, ObjectDiff caSourceObjectDiff, ObjectDiff caTargetObjectDiff) { - List leftMergeDiffFieldNames = leftMergeDiff.diffs.fieldName - List rightMergeDiffFieldNames = rightMergeDiff.diffs.fieldName + this.diffs = sourceTargetObjectDiff.diffs.collect { sourceTargetDiff -> - this.diffs = existingDiffs.collect { diff -> + String diffFieldName = sourceTargetDiff.fieldName + FieldDiff caSourceFieldDiff = caSourceObjectDiff.find { it.fieldName == diffFieldName } + FieldDiff caTargetFieldDiff = caTargetObjectDiff.find { it.fieldName == diffFieldName } + /* + * Fieldname of a diff between the source and target will be either + * - a change between caSource AND caTarget : both sides made a change likely resulting in a merge conflict as there is now a diff + * - a change between caSource ONLY : only change is on the source side no change on the target side + * + * Because fields are hierarchical a diff on both sides does not indicate a definite merge conflict on arrays + */ - if (diff.fieldName in leftMergeDiffFieldNames && diff.fieldName in rightMergeDiffFieldNames) { - if (isArrayDiff(diff)) { - return updateArrayMergeDiffPresentOnBothSides(diff as ArrayDiff, - leftMergeDiff.diffs.find { it.fieldName == diff.fieldName } as ArrayDiff, - rightMergeDiff.diffs.find { it.fieldName == diff.fieldName } as ArrayDiff) + // If the fieldname is in the caSource and caTarget then there are changes to the field on both sides from the CA + // and those changes differ which should result in a merge conflict + if (caSourceFieldDiff && caTargetFieldDiff) { + if (isArrayDiff(sourceTargetDiff)) { + return createArrayMergeDiffPresentOnBothSides(sourceTargetDiff as ArrayDiff, + caSourceFieldDiff as ArrayDiff, + caTargetFieldDiff as ArrayDiff) } - if (isFieldDiff(diff)) { - return updateFieldMergeDiffPresentOnBothSides(diff, - rightMergeDiff.diffs.find { it.fieldName == diff.fieldName }) + if (isFieldDiff(sourceTargetDiff)) { + return createFieldMergeDiffPresentOnBothSides(sourceTargetDiff, caSourceFieldDiff.left) } + log.warn('Unhandled diff type {}', sourceTargetDiff.diffType) + return null } - // An entire Diffable type can not be present on the right, so there will be no merge conflicts - if (diff.fieldName in leftMergeDiffFieldNames) { - if (isArrayDiff(diff)) { - return updateArrayMergeDiffPresentOnOneSide(diff as ArrayDiff, - leftMergeDiff.diffs.find { it.fieldName == diff.fieldName } as ArrayDiff) + // If the field name is not in the caTarget then it is a diff added from the source side only + if (caSourceFieldDiff) { + if (isArrayDiff(sourceTargetDiff)) { + return createArrayMergeDiffPresentOnOneSide(sourceTargetDiff as ArrayDiff, + caSourceFieldDiff as ArrayDiff) } - if (isFieldDiff(diff)) { - return updateFieldMergeDiffPresentOnOneSide(diff as FieldDiff) + if (isFieldDiff(sourceTargetDiff)) { + return createFieldMergeDiffPresentOnOneSide(sourceTargetDiff, caSourceFieldDiff.left) } + log.warn('Unhandled diff type {}', sourceTargetDiff.diffType) + return null } - // If not in LHS then dont add + // If the field name is not in the source side then its not relevant wrt merging from target to source null }.findAll() // Strip null values this } - ArrayDiff updateArrayMergeDiffPresentOnBothSides(ArrayDiff arrayDiff, ArrayDiff leftArrayDiff, ArrayDiff rightArrayDiff) { - - arrayDiff.created = findAllCreatedMergeDiffs(arrayDiff.created, leftArrayDiff) - arrayDiff.deleted = findAllDeletedMergeDiffs(arrayDiff.deleted, leftArrayDiff, rightArrayDiff) - arrayDiff.modified = findAllModifiedMergeDiffs(arrayDiff.modified, leftArrayDiff, rightArrayDiff) - arrayDiff + static ArrayDiff createArrayMergeDiffPresentOnBothSides(ArrayDiff sourceTargetDiff, + ArrayDiff caSourceDiff, + ArrayDiff caTargetDiff) { + arrayDiff(sourceTargetDiff) + .commonAncestor(caSourceDiff.left) + .withCreatedDiffs(createCreationMergeDiffs(sourceTargetDiff.created, caSourceDiff, caTargetDiff)) + .withDeletedDiffs(findAllDeletedMergeDiffs(sourceTargetDiff.deleted, caSourceDiff, caTargetDiff)) + // .modified(findAllModifiedMergeDiffs(sourceTargetDiff.modified, caSourceDiff, caTargetDiff)) as ArrayDiff } - FieldDiff updateFieldMergeDiffPresentOnBothSides(FieldDiff diff, FieldDiff rightFieldDiff) { - diff.isMergeConflict = true - diff.commonAncestorValue = rightFieldDiff.left - diff - } - ArrayDiff updateArrayMergeDiffPresentOnOneSide(ArrayDiff arrayDiff, ArrayDiff leftArrayDiff) { - arrayDiff.deleted.each { it.isMergeConflict = false } - arrayDiff.created.each { it.isMergeConflict = false } + static ArrayDiff createArrayMergeDiffPresentOnOneSide(ArrayDiff sourceTargetDiff, ArrayDiff caSourceDiff) { - arrayDiff.modified.each { objDiff -> - def diffIdentifier = objDiff.right.diffIdentifier - def leftObjDiff = leftArrayDiff.modified.find { it.left.diffIdentifier == diffIdentifier } as ObjectDiff - // call recursively - objDiff = objDiff.mergeDiff(leftObjDiff, new ObjectDiff<>()) - objDiff.isMergeConflict = false + // Modified diffs represent diffs which have modifications down the chain + Collection modifiedMergeDiffs = sourceTargetDiff.modified.collect { objDiff -> + // Identify the relevant caSourceObjectDiff for this modified diff + ObjectDiff caSourceObjectDiff = caSourceDiff.modified.find { it.rightIdentifier == objDiff.leftIdentifier } + // call recursively with no diffs on the caTarget side + mergeDiff(objDiff, caSourceObjectDiff.right).diff(objDiff, caSourceObjectDiff, new ObjectDiff<>()) } - arrayDiff - } - FieldDiff updateFieldMergeDiffPresentOnOneSide(FieldDiff fieldDiff) { - fieldDiff.isMergeConflict = false - fieldDiff + // Created and Deleted diffs in this array are left as-is as they are guaranteed to be unique and no issue + arrayDiff(sourceTargetDiff) + .commonAncestor(caSourceDiff.left) + .withCreatedDiffs(sourceTargetDiff.created) + .withDeletedDiffs(sourceTargetDiff.deleted) + .modified(modifiedMergeDiffs) } - Collection findAllCreatedMergeDiffs(Collection created, ArrayDiff leftArrayDiff) { - created.collect { MergeWrapper wrapper -> - def diffIdentifier = wrapper.value.diffIdentifier - if (diffIdentifier in leftArrayDiff.created.value.diffIdentifier) { - // top created, left created - wrapper.isMergeConflict = false - return wrapper - } - if (diffIdentifier in leftArrayDiff.modified.left.diffIdentifier) { - // top created, left modified - wrapper.isMergeConflict = true - wrapper.commonAncestorValue = leftArrayDiff.left.find { it.diffIdentifier == diffIdentifier } - return wrapper - } - null - }.findAll() + static FieldDiff createFieldMergeDiffPresentOnOneSide(FieldDiff sourceTargetDiff, F commonAncestor) { + fieldDiff(sourceTargetDiff).commonAncestor(commonAncestor) } - Collection findAllDeletedMergeDiffs(Collection deleted, ArrayDiff leftArrayDiff, ArrayDiff rightArrayDiff) { - deleted.collect { MergeWrapper wrapper -> - def diffIdentifier = wrapper.value.diffIdentifier - if (diffIdentifier in rightArrayDiff.modified.left.diffIdentifier) { - // top deleted, right modified - wrapper.isMergeConflict = true - wrapper.commonAncestorValue = rightArrayDiff.left.find { it.diffIdentifier == diffIdentifier } - return wrapper - } else if (diffIdentifier in leftArrayDiff.deleted.value.diffIdentifier) { - // top deleted, right not modified, left deleted - wrapper.isMergeConflict = false - return wrapper - } - null - }.findAll() + static FieldDiff createFieldMergeDiffPresentOnBothSides(FieldDiff sourceTargetDiff, F commonAncestor) { + createFieldMergeDiffPresentOnOneSide(sourceTargetDiff, commonAncestor).asMergeConflict() } - Collection findAllModifiedMergeDiffs(Collection modified, ArrayDiff leftArrayDiff, ArrayDiff rightArrayDiff) { - modified.collect { ObjectDiff objDiff -> - def diffIdentifier = objDiff.right.diffIdentifier - if (diffIdentifier in leftArrayDiff.created.value.diffIdentifier) { - return updateModifiedObjectMergeDiffCreatedOnOneSide(objDiff) + /** + * Identify all the objects in the array field created on the LHS and flag all those which + * + * Important to note that due to the way diff works the same object cannot exist in more than one of created, deleted, modified in an ArrayDIff + */ + static Collection createCreationMergeDiffs(Collection> created, + ArrayDiff caSourceDiff, + ArrayDiff caTargetDiff) { + created.collect { diff -> + // top created, source created, target doesnt exist + if (diff.valueIdentifier in caSourceDiff.created*.valueIdentifier) { + return creationDiff(diff.targetClass).created(diff.value) } - - if (diffIdentifier in leftArrayDiff.modified.left.diffIdentifier) { - if (diffIdentifier in rightArrayDiff.modified.left.diffIdentifier) { - // top modified, left modified, right modified - return createModifiedObjectMergeDiffModifiedOnBothSides(objDiff, - leftArrayDiff.modified. - find { it.left.diffIdentifier == diffIdentifier } as ObjectDiff, - rightArrayDiff.modified. - find { it.left.diffIdentifier == diffIdentifier } as ObjectDiff, - rightArrayDiff.left.find { it.diffIdentifier == diffIdentifier } - ) - } - // top modified, left modified, right not modified - return updateModifiedObjectMergeDiffPresentOnOneSide(objDiff) + if (diff.valueIdentifier in caSourceDiff.deleted*.valueIdentifier) { + log.info('source/target created {} exists in ca/source deleted', diff.valueIdentifier) + } + if (diff.valueIdentifier in caSourceDiff.modified*.getRightIdentifier()) { + log.info('source/target created {} exists in ca/source modified', diff.valueIdentifier) + } + if (diff.valueIdentifier in caTargetDiff.created*.valueIdentifier) { + log.info('source/target created {} exists in ca/target created', diff.valueIdentifier) + } + if (diff.valueIdentifier in caTargetDiff.deleted*.valueIdentifier) { + log.info('source/target created {} exists in ca/target created', diff.valueIdentifier) + } + if (diff.valueIdentifier in caTargetDiff.modified*.getRightIdentifier()) { + log.info('source/target created {} exists in ca/target modified', diff.valueIdentifier) } null - }.findAll() + // if (diff.valueIdentifier in caSourceDiff.modified*.sourceIdentifier) { + // // top created, source modified + // return creationDiff(diff.targetClass) + // .created(diff.value) + // .commonAncestor(caSourceDiff.source.find { it.diffIdentifier == diff.valueIdentifier }) + // .asMergeConflict() + // } null + }.findAll() as Collection } - ObjectDiff updateModifiedObjectMergeDiffCreatedOnOneSide(ObjectDiff objectDiff) { - // top modified, right created, (left also created) - objectDiff.diffs.each { - it.isMergeConflict = true - it.commonAncestorValue = null - } - objectDiff.isMergeConflict = true - objectDiff.commonAncestorValue = null - return objectDiff - } + /** + * Identify all the objects in the array field deleted on the LHS and flag all those which + * + * Important to note that due to the way diff works the same object cannot exist in more than one of created, deleted, modified in an ArrayDIff + * + */ + static Collection findAllDeletedMergeDiffs(Collection> deleted, + ArrayDiff caSourceDiff, + ArrayDiff caTargetDiff) { + deleted.collect { diff -> - ObjectDiff createModifiedObjectMergeDiffModifiedOnBothSides(ObjectDiff objectDiff, ObjectDiff leftObjDiff, ObjectDiff rightObjDiff, - Object commonAncestorValue) { - // call recursively - ObjectDiff mergeDiff = objectDiff.mergeDiff(leftObjDiff, rightObjDiff) - mergeDiff.isMergeConflict = true - mergeDiff.commonAncestorValue = commonAncestorValue - return mergeDiff + if (diff.valueIdentifier in caSourceDiff.created*.valueIdentifier) { + log.info('source/target deleted {} exists in ca/source created', diff.valueIdentifier) + } + // top deleted, source deleted, target not modified + if (diff.valueIdentifier in caSourceDiff.deleted*.valueIdentifier) { + return deletionDiff(diff.targetClass).deleted(diff.value) + } + if (diff.valueIdentifier in caSourceDiff.modified*.getRightIdentifier()) { + log.info('source/target deleted {} exists in ca/source modified', diff.valueIdentifier) + } + if (diff.valueIdentifier in caTargetDiff.created*.valueIdentifier) { + log.info('source/target deleted {} exists in ca/target created', diff.valueIdentifier) + } + if (diff.valueIdentifier in caTargetDiff.deleted*.valueIdentifier) { + log.info('source/target deleted {} exists in ca/target created', diff.valueIdentifier) + } + if (diff.valueIdentifier in caTargetDiff.modified*.getRightIdentifier()) { + log.info('source/target deleted {} exists in ca/target modified', diff.valueIdentifier) + } + // if (diff.valueIdentifier in caTargetDiff.modified.source.diffIdentifier) { + // // top deleted, target modified + // return deletionDiff(diff.targetClass) + // .deleted(diff.value) + // .commonAncestor(caTargetDiff.source.find { it.diffIdentifier == diff.valueIdentifier }) + // .asMergeConflict() + // } + null + }.findAll() as Collection } - ObjectDiff updateModifiedObjectMergeDiffPresentOnOneSide(ObjectDiff objectDiff) { - objectDiff.diffs.each { it.isMergeConflict = false } - objectDiff.isMergeConflict = false - objectDiff - } + // static Collection findAllModifiedMergeDiffs(Collection modified, ArrayDiff sourceArrayDiff, ArrayDiff + // targetArrayDiff) { + // modified.collect { ObjectDiff objDiff -> + // def diffIdentifier = objDiff.target.diffIdentifier + // if (diffIdentifier in sourceArrayDiff.created.value.diffIdentifier) { return updateModifiedObjectMergeDiffCreatedOnOneSide + // (objDiff) } + // if + // (diffIdentifier in sourceArrayDiff.modified.source.diffIdentifier) { + // if (diffIdentifier in targetArrayDiff.modified.source.diffIdentifier) { + // // + // top modified, source modified , target modified + // return createModifiedObjectMergeDiffModifiedOnBothSides(objDiff, + // sourceArrayDiff.modified. + // find { it.source.diffIdentifier == diffIdentifier } as + // ObjectDiff, + // targetArrayDiff.modified. + // find { it.source.diffIdentifier == diffIdentifier } as + // ObjectDiff, + // targetArrayDiff.source.find { it.diffIdentifier == diffIdentifier }) + // } // top modified, source modified, target not modified + // return updateModifiedObjectMergeDiffPresentOnOneSide(objDiff) + // } null + // }.findAll() + // } + // static ObjectDiff updateModifiedObjectMergeDiffCreatedOnOneSide(ObjectDiff objectDiff) { + // // top modified, target created, (source also created) + // objectDiff.diffs.each { + // it.isMergeConflict = true + // it.commonAncestorValue = null + // } objectDiff . isMergeConflict = true + // objectDiff.commonAncestorValue = null + // return objectDiff + // } + // + // static ObjectDiff createModifiedObjectMergeDiffModifiedOnBothSides(ObjectDiff objectDiff, ObjectDiff sourceObjDiff, + // ObjectDiff targetObjDiff, + // Object commonAncestorValue) { + // // call recursively + // ObjectDiff mergeDiff = objectDiff.mergeDiff(sourceObjDiff, targetObjDiff) + // mergeDiff.isMergeConflict = true + // mergeDiff.commonAncestorValue = commonAncestorValue + // return mergeDiff + // } + // + // static ObjectDiff updateModifiedObjectMergeDiffPresentOnOneSide(ObjectDiff objectDiff) { + // objectDiff.diffs.each { + // it.isMergeConflict = false + // } + // objectDiff.isMergeConflict = false + // objectDiff + // } } From 5d3f7cc65f5bf0b0729a238e5ccce03b4901a52d Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Thu, 17 Jun 2021 12:01:05 +0100 Subject: [PATCH 11/82] rename commonancestorvalue --- .../views/fieldDiff/_fieldDiff.gson | 2 +- .../views/mergeWrapper/_mergeWrapper.gson | 2 +- .../maurodatamapper/core/diff/Diff.groovy | 4 +-- .../DataModelServiceIntegrationSpec.groovy | 30 +++++++++---------- .../TerminologyServiceIntegrationSpec.groovy | 2 +- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson b/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson index 295d30aba4..5965c19434 100644 --- a/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson +++ b/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson @@ -10,7 +10,7 @@ json("${fieldDiff.fieldName}") { if (fieldDiff.mergeConflict != null) { isMergeConflict fieldDiff.mergeConflict - if (fieldDiff.mergeConflict) commonAncestorValue fieldDiff.commonAncestorValue + if (fieldDiff.mergeConflict) commonAncestorValue fieldDiff.commonAncestor } } diff --git a/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson b/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson index b7129a4e50..43a0c9082e 100644 --- a/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson +++ b/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson @@ -9,6 +9,6 @@ json { value tmpl.'/diffable/diffable'(mergeWrapper.value) if (mergeWrapper.mergeConflict != null) { isMergeConflict mergeWrapper.mergeConflict - if (mergeWrapper.mergeConflict) commonAncestorValue tmpl.'/diffable/diffable'(mergeWrapper.commonAncestorValue) + if (mergeWrapper.mergeConflict) commonAncestorValue tmpl.'/diffable/diffable'(mergeWrapper.commonAncestor) } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diff.groovy index 25fcd00575..9d35bcfe8e 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diff.groovy @@ -24,7 +24,7 @@ abstract class Diff { T value Boolean mergeConflict - T commonAncestorValue + T commonAncestor Class targetClass protected Diff(Class targetClass) { @@ -59,7 +59,7 @@ abstract class Diff { } Diff commonAncestor(T ca) { - this.commonAncestorValue = ca + this.commonAncestor = ca this } diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy index 40bb0d1c67..1f7ead27a0 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy @@ -1067,7 +1067,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { then: stringFieldDiff.left == 'test' stringFieldDiff.right == VersionAwareConstraints.DEFAULT_BRANCH_NAME - stringFieldDiff.commonAncestorValue == VersionAwareConstraints.DEFAULT_BRANCH_NAME + stringFieldDiff.commonAncestor == VersionAwareConstraints.DEFAULT_BRANCH_NAME !stringFieldDiff.isMergeConflict() when: 'organisation is a non-conflicting change' @@ -1076,7 +1076,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { then: stringFieldDiff.left == 'under test' stringFieldDiff.right == null - stringFieldDiff.commonAncestorValue == null + stringFieldDiff.commonAncestor == null !stringFieldDiff.isMergeConflict() when: 'author is a conflicting change' @@ -1085,7 +1085,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { then: stringFieldDiff.left == 'harry' stringFieldDiff.right == 'dick' - stringFieldDiff.commonAncestorValue == 'john' + stringFieldDiff.commonAncestor == 'john' stringFieldDiff.isMergeConflict() when: 'single array change in datatypes' @@ -1097,7 +1097,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { dataTypeDiff.created.size() == 1 dataTypeDiff.created.first().valueIdentifier == 'addSourceOnlyOnlyChangeInArray' !dataTypeDiff.created.first().isMergeConflict() - !dataTypeDiff.created.first().commonAncestorValue + !dataTypeDiff.created.first().commonAncestor when: 'metadata has array diffs' ArrayDiff metadataDiff = mergeDiff.find { it.fieldName == 'metadata' } as ArrayDiff @@ -1105,7 +1105,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { then: metadataDiff.left.size() == 1 metadataDiff.right.size() == 2 - metadataDiff.commonAncestorValue.size() == 2 + metadataDiff.commonAncestor.size() == 2 metadataDiff.created.isEmpty() metadataDiff.deleted.size() == 1 metadataDiff.deleted.first().valueIdentifier == 'test.deleteSourceOnly' @@ -1115,7 +1115,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { metadataDiff.modified.first().diffs.first().fieldName == 'value' metadataDiff.modified.first().diffs.first().left == 'altered' metadataDiff.modified.first().diffs.first().right == 'modifySourceOnly' - metadataDiff.modified.first().diffs.first().commonAncestorValue == 'modifySourceOnly' + metadataDiff.modified.first().diffs.first().commonAncestor == 'modifySourceOnly' !metadataDiff.modified.first().diffs.first().isMergeConflict() @@ -1127,24 +1127,24 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { dataClassesDiff.created.value.label as Set == ['addSourceOnly', 'leftParentDataClass', 'modifyAndDelete'] as Set !dataClassesDiff.created.find { it.value.label == 'addSourceOnly' }.isMergeConflict() - !dataClassesDiff.created.find { it.value.label == 'addSourceOnly' }.commonAncestorValue + !dataClassesDiff.created.find { it.value.label == 'addSourceOnly' }.commonAncestor !dataClassesDiff.created.find { it.value.label == 'leftParentDataClass' }.isMergeConflict() - !dataClassesDiff.created.find { it.value.label == 'leftParentDataClass' }.commonAncestorValue + !dataClassesDiff.created.find { it.value.label == 'leftParentDataClass' }.commonAncestor dataClassesDiff.created.find { it.value.label == 'modifyAndDelete' }.isMergeConflict() - dataClassesDiff.created.find { it.value.label == 'modifyAndDelete' }.commonAncestorValue + dataClassesDiff.created.find { it.value.label == 'modifyAndDelete' }.commonAncestor dataClassesDiff.deleted.value.label as Set == ['deleteAndModify', 'deleteSourceOnly'] as Set dataClassesDiff.deleted.find { it.value.label == 'deleteAndModify' }.isMergeConflict() - dataClassesDiff.deleted.find { it.value.label == 'deleteAndModify' }.commonAncestorValue + dataClassesDiff.deleted.find { it.value.label == 'deleteAndModify' }.commonAncestor !dataClassesDiff.deleted.find { it.value.label == 'deleteSourceOnly' }.isMergeConflict() - !dataClassesDiff.deleted.find { it.value.label == 'deleteSourceOnly' }.commonAncestorValue + !dataClassesDiff.deleted.find { it.value.label == 'deleteSourceOnly' }.commonAncestor dataClassesDiff.modified.left.diffIdentifier as Set == ['existingClass', 'modifyAndModifyReturningDifference', 'modifySourceOnly', 'addAndAddReturningDifference'] as Set dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyAndModifyReturningDifference' }.diffs[0].fieldName == 'description' dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyAndModifyReturningDifference' }.isMergeConflict() - dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyAndModifyReturningDifference' }.commonAncestorValue + dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyAndModifyReturningDifference' }.commonAncestor dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].fieldName == 'dataClasses' dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.isMergeConflict() - dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.commonAncestorValue + dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.commonAncestor dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].created[0].value.label == 'addSourceToExistingClass' dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].deleted[0].value.label == 'deleteSourceOnlyFromExistingClass' @@ -1154,10 +1154,10 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { !dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].deleted[0].commonAncestorValue dataClassesDiff.modified.find { it.left.diffIdentifier == 'addAndAddReturningDifference' }.diffs[0].fieldName == 'description' dataClassesDiff.modified.find { it.left.diffIdentifier == 'addAndAddReturningDifference' }.diffs[0].isMergeConflict() - !dataClassesDiff.modified.find { it.left.diffIdentifier == 'addAndAddReturningDifference' }.diffs[0].commonAncestorValue + !dataClassesDiff.modified.find { it.left.diffIdentifier == 'addAndAddReturningDifference' }.diffs[0].commonAncestor dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifySourceOnly' }.diffs[0].fieldName == 'description' !dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifySourceOnly' }.diffs[0].isMergeConflict() - !dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifySourceOnly' }.diffs[0].commonAncestorValue + !dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifySourceOnly' }.diffs[0].commonAncestor } void 'DMSM02 : test merging diff into draft model'() { diff --git a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy index 71a983324d..693103e982 100644 --- a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy +++ b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy @@ -365,7 +365,7 @@ class TerminologyServiceIntegrationSpec extends BaseTerminologyIntegrationSpec { mergeDiff.diffs[0].left == 'right' mergeDiff.diffs[0].right == 'left' mergeDiff.diffs[0].isMergeConflict - mergeDiff.diffs[0].commonAncestorValue == VersionAwareConstraints.DEFAULT_BRANCH_NAME + mergeDiff.diffs[0].commonAncestor == VersionAwareConstraints.DEFAULT_BRANCH_NAME } } From 22699e8cb1dbdbb676463b9d603c1f671cad1858 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Mon, 21 Jun 2021 09:32:30 +0100 Subject: [PATCH 12/82] Tidy up the bidrectional and unidirectional diffs Pull out all the merge stuff as thats going into a new type of diff --- .../maurodatamapper/core/diff/Diff.groovy | 19 ++----------- .../core/diff/DiffBuilder.groovy | 27 +++---------------- .../core/diff/bidirectional/ArrayDiff.groovy | 24 +++++++---------- .../core/diff/bidirectional/FieldDiff.groovy | 17 ++---------- .../core/diff/bidirectional/ObjectDiff.groovy | 17 ++++++------ .../diff/unidirectional/CreationDiff.groovy | 15 +++++------ .../diff/unidirectional/DeletionDiff.groovy | 15 +++++------ 7 files changed, 39 insertions(+), 95 deletions(-) diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diff.groovy index 9d35bcfe8e..c56f3de7cc 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diff.groovy @@ -23,13 +23,12 @@ import groovy.transform.CompileStatic abstract class Diff { T value - Boolean mergeConflict - T commonAncestor + Class targetClass protected Diff(Class targetClass) { this.targetClass = targetClass - mergeConflict = false + } abstract Integer getNumberOfDiffs() @@ -53,18 +52,4 @@ abstract class Diff { boolean objectsAreIdentical() { !getNumberOfDiffs() } - - Boolean isMergeConflict() { - mergeConflict - } - - Diff commonAncestor(T ca) { - this.commonAncestor = ca - this - } - - Diff asMergeConflict() { - this.mergeConflict = true - this - } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy index 04b054cf75..41c51b4a2a 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy @@ -2,8 +2,8 @@ package uk.ac.ox.softeng.maurodatamapper.core.diff import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ArrayDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.FieldDiff -import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.MergeDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.CreationDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.DeletionDiff @@ -12,28 +12,14 @@ import groovy.transform.CompileStatic @CompileStatic class DiffBuilder { - static ArrayDiff arrayDiff(Class arrayClass) { - new ArrayDiff(arrayClass as Class>) - } - - static ArrayDiff arrayDiff(ArrayDiff original) { - arrayDiff(original.targetClass.arrayType() as Class) - .fieldName(original.fieldName) - .leftHandSide(original.left) - .rightHandSide(original.right) + static ArrayDiff arrayDiff(Class> arrayClass) { + new ArrayDiff(arrayClass) } static FieldDiff fieldDiff(Class fieldClass) { new FieldDiff(fieldClass) } - static FieldDiff fieldDiff(FieldDiff original) { - fieldDiff(original.targetClass) - .fieldName(original.fieldName) - .leftHandSide(original.left) - .rightHandSide(original.right) - } - static ObjectDiff objectDiff(Class objectClass) { new ObjectDiff(objectClass) } @@ -42,13 +28,6 @@ class DiffBuilder { new MergeDiff(objectClass) } - static MergeDiff mergeDiff(ObjectDiff objectDiff, K commonAncestor) { - mergeDiff(objectDiff.targetClass) - .leftHandSide(objectDiff.left) - .rightHandSide(objectDiff.right) - .commonAncestor(commonAncestor) - } - static CreationDiff creationDiff(Class objectClass) { new CreationDiff(objectClass) } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ArrayDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ArrayDiff.groovy index 0781b4de2a..82d2b74ce0 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ArrayDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ArrayDiff.groovy @@ -17,11 +17,13 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional -import uk.ac.ox.softeng.maurodatamapper.core.diff.Diff + import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.CreationDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.DeletionDiff +import groovy.transform.CompileStatic + import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.creationDiff import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.deletionDiff @@ -29,30 +31,31 @@ import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.deletionDif * Note the same object cannot exist in more than one of created, deleted, modified. * These collections are mutually exclusive */ +@CompileStatic class ArrayDiff extends FieldDiff> { Collection> created Collection> deleted Collection> modified - ArrayDiff(Class> targetClass) { - super(targetClass) + ArrayDiff(Class> targetArrayClass) { + super(targetArrayClass) created = [] deleted = [] modified = [] } - ArrayDiff created(Collection created) { + ArrayDiff createdObjects(Collection created) { this.created = created.collect { creationDiff(it.class as Class).created(it) } this } - ArrayDiff deleted(Collection deleted) { + ArrayDiff deletedObjects(Collection deleted) { this.deleted = deleted.collect { deletionDiff(it.class as Class).deleted(it) } this } - ArrayDiff modified(Collection> modified) { + ArrayDiff withModifiedDiffs(Collection> modified) { this.modified = modified this } @@ -82,11 +85,6 @@ class ArrayDiff extends FieldDiff> { super.rightHandSide(rhs) as ArrayDiff } - @Override - ArrayDiff commonAncestor(Collection ca) { - super.commonAncestor(ca) as ArrayDiff - } - @Override Integer getNumberOfDiffs() { created.size() + deleted.size() + ((modified.sum { it.getNumberOfDiffs() } ?: 0) as Integer) @@ -107,8 +105,4 @@ class ArrayDiff extends FieldDiff> { } stringBuilder.toString() } - - static boolean isArrayDiff(Diff diff) { - diff.diffType == ArrayDiff.simpleName - } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/FieldDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/FieldDiff.groovy index a63e1e081a..aca74ed68b 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/FieldDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/FieldDiff.groovy @@ -17,8 +17,9 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional -import uk.ac.ox.softeng.maurodatamapper.core.diff.Diff +import groovy.transform.CompileStatic +@CompileStatic class FieldDiff extends BiDirectionalDiff { String fieldName @@ -60,22 +61,8 @@ class FieldDiff extends BiDirectionalDiff { super.rightHandSide(rhs) as FieldDiff } - @Override - FieldDiff commonAncestor(F ca) { - super.commonAncestor(ca) as FieldDiff - } - - @Override - FieldDiff asMergeConflict() { - super.asMergeConflict() as FieldDiff - } - @Override String toString() { "${fieldName} :: ${left?.toString()} <> ${right?.toString()}" } - - static boolean isFieldDiff(Diff diff) { - diff.diffType == FieldDiff.simpleName - } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy index ef2c2899fd..066030a65a 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy @@ -38,7 +38,6 @@ class ObjectDiff extends BiDirectionalDiff { String leftIdentifier String rightIdentifier - String path ObjectDiff(Class targetClass) { super(targetClass) @@ -118,20 +117,22 @@ class ObjectDiff extends BiDirectionalDiff { // If no lhs or rhs then nothing to compare if (!lhs && !rhs) return this - ArrayDiff diff = arrayDiff(diffableClass) + List diffableList = [] + + ArrayDiff diff = arrayDiff(diffableList.class) .fieldName(fieldName) .leftHandSide(lhs) - .rightHandSide(rhs) + .rightHandSide(rhs) as ArrayDiff // If no lhs then all rhs have been created/added if (!lhs) { - return append(diff.created(rhs)) + return append(diff.createdObjects(rhs)) } // If no rhs then all lhs have been deleted/removed if (!rhs) { - return append(diff.deleted(lhs)) + return append(diff.deletedObjects(lhs)) } Collection deleted = [] @@ -161,9 +162,9 @@ class ObjectDiff extends BiDirectionalDiff { } if (created || deleted || modified) { - append(diff.created(created) - .deleted(deleted) - .modified(modified)) + append(diff.createdObjects(created) + .deletedObjects(deleted) + .withModifiedDiffs(modified)) } this } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/CreationDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/CreationDiff.groovy index 4b3ce59605..65586df513 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/CreationDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/CreationDiff.groovy @@ -11,17 +11,16 @@ class CreationDiff extends UniDirectionalDiff { super(targetClass) } - CreationDiff created(C object) { - this.value = object - this + C getCreated() { + super.getValue() as C } - CreationDiff commonAncestor(C ca) { - super.commonAncestor(ca) as CreationDiff + String getCreatedIdentifier() { + value.diffIdentifier } - @Override - CreationDiff asMergeConflict() { - super.asMergeConflict() as CreationDiff + CreationDiff created(C object) { + this.value = object + this } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/DeletionDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/DeletionDiff.groovy index 8b4c501c2d..7690ae5790 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/DeletionDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/DeletionDiff.groovy @@ -11,17 +11,16 @@ class DeletionDiff extends UniDirectionalDiff { super(targetClass) } - DeletionDiff deleted(D object) { - this.value = object - this + D getDeleted() { + super.getValue() as D } - DeletionDiff commonAncestor(D ca) { - super.commonAncestor(ca) as DeletionDiff + String getDeletedIdentifier() { + value.diffIdentifier } - @Override - DeletionDiff asMergeConflict() { - super.asMergeConflict() as DeletionDiff + DeletionDiff deleted(D object) { + this.value = object + this } } From fb3239a68b3073443e20b598cd4f725dae5a92af Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Mon, 21 Jun 2021 09:33:05 +0100 Subject: [PATCH 13/82] Remove dataelements from the list of diffs in a datamodel These are included in the dataclass, where they actually belong so this is an unnecessary diff level --- .../uk/ac/ox/softeng/maurodatamapper/datamodel/DataModel.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModel.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModel.groovy index ba9fa8612a..bbbd9b4796 100644 --- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModel.groovy +++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModel.groovy @@ -189,7 +189,6 @@ class DataModel implements Model, SummaryMetadataAware, IndexedSiblin modelDiffBuilder(DataModel, this, otherDataModel) .appendList(DataType, 'dataTypes', this.getDataTypes(), otherDataModel.getDataTypes()) .appendList(DataClass, 'dataClasses', this.childDataClasses, otherDataModel.childDataClasses) - .appendList(DataElement, 'dataElements', this.getAllDataElements(), otherDataModel.getAllDataElements()) } def beforeValidate() { From ecf3a79aa2cf57ac875dd53dc3a7f7a1f58a02db Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Mon, 21 Jun 2021 14:22:17 +0100 Subject: [PATCH 14/82] More wip --- .../views/fieldDiff/_fieldDiff.gson | 8 +- .../views/mergeWrapper/_mergeWrapper.gson | 8 +- .../core/diff/DiffBuilder.groovy | 20 + .../core/diff/bidirectional/MergeDiff.groovy | 284 --------------- .../diff/tridirectional/ArrayMergeDiff.groovy | 101 ++++++ .../tridirectional/CreationMergeDiff.groovy | 63 ++++ .../tridirectional/DeletionMergeDiff.groovy | 64 ++++ .../diff/tridirectional/FieldMergeDiff.groovy | 78 ++++ .../core/diff/tridirectional/MergeDiff.groovy | 341 ++++++++++++++++++ .../tridirectional/TriDirectionalDiff.groovy | 106 ++++++ .../core/model/ModelService.groovy | 28 +- .../DataModelServiceIntegrationSpec.groovy | 73 ++-- 12 files changed, 839 insertions(+), 335 deletions(-) delete mode 100644 mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/MergeDiff.groovy create mode 100644 mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy create mode 100644 mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy create mode 100644 mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy create mode 100644 mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy create mode 100644 mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy create mode 100644 mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy diff --git a/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson b/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson index 5965c19434..7c841eb15a 100644 --- a/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson +++ b/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson @@ -8,9 +8,9 @@ json("${fieldDiff.fieldName}") { left fieldDiff.getLeft() right fieldDiff.getRight() - if (fieldDiff.mergeConflict != null) { - isMergeConflict fieldDiff.mergeConflict - if (fieldDiff.mergeConflict) commonAncestorValue fieldDiff.commonAncestor - } + // if (fieldDiff.mergeConflict != null) { + // isMergeConflict fieldDiff.mergeConflict + // if (fieldDiff.mergeConflict) commonAncestorValue fieldDiff.commonAncestor + // } } diff --git a/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson b/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson index 43a0c9082e..83d4bd7aa9 100644 --- a/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson +++ b/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson @@ -7,8 +7,8 @@ model { json { value tmpl.'/diffable/diffable'(mergeWrapper.value) - if (mergeWrapper.mergeConflict != null) { - isMergeConflict mergeWrapper.mergeConflict - if (mergeWrapper.mergeConflict) commonAncestorValue tmpl.'/diffable/diffable'(mergeWrapper.commonAncestor) - } + // if (mergeWrapper.mergeConflict != null) { + // isMergeConflict mergeWrapper.mergeConflict + // if (mergeWrapper.mergeConflict) commonAncestorValue tmpl.'/diffable/diffable'(mergeWrapper.commonAncestor) + // } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy index 41c51b4a2a..802e22fab6 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy @@ -3,6 +3,10 @@ package uk.ac.ox.softeng.maurodatamapper.core.diff import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ArrayDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.FieldDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.ArrayMergeDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.CreationMergeDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.DeletionMergeDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.FieldMergeDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.CreationDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.DeletionDiff @@ -35,4 +39,20 @@ class DiffBuilder { static DeletionDiff deletionDiff(Class objectClass) { new DeletionDiff(objectClass) } + + static ArrayMergeDiff arrayMergeDiff(Class> arrayClass) { + new ArrayMergeDiff(arrayClass) + } + + static FieldMergeDiff fieldMergeDiff(Class fieldClass) { + new FieldMergeDiff(fieldClass) + } + + static CreationMergeDiff creationMergeDiff(Class objectClass) { + new CreationMergeDiff(objectClass) + } + + static DeletionMergeDiff deletionMergeDiff(Class objectClass) { + new DeletionMergeDiff(objectClass) + } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/MergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/MergeDiff.groovy deleted file mode 100644 index 7e2aa9b66d..0000000000 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/MergeDiff.groovy +++ /dev/null @@ -1,284 +0,0 @@ -package uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional - - -import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable -import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.CreationDiff -import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.DeletionDiff - -import groovy.util.logging.Slf4j - -import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.arrayDiff -import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.creationDiff -import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.deletionDiff -import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.fieldDiff -import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.mergeDiff -import static uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ArrayDiff.isArrayDiff -import static uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.FieldDiff.isFieldDiff - -/** - * Holds the filtered result of 3 diffs which provides a unified diff of the changes which one object has made which another object has not made - * based off a common ancestor - * - * The final diff should be the intent of the changes to merge the LHS/source INTO the LHS/target - * - *
- *  LHS --- sourceTargetObjectDiff / top --->  RHS
- *    ^                                     ^
- *     \                                   /
- *   caSourceObjectDiff / source     caTargetObjectDiff / target
- *           \                           /
- *            \                         /
- *                  commonAncestor
- * 
- * - * - */ -@Slf4j -class MergeDiff extends ObjectDiff { - - MergeDiff(Class targetClass) { - super(targetClass) - } - - MergeDiff leftHandSide(M lhs) { - super.leftHandSide(lhs) as MergeDiff - } - - MergeDiff rightHandSide(M rhs) { - super.rightHandSide(rhs) as MergeDiff - } - - MergeDiff commonAncestor(M ca) { - super.commonAncestor(ca) as MergeDiff - } - - /** - * Filters an ObjectDiff of two {@code Diffable}s based on the differences of each of the Diffables to their common ancestor. See MC-9228 - * for details on filtering criteria. - * - * The resulting MergeDiff should display all the actual differences including merge conflicts with the intent of merging the LHS/source into the - * RHS/target. - * - * @param sourceTargetObjectDiff Object diff between LHS and RHS - * @param caSourceObjectDiff ObjectDiff between common ancestor and LHS - * @param caTargetObjectDiff ObjectDiff between common ancestor and RHS - * @return this ObjectDiff with f - */ - @SuppressWarnings('GroovyVariableNotAssigned') - MergeDiff diff(ObjectDiff sourceTargetObjectDiff, ObjectDiff caSourceObjectDiff, ObjectDiff caTargetObjectDiff) { - - this.diffs = sourceTargetObjectDiff.diffs.collect { sourceTargetDiff -> - - String diffFieldName = sourceTargetDiff.fieldName - FieldDiff caSourceFieldDiff = caSourceObjectDiff.find { it.fieldName == diffFieldName } - FieldDiff caTargetFieldDiff = caTargetObjectDiff.find { it.fieldName == diffFieldName } - /* - * Fieldname of a diff between the source and target will be either - * - a change between caSource AND caTarget : both sides made a change likely resulting in a merge conflict as there is now a diff - * - a change between caSource ONLY : only change is on the source side no change on the target side - * - * Because fields are hierarchical a diff on both sides does not indicate a definite merge conflict on arrays - */ - - // If the fieldname is in the caSource and caTarget then there are changes to the field on both sides from the CA - // and those changes differ which should result in a merge conflict - if (caSourceFieldDiff && caTargetFieldDiff) { - if (isArrayDiff(sourceTargetDiff)) { - return createArrayMergeDiffPresentOnBothSides(sourceTargetDiff as ArrayDiff, - caSourceFieldDiff as ArrayDiff, - caTargetFieldDiff as ArrayDiff) - } - if (isFieldDiff(sourceTargetDiff)) { - return createFieldMergeDiffPresentOnBothSides(sourceTargetDiff, caSourceFieldDiff.left) - } - log.warn('Unhandled diff type {}', sourceTargetDiff.diffType) - return null - } - - // If the field name is not in the caTarget then it is a diff added from the source side only - if (caSourceFieldDiff) { - if (isArrayDiff(sourceTargetDiff)) { - return createArrayMergeDiffPresentOnOneSide(sourceTargetDiff as ArrayDiff, - caSourceFieldDiff as ArrayDiff) - } - if (isFieldDiff(sourceTargetDiff)) { - return createFieldMergeDiffPresentOnOneSide(sourceTargetDiff, caSourceFieldDiff.left) - } - log.warn('Unhandled diff type {}', sourceTargetDiff.diffType) - return null - } - // If the field name is not in the source side then its not relevant wrt merging from target to source - null - }.findAll() // Strip null values - this - } - - static
ArrayDiff createArrayMergeDiffPresentOnBothSides(ArrayDiff sourceTargetDiff, - ArrayDiff caSourceDiff, - ArrayDiff caTargetDiff) { - arrayDiff(sourceTargetDiff) - .commonAncestor(caSourceDiff.left) - .withCreatedDiffs(createCreationMergeDiffs(sourceTargetDiff.created, caSourceDiff, caTargetDiff)) - .withDeletedDiffs(findAllDeletedMergeDiffs(sourceTargetDiff.deleted, caSourceDiff, caTargetDiff)) - // .modified(findAllModifiedMergeDiffs(sourceTargetDiff.modified, caSourceDiff, caTargetDiff)) as ArrayDiff - } - - - static ArrayDiff createArrayMergeDiffPresentOnOneSide(ArrayDiff sourceTargetDiff, ArrayDiff caSourceDiff) { - - // Modified diffs represent diffs which have modifications down the chain - Collection modifiedMergeDiffs = sourceTargetDiff.modified.collect { objDiff -> - // Identify the relevant caSourceObjectDiff for this modified diff - ObjectDiff caSourceObjectDiff = caSourceDiff.modified.find { it.rightIdentifier == objDiff.leftIdentifier } - // call recursively with no diffs on the caTarget side - mergeDiff(objDiff, caSourceObjectDiff.right).diff(objDiff, caSourceObjectDiff, new ObjectDiff<>()) - } - - // Created and Deleted diffs in this array are left as-is as they are guaranteed to be unique and no issue - arrayDiff(sourceTargetDiff) - .commonAncestor(caSourceDiff.left) - .withCreatedDiffs(sourceTargetDiff.created) - .withDeletedDiffs(sourceTargetDiff.deleted) - .modified(modifiedMergeDiffs) - } - - static FieldDiff createFieldMergeDiffPresentOnOneSide(FieldDiff sourceTargetDiff, F commonAncestor) { - fieldDiff(sourceTargetDiff).commonAncestor(commonAncestor) - } - - static FieldDiff createFieldMergeDiffPresentOnBothSides(FieldDiff sourceTargetDiff, F commonAncestor) { - createFieldMergeDiffPresentOnOneSide(sourceTargetDiff, commonAncestor).asMergeConflict() - } - - /** - * Identify all the objects in the array field created on the LHS and flag all those which - * - * Important to note that due to the way diff works the same object cannot exist in more than one of created, deleted, modified in an ArrayDIff - */ - static Collection createCreationMergeDiffs(Collection> created, - ArrayDiff caSourceDiff, - ArrayDiff caTargetDiff) { - created.collect { diff -> - // top created, source created, target doesnt exist - if (diff.valueIdentifier in caSourceDiff.created*.valueIdentifier) { - return creationDiff(diff.targetClass).created(diff.value) - } - if (diff.valueIdentifier in caSourceDiff.deleted*.valueIdentifier) { - log.info('source/target created {} exists in ca/source deleted', diff.valueIdentifier) - } - if (diff.valueIdentifier in caSourceDiff.modified*.getRightIdentifier()) { - log.info('source/target created {} exists in ca/source modified', diff.valueIdentifier) - } - if (diff.valueIdentifier in caTargetDiff.created*.valueIdentifier) { - log.info('source/target created {} exists in ca/target created', diff.valueIdentifier) - } - if (diff.valueIdentifier in caTargetDiff.deleted*.valueIdentifier) { - log.info('source/target created {} exists in ca/target created', diff.valueIdentifier) - } - if (diff.valueIdentifier in caTargetDiff.modified*.getRightIdentifier()) { - log.info('source/target created {} exists in ca/target modified', diff.valueIdentifier) - } - null - // if (diff.valueIdentifier in caSourceDiff.modified*.sourceIdentifier) { - // // top created, source modified - // return creationDiff(diff.targetClass) - // .created(diff.value) - // .commonAncestor(caSourceDiff.source.find { it.diffIdentifier == diff.valueIdentifier }) - // .asMergeConflict() - // } null - }.findAll() as Collection - } - - /** - * Identify all the objects in the array field deleted on the LHS and flag all those which - * - * Important to note that due to the way diff works the same object cannot exist in more than one of created, deleted, modified in an ArrayDIff - * - */ - static Collection findAllDeletedMergeDiffs(Collection> deleted, - ArrayDiff caSourceDiff, - ArrayDiff caTargetDiff) { - deleted.collect { diff -> - - if (diff.valueIdentifier in caSourceDiff.created*.valueIdentifier) { - log.info('source/target deleted {} exists in ca/source created', diff.valueIdentifier) - } - // top deleted, source deleted, target not modified - if (diff.valueIdentifier in caSourceDiff.deleted*.valueIdentifier) { - return deletionDiff(diff.targetClass).deleted(diff.value) - } - if (diff.valueIdentifier in caSourceDiff.modified*.getRightIdentifier()) { - log.info('source/target deleted {} exists in ca/source modified', diff.valueIdentifier) - } - if (diff.valueIdentifier in caTargetDiff.created*.valueIdentifier) { - log.info('source/target deleted {} exists in ca/target created', diff.valueIdentifier) - } - if (diff.valueIdentifier in caTargetDiff.deleted*.valueIdentifier) { - log.info('source/target deleted {} exists in ca/target created', diff.valueIdentifier) - } - if (diff.valueIdentifier in caTargetDiff.modified*.getRightIdentifier()) { - log.info('source/target deleted {} exists in ca/target modified', diff.valueIdentifier) - } - // if (diff.valueIdentifier in caTargetDiff.modified.source.diffIdentifier) { - // // top deleted, target modified - // return deletionDiff(diff.targetClass) - // .deleted(diff.value) - // .commonAncestor(caTargetDiff.source.find { it.diffIdentifier == diff.valueIdentifier }) - // .asMergeConflict() - // } - null - }.findAll() as Collection - } - - // static Collection findAllModifiedMergeDiffs(Collection modified, ArrayDiff sourceArrayDiff, ArrayDiff - // targetArrayDiff) { - // modified.collect { ObjectDiff objDiff -> - // def diffIdentifier = objDiff.target.diffIdentifier - // if (diffIdentifier in sourceArrayDiff.created.value.diffIdentifier) { return updateModifiedObjectMergeDiffCreatedOnOneSide - // (objDiff) } - // if - // (diffIdentifier in sourceArrayDiff.modified.source.diffIdentifier) { - // if (diffIdentifier in targetArrayDiff.modified.source.diffIdentifier) { - // // - // top modified, source modified , target modified - // return createModifiedObjectMergeDiffModifiedOnBothSides(objDiff, - // sourceArrayDiff.modified. - // find { it.source.diffIdentifier == diffIdentifier } as - // ObjectDiff, - // targetArrayDiff.modified. - // find { it.source.diffIdentifier == diffIdentifier } as - // ObjectDiff, - // targetArrayDiff.source.find { it.diffIdentifier == diffIdentifier }) - // } // top modified, source modified, target not modified - // return updateModifiedObjectMergeDiffPresentOnOneSide(objDiff) - // } null - // }.findAll() - // } - // static ObjectDiff updateModifiedObjectMergeDiffCreatedOnOneSide(ObjectDiff objectDiff) { - // // top modified, target created, (source also created) - // objectDiff.diffs.each { - // it.isMergeConflict = true - // it.commonAncestorValue = null - // } objectDiff . isMergeConflict = true - // objectDiff.commonAncestorValue = null - // return objectDiff - // } - // - // static ObjectDiff createModifiedObjectMergeDiffModifiedOnBothSides(ObjectDiff objectDiff, ObjectDiff sourceObjDiff, - // ObjectDiff targetObjDiff, - // Object commonAncestorValue) { - // // call recursively - // ObjectDiff mergeDiff = objectDiff.mergeDiff(sourceObjDiff, targetObjDiff) - // mergeDiff.isMergeConflict = true - // mergeDiff.commonAncestorValue = commonAncestorValue - // return mergeDiff - // } - // - // static ObjectDiff updateModifiedObjectMergeDiffPresentOnOneSide(ObjectDiff objectDiff) { - // objectDiff.diffs.each { - // it.isMergeConflict = false - // } - // objectDiff.isMergeConflict = false - // objectDiff - // } -} diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy new file mode 100644 index 0000000000..76aeb6f683 --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy @@ -0,0 +1,101 @@ +/* + * 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.diff.tridirectional + +import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable + +import groovy.transform.CompileStatic + +/** + * Note the same object cannot exist in more than one of created, deleted, modified. + * These collections are mutually exclusive + */ +@CompileStatic +class ArrayMergeDiff extends FieldMergeDiff> { + + Collection> created + Collection> deleted + Collection> modified + + ArrayMergeDiff(Class> targetArrayClass) { + super(targetArrayClass) + created = [] + deleted = [] + modified = [] + } + + ArrayMergeDiff withModifiedMergeDiffs(Collection> modified) { + this.modified = modified + this + } + + ArrayMergeDiff withCreatedMergeDiffs(Collection> created) { + this.created = created + this + } + + ArrayMergeDiff withDeletedMergeDiffs(Collection> deleted) { + this.deleted = deleted + this + } + + @Override + ArrayMergeDiff forFieldName(String fieldName) { + super.forFieldName(fieldName) as ArrayMergeDiff + } + + @Override + ArrayMergeDiff withSource(Collection lhs) { + super.withSource(lhs) as ArrayMergeDiff + } + + @Override + ArrayMergeDiff withTarget(Collection rhs) { + super.withTarget(rhs) as ArrayMergeDiff + } + + @Override + ArrayMergeDiff withCommonAncestor(Collection ca) { + super.withCommonAncestor(ca) as ArrayMergeDiff + } + + ArrayMergeDiff asMergeConflict() { + super.asMergeConflict() as ArrayMergeDiff + } + + @Override + Integer getNumberOfDiffs() { + created.size() + deleted.size() + ((modified.sum { it.getNumberOfDiffs() } ?: 0) as Integer) + } + + @Override + String toString() { + StringBuilder stringBuilder = new StringBuilder(super.toString()) + + if (created) { + stringBuilder.append('\n Created ::\n').append(created) + } + if (deleted) { + stringBuilder.append('\n Deleted ::\n').append(deleted) + } + if (modified) { + stringBuilder.append('\n Modified ::\n').append(modified) + } + stringBuilder.toString() + } +} diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy new file mode 100644 index 0000000000..64ff8e491d --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy @@ -0,0 +1,63 @@ +package uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional + +import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable + +import groovy.transform.CompileStatic + +@CompileStatic +class CreationMergeDiff extends TriDirectionalDiff { + + CreationMergeDiff(Class targetClass) { + super(targetClass) + } + + @Override + Integer getNumberOfDiffs() { + 1 + } + + C getCreated() { + super.getValue() as C + } + + String getCreatedIdentifier() { + created.diffIdentifier + } + + @SuppressWarnings('GrDeprecatedAPIUsage') + CreationMergeDiff whichCreated(C object) { + withSource(object) as CreationMergeDiff + } + + CreationMergeDiff withCommonAncestor(C ca) { + super.withCommonAncestor(ca) as CreationMergeDiff + } + + CreationMergeDiff withMergeDeletion(C deleted) { + super.rightHandSide(deleted) as CreationMergeDiff + } + + CreationMergeDiff withNoMergeDeletion() { + this + } + + @Override + CreationMergeDiff asMergeConflict() { + super.asMergeConflict() as CreationMergeDiff + } + + @Deprecated + CreationMergeDiff withSource(C source) { + super.withSource(source) as CreationMergeDiff + } + + @Deprecated + CreationMergeDiff withTarget(C target) { + super.withTarget(target) as CreationMergeDiff + } + + @Override + String toString() { + "Created :: ${createdIdentifier}" + } +} diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy new file mode 100644 index 0000000000..fc12332427 --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy @@ -0,0 +1,64 @@ +package uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional + +import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable + +import groovy.transform.CompileStatic + +@CompileStatic +class DeletionMergeDiff extends TriDirectionalDiff { + + DeletionMergeDiff(Class targetClass) { + super(targetClass) + } + + @Override + Integer getNumberOfDiffs() { + 1 + } + + D getDeleted() { + super.getValue() as D + } + + String getDeletedIdentifier() { + value.diffIdentifier + } + + DeletionMergeDiff whichDeleted(D object) { + this.value = object + this.commonAncestor = object + this + } + + DeletionMergeDiff withMergeModification(D modified) { + super.rightHandSide(modified) as DeletionMergeDiff + } + + DeletionMergeDiff withNoMergeModification() { + this + } + + DeletionMergeDiff withCommonAncestor(D ca) { + super.withCommonAncestor(ca) as DeletionMergeDiff + } + + @Override + DeletionMergeDiff asMergeConflict() { + super.asMergeConflict() as DeletionMergeDiff + } + + @Deprecated + DeletionMergeDiff withSource(D source) { + super.withSource(source) as DeletionMergeDiff + } + + @Deprecated + DeletionMergeDiff withTarget(D target) { + super.withTarget(target) as DeletionMergeDiff + } + + @Override + String toString() { + "Deleted :: ${deletedIdentifier}" + } +} diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy new file mode 100644 index 0000000000..d08b46738d --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy @@ -0,0 +1,78 @@ +/* + * 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.diff.tridirectional + + +import groovy.transform.CompileStatic + +@CompileStatic +class FieldMergeDiff extends TriDirectionalDiff { + + String fieldName + + FieldMergeDiff(Class targetClass) { + super(targetClass) + } + + FieldMergeDiff forFieldName(String fieldName) { + this.fieldName = fieldName + this + } + + @Override + FieldMergeDiff withSource(F lhs) { + super.withSource(lhs) as FieldMergeDiff + } + + @Override + FieldMergeDiff withTarget(F rhs) { + super.withTarget(rhs) as FieldMergeDiff + } + + @Override + FieldMergeDiff withCommonAncestor(F ca) { + super.withCommonAncestor(ca) as FieldMergeDiff + } + + FieldMergeDiff asMergeConflict() { + super.asMergeConflict() as FieldMergeDiff + } + + @Override + boolean equals(o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + if (!super.equals(o)) return false + + FieldMergeDiff fieldDiff = (FieldMergeDiff) o + + if (fieldName != fieldDiff.fieldName) return false + + return true + } + + @Override + Integer getNumberOfDiffs() { + 1 + } + + @Override + String toString() { + "${fieldName} :: ${source} <> ${target} :: ${commonAncestor} " + } +} diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy new file mode 100644 index 0000000000..723b5a2fe9 --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy @@ -0,0 +1,341 @@ +package uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional + +import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable +import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ArrayDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.FieldDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.CreationDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.DeletionDiff + +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType +import groovy.util.logging.Slf4j + +import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.arrayMergeDiff +import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.creationDiff +import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.creationMergeDiff +import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.deletionDiff +import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.deletionMergeDiff +import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.fieldMergeDiff +import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.mergeDiff +import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.objectDiff + +/** + * Holds the result of 2 diffs which provides a unified diff of the changes which one object has made which another object has not made + * based off a common ancestor + * + * The final diff should be the intent of the changes to merge the LHS/source INTO the LHS/target + * + * See YouTrack MC-9228 for rules + * + *
+ *  source                                  target
+ *    ^                                     ^
+ *     \                                   /
+ *   caSourceObjectDiff / source     caTargetObjectDiff / target
+ *           \                           /
+ *            \                         /
+ *                  commonAncestor
+ * 
+ */ +@Slf4j +class MergeDiff extends TriDirectionalDiff { + + List diffs + + ObjectDiff commonAncestorDiffSource + ObjectDiff commonAncestorDiffTarget + + MergeDiff(Class targetClass) { + super(targetClass) + diffs = [] + } + + @Override + Integer getNumberOfDiffs() { + diffs?.sum { it.getNumberOfDiffs() } as Integer ?: 0 + } + + MergeDiff forMergingDiffable(M sourceSide) { + super.withSource(sourceSide) as MergeDiff + } + + MergeDiff intoDiffable(M targetSide) { + super.withTarget(targetSide) as MergeDiff + } + + MergeDiff havingCommonAncestor(M ca) { + super.withCommonAncestor(ca) as MergeDiff + } + + MergeDiff withCommonAncestorDiffedAgainstSource(ObjectDiff commonAncestorDiffSource) { + this.commonAncestorDiffSource = commonAncestorDiffSource + this + } + + MergeDiff withCommonAncestorDiffedAgainstTarget(ObjectDiff commonAncestorDiffTarget) { + this.commonAncestorDiffTarget = commonAncestorDiffTarget + this + } + + MergeDiff append(FieldMergeDiff fieldDiff) { + diffs.add(fieldDiff) + this + } + + /** + * The resulting MergeDiff should display all the actual differences including merge conflicts with the intent of merging the LHS/source into the + * RHS/target. + * + */ + @SuppressWarnings('GroovyVariableNotAssigned') + MergeDiff generate() { + log.debug('Generating merge diff') + commonAncestorDiffSource.diffs.each { FieldDiff caSourceFieldDiff -> + FieldDiff caTargetFieldDiff = commonAncestorDiffTarget.find { it.fieldName == caSourceFieldDiff.fieldName } + + // If diff also exists on the target side then it may be a conflicting change if both sides a different + // Or it is an identical change in which case it does not need to be included in this merge diff + if (caTargetFieldDiff) { + switch (caSourceFieldDiff.diffType) { + case ArrayDiff.simpleName: + append createArrayMergeDiffPresentOnBothSides(caSourceFieldDiff as ArrayDiff, + caTargetFieldDiff as ArrayDiff) + break + case FieldDiff.simpleName: + append createFieldMergeDiffPresentOnBothSides(caSourceFieldDiff, caTargetFieldDiff) + break + default: + log.warn('Unhandled diff type {} on both sides', caSourceFieldDiff.diffType) + } + } + // If no diff between CA and target then this is a non-conflicting change + else { + switch (caSourceFieldDiff.diffType) { + case ArrayDiff.simpleName: + append createArrayMergeDiffPresentOnOneSide(caSourceFieldDiff as ArrayDiff) + break + case FieldDiff.simpleName: + append createFieldMergeDiffPresentOnOneSide(caSourceFieldDiff) + break + default: + log.warn('Unhandled diff type {} on both sides', caSourceFieldDiff.diffType) + } + } + } + this + } + + FieldMergeDiff find(@DelegatesTo(List) @ClosureParams(value = SimpleType, + options = 'uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.FieldMergeDiff') Closure closure) { + diffs.find closure + } + + static ArrayMergeDiff createArrayMergeDiffPresentOnBothSides(ArrayDiff caSourceArrayDiff, + ArrayDiff caTargetArrayDiff) { + // createBaseArrayMergeDiffPresentOnOneSide(caSourceArrayDiff) + // .rightHandSide(caTargetArrayDiff.right) + // .withCreatedDiffs(createCreationMergeDiffs(caSourceArrayDiff, caTargetArrayDiff)) + // .withDeletedDiffs(findAllDeletedMergeDiffs(caSourceArrayDiff.deleted, caTargetArrayDiff)) + // // .modified(findAllModifiedMergeDiffs(sourceTargetDiff.modified, caSourceDiff, caTargetDiff)) + [] + } + + + static ArrayMergeDiff createArrayMergeDiffPresentOnOneSide(ArrayDiff caSourceArrayDiff) { + + // Created and Deleted diffs in this array are left as-is as they are guaranteed to be unique and no issue + createBaseArrayMergeDiffPresentOnOneSide(caSourceArrayDiff) + .withCreatedMergeDiffs(createCreationMergeDiffsForOneSide(caSourceArrayDiff.created)) + .withDeletedMergeDiffs(createDeletionMergeDiffsForOneSide(caSourceArrayDiff.deleted)) + .withModifiedMergeDiffs(createModifiedMergeDiffsForOneSide(caSourceArrayDiff.modified)) + } + + static FieldMergeDiff createFieldMergeDiffPresentOnOneSide(FieldDiff caSourceFieldDiff) { + fieldMergeDiff(caSourceFieldDiff.targetClass) + .forFieldName(caSourceFieldDiff.fieldName) + .withSource(caSourceFieldDiff.right) + // just use whats in the commonAncestor as no caTarget data denotes no difference between the CA and the target + .withTarget(caSourceFieldDiff.left) + .withCommonAncestor(caSourceFieldDiff.left) // both diffs have the same LHS + } + + static FieldMergeDiff createFieldMergeDiffPresentOnBothSides(FieldDiff caSourceFieldDiff, FieldDiff caTargetFieldDiff) { + createFieldMergeDiffPresentOnOneSide(caSourceFieldDiff) + .withTarget(caTargetFieldDiff.right) + .asMergeConflict() + } + + static ArrayMergeDiff createBaseArrayMergeDiffPresentOnOneSide(ArrayDiff caSourceArrayDiff) { + + // Created and Deleted diffs in this array are left as-is as they are guaranteed to be unique and no issue + arrayMergeDiff(caSourceArrayDiff.targetClass) + .forFieldName(caSourceArrayDiff.fieldName) + .withSource(caSourceArrayDiff.right) + // just use whats in the commonAncestor as no caTarget data denotes no difference between the CA and the target + .withTarget(caSourceArrayDiff.left) + .withCommonAncestor(caSourceArrayDiff.left) // both diffs have the same LHS + } + + static Collection> createModifiedMergeDiffsForOneSide(Collection> caSourceModifiedDiffs) { + // Modified diffs represent diffs which have modifications down the chain but no changes on the target side + // Therefore we can use an empty object diff + caSourceModifiedDiffs.collect { objDiff -> + Class targetClass = objDiff.left.class as Class + mergeDiff(targetClass) + .forMergingDiffable(objDiff.right) + .intoDiffable(objDiff.left) + .havingCommonAncestor(objDiff.left) + .withCommonAncestorDiffedAgainstSource(objDiff) + .withCommonAncestorDiffedAgainstTarget(objectDiff(targetClass)) + .generate() + } + } + + static Collection> createCreationMergeDiffsForOneSide( + Collection> caSourceCreatedDiffs) { + caSourceCreatedDiffs.collect { created -> + creationMergeDiff(created.targetClass) + .whichCreated(created.created) + } + } + + static Collection> createDeletionMergeDiffsForOneSide( + Collection> caSourceDeletionDiffs) { + caSourceDeletionDiffs.collect { deleted -> + deletionMergeDiff(deleted.targetClass) + .whichDeleted(deleted.deleted) + } + } + + /** + * Identify all the objects in the array field created on the LHS and flag all those which + * + * Important to note that due to the way diff works the same object cannot exist in more than one of created, deleted, modified in an + * ArrayDIff + */ + static Collection createCreationMergeDiffs(ArrayDiff caSourceDiff, + ArrayDiff caTargetDiff) { + List> creationDiffs = caSourceDiff.created.collect { diff -> + if (diff.createdIdentifier in caTargetDiff.created*.createdIdentifier) { + // Both sides added : potential conflict and therefore is a modified rather than create or no diff + log.debug('ca/source created {} exists in ca/target created', diff.createdIdentifier) + return null + } + if (diff.createdIdentifier in caTargetDiff.deleted*.deletedIdentifier) { + // Impossible as it didnt exist in CA therefore target can't have deleted it + return null + } + if (diff.createdIdentifier in caTargetDiff.modified*.getRightIdentifier()) { + // Impossible as it didnt exist in CA therefore target can't have modified it + return null + } + // Only added on source side : no conflict + log.debug('ca/source created {} doesnt exist in ca/target', diff.createdIdentifier) + return creationDiff(diff.targetClass).created(diff.value) + }.findAll() as Collection + + // creationDiffs.addAll(caSourceDiff.modified.collect { caSourceModifiedDiff -> + // DeletionDiff caTargetDeletedDiff = caTargetDiff.deleted.find { it.deletedIdentifier == caSourceModifiedDiff.leftIdentifier } + // if (caTargetDeletedDiff) { + // // Modified in source but deleted in target : conflict but should appear as creation + // return creationDiff(caSourceModifiedDiff.targetClass.arrayType() as Class) + // .created(caSourceModifiedDiff.right) + // .withMergeDeletion(caTargetDeletedDiff.deleted) + // .withCommonAncestor(caSourceDiff.left) + // .asMergeConflict() + // } + // null + // }.findAll() as Collection>) + + creationDiffs + } + + /** + * Identify all the objects in the array field deleted on the LHS and flag all those which + * + * Important to note that due to the way diff works the same object cannot exist in more than one of created, deleted, modified in an + * ArrayDIff + * + */ + static Collection findAllDeletedMergeDiffs(Collection> caSourceDeletedDiffs, + ArrayDiff caTargetDiff) { + caSourceDeletedDiffs.collect { diff -> + if (diff.deletedIdentifier in caTargetDiff.created*.createdIdentifier) { + // Impossible as you can't delete something which never existed + return null + } + if (diff.deletedIdentifier in caTargetDiff.deleted*.deletedIdentifier) { + // Deleted in source, deleted in target : no conflict no diff + log.debug('ca/source deleted {} exists in ca/target deleted', diff.deletedIdentifier) + return null + } + ObjectDiff modifiedTargetDiff = caTargetDiff.modified.find { it.rightIdentifier == diff.deletedIdentifier } + if (modifiedTargetDiff) { + // Deleted in source, modified in target : conflict as deletion diff + log.debug('ca/source deleted {} exists in ca/target modified', diff.deletedIdentifier) + return deletionDiff(diff.targetClass) + .deleted(diff.value) + .withMergeModification(modifiedTargetDiff.right) + .withCommonAncestor(modifiedTargetDiff.left) + .asMergeConflict() + } + // Deleted in source but not touched in target : no conflict + log.debug('ca/source deleted {} doesnt exist in ca/target', diff.deletedIdentifier) + return deletionDiff(diff.targetClass).deleted(diff.value).withNoMergeModification() + }.findAll() as Collection + } + + // static Collection findAllModifiedMergeDiffs(Collection modified, ArrayDiff sourceArrayDiff, ArrayDiff + // targetArrayDiff) { + // modified.collect { ObjectDiff objDiff -> + // def diffIdentifier = objDiff.target.diffIdentifier + // if (diffIdentifier in sourceArrayDiff.created.value.diffIdentifier) { return updateModifiedObjectMergeDiffCreatedOnOneSide + // (objDiff) } + // if + // (diffIdentifier in sourceArrayDiff.modified.source.diffIdentifier) { + // if (diffIdentifier in targetArrayDiff.modified.source.diffIdentifier) { + // // + // top modified, source modified , target modified + // return createModifiedObjectMergeDiffModifiedOnBothSides(objDiff, + // sourceArrayDiff.modified. + // find { it.source.diffIdentifier == diffIdentifier } as + // ObjectDiff, + // targetArrayDiff.modified. + // find { it.source.diffIdentifier == diffIdentifier } as + // ObjectDiff, + // targetArrayDiff.source.find { it.diffIdentifier == + // diffIdentifier }) + // } // top modified, source modified, target not modified + // return updateModifiedObjectMergeDiffPresentOnOneSide(objDiff) + // } null + // }.findAll() + // } + // static ObjectDiff updateModifiedObjectMergeDiffCreatedOnOneSide(ObjectDiff objectDiff) { + // // top modified, target created, (source also created) + // objectDiff.diffs.each { + // it.isMergeConflict = true + // it.commonAncestorValue = null + // } objectDiff . isMergeConflict = true + // objectDiff.commonAncestorValue = null + // return objectDiff + // } + // + // static ObjectDiff createModifiedObjectMergeDiffModifiedOnBothSides(ObjectDiff objectDiff, ObjectDiff sourceObjDiff, + // ObjectDiff targetObjDiff, + // Object commonAncestorValue) { + // // call recursively + // ObjectDiff mergeDiff = objectDiff.mergeDiff(sourceObjDiff, targetObjDiff) + // mergeDiff.isMergeConflict = true + // mergeDiff.commonAncestorValue = commonAncestorValue + // return mergeDiff + // } + // + // static ObjectDiff updateModifiedObjectMergeDiffPresentOnOneSide(ObjectDiff objectDiff) { + // objectDiff.diffs.each { + // it.isMergeConflict = false + // } + // objectDiff.isMergeConflict = false + // objectDiff + // } +} diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy new file mode 100644 index 0000000000..45fe35a5f1 --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy @@ -0,0 +1,106 @@ +package uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional + + +import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.BiDirectionalDiff + +import groovy.transform.CompileStatic + +@CompileStatic +abstract class TriDirectionalDiff extends BiDirectionalDiff { + + Boolean mergeConflict + T commonAncestor + + protected TriDirectionalDiff(Class targetClass) { + super(targetClass) + mergeConflict = false + } + + @Override + boolean equals(o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + + TriDirectionalDiff diff = (TriDirectionalDiff) o + + if (left != diff.left) return false + if (right != diff.right) return false + + return true + } + + @SuppressWarnings('GrDeprecatedAPIUsage') + TriDirectionalDiff withSource(T source) { + this.left = source + this + } + + @SuppressWarnings('GrDeprecatedAPIUsage') + TriDirectionalDiff withTarget(T target) { + this.right = target + this + } + + TriDirectionalDiff withCommonAncestor(T ca) { + this.commonAncestor = ca + this + } + + TriDirectionalDiff asMergeConflict() { + this.mergeConflict = true + this + } + + Boolean isMergeConflict() { + mergeConflict + } + + T getTarget() { + super.getRight() + } + + T getSource() { + super.getLeft() + } + + @Deprecated + @Override + T getRight() { + super.getRight() + } + + @Deprecated + @Override + void setRight(T right) { + super.setRight(right) + } + + @Deprecated + @Override + BiDirectionalDiff leftHandSide(T lhs) { + super.leftHandSide(lhs) + } + + @Deprecated + @Override + BiDirectionalDiff rightHandSide(T rhs) { + super.rightHandSide(rhs) + } + + @Deprecated + @Override + void setLeft(T left) { + super.setLeft(left) + } + + @Deprecated + @Override + T getLeft() { + super.getLeft() + } + + @Override + String toString() { + "${source} --> ${target} [${commonAncestor}]" + } +} diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy index aded114797..2f402fcd6e 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy @@ -24,8 +24,8 @@ 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.container.Folder import uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder -import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.MergeDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTreeService import uk.ac.ox.softeng.maurodatamapper.core.facet.EditService import uk.ac.ox.softeng.maurodatamapper.core.facet.EditTitle @@ -395,7 +395,7 @@ abstract class ModelService extends CatalogueItemService imp //TODO validation on saving merges if (!modelMergeObjectDiff.hasDiffs()) return targetModel log.debug('Merging {} diffs into model {}', modelMergeObjectDiff.getValidDiffs().size(), targetModel.label) - modelMergeObjectDiff.getValidDiffs().each {mergeFieldDiff -> + modelMergeObjectDiff.getValidDiffs().each { mergeFieldDiff -> log.debug('{}', mergeFieldDiff.summary) if (mergeFieldDiff.isFieldChange()) { @@ -403,7 +403,7 @@ abstract class ModelService extends CatalogueItemService imp } else if (mergeFieldDiff.isMetadataChange()) { mergeMetadataIntoCatalogueItem(mergeFieldDiff, targetModel, userSecurityPolicyManager) } else { - ModelItemService modelItemService = modelItemServices.find {it.handles(mergeFieldDiff.fieldName)} + ModelItemService modelItemService = modelItemServices.find { it.handles(mergeFieldDiff.fieldName) } if (modelItemService) { modelItemService.processMergeFieldDiff(mergeFieldDiff, targetModel, userSecurityPolicyManager) } else { @@ -428,7 +428,7 @@ abstract class ModelService extends CatalogueItemService imp if (versionLinkType == VersionLinkType.NEW_FORK_OF) return includeForks ? versionTreeModelList : [] List versionLinks = versionLinkService.findAllByTargetModelId(instance.id) - versionLinks.each {link -> + versionLinks.each { link -> K linkedModel = get(link.multiFacetAwareItemId) versionTreeModelList. addAll(buildModelVersionTree(linkedModel, link.linkType, rootVersionTreeModel, includeForks, branchesOnly, userSecurityPolicyManager)) @@ -443,8 +443,9 @@ abstract class ModelService extends CatalogueItemService imp K findCommonAncestorBetweenModels(K leftModel, K rightModel) { if (leftModel.label != rightModel.label) { - throw new ApiBadRequestException('MS03', "Model [${leftModel.id}] does not share its label with [${leftModel.id}] therefore they cannot have a " + - "common ancestor") + throw new ApiBadRequestException('MS03', + "Model [${leftModel.id}] does not share its label with [${leftModel.id}] therefore they cannot have a " + + "common ancestor") } K finalisedLeftParent = getFinalisedParent(leftModel) @@ -518,11 +519,16 @@ abstract class ModelService extends CatalogueItemService imp ObjectDiff caDiffSource = commonAncestor.diff(sourceModel) ObjectDiff caDiffTarget = commonAncestor.diff(targetModel) - ObjectDiff sourceDiffTarget = sourceModel.diff(targetModel) + // ObjectDiff sourceDiffTarget = sourceModel.diff(targetModel) DiffBuilder - .mergeDiff(sourceDiffTarget, commonAncestor) - .diff(sourceDiffTarget, caDiffTarget, caDiffSource) as MergeDiff + .mergeDiff(sourceModel.class as Class) + .forMergingDiffable(sourceModel) + .intoDiffable(targetModel) + .havingCommonAncestor(commonAncestor) + .withCommonAncestorDiffedAgainstSource(caDiffSource) + .withCommonAncestorDiffedAgainstTarget(caDiffTarget) + .generate() } @Override @@ -635,12 +641,12 @@ abstract class ModelService extends CatalogueItemService imp if (countByAuthorityAndLabel(model.authority, model.label)) { List existingModels = findAllByAuthorityAndLabel(model.authority, model.label) - existingModels.each {existing -> + existingModels.each { existing -> log.debug('Setting Model as new documentation version of [{}:{}]', existing.label, existing.documentationVersion) if (!existing.finalised) finaliseModel(existing, catalogueUser, null, null, null) setModelIsNewDocumentationVersionOfModel(model, existing, catalogueUser) } - Version latestVersion = existingModels.max {it.documentationVersion}.documentationVersion + Version latestVersion = existingModels.max { it.documentationVersion }.documentationVersion model.documentationVersion = Version.nextMajorVersion(latestVersion) } else log.info('Marked as importAsNewDocumentationVersion but no existing Models with label [{}]', model.label) diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy index 1f7ead27a0..be4b597105 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy @@ -18,10 +18,10 @@ package uk.ac.ox.softeng.maurodatamapper.datamodel import uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress -import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ArrayDiff -import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.FieldDiff -import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.MergeDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.ArrayMergeDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.FieldMergeDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata import uk.ac.ox.softeng.maurodatamapper.core.facet.MetadataService import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType @@ -1062,11 +1062,11 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { // mergeDiff.numberOfDiffs == 11 when: 'branch name is a non-conflicting diff' - FieldDiff stringFieldDiff = mergeDiff.find { it.fieldName == 'branchName' } + FieldMergeDiff stringFieldDiff = mergeDiff.find { it.fieldName == 'branchName' } then: - stringFieldDiff.left == 'test' - stringFieldDiff.right == VersionAwareConstraints.DEFAULT_BRANCH_NAME + stringFieldDiff.source == 'test' + stringFieldDiff.target == VersionAwareConstraints.DEFAULT_BRANCH_NAME stringFieldDiff.commonAncestor == VersionAwareConstraints.DEFAULT_BRANCH_NAME !stringFieldDiff.isMergeConflict() @@ -1074,8 +1074,8 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { stringFieldDiff = mergeDiff.find { it.fieldName == 'organisation' } then: - stringFieldDiff.left == 'under test' - stringFieldDiff.right == null + stringFieldDiff.source == 'under test' + stringFieldDiff.target == null stringFieldDiff.commonAncestor == null !stringFieldDiff.isMergeConflict() @@ -1083,60 +1083,69 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { stringFieldDiff = mergeDiff.find { it.fieldName == 'author' } then: - stringFieldDiff.left == 'harry' - stringFieldDiff.right == 'dick' + stringFieldDiff.source == 'harry' + stringFieldDiff.target == 'dick' stringFieldDiff.commonAncestor == 'john' stringFieldDiff.isMergeConflict() when: 'single array change in datatypes' - ArrayDiff dataTypeDiff = mergeDiff.find { it.fieldName == 'dataTypes' } as ArrayDiff + ArrayMergeDiff dataTypeDiff = mergeDiff.find { it.fieldName == 'dataTypes' } as ArrayMergeDiff then: dataTypeDiff.deleted.isEmpty() dataTypeDiff.modified.isEmpty() dataTypeDiff.created.size() == 1 - dataTypeDiff.created.first().valueIdentifier == 'addSourceOnlyOnlyChangeInArray' + dataTypeDiff.created.first().createdIdentifier == 'addSourceOnlyOnlyChangeInArray' !dataTypeDiff.created.first().isMergeConflict() !dataTypeDiff.created.first().commonAncestor + !dataTypeDiff.created.first().target when: 'metadata has array diffs' - ArrayDiff metadataDiff = mergeDiff.find { it.fieldName == 'metadata' } as ArrayDiff + ArrayMergeDiff metadataDiff = mergeDiff.find { it.fieldName == 'metadata' } as ArrayMergeDiff then: - metadataDiff.left.size() == 1 - metadataDiff.right.size() == 2 + metadataDiff.source.size() == 1 + metadataDiff.target.size() == 2 metadataDiff.commonAncestor.size() == 2 metadataDiff.created.isEmpty() metadataDiff.deleted.size() == 1 - metadataDiff.deleted.first().valueIdentifier == 'test.deleteSourceOnly' + metadataDiff.deleted.first().deletedIdentifier == 'test.deleteSourceOnly' metadataDiff.modified.size() == 1 - metadataDiff.modified.first().leftIdentifier == 'test.modifySourceOnly' + metadataDiff.modified.first().sourceIdentifier == 'test.modifySourceOnly' metadataDiff.modified.first().diffs.size() == 1 metadataDiff.modified.first().diffs.first().fieldName == 'value' - metadataDiff.modified.first().diffs.first().left == 'altered' - metadataDiff.modified.first().diffs.first().right == 'modifySourceOnly' + metadataDiff.modified.first().diffs.first().source == 'altered' + metadataDiff.modified.first().diffs.first().target == 'modifySourceOnly' metadataDiff.modified.first().diffs.first().commonAncestor == 'modifySourceOnly' !metadataDiff.modified.first().diffs.first().isMergeConflict() and: 'array diffs on the dataclass list' - ArrayDiff dataClassesDiff = mergeDiff.diffs.find { it.fieldName == 'dataClasses' } as ArrayDiff - dataClassesDiff.created.size() == 3 - dataClassesDiff.deleted.size() == 2 + ArrayMergeDiff dataClassesDiff = mergeDiff.diffs.find { it.fieldName == 'dataClasses' } as ArrayMergeDiff + dataClassesDiff.created.size() == 1 + // dataClassesDiff.deleted.size() == 2 // dataClassesDiff.modified.size() == 4 - dataClassesDiff.created.value.label as Set == ['addSourceOnly', 'leftParentDataClass', 'modifyAndDelete'] as Set - !dataClassesDiff.created.find { it.value.label == 'addSourceOnly' }.isMergeConflict() - !dataClassesDiff.created.find { it.value.label == 'addSourceOnly' }.commonAncestor - !dataClassesDiff.created.find { it.value.label == 'leftParentDataClass' }.isMergeConflict() - !dataClassesDiff.created.find { it.value.label == 'leftParentDataClass' }.commonAncestor - dataClassesDiff.created.find { it.value.label == 'modifyAndDelete' }.isMergeConflict() - dataClassesDiff.created.find { it.value.label == 'modifyAndDelete' }.commonAncestor - dataClassesDiff.deleted.value.label as Set == ['deleteAndModify', 'deleteSourceOnly'] as Set - dataClassesDiff.deleted.find { it.value.label == 'deleteAndModify' }.isMergeConflict() - dataClassesDiff.deleted.find { it.value.label == 'deleteAndModify' }.commonAncestor + and: 'created' + dataClassesDiff.created.value.label as Set == ['addSourceOnly'] as Set + !dataClassesDiff.created.find { it.createdIdentifier == 'addSourceOnly' }.isMergeConflict() + !dataClassesDiff.created.find { it.createdIdentifier == 'addSourceOnly' }.commonAncestor + // + // !dataClassesDiff.created.find { it.value.label == 'targetParentDataClass' }.isMergeConflict() + // !dataClassesDiff.created.find { it.value.label == 'targetParentDataClass' }.commonAncestor + // + // dataClassesDiff.created.find { it.value.label == 'modifyAndDelete' }.isMergeConflict() + // dataClassesDiff.created.find { it.value.label == 'modifyAndDelete' }.commonAncestor + + and: + dataClassesDiff.deleted.value.label as Set == ['deleteSourceAndModifyTarget', 'deleteSourceOnly'] as Set + dataClassesDiff.deleted.find { it.value.label == 'deleteSourceAndModifyTarget' }.isMergeConflict() + dataClassesDiff.deleted.find { it.value.label == 'deleteSourceAndModifyTarget' }.commonAncestor + dataClassesDiff.deleted.find { it.value.label == 'deleteSourceAndModifyTarget' }.right !dataClassesDiff.deleted.find { it.value.label == 'deleteSourceOnly' }.isMergeConflict() !dataClassesDiff.deleted.find { it.value.label == 'deleteSourceOnly' }.commonAncestor + + and: dataClassesDiff.modified.left.diffIdentifier as Set == ['existingClass', 'modifyAndModifyReturningDifference', 'modifySourceOnly', 'addAndAddReturningDifference'] as Set dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyAndModifyReturningDifference' }.diffs[0].fieldName == 'description' From cdceabf5521d950aac0b38924df4c76be2ccfff6 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Tue, 22 Jun 2021 18:46:32 +0100 Subject: [PATCH 15/82] MergeDiff all updated apart from modification on source and deletion on target Test passing to prove all the cases --- .../diff/tridirectional/ArrayMergeDiff.groovy | 6 +- .../tridirectional/DeletionMergeDiff.groovy | 14 +- .../diff/tridirectional/FieldMergeDiff.groovy | 4 + .../core/diff/tridirectional/MergeDiff.groovy | 415 +++++++++++++----- .../tridirectional/TriDirectionalDiff.groovy | 8 +- .../DataModelServiceIntegrationSpec.groovy | 263 +++++++---- 6 files changed, 499 insertions(+), 211 deletions(-) diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy index 76aeb6f683..f5a9f5089e 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy @@ -80,7 +80,11 @@ class ArrayMergeDiff extends FieldMergeDiff> { @Override Integer getNumberOfDiffs() { - created.size() + deleted.size() + ((modified.sum { it.getNumberOfDiffs() } ?: 0) as Integer) + created.size() + deleted.size() + ((modified.sum {it.getNumberOfDiffs()} ?: 0) as Integer) + } + + boolean hasDiffs() { + getNumberOfDiffs() != 0 } @Override diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy index fc12332427..b9e851825f 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy @@ -1,12 +1,15 @@ package uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable +import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import groovy.transform.CompileStatic @CompileStatic class DeletionMergeDiff extends TriDirectionalDiff { + ObjectDiff mergeModificationDiff + DeletionMergeDiff(Class targetClass) { super(targetClass) } @@ -26,12 +29,12 @@ class DeletionMergeDiff extends TriDirectionalDiff { DeletionMergeDiff whichDeleted(D object) { this.value = object - this.commonAncestor = object - this + withCommonAncestor object } - DeletionMergeDiff withMergeModification(D modified) { - super.rightHandSide(modified) as DeletionMergeDiff + DeletionMergeDiff withMergeModification(ObjectDiff modifiedDiff) { + this.mergeModificationDiff = modifiedDiff + this } DeletionMergeDiff withNoMergeModification() { @@ -59,6 +62,7 @@ class DeletionMergeDiff extends TriDirectionalDiff { @Override String toString() { - "Deleted :: ${deletedIdentifier}" + String str = "Deleted :: ${deletedIdentifier}" + mergeModificationDiff ? "${str} Modified :: ${mergeModificationDiff}" : str } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy index d08b46738d..0947716802 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy @@ -71,6 +71,10 @@ class FieldMergeDiff extends TriDirectionalDiff { 1 } + boolean hasDiff() { + source != target + } + @Override String toString() { "${fieldName} :: ${source} <> ${target} :: ${commonAncestor} " diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy index 723b5a2fe9..db8598eca5 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy @@ -1,5 +1,6 @@ package uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional + import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ArrayDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.FieldDiff @@ -12,13 +13,10 @@ import groovy.transform.stc.SimpleType import groovy.util.logging.Slf4j import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.arrayMergeDiff -import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.creationDiff import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.creationMergeDiff -import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.deletionDiff import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.deletionMergeDiff import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.fieldMergeDiff import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.mergeDiff -import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.objectDiff /** * Holds the result of 2 diffs which provides a unified diff of the changes which one object has made which another object has not made @@ -41,19 +39,50 @@ import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.objectDiff @Slf4j class MergeDiff extends TriDirectionalDiff { - List diffs + private List diffs - ObjectDiff commonAncestorDiffSource - ObjectDiff commonAncestorDiffTarget + private ObjectDiff commonAncestorDiffSource + private ObjectDiff commonAncestorDiffTarget + private ObjectDiff sourceDiffTarget MergeDiff(Class targetClass) { super(targetClass) diffs = [] } + @Override + Boolean isMergeConflict() { + !objectsAreIdentical() + } + @Override Integer getNumberOfDiffs() { - diffs?.sum { it.getNumberOfDiffs() } as Integer ?: 0 + diffs?.sum {it.getNumberOfDiffs()} as Integer ?: 0 + } + + String getSourceIdentifier() { + source.diffIdentifier + } + + String getTargetIdentifier() { + target.diffIdentifier + } + + FieldMergeDiff first() { + diffs.first() + } + + int size() { + diffs.size() + } + + boolean isEmpty() { + diffs.isEmpty() + } + + @Override + String toString() { + "${sourceIdentifier} --> ${targetIdentifier} [${commonAncestor.diffIdentifier}]" } MergeDiff forMergingDiffable(M sourceSide) { @@ -78,11 +107,25 @@ class MergeDiff extends TriDirectionalDiff { this } + MergeDiff withSourceDiffedAgainstTarget(ObjectDiff sourceDiffTarget) { + this.sourceDiffTarget = sourceDiffTarget + this + } + MergeDiff append(FieldMergeDiff fieldDiff) { - diffs.add(fieldDiff) + if (fieldDiff) diffs.add(fieldDiff) this } + MergeDiff asMergeConflict() { + super.asMergeConflict() as MergeDiff + } + + FieldMergeDiff find(@DelegatesTo(List) @ClosureParams(value = SimpleType, + options = 'uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.FieldMergeDiff') Closure closure) { + diffs.find closure + } + /** * The resulting MergeDiff should display all the actual differences including merge conflicts with the intent of merging the LHS/source into the * RHS/target. @@ -90,13 +133,31 @@ class MergeDiff extends TriDirectionalDiff { */ @SuppressWarnings('GroovyVariableNotAssigned') MergeDiff generate() { - log.debug('Generating merge diff') - commonAncestorDiffSource.diffs.each { FieldDiff caSourceFieldDiff -> - FieldDiff caTargetFieldDiff = commonAncestorDiffTarget.find { it.fieldName == caSourceFieldDiff.fieldName } + + // Both sides then calculate the merge + if (commonAncestorDiffSource && commonAncestorDiffTarget) { + return generateMergeDiffFromCommonAncestorAgainstSourceAndTarget() + } + // Only source means this is a merge diff recurse where there is no content on the target side + if (commonAncestorDiffSource) { + return generateMergeDiffFromCommonAncestorAgainstSource() + } + // Otherwise we have the source diff target where there is no common ancestor + generateMergeDiffFromSourceAgainstTarget() + } + + + private MergeDiff generateMergeDiffFromCommonAncestorAgainstSourceAndTarget() { + log.debug('Generating merge diff from common ancestor diffs against source and target for [{}]', commonAncestorDiffSource.leftIdentifier) + commonAncestorDiffSource.diffs.each {FieldDiff caSourceFieldDiff -> + + log.debug('Processing field [{}] with change type [{}] present between common ancestor and source', caSourceFieldDiff.fieldName, caSourceFieldDiff.diffType) + FieldDiff caTargetFieldDiff = commonAncestorDiffTarget.find {it.fieldName == caSourceFieldDiff.fieldName} // If diff also exists on the target side then it may be a conflicting change if both sides a different // Or it is an identical change in which case it does not need to be included in this merge diff if (caTargetFieldDiff) { + log.debug('[{}] Change present between common ancestor and target', caSourceFieldDiff.fieldName) switch (caSourceFieldDiff.diffType) { case ArrayDiff.simpleName: append createArrayMergeDiffPresentOnBothSides(caSourceFieldDiff as ArrayDiff, @@ -111,6 +172,7 @@ class MergeDiff extends TriDirectionalDiff { } // If no diff between CA and target then this is a non-conflicting change else { + log.debug('[{}] No change present between common ancestor and target', caSourceFieldDiff.fieldName) switch (caSourceFieldDiff.diffType) { case ArrayDiff.simpleName: append createArrayMergeDiffPresentOnOneSide(caSourceFieldDiff as ArrayDiff) @@ -126,100 +188,154 @@ class MergeDiff extends TriDirectionalDiff { this } - FieldMergeDiff find(@DelegatesTo(List) @ClosureParams(value = SimpleType, - options = 'uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.FieldMergeDiff') Closure closure) { - diffs.find closure + private MergeDiff generateMergeDiffFromCommonAncestorAgainstSource() { + log.debug('Generating merge diff from common ancestor diff against source for [{}]', commonAncestorDiffSource.leftIdentifier) + commonAncestorDiffSource.diffs.each {FieldDiff caSourceFieldDiff -> + log.debug('Processing field [{}] with change type [{}] present between common ancestor and source', caSourceFieldDiff.fieldName, caSourceFieldDiff.diffType) + switch (caSourceFieldDiff.diffType) { + case ArrayDiff.simpleName: + append createArrayMergeDiffPresentOnOneSide(caSourceFieldDiff as ArrayDiff) + break + case FieldDiff.simpleName: + append createFieldMergeDiffPresentOnOneSide(caSourceFieldDiff) + break + default: + log.warn('Unhandled diff type {} on both sides', caSourceFieldDiff.diffType) + } + } + this + } + + private MergeDiff generateMergeDiffFromSourceAgainstTarget() { + log.debug('Generating merge diff from source diff against target for [{}]', sourceDiffTarget.leftIdentifier) + sourceDiffTarget.diffs.each {FieldDiff sourceTargetFieldDiff -> + log.debug('Processing field [{}] with change type [{}] present between source and target', sourceTargetFieldDiff.fieldName, sourceTargetFieldDiff.diffType) + switch (sourceTargetFieldDiff.diffType) { + case ArrayDiff.simpleName: + append createArrayMergeDiffFromSourceTargetArrayDiff(sourceTargetFieldDiff as ArrayDiff) + break + case FieldDiff.simpleName: + append createFieldMergeDiffFromSourceTargetFieldDiff(sourceTargetFieldDiff) + break + default: + log.warn('Unhandled diff type {} on both sides', sourceTargetFieldDiff.diffType) + } + } + this + } + + static ArrayMergeDiff createArrayMergeDiffFromSourceTargetArrayDiff(ArrayDiff sourceTargetArrayDiff) { + log.debug('[{}] Processing array differences against target from source', sourceTargetArrayDiff.fieldName) + // Created and Deleted diffs in this array are left as-is as they are guaranteed to be unique and no issue + arrayMergeDiff(sourceTargetArrayDiff.targetClass) + .forFieldName(sourceTargetArrayDiff.fieldName) + .withSource(sourceTargetArrayDiff.left) + .withTarget(sourceTargetArrayDiff.right) + .withCommonAncestor(null) + .withCreatedMergeDiffs(sourceTargetArrayDiff.created.collect {c -> + creationMergeDiff(c.targetClass).whichCreated(c.created) + }) + .withDeletedMergeDiffs(sourceTargetArrayDiff.deleted.collect {d -> + deletionMergeDiff(d.targetClass).whichDeleted(d.deleted) + }) + .withModifiedMergeDiffs(sourceTargetArrayDiff.modified.collect {m -> + mergeDiff(m.targetClass) + .forMergingDiffable(m.left) + .intoDiffable(m.right) + .havingCommonAncestor(null) + .withSourceDiffedAgainstTarget(m) + .generate() + }) + + } + + static ArrayMergeDiff createBaseArrayMergeDiffPresentOnOneSide(ArrayDiff caSourceArrayDiff) { + + // Created and Deleted diffs in this array are left as-is as they are guaranteed to be unique and no issue + arrayMergeDiff(caSourceArrayDiff.targetClass) + .forFieldName(caSourceArrayDiff.fieldName) + .withSource(caSourceArrayDiff.right) + // just use whats in the commonAncestor as no caTarget data denotes no difference between the CA and the target + .withTarget(caSourceArrayDiff.left) + .withCommonAncestor(caSourceArrayDiff.left) // both diffs have the same LHS } static ArrayMergeDiff createArrayMergeDiffPresentOnBothSides(ArrayDiff caSourceArrayDiff, ArrayDiff caTargetArrayDiff) { - // createBaseArrayMergeDiffPresentOnOneSide(caSourceArrayDiff) - // .rightHandSide(caTargetArrayDiff.right) - // .withCreatedDiffs(createCreationMergeDiffs(caSourceArrayDiff, caTargetArrayDiff)) - // .withDeletedDiffs(findAllDeletedMergeDiffs(caSourceArrayDiff.deleted, caTargetArrayDiff)) - // // .modified(findAllModifiedMergeDiffs(sourceTargetDiff.modified, caSourceDiff, caTargetDiff)) - [] + log.debug('[{}] Processing array differences against common ancestor on both sides', caSourceArrayDiff.fieldName) + ArrayMergeDiff arrayMergeDiff = createBaseArrayMergeDiffPresentOnOneSide(caSourceArrayDiff) + .withTarget(caTargetArrayDiff.right) + .withCreatedMergeDiffs(createCreationMergeDiffsPresentOnBothSides(caSourceArrayDiff, caTargetArrayDiff)) + .withDeletedMergeDiffs(createDeletionMergeDiffsPresentOnBothSides(caSourceArrayDiff.deleted, caTargetArrayDiff)) + .withModifiedMergeDiffs(createModifiedMergeDiffsPresentOnBothSides(caSourceArrayDiff, caTargetArrayDiff)) + + // If no actual differences in any section then return null + arrayMergeDiff.hasDiffs() ? arrayMergeDiff : null } static ArrayMergeDiff createArrayMergeDiffPresentOnOneSide(ArrayDiff caSourceArrayDiff) { - + log.debug('[{}] Processing array differences against common ancestor on one side', caSourceArrayDiff.fieldName) // Created and Deleted diffs in this array are left as-is as they are guaranteed to be unique and no issue createBaseArrayMergeDiffPresentOnOneSide(caSourceArrayDiff) - .withCreatedMergeDiffs(createCreationMergeDiffsForOneSide(caSourceArrayDiff.created)) - .withDeletedMergeDiffs(createDeletionMergeDiffsForOneSide(caSourceArrayDiff.deleted)) - .withModifiedMergeDiffs(createModifiedMergeDiffsForOneSide(caSourceArrayDiff.modified)) + .withCreatedMergeDiffs(createCreationMergeDiffsPresentOnOneSide(caSourceArrayDiff.created)) + .withDeletedMergeDiffs(createDeletionMergeDiffsPresentOnOneSide(caSourceArrayDiff.deleted)) + .withModifiedMergeDiffs(createModifiedMergeDiffsPresentOnOneSide(caSourceArrayDiff.modified)) } - static FieldMergeDiff createFieldMergeDiffPresentOnOneSide(FieldDiff caSourceFieldDiff) { - fieldMergeDiff(caSourceFieldDiff.targetClass) - .forFieldName(caSourceFieldDiff.fieldName) - .withSource(caSourceFieldDiff.right) - // just use whats in the commonAncestor as no caTarget data denotes no difference between the CA and the target - .withTarget(caSourceFieldDiff.left) - .withCommonAncestor(caSourceFieldDiff.left) // both diffs have the same LHS + static FieldMergeDiff createFieldMergeDiffFromSourceTargetFieldDiff(FieldDiff sourceTargetFieldDiff) { + log.debug('[{}] Processing field difference against target from source', sourceTargetFieldDiff.fieldName) + fieldMergeDiff(sourceTargetFieldDiff.targetClass) + .forFieldName(sourceTargetFieldDiff.fieldName) + .withSource(sourceTargetFieldDiff.left) + .withTarget(sourceTargetFieldDiff.right) + .withCommonAncestor(null) + .asMergeConflict() // Always a merge conflict as the values have to be different otherwise we wouldnt be here } static FieldMergeDiff createFieldMergeDiffPresentOnBothSides(FieldDiff caSourceFieldDiff, FieldDiff caTargetFieldDiff) { - createFieldMergeDiffPresentOnOneSide(caSourceFieldDiff) + log.debug('[{}] Processing field difference against common ancestor on both sides', caSourceFieldDiff.fieldName) + FieldMergeDiff fieldMergeDiff = createBaseFieldMergeDiffPresentOnOneSide(caSourceFieldDiff) .withTarget(caTargetFieldDiff.right) .asMergeConflict() + // This is a safety check to handle when 2 diffs are used with modifications but no actual difference + fieldMergeDiff.hasDiff() ? fieldMergeDiff : null } - static ArrayMergeDiff createBaseArrayMergeDiffPresentOnOneSide(ArrayDiff caSourceArrayDiff) { - - // Created and Deleted diffs in this array are left as-is as they are guaranteed to be unique and no issue - arrayMergeDiff(caSourceArrayDiff.targetClass) - .forFieldName(caSourceArrayDiff.fieldName) - .withSource(caSourceArrayDiff.right) + static FieldMergeDiff createFieldMergeDiffPresentOnOneSide(FieldDiff caSourceFieldDiff) { + log.debug('[{}] Processing field difference against common ancestor on one side', caSourceFieldDiff.fieldName) + createBaseFieldMergeDiffPresentOnOneSide(caSourceFieldDiff) // just use whats in the commonAncestor as no caTarget data denotes no difference between the CA and the target - .withTarget(caSourceArrayDiff.left) - .withCommonAncestor(caSourceArrayDiff.left) // both diffs have the same LHS + .withTarget(caSourceFieldDiff.left) } - static Collection> createModifiedMergeDiffsForOneSide(Collection> caSourceModifiedDiffs) { - // Modified diffs represent diffs which have modifications down the chain but no changes on the target side - // Therefore we can use an empty object diff - caSourceModifiedDiffs.collect { objDiff -> - Class targetClass = objDiff.left.class as Class - mergeDiff(targetClass) - .forMergingDiffable(objDiff.right) - .intoDiffable(objDiff.left) - .havingCommonAncestor(objDiff.left) - .withCommonAncestorDiffedAgainstSource(objDiff) - .withCommonAncestorDiffedAgainstTarget(objectDiff(targetClass)) - .generate() - } + static FieldMergeDiff createBaseFieldMergeDiffPresentOnOneSide(FieldDiff caSourceFieldDiff) { + fieldMergeDiff(caSourceFieldDiff.targetClass) + .forFieldName(caSourceFieldDiff.fieldName) + .withSource(caSourceFieldDiff.right) + .withCommonAncestor(caSourceFieldDiff.left) // both diffs have the same LHS } - static Collection> createCreationMergeDiffsForOneSide( + static Collection> createCreationMergeDiffsPresentOnOneSide( Collection> caSourceCreatedDiffs) { - caSourceCreatedDiffs.collect { created -> + caSourceCreatedDiffs.collect {created -> creationMergeDiff(created.targetClass) .whichCreated(created.created) } } - static Collection> createDeletionMergeDiffsForOneSide( - Collection> caSourceDeletionDiffs) { - caSourceDeletionDiffs.collect { deleted -> - deletionMergeDiff(deleted.targetClass) - .whichDeleted(deleted.deleted) - } - } - /** - * Identify all the objects in the array field created on the LHS and flag all those which + * Identify all the objects in the array field created on the LHS and flag in the logs all those which were created on both sides as they may be a merge conflict * * Important to note that due to the way diff works the same object cannot exist in more than one of created, deleted, modified in an * ArrayDIff */ - static Collection createCreationMergeDiffs(ArrayDiff caSourceDiff, - ArrayDiff caTargetDiff) { - List> creationDiffs = caSourceDiff.created.collect { diff -> + static Collection createCreationMergeDiffsPresentOnBothSides(ArrayDiff caSourceDiff, + ArrayDiff caTargetDiff) { + caSourceDiff.created.collect {diff -> if (diff.createdIdentifier in caTargetDiff.created*.createdIdentifier) { // Both sides added : potential conflict and therefore is a modified rather than create or no diff - log.debug('ca/source created {} exists in ca/target created', diff.createdIdentifier) + log.trace('[{}] ca/source created exists in ca/target created. Possible merge conflict will be rendered as a modified MergeDiff', diff.createdIdentifier) return null } if (diff.createdIdentifier in caTargetDiff.deleted*.deletedIdentifier) { @@ -231,24 +347,18 @@ class MergeDiff extends TriDirectionalDiff { return null } // Only added on source side : no conflict - log.debug('ca/source created {} doesnt exist in ca/target', diff.createdIdentifier) - return creationDiff(diff.targetClass).created(diff.value) - }.findAll() as Collection - - // creationDiffs.addAll(caSourceDiff.modified.collect { caSourceModifiedDiff -> - // DeletionDiff caTargetDeletedDiff = caTargetDiff.deleted.find { it.deletedIdentifier == caSourceModifiedDiff.leftIdentifier } - // if (caTargetDeletedDiff) { - // // Modified in source but deleted in target : conflict but should appear as creation - // return creationDiff(caSourceModifiedDiff.targetClass.arrayType() as Class) - // .created(caSourceModifiedDiff.right) - // .withMergeDeletion(caTargetDeletedDiff.deleted) - // .withCommonAncestor(caSourceDiff.left) - // .asMergeConflict() - // } - // null - // }.findAll() as Collection>) - - creationDiffs + log.debug('[{}] ca/source created doesnt exist in ca/target', diff.createdIdentifier) + return creationMergeDiff(diff.targetClass).whichCreated(diff.value) + }.findAll() as Collection + } + + + static Collection> createDeletionMergeDiffsPresentOnOneSide( + Collection> caSourceDeletionDiffs) { + caSourceDeletionDiffs.collect {deleted -> + deletionMergeDiff(deleted.targetClass) + .whichDeleted(deleted.deleted) + } } /** @@ -258,40 +368,122 @@ class MergeDiff extends TriDirectionalDiff { * ArrayDIff * */ - static Collection findAllDeletedMergeDiffs(Collection> caSourceDeletedDiffs, - ArrayDiff caTargetDiff) { - caSourceDeletedDiffs.collect { diff -> + static Collection createDeletionMergeDiffsPresentOnBothSides(Collection> caSourceDeletedDiffs, + ArrayDiff caTargetDiff) { + caSourceDeletedDiffs.collect {diff -> if (diff.deletedIdentifier in caTargetDiff.created*.createdIdentifier) { // Impossible as you can't delete something which never existed return null } if (diff.deletedIdentifier in caTargetDiff.deleted*.deletedIdentifier) { // Deleted in source, deleted in target : no conflict no diff - log.debug('ca/source deleted {} exists in ca/target deleted', diff.deletedIdentifier) + log.trace('[{}] ca/source deleted exists in ca/target deleted', diff.deletedIdentifier) return null } - ObjectDiff modifiedTargetDiff = caTargetDiff.modified.find { it.rightIdentifier == diff.deletedIdentifier } - if (modifiedTargetDiff) { + ObjectDiff caTargetModifiedDiff = caTargetDiff.modified.find {it.rightIdentifier == diff.deletedIdentifier} + if (caTargetModifiedDiff) { // Deleted in source, modified in target : conflict as deletion diff - log.debug('ca/source deleted {} exists in ca/target modified', diff.deletedIdentifier) - return deletionDiff(diff.targetClass) - .deleted(diff.value) - .withMergeModification(modifiedTargetDiff.right) - .withCommonAncestor(modifiedTargetDiff.left) + log.debug('[{}] ca/source deleted exists in ca/target modified', diff.deletedIdentifier) + return deletionMergeDiff(diff.targetClass) + .whichDeleted(diff.value) + .withMergeModification(caTargetModifiedDiff) // TODO does this work with giving the information needed??? + .withCommonAncestor(caTargetModifiedDiff.left) .asMergeConflict() } // Deleted in source but not touched in target : no conflict - log.debug('ca/source deleted {} doesnt exist in ca/target', diff.deletedIdentifier) - return deletionDiff(diff.targetClass).deleted(diff.value).withNoMergeModification() - }.findAll() as Collection + log.debug('[{}] ca/source deleted doesnt exist in ca/target', diff.deletedIdentifier) + return deletionMergeDiff(diff.targetClass).whichDeleted(diff.value).withNoMergeModification() + }.findAll() } - // static Collection findAllModifiedMergeDiffs(Collection modified, ArrayDiff sourceArrayDiff, ArrayDiff - // targetArrayDiff) { - // modified.collect { ObjectDiff objDiff -> + static Collection> createModifiedMergeDiffsPresentOnOneSide(Collection> caSourceModifiedDiffs) { + // Modified diffs represent diffs which have modifications down the chain but no changes on the target side + // Therefore we can use an empty object diff + caSourceModifiedDiffs.collect {objDiff -> + Class targetClass = objDiff.left.class as Class + mergeDiff(targetClass) + .forMergingDiffable(objDiff.right) + .intoDiffable(objDiff.left) + .havingCommonAncestor(objDiff.left) + .withCommonAncestorDiffedAgainstSource(objDiff) + .generate() + } + } + + static Collection> createModifiedMergeDiffsPresentOnBothSides(ArrayDiff caSourceDiff, ArrayDiff caTargetDiff) { + + List> modifiedDiffs = caSourceDiff.modified.collect {caSourceModifiedDiff -> + ObjectDiff caTargetModifiedDiff = caTargetDiff.modified.find {it.leftIdentifier == caSourceModifiedDiff.leftIdentifier} + if (caTargetModifiedDiff) { + log.debug('[{}] modified on both sides', caSourceModifiedDiff.leftIdentifier) + + // ObjectDiff sourceTargetDiff = caSourceModifiedDiff.right.diff(caTargetModifiedDiff.right) + // + // if (sourceTargetDiff.objectsAreIdentical()) { + // log.debug('Both sides modified but the modifications are identical') + // return null + // } + + MergeDiff sourceTargetMergeDiff = mergeDiff(caSourceModifiedDiff.targetClass) + .forMergingDiffable(caSourceModifiedDiff.right) + .intoDiffable(caTargetModifiedDiff.right) + .havingCommonAncestor(caSourceModifiedDiff.left) + .withCommonAncestorDiffedAgainstSource(caSourceModifiedDiff) + .withCommonAncestorDiffedAgainstTarget(caTargetModifiedDiff) + .generate() + // If no diffs then the modifications are the same so dont include + return sourceTargetMergeDiff.isEmpty() ? null : sourceTargetMergeDiff + + } + DeletionDiff caTargetDeletionDiff = caTargetDiff.deleted.find {it.deletedIdentifier == caSourceModifiedDiff.leftIdentifier} + if (caTargetDeletionDiff) { + log.debug('[{}] modified on ca/source side and deleted on ca/target side. NOT HANDLED', caSourceModifiedDiff.leftIdentifier) + return null + } + + log.debug('[{}] only modified on ca/source side', caSourceModifiedDiff.leftIdentifier) + mergeDiff(caSourceModifiedDiff.targetClass) + .forMergingDiffable(caSourceModifiedDiff.right) + .intoDiffable(caSourceModifiedDiff.left) + .havingCommonAncestor(caSourceModifiedDiff.left) + .withCommonAncestorDiffedAgainstSource(caSourceModifiedDiff) + .generate() + }.findAll() + + + List> createdModifiedDiffs = caSourceDiff.created.collect {caSourceCreationDiff -> + CreationDiff caTargetCreationDiff = caTargetDiff.created.find {it.createdIdentifier == caSourceCreationDiff.createdIdentifier} + if (caTargetCreationDiff) { + // Both sides added : potential conflict and therefore is a modified rather than create or no diff + log.debug('[{}] ca/source created exists in ca/target created. This is a potential merge modification', caSourceCreationDiff.createdIdentifier) + + // Get the diff of the 2 objects, we need to determine if theres actually a merge conflict + ObjectDiff sourceTargetDiff = caSourceCreationDiff.created.diff(caTargetCreationDiff.created) + // If objects are identical then theres no merge difference so it can be ignored + if (sourceTargetDiff.objectsAreIdentical()) { + log.debug('Both sides created but the creations are identical') + return null + } + + return mergeDiff(sourceTargetDiff.targetClass as Class) + .forMergingDiffable(caSourceCreationDiff.created) + .intoDiffable(caTargetCreationDiff.created) + .havingCommonAncestor(null) + .withSourceDiffedAgainstTarget(sourceTargetDiff) + .generate() + } + null + }.findAll() + + modifiedDiffs + createdModifiedDiffs + } + + // modified.collect {ObjectDiff objDiff -> // def diffIdentifier = objDiff.target.diffIdentifier - // if (diffIdentifier in sourceArrayDiff.created.value.diffIdentifier) { return updateModifiedObjectMergeDiffCreatedOnOneSide - // (objDiff) } + // if (diffIdentifier in sourceArrayDiff.created.value.diffIdentifier) { + // return updateModifiedObjectMergeDiffCreatedOnOneSide + // (objDiff) + // } // if // (diffIdentifier in sourceArrayDiff.modified.source.diffIdentifier) { // if (diffIdentifier in targetArrayDiff.modified.source.diffIdentifier) { @@ -299,18 +491,21 @@ class MergeDiff extends TriDirectionalDiff { // top modified, source modified , target modified // return createModifiedObjectMergeDiffModifiedOnBothSides(objDiff, // sourceArrayDiff.modified. - // find { it.source.diffIdentifier == diffIdentifier } as + // find {it.source.diffIdentifier == diffIdentifier} as // ObjectDiff, // targetArrayDiff.modified. - // find { it.source.diffIdentifier == diffIdentifier } as + // find {it.source.diffIdentifier == diffIdentifier} as // ObjectDiff, - // targetArrayDiff.source.find { it.diffIdentifier == - // diffIdentifier }) + // targetArrayDiff.source.find { + // it.diffIdentifier == + // diffIdentifier + // }) // } // top modified, source modified, target not modified // return updateModifiedObjectMergeDiffPresentOnOneSide(objDiff) // } null // }.findAll() - // } + + // static ObjectDiff updateModifiedObjectMergeDiffCreatedOnOneSide(ObjectDiff objectDiff) { // // top modified, target created, (source also created) // objectDiff.diffs.each { diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy index 45fe35a5f1..aad1add320 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy @@ -8,8 +8,8 @@ import groovy.transform.CompileStatic @CompileStatic abstract class TriDirectionalDiff extends BiDirectionalDiff { - Boolean mergeConflict - T commonAncestor + private Boolean mergeConflict + private T commonAncestor protected TriDirectionalDiff(Class targetClass) { super(targetClass) @@ -55,6 +55,10 @@ abstract class TriDirectionalDiff extends BiDirectionalDiff { mergeConflict } + T getCommonAncestor() { + commonAncestor + } + T getTarget() { super.getRight() } diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy index be4b597105..9e3f3edaba 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy @@ -18,8 +18,9 @@ package uk.ac.ox.softeng.maurodatamapper.datamodel import uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress -import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.ArrayMergeDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.CreationMergeDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.DeletionMergeDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.FieldMergeDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata @@ -245,7 +246,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { then: result.errors.allErrors.size() == 1 - result.errors.allErrors.find { it.code == 'invalid.version.aware.new.version.not.finalised.message' } + result.errors.allErrors.find {it.code == 'invalid.version.aware.new.version.not.finalised.message'} } void 'DMSC02 : test creating a new documentation version on finalised model'() { @@ -291,17 +292,17 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { newDocVersion.edits.size() == 1 and: 'new version of link between old and new version' - newDocVersion.versionLinks.any { it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_DOCUMENTATION_VERSION_OF } + newDocVersion.versionLinks.any {it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_DOCUMENTATION_VERSION_OF} and: - dataModel.dataTypes.every { odt -> + dataModel.dataTypes.every {odt -> newDocVersion.dataTypes.any { it.label == odt.label && it.id != odt.id && it.domainType == odt.domainType } } - dataModel.dataClasses.every { odc -> + dataModel.dataClasses.every {odc -> newDocVersion.dataClasses.any { int idcs = it.dataClasses?.size() ?: 0 int odcs = odc.dataClasses?.size() ?: 0 @@ -358,17 +359,17 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { newDocVersion.edits.size() == 1 and: 'new version of link between old and new version' - newDocVersion.versionLinks.any { it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_DOCUMENTATION_VERSION_OF } + newDocVersion.versionLinks.any {it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_DOCUMENTATION_VERSION_OF} and: - dataModel.dataTypes.every { odt -> + dataModel.dataTypes.every {odt -> newDocVersion.dataTypes.any { it.label == odt.label && it.id != odt.id && it.domainType == odt.domainType } } - dataModel.dataClasses.every { odc -> + dataModel.dataClasses.every {odc -> newDocVersion.dataClasses.any { int idcs = it.dataClasses?.size() ?: 0 int odcs = odc.dataClasses?.size() ?: 0 @@ -400,7 +401,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { then: result.errors.allErrors.size() == 1 - result.errors.allErrors.find { it.code == 'invalid.version.aware.new.version.superseded.message' } + result.errors.allErrors.find {it.code == 'invalid.version.aware.new.version.superseded.message'} } void 'DMSC05 : test creating a new fork version on draft model'() { @@ -466,14 +467,14 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { } and: - dataModel.dataTypes.every { odt -> + dataModel.dataTypes.every {odt -> newVersion.dataTypes.any { it.label == odt.label && it.id != odt.id && it.domainType == odt.domainType } } - dataModel.dataClasses.every { odc -> + dataModel.dataClasses.every {odc -> newVersion.dataClasses.any { int idcs = it.dataClasses?.size() ?: 0 int odcs = odc.dataClasses?.size() ?: 0 @@ -529,17 +530,17 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { and: 'link between old and new version' - newVersion.versionLinks.any { it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_FORK_OF } + newVersion.versionLinks.any {it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_FORK_OF} and: - dataModel.dataTypes.every { odt -> + dataModel.dataTypes.every {odt -> newVersion.dataTypes.any { it.label == odt.label && it.id != odt.id && it.domainType == odt.domainType } } - dataModel.dataClasses.every { odc -> + dataModel.dataClasses.every {odc -> newVersion.dataClasses.any { int idcs = it.dataClasses?.size() ?: 0 int odcs = odc.dataClasses?.size() ?: 0 @@ -570,7 +571,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { then: result.errors.allErrors.size() == 1 - result.errors.allErrors.find { it.code == 'invalid.version.aware.new.version.superseded.message' } + result.errors.allErrors.find {it.code == 'invalid.version.aware.new.version.superseded.message'} } void 'DMSC09 : test creating a new branch model version on draft model'() { @@ -587,7 +588,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { then: result.errors.allErrors.size() == 1 - result.errors.allErrors.find { it.code == 'invalid.version.aware.new.version.not.finalised.message' } + result.errors.allErrors.find {it.code == 'invalid.version.aware.new.version.not.finalised.message'} } void 'DMSC10 : test creating a new branch model version on finalised model'() { @@ -636,17 +637,17 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { newBranch.edits.size() == 1 and: 'new version of link between old and new version' - newBranch.versionLinks.any { it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_MODEL_VERSION_OF } + newBranch.versionLinks.any {it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_MODEL_VERSION_OF} and: - dataModel.dataTypes.every { odt -> + dataModel.dataTypes.every {odt -> newBranch.dataTypes.any { it.label == odt.label && it.id != odt.id && it.domainType == odt.domainType } } - def missing = dataModel.dataClasses.findAll { odc -> + def missing = dataModel.dataClasses.findAll {odc -> !newBranch.dataClasses.find { int idcs = it.dataClasses?.size() ?: 0 int odcs = odc.dataClasses?.size() ?: 0 @@ -707,17 +708,17 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { newBranch.edits.size() == 1 and: 'new version of link between old and new version' - newBranch.versionLinks.any { it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_MODEL_VERSION_OF } + newBranch.versionLinks.any {it.targetModelId == dataModel.id && it.linkType == VersionLinkType.NEW_MODEL_VERSION_OF} and: - dataModel.dataTypes.every { odt -> + dataModel.dataTypes.every {odt -> newBranch.dataTypes.any { it.label == odt.label && it.id != odt.id && it.domainType == odt.domainType } } - dataModel.dataClasses.every { odc -> + dataModel.dataClasses.every {odc -> newBranch.dataClasses.any { int idcs = it.dataClasses?.size() ?: 0 int odcs = odc.dataClasses?.size() ?: 0 @@ -749,7 +750,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { then: result.errors.allErrors.size() == 1 - result.errors.allErrors.find { it.code == 'invalid.version.aware.new.version.superseded.message' } + result.errors.allErrors.find {it.code == 'invalid.version.aware.new.version.superseded.message'} } void 'DMSC13 : test creating a new branch model version using main branch name when it already exists'() { @@ -775,7 +776,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { then: result.errors.allErrors.size() == 1 - result.errors.allErrors.find { it.code == 'version.aware.label.branch.name.already.exists' } + result.errors.allErrors.find {it.code == 'version.aware.label.branch.name.already.exists'} } void 'DMSF01 : test finding common ancestor of two datamodels'() { @@ -936,8 +937,8 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { then: availableBranches.size() == 2 - availableBranches.each { it.id in [draftModel.id, testModel.id] } - availableBranches.each { it.label == dataModel.label } + availableBranches.each {it.id in [draftModel.id, testModel.id]} + availableBranches.each {it.label == dataModel.label} } @@ -945,18 +946,18 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { given: setupData() - when: + when: 'generate common ancestor' DataModel dataModel = dataModelService.get(id) dataModel.author = 'john' dataModel.addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteSourceOnly')) .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteTargetOnly')) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifySourceOnly')) + .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifySourceOnly', description: 'common')) .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifyTargetOnly')) .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteBoth')) .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteSourceAndModifyTarget')) .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifySourceAndDeleteTarget')) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifyBothReturningNoDifference')) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifyBothReturningDifference')) + .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifyBothReturningNoDifference', description: 'common')) + .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifyBothReturningDifference', description: 'common')) dataModel.addToDataClasses( new DataClass(createdByUser: admin, label: 'existingClass') .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteSourceOnlyFromExistingClass')) @@ -969,7 +970,8 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { then: dataModel.branchName == VersionAwareConstraints.DEFAULT_BRANCH_NAME - when: + + when: 'Generate main/target branch' UUID rightMainId = createAndSaveNewBranchModel(VersionAwareConstraints.DEFAULT_BRANCH_NAME, dataModel) dataClassService.delete(dataClassService.findByDataModelIdAndLabel(rightMainId, 'deleteTargetOnlyFromExistingClass')) @@ -995,8 +997,8 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { DataModel draftModel = dataModelService.get(rightMainId) draftModel.author = 'dick' - checkAndSave new DataClass(createdByUser: admin, label: 'targetParentDataClass', dataModel: draftModel) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'targetChildDataClass', dataModel: draftModel)) + checkAndSave new DataClass(createdByUser: admin, label: 'addTargetWithNestedChild', dataModel: draftModel) + .addToDataClasses(new DataClass(createdByUser: admin, label: 'addTargetNestedChild', dataModel: draftModel)) checkAndSave new DataClass(createdByUser: admin, label: 'addTargetOnly', dataModel: draftModel) checkAndSave new DataClass(createdByUser: admin, label: 'addBothReturningNoDifference', dataModel: draftModel) checkAndSave new DataClass(createdByUser: admin, label: 'addBothReturningDifference', description: 'target', dataModel: draftModel) @@ -1008,13 +1010,15 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { sessionFactory.currentSession.flush() sessionFactory.currentSession.clear() + + and: 'Generate test/source branch' UUID leftTestId = createAndSaveNewBranchModel('test', dataModel) dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteSourceOnlyFromExistingClass')) dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteSourceOnly')) dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteBoth')) dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteSourceAndModifyTarget')) - metadataService.delete(metadataService.findAllByMultiFacetAwareItemId(leftTestId).find { it.key == 'deleteSourceOnly' }) + metadataService.delete(metadataService.findAllByMultiFacetAwareItemId(leftTestId).find {it.key == 'deleteSourceOnly'}) checkAndSave dataClassService.findByDataModelIdAndLabel(leftTestId, 'modifySourceOnly').tap { description = 'Description' @@ -1035,8 +1039,8 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { DataModel testModel = dataModelService.get(leftTestId) testModel.organisation = 'under test' testModel.author = 'harry' - checkAndSave new DataClass(createdByUser: admin, label: 'targetParentDataClass', dataModel: testModel) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'targetChildDataClass', dataModel: testModel)) + checkAndSave new DataClass(createdByUser: admin, label: 'addSourceWithNestedChild', dataModel: testModel) + .addToDataClasses(new DataClass(createdByUser: admin, label: 'addSourceNestedChild', dataModel: testModel)) checkAndSave new DataClass(createdByUser: admin, label: 'addSourceOnly', dataModel: testModel) checkAndSave new DataClass(createdByUser: admin, label: 'addBothReturningNoDifference', dataModel: testModel) @@ -1044,7 +1048,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { checkAndSave new PrimitiveType(createdBy: StandardEmailAddress.ADMIN, label: 'addSourceOnlyOnlyChangeInArray', dataModel: testModel) - checkAndSave metadataService.findAllByMultiFacetAwareItemId(leftTestId).find { it.key == 'modifySourceOnly' }.tap { + checkAndSave metadataService.findAllByMultiFacetAwareItemId(leftTestId).find {it.key == 'modifySourceOnly'}.tap { value = 'altered' } @@ -1052,17 +1056,18 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { sessionFactory.currentSession.flush() sessionFactory.currentSession.clear() + and: 'load models from databse' DataModel rightMain = dataModelService.get(rightMainId) DataModel leftTest = dataModelService.get(leftTestId) MergeDiff mergeDiff = dataModelService.getMergeDiffForModels(leftTest, rightMain) then: - mergeDiff.diffs + !mergeDiff.isEmpty() // mergeDiff.numberOfDiffs == 11 when: 'branch name is a non-conflicting diff' - FieldMergeDiff stringFieldDiff = mergeDiff.find { it.fieldName == 'branchName' } + FieldMergeDiff stringFieldDiff = mergeDiff.find {it.fieldName == 'branchName'} then: stringFieldDiff.source == 'test' @@ -1071,7 +1076,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { !stringFieldDiff.isMergeConflict() when: 'organisation is a non-conflicting change' - stringFieldDiff = mergeDiff.find { it.fieldName == 'organisation' } + stringFieldDiff = mergeDiff.find {it.fieldName == 'organisation'} then: stringFieldDiff.source == 'under test' @@ -1080,7 +1085,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { !stringFieldDiff.isMergeConflict() when: 'author is a conflicting change' - stringFieldDiff = mergeDiff.find { it.fieldName == 'author' } + stringFieldDiff = mergeDiff.find {it.fieldName == 'author'} then: stringFieldDiff.source == 'harry' @@ -1089,7 +1094,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { stringFieldDiff.isMergeConflict() when: 'single array change in datatypes' - ArrayMergeDiff dataTypeDiff = mergeDiff.find { it.fieldName == 'dataTypes' } as ArrayMergeDiff + ArrayMergeDiff dataTypeDiff = mergeDiff.find {it.fieldName == 'dataTypes'} as ArrayMergeDiff then: dataTypeDiff.deleted.isEmpty() @@ -1101,7 +1106,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { !dataTypeDiff.created.first().target when: 'metadata has array diffs' - ArrayMergeDiff metadataDiff = mergeDiff.find { it.fieldName == 'metadata' } as ArrayMergeDiff + ArrayMergeDiff metadataDiff = mergeDiff.find {it.fieldName == 'metadata'} as ArrayMergeDiff then: metadataDiff.source.size() == 1 @@ -1112,61 +1117,133 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { metadataDiff.deleted.first().deletedIdentifier == 'test.deleteSourceOnly' metadataDiff.modified.size() == 1 metadataDiff.modified.first().sourceIdentifier == 'test.modifySourceOnly' - metadataDiff.modified.first().diffs.size() == 1 - metadataDiff.modified.first().diffs.first().fieldName == 'value' - metadataDiff.modified.first().diffs.first().source == 'altered' - metadataDiff.modified.first().diffs.first().target == 'modifySourceOnly' - metadataDiff.modified.first().diffs.first().commonAncestor == 'modifySourceOnly' - !metadataDiff.modified.first().diffs.first().isMergeConflict() - - - and: 'array diffs on the dataclass list' - ArrayMergeDiff dataClassesDiff = mergeDiff.diffs.find { it.fieldName == 'dataClasses' } as ArrayMergeDiff - dataClassesDiff.created.size() == 1 - // dataClassesDiff.deleted.size() == 2 + metadataDiff.modified.first().size() == 1 + metadataDiff.modified.first().first().fieldName == 'value' + metadataDiff.modified.first().first().source == 'altered' + metadataDiff.modified.first().first().target == 'modifySourceOnly' + metadataDiff.modified.first().first().commonAncestor == 'modifySourceOnly' + !metadataDiff.modified.first().first().isMergeConflict() + + + when: 'array diffs on the dataclass list' + ArrayMergeDiff dataClassesDiff = mergeDiff.find {it.fieldName == 'dataClasses'} as ArrayMergeDiff + + then: + dataClassesDiff.created.size() == 2 + dataClassesDiff.deleted.size() == 2 // dataClassesDiff.modified.size() == 4 - and: 'created' - dataClassesDiff.created.value.label as Set == ['addSourceOnly'] as Set - !dataClassesDiff.created.find { it.createdIdentifier == 'addSourceOnly' }.isMergeConflict() - !dataClassesDiff.created.find { it.createdIdentifier == 'addSourceOnly' }.commonAncestor - // - // !dataClassesDiff.created.find { it.value.label == 'targetParentDataClass' }.isMergeConflict() - // !dataClassesDiff.created.find { it.value.label == 'targetParentDataClass' }.commonAncestor - // - // dataClassesDiff.created.find { it.value.label == 'modifyAndDelete' }.isMergeConflict() - // dataClassesDiff.created.find { it.value.label == 'modifyAndDelete' }.commonAncestor + when: 'created on source side' + CreationMergeDiff creationMergeDiff = dataClassesDiff.created.find {it.createdIdentifier == 'addSourceOnly'} + + then: + creationMergeDiff + creationMergeDiff.created + !creationMergeDiff.isMergeConflict() + !creationMergeDiff.commonAncestor + + when: 'created with nested creation on source side only' + creationMergeDiff = dataClassesDiff.created.find {it.createdIdentifier == 'addSourceWithNestedChild'} + + then: + creationMergeDiff + creationMergeDiff.created //TODO more info in gson as we need to include the nested child + !creationMergeDiff.isMergeConflict() + !creationMergeDiff.commonAncestor + + when: + DeletionMergeDiff deleteSourceOnly = dataClassesDiff.deleted.find {it.deletedIdentifier == 'deleteSourceOnly'} + DeletionMergeDiff deleteAndModify = dataClassesDiff.deleted.find {it.deletedIdentifier == 'deleteSourceAndModifyTarget'} + + then: + deleteSourceOnly + deleteSourceOnly.deleted + !deleteSourceOnly.isMergeConflict() + deleteSourceOnly.commonAncestor and: - dataClassesDiff.deleted.value.label as Set == ['deleteSourceAndModifyTarget', 'deleteSourceOnly'] as Set - dataClassesDiff.deleted.find { it.value.label == 'deleteSourceAndModifyTarget' }.isMergeConflict() - dataClassesDiff.deleted.find { it.value.label == 'deleteSourceAndModifyTarget' }.commonAncestor - dataClassesDiff.deleted.find { it.value.label == 'deleteSourceAndModifyTarget' }.right - !dataClassesDiff.deleted.find { it.value.label == 'deleteSourceOnly' }.isMergeConflict() - !dataClassesDiff.deleted.find { it.value.label == 'deleteSourceOnly' }.commonAncestor + deleteAndModify + deleteAndModify.deleted + deleteAndModify.isMergeConflict() + deleteAndModify.commonAncestor + deleteAndModify.mergeModificationDiff and: - dataClassesDiff.modified.left.diffIdentifier as Set == ['existingClass', 'modifyAndModifyReturningDifference', 'modifySourceOnly', - 'addAndAddReturningDifference'] as Set - dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyAndModifyReturningDifference' }.diffs[0].fieldName == 'description' - dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyAndModifyReturningDifference' }.isMergeConflict() - dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyAndModifyReturningDifference' }.commonAncestor - dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].fieldName == 'dataClasses' - dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.isMergeConflict() - dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.commonAncestor - dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].created[0].value.label == 'addSourceToExistingClass' - dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].deleted[0].value.label == - 'deleteSourceOnlyFromExistingClass' - !dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].created[0].isMergeConflict() - !dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].created[0].commonAncestorValue - !dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].deleted[0].isMergeConflict() - !dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].deleted[0].commonAncestorValue - dataClassesDiff.modified.find { it.left.diffIdentifier == 'addAndAddReturningDifference' }.diffs[0].fieldName == 'description' - dataClassesDiff.modified.find { it.left.diffIdentifier == 'addAndAddReturningDifference' }.diffs[0].isMergeConflict() - !dataClassesDiff.modified.find { it.left.diffIdentifier == 'addAndAddReturningDifference' }.diffs[0].commonAncestor - dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifySourceOnly' }.diffs[0].fieldName == 'description' - !dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifySourceOnly' }.diffs[0].isMergeConflict() - !dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifySourceOnly' }.diffs[0].commonAncestor + deleteAndModify.mergeModificationDiff //TODO more info + + + when: + MergeDiff addBothReturningDifferenceMerge = dataClassesDiff.modified.find {it.sourceIdentifier == 'addBothReturningDifference'} + + then: + addBothReturningDifferenceMerge + addBothReturningDifferenceMerge.size() == 1 + addBothReturningDifferenceMerge.first().diffType == 'FieldMergeDiff' + addBothReturningDifferenceMerge.first().fieldName == 'description' + addBothReturningDifferenceMerge.first().isMergeConflict() + addBothReturningDifferenceMerge.first().source == 'source' + addBothReturningDifferenceMerge.first().target == 'target' + !addBothReturningDifferenceMerge.first().commonAncestor + + when: + MergeDiff modifySourceOnlyMerge = dataClassesDiff.modified.find {it.sourceIdentifier == 'modifySourceOnly'} + + then: + modifySourceOnlyMerge + modifySourceOnlyMerge.size() == 1 + modifySourceOnlyMerge.first().diffType == 'FieldMergeDiff' + modifySourceOnlyMerge.first().fieldName == 'description' + !modifySourceOnlyMerge.first().isMergeConflict() + modifySourceOnlyMerge.first().source == 'Description' + modifySourceOnlyMerge.first().target == 'common' + modifySourceOnlyMerge.first().commonAncestor == 'common' + + + when: + MergeDiff modifyBothNoDifferenceMerge = dataClassesDiff.modified.find {it.sourceIdentifier == 'modifyBothReturningNoDifference'} + MergeDiff modifyBothWithDifferenceMerge = dataClassesDiff.modified.find {it.sourceIdentifier == 'modifyBothReturningDifference'} + + then: + !modifyBothNoDifferenceMerge + + and: + modifyBothWithDifferenceMerge.size() == 1 + modifyBothWithDifferenceMerge.isMergeConflict() + modifyBothWithDifferenceMerge.first().fieldName == 'description' + modifyBothWithDifferenceMerge.first().isMergeConflict() + modifyBothWithDifferenceMerge.first().source == 'DescriptionSource' + modifyBothWithDifferenceMerge.first().target == 'DescriptionTarget' + modifyBothWithDifferenceMerge.first().commonAncestor == 'common' + + + when: + MergeDiff existingClassMerge = dataClassesDiff.modified.find {it.sourceIdentifier == 'existingClass'} + + then: + existingClassMerge + existingClassMerge.isMergeConflict() + existingClassMerge.source.dataClasses.size() == 2 + existingClassMerge.target.dataClasses.size() == 2 + existingClassMerge.commonAncestor.dataClasses.size() == 2 + existingClassMerge.size() == 1 + existingClassMerge.first().fieldName == 'dataClasses' + existingClassMerge.first().diffType == 'ArrayMergeDiff' + + when: + ArrayMergeDiff existingClassDiff = existingClassMerge.first() as ArrayMergeDiff + + then: + existingClassDiff.modified.isEmpty() + existingClassDiff.created.size() == 1 + existingClassDiff.created.first().createdIdentifier == 'existingClass/addSourceToExistingClass' + !existingClassDiff.created.first().isMergeConflict() + !existingClassDiff.created.first().commonAncestor + + existingClassDiff.deleted.size() == 1 + existingClassDiff.deleted.size() == 1 + existingClassDiff.deleted.first().deletedIdentifier == 'existingClass/deleteSourceOnlyFromExistingClass' + !existingClassDiff.deleted.first().isMergeConflict() + existingClassDiff.deleted.first().commonAncestor } void 'DMSM02 : test merging diff into draft model'() { @@ -1671,7 +1748,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { results.size() == 3 when: - DataElementSimilarityResult childRes = results.find { it.source.label == 'child' } + DataElementSimilarityResult childRes = results.find {it.source.label == 'child'} DataElementSimilarityResult ele1Res = results.find { it.source.label == 'ele1' } DataElementSimilarityResult ele2Res = results.find { it.source.label == 'element2' } From 38ba0061d03b50e5016acb6886d62e9394269ae7 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Wed, 23 Jun 2021 11:31:33 +0100 Subject: [PATCH 16/82] MergeDiff working and fully tested * Also pull out the model generaiton code as we use the same code twice for generating the diff and testing applying it --- .../core/diff/tridirectional/MergeDiff.groovy | 79 +---- .../bootstrap/BootstrapModels.groovy | 136 ++++++++ .../DataModelServiceIntegrationSpec.groovy | 326 +++--------------- 3 files changed, 202 insertions(+), 339 deletions(-) diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy index db8598eca5..0b9226644c 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy @@ -332,7 +332,18 @@ class MergeDiff extends TriDirectionalDiff { */ static Collection createCreationMergeDiffsPresentOnBothSides(ArrayDiff caSourceDiff, ArrayDiff caTargetDiff) { - caSourceDiff.created.collect {diff -> + List modificationCreationMergeDiffs = caSourceDiff.modified.collect {caSourceModificationDiff -> + DeletionDiff caTargetDeletionDiff = caTargetDiff.deleted.find {it.deletedIdentifier == caSourceModificationDiff.leftIdentifier} + if (caTargetDeletionDiff) { + log.debug('[{}] ca/source modified exists in ca/target deleted.') + return creationMergeDiff(caSourceModificationDiff.targetClass) + .whichCreated(caSourceModificationDiff.right) + .withCommonAncestor(caSourceModificationDiff.left) + .asMergeConflict() + } + null + }.findAll() as Collection + List creationMergeDiffs = caSourceDiff.created.collect {diff -> if (diff.createdIdentifier in caTargetDiff.created*.createdIdentifier) { // Both sides added : potential conflict and therefore is a modified rather than create or no diff log.trace('[{}] ca/source created exists in ca/target created. Possible merge conflict will be rendered as a modified MergeDiff', diff.createdIdentifier) @@ -350,6 +361,7 @@ class MergeDiff extends TriDirectionalDiff { log.debug('[{}] ca/source created doesnt exist in ca/target', diff.createdIdentifier) return creationMergeDiff(diff.targetClass).whichCreated(diff.value) }.findAll() as Collection + modificationCreationMergeDiffs + creationMergeDiffs } @@ -417,13 +429,6 @@ class MergeDiff extends TriDirectionalDiff { if (caTargetModifiedDiff) { log.debug('[{}] modified on both sides', caSourceModifiedDiff.leftIdentifier) - // ObjectDiff sourceTargetDiff = caSourceModifiedDiff.right.diff(caTargetModifiedDiff.right) - // - // if (sourceTargetDiff.objectsAreIdentical()) { - // log.debug('Both sides modified but the modifications are identical') - // return null - // } - MergeDiff sourceTargetMergeDiff = mergeDiff(caSourceModifiedDiff.targetClass) .forMergingDiffable(caSourceModifiedDiff.right) .intoDiffable(caTargetModifiedDiff.right) @@ -437,7 +442,7 @@ class MergeDiff extends TriDirectionalDiff { } DeletionDiff caTargetDeletionDiff = caTargetDiff.deleted.find {it.deletedIdentifier == caSourceModifiedDiff.leftIdentifier} if (caTargetDeletionDiff) { - log.debug('[{}] modified on ca/source side and deleted on ca/target side. NOT HANDLED', caSourceModifiedDiff.leftIdentifier) + log.debug('[{}] modified on ca/source side and deleted on ca/target side. TREATED AS CREATION', caSourceModifiedDiff.leftIdentifier) return null } @@ -477,60 +482,4 @@ class MergeDiff extends TriDirectionalDiff { modifiedDiffs + createdModifiedDiffs } - - // modified.collect {ObjectDiff objDiff -> - // def diffIdentifier = objDiff.target.diffIdentifier - // if (diffIdentifier in sourceArrayDiff.created.value.diffIdentifier) { - // return updateModifiedObjectMergeDiffCreatedOnOneSide - // (objDiff) - // } - // if - // (diffIdentifier in sourceArrayDiff.modified.source.diffIdentifier) { - // if (diffIdentifier in targetArrayDiff.modified.source.diffIdentifier) { - // // - // top modified, source modified , target modified - // return createModifiedObjectMergeDiffModifiedOnBothSides(objDiff, - // sourceArrayDiff.modified. - // find {it.source.diffIdentifier == diffIdentifier} as - // ObjectDiff, - // targetArrayDiff.modified. - // find {it.source.diffIdentifier == diffIdentifier} as - // ObjectDiff, - // targetArrayDiff.source.find { - // it.diffIdentifier == - // diffIdentifier - // }) - // } // top modified, source modified, target not modified - // return updateModifiedObjectMergeDiffPresentOnOneSide(objDiff) - // } null - // }.findAll() - - - // static ObjectDiff updateModifiedObjectMergeDiffCreatedOnOneSide(ObjectDiff objectDiff) { - // // top modified, target created, (source also created) - // objectDiff.diffs.each { - // it.isMergeConflict = true - // it.commonAncestorValue = null - // } objectDiff . isMergeConflict = true - // objectDiff.commonAncestorValue = null - // return objectDiff - // } - // - // static ObjectDiff createModifiedObjectMergeDiffModifiedOnBothSides(ObjectDiff objectDiff, ObjectDiff sourceObjDiff, - // ObjectDiff targetObjDiff, - // Object commonAncestorValue) { - // // call recursively - // ObjectDiff mergeDiff = objectDiff.mergeDiff(sourceObjDiff, targetObjDiff) - // mergeDiff.isMergeConflict = true - // mergeDiff.commonAncestorValue = commonAncestorValue - // return mergeDiff - // } - // - // static ObjectDiff updateModifiedObjectMergeDiffPresentOnOneSide(ObjectDiff objectDiff) { - // objectDiff.diffs.each { - // it.isMergeConflict = false - // } - // objectDiff.isMergeConflict = false - // objectDiff - // } } diff --git a/mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/bootstrap/BootstrapModels.groovy b/mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/bootstrap/BootstrapModels.groovy index 7ecad5be37..c1cbacff08 100644 --- a/mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/bootstrap/BootstrapModels.groovy +++ b/mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/bootstrap/BootstrapModels.groovy @@ -17,13 +17,17 @@ */ package uk.ac.ox.softeng.maurodatamapper.datamodel.bootstrap +import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInvalidModelException import uk.ac.ox.softeng.maurodatamapper.core.authority.Authority +import uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier import uk.ac.ox.softeng.maurodatamapper.core.container.Folder import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata +import uk.ac.ox.softeng.maurodatamapper.core.facet.MetadataService import uk.ac.ox.softeng.maurodatamapper.core.facet.Rule import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLink import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkType +import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModelService import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClass @@ -40,6 +44,7 @@ import uk.ac.ox.softeng.maurodatamapper.util.Version import asset.pipeline.grails.AssetResourceLocator import groovy.util.logging.Slf4j +import org.hibernate.SessionFactory import org.springframework.context.MessageSource import org.springframework.core.io.Resource @@ -47,7 +52,9 @@ import java.time.OffsetDateTime import java.time.ZoneOffset import static uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress.DEVELOPMENT +import static uk.ac.ox.softeng.maurodatamapper.util.GormUtils.check import static uk.ac.ox.softeng.maurodatamapper.util.GormUtils.checkAndSave +import static uk.ac.ox.softeng.maurodatamapper.util.GormUtils.outputDomainErrors @Slf4j class BootstrapModels { @@ -621,4 +628,133 @@ v1 --------------------------- v2 -- v3 -- v4 --------------- v5 --- main } + + static Map buildMergeModelsForTestingOnly(UUID id, User creator, DataModelService dataModelService, DataClassService dataClassService, + MetadataService metadataService, SessionFactory sessionFactory, MessageSource messageSource) { + // generate common ancestor + UserSecurityPolicyManager policyManager = PublicAccessSecurityPolicyManager.instance + DataModel commonAncestor = dataModelService.get(id) + commonAncestor.author = 'john' + commonAncestor.addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'deleteSourceOnly')) + .addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'deleteTargetOnly')) + .addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'modifySourceOnly', description: 'common')) + .addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'modifyTargetOnly')) + .addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'deleteBoth')) + .addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'deleteSourceAndModifyTarget')) + .addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'modifySourceAndDeleteTarget')) + .addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'modifyBothReturningNoDifference', description: 'common')) + .addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'modifyBothReturningDifference', description: 'common')) + commonAncestor.addToDataClasses( + new DataClass(createdBy: creator.emailAddress, label: 'existingClass') + .addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'deleteSourceOnlyFromExistingClass')) + .addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'deleteTargetOnlyFromExistingClass')) + ).addToMetadata(namespace: 'test', key: 'deleteSourceOnly', value: 'deleteSourceOnly') + .addToMetadata(namespace: 'test', key: 'modifySourceOnly', value: 'modifySourceOnly') + dataModelService.finaliseModel(commonAncestor, creator, null, null, null) + checkAndSave(messageSource, commonAncestor) + + assert commonAncestor.branchName == VersionAwareConstraints.DEFAULT_BRANCH_NAME + + + // Generate main/target branch + UUID rightMainId = createAndSaveNewBranchModel(VersionAwareConstraints.DEFAULT_BRANCH_NAME, commonAncestor, creator, dataModelService, + messageSource, policyManager) + + dataClassService.delete(dataClassService.findByDataModelIdAndLabel(rightMainId, 'deleteTargetOnlyFromExistingClass')) + dataClassService.delete(dataClassService.findByDataModelIdAndLabel(rightMainId, 'deleteTargetOnly')) + dataClassService.delete(dataClassService.findByDataModelIdAndLabel(rightMainId, 'deleteBoth')) + dataClassService.delete(dataClassService.findByDataModelIdAndLabel(rightMainId, 'modifySourceAndDeleteTarget')) + + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(rightMainId, 'modifyTargetOnly').tap { + description = 'Description' + } + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(rightMainId, 'deleteSourceAndModifyTarget').tap { + description = 'Description' + } + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(rightMainId, 'modifyBothReturningNoDifference').tap { + description = 'Description' + } + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(rightMainId, 'modifyBothReturningDifference').tap { + description = 'DescriptionTarget' + } + + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(rightMainId, 'existingClass') + .addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'addTargetToExistingClass')) + + DataModel draftModel = dataModelService.get(rightMainId) + draftModel.author = 'dick' + checkAndSave messageSource, new DataClass(createdBy: creator.emailAddress, label: 'addTargetWithNestedChild', dataModel: draftModel) + .addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'addTargetNestedChild', dataModel: draftModel)) + checkAndSave messageSource, new DataClass(createdBy: creator.emailAddress, label: 'addTargetOnly', dataModel: draftModel) + checkAndSave messageSource, new DataClass(createdBy: creator.emailAddress, label: 'addBothReturningNoDifference', dataModel: draftModel) + checkAndSave messageSource, new DataClass(createdBy: creator.emailAddress, label: 'addBothReturningDifference', description: 'target', dataModel: draftModel) + + checkAndSave messageSource, dataModelService.get(rightMainId).tap { + description = 'DescriptionTarget' + } + + sessionFactory.currentSession.flush() + sessionFactory.currentSession.clear() + + + // Generate test/source branch + UUID leftTestId = createAndSaveNewBranchModel('test', commonAncestor, creator, dataModelService, messageSource, policyManager) + + dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteSourceOnlyFromExistingClass')) + dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteSourceOnly')) + dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteBoth')) + dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteSourceAndModifyTarget')) + metadataService.delete(metadataService.findAllByMultiFacetAwareItemId(leftTestId).find {it.key == 'deleteSourceOnly'}) + + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(leftTestId, 'modifySourceOnly').tap { + description = 'Description' + } + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(leftTestId, 'modifySourceAndDeleteTarget').tap { + description = 'Description' + } + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(leftTestId, 'modifyBothReturningNoDifference').tap { + description = 'Description' + } + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(leftTestId, 'modifyBothReturningDifference').tap { + description = 'DescriptionSource' + } + + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(leftTestId, 'existingClass') + .addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'addSourceToExistingClass')) + + DataModel testModel = dataModelService.get(leftTestId) + testModel.organisation = 'under test' + testModel.author = 'harry' + checkAndSave messageSource, new DataClass(createdBy: creator.emailAddress, label: 'addSourceWithNestedChild', dataModel: testModel) + .addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'addSourceNestedChild', dataModel: testModel)) + + checkAndSave messageSource, new DataClass(createdBy: creator.emailAddress, label: 'addSourceOnly', dataModel: testModel) + checkAndSave messageSource, new DataClass(createdBy: creator.emailAddress, label: 'addBothReturningNoDifference', dataModel: testModel) + checkAndSave messageSource, new DataClass(createdBy: creator.emailAddress, label: 'addBothReturningDifference', description: 'source', dataModel: testModel) + checkAndSave messageSource, new PrimitiveType(createdBy: StandardEmailAddress.ADMIN, label: 'addSourceOnlyOnlyChangeInArray', dataModel: testModel) + + + checkAndSave messageSource, metadataService.findAllByMultiFacetAwareItemId(leftTestId).find {it.key == 'modifySourceOnly'}.tap { + value = 'altered' + } + + sessionFactory.currentSession.flush() + sessionFactory.currentSession.clear() + + [commonAncestorId: commonAncestor.id, + sourceId : leftTestId, + targetId : rightMainId] + } + + static UUID createAndSaveNewBranchModel(String branchName, DataModel base, User creator, DataModelService dataModelService, MessageSource messageSource, + UserSecurityPolicyManager userSecurityPolicyManager) { + DataModel dataModel = dataModelService.createNewBranchModelVersion(branchName, base, creator, false, userSecurityPolicyManager) + if (dataModel.hasErrors()) { + outputDomainErrors(messageSource, dataModel) + throw new ApiInvalidModelException('BM01', 'Could not create new branch version', dataModel.errors) + } + check(messageSource, dataModel) + dataModelService.saveModelWithContent(dataModel) + dataModel.id + } } \ No newline at end of file diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy index 9e3f3edaba..6474c49dc0 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy @@ -17,7 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.datamodel -import uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress + import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.ArrayMergeDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.CreationMergeDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.DeletionMergeDiff @@ -30,6 +30,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwa import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.MergeFieldDiffData import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.MergeItemData import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.MergeObjectDiffData +import uk.ac.ox.softeng.maurodatamapper.datamodel.bootstrap.BootstrapModels import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClass import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClassService import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataElement @@ -941,130 +942,20 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { availableBranches.each {it.label == dataModel.label} } - void 'DMSM01 : test finding merge difference between two datamodels'() { given: setupData() - when: 'generate common ancestor' - DataModel dataModel = dataModelService.get(id) - dataModel.author = 'john' - dataModel.addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteSourceOnly')) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteTargetOnly')) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifySourceOnly', description: 'common')) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifyTargetOnly')) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteBoth')) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteSourceAndModifyTarget')) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifySourceAndDeleteTarget')) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifyBothReturningNoDifference', description: 'common')) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'modifyBothReturningDifference', description: 'common')) - dataModel.addToDataClasses( - new DataClass(createdByUser: admin, label: 'existingClass') - .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteSourceOnlyFromExistingClass')) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteTargetOnlyFromExistingClass')) - ).addToMetadata(namespace: 'test', key: 'deleteSourceOnly', value: 'deleteSourceOnly') - .addToMetadata(namespace: 'test', key: 'modifySourceOnly', value: 'modifySourceOnly') - dataModelService.finaliseModel(dataModel, admin, null, null, null) - checkAndSave(dataModel) - - then: - dataModel.branchName == VersionAwareConstraints.DEFAULT_BRANCH_NAME - - - when: 'Generate main/target branch' - UUID rightMainId = createAndSaveNewBranchModel(VersionAwareConstraints.DEFAULT_BRANCH_NAME, dataModel) - - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(rightMainId, 'deleteTargetOnlyFromExistingClass')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(rightMainId, 'deleteTargetOnly')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(rightMainId, 'deleteBoth')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(rightMainId, 'modifySourceAndDeleteTarget')) - - checkAndSave dataClassService.findByDataModelIdAndLabel(rightMainId, 'modifyTargetOnly').tap { - description = 'Description' - } - checkAndSave dataClassService.findByDataModelIdAndLabel(rightMainId, 'deleteSourceAndModifyTarget').tap { - description = 'Description' - } - checkAndSave dataClassService.findByDataModelIdAndLabel(rightMainId, 'modifyBothReturningNoDifference').tap { - description = 'Description' - } - checkAndSave dataClassService.findByDataModelIdAndLabel(rightMainId, 'modifyBothReturningDifference').tap { - description = 'DescriptionTarget' - } - - checkAndSave dataClassService.findByDataModelIdAndLabel(rightMainId, 'existingClass') - .addToDataClasses(new DataClass(createdByUser: admin, label: 'addTargetToExistingClass')) - - DataModel draftModel = dataModelService.get(rightMainId) - draftModel.author = 'dick' - checkAndSave new DataClass(createdByUser: admin, label: 'addTargetWithNestedChild', dataModel: draftModel) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'addTargetNestedChild', dataModel: draftModel)) - checkAndSave new DataClass(createdByUser: admin, label: 'addTargetOnly', dataModel: draftModel) - checkAndSave new DataClass(createdByUser: admin, label: 'addBothReturningNoDifference', dataModel: draftModel) - checkAndSave new DataClass(createdByUser: admin, label: 'addBothReturningDifference', description: 'target', dataModel: draftModel) - - checkAndSave dataModelService.get(rightMainId).tap { - description = 'DescriptionTarget' - } - - sessionFactory.currentSession.flush() - sessionFactory.currentSession.clear() - - - and: 'Generate test/source branch' - UUID leftTestId = createAndSaveNewBranchModel('test', dataModel) - - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteSourceOnlyFromExistingClass')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteSourceOnly')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteBoth')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteSourceAndModifyTarget')) - metadataService.delete(metadataService.findAllByMultiFacetAwareItemId(leftTestId).find {it.key == 'deleteSourceOnly'}) - - checkAndSave dataClassService.findByDataModelIdAndLabel(leftTestId, 'modifySourceOnly').tap { - description = 'Description' - } - checkAndSave dataClassService.findByDataModelIdAndLabel(leftTestId, 'modifySourceAndDeleteTarget').tap { - description = 'Description' - } - checkAndSave dataClassService.findByDataModelIdAndLabel(leftTestId, 'modifyBothReturningNoDifference').tap { - description = 'Description' - } - checkAndSave dataClassService.findByDataModelIdAndLabel(leftTestId, 'modifyBothReturningDifference').tap { - description = 'DescriptionSource' - } - - checkAndSave dataClassService.findByDataModelIdAndLabel(leftTestId, 'existingClass') - .addToDataClasses(new DataClass(createdByUser: admin, label: 'addSourceToExistingClass')) - - DataModel testModel = dataModelService.get(leftTestId) - testModel.organisation = 'under test' - testModel.author = 'harry' - checkAndSave new DataClass(createdByUser: admin, label: 'addSourceWithNestedChild', dataModel: testModel) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'addSourceNestedChild', dataModel: testModel)) - - checkAndSave new DataClass(createdByUser: admin, label: 'addSourceOnly', dataModel: testModel) - checkAndSave new DataClass(createdByUser: admin, label: 'addBothReturningNoDifference', dataModel: testModel) - checkAndSave new DataClass(createdByUser: admin, label: 'addBothReturningDifference', description: 'source', dataModel: testModel) - checkAndSave new PrimitiveType(createdBy: StandardEmailAddress.ADMIN, label: 'addSourceOnlyOnlyChangeInArray', dataModel: testModel) - - - checkAndSave metadataService.findAllByMultiFacetAwareItemId(leftTestId).find {it.key == 'modifySourceOnly'}.tap { - value = 'altered' - } - - - sessionFactory.currentSession.flush() - sessionFactory.currentSession.clear() - - and: 'load models from databse' - DataModel rightMain = dataModelService.get(rightMainId) - DataModel leftTest = dataModelService.get(leftTestId) - + when: 'generate models' + Map mergeData = BootstrapModels.buildMergeModelsForTestingOnly(id, admin, dataModelService, dataClassService, metadataService, sessionFactory, + messageSource) + DataModel rightMain = dataModelService.get(mergeData.targetId) + DataModel leftTest = dataModelService.get(mergeData.sourceId) MergeDiff mergeDiff = dataModelService.getMergeDiffForModels(leftTest, rightMain) then: !mergeDiff.isEmpty() - // mergeDiff.numberOfDiffs == 11 + mergeDiff.numberOfDiffs == 16 when: 'branch name is a non-conflicting diff' FieldMergeDiff stringFieldDiff = mergeDiff.find {it.fieldName == 'branchName'} @@ -1129,9 +1020,9 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { ArrayMergeDiff dataClassesDiff = mergeDiff.find {it.fieldName == 'dataClasses'} as ArrayMergeDiff then: - dataClassesDiff.created.size() == 2 + dataClassesDiff.created.size() == 3 dataClassesDiff.deleted.size() == 2 - // dataClassesDiff.modified.size() == 4 + dataClassesDiff.modified.size() == 4 when: 'created on source side' CreationMergeDiff creationMergeDiff = dataClassesDiff.created.find {it.createdIdentifier == 'addSourceOnly'} @@ -1151,7 +1042,16 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { !creationMergeDiff.isMergeConflict() !creationMergeDiff.commonAncestor - when: + when: 'modified on source side and deleted on target side' + creationMergeDiff = dataClassesDiff.created.find {it.createdIdentifier == 'modifySourceAndDeleteTarget'} + + then: + creationMergeDiff + creationMergeDiff.created + creationMergeDiff.isMergeConflict() + creationMergeDiff.commonAncestor + + when: 'deleted on source side' DeletionMergeDiff deleteSourceOnly = dataClassesDiff.deleted.find {it.deletedIdentifier == 'deleteSourceOnly'} DeletionMergeDiff deleteAndModify = dataClassesDiff.deleted.find {it.deletedIdentifier == 'deleteSourceAndModifyTarget'} @@ -1172,7 +1072,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { deleteAndModify.mergeModificationDiff //TODO more info - when: + when: 'additions on both with differences' MergeDiff addBothReturningDifferenceMerge = dataClassesDiff.modified.find {it.sourceIdentifier == 'addBothReturningDifference'} then: @@ -1185,7 +1085,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { addBothReturningDifferenceMerge.first().target == 'target' !addBothReturningDifferenceMerge.first().commonAncestor - when: + when: 'modified on source side' MergeDiff modifySourceOnlyMerge = dataClassesDiff.modified.find {it.sourceIdentifier == 'modifySourceOnly'} then: @@ -1199,7 +1099,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { modifySourceOnlyMerge.first().commonAncestor == 'common' - when: + when: 'modified on both sides' MergeDiff modifyBothNoDifferenceMerge = dataClassesDiff.modified.find {it.sourceIdentifier == 'modifyBothReturningNoDifference'} MergeDiff modifyBothWithDifferenceMerge = dataClassesDiff.modified.find {it.sourceIdentifier == 'modifyBothReturningDifference'} @@ -1216,7 +1116,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { modifyBothWithDifferenceMerge.first().commonAncestor == 'common' - when: + when: 'nested changes made inside existing class' MergeDiff existingClassMerge = dataClassesDiff.modified.find {it.sourceIdentifier == 'existingClass'} then: @@ -1250,145 +1150,24 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { given: setupData() - when: - DataModel dataModel = dataModelService.get(id) - def deleteLeftOnly = new DataClass(createdByUser: admin, label: 'deleteLeftOnly') - def modifyLeftOnly = new DataClass(createdByUser: admin, label: 'modifyLeftOnly') - def deleteAndModify = new DataClass(createdByUser: admin, label: 'deleteAndModify') - def modifyAndDelete = new DataClass(createdByUser: admin, label: 'modifyAndDelete') - def modifyAndModifyReturningDifference = new DataClass(createdByUser: admin, label: 'modifyAndModifyReturningDifference') - dataModel.addToDataClasses(deleteLeftOnly) - .addToDataClasses(modifyLeftOnly) - .addToDataClasses(deleteAndModify) - .addToDataClasses(modifyAndDelete) - .addToDataClasses(modifyAndModifyReturningDifference) - def existingClass = new DataClass(createdByUser: admin, label: 'existingClass') - def deleteLeftOnlyFromExistingClass = new DataClass(createdByUser: admin, label: 'deleteLeftOnlyFromExistingClass') - dataModel.addToDataClasses(existingClass.addToDataClasses(deleteLeftOnlyFromExistingClass)) - dataModelService.finaliseModel(dataModel, admin, null, null, null) - checkAndSave(dataModel) + when: 'generate models' + Map mergeData = BootstrapModels.buildMergeModelsForTestingOnly(id, admin, dataModelService, dataClassService, metadataService, sessionFactory, + messageSource) + DataModel rightMain = dataModelService.get(mergeData.targetId) + DataModel leftTest = dataModelService.get(mergeData.sourceId) + MergeDiff mergeDiff = dataModelService.getMergeDiffForModels(leftTest, rightMain) then: - dataModel.branchName == VersionAwareConstraints.DEFAULT_BRANCH_NAME - - when: - UUID draftId = createAndSaveNewBranchModel(VersionAwareConstraints.DEFAULT_BRANCH_NAME, dataModel) - - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(draftId, 'modifyAndDelete')) - - checkAndSave dataClassService.findByDataModelIdAndLabel(draftId, 'deleteAndModify').tap { - description = 'Description' - } - checkAndSave dataClassService.findByDataModelIdAndLabel(draftId, 'modifyAndModifyReturningDifference').tap { - description = 'DescriptionRight' - } - - checkAndSave dataClassService.findByDataModelIdAndLabel(draftId, 'existingClass') - .addToDataClasses(new DataClass(createdByUser: admin, label: 'addRightToExistingClass')) - - DataModel draftModel = dataModelService.get(draftId) - checkAndSave new DataClass(createdByUser: admin, label: 'addAndAddReturningDifference', description: 'right', dataModel: draftModel) - - checkAndSave dataModelService.get(draftId).tap { - description = 'DescriptionRight' - } - - sessionFactory.currentSession.flush() - sessionFactory.currentSession.clear() - - UUID testId = createAndSaveNewBranchModel('test', dataModel) - - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(testId, 'deleteLeftOnlyFromExistingClass')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(testId, 'deleteLeftOnly')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(testId, 'deleteAndModify')) - - checkAndSave dataClassService.findByDataModelIdAndLabel(testId, 'modifyLeftOnly').tap { - description = 'Description' - } - checkAndSave dataClassService.findByDataModelIdAndLabel(testId, 'modifyAndDelete').tap { - description = 'Description' - } - checkAndSave dataClassService.findByDataModelIdAndLabel(testId, 'modifyAndModifyReturningDifference').tap { - description = 'DescriptionLeft' - } - - checkAndSave dataClassService.findByDataModelIdAndLabel(testId, 'existingClass') - .addToDataClasses(new DataClass(createdByUser: admin, label: 'addLeftToExistingClass')) - - DataModel testModel = dataModelService.get(testId) - - checkAndSave new DataClass(createdByUser: admin, label: 'leftParentDataClass', dataModel: testModel) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'leftChildDataClass', dataModel: testModel)) - - checkAndSave new DataClass(createdByUser: admin, label: 'addLeftOnly', dataModel: testModel) - checkAndSave new DataClass(createdByUser: admin, label: 'addAndAddReturningDifference', description: 'left', dataModel: testModel) - - checkAndSave dataModelService.get(testId).tap { - description = 'DescriptionLeft' - } - - sessionFactory.currentSession.flush() - sessionFactory.currentSession.clear() - - DataModel draft = dataModelService.get(draftId) - DataModel test = dataModelService.get(testId) - - def mergeDiff = dataModelService.getMergeDiffForModels(test, draft) - - then: - mergeDiff.class == ObjectDiff - mergeDiff.diffs - mergeDiff.numberOfDiffs == 12 - mergeDiff.diffs.fieldName as Set == ['branchName', 'dataClasses', 'description'] as Set - def branchNameDiff = mergeDiff.diffs.find { it.fieldName == 'branchName' } - branchNameDiff.left == VersionAwareConstraints.DEFAULT_BRANCH_NAME - branchNameDiff.right == 'test' - !branchNameDiff.isMergeConflict - def dataClassesDiff = mergeDiff.diffs.find { it.fieldName == 'dataClasses' } - dataClassesDiff.created.size == 3 - dataClassesDiff.deleted.size == 2 - dataClassesDiff.modified.size == 4 - dataClassesDiff.created.value.label as Set == ['addLeftOnly', 'leftParentDataClass', 'modifyAndDelete'] as Set - !dataClassesDiff.created.find { it.value.label == 'addLeftOnly' }.isMergeConflict - !dataClassesDiff.created.find { it.value.label == 'addLeftOnly' }.commonAncestorValue - !dataClassesDiff.created.find { it.value.label == 'leftParentDataClass' }.isMergeConflict - !dataClassesDiff.created.find { it.value.label == 'leftParentDataClass' }.commonAncestorValue - dataClassesDiff.created.find { it.value.label == 'modifyAndDelete' }.isMergeConflict - dataClassesDiff.created.find { it.value.label == 'modifyAndDelete' }.commonAncestorValue - dataClassesDiff.deleted.value.label as Set == ['deleteAndModify', 'deleteLeftOnly'] as Set - dataClassesDiff.deleted.find { it.value.label == 'deleteAndModify' }.isMergeConflict - dataClassesDiff.deleted.find { it.value.label == 'deleteAndModify' }.commonAncestorValue - !dataClassesDiff.deleted.find { it.value.label == 'deleteLeftOnly' }.isMergeConflict - !dataClassesDiff.deleted.find { it.value.label == 'deleteLeftOnly' }.commonAncestorValue - dataClassesDiff.modified.left.diffIdentifier as Set == ['existingClass', 'modifyAndModifyReturningDifference', 'modifyLeftOnly', - 'addAndAddReturningDifference'] as Set - dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyAndModifyReturningDifference' }.diffs[0].fieldName == 'description' - dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyAndModifyReturningDifference' }.isMergeConflict - dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyAndModifyReturningDifference' }.commonAncestorValue - dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].fieldName == 'dataClasses' - dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.isMergeConflict - dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.commonAncestorValue - dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].created[0].value.label == 'addLeftToExistingClass' - dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].deleted[0].value.label == - 'deleteLeftOnlyFromExistingClass' - !dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].created[0].isMergeConflict - !dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].created[0].commonAncestorValue - !dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].deleted[0].isMergeConflict - !dataClassesDiff.modified.find { it.left.diffIdentifier == 'existingClass' }.diffs[0].deleted[0].commonAncestorValue - dataClassesDiff.modified.find { it.left.diffIdentifier == 'addAndAddReturningDifference' }.diffs[0].fieldName == 'description' - dataClassesDiff.modified.find { it.left.diffIdentifier == 'addAndAddReturningDifference' }.diffs[0].isMergeConflict - !dataClassesDiff.modified.find { it.left.diffIdentifier == 'addAndAddReturningDifference' }.diffs[0].commonAncestorValue - dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyLeftOnly' }.diffs[0].fieldName == 'description' - !dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyLeftOnly' }.diffs[0].isMergeConflict - !dataClassesDiff.modified.find { it.left.diffIdentifier == 'modifyLeftOnly' }.diffs[0].commonAncestorValue + !mergeDiff.isEmpty() + mergeDiff.numberOfDiffs == 16 when: - DataClass addLeftOnly = dataClassService.findByDataModelIdAndLabel(testId, 'addLeftOnly') - DataClass addAndAddReturningDifference = dataClassService.findByDataModelIdAndLabel(testId, 'addAndAddReturningDifference') - DataClass addLeftToExistingClass = dataClassService.findByDataModelIdAndLabel(testId, 'addLeftToExistingClass') + DataClass addLeftOnly = dataClassService.findByDataModelIdAndLabel(mergeData.sourceId, 'addTargetOnly') + DataClass addAndAddReturningDifference = dataClassService.findByDataModelIdAndLabel(mergeData.sourceId, 'addBothReturningDifference') + DataClass addLeftToExistingClass = dataClassService.findByDataModelIdAndLabel(mergeData.sourceId, 'addTargetToExistingClass') def patch = new MergeObjectDiffData( - leftId: draft.id, - rightId: test.id, + targetId: rightMain.id, + sourceId: leftTest.id, diffs: [ new MergeFieldDiffData( fieldName: 'description', @@ -1398,11 +1177,11 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { fieldName: 'dataClasses', deleted: [ new MergeItemData( - id: dataClassService.findByParentAndLabel(draft, deleteAndModify.label).id, + id: dataClassService.findByParentAndLabel(rightMain, deleteAndModify.label).id, label: deleteAndModify.label ), new MergeItemData( - id: dataClassService.findByParentAndLabel(draft, deleteLeftOnly.label).id, + id: dataClassService.findByParentAndLabel(rightMain, deleteLeftOnly.label).id, label: deleteLeftOnly.label ) ], @@ -1412,7 +1191,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { label: addLeftOnly.label ), new MergeItemData( - id: dataClassService.findByParentAndLabel(test, modifyAndDelete.label).id, + id: dataClassService.findByParentAndLabel(leftTest, modifyAndDelete.label).id, label: modifyAndDelete.label ) ], @@ -1428,7 +1207,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { ] ), new MergeObjectDiffData( - leftId: dataClassService.findByParentAndLabel(draft, existingClass.label).id, + leftId: dataClassService.findByParentAndLabel(rightMain, existingClass.label).id, label: existingClass.label, diffs: [ new MergeFieldDiffData( @@ -1437,7 +1216,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { deleted: [ new MergeItemData( id: dataClassService.findByParentAndLabel( - dataClassService.findByParentAndLabel(draft, existingClass.label), + dataClassService.findByParentAndLabel(rightMain, existingClass.label), deleteLeftOnlyFromExistingClass.label).id, label: deleteLeftOnlyFromExistingClass.label ) @@ -1453,7 +1232,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { ] ), new MergeObjectDiffData( - leftId: dataClassService.findByParentAndLabel(draft, modifyAndModifyReturningDifference.label).id, + leftId: dataClassService.findByParentAndLabel(rightMain, modifyAndModifyReturningDifference.label).id, label: modifyAndModifyReturningDifference.label, diffs: [ new MergeFieldDiffData( @@ -1463,7 +1242,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { ] ), new MergeObjectDiffData( - leftId: dataClassService.findByParentAndLabel(draft, "modifyLeftOnly").id, + leftId: dataClassService.findByParentAndLabel(rightMain, "modifyLeftOnly").id, label: "modifyLeftOnly", diffs: [ new MergeFieldDiffData( @@ -1478,7 +1257,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { ) ] ) - def mergedModel = dataModelService.mergeObjectDiffIntoModel(patch, draft, adminSecurityPolicyManager) + def mergedModel = dataModelService.mergeObjectDiffIntoModel(patch, rightMain, adminSecurityPolicyManager) then: mergedModel.description == 'DescriptionLeft' @@ -1486,10 +1265,10 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { mergedModel.dataClasses.label as Set == ['existingClass', 'modifyAndModifyReturningDifference', 'modifyLeftOnly', 'sdmclass', 'addAndAddReturningDifference', 'addLeftOnly', 'modifyAndDelete', 'addLeftToExistingClass', 'addRightToExistingClass'] as Set - mergedModel.dataClasses.find { it.label == 'existingClass' }.dataClasses.label as Set == ['addRightToExistingClass', - 'addLeftToExistingClass'] as Set - mergedModel.dataClasses.find { it.label == 'modifyAndModifyReturningDifference' }.description == 'DescriptionLeft' - mergedModel.dataClasses.find { it.label == 'modifyLeftOnly' }.description == 'Description' + mergedModel.dataClasses.find {it.label == 'existingClass'}.dataClasses.label as Set == ['addRightToExistingClass', + 'addLeftToExistingClass'] as Set + mergedModel.dataClasses.find {it.label == 'modifyAndModifyReturningDifference'}.description == 'DescriptionLeft' + mergedModel.dataClasses.find {it.label == 'modifyLeftOnly'}.description == 'Description' } @@ -1626,7 +1405,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { invalid.errors.errorCount == 1 invalid.errors.globalErrorCount == 0 invalid.errors.fieldErrorCount == 1 - invalid.errors.fieldErrors.any { it.field == 'dataClasses[0].label' } + invalid.errors.fieldErrors.any {it.field == 'dataClasses[0].label'} cleanup: GormUtils.outputDomainErrors(messageSource, invalid) @@ -1749,8 +1528,8 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { when: DataElementSimilarityResult childRes = results.find {it.source.label == 'child'} - DataElementSimilarityResult ele1Res = results.find { it.source.label == 'ele1' } - DataElementSimilarityResult ele2Res = results.find { it.source.label == 'element2' } + DataElementSimilarityResult ele1Res = results.find {it.source.label == 'ele1'} + DataElementSimilarityResult ele2Res = results.find {it.source.label == 'element2'} then: ele1Res @@ -1769,6 +1548,5 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { ele2Res ele2Res.size() == 0 } - } From 2f3b602689c603c277b61112c0f9ded73e6c4e90 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Wed, 23 Jun 2021 13:49:50 +0100 Subject: [PATCH 17/82] Update the application of a patch to a model * Use the term "patch" rather than mergediff * Add additional validation checks * Update the DataModelServiceIntegrationSpec to prove it works --- mdm-core/grails-app/i18n/messages.properties | 4 +- .../core/facet/MetadataService.groovy | 16 +-- .../FieldPatchData.groovy} | 30 ++-- .../ItemPatchData.groovy} | 9 +- .../{model => merge}/MergeIntoData.groovy | 4 +- .../ObjectPatchData.groovy} | 39 +++-- .../core/controller/ModelController.groovy | 20 ++- .../core/model/CatalogueItemService.groovy | 17 ++- .../core/model/ModelItemService.groovy | 33 +++-- .../core/model/ModelService.groovy | 19 +-- .../DataModelServiceIntegrationSpec.groovy | 136 ++++++++++-------- .../terminology/CodeSetService.groovy | 14 +- 12 files changed, 191 insertions(+), 150 deletions(-) rename mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/{model/MergeFieldDiffData.groovy => merge/FieldPatchData.groovy} (62%) rename mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/{model/MergeItemData.groovy => merge/ItemPatchData.groovy} (79%) rename mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/{model => merge}/MergeIntoData.groovy (91%) rename mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/{model/MergeObjectDiffData.groovy => merge/ObjectPatchData.groovy} (53%) diff --git a/mdm-core/grails-app/i18n/messages.properties b/mdm-core/grails-app/i18n/messages.properties index 510eee15a0..a0a4a17513 100644 --- a/mdm-core/grails-app/i18n/messages.properties +++ b/mdm-core/grails-app/i18n/messages.properties @@ -67,4 +67,6 @@ version.aware.documentation.version.change.not.allowed=Property [{0}] of class [ invalid.version.aware.new.version.not.finalised.message={0} [{1}({2})] cannot have a new version as it is not finalised invalid.version.aware.new.version.superseded.message={0} [{1}({2})] cannot have a new version as it has been superseded by [{3}({4})] invalid.api.property.key.format=Api Property key with value [{2}] must be lowercase, dot-separated only -invalid.versioned.folder.child.folders=Cannot have any VersionedFolders inside a VersionedFolder \ No newline at end of file +invalid.versioned.folder.child.folders=Cannot have any VersionedFolders inside a VersionedFolder +invalid.patch.value.and.array.changes=Cannot have a value and array changes in a patch +invalid.patch.no.changes=A patch must declare some changes \ No newline at end of file diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy index 025379b734..37948f540d 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy @@ -22,7 +22,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.model.facet.MetadataAware import uk.ac.ox.softeng.maurodatamapper.core.model.facet.MultiFacetAware import uk.ac.ox.softeng.maurodatamapper.core.provider.MauroDataMapperServiceProviderService import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.facet.NamespaceKeys -import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.MergeObjectDiffData +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.ObjectPatchData import uk.ac.ox.softeng.maurodatamapper.core.traits.service.MultiFacetAwareService import uk.ac.ox.softeng.maurodatamapper.core.traits.service.MultiFacetItemAwareService import uk.ac.ox.softeng.maurodatamapper.gorm.PaginatedResultList @@ -234,19 +234,19 @@ class MetadataService implements MultiFacetItemAwareService { } - void mergeMetadataIntoCatalogueItem(CatalogueItem targetCatalogueItem, MergeObjectDiffData mergeObjectDiffData) { + void mergeMetadataIntoCatalogueItem(CatalogueItem targetCatalogueItem, ObjectPatchData objectPatchData) { - if (!mergeObjectDiffData.hasDiffs()) return + if (!objectPatchData.hasPatches()) return - Metadata targetMetadata = findByMultiFacetAwareItemIdAndId(targetCatalogueItem.id, mergeObjectDiffData.leftId) + Metadata targetMetadata = findByMultiFacetAwareItemIdAndId(targetCatalogueItem.id, objectPatchData.targetId) if (!targetMetadata) { - log.error('Attempted to merge non-existent metadata [{}] inside target catalogue item [{}]', mergeObjectDiffData.leftId, + log.error('Attempted to merge non-existent metadata [{}] inside target catalogue item [{}]', objectPatchData.targetId, targetCatalogueItem.id) } - mergeObjectDiffData.getValidDiffs().each {mergeFieldDiffData -> - if (mergeFieldDiffData.value) { - targetMetadata.setProperty(mergeFieldDiffData.fieldName, mergeFieldDiffData.value) + objectPatchData.getPatches().each {fieldPatchData -> + if (fieldPatchData.value) { + targetMetadata.setProperty(fieldPatchData.fieldName, fieldPatchData.value) } else { log.error('Only field diff types can be handled inside MetadataService') } diff --git a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/MergeFieldDiffData.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy similarity index 62% rename from mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/MergeFieldDiffData.groovy rename to mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy index 8e01740ca4..abfff48450 100644 --- a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/MergeFieldDiffData.groovy +++ b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy @@ -15,7 +15,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model +package uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge import grails.validation.Validateable @@ -23,25 +23,33 @@ import grails.validation.Validateable /** * @since 07/02/2018 */ -class MergeFieldDiffData implements Validateable { +class FieldPatchData implements Validateable { String fieldName T value - Collection created - Collection deleted - Collection modified + Collection created + Collection deleted + Collection modified - MergeFieldDiffData() { + static constraints = { + fieldName nullable: false, blank: false + value validator: {val, obj -> + if (val && (created || deleted || modified)) return ['invalid.patch.value.and.array.changes'] + if (!val && !created && !deleted && !modified) return ['invalid.patch.no.changes'] + true + } + } + + FieldPatchData() { created = [] deleted = [] modified = [] } - boolean hasDiffs() { - value || !created.isEmpty() || !deleted.isEmpty() || modified.any {it.hasDiffs()} + boolean hasPatches() { + value || !created.isEmpty() || !deleted.isEmpty() || modified.any {it.hasPatches()} } - boolean isFieldChange() { value } @@ -51,12 +59,12 @@ class MergeFieldDiffData implements Validateable { } String getSummary() { - String prefix = "Merge Summary on field [${fieldName}]" + String prefix = "Merge patch summary on field [${fieldName}]" if (isFieldChange()) return "${prefix}: Changing value" "${prefix}: Creating ${created.size()} Deleting ${deleted.size()} Modifying ${modified.size()}" } String toString() { - "Merge on field [${fieldName}]" + "Merge patch on field [${fieldName}]" } } diff --git a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/MergeItemData.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/ItemPatchData.groovy similarity index 79% rename from mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/MergeItemData.groovy rename to mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/ItemPatchData.groovy index 589df8b46a..3bcfa510ff 100644 --- a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/MergeItemData.groovy +++ b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/ItemPatchData.groovy @@ -15,7 +15,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model +package uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge import grails.validation.Validateable @@ -23,8 +23,13 @@ import grails.validation.Validateable /** * @since 07/02/2018 */ -class MergeItemData implements Validateable { +class ItemPatchData implements Validateable { UUID id String label + + static constraints = { + id nullable: false + label nullable: false, blank: false + } } diff --git a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/MergeIntoData.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/MergeIntoData.groovy similarity index 91% rename from mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/MergeIntoData.groovy rename to mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/MergeIntoData.groovy index 75c86c5973..9f1300e772 100644 --- a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/MergeIntoData.groovy +++ b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/MergeIntoData.groovy @@ -15,7 +15,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model +package uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge import grails.validation.Validateable @@ -24,7 +24,7 @@ import grails.validation.Validateable */ class MergeIntoData implements Validateable { - MergeObjectDiffData patch + ObjectPatchData patch boolean deleteBranch = false String changeNotice diff --git a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/MergeObjectDiffData.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/ObjectPatchData.groovy similarity index 53% rename from mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/MergeObjectDiffData.groovy rename to mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/ObjectPatchData.groovy index 2533f55a60..37836647c0 100644 --- a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/MergeObjectDiffData.groovy +++ b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/ObjectPatchData.groovy @@ -15,7 +15,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model +package uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge import grails.validation.Validateable @@ -23,22 +23,39 @@ import grails.validation.Validateable /** * @since 07/02/2018 */ -class MergeObjectDiffData implements Validateable { +class ObjectPatchData implements Validateable { - UUID leftId - UUID rightId + UUID sourceId + UUID targetId String label - List diffs + private List patches - MergeObjectDiffData() { - diffs = [] + static constraints = { + sourceId nullable: false + targetId nullable: false + label nullable: true, blank: false + patches minSize: 1 } - boolean hasDiffs() { - diffs.any {it.hasDiffs()} + ObjectPatchData() { + patches = [] } - List getValidDiffs() { - diffs.findAll {it.hasDiffs()} + boolean hasPatches() { + patches.any {it.hasPatches()} + } + + List getPatches() { + patches.findAll {it.hasPatches()} + } + + @Deprecated + void setLeftId(UUID leftId) { + this.targetId = leftId + } + + @Deprecated + void setRightId(UUID rightId) { + this.sourceId = rightId } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy index 891ffdc0c8..9b5b5feedf 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy @@ -34,9 +34,9 @@ import uk.ac.ox.softeng.maurodatamapper.core.provider.exporter.ExporterProviderS import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.ImporterProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.ModelImporterProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.ModelImporterProviderServiceParameters +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.MergeIntoData import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.CreateNewVersionData import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.FinaliseData -import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.MergeIntoData import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.VersionTreeModel import uk.ac.ox.softeng.maurodatamapper.security.SecurityPolicyManagerService import uk.ac.ox.softeng.maurodatamapper.util.Utils @@ -264,15 +264,13 @@ abstract class ModelController extends CatalogueItemController< def mergeDiff() { - // Test branch - T left = queryForResource params.otherModelId - if (!left) return notFound(params.otherModelId) + T source = queryForResource params[alternateParamsIdKey] + if (!source) return notFound(params[alternateParamsIdKey]) - // Main branch - T right = queryForResource params[alternateParamsIdKey] - if (!right) return notFound(params[alternateParamsIdKey]) + T target = queryForResource params.otherModelId + if (!target) return notFound(params.otherModelId) - respond modelService.getMergeDiffForModels(left, right) + respond modelService.getMergeDiffForModels(source, target) } @Transactional @@ -282,10 +280,10 @@ abstract class ModelController extends CatalogueItemController< return } - if (mergeIntoData.patch.rightId != params[alternateParamsIdKey]) { + if (mergeIntoData.patch.sourceId != params[alternateParamsIdKey]) { return errorResponse(UNPROCESSABLE_ENTITY, 'Source model id passed in request body does not match source model id in URI.') } - if (mergeIntoData.patch.leftId != params.otherModelId) { + if (mergeIntoData.patch.targetId != params.otherModelId) { return errorResponse(UNPROCESSABLE_ENTITY, 'Target model id passed in request body does not match target model id in URI.') } @@ -295,7 +293,7 @@ abstract class ModelController extends CatalogueItemController< T targetModel = queryForResource params.otherModelId if (!targetModel) return notFound(params.otherModelId) - T instance = modelService.mergeObjectDiffIntoModel(mergeIntoData.patch, targetModel, currentUserSecurityPolicyManager) as T + T instance = modelService.mergeObjectPatchDataIntoModel(mergeIntoData.patch, targetModel, currentUserSecurityPolicyManager) as T if (!validateResource(instance, 'merge')) return diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy index e0dabbeece..7a23126412 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy @@ -28,9 +28,8 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.Rule import uk.ac.ox.softeng.maurodatamapper.core.facet.RuleService import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkService import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkType +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.FieldPatchData import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.CopyInformation -import uk.ac.ox.softeng.maurodatamapper.core.facet.rule.RuleRepresentation -import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.MergeFieldDiffData import uk.ac.ox.softeng.maurodatamapper.core.traits.service.DomainService import uk.ac.ox.softeng.maurodatamapper.core.traits.service.MultiFacetAwareService import uk.ac.ox.softeng.maurodatamapper.security.User @@ -193,23 +192,23 @@ abstract class CatalogueItemService implements DomainSe null } - void mergeMetadataIntoCatalogueItem(MergeFieldDiffData mergeFieldDiff, K targetCatalogueItem, + void mergeMetadataIntoCatalogueItem(FieldPatchData fieldPatchData, K targetCatalogueItem, UserSecurityPolicyManager userSecurityPolicyManager) { log.debug('Merging Metadata into Catalogue Item') // call metadataService version of below - mergeFieldDiff.deleted.each { mergeItemData -> - Metadata metadata = metadataService.get(mergeItemData.id) + fieldPatchData.deleted.each {deletedItemPatchData -> + Metadata metadata = metadataService.get(deletedItemPatchData.id) metadataService.delete(metadata) } // copy additions from source to target object - mergeFieldDiff.created.each { mergeItemData -> - Metadata metadata = metadataService.get(mergeItemData.id) + fieldPatchData.created.each {createdItemPatchData -> + Metadata metadata = metadataService.get(createdItemPatchData.id) metadataService.copy(targetCatalogueItem, metadata, userSecurityPolicyManager) } // for modifications, recursively call this method - mergeFieldDiff.modified.each { mergeObjectDiffData -> - metadataService.mergeMetadataIntoCatalogueItem(targetCatalogueItem, mergeObjectDiffData) + fieldPatchData.modified.each {modifiedObjectPatchData -> + metadataService.mergeMetadataIntoCatalogueItem(targetCatalogueItem, modifiedObjectPatchData) } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy index 169feb8217..8d8bd2f9da 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy @@ -20,8 +20,8 @@ package uk.ac.ox.softeng.maurodatamapper.core.model import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiNotYetImplementedException import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.CopyInformation -import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.MergeFieldDiffData -import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.MergeObjectDiffData +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.FieldPatchData +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.ObjectPatchData import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.util.Utils @@ -59,12 +59,12 @@ abstract class ModelItemService extends CatalogueItemServic throw new ApiNotYetImplementedException('MIS03', "copy [for ModelItem ${getModelItemClass().simpleName}] (with parent id), and relabel") } - Model mergeObjectDiffIntoModelItem(MergeObjectDiffData mergeObjectDiff, K targetModelItem, Model targetModel, - UserSecurityPolicyManager userSecurityPolicyManager) { + Model mergeObjectPatchDataIntoModelItem(ObjectPatchData objectPatchData, K targetModelItem, Model targetModel, + UserSecurityPolicyManager userSecurityPolicyManager) { //TODO validation on saving merges - if (!mergeObjectDiff.hasDiffs()) return targetModel - log.debug('Merging {} diffs into modelItem [{}]', mergeObjectDiff.getValidDiffs().size(), targetModelItem.label) - mergeObjectDiff.getValidDiffs().each {mergeFieldDiff -> + if (!objectPatchData.hasPatches()) return targetModel + log.debug('Merging {} diffs into modelItem [{}]', objectPatchData.getPatches().size(), targetModelItem.label) + objectPatchData.getPatches().each {mergeFieldDiff -> log.debug('{}', mergeFieldDiff.summary) if (mergeFieldDiff.isFieldChange()) { @@ -83,7 +83,7 @@ abstract class ModelItemService extends CatalogueItemServic } if (modelItemService) { - modelItemService.processMergeFieldDiff(mergeFieldDiff, targetModel, userSecurityPolicyManager, parentId) + modelItemService.processFieldPatchData(mergeFieldDiff, targetModel, userSecurityPolicyManager, parentId) } else { log.error('Unknown ModelItem field to merge [{}]', mergeFieldDiff.fieldName) @@ -95,17 +95,16 @@ abstract class ModelItemService extends CatalogueItemServic targetModel } - void processMergeFieldDiff(MergeFieldDiffData mergeFieldDiff, Model targetModel, UserSecurityPolicyManager userSecurityPolicyManager, - UUID parentId = null) { + void processFieldPatchData(FieldPatchData fieldPatchData, Model targetModel, UserSecurityPolicyManager userSecurityPolicyManager, UUID parentId = null) { // apply deletions of children to target object - mergeFieldDiff.deleted.each {mergeItemData -> - ModelItem modelItem = get(mergeItemData.id) as ModelItem + fieldPatchData.deleted.each {deletedItemPatchData -> + ModelItem modelItem = get(deletedItemPatchData.id) as ModelItem delete(modelItem) } // copy additions from source to target object - mergeFieldDiff.created.each {mergeItemData -> - ModelItem modelItem = get(mergeItemData.id) as ModelItem + fieldPatchData.created.each {createdItemPatchData -> + ModelItem modelItem = get(createdItemPatchData.id) as ModelItem ModelItem copyModelItem if (parentId) { copyModelItem = copy(targetModel, modelItem, userSecurityPolicyManager, parentId) @@ -116,9 +115,9 @@ abstract class ModelItemService extends CatalogueItemServic } // for modifications, recursively call this method - mergeFieldDiff.modified.each {mergeObjectDiffData -> - ModelItem modelItem = get(mergeObjectDiffData.leftId) as ModelItem - mergeObjectDiffIntoModelItem(mergeObjectDiffData, modelItem, targetModel, userSecurityPolicyManager) + fieldPatchData.modified.each {modifiedObjectPatchData -> + ModelItem modelItem = get(modifiedObjectPatchData.targetId) as ModelItem + mergeObjectPatchDataIntoModelItem(modifiedObjectPatchData, modelItem, targetModel, userSecurityPolicyManager) } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy index 2f402fcd6e..9610f452b4 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy @@ -38,7 +38,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.provider.dataloader.DataLoaderProvi import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.ModelImporterProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.ModelImporterProviderServiceParameters import uk.ac.ox.softeng.maurodatamapper.core.rest.converter.json.OffsetDateTimeConverter -import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.MergeObjectDiffData +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.ObjectPatchData import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.VersionTreeModel import uk.ac.ox.softeng.maurodatamapper.core.traits.service.VersionLinkAwareService import uk.ac.ox.softeng.maurodatamapper.security.SecurableResourceService @@ -385,17 +385,18 @@ abstract class ModelService extends CatalogueItemService imp * from ObjectDiff.mergeDiff(), customised by the user. * @param sourceModel Source model * @param targetModel Target model - * @param modelMergeObjectDiff Differences to merge, based on return from ObjectDiff.mergeDiff(), customised by user + * @param objectPatchData Differences to merge, based on return from ObjectDiff.mergeDiff(), customised by user * @param userSecurityPolicyManager To get user details and permissions when copying "added" items * @param domainService Service which handles catalogueItems of the leftModel and rightModel type. * @return The model resulting from the merging of changes. */ - K mergeObjectDiffIntoModel(MergeObjectDiffData modelMergeObjectDiff, K targetModel, - UserSecurityPolicyManager userSecurityPolicyManager) { + K mergeObjectPatchDataIntoModel(ObjectPatchData objectPatchData, K targetModel, + UserSecurityPolicyManager userSecurityPolicyManager) { //TODO validation on saving merges - if (!modelMergeObjectDiff.hasDiffs()) return targetModel - log.debug('Merging {} diffs into model {}', modelMergeObjectDiff.getValidDiffs().size(), targetModel.label) - modelMergeObjectDiff.getValidDiffs().each { mergeFieldDiff -> + if (!objectPatchData.hasPatches()) return targetModel + + log.debug('Merging {} diffs into model {}', objectPatchData.getPatches().size(), targetModel.label) + objectPatchData.getPatches().each {mergeFieldDiff -> log.debug('{}', mergeFieldDiff.summary) if (mergeFieldDiff.isFieldChange()) { @@ -403,9 +404,9 @@ abstract class ModelService extends CatalogueItemService imp } else if (mergeFieldDiff.isMetadataChange()) { mergeMetadataIntoCatalogueItem(mergeFieldDiff, targetModel, userSecurityPolicyManager) } else { - ModelItemService modelItemService = modelItemServices.find { it.handles(mergeFieldDiff.fieldName) } + ModelItemService modelItemService = modelItemServices.find {it.handles(mergeFieldDiff.fieldName)} if (modelItemService) { - modelItemService.processMergeFieldDiff(mergeFieldDiff, targetModel, userSecurityPolicyManager) + modelItemService.processFieldPatchData(mergeFieldDiff, targetModel, userSecurityPolicyManager) } else { log.error('Unknown ModelItem field to merge [{}]', mergeFieldDiff.fieldName) } diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy index 6474c49dc0..1628f91617 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy @@ -27,9 +27,9 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata import uk.ac.ox.softeng.maurodatamapper.core.facet.MetadataService import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints -import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.MergeFieldDiffData -import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.MergeItemData -import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.MergeObjectDiffData +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.FieldPatchData +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.ItemPatchData +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.ObjectPatchData import uk.ac.ox.softeng.maurodatamapper.datamodel.bootstrap.BootstrapModels import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClass import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClassService @@ -1162,92 +1162,89 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { mergeDiff.numberOfDiffs == 16 when: - DataClass addLeftOnly = dataClassService.findByDataModelIdAndLabel(mergeData.sourceId, 'addTargetOnly') - DataClass addAndAddReturningDifference = dataClassService.findByDataModelIdAndLabel(mergeData.sourceId, 'addBothReturningDifference') - DataClass addLeftToExistingClass = dataClassService.findByDataModelIdAndLabel(mergeData.sourceId, 'addTargetToExistingClass') - def patch = new MergeObjectDiffData( + DataClass targetExistingClass = dataClassService.findByParentAndLabel(rightMain, 'existingClass') + DataClass sourceExistingClass = dataClassService.findByParentAndLabel(leftTest, 'existingClass') + + def patch = new ObjectPatchData( targetId: rightMain.id, sourceId: leftTest.id, - diffs: [ - new MergeFieldDiffData( + patches: [ + new FieldPatchData( fieldName: 'description', value: 'DescriptionLeft' ), - new MergeFieldDiffData( + new FieldPatchData( fieldName: 'dataClasses', deleted: [ - new MergeItemData( - id: dataClassService.findByParentAndLabel(rightMain, deleteAndModify.label).id, - label: deleteAndModify.label + new ItemPatchData( + id: dataClassService.findByParentAndLabel(rightMain, 'deleteSourceAndModifyTarget').id, + label: 'deleteSourceAndModifyTarget' ), - new MergeItemData( - id: dataClassService.findByParentAndLabel(rightMain, deleteLeftOnly.label).id, - label: deleteLeftOnly.label + new ItemPatchData( + id: dataClassService.findByParentAndLabel(rightMain, 'deleteSourceOnly').id, + label: 'deleteSourceOnly' ) ], created: [ - new MergeItemData( - id: addLeftOnly.id, - label: addLeftOnly.label + new ItemPatchData( + id: dataClassService.findByParentAndLabel(leftTest, 'addSourceOnly').id, + label: 'addSourceOnly' ), - new MergeItemData( - id: dataClassService.findByParentAndLabel(leftTest, modifyAndDelete.label).id, - label: modifyAndDelete.label + new ItemPatchData( + id: dataClassService.findByParentAndLabel(leftTest, 'modifySourceAndDeleteTarget').id, + label: 'modifySourceAndDeleteTarget' ) ], modified: [ - new MergeObjectDiffData( - leftId: addAndAddReturningDifference.id, - label: addAndAddReturningDifference.label, - diffs: [ - new MergeFieldDiffData( + new ObjectPatchData( + targetId: dataClassService.findByParentAndLabel(rightMain, 'addBothReturningDifference').id, + label: 'addBothReturningDifference', + patches: [ + new FieldPatchData( fieldName: 'description', value: 'addedDescriptionSource' ) ] ), - new MergeObjectDiffData( - leftId: dataClassService.findByParentAndLabel(rightMain, existingClass.label).id, - label: existingClass.label, - diffs: [ - new MergeFieldDiffData( + new ObjectPatchData( + targetId: targetExistingClass.id, + label: 'existingClass', + patches: [ + new FieldPatchData( fieldName: "dataClasses", - deleted: [ - new MergeItemData( - id: dataClassService.findByParentAndLabel( - dataClassService.findByParentAndLabel(rightMain, existingClass.label), - deleteLeftOnlyFromExistingClass.label).id, - label: deleteLeftOnlyFromExistingClass.label + new ItemPatchData( + id: dataClassService.findByParentAndLabel(targetExistingClass, 'deleteSourceOnlyFromExistingClass').id, + label: 'deleteSourceOnlyFromExistingClass' ) ], created: [ - new MergeItemData( - id: addLeftToExistingClass.id, - label: addLeftToExistingClass.label + new ItemPatchData( + id: dataClassService.findByParentAndLabel(sourceExistingClass, 'addSourceToExistingClass').id, + label: 'addSourceToExistingClass' ) ] ) ] ), - new MergeObjectDiffData( - leftId: dataClassService.findByParentAndLabel(rightMain, modifyAndModifyReturningDifference.label).id, - label: modifyAndModifyReturningDifference.label, - diffs: [ - new MergeFieldDiffData( + new ObjectPatchData( + targetId: dataClassService.findByParentAndLabel(rightMain, 'modifyBothReturningDifference').id, + label: 'modifyBothReturningDifference', + patches: [ + new FieldPatchData( fieldName: 'description', - value: 'DescriptionLeft' + value: 'DescriptionSource' ), ] ), - new MergeObjectDiffData( - leftId: dataClassService.findByParentAndLabel(rightMain, "modifyLeftOnly").id, - label: "modifyLeftOnly", - diffs: [ - new MergeFieldDiffData( + new ObjectPatchData( + targetId: dataClassService.findByParentAndLabel(rightMain, 'modifySourceOnly').id, + label: 'modifySourceOnly', + patches: [ + new FieldPatchData( fieldName: 'description', - value: 'Description' + value: 'DescriptionSource' ) ] @@ -1257,18 +1254,33 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { ) ] ) - def mergedModel = dataModelService.mergeObjectDiffIntoModel(patch, rightMain, adminSecurityPolicyManager) + then: + check(patch) + + when: + def mergedModel = dataModelService.mergeObjectPatchDataIntoModel(patch, rightMain, adminSecurityPolicyManager) + List dataClassLabels = mergedModel.dataClasses*.label then: mergedModel.description == 'DescriptionLeft' - mergedModel.dataClasses.size() == 9 - mergedModel.dataClasses.label as Set == ['existingClass', 'modifyAndModifyReturningDifference', 'modifyLeftOnly', 'sdmclass', - 'addAndAddReturningDifference', 'addLeftOnly', 'modifyAndDelete', 'addLeftToExistingClass', - 'addRightToExistingClass'] as Set - mergedModel.dataClasses.find {it.label == 'existingClass'}.dataClasses.label as Set == ['addRightToExistingClass', - 'addLeftToExistingClass'] as Set - mergedModel.dataClasses.find {it.label == 'modifyAndModifyReturningDifference'}.description == 'DescriptionLeft' - mergedModel.dataClasses.find {it.label == 'modifyLeftOnly'}.description == 'Description' + mergedModel.dataClasses.size() == 15 + + and: 'created are present' + 'addSourceOnly' in dataClassLabels + 'modifySourceAndDeleteTarget' in dataClassLabels + + and: 'deleted are not present' + !('deleteSourceOnly' in dataClassLabels) + !('deleteSourceAndModifyTarget' in dataClassLabels) + + + and: 'existing class has correct child content' + mergedModel.dataClasses.find {it.label == 'existingClass'}.dataClasses*.label as Set == ['addTargetToExistingClass', 'addSourceToExistingClass'] as Set + + and: 'modifications are correct' + mergedModel.dataClasses.find {it.label == 'addBothReturningDifference'}.description == 'addedDescriptionSource' + mergedModel.dataClasses.find {it.label == 'modifyBothReturningDifference'}.description == 'DescriptionSource' + mergedModel.dataClasses.find {it.label == 'modifySourceOnly'}.description == 'DescriptionSource' } diff --git a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy index bfeee0e9d3..a6abe1871b 100644 --- a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy +++ b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy @@ -32,7 +32,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.path.PathService import uk.ac.ox.softeng.maurodatamapper.core.provider.dataloader.DataLoaderProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.ModelImporterProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.ModelImporterProviderServiceParameters -import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.MergeObjectDiffData +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.ObjectPatchData import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.security.basic.PublicAccessSecurityPolicyManager @@ -203,20 +203,20 @@ class CodeSetService extends ModelService { } @Override - CodeSet mergeObjectDiffIntoModel(MergeObjectDiffData modelMergeObjectDiff, CodeSet targetModel, - UserSecurityPolicyManager userSecurityPolicyManager) { + CodeSet mergeObjectPatchDataIntoModel(ObjectPatchData objectPatchData, CodeSet targetModel, + UserSecurityPolicyManager userSecurityPolicyManager) { - if (!modelMergeObjectDiff.hasDiffs()) return targetModel + if (!objectPatchData.hasPatches()) return targetModel - modelMergeObjectDiff.getValidDiffs().each { mergeFieldDiff -> + objectPatchData.getPatches().each {mergeFieldDiff -> if (mergeFieldDiff.isFieldChange()) { targetModel.setProperty(mergeFieldDiff.fieldName, mergeFieldDiff.value) } else if (mergeFieldDiff.isMetadataChange()) { mergeMetadataIntoCatalogueItem(mergeFieldDiff, targetModel, userSecurityPolicyManager) } else { - ModelItemService modelItemService = modelItemServices.find { it.handles(mergeFieldDiff.fieldName) } + ModelItemService modelItemService = modelItemServices.find {it.handles(mergeFieldDiff.fieldName)} if (modelItemService) { @@ -255,7 +255,7 @@ class CodeSetService extends ModelService { // for modifications, recursively call this method mergeFieldDiff.modified.each { mergeObjectDiffData -> ModelItem modelItem = modelItemService.get(mergeObjectDiffData.leftId) as ModelItem - modelItemService.mergeObjectDiffIntoModelItem(mergeObjectDiffData, modelItem, targetModel, userSecurityPolicyManager) + modelItemService.mergeObjectPatchDataIntoModelItem(mergeObjectDiffData, modelItem, targetModel, userSecurityPolicyManager) } } } else { From 48bc5054b16dbc1fd864468881956fc66dc8a978 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Wed, 23 Jun 2021 14:15:03 +0100 Subject: [PATCH 18/82] Update classes to use the new builder class --- .../ox/softeng/maurodatamapper/core/facet/Annotation.groovy | 3 ++- .../ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy | 3 ++- .../uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy | 3 ++- .../core/facet/rule/RuleRepresentation.groovy | 3 ++- .../softeng/maurodatamapper/core/model/CatalogueItem.groovy | 5 ++--- .../referencedata/item/ReferenceDataValue.groovy | 4 ++-- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy index 961e6af97a..43d9aefe8c 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy @@ -17,6 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.facet +import uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.InformationAwareConstraints @@ -112,7 +113,7 @@ class Annotation implements MultiFacetItemAware, PathAware, InformationAware, Cr @Override ObjectDiff diff(Annotation otherAnnotation) { - ObjectDiff.builder(Annotation) + DiffBuilder.objectDiff(Annotation) .leftHandSide(this.id.toString(), this) .rightHandSide(otherAnnotation.id.toString(), otherAnnotation) .appendString('description', this.description, otherAnnotation.description) diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy index 2b704d8eb1..6cf33a5f25 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy @@ -17,6 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.facet +import uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.MultiFacetItemAware @@ -90,7 +91,7 @@ class Metadata implements MultiFacetItemAware, CreatorAware, Diffable @Override ObjectDiff diff(Metadata obj) { - ObjectDiff.builder(Metadata) + DiffBuilder.objectDiff(Metadata) .leftHandSide(id.toString(), this) .rightHandSide(obj.id.toString(), obj) .appendString('namespace', this.namespace, obj.namespace) diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy index 8da0ec5499..91aaa2ddfe 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy @@ -17,6 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.facet +import uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.facet.rule.RuleRepresentation @@ -88,7 +89,7 @@ class Rule implements MultiFacetItemAware, CreatorAware, Diffable { @Override ObjectDiff diff(Rule obj) { - ObjectDiff.builder(Rule) + DiffBuilder.objectDiff(Rule) .leftHandSide(id.toString(), this) .rightHandSide(obj.id.toString(), obj) .appendString('name', this.name, obj.name) diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy index c601c1103c..d741287e53 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy @@ -17,6 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.facet.rule +import uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.facet.Rule @@ -83,7 +84,7 @@ class RuleRepresentation implements Diffable, EditHistoryAwa @Override ObjectDiff diff(RuleRepresentation obj) { - ObjectDiff.builder(RuleRepresentation) + DiffBuilder.objectDiff(RuleRepresentation) .leftHandSide(id.toString(), this) .rightHandSide(obj.id.toString(), obj) .appendString('language', this.language, obj.language) diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy index 31944fe2e8..a1e6f8c48e 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy @@ -17,7 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.model - +import uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation @@ -95,8 +95,7 @@ trait CatalogueItem implements InformationAware, EditHistory static ObjectDiff catalogueItemDiffBuilder(Class diffClass, T lhs, T rhs) { String lhsId = lhs.id ?: "Left:Unsaved_${lhs.domainType}" String rhsId = rhs.id ?: "Right:Unsaved_${rhs.domainType}" - ObjectDiff - .builder(diffClass) + DiffBuilder.objectDiff(diffClass) .leftHandSide(lhsId, lhs) .rightHandSide(rhsId, rhs) .appendString('label', lhs.label, rhs.label) diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValue.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValue.groovy index c34c16b5f8..8eaee7abd7 100644 --- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValue.groovy +++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValue.groovy @@ -17,6 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.referencedata.item +import uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModel @@ -67,8 +68,7 @@ class ReferenceDataValue implements CreatorAware, Diffable { ObjectDiff diff(ReferenceDataValue otherValue) { String lhsId = this.id ?: "Left:Unsaved_${this.domainType}" String rhsId = otherValue.id ?: "Right:Unsaved_${otherValue.domainType}" - ObjectDiff - .builder(ReferenceDataValue) + DiffBuilder.objectDiff(ReferenceDataValue) .leftHandSide(lhsId, this) .rightHandSide(rhsId, otherValue) .appendNumber('rowNumber', this.rowNumber, otherValue.rowNumber) From d9b572b4abcda75b8f8764e1fb315a612380aa49 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Wed, 23 Jun 2021 14:15:43 +0100 Subject: [PATCH 19/82] Fix the fact that the identifier fields in ObjectDiff used to store the actual object ids rather than diffIdentifiers --- .../core/diff/bidirectional/ObjectDiff.groovy | 41 +++++++------------ 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy index 066030a65a..a20815f688 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy @@ -36,8 +36,8 @@ class ObjectDiff extends BiDirectionalDiff { List diffs - String leftIdentifier - String rightIdentifier + String leftId + String rightId ObjectDiff(Class targetClass) { super(targetClass) @@ -52,8 +52,8 @@ class ObjectDiff extends BiDirectionalDiff { ObjectDiff objectDiff = (ObjectDiff) o - if (leftIdentifier != objectDiff.leftIdentifier) return false - if (rightIdentifier != objectDiff.rightIdentifier) return false + if (leftId != objectDiff.leftId) return false + if (rightId != objectDiff.rightId) return false if (diffs != objectDiff.diffs) return false return true @@ -63,33 +63,31 @@ class ObjectDiff extends BiDirectionalDiff { String toString() { int numberOfDiffs = getNumberOfDiffs() if (!numberOfDiffs) return "${leftIdentifier} == ${rightIdentifier}" - "${leftIdentifier} <> ${rightIdentifier} :: ${numberOfDiffs} differences\n ${diffs.collect { it.toString() }.join('\n ')}" + "${leftIdentifier} <> ${rightIdentifier} :: ${numberOfDiffs} differences\n ${diffs.collect {it.toString()}.join('\n ')}" } @Override Integer getNumberOfDiffs() { - diffs?.sum { it.getNumberOfDiffs() } as Integer ?: 0 + diffs?.sum {it.getNumberOfDiffs()} as Integer ?: 0 } - @Deprecated - ObjectDiff leftHandSide(String leftId, O lhs) { - leftHandSide(lhs) + String getLeftIdentifier() { + left.diffIdentifier } - @Deprecated - ObjectDiff rightHandSide(String rightId, O rhs) { - rightHandSide(rhs) + String getRightIdentifier() { + right.diffIdentifier } - ObjectDiff leftHandSide(O lhs) { + ObjectDiff leftHandSide(String leftId, O lhs) { super.leftHandSide(lhs) - this.leftIdentifier = lhs.diffIdentifier + this.leftId = leftId this } - ObjectDiff rightHandSide(O rhs) { - super.rightHandSide(rhs) - this.rightIdentifier = rhs.diffIdentifier + ObjectDiff rightHandSide(String rightId, O rhs) { + rightHandSide(rhs) + this.rightId = rightId this } @@ -199,14 +197,5 @@ class ObjectDiff extends BiDirectionalDiff { static String clean(String s) { s?.trim() ?: null } - - /** - * @use DiffBuilder.objectDiff* @param objectClass - * @return - */ - @Deprecated - static ObjectDiff builder(Class objectClass) { - new ObjectDiff() - } } From eb9256fd3a9ba7efe0dac04bfea935da17cacd73 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Wed, 23 Jun 2021 14:16:26 +0100 Subject: [PATCH 20/82] Update the diff gson views to handle the underlying changes to the uni and bi directional diffs --- mdm-core/grails-app/views/arrayDiff/_arrayDiff.gson | 6 +++--- .../grails-app/views/creationDiff/_creationDiff.gson | 9 +++++++++ .../grails-app/views/deletionDiff/_deletionDiff.gson | 10 ++++++++++ mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson | 6 ------ mdm-core/grails-app/views/objectDiff/_objectDiff.gson | 4 ++-- 5 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 mdm-core/grails-app/views/creationDiff/_creationDiff.gson create mode 100644 mdm-core/grails-app/views/deletionDiff/_deletionDiff.gson diff --git a/mdm-core/grails-app/views/arrayDiff/_arrayDiff.gson b/mdm-core/grails-app/views/arrayDiff/_arrayDiff.gson index 38a688976a..69f6974be6 100644 --- a/mdm-core/grails-app/views/arrayDiff/_arrayDiff.gson +++ b/mdm-core/grails-app/views/arrayDiff/_arrayDiff.gson @@ -6,7 +6,7 @@ model { } json("${arrayDiff.fieldName}") { - if (arrayDiff.deleted) deleted tmpl.'/mergeWrapper/mergeWrapper'(arrayDiff.deleted) - if (arrayDiff.created) created tmpl.'/mergeWrapper/mergeWrapper'(arrayDiff.created) - if (arrayDiff.modified) modified tmpl.'/objectDiff/objectDiff'(arrayDiff.modified) + if (arrayDiff.deleted) deleted g.render(arrayDiff.deleted) + if (arrayDiff.created) created g.render(arrayDiff.created) + if (arrayDiff.modified) modified g.render(arrayDiff.modified) } diff --git a/mdm-core/grails-app/views/creationDiff/_creationDiff.gson b/mdm-core/grails-app/views/creationDiff/_creationDiff.gson new file mode 100644 index 0000000000..b6d7900eae --- /dev/null +++ b/mdm-core/grails-app/views/creationDiff/_creationDiff.gson @@ -0,0 +1,9 @@ +import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.CreationDiff + +model { + CreationDiff creationDiff +} + +json { + value tmpl.'/diffable/diffable'(creationDiff.value) +} diff --git a/mdm-core/grails-app/views/deletionDiff/_deletionDiff.gson b/mdm-core/grails-app/views/deletionDiff/_deletionDiff.gson new file mode 100644 index 0000000000..f399b4fb57 --- /dev/null +++ b/mdm-core/grails-app/views/deletionDiff/_deletionDiff.gson @@ -0,0 +1,10 @@ +import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.DeletionDiff + +model { + DeletionDiff deletionDiff + +} + +json { + value tmpl.'/diffable/diffable'(deletionDiff.value) +} diff --git a/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson b/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson index 7c841eb15a..2e6712598f 100644 --- a/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson +++ b/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson @@ -7,10 +7,4 @@ model { json("${fieldDiff.fieldName}") { left fieldDiff.getLeft() right fieldDiff.getRight() - - // if (fieldDiff.mergeConflict != null) { - // isMergeConflict fieldDiff.mergeConflict - // if (fieldDiff.mergeConflict) commonAncestorValue fieldDiff.commonAncestor - // } - } diff --git a/mdm-core/grails-app/views/objectDiff/_objectDiff.gson b/mdm-core/grails-app/views/objectDiff/_objectDiff.gson index 707c8f36d9..89e038a6dc 100644 --- a/mdm-core/grails-app/views/objectDiff/_objectDiff.gson +++ b/mdm-core/grails-app/views/objectDiff/_objectDiff.gson @@ -10,8 +10,8 @@ model { json { - leftId objectDiff.getLeftIdentifier() - rightId objectDiff.getRightIdentifier() + leftId objectDiff.getLeftId() + rightId objectDiff.getRightId() if (objectDiff.getLeft() instanceof InformationAware) { label(((InformationAware) objectDiff.getLeft()).getLabel()) } From b0eb72ef652427ba80d64d33cb938216e50b0353 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Wed, 23 Jun 2021 16:01:41 +0100 Subject: [PATCH 21/82] Add mergeDiff views to render the existing expected format from the new classes --- .../views/arrayMergeDiff/_arrayMergeDiff.gson | 12 + .../creationMergeDiff/_creationMergeDiff.gson | 11 + .../deletionMergeDiff/_deletionMergeDiff.gson | 12 + .../views/fieldMergeDiff/_fieldMergeDiff.gson | 14 + .../views/mergeDiff/_mergeDiff.gson | 34 + .../views/mergeWrapper/_mergeWrapper.gson | 14 - .../core/controller/ModelController.groovy | 6 +- .../core/diff/tridirectional/MergeDiff.groovy | 13 + .../grails-app/views/dataModel/mergeDiff.gson | 6 +- .../datamodel/DataModelFunctionalSpec.groovy | 579 +++++++++--------- .../views/referenceDataModel/mergeDiff.gson | 11 +- .../grails-app/views/codeSet/mergeDiff.gson | 6 +- .../views/terminology/mergeDiff.gson | 6 +- 13 files changed, 405 insertions(+), 319 deletions(-) create mode 100644 mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson create mode 100644 mdm-core/grails-app/views/creationMergeDiff/_creationMergeDiff.gson create mode 100644 mdm-core/grails-app/views/deletionMergeDiff/_deletionMergeDiff.gson create mode 100644 mdm-core/grails-app/views/fieldMergeDiff/_fieldMergeDiff.gson create mode 100644 mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson delete mode 100644 mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson diff --git a/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson b/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson new file mode 100644 index 0000000000..7ab76f2c56 --- /dev/null +++ b/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson @@ -0,0 +1,12 @@ +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.ArrayMergeDiff + +model { + ArrayMergeDiff arrayMergeDiff + +} + +json("${arrayMergeDiff.fieldName}") { + if (arrayMergeDiff.deleted) deleted g.render(arrayMergeDiff.deleted) + if (arrayMergeDiff.created) created g.render(arrayMergeDiff.created) + if (arrayMergeDiff.modified) modified g.render(arrayMergeDiff.modified) +} diff --git a/mdm-core/grails-app/views/creationMergeDiff/_creationMergeDiff.gson b/mdm-core/grails-app/views/creationMergeDiff/_creationMergeDiff.gson new file mode 100644 index 0000000000..30d6f0358d --- /dev/null +++ b/mdm-core/grails-app/views/creationMergeDiff/_creationMergeDiff.gson @@ -0,0 +1,11 @@ +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.CreationMergeDiff + +model { + CreationMergeDiff creationMergeDiff +} + +json { + value tmpl.'/diffable/diffable'(creationMergeDiff.value) + isMergeConflict creationMergeDiff.isMergeConflict() + if (creationMergeDiff.isMergeConflict()) commonAncestorValue tmpl.'/diffable/diffable'(creationMergeDiff.commonAncestor) +} diff --git a/mdm-core/grails-app/views/deletionMergeDiff/_deletionMergeDiff.gson b/mdm-core/grails-app/views/deletionMergeDiff/_deletionMergeDiff.gson new file mode 100644 index 0000000000..bda855a270 --- /dev/null +++ b/mdm-core/grails-app/views/deletionMergeDiff/_deletionMergeDiff.gson @@ -0,0 +1,12 @@ +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.DeletionMergeDiff + +model { + DeletionMergeDiff deletionMergeDiff + +} + +json { + value tmpl.'/diffable/diffable'(deletionMergeDiff.value) + isMergeConflict deletionMergeDiff.isMergeConflict() + if (deletionMergeDiff.isMergeConflict()) commonAncestorValue tmpl.'/diffable/diffable'(deletionMergeDiff.commonAncestor) +} diff --git a/mdm-core/grails-app/views/fieldMergeDiff/_fieldMergeDiff.gson b/mdm-core/grails-app/views/fieldMergeDiff/_fieldMergeDiff.gson new file mode 100644 index 0000000000..ca78703897 --- /dev/null +++ b/mdm-core/grails-app/views/fieldMergeDiff/_fieldMergeDiff.gson @@ -0,0 +1,14 @@ +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.FieldMergeDiff + +model { + FieldMergeDiff fieldMergeDiff + +} + +json("${fieldMergeDiff.fieldName}") { + left fieldMergeDiff.getTarget() + right fieldMergeDiff.getSource() + + isMergeConflict fieldMergeDiff.mergeConflict + if (fieldMergeDiff.mergeConflict) commonAncestorValue fieldMergeDiff.commonAncestor +} diff --git a/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson b/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson new file mode 100644 index 0000000000..4492efc26f --- /dev/null +++ b/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson @@ -0,0 +1,34 @@ +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff +import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata +import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem +import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.InformationAware + +model { + MergeDiff mergeDiff +} + +json { + + leftId mergeDiff.getTargetId() + rightId mergeDiff.getSourceId() + if (mergeDiff.getTarget() instanceof InformationAware) { + label(((InformationAware) mergeDiff.getTarget()).getLabel()) + } + + if (mergeDiff.getTarget() instanceof Metadata) { + namespace(((Metadata) mergeDiff.getTarget()).getNamespace()) + key(((Metadata) mergeDiff.getTarget()).getKey()) + } + + if (mergeDiff.getTarget() instanceof ModelItem && mergeDiff.getRight() instanceof ModelItem) { + ModelItem source = mergeDiff.getTarget() as ModelItem + ModelItem target = mergeDiff.getTarget() as ModelItem + leftBreadcrumbs g.render(source.getBreadcrumbs()) + rightBreadcrumbs g.render(target.getBreadcrumbs()) + } + + count mergeDiff.getNumberOfDiffs() + diffs g.render(mergeDiff.getDiffs()) + + +} diff --git a/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson b/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson deleted file mode 100644 index 83d4bd7aa9..0000000000 --- a/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson +++ /dev/null @@ -1,14 +0,0 @@ -import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.UniDirectionalDiff - -model { - UniDirectionalDiff mergeWrapper - -} - -json { - value tmpl.'/diffable/diffable'(mergeWrapper.value) - // if (mergeWrapper.mergeConflict != null) { - // isMergeConflict mergeWrapper.mergeConflict - // if (mergeWrapper.mergeConflict) commonAncestorValue tmpl.'/diffable/diffable'(mergeWrapper.commonAncestor) - // } -} diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy index 9b5b5feedf..7dc99dfe83 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy @@ -270,7 +270,11 @@ abstract class ModelController extends CatalogueItemController< T target = queryForResource params.otherModelId if (!target) return notFound(params.otherModelId) - respond modelService.getMergeDiffForModels(source, target) + String view = 'mergeDiff' + if (params.style == 'legacy') view = 'legacyMergeDiff' + if (params.style == 'new') view = 'updatedMergeDiff' + + respond modelService.getMergeDiffForModels(source, target), view: view } @Transactional diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy index 0b9226644c..7427e9eecc 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy @@ -7,6 +7,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.FieldDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.CreationDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.DeletionDiff +import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware import groovy.transform.stc.ClosureParams import groovy.transform.stc.SimpleType @@ -68,6 +69,14 @@ class MergeDiff extends TriDirectionalDiff { target.diffIdentifier } + String getSourceId() { + (source as CreatorAware).id + } + + String getTargetId() { + (target as CreatorAware).id + } + FieldMergeDiff first() { diffs.first() } @@ -80,6 +89,10 @@ class MergeDiff extends TriDirectionalDiff { diffs.isEmpty() } + List getDiffs() { + diffs + } + @Override String toString() { "${sourceIdentifier} --> ${targetIdentifier} [${commonAncestor.diffIdentifier}]" diff --git a/mdm-plugin-datamodel/grails-app/views/dataModel/mergeDiff.gson b/mdm-plugin-datamodel/grails-app/views/dataModel/mergeDiff.gson index 7acf79c1e2..4b0eee2a2a 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataModel/mergeDiff.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataModel/mergeDiff.gson @@ -1,8 +1,8 @@ -import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel model { - ObjectDiff objectDiff + MergeDiff mergeDiff } -json tmpl.'/objectDiff/objectDiff'(objectDiff) \ No newline at end of file +json tmpl.'/mergeDiff/mergeDiff'(mergeDiff) \ No newline at end of file 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 24f3ac7400..5fb4d5eada 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 @@ -153,288 +153,288 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { String getExpectedMergeDiffJson() { '''{ - "leftId": "${json-unit.matches:id}", - "rightId": "${json-unit.matches:id}", - "label": "Functional Test Model", - "count": 11, - "diffs": [ - { - "description": { - "left": "DescriptionRight", - "right": "DescriptionLeft", - "isMergeConflict": true, - "commonAncestorValue": null + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "Functional Test Model", + "count": 11, + "diffs": [ + { + "description": { + "left": "DescriptionRight", + "right": "DescriptionLeft", + "isMergeConflict": true, + "commonAncestorValue": null + } + }, + { + "branchName": { + "left": "main", + "right": "source", + "isMergeConflict": false + } + }, + { + "dataClasses": { + "deleted": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "deleteAndModify", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": true + } + ] + }, + "isMergeConflict": true, + "commonAncestorValue": { + "id": "${json-unit.matches:id}", + "label": "deleteAndModify", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": true + } + ] } - }, - { - "branchName": { - "left": "main", - "right": "source", - "isMergeConflict": false + }, + { + "value": { + "id": "${json-unit.matches:id}", + "label": "deleteLeftOnly", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": true + } + ] + }, + "isMergeConflict": false + } + ], + "created": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "addLeftOnly", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": false + } + ] + }, + "isMergeConflict": false + }, + { + "value": { + "id": "${json-unit.matches:id}", + "label": "modifyAndDelete", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": false + } + ] + }, + "isMergeConflict": true, + "commonAncestorValue": { + "id": "${json-unit.matches:id}", + "label": "modifyAndDelete", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": true + } + ] } - }, - { - "dataClasses": { - "deleted": [ + } + ], + "modified": [ + { + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "addAndAddReturningDifference", + "leftBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": false + } + ], + "rightBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": false + } + ], + "count": 1, + "diffs": [ + { + "description": { + "left": "DescriptionRight", + "right": "DescriptionLeft", + "isMergeConflict": true, + "commonAncestorValue": null + } + } + ] + }, + { + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "existingClass", + "leftBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": false + } + ], + "rightBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": false + } + ], + "count": 2, + "diffs": [ + { + "dataClasses": { + "deleted": [ { - "value": { - "id": "${json-unit.matches:id}", - "label": "deleteAndModify", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ] - }, - "isMergeConflict": true, - "commonAncestorValue": { + "value": { + "id": "${json-unit.matches:id}", + "label": "deleteLeftOnlyFromExistingClass", + "breadcrumbs": [ + { "id": "${json-unit.matches:id}", - "label": "deleteAndModify", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": true - } - ] - } - }, - { - "value": { + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": true + }, + { "id": "${json-unit.matches:id}", - "label": "deleteLeftOnly", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ] - }, - "isMergeConflict": false + "label": "existingClass", + "domainType": "DataClass" + } + ] + }, + "isMergeConflict": false } - ], - "created": [ + ], + "created": [ { - "value": { + "value": { + "id": "${json-unit.matches:id}", + "label": "addLeftToExistingClass", + "breadcrumbs": [ + { "id": "${json-unit.matches:id}", - "label": "addLeftOnly", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ] - }, - "isMergeConflict": false - }, - { - "value": { - "id": "${json-unit.matches:id}", - "label": "modifyAndDelete", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ] - }, - "isMergeConflict": true, - "commonAncestorValue": { + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": false + }, + { "id": "${json-unit.matches:id}", - "label": "modifyAndDelete", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": true - } - ] - } - } - ], - "modified": [ - { - "leftId": "${json-unit.matches:id}", - "rightId": "${json-unit.matches:id}", - "label": "addAndAddReturningDifference", - "leftBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ], - "rightBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ], - "count": 1, - "diffs": [ - { - "description": { - "left": "DescriptionRight", - "right": "DescriptionLeft", - "isMergeConflict": true, - "commonAncestorValue": null - } - } - ] - }, - { - "leftId": "${json-unit.matches:id}", - "rightId": "${json-unit.matches:id}", - "label": "existingClass", - "leftBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ], - "rightBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ], - "count": 2, - "diffs": [ - { - "dataClasses": { - "deleted": [ - { - "value": { - "id": "${json-unit.matches:id}", - "label": "deleteLeftOnlyFromExistingClass", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - }, - { - "id": "${json-unit.matches:id}", - "label": "existingClass", - "domainType": "DataClass" - } - ] - }, - "isMergeConflict": false - } - ], - "created": [ - { - "value": { - "id": "${json-unit.matches:id}", - "label": "addLeftToExistingClass", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - }, - { - "id": "${json-unit.matches:id}", - "label": "existingClass", - "domainType": "DataClass" - } - ] - }, - "isMergeConflict": false - } - ] - } - } - ] - }, - { - "leftId": "${json-unit.matches:id}", - "rightId": "${json-unit.matches:id}", - "label": "modifyAndModifyReturningDifference", - "leftBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ], - "rightBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ], - "count": 1, - "diffs": [ - { - "description": { - "left": "DescriptionRight", - "right": "DescriptionLeft", - "isMergeConflict": true, - "commonAncestorValue": null - } - } - ] - }, - { - "leftId": "${json-unit.matches:id}", - "rightId": "${json-unit.matches:id}", - "label": "modifyLeftOnly", - "leftBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ], - "rightBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ], - "count": 1, - "diffs": [ - { - "description": { - "left": null, - "right": "Description", - "isMergeConflict": false - } - } + "label": "existingClass", + "domainType": "DataClass" + } ] + }, + "isMergeConflict": false } - ] - } - } - ] + ] + } + } + ] + }, + { + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "modifyAndModifyReturningDifference", + "leftBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": false + } + ], + "rightBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": false + } + ], + "count": 1, + "diffs": [ + { + "description": { + "left": "DescriptionRight", + "right": "DescriptionLeft", + "isMergeConflict": true, + "commonAncestorValue": null + } + } + ] + }, + { + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "modifyLeftOnly", + "leftBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": true + } + ], + "rightBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": true + } + ], + "count": 1, + "diffs": [ + { + "description": { + "left": null, + "right": "Description", + "isMergeConflict": false + } + } + ] + } + ] + } + } + ] }''' } @@ -1346,7 +1346,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { cleanUpData(id) } - void 'VB08a : test finding merge difference of two datamodels'() { + void 'MD01 : test finding merge difference of two datamodels'() { given: String id = createNewItem(validJson) PUT("$id/finalise", [versionChangeType: 'Major']) @@ -1362,14 +1362,8 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { String rightId = responseBody().id when: - GET("$leftId/mergeDiff/$rightId") - - then: - verifyResponse OK, response - responseBody().leftId == rightId - responseBody().rightId == leftId - - when: + GET("$leftId/mergeDiff/$mainId", STRING_ARG) + log.debug('{}', jsonResponseBody()) GET("$leftId/mergeDiff/$mainId") then: @@ -1378,6 +1372,8 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { responseBody().rightId == leftId when: + GET("$rightId/mergeDiff/$mainId", STRING_ARG) + log.debug('{}', jsonResponseBody()) GET("$rightId/mergeDiff/$mainId") then: @@ -1392,7 +1388,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { cleanUpData(id) } - void 'VB08b : test finding merge difference of two complex datamodels'() { + void 'MD02 : test finding merge difference of two complex datamodels'() { given: String id = createNewItem(validJson) @@ -1555,7 +1551,6 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { when: GET("$source/mergeDiff/$target", STRING_ARG) - // GET("$source/mergeDiff/$target") then: verifyJsonResponse OK, expectedMergeDiffJson @@ -1566,7 +1561,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { cleanUpData(id) } - void 'VB09a : test merging diff with no patch data'() { + void 'MP01 : test merging diff with no patch data'() { given: String id = createNewItem(validJson) @@ -1593,7 +1588,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { cleanUpData(id) } - void 'VB09b : test merging diff with URI id not matching body id'() { + void 'MP02 : test merging diff with URI id not matching body id'() { given: String id = createNewItem(validJson) @@ -1642,7 +1637,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { cleanUpData(id) } - void 'VB09c : test merging diff into draft model'() { + void 'MP03 : test merging diff into draft model'() { given: String id = createNewItem(validJson) @@ -1940,7 +1935,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { cleanUpData(id) } - void 'VB09d : test merging metadata diff into draft model'() { + void 'MP04 : test merging metadata diff into draft model'() { given: String id = createNewItem(validJson) @@ -2251,7 +2246,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { * back into main, and we check that the DataElement which was created on the source branch is correctly added to the * DataClass on the main branch. */ - void 'VB09e : test merging diff in which a DataElement has been created on a DataClass - failing test for MC-9433'() { + void 'MP05 : test merging diff in which a DataElement has been created on a DataClass - failing test for MC-9433'() { given: 'A DataModel is created' String id = createNewItem(validJson) @@ -2491,6 +2486,11 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { verifyResponse CREATED, response when: 'performing diff' + // just grab the raw json for visual checking + GET("$testId/diff/$mainId", STRING_ARG) + log.debug('{}', jsonResponseBody()) + + and: GET("$testId/diff/$mainId") then: @@ -2546,6 +2546,11 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { PUT("$testId/dataClasses/$contentId", [description: 'a change to the description']) when: 'performing diff' + // just grab the raw json for visual checking + GET("$testId/diff/$mainId", STRING_ARG) + log.debug('{}', jsonResponseBody()) + + and: GET("$testId/diff/$mainId") then: diff --git a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/mergeDiff.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/mergeDiff.gson index 2a86644a5d..33d016c5e8 100644 --- a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/mergeDiff.gson +++ b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/mergeDiff.gson @@ -1,13 +1,8 @@ -import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModel model { - ObjectDiff left - ObjectDiff right -} - -json { - left tmpl.'/objectDiff/objectDiff'(left) - right tmpl.'/objectDiff/objectDiff'(right) + MergeDiff mergeDiff } +json tmpl.'/mergeDiff/mergeDiff'(mergeDiff) \ No newline at end of file diff --git a/mdm-plugin-terminology/grails-app/views/codeSet/mergeDiff.gson b/mdm-plugin-terminology/grails-app/views/codeSet/mergeDiff.gson index f40e575bf9..ef19d66fc1 100644 --- a/mdm-plugin-terminology/grails-app/views/codeSet/mergeDiff.gson +++ b/mdm-plugin-terminology/grails-app/views/codeSet/mergeDiff.gson @@ -1,8 +1,8 @@ -import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff import uk.ac.ox.softeng.maurodatamapper.terminology.CodeSet model { - ObjectDiff objectDiff + MergeDiff mergeDiff } -json tmpl.'/objectDiff/objectDiff'(objectDiff) \ No newline at end of file +json tmpl.'/mergeDiff/mergeDiff'(mergeDiff) \ No newline at end of file diff --git a/mdm-plugin-terminology/grails-app/views/terminology/mergeDiff.gson b/mdm-plugin-terminology/grails-app/views/terminology/mergeDiff.gson index 0c9690bb29..725da163c5 100644 --- a/mdm-plugin-terminology/grails-app/views/terminology/mergeDiff.gson +++ b/mdm-plugin-terminology/grails-app/views/terminology/mergeDiff.gson @@ -1,8 +1,8 @@ -import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff import uk.ac.ox.softeng.maurodatamapper.terminology.Terminology model { - ObjectDiff objectDiff + MergeDiff mergeDiff } -json tmpl.'/objectDiff/objectDiff'(objectDiff) \ No newline at end of file +json tmpl.'/mergeDiff/mergeDiff'(mergeDiff) \ No newline at end of file From fa48f6e7b8aee24d7a756847e51c69d578ce72cf Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Wed, 23 Jun 2021 17:04:19 +0100 Subject: [PATCH 22/82] Convert existing mergeDiff views to legacy views --- .../mergeDiff/_legacyCreationMergeDiff.gson | 11 ++++++ .../mergeDiff/_legacyDeletionMergeDiff.gson | 11 ++++++ .../mergeDiff/_legacyFieldMergeDiff.gson | 23 +++++++++++++ .../views/mergeDiff/_legacyMergeDiff.gson | 34 +++++++++++++++++++ .../core/controller/ModelController.groovy | 5 +-- .../diff/tridirectional/ArrayMergeDiff.groovy | 9 ++--- .../tridirectional/DeletionMergeDiff.groovy | 2 +- .../diff/tridirectional/FieldMergeDiff.groovy | 2 +- .../core/diff/tridirectional/MergeDiff.groovy | 4 ++- .../views/dataModel/legacyMergeDiff.gson | 8 +++++ .../referenceDataModel/legacyMergeDiff.gson | 8 +++++ .../views/codeSet/legacyMergeDiff.gson | 8 +++++ .../views/terminology/legacyMergeDiff.gson | 8 +++++ 13 files changed, 124 insertions(+), 9 deletions(-) create mode 100644 mdm-core/grails-app/views/mergeDiff/_legacyCreationMergeDiff.gson create mode 100644 mdm-core/grails-app/views/mergeDiff/_legacyDeletionMergeDiff.gson create mode 100644 mdm-core/grails-app/views/mergeDiff/_legacyFieldMergeDiff.gson create mode 100644 mdm-core/grails-app/views/mergeDiff/_legacyMergeDiff.gson create mode 100644 mdm-plugin-datamodel/grails-app/views/dataModel/legacyMergeDiff.gson create mode 100644 mdm-plugin-referencedata/grails-app/views/referenceDataModel/legacyMergeDiff.gson create mode 100644 mdm-plugin-terminology/grails-app/views/codeSet/legacyMergeDiff.gson create mode 100644 mdm-plugin-terminology/grails-app/views/terminology/legacyMergeDiff.gson diff --git a/mdm-core/grails-app/views/mergeDiff/_legacyCreationMergeDiff.gson b/mdm-core/grails-app/views/mergeDiff/_legacyCreationMergeDiff.gson new file mode 100644 index 0000000000..30d6f0358d --- /dev/null +++ b/mdm-core/grails-app/views/mergeDiff/_legacyCreationMergeDiff.gson @@ -0,0 +1,11 @@ +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.CreationMergeDiff + +model { + CreationMergeDiff creationMergeDiff +} + +json { + value tmpl.'/diffable/diffable'(creationMergeDiff.value) + isMergeConflict creationMergeDiff.isMergeConflict() + if (creationMergeDiff.isMergeConflict()) commonAncestorValue tmpl.'/diffable/diffable'(creationMergeDiff.commonAncestor) +} diff --git a/mdm-core/grails-app/views/mergeDiff/_legacyDeletionMergeDiff.gson b/mdm-core/grails-app/views/mergeDiff/_legacyDeletionMergeDiff.gson new file mode 100644 index 0000000000..fe3ca60a20 --- /dev/null +++ b/mdm-core/grails-app/views/mergeDiff/_legacyDeletionMergeDiff.gson @@ -0,0 +1,11 @@ +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.DeletionMergeDiff + +model { + DeletionMergeDiff deletionMergeDiff +} + +json { + value tmpl.'/diffable/diffable'(deletionMergeDiff.value) + isMergeConflict deletionMergeDiff.isMergeConflict() + if (deletionMergeDiff.isMergeConflict()) commonAncestorValue tmpl.'/diffable/diffable'(deletionMergeDiff.commonAncestor) +} diff --git a/mdm-core/grails-app/views/mergeDiff/_legacyFieldMergeDiff.gson b/mdm-core/grails-app/views/mergeDiff/_legacyFieldMergeDiff.gson new file mode 100644 index 0000000000..41952113ac --- /dev/null +++ b/mdm-core/grails-app/views/mergeDiff/_legacyFieldMergeDiff.gson @@ -0,0 +1,23 @@ +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.ArrayMergeDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.FieldMergeDiff + +model { + FieldMergeDiff fieldMergeDiff + ArrayMergeDiff arrayMergeDiff +} + +if (arrayMergeDiff) { + json("${arrayMergeDiff.fieldName}") { + if (arrayMergeDiff.deleted) deleted tmpl.'legacyDeletionMergeDiff'(arrayMergeDiff.deleted) + if (arrayMergeDiff.created) created tmpl.'legacyCreationMergeDiff'(arrayMergeDiff.created) + if (arrayMergeDiff.modified) modified tmpl.'legacyMergeDiff'(arrayMergeDiff.modified) + } +} else if (fieldMergeDiff) { + json("${fieldMergeDiff.fieldName}") { + left fieldMergeDiff.getTarget() + right fieldMergeDiff.getSource() + + isMergeConflict fieldMergeDiff.mergeConflict + if (fieldMergeDiff.mergeConflict) commonAncestorValue fieldMergeDiff.commonAncestor + } +} diff --git a/mdm-core/grails-app/views/mergeDiff/_legacyMergeDiff.gson b/mdm-core/grails-app/views/mergeDiff/_legacyMergeDiff.gson new file mode 100644 index 0000000000..417ded9566 --- /dev/null +++ b/mdm-core/grails-app/views/mergeDiff/_legacyMergeDiff.gson @@ -0,0 +1,34 @@ +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff +import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata +import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem +import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.InformationAware + +model { + MergeDiff mergeDiff +} + +json { + + leftId mergeDiff.getTargetId() + rightId mergeDiff.getSourceId() + if (mergeDiff.getTarget() instanceof InformationAware) { + label(((InformationAware) mergeDiff.getTarget()).getLabel()) + } + + if (mergeDiff.getTarget() instanceof Metadata) { + namespace(((Metadata) mergeDiff.getTarget()).getNamespace()) + key(((Metadata) mergeDiff.getTarget()).getKey()) + } + + if (mergeDiff.getTarget() instanceof ModelItem && mergeDiff.getSource() instanceof ModelItem) { + ModelItem source = mergeDiff.getTarget() as ModelItem + ModelItem target = mergeDiff.getTarget() as ModelItem + leftBreadcrumbs g.render(source.getBreadcrumbs()) + rightBreadcrumbs g.render(target.getBreadcrumbs()) + } + + count mergeDiff.getNumberOfDiffs() + diffs tmpl.legacyFieldMergeDiff(mergeDiff.getDiffs()) + + +} diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy index 7dc99dfe83..f156551b97 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy @@ -270,9 +270,10 @@ abstract class ModelController extends CatalogueItemController< T target = queryForResource params.otherModelId if (!target) return notFound(params.otherModelId) - String view = 'mergeDiff' + String view = 'legacyMergeDiff' + // default to legacy until UI is updated if (params.style == 'legacy') view = 'legacyMergeDiff' - if (params.style == 'new') view = 'updatedMergeDiff' + if (params.style == 'new') view = 'mergeDiff' respond modelService.getMergeDiffForModels(source, target), view: view } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy index f5a9f5089e..68c10936dd 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy @@ -89,16 +89,17 @@ class ArrayMergeDiff extends FieldMergeDiff> { @Override String toString() { - StringBuilder stringBuilder = new StringBuilder(super.toString()) + String diffIdentifier = source?.first()?.diffIdentifier ?: target?.first()?.diffIdentifier ?: commonAncestor?.first()?.diffIdentifier + StringBuilder stringBuilder = new StringBuilder("${diffIdentifier}.${fieldName} :: ${source.size()} <> ${target.size()} :: ${commonAncestor.size()}") if (created) { - stringBuilder.append('\n Created ::\n').append(created) + stringBuilder.append('\n').append(created.join('\n')) } if (deleted) { - stringBuilder.append('\n Deleted ::\n').append(deleted) + stringBuilder.append('\n').append(deleted.join('\n')) } if (modified) { - stringBuilder.append('\n Modified ::\n').append(modified) + stringBuilder.append('\n Modified ::\n').append(modified.join('\n')) } stringBuilder.toString() } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy index b9e851825f..d21d9994ae 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy @@ -63,6 +63,6 @@ class DeletionMergeDiff extends TriDirectionalDiff { @Override String toString() { String str = "Deleted :: ${deletedIdentifier}" - mergeModificationDiff ? "${str} Modified :: ${mergeModificationDiff}" : str + mergeModificationDiff ? "${str}\n >> Modified :: ${mergeModificationDiff}" : str } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy index 0947716802..43665e8e22 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy @@ -77,6 +77,6 @@ class FieldMergeDiff extends TriDirectionalDiff { @Override String toString() { - "${fieldName} :: ${source} <> ${target} :: ${commonAncestor} " + "${fieldName} :: ${source} <> ${target} :: ${commonAncestor}" } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy index 7427e9eecc..5bb77b4998 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy @@ -95,7 +95,9 @@ class MergeDiff extends TriDirectionalDiff { @Override String toString() { - "${sourceIdentifier} --> ${targetIdentifier} [${commonAncestor.diffIdentifier}]" + String str = "${sourceIdentifier} --> ${targetIdentifier}" + if (commonAncestor) str = "${str} [${commonAncestor.diffIdentifier}]" + str } MergeDiff forMergingDiffable(M sourceSide) { diff --git a/mdm-plugin-datamodel/grails-app/views/dataModel/legacyMergeDiff.gson b/mdm-plugin-datamodel/grails-app/views/dataModel/legacyMergeDiff.gson new file mode 100644 index 0000000000..b41f7ef9b0 --- /dev/null +++ b/mdm-plugin-datamodel/grails-app/views/dataModel/legacyMergeDiff.gson @@ -0,0 +1,8 @@ +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff +import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel + +model { + MergeDiff mergeDiff +} + +json tmpl.'/mergeDiff/legacyMergeDiff'(mergeDiff) \ No newline at end of file diff --git a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/legacyMergeDiff.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/legacyMergeDiff.gson new file mode 100644 index 0000000000..38ecd83c2c --- /dev/null +++ b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/legacyMergeDiff.gson @@ -0,0 +1,8 @@ +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff +import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModel + +model { + MergeDiff mergeDiff +} + +json tmpl.'/mergeDiff/legacyMergeDiff'(mergeDiff) \ No newline at end of file diff --git a/mdm-plugin-terminology/grails-app/views/codeSet/legacyMergeDiff.gson b/mdm-plugin-terminology/grails-app/views/codeSet/legacyMergeDiff.gson new file mode 100644 index 0000000000..70d1ec4e66 --- /dev/null +++ b/mdm-plugin-terminology/grails-app/views/codeSet/legacyMergeDiff.gson @@ -0,0 +1,8 @@ +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff +import uk.ac.ox.softeng.maurodatamapper.terminology.CodeSet + +model { + MergeDiff mergeDiff +} + +json tmpl.'/mergeDiff/legacyMergeDiff'(mergeDiff) \ No newline at end of file diff --git a/mdm-plugin-terminology/grails-app/views/terminology/legacyMergeDiff.gson b/mdm-plugin-terminology/grails-app/views/terminology/legacyMergeDiff.gson new file mode 100644 index 0000000000..4b970b4a3c --- /dev/null +++ b/mdm-plugin-terminology/grails-app/views/terminology/legacyMergeDiff.gson @@ -0,0 +1,8 @@ +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff +import uk.ac.ox.softeng.maurodatamapper.terminology.Terminology + +model { + MergeDiff mergeDiff +} + +json tmpl.'/mergeDiff/legacyMergeDiff'(mergeDiff) \ No newline at end of file From 5d8e379ddb9fc65a3a05c8fe819205387010bfcc Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Wed, 23 Jun 2021 17:39:02 +0100 Subject: [PATCH 23/82] Initial work on the new style mergediff json --- .../views/arrayMergeDiff/_arrayMergeDiff.gson | 3 +- .../creationMergeDiff/_creationMergeDiff.gson | 6 +- .../deletionMergeDiff/_deletionMergeDiff.gson | 7 +- .../views/fieldMergeDiff/_fieldMergeDiff.gson | 12 +- .../views/mergeDiff/_mergeDiff.gson | 9 +- .../tridirectional/CreationMergeDiff.groovy | 12 +- .../tridirectional/DeletionMergeDiff.groovy | 4 + .../datamodel/DataModelFunctionalSpec.groovy | 364 +++++++++++++++++- 8 files changed, 372 insertions(+), 45 deletions(-) diff --git a/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson b/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson index 7ab76f2c56..0f8ccaf077 100644 --- a/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson +++ b/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson @@ -2,10 +2,9 @@ import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.ArrayMergeDiff model { ArrayMergeDiff arrayMergeDiff - } -json("${arrayMergeDiff.fieldName}") { +json(arrayMergeDiff.fieldName) { if (arrayMergeDiff.deleted) deleted g.render(arrayMergeDiff.deleted) if (arrayMergeDiff.created) created g.render(arrayMergeDiff.created) if (arrayMergeDiff.modified) modified g.render(arrayMergeDiff.modified) diff --git a/mdm-core/grails-app/views/creationMergeDiff/_creationMergeDiff.gson b/mdm-core/grails-app/views/creationMergeDiff/_creationMergeDiff.gson index 30d6f0358d..67e87ecf4e 100644 --- a/mdm-core/grails-app/views/creationMergeDiff/_creationMergeDiff.gson +++ b/mdm-core/grails-app/views/creationMergeDiff/_creationMergeDiff.gson @@ -5,7 +5,7 @@ model { } json { - value tmpl.'/diffable/diffable'(creationMergeDiff.value) + created tmpl.'/diffable/diffable'(creationMergeDiff.created) isMergeConflict creationMergeDiff.isMergeConflict() - if (creationMergeDiff.isMergeConflict()) commonAncestorValue tmpl.'/diffable/diffable'(creationMergeDiff.commonAncestor) -} + isSourceModificationAndTargetDeletion creationMergeDiff.isSourceModificationAndTargetDeletion() +} \ No newline at end of file diff --git a/mdm-core/grails-app/views/deletionMergeDiff/_deletionMergeDiff.gson b/mdm-core/grails-app/views/deletionMergeDiff/_deletionMergeDiff.gson index bda855a270..efbf455d39 100644 --- a/mdm-core/grails-app/views/deletionMergeDiff/_deletionMergeDiff.gson +++ b/mdm-core/grails-app/views/deletionMergeDiff/_deletionMergeDiff.gson @@ -2,11 +2,10 @@ import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.DeletionMergeDi model { DeletionMergeDiff deletionMergeDiff - } json { - value tmpl.'/diffable/diffable'(deletionMergeDiff.value) + deleted tmpl.'/diffable/diffable'(deletionMergeDiff.deleted) isMergeConflict deletionMergeDiff.isMergeConflict() - if (deletionMergeDiff.isMergeConflict()) commonAncestorValue tmpl.'/diffable/diffable'(deletionMergeDiff.commonAncestor) -} + isSourceDeletionAndTargetModification deletionMergeDiff.isSourceDeletionAndTargetModification() +} \ No newline at end of file diff --git a/mdm-core/grails-app/views/fieldMergeDiff/_fieldMergeDiff.gson b/mdm-core/grails-app/views/fieldMergeDiff/_fieldMergeDiff.gson index ca78703897..d4f2084449 100644 --- a/mdm-core/grails-app/views/fieldMergeDiff/_fieldMergeDiff.gson +++ b/mdm-core/grails-app/views/fieldMergeDiff/_fieldMergeDiff.gson @@ -2,13 +2,11 @@ import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.FieldMergeDiff model { FieldMergeDiff fieldMergeDiff - } -json("${fieldMergeDiff.fieldName}") { - left fieldMergeDiff.getTarget() - right fieldMergeDiff.getSource() - +json(fieldMergeDiff.fieldName) { + source fieldMergeDiff.getSource() + target fieldMergeDiff.getTarget() + commonAncestor fieldMergeDiff.commonAncestor isMergeConflict fieldMergeDiff.mergeConflict - if (fieldMergeDiff.mergeConflict) commonAncestorValue fieldMergeDiff.commonAncestor -} +} \ No newline at end of file diff --git a/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson b/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson index 4492efc26f..33024deb13 100644 --- a/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson +++ b/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson @@ -9,8 +9,9 @@ model { json { - leftId mergeDiff.getTargetId() - rightId mergeDiff.getSourceId() + sourceId mergeDiff.getSourceId() + targetId mergeDiff.getTargetId() + if (mergeDiff.getTarget() instanceof InformationAware) { label(((InformationAware) mergeDiff.getTarget()).getLabel()) } @@ -20,7 +21,7 @@ json { key(((Metadata) mergeDiff.getTarget()).getKey()) } - if (mergeDiff.getTarget() instanceof ModelItem && mergeDiff.getRight() instanceof ModelItem) { + if (mergeDiff.getTarget() instanceof ModelItem && mergeDiff.getSource() instanceof ModelItem) { ModelItem source = mergeDiff.getTarget() as ModelItem ModelItem target = mergeDiff.getTarget() as ModelItem leftBreadcrumbs g.render(source.getBreadcrumbs()) @@ -29,6 +30,4 @@ json { count mergeDiff.getNumberOfDiffs() diffs g.render(mergeDiff.getDiffs()) - - } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy index 64ff8e491d..f6a4895f0d 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy @@ -24,6 +24,10 @@ class CreationMergeDiff extends TriDirectionalDiff { created.diffIdentifier } + boolean isSourceModificationAndTargetDeletion() { + commonAncestor != null + } + @SuppressWarnings('GrDeprecatedAPIUsage') CreationMergeDiff whichCreated(C object) { withSource(object) as CreationMergeDiff @@ -33,14 +37,6 @@ class CreationMergeDiff extends TriDirectionalDiff { super.withCommonAncestor(ca) as CreationMergeDiff } - CreationMergeDiff withMergeDeletion(C deleted) { - super.rightHandSide(deleted) as CreationMergeDiff - } - - CreationMergeDiff withNoMergeDeletion() { - this - } - @Override CreationMergeDiff asMergeConflict() { super.asMergeConflict() as CreationMergeDiff diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy index d21d9994ae..6eb793463f 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy @@ -27,6 +27,10 @@ class DeletionMergeDiff extends TriDirectionalDiff { value.diffIdentifier } + boolean isSourceDeletionAndTargetModification() { + mergeModificationDiff != null + } + DeletionMergeDiff whichDeleted(D object) { this.value = object withCommonAncestor object 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 5fb4d5eada..835f0b9c41 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 @@ -151,7 +151,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { }''' } - String getExpectedMergeDiffJson() { + String getExpectedLegacyMergeDiffJson() { '''{ "leftId": "${json-unit.matches:id}", "rightId": "${json-unit.matches:id}", @@ -438,6 +438,274 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { }''' } + String getExpectedMergeDiffJson() { + '''{ + "sourceId": "${json-unit.matches:id}", + "targetId": "${json-unit.matches:id}", + "label": "Functional Test Model", + "count": 11, + "diffs": [ + { + "description": { + "source": "DescriptionLeft", + "target": "DescriptionRight", + "commonAncestor": null, + "isMergeConflict": true + } + }, + { + "branchName": { + "source": "source", + "target": "main", + "commonAncestor": "main", + "isMergeConflict": false + } + }, + { + "dataClasses": { + "deleted": [ + { + "deleted": { + "id": "${json-unit.matches:id}", + "label": "deleteAndModify", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": true + } + ] + }, + "isMergeConflict": true, + "isSourceDeletionAndTargetModification": true + }, + { + "deleted": { + "id": "${json-unit.matches:id}", + "label": "deleteLeftOnly", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": true + } + ] + }, + "isMergeConflict": false, + "isSourceDeletionAndTargetModification": false + } + ], + "created": [ + { + "created": { + "id": "${json-unit.matches:id}", + "label": "addLeftOnly", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": false + } + ] + }, + "isMergeConflict": false, + "isSourceModificationAndTargetDeletion": false + }, + { + "created": { + "id": "${json-unit.matches:id}", + "label": "modifyAndDelete", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": false + } + ] + }, + "isMergeConflict": true, + "isSourceModificationAndTargetDeletion": true + } + ], + "modified": [ + { + "targetId": "${json-unit.matches:id}", + "sourceId": "${json-unit.matches:id}", + "label": "addAndAddReturningDifference", + "targetBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": false + } + ], + "sourceBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": false + } + ], + "count": 1, + "diffs": [ + { + "description": { + "target": "DescriptionRight", + "source": "DescriptionLeft", + "isMergeConflict": true, + "commonAncestor": null + } + } + ] + }, + { + "targetId": "${json-unit.matches:id}", + "sourceId": "${json-unit.matches:id}", + "label": "existingClass", + "targetBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": false + } + ], + "sourceBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": false + } + ], + "count": 2, + "diffs": [ + { + "dataClasses": { + "deleted": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "deleteLeftOnlyFromExistingClass", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": true + }, + { + "id": "${json-unit.matches:id}", + "label": "existingClass", + "domainType": "DataClass" + } + ] + }, + "isMergeConflict": false + } + ], + "created": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "addLeftToExistingClass", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": false + }, + { + "id": "${json-unit.matches:id}", + "label": "existingClass", + "domainType": "DataClass" + } + ] + }, + "isMergeConflict": false + } + ] + } + } + ] + }, + { + "targetId": "${json-unit.matches:id}", + "sourceId": "${json-unit.matches:id}", + "label": "modifyAndModifyReturningDifference", + "targetBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": false + } + ], + "sourceBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": false + } + ], + "count": 1, + "diffs": [ + { + "description": { + "target": "DescriptionRight", + "source": "DescriptionLeft", + "isMergeConflict": true, + "commonAncestor": null + } + } + ] + }, + { + "targetId": "${json-unit.matches:id}", + "sourceId": "${json-unit.matches:id}", + "label": "modifyLeftOnly", + "targetBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": true + } + ], + "sourceBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": true + } + ], + "count": 1, + "diffs": [ + { + "description": { + "target": null, + "source": "Description", + "isMergeConflict": false + } + } + ] + } + ] + } + } + ] +}''' + } + void 'test getting DataModel types'() { when: GET('types', STRING_ARG) @@ -1389,8 +1657,83 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { } void 'MD02 : test finding merge difference of two complex datamodels'() { + given: + Map mergeData = buildComplexDataModelsForMerging() + + when: + GET("$mergeData.source/mergeDiff/$mergeData.target", STRING_ARG) + + then: + log.debug('{}', jsonResponseBody()) + verifyJsonResponse OK, expectedLegacyMergeDiffJson + + cleanup: + cleanUpData(mergeData.source) + cleanUpData(mergeData.target) + cleanUpData(mergeData.id) + } + + void 'MD03 : test finding merge difference of two datamodels with the new style'() { given: String id = createNewItem(validJson) + PUT("$id/finalise", [versionChangeType: 'Major']) + verifyResponse OK, response + PUT("$id/newBranchModelVersion", [:]) + verifyResponse CREATED, response + String mainId = responseBody().id + PUT("$id/newBranchModelVersion", [branchName: 'left']) + verifyResponse CREATED, response + String leftId = responseBody().id + PUT("$id/newBranchModelVersion", [branchName: 'right']) + verifyResponse CREATED, response + String rightId = responseBody().id + + when: + GET("$leftId/mergeDiff/$mainId?style=new", STRING_ARG) + log.debug('{}', jsonResponseBody()) + GET("$leftId/mergeDiff/$mainId?style=new") + + then: + verifyResponse OK, response + responseBody().targetId == mainId + responseBody().sourceId == leftId + + when: + GET("$rightId/mergeDiff/$mainId?style=new", STRING_ARG) + log.debug('{}', jsonResponseBody()) + GET("$rightId/mergeDiff/$mainId?style=new") + + then: + verifyResponse OK, response + responseBody().targetId == mainId + responseBody().sourceId == rightId + + cleanup: + cleanUpData(mainId) + cleanUpData(leftId) + cleanUpData(rightId) + cleanUpData(id) + } + + void 'MD04 : test finding merge difference of two complex datamodels with the new style'() { + given: + Map mergeData = buildComplexDataModelsForMerging() + + when: + GET("$mergeData.source/mergeDiff/$mergeData.target?style=new", STRING_ARG) + + then: + verifyJsonResponse OK, expectedMergeDiffJson + + cleanup: + cleanUpData(mergeData.source) + cleanUpData(mergeData.target) + cleanUpData(mergeData.id) + } + + Map buildComplexDataModelsForMerging() { + + String id = createNewItem(validJson) POST("$id/dataClasses", [label: 'deleteLeftOnly']) verifyResponse CREATED, response @@ -1427,7 +1770,6 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { verifyResponse CREATED, response String source = responseBody().id - when: GET("$source/path/dm%3A%7Cdc%3AexistingClass") verifyResponse OK, response existingClass = responseBody().id @@ -1457,7 +1799,6 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { verifyResponse OK, response String modifyAndModifyReturningDifference = responseBody().id - then: DELETE("$source/dataClasses/$deleteAndDelete") verifyResponse NO_CONTENT, response DELETE("$source/dataClasses/$existingClass/dataClasses/$deleteLeftOnlyFromExistingClass") @@ -1488,7 +1829,6 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { PUT("$source", [description: 'DescriptionLeft']) verifyResponse OK, response - when: GET("$target/path/dm%3A%7Cdc%3AexistingClass") verifyResponse OK, response existingClass = responseBody().id @@ -1518,7 +1858,6 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { verifyResponse OK, response modifyAndModifyReturningDifference = responseBody().id - then: DELETE("$target/dataClasses/$existingClass/dataClasses/$deleteRightOnlyFromExistingClass") verifyResponse NO_CONTENT, response DELETE("$target/dataClasses/$deleteRightOnly") @@ -1549,16 +1888,9 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { PUT("$target", [description: 'DescriptionRight']) verifyResponse OK, response - when: - GET("$source/mergeDiff/$target", STRING_ARG) - - then: - verifyJsonResponse OK, expectedMergeDiffJson - - cleanup: - cleanUpData(source) - cleanUpData(target) - cleanUpData(id) + [id : id, + source: source, + target: target] } void 'MP01 : test merging diff with no patch data'() { @@ -1794,7 +2126,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { GET("$source/mergeDiff/$target", STRING_ARG) then: - verifyJsonResponse OK, expectedMergeDiffJson + verifyJsonResponse OK, expectedLegacyMergeDiffJson when: String modifiedDescriptionSource = 'modifiedDescriptionSource' From 83f544ff715b87a0f98ca845298ca73bd6183d61 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Wed, 23 Jun 2021 22:18:47 +0100 Subject: [PATCH 24/82] work on the new style mergediff json --- mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson b/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson index 0f8ccaf077..58b377ea68 100644 --- a/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson +++ b/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson @@ -7,5 +7,5 @@ model { json(arrayMergeDiff.fieldName) { if (arrayMergeDiff.deleted) deleted g.render(arrayMergeDiff.deleted) if (arrayMergeDiff.created) created g.render(arrayMergeDiff.created) - if (arrayMergeDiff.modified) modified g.render(arrayMergeDiff.modified) + if (arrayMergeDiff.modified) modified g.render(arrayMergeDiff.modified.sort {it.sourceIdentifier}) } From 115e15728bad24d0c572765199442576e3b4fe11 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Thu, 24 Jun 2021 09:40:43 +0100 Subject: [PATCH 25/82] Sort the mergediffs before rendering FunctionalSpec updated to the new sorted order and to use source and target with the new fields available --- .../views/arrayMergeDiff/_arrayMergeDiff.gson | 6 +- .../views/mergeDiff/_mergeDiff.gson | 4 +- .../tridirectional/CreationMergeDiff.groovy | 7 +- .../tridirectional/DeletionMergeDiff.groovy | 7 +- .../diff/tridirectional/FieldMergeDiff.groovy | 9 +- .../core/diff/tridirectional/MergeDiff.groovy | 9 +- .../datamodel/DataModelFunctionalSpec.groovy | 125 +++++++++--------- 7 files changed, 96 insertions(+), 71 deletions(-) diff --git a/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson b/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson index 58b377ea68..1a980a8577 100644 --- a/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson +++ b/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson @@ -5,7 +5,7 @@ model { } json(arrayMergeDiff.fieldName) { - if (arrayMergeDiff.deleted) deleted g.render(arrayMergeDiff.deleted) - if (arrayMergeDiff.created) created g.render(arrayMergeDiff.created) - if (arrayMergeDiff.modified) modified g.render(arrayMergeDiff.modified.sort {it.sourceIdentifier}) + if (arrayMergeDiff.created) created g.render(arrayMergeDiff.created.sort()) + if (arrayMergeDiff.deleted) deleted g.render(arrayMergeDiff.deleted.sort()) + if (arrayMergeDiff.modified) modified g.render(arrayMergeDiff.modified.sort()) } diff --git a/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson b/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson index 33024deb13..19948b58a0 100644 --- a/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson +++ b/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson @@ -24,8 +24,8 @@ json { if (mergeDiff.getTarget() instanceof ModelItem && mergeDiff.getSource() instanceof ModelItem) { ModelItem source = mergeDiff.getTarget() as ModelItem ModelItem target = mergeDiff.getTarget() as ModelItem - leftBreadcrumbs g.render(source.getBreadcrumbs()) - rightBreadcrumbs g.render(target.getBreadcrumbs()) + sourceBreadcrumbs g.render(source.getBreadcrumbs()) + targetBreadcrumbs g.render(target.getBreadcrumbs()) } count mergeDiff.getNumberOfDiffs() diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy index f6a4895f0d..14ab835996 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy @@ -5,7 +5,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable import groovy.transform.CompileStatic @CompileStatic -class CreationMergeDiff extends TriDirectionalDiff { +class CreationMergeDiff extends TriDirectionalDiff implements Comparable { CreationMergeDiff(Class targetClass) { super(targetClass) @@ -56,4 +56,9 @@ class CreationMergeDiff extends TriDirectionalDiff { String toString() { "Created :: ${createdIdentifier}" } + + @Override + int compareTo(CreationMergeDiff that) { + this.createdIdentifier <=> that.createdIdentifier + } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy index 6eb793463f..5c8e5d7338 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy @@ -6,7 +6,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import groovy.transform.CompileStatic @CompileStatic -class DeletionMergeDiff extends TriDirectionalDiff { +class DeletionMergeDiff extends TriDirectionalDiff implements Comparable { ObjectDiff mergeModificationDiff @@ -69,4 +69,9 @@ class DeletionMergeDiff extends TriDirectionalDiff { String str = "Deleted :: ${deletedIdentifier}" mergeModificationDiff ? "${str}\n >> Modified :: ${mergeModificationDiff}" : str } + + @Override + int compareTo(DeletionMergeDiff that) { + this.deletedIdentifier <=> that.deletedIdentifier + } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy index 43665e8e22..9cafad9a4a 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy @@ -21,7 +21,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional import groovy.transform.CompileStatic @CompileStatic -class FieldMergeDiff extends TriDirectionalDiff { +class FieldMergeDiff extends TriDirectionalDiff implements Comparable { String fieldName @@ -79,4 +79,11 @@ class FieldMergeDiff extends TriDirectionalDiff { String toString() { "${fieldName} :: ${source} <> ${target} :: ${commonAncestor}" } + + @Override + int compareTo(FieldMergeDiff that) { + if (this.diffType == FieldMergeDiff.simpleName && that.diffType == ArrayMergeDiff.simpleName) return -1 + if (this.diffType == ArrayMergeDiff.simpleName && that.diffType == FieldMergeDiff.simpleName) return 1 + this.fieldName <=> that.fieldName + } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy index 5bb77b4998..cf814c0e7b 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy @@ -38,7 +38,7 @@ import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.mergeDiff *
*/ @Slf4j -class MergeDiff extends TriDirectionalDiff { +class MergeDiff extends TriDirectionalDiff implements Comparable { private List diffs @@ -90,7 +90,7 @@ class MergeDiff extends TriDirectionalDiff { } List getDiffs() { - diffs + diffs.sort() } @Override @@ -497,4 +497,9 @@ class MergeDiff extends TriDirectionalDiff { modifiedDiffs + createdModifiedDiffs } + + @Override + int compareTo(MergeDiff that) { + this.sourceIdentifier <=> that.sourceIdentifier + } } 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 835f0b9c41..d0a2ee401b 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 @@ -445,14 +445,6 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "label": "Functional Test Model", "count": 11, "diffs": [ - { - "description": { - "source": "DescriptionLeft", - "target": "DescriptionRight", - "commonAncestor": null, - "isMergeConflict": true - } - }, { "branchName": { "source": "source", @@ -461,82 +453,90 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "isMergeConflict": false } }, + { + "description": { + "source": "DescriptionLeft", + "target": "DescriptionRight", + "commonAncestor": null, + "isMergeConflict": true + } + }, { "dataClasses": { - "deleted": [ + "created": [ { - "deleted": { + "created": { "id": "${json-unit.matches:id}", - "label": "deleteAndModify", + "label": "addLeftOnly", "breadcrumbs": [ { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "DataModel", - "finalised": true + "finalised": false } ] }, - "isMergeConflict": true, - "isSourceDeletionAndTargetModification": true + "isMergeConflict": false, + "isSourceModificationAndTargetDeletion": false }, - { - "deleted": { + { + "created": { "id": "${json-unit.matches:id}", - "label": "deleteLeftOnly", + "label": "modifyAndDelete", "breadcrumbs": [ { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "DataModel", - "finalised": true + "finalised": false } ] }, - "isMergeConflict": false, - "isSourceDeletionAndTargetModification": false + "isMergeConflict": true, + "isSourceModificationAndTargetDeletion": true } ], - "created": [ + "deleted": [ { - "created": { + "deleted": { "id": "${json-unit.matches:id}", - "label": "addLeftOnly", + "label": "deleteAndModify", "breadcrumbs": [ { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "DataModel", - "finalised": false + "finalised": true } ] }, - "isMergeConflict": false, - "isSourceModificationAndTargetDeletion": false + "isMergeConflict": true, + "isSourceDeletionAndTargetModification": true }, { - "created": { + "deleted": { "id": "${json-unit.matches:id}", - "label": "modifyAndDelete", + "label": "deleteLeftOnly", "breadcrumbs": [ { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "DataModel", - "finalised": false + "finalised": true } ] }, - "isMergeConflict": true, - "isSourceModificationAndTargetDeletion": true + "isMergeConflict": false, + "isSourceDeletionAndTargetModification": false } ], - "modified": [ + "modified": [ { - "targetId": "${json-unit.matches:id}", "sourceId": "${json-unit.matches:id}", + "targetId": "${json-unit.matches:id}", "label": "addAndAddReturningDifference", - "targetBreadcrumbs": [ + "sourceBreadcrumbs": [ { "id": "${json-unit.matches:id}", "label": "Functional Test Model", @@ -544,7 +544,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "finalised": false } ], - "sourceBreadcrumbs": [ + "targetBreadcrumbs": [ { "id": "${json-unit.matches:id}", "label": "Functional Test Model", @@ -556,19 +556,19 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "diffs": [ { "description": { - "target": "DescriptionRight", "source": "DescriptionLeft", - "isMergeConflict": true, - "commonAncestor": null + "target": "DescriptionRight", + "commonAncestor": null, + "isMergeConflict": true } } ] }, { - "targetId": "${json-unit.matches:id}", "sourceId": "${json-unit.matches:id}", + "targetId": "${json-unit.matches:id}", "label": "existingClass", - "targetBreadcrumbs": [ + "sourceBreadcrumbs": [ { "id": "${json-unit.matches:id}", "label": "Functional Test Model", @@ -576,7 +576,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "finalised": false } ], - "sourceBreadcrumbs": [ + "targetBreadcrumbs": [ { "id": "${json-unit.matches:id}", "label": "Functional Test Model", @@ -588,17 +588,17 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "diffs": [ { "dataClasses": { - "deleted": [ + "created": [ { - "value": { + "created": { "id": "${json-unit.matches:id}", - "label": "deleteLeftOnlyFromExistingClass", + "label": "addLeftToExistingClass", "breadcrumbs": [ { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "DataModel", - "finalised": true + "finalised": false }, { "id": "${json-unit.matches:id}", @@ -607,20 +607,21 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { } ] }, - "isMergeConflict": false + "isMergeConflict": false, + "isSourceModificationAndTargetDeletion": false } ], - "created": [ + "deleted": [ { - "value": { + "deleted": { "id": "${json-unit.matches:id}", - "label": "addLeftToExistingClass", + "label": "deleteLeftOnlyFromExistingClass", "breadcrumbs": [ { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "DataModel", - "finalised": false + "finalised": true }, { "id": "${json-unit.matches:id}", @@ -629,7 +630,8 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { } ] }, - "isMergeConflict": false + "isMergeConflict": false, + "isSourceDeletionAndTargetModification": false } ] } @@ -637,10 +639,10 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { ] }, { - "targetId": "${json-unit.matches:id}", "sourceId": "${json-unit.matches:id}", + "targetId": "${json-unit.matches:id}", "label": "modifyAndModifyReturningDifference", - "targetBreadcrumbs": [ + "sourceBreadcrumbs": [ { "id": "${json-unit.matches:id}", "label": "Functional Test Model", @@ -648,7 +650,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "finalised": false } ], - "sourceBreadcrumbs": [ + "targetBreadcrumbs": [ { "id": "${json-unit.matches:id}", "label": "Functional Test Model", @@ -660,19 +662,19 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "diffs": [ { "description": { - "target": "DescriptionRight", "source": "DescriptionLeft", - "isMergeConflict": true, - "commonAncestor": null + "target": "DescriptionRight", + "commonAncestor": null, + "isMergeConflict": true } } ] }, { - "targetId": "${json-unit.matches:id}", "sourceId": "${json-unit.matches:id}", + "targetId": "${json-unit.matches:id}", "label": "modifyLeftOnly", - "targetBreadcrumbs": [ + "sourceBreadcrumbs": [ { "id": "${json-unit.matches:id}", "label": "Functional Test Model", @@ -680,7 +682,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "finalised": true } ], - "sourceBreadcrumbs": [ + "targetBreadcrumbs": [ { "id": "${json-unit.matches:id}", "label": "Functional Test Model", @@ -692,8 +694,9 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "diffs": [ { "description": { - "target": null, "source": "Description", + "target": null, + "commonAncestor": null, "isMergeConflict": false } } From 0d73e283e539cee503698755d5a674b76d9b6385 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Thu, 24 Jun 2021 12:11:05 +0100 Subject: [PATCH 26/82] Add pathPrefix to every domain This will allow faster path building and identification and path building from breadcrumbs --- .../maurodatamapper/traits/domain/CreatorAware.groovy | 2 ++ .../softeng/maurodatamapper/core/admin/ApiProperty.groovy | 5 +++++ .../maurodatamapper/core/authority/Authority.groovy | 5 +++++ .../maurodatamapper/core/container/Classifier.groovy | 4 ++++ .../softeng/maurodatamapper/core/container/Folder.groovy | 4 ++++ .../maurodatamapper/core/container/VersionedFolder.groovy | 5 +++++ .../softeng/maurodatamapper/core/facet/Annotation.groovy | 5 +++++ .../ac/ox/softeng/maurodatamapper/core/facet/Edit.groovy | 4 ++++ .../ox/softeng/maurodatamapper/core/facet/Metadata.groovy | 4 ++++ .../maurodatamapper/core/facet/ReferenceFile.groovy | 4 ++++ .../ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy | 4 ++++ .../softeng/maurodatamapper/core/facet/SemanticLink.groovy | 4 ++++ .../softeng/maurodatamapper/core/facet/VersionLink.groovy | 4 ++++ .../core/facet/rule/RuleRepresentation.groovy | 5 +++++ .../softeng/maurodatamapper/core/file/UserImageFile.groovy | 5 +++++ .../maurodatamapper/core/util/test/BasicModel.groovy | 5 +++++ .../maurodatamapper/core/util/test/BasicModelItem.groovy | 4 ++++ .../ac/ox/softeng/maurodatamapper/dataflow/DataFlow.groovy | 5 +++++ .../dataflow/component/DataClassComponent.groovy | 5 +++++ .../dataflow/component/DataElementComponent.groovy | 5 +++++ .../ox/softeng/maurodatamapper/datamodel/DataModel.groovy | 5 +++++ .../maurodatamapper/datamodel/facet/SummaryMetadata.groovy | 5 +++++ .../facet/summarymetadata/SummaryMetadataReport.groovy | 7 ++++++- .../maurodatamapper/datamodel/item/DataClass.groovy | 4 ++++ .../maurodatamapper/datamodel/item/DataElement.groovy | 4 ++++ .../datamodel/item/datatype/DataType.groovy | 5 +++++ .../item/datatype/enumeration/EnumerationValue.groovy | 4 ++++ .../maurodatamapper/federation/SubscribedModel.groovy | 5 +++++ .../referencedata/ReferenceDataModel.groovy | 7 ++++++- .../referencedata/facet/ReferenceSummaryMetadata.groovy | 5 +++++ .../summarymetadata/ReferenceSummaryMetadataReport.groovy | 7 ++++++- .../referencedata/item/ReferenceDataElement.groovy | 4 ++++ .../referencedata/item/datatype/ReferenceDataType.groovy | 5 +++++ .../datatype/enumeration/ReferenceEnumerationValue.groovy | 4 ++++ .../ox/softeng/maurodatamapper/terminology/CodeSet.groovy | 5 +++++ .../softeng/maurodatamapper/terminology/Terminology.groovy | 5 +++++ .../softeng/maurodatamapper/terminology/item/Term.groovy | 5 +++++ .../terminology/item/TermRelationshipType.groovy | 5 +++++ .../terminology/item/term/TermRelationship.groovy | 5 +++++ .../softeng/maurodatamapper/security/CatalogueUser.groovy | 5 +++++ .../ox/softeng/maurodatamapper/security/UserGroup.groovy | 5 +++++ .../softeng/maurodatamapper/security/role/GroupRole.groovy | 5 +++++ .../security/role/SecurableResourceGroupRole.groovy | 5 +++++ .../maurodatamapper/security/test/BasicModel.groovy | 5 +++++ .../maurodatamapper/security/test/BasicModelItem.groovy | 4 ++++ 45 files changed, 210 insertions(+), 3 deletions(-) diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/traits/domain/CreatorAware.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/traits/domain/CreatorAware.groovy index db6e51c999..a99ce007b5 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/traits/domain/CreatorAware.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/traits/domain/CreatorAware.groovy @@ -45,6 +45,8 @@ trait CreatorAware { abstract String getDomainType() + abstract String getPathPrefix() + @Deprecated(forRemoval = true) void setCreatedByUser(User user) { this.createdBy = user?.emailAddress diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/admin/ApiProperty.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/admin/ApiProperty.groovy index f9edf5b225..0d92fcfeb0 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/admin/ApiProperty.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/admin/ApiProperty.groovy @@ -65,6 +65,11 @@ class ApiProperty implements CreatorAware { ApiProperty.simpleName } + @Override + String getPathPrefix() { + 'api' + } + def beforeValidate() { if (!lastUpdatedBy) lastUpdatedBy = createdBy } diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/authority/Authority.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/authority/Authority.groovy index 683a998343..6b96e6cc2f 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/authority/Authority.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/authority/Authority.groovy @@ -55,6 +55,11 @@ class Authority implements InformationAware, CreatorAware, SecurableResource { Authority.simpleName } + @Override + String getPathPrefix() { + 'auth' + } + @Override String toString() { diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Classifier.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Classifier.groovy index 35d467a4b4..9c9473f99a 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Classifier.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Classifier.groovy @@ -94,6 +94,10 @@ class Classifier implements Container { Classifier.simpleName } + @Override + String getPathPrefix() { + 'cl' + } @Override Classifier getPathParent() { diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Folder.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Folder.groovy index 47fbcfe370..83c2f14f34 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Folder.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Folder.groovy @@ -99,6 +99,10 @@ class Folder implements Container { Folder.simpleName } + @Override + String getPathPrefix() { + 'fo' + } boolean hasChildFolders() { Folder.countByParentFolder(this) diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolder.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolder.groovy index 8b82ccd312..f62befd2a8 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolder.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolder.groovy @@ -69,6 +69,11 @@ class VersionedFolder extends Folder implements VersionAware, VersionLinkAware { VersionedFolder.simpleName } + @Override + String getPathPrefix() { + 'vf' + } + static DetachedCriteria by() { new DetachedCriteria(VersionedFolder) } diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy index 43d9aefe8c..f80334e1f0 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy @@ -77,6 +77,11 @@ class Annotation implements MultiFacetItemAware, PathAware, InformationAware, Cr Annotation.simpleName } + @Override + String getPathPrefix() { + 'ann' + } + @Override Annotation getPathParent() { parentAnnotation diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Edit.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Edit.groovy index 4a1bde9b9a..44724ce3d8 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Edit.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Edit.groovy @@ -49,6 +49,10 @@ class Edit implements CreatorAware { Edit.simpleName } + @Override + String getPathPrefix() { + 'ed' + } @SuppressWarnings("UnnecessaryQualifiedReference") static List findAllByResource(String resourceDomainType, UUID resourceId, Map pagination = [:]) { diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy index 6cf33a5f25..d9ada1f6a8 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy @@ -74,6 +74,10 @@ class Metadata implements MultiFacetItemAware, CreatorAware, Diffable Metadata.simpleName } + @Override + String getPathPrefix() { + 'md' + } @Override String toString() { diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/ReferenceFile.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/ReferenceFile.groovy index 16fcf96e50..6df5d3f391 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/ReferenceFile.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/ReferenceFile.groovy @@ -53,6 +53,10 @@ class ReferenceFile implements CatalogueFile, MultiFacetItemAware { ReferenceFile.simpleName } + @Override + String getPathPrefix() { + 'rf' + } def beforeValidate() { fileSize = fileContents?.size() diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy index 91aaa2ddfe..4183fd5d51 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy @@ -76,6 +76,10 @@ class Rule implements MultiFacetItemAware, CreatorAware, Diffable { Rule.simpleName } + @Override + String getPathPrefix() { + 'ru' + } @Override String toString() { diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/SemanticLink.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/SemanticLink.groovy index fee4aba51f..52080b3c3e 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/SemanticLink.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/SemanticLink.groovy @@ -74,6 +74,10 @@ class SemanticLink implements MultiFacetItemAware, CreatorAware { SemanticLink.simpleName } + @Override + String getPathPrefix() { + 'sl' + } @Override String getEditLabel() { diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/VersionLink.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/VersionLink.groovy index b49a251a44..4ced12e44f 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/VersionLink.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/VersionLink.groovy @@ -74,6 +74,10 @@ class VersionLink implements MultiFacetItemAware, CreatorAware { VersionLink.simpleName } + @Override + String getPathPrefix() { + 'vl' + } @Override String getEditLabel() { diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy index d741287e53..9bdcc6b7ce 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy @@ -72,6 +72,11 @@ class RuleRepresentation implements Diffable, EditHistoryAwa RuleRepresentation.simpleName } + @Override + String getPathPrefix() { + 'rr' + } + @Override String toString() { "${getClass().getName()} : ${language}/${representation} : ${id ?: '(unsaved)'}" diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/file/UserImageFile.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/file/UserImageFile.groovy index eb4159247c..0cfb3b0128 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/file/UserImageFile.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/file/UserImageFile.groovy @@ -50,6 +50,11 @@ class UserImageFile implements CatalogueFile { UserImageFile.simpleName } + @Override + String getPathPrefix() { + 'uif' + } + def beforeValidate() { if (!fileName) fileName = "${userId}-profile" fileSize = fileContents?.size() diff --git a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/BasicModel.groovy b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/BasicModel.groovy index f621c81819..b7cd648ed0 100644 --- a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/BasicModel.groovy +++ b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/BasicModel.groovy @@ -71,6 +71,11 @@ class BasicModel implements Model, GormEntity { BasicModel.simpleName } + @Override + String getPathPrefix() { + 'bm' + } + Set getAllModelItems() { (modelItems ?: []) + modelItems.collect {it.getAllModelItems()}.flatten() } diff --git a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/BasicModelItem.groovy b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/BasicModelItem.groovy index 11c0c9916f..185af66d90 100644 --- a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/BasicModelItem.groovy +++ b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/BasicModelItem.groovy @@ -76,6 +76,10 @@ class BasicModelItem implements ModelItem, GormEntit BasicModelItem.simpleName } + @Override + String getPathPrefix() { + 'bmi' + } @Override String getEditLabel() { diff --git a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlow.groovy b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlow.groovy index 2fb55da8b4..d5b236aa5d 100644 --- a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlow.groovy +++ b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlow.groovy @@ -90,6 +90,11 @@ class DataFlow implements ModelItem { DataFlow.simpleName } + @Override + String getPathPrefix() { + 'df' + } + @Override GormEntity getPathParent() { target diff --git a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponent.groovy b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponent.groovy index 8675a8f559..8ba06f2672 100644 --- a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponent.groovy +++ b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponent.groovy @@ -88,6 +88,11 @@ class DataClassComponent implements ModelItem { DataClassComponent.simpleName } + @Override + String getPathPrefix() { + 'dcc' + } + @Override GormEntity getPathParent() { dataFlow diff --git a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponent.groovy b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponent.groovy index 796b90ca39..1e4b6136bb 100644 --- a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponent.groovy +++ b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponent.groovy @@ -87,6 +87,11 @@ class DataElementComponent implements ModelItem DataElementComponent.simpleName } + @Override + String getPathPrefix() { + 'dec' + } + @Override GormEntity getPathParent() { dataClassComponent diff --git a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModel.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModel.groovy index bbbd9b4796..39eaa0fad9 100644 --- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModel.groovy +++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModel.groovy @@ -163,6 +163,11 @@ class DataModel implements Model, SummaryMetadataAware, IndexedSiblin DataModel.simpleName } + @Override + String getPathPrefix() { + 'dm' + } + void setType(DataModelType type) { modelType = type.label } diff --git a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/SummaryMetadata.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/SummaryMetadata.groovy index 171f01cbb6..5fd4f440d9 100644 --- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/SummaryMetadata.groovy +++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/SummaryMetadata.groovy @@ -64,6 +64,11 @@ class SummaryMetadata implements MultiFacetItemAware, InformationAware, CreatorA SummaryMetadata.simpleName } + @Override + String getPathPrefix() { + 'sm' + } + String toString() { "${getClass().getName()} : ${label} : ${id ?: '(unsaved)'}" } diff --git a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/summarymetadata/SummaryMetadataReport.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/summarymetadata/SummaryMetadataReport.groovy index 979bbe9381..4a35e44b12 100644 --- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/summarymetadata/SummaryMetadataReport.groovy +++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/summarymetadata/SummaryMetadataReport.groovy @@ -52,7 +52,12 @@ class SummaryMetadataReport implements CreatorAware { @Override String getDomainType() { - SummaryMetadata.simpleName + SummaryMetadataReport.simpleName + } + + @Override + String getPathPrefix() { + 'smr' } String getEditLabel() { diff --git a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClass.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClass.groovy index ca69d0272c..6ff901e9d7 100644 --- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClass.groovy +++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClass.groovy @@ -165,6 +165,10 @@ class DataClass implements ModelItem, MultiplicityAware, S DataClass.simpleName } + @Override + String getPathPrefix() { + 'dc' + } @Field(index = Index.YES, bridge = @FieldBridge(impl = UUIDBridge)) UUID getModelId() { diff --git a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElement.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElement.groovy index 5cdac51141..b668ee70c2 100644 --- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElement.groovy +++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElement.groovy @@ -126,6 +126,10 @@ class DataElement implements ModelItem, MultiplicityAwar DataElement.simpleName } + @Override + String getPathPrefix() { + 'de' + } @Field(index = Index.YES, bridge = @FieldBridge(impl = UUIDBridge)) UUID getModelId() { diff --git a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataType.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataType.groovy index bc440c3eeb..f819ea2213 100644 --- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataType.groovy +++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataType.groovy @@ -108,6 +108,11 @@ abstract class DataType implements ModelItem, SummaryMetadataAw DataType() { } + @Override + String getPathPrefix() { + 'dt' + } + @Field(index = Index.YES, bridge = @FieldBridge(impl = UUIDBridge)) UUID getModelId() { dataModel.id diff --git a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValue.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValue.groovy index 2235fd8bce..29bda21345 100644 --- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValue.groovy +++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValue.groovy @@ -91,6 +91,10 @@ class EnumerationValue implements ModelItem { EnumerationValue.simpleName } + @Override + String getPathPrefix() { + 'ev' + } @Override GormEntity getPathParent() { 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 a19960d5fc..6ccaf59a6e 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 @@ -65,6 +65,11 @@ class SubscribedModel implements SecurableResource, EditHistoryAware { SubscribedModel.simpleName } + @Override + String getPathPrefix() { + 'subm' + } + @Override String getEditLabel() { "SubscribedModel:${id}" diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModel.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModel.groovy index 253ecdc734..72bb9711e7 100644 --- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModel.groovy +++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModel.groovy @@ -114,6 +114,11 @@ class ReferenceDataModel implements Model, ReferenceSummaryM ReferenceDataModel.simpleName } + @Override + String getPathPrefix() { + 'rdm' + } + ObjectDiff diff(ReferenceDataModel otherDataModel) { modelDiffBuilder(ReferenceDataModel, this, otherDataModel) .appendList(ReferenceDataType, 'referenceDataTypes', this.referenceDataTypes, otherDataModel.referenceDataTypes) @@ -122,7 +127,7 @@ class ReferenceDataModel implements Model, ReferenceSummaryM def beforeValidate() { beforeValidateCatalogueItem() - this.referenceDataTypes?.each { it.beforeValidate() } + this.referenceDataTypes?.each {it.beforeValidate()} this.referenceDataElements?.each { it.beforeValidate() } } diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadata.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadata.groovy index 9db1512709..3dff2bc83c 100644 --- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadata.groovy +++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadata.groovy @@ -64,6 +64,11 @@ class ReferenceSummaryMetadata implements MultiFacetItemAware, InformationAware, ReferenceSummaryMetadata.simpleName } + @Override + String getPathPrefix() { + 'rsm' + } + String toString() { "${getClass().getName()} : ${label} : ${id ?: '(unsaved)'}" } diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/summarymetadata/ReferenceSummaryMetadataReport.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/summarymetadata/ReferenceSummaryMetadataReport.groovy index 953c1c9ab4..e1df51b721 100644 --- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/summarymetadata/ReferenceSummaryMetadataReport.groovy +++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/summarymetadata/ReferenceSummaryMetadataReport.groovy @@ -52,7 +52,12 @@ class ReferenceSummaryMetadataReport implements CreatorAware { @Override String getDomainType() { - ReferenceSummaryMetadata.simpleName + ReferenceSummaryMetadataReport.simpleName + } + + @Override + String getPathPrefix() { + 'rsmr' } String getEditLabel() { diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataElement.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataElement.groovy index a8da1a7e98..b43fdbb46a 100644 --- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataElement.groovy +++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataElement.groovy @@ -110,6 +110,10 @@ class ReferenceDataElement implements ModelItem implements ModelItem, ReferenceDataType() { } + @Override + String getPathPrefix() { + 'rdt' + } + @Field(index = Index.YES, bridge = @FieldBridge(impl = UUIDBridge)) UUID getModelId() { referenceDataModel.id diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/enumeration/ReferenceEnumerationValue.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/enumeration/ReferenceEnumerationValue.groovy index d0a9ccf4fb..e1df710e86 100644 --- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/enumeration/ReferenceEnumerationValue.groovy +++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/enumeration/ReferenceEnumerationValue.groovy @@ -90,6 +90,10 @@ class ReferenceEnumerationValue implements ModelItem { CodeSet.simpleName } + @Override + String getPathPrefix() { + 'cs' + } + @Override String getEditLabel() { "CodeSet:${label}" diff --git a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/Terminology.groovy b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/Terminology.groovy index f2debe0550..076511f0ba 100644 --- a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/Terminology.groovy +++ b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/Terminology.groovy @@ -106,6 +106,11 @@ class Terminology implements Model { Terminology.simpleName } + @Override + String getPathPrefix() { + 'te' + } + @Override String getEditLabel() { "Terminology:${label}" diff --git a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/Term.groovy b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/Term.groovy index e6dd333e23..ddf5fd81a6 100644 --- a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/Term.groovy +++ b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/Term.groovy @@ -121,6 +121,11 @@ class Term implements ModelItem { Term.simpleName } + @Override + String getPathPrefix() { + 'tm' + } + @Field(index = Index.YES, bridge = @FieldBridge(impl = UUIDBridge)) UUID getModelId() { terminology.id diff --git a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermRelationshipType.groovy b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermRelationshipType.groovy index 8740bf0ae6..f351ac7031 100644 --- a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermRelationshipType.groovy +++ b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermRelationshipType.groovy @@ -89,6 +89,11 @@ class TermRelationshipType implements ModelItem { TermRelationship.simpleName } + @Override + String getPathPrefix() { + 'tr' + } + def beforeValidate() { label = relationshipType?.label beforeValidateModelItem() diff --git a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/CatalogueUser.groovy b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/CatalogueUser.groovy index 1954910fbc..301ed94119 100644 --- a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/CatalogueUser.groovy +++ b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/CatalogueUser.groovy @@ -138,6 +138,11 @@ class CatalogueUser implements Principal, EditHistoryAware, CreatorAware, User { CatalogueUser.simpleName } + @Override + String getPathPrefix() { + 'cu' + } + boolean isDisabled() { disabled } diff --git a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/UserGroup.groovy b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/UserGroup.groovy index b4ea0c9c59..ef748aa62e 100644 --- a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/UserGroup.groovy +++ b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/UserGroup.groovy @@ -72,6 +72,11 @@ class UserGroup implements EditHistoryAware, SecurableResource, Principal { UserGroup.simpleName } + @Override + String getPathPrefix() { + 'ug' + } + @Override Boolean getReadableByEveryone() { false diff --git a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/GroupRole.groovy b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/GroupRole.groovy index 3861273090..a5407776c7 100644 --- a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/GroupRole.groovy +++ b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/GroupRole.groovy @@ -114,6 +114,11 @@ class GroupRole implements EditHistoryAware, PathAware, SecurableResource, Compa GroupRole.simpleName } + @Override + String getPathPrefix() { + 'gr' + } + @Override Boolean getReadableByEveryone() { false diff --git a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/SecurableResourceGroupRole.groovy b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/SecurableResourceGroupRole.groovy index 8b70f5f6a3..edb3c89662 100644 --- a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/SecurableResourceGroupRole.groovy +++ b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/SecurableResourceGroupRole.groovy @@ -76,6 +76,11 @@ class SecurableResourceGroupRole implements EditHistoryAware { SecurableResourceGroupRole.simpleName } + @Override + String getPathPrefix() { + 'srgr' + } + void setSecurableResource(SecurableResource securableResource, boolean justLoad) { this.securableResource = securableResource if (!justLoad) { diff --git a/mdm-security/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/security/test/BasicModel.groovy b/mdm-security/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/security/test/BasicModel.groovy index 4ec2725b64..5a7160e808 100644 --- a/mdm-security/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/security/test/BasicModel.groovy +++ b/mdm-security/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/security/test/BasicModel.groovy @@ -100,6 +100,11 @@ class BasicModel implements Model, GormEntity { modelDiffBuilder(BasicModel, this, obj) } + @Override + String getPathPrefix() { + 'bm' + } + static BasicModel findByIdJoinClassifiers(UUID id) { new DetachedCriteria(BasicModel).idEq(id).join('classifiers').get() } diff --git a/mdm-security/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/security/test/BasicModelItem.groovy b/mdm-security/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/security/test/BasicModelItem.groovy index 3737a922bc..9073eee2f4 100644 --- a/mdm-security/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/security/test/BasicModelItem.groovy +++ b/mdm-security/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/security/test/BasicModelItem.groovy @@ -76,6 +76,10 @@ class BasicModelItem implements ModelItem, GormEntit BasicModelItem.simpleName } + @Override + String getPathPrefix() { + 'bmi' + } @Override String getEditLabel() { From a5bd0e41fd49756346bee705025931b0085139a2 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Thu, 24 Jun 2021 17:35:10 +0100 Subject: [PATCH 27/82] Flatten the structure and clean up for MergeDiff rendering * Add paths * Remove seemingly unnecessary components --- .../views/arrayMergeDiff/_arrayMergeDiff.gson | 4 +- .../creationMergeDiff/_creationMergeDiff.gson | 3 +- .../deletionMergeDiff/_deletionMergeDiff.gson | 3 +- .../grails-app/views/diffable/_diffable.gson | 4 +- .../views/fieldMergeDiff/_fieldMergeDiff.gson | 4 +- .../views/mergeDiff/_mergeDiff.gson | 15 +- .../maurodatamapper/core/diff/Diffable.groovy | 2 + .../diff/tridirectional/ArrayMergeDiff.groovy | 16 +- .../tridirectional/CreationMergeDiff.groovy | 11 +- .../tridirectional/DeletionMergeDiff.groovy | 10 + .../diff/tridirectional/FieldMergeDiff.groovy | 18 +- .../core/diff/tridirectional/MergeDiff.groovy | 163 +++++++---- .../tridirectional/TriDirectionalDiff.groovy | 10 +- .../datamodel/DataModelFunctionalSpec.groovy | 269 +----------------- 14 files changed, 186 insertions(+), 346 deletions(-) diff --git a/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson b/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson index 1a980a8577..f89727675d 100644 --- a/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson +++ b/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson @@ -4,7 +4,9 @@ model { ArrayMergeDiff arrayMergeDiff } -json(arrayMergeDiff.fieldName) { +json { + fieldName arrayMergeDiff.fieldName + path arrayMergeDiff.fullyQualifiedPath if (arrayMergeDiff.created) created g.render(arrayMergeDiff.created.sort()) if (arrayMergeDiff.deleted) deleted g.render(arrayMergeDiff.deleted.sort()) if (arrayMergeDiff.modified) modified g.render(arrayMergeDiff.modified.sort()) diff --git a/mdm-core/grails-app/views/creationMergeDiff/_creationMergeDiff.gson b/mdm-core/grails-app/views/creationMergeDiff/_creationMergeDiff.gson index 67e87ecf4e..684dd3b189 100644 --- a/mdm-core/grails-app/views/creationMergeDiff/_creationMergeDiff.gson +++ b/mdm-core/grails-app/views/creationMergeDiff/_creationMergeDiff.gson @@ -5,7 +5,8 @@ model { } json { - created tmpl.'/diffable/diffable'(creationMergeDiff.created) + path creationMergeDiff.fullyQualifiedPath + // objectCreated tmpl.'/diffable/diffable'(diffable: creationMergeDiff.created, renderBreadcrumbs: false) isMergeConflict creationMergeDiff.isMergeConflict() isSourceModificationAndTargetDeletion creationMergeDiff.isSourceModificationAndTargetDeletion() } \ No newline at end of file diff --git a/mdm-core/grails-app/views/deletionMergeDiff/_deletionMergeDiff.gson b/mdm-core/grails-app/views/deletionMergeDiff/_deletionMergeDiff.gson index efbf455d39..c30386b148 100644 --- a/mdm-core/grails-app/views/deletionMergeDiff/_deletionMergeDiff.gson +++ b/mdm-core/grails-app/views/deletionMergeDiff/_deletionMergeDiff.gson @@ -5,7 +5,8 @@ model { } json { - deleted tmpl.'/diffable/diffable'(deletionMergeDiff.deleted) + path deletionMergeDiff.fullyQualifiedPath + // objectDeleted tmpl.'/diffable/diffable'(diffable: deletionMergeDiff.deleted, renderBreadcrumbs: false) isMergeConflict deletionMergeDiff.isMergeConflict() isSourceDeletionAndTargetModification deletionMergeDiff.isSourceDeletionAndTargetModification() } \ No newline at end of file diff --git a/mdm-core/grails-app/views/diffable/_diffable.gson b/mdm-core/grails-app/views/diffable/_diffable.gson index c58ed2b9ec..db920bef40 100644 --- a/mdm-core/grails-app/views/diffable/_diffable.gson +++ b/mdm-core/grails-app/views/diffable/_diffable.gson @@ -6,7 +6,7 @@ import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware model { Diffable diffable - + Boolean renderBreadcrumbs = true } json { @@ -24,7 +24,7 @@ json { value(((Metadata) diffable).getValue()) } - if (diffable instanceof ModelItem) { + if (renderBreadcrumbs && diffable instanceof ModelItem) { breadcrumbs g.render(((ModelItem) diffable).getBreadcrumbs()) } } diff --git a/mdm-core/grails-app/views/fieldMergeDiff/_fieldMergeDiff.gson b/mdm-core/grails-app/views/fieldMergeDiff/_fieldMergeDiff.gson index d4f2084449..a9c5f9f19c 100644 --- a/mdm-core/grails-app/views/fieldMergeDiff/_fieldMergeDiff.gson +++ b/mdm-core/grails-app/views/fieldMergeDiff/_fieldMergeDiff.gson @@ -4,7 +4,9 @@ model { FieldMergeDiff fieldMergeDiff } -json(fieldMergeDiff.fieldName) { +json { + fieldName fieldMergeDiff.fieldName + path fieldMergeDiff.fullyQualifiedPath source fieldMergeDiff.getSource() target fieldMergeDiff.getTarget() commonAncestor fieldMergeDiff.commonAncestor diff --git a/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson b/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson index 19948b58a0..354a6fc6d9 100644 --- a/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson +++ b/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson @@ -1,6 +1,5 @@ import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata -import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.InformationAware model { @@ -12,6 +11,8 @@ json { sourceId mergeDiff.getSourceId() targetId mergeDiff.getTargetId() + path mergeDiff.fullyQualifiedPath + if (mergeDiff.getTarget() instanceof InformationAware) { label(((InformationAware) mergeDiff.getTarget()).getLabel()) } @@ -21,12 +22,12 @@ json { key(((Metadata) mergeDiff.getTarget()).getKey()) } - if (mergeDiff.getTarget() instanceof ModelItem && mergeDiff.getSource() instanceof ModelItem) { - ModelItem source = mergeDiff.getTarget() as ModelItem - ModelItem target = mergeDiff.getTarget() as ModelItem - sourceBreadcrumbs g.render(source.getBreadcrumbs()) - targetBreadcrumbs g.render(target.getBreadcrumbs()) - } + // if (mergeDiff.getTarget() instanceof ModelItem && mergeDiff.getSource() instanceof ModelItem) { + // ModelItem source = mergeDiff.getTarget() as ModelItem + // ModelItem target = mergeDiff.getTarget() as ModelItem + // sourceBreadcrumbs g.render(source.getBreadcrumbs()) + // targetBreadcrumbs g.render(target.getBreadcrumbs()) + // } count mergeDiff.getNumberOfDiffs() diffs g.render(mergeDiff.getDiffs()) diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diffable.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diffable.groovy index 03b8dce1a0..95e3e781b3 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diffable.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diffable.groovy @@ -27,4 +27,6 @@ interface Diffable { ObjectDiff diff(T obj) String getDiffIdentifier() + + String getPathPrefix() } \ No newline at end of file diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy index 68c10936dd..36ee1ecbfb 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy @@ -59,6 +59,10 @@ class ArrayMergeDiff extends FieldMergeDiff> { super.forFieldName(fieldName) as ArrayMergeDiff } + ArrayMergeDiff insideFullyQualifiedObjectPath(String fullyQualifiedObjectPath) { + super.insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) as ArrayMergeDiff + } + @Override ArrayMergeDiff withSource(Collection lhs) { super.withSource(lhs) as ArrayMergeDiff @@ -79,14 +83,20 @@ class ArrayMergeDiff extends FieldMergeDiff> { } @Override - Integer getNumberOfDiffs() { - created.size() + deleted.size() + ((modified.sum {it.getNumberOfDiffs()} ?: 0) as Integer) + ArrayMergeDiff getValidOnly() { + super.getValidOnly() as ArrayMergeDiff } - boolean hasDiffs() { + @Override + boolean hasDiff() { getNumberOfDiffs() != 0 } + @Override + Integer getNumberOfDiffs() { + created.size() + deleted.size() + ((modified.sum {it.getNumberOfDiffs()} ?: 0) as Integer) + } + @Override String toString() { String diffIdentifier = source?.first()?.diffIdentifier ?: target?.first()?.diffIdentifier ?: commonAncestor?.first()?.diffIdentifier diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy index 14ab835996..c0af812afb 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy @@ -24,15 +24,24 @@ class CreationMergeDiff extends TriDirectionalDiff implem created.diffIdentifier } + String getFullyQualifiedPath() { + String cleanedIdentifier = createdIdentifier.split('/').last() + "${fullyQualifiedObjectPath}|${created.pathPrefix}:${cleanedIdentifier}" + } + boolean isSourceModificationAndTargetDeletion() { commonAncestor != null } @SuppressWarnings('GrDeprecatedAPIUsage') - CreationMergeDiff whichCreated(C object) { + CreationMergeDiff whichCreated(C object) { withSource(object) as CreationMergeDiff } + CreationMergeDiff insideFullyQualifiedObjectPath(String fullyQualifiedObjectPath) { + super.insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) as CreationMergeDiff + } + CreationMergeDiff withCommonAncestor(C ca) { super.withCommonAncestor(ca) as CreationMergeDiff } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy index 5c8e5d7338..a2142a8114 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy @@ -31,11 +31,21 @@ class DeletionMergeDiff extends TriDirectionalDiff implem mergeModificationDiff != null } + String getFullyQualifiedPath() { + String cleanedIdentifier = deletedIdentifier.split('/').last() + "${fullyQualifiedObjectPath}|${deleted.pathPrefix}:${cleanedIdentifier}" + } + DeletionMergeDiff whichDeleted(D object) { this.value = object withCommonAncestor object } + @Override + DeletionMergeDiff insideFullyQualifiedObjectPath(String fullyQualifiedObjectPath) { + super.insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) as DeletionMergeDiff + } + DeletionMergeDiff withMergeModification(ObjectDiff modifiedDiff) { this.mergeModificationDiff = modifiedDiff this diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy index 9cafad9a4a..2b62f6dcb1 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy @@ -23,7 +23,7 @@ import groovy.transform.CompileStatic @CompileStatic class FieldMergeDiff extends TriDirectionalDiff implements Comparable { - String fieldName + private String fieldName FieldMergeDiff(Class targetClass) { super(targetClass) @@ -34,6 +34,10 @@ class FieldMergeDiff extends TriDirectionalDiff implements Comparable insideFullyQualifiedObjectPath(String fullyQualifiedObjectPath) { + super.insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) as FieldMergeDiff + } + @Override FieldMergeDiff withSource(F lhs) { super.withSource(lhs) as FieldMergeDiff @@ -53,6 +57,10 @@ class FieldMergeDiff extends TriDirectionalDiff implements Comparable } + FieldMergeDiff getValidOnly() { + hasDiff() ? this : null + } + @Override boolean equals(o) { if (this.is(o)) return true @@ -71,10 +79,18 @@ class FieldMergeDiff extends TriDirectionalDiff implements Comparable ${target} :: ${commonAncestor}" diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy index cf814c0e7b..96ecb18ef0 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy @@ -9,6 +9,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.CreationDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.DeletionDiff import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware +import groovy.transform.CompileStatic import groovy.transform.stc.ClosureParams import groovy.transform.stc.SimpleType import groovy.util.logging.Slf4j @@ -38,6 +39,7 @@ import static uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder.mergeDiff * */ @Slf4j +@CompileStatic class MergeDiff extends TriDirectionalDiff implements Comparable { private List diffs @@ -77,6 +79,12 @@ class MergeDiff extends TriDirectionalDiff implements Com (target as CreatorAware).id } + String getFullyQualifiedPath() { + String cleanedIdentifier = sourceIdentifier.split('/').last() + String localPath = "${source.pathPrefix}:${cleanedIdentifier}" + fullyQualifiedObjectPath ? "${fullyQualifiedObjectPath}|${localPath}" : localPath + } + FieldMergeDiff first() { diffs.first() } @@ -104,6 +112,11 @@ class MergeDiff extends TriDirectionalDiff implements Com super.withSource(sourceSide) as MergeDiff } + @Override + MergeDiff insideFullyQualifiedObjectPath(String fullyQualifiedObjectPath) { + super.insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) as MergeDiff + } + MergeDiff intoDiffable(M targetSide) { super.withTarget(targetSide) as MergeDiff } @@ -141,6 +154,11 @@ class MergeDiff extends TriDirectionalDiff implements Com diffs.find closure } + @Override + int compareTo(MergeDiff that) { + this.sourceIdentifier <=> that.sourceIdentifier + } + /** * The resulting MergeDiff should display all the actual differences including merge conflicts with the intent of merging the LHS/source into the * RHS/target. @@ -161,8 +179,8 @@ class MergeDiff extends TriDirectionalDiff implements Com generateMergeDiffFromSourceAgainstTarget() } - private MergeDiff generateMergeDiffFromCommonAncestorAgainstSourceAndTarget() { + log.debug('Generating merge diff from common ancestor diffs against source and target for [{}]', commonAncestorDiffSource.leftIdentifier) commonAncestorDiffSource.diffs.each {FieldDiff caSourceFieldDiff -> @@ -175,11 +193,12 @@ class MergeDiff extends TriDirectionalDiff implements Com log.debug('[{}] Change present between common ancestor and target', caSourceFieldDiff.fieldName) switch (caSourceFieldDiff.diffType) { case ArrayDiff.simpleName: - append createArrayMergeDiffPresentOnBothSides(caSourceFieldDiff as ArrayDiff, + append createArrayMergeDiffPresentOnBothSides(fullyQualifiedPath, + caSourceFieldDiff as ArrayDiff, caTargetFieldDiff as ArrayDiff) break case FieldDiff.simpleName: - append createFieldMergeDiffPresentOnBothSides(caSourceFieldDiff, caTargetFieldDiff) + append createFieldMergeDiffPresentOnBothSides(fullyQualifiedPath, caSourceFieldDiff, caTargetFieldDiff) break default: log.warn('Unhandled diff type {} on both sides', caSourceFieldDiff.diffType) @@ -190,10 +209,10 @@ class MergeDiff extends TriDirectionalDiff implements Com log.debug('[{}] No change present between common ancestor and target', caSourceFieldDiff.fieldName) switch (caSourceFieldDiff.diffType) { case ArrayDiff.simpleName: - append createArrayMergeDiffPresentOnOneSide(caSourceFieldDiff as ArrayDiff) + append createArrayMergeDiffPresentOnOneSide(fullyQualifiedPath, caSourceFieldDiff as ArrayDiff) break case FieldDiff.simpleName: - append createFieldMergeDiffPresentOnOneSide(caSourceFieldDiff) + append createFieldMergeDiffPresentOnOneSide(fullyQualifiedPath, caSourceFieldDiff) break default: log.warn('Unhandled diff type {} on both sides', caSourceFieldDiff.diffType) @@ -204,15 +223,16 @@ class MergeDiff extends TriDirectionalDiff implements Com } private MergeDiff generateMergeDiffFromCommonAncestorAgainstSource() { + log.debug('Generating merge diff from common ancestor diff against source for [{}]', commonAncestorDiffSource.leftIdentifier) commonAncestorDiffSource.diffs.each {FieldDiff caSourceFieldDiff -> log.debug('Processing field [{}] with change type [{}] present between common ancestor and source', caSourceFieldDiff.fieldName, caSourceFieldDiff.diffType) switch (caSourceFieldDiff.diffType) { case ArrayDiff.simpleName: - append createArrayMergeDiffPresentOnOneSide(caSourceFieldDiff as ArrayDiff) + append createArrayMergeDiffPresentOnOneSide(fullyQualifiedPath, caSourceFieldDiff as ArrayDiff) break case FieldDiff.simpleName: - append createFieldMergeDiffPresentOnOneSide(caSourceFieldDiff) + append createFieldMergeDiffPresentOnOneSide(fullyQualifiedPath, caSourceFieldDiff) break default: log.warn('Unhandled diff type {} on both sides', caSourceFieldDiff.diffType) @@ -223,14 +243,15 @@ class MergeDiff extends TriDirectionalDiff implements Com private MergeDiff generateMergeDiffFromSourceAgainstTarget() { log.debug('Generating merge diff from source diff against target for [{}]', sourceDiffTarget.leftIdentifier) + sourceDiffTarget.diffs.each {FieldDiff sourceTargetFieldDiff -> log.debug('Processing field [{}] with change type [{}] present between source and target', sourceTargetFieldDiff.fieldName, sourceTargetFieldDiff.diffType) switch (sourceTargetFieldDiff.diffType) { case ArrayDiff.simpleName: - append createArrayMergeDiffFromSourceTargetArrayDiff(sourceTargetFieldDiff as ArrayDiff) + append createArrayMergeDiffFromSourceTargetArrayDiff(fullyQualifiedPath, sourceTargetFieldDiff as ArrayDiff) break case FieldDiff.simpleName: - append createFieldMergeDiffFromSourceTargetFieldDiff(sourceTargetFieldDiff) + append createFieldMergeDiffFromSourceTargetFieldDiff(fullyQualifiedPath, sourceTargetFieldDiff) break default: log.warn('Unhandled diff type {} on both sides', sourceTargetFieldDiff.diffType) @@ -239,19 +260,24 @@ class MergeDiff extends TriDirectionalDiff implements Com this } - static ArrayMergeDiff createArrayMergeDiffFromSourceTargetArrayDiff(ArrayDiff sourceTargetArrayDiff) { + static ArrayMergeDiff createArrayMergeDiffFromSourceTargetArrayDiff(String fullyQualifiedObjectPath, ArrayDiff sourceTargetArrayDiff) { log.debug('[{}] Processing array differences against target from source', sourceTargetArrayDiff.fieldName) // Created and Deleted diffs in this array are left as-is as they are guaranteed to be unique and no issue arrayMergeDiff(sourceTargetArrayDiff.targetClass) .forFieldName(sourceTargetArrayDiff.fieldName) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) .withSource(sourceTargetArrayDiff.left) .withTarget(sourceTargetArrayDiff.right) .withCommonAncestor(null) .withCreatedMergeDiffs(sourceTargetArrayDiff.created.collect {c -> - creationMergeDiff(c.targetClass).whichCreated(c.created) + creationMergeDiff(c.targetClass) + .whichCreated(c.created) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) }) .withDeletedMergeDiffs(sourceTargetArrayDiff.deleted.collect {d -> - deletionMergeDiff(d.targetClass).whichDeleted(d.deleted) + deletionMergeDiff(d.targetClass) + .whichDeleted(d.deleted) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) }) .withModifiedMergeDiffs(sourceTargetArrayDiff.modified.collect {m -> mergeDiff(m.targetClass) @@ -264,78 +290,85 @@ class MergeDiff extends TriDirectionalDiff implements Com } - static ArrayMergeDiff createBaseArrayMergeDiffPresentOnOneSide(ArrayDiff caSourceArrayDiff) { + static ArrayMergeDiff createBaseArrayMergeDiffPresentOnOneSide(String fullyQualifiedObjectPath, + ArrayDiff caSourceArrayDiff) { // Created and Deleted diffs in this array are left as-is as they are guaranteed to be unique and no issue arrayMergeDiff(caSourceArrayDiff.targetClass) .forFieldName(caSourceArrayDiff.fieldName) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) .withSource(caSourceArrayDiff.right) // just use whats in the commonAncestor as no caTarget data denotes no difference between the CA and the target .withTarget(caSourceArrayDiff.left) .withCommonAncestor(caSourceArrayDiff.left) // both diffs have the same LHS } - static ArrayMergeDiff createArrayMergeDiffPresentOnBothSides(ArrayDiff caSourceArrayDiff, + static ArrayMergeDiff createArrayMergeDiffPresentOnBothSides(String fullyQualifiedObjectPath, + ArrayDiff caSourceArrayDiff, ArrayDiff caTargetArrayDiff) { log.debug('[{}] Processing array differences against common ancestor on both sides', caSourceArrayDiff.fieldName) - ArrayMergeDiff arrayMergeDiff = createBaseArrayMergeDiffPresentOnOneSide(caSourceArrayDiff) + createBaseArrayMergeDiffPresentOnOneSide(fullyQualifiedObjectPath, caSourceArrayDiff) .withTarget(caTargetArrayDiff.right) - .withCreatedMergeDiffs(createCreationMergeDiffsPresentOnBothSides(caSourceArrayDiff, caTargetArrayDiff)) - .withDeletedMergeDiffs(createDeletionMergeDiffsPresentOnBothSides(caSourceArrayDiff.deleted, caTargetArrayDiff)) - .withModifiedMergeDiffs(createModifiedMergeDiffsPresentOnBothSides(caSourceArrayDiff, caTargetArrayDiff)) - - // If no actual differences in any section then return null - arrayMergeDiff.hasDiffs() ? arrayMergeDiff : null + .withCreatedMergeDiffs(createCreationMergeDiffsPresentOnBothSides(fullyQualifiedObjectPath, caSourceArrayDiff, caTargetArrayDiff)) + .withDeletedMergeDiffs(createDeletionMergeDiffsPresentOnBothSides(fullyQualifiedObjectPath, caSourceArrayDiff.deleted, caTargetArrayDiff)) + .withModifiedMergeDiffs(createModifiedMergeDiffsPresentOnBothSides(fullyQualifiedObjectPath, caSourceArrayDiff, caTargetArrayDiff)) + .getValidOnly() } - static ArrayMergeDiff createArrayMergeDiffPresentOnOneSide(ArrayDiff caSourceArrayDiff) { + static ArrayMergeDiff createArrayMergeDiffPresentOnOneSide(String fullyQualifiedObjectPath, + ArrayDiff caSourceArrayDiff) { log.debug('[{}] Processing array differences against common ancestor on one side', caSourceArrayDiff.fieldName) // Created and Deleted diffs in this array are left as-is as they are guaranteed to be unique and no issue - createBaseArrayMergeDiffPresentOnOneSide(caSourceArrayDiff) - .withCreatedMergeDiffs(createCreationMergeDiffsPresentOnOneSide(caSourceArrayDiff.created)) - .withDeletedMergeDiffs(createDeletionMergeDiffsPresentOnOneSide(caSourceArrayDiff.deleted)) - .withModifiedMergeDiffs(createModifiedMergeDiffsPresentOnOneSide(caSourceArrayDiff.modified)) + createBaseArrayMergeDiffPresentOnOneSide(fullyQualifiedObjectPath, caSourceArrayDiff) + .withCreatedMergeDiffs(createCreationMergeDiffsPresentOnOneSide(fullyQualifiedObjectPath, caSourceArrayDiff.created)) + .withDeletedMergeDiffs(createDeletionMergeDiffsPresentOnOneSide(fullyQualifiedObjectPath, caSourceArrayDiff.deleted)) + .withModifiedMergeDiffs(createModifiedMergeDiffsPresentOnOneSide(fullyQualifiedObjectPath, caSourceArrayDiff.modified)) + .getValidOnly() } - static FieldMergeDiff createFieldMergeDiffFromSourceTargetFieldDiff(FieldDiff sourceTargetFieldDiff) { + static FieldMergeDiff createFieldMergeDiffFromSourceTargetFieldDiff(String fullyQualifiedObjectPath, FieldDiff sourceTargetFieldDiff) { log.debug('[{}] Processing field difference against target from source', sourceTargetFieldDiff.fieldName) fieldMergeDiff(sourceTargetFieldDiff.targetClass) .forFieldName(sourceTargetFieldDiff.fieldName) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) .withSource(sourceTargetFieldDiff.left) .withTarget(sourceTargetFieldDiff.right) .withCommonAncestor(null) .asMergeConflict() // Always a merge conflict as the values have to be different otherwise we wouldnt be here + .getValidOnly() } - static FieldMergeDiff createFieldMergeDiffPresentOnBothSides(FieldDiff caSourceFieldDiff, FieldDiff caTargetFieldDiff) { + static FieldMergeDiff createFieldMergeDiffPresentOnBothSides(String fullyQualifiedObjectPath, FieldDiff caSourceFieldDiff, FieldDiff caTargetFieldDiff) { log.debug('[{}] Processing field difference against common ancestor on both sides', caSourceFieldDiff.fieldName) - FieldMergeDiff fieldMergeDiff = createBaseFieldMergeDiffPresentOnOneSide(caSourceFieldDiff) + createBaseFieldMergeDiffPresentOnOneSide(fullyQualifiedObjectPath, caSourceFieldDiff) .withTarget(caTargetFieldDiff.right) .asMergeConflict() - // This is a safety check to handle when 2 diffs are used with modifications but no actual difference - fieldMergeDiff.hasDiff() ? fieldMergeDiff : null + .getValidOnly() // This is a safety check to handle when 2 diffs are used with modifications but no actual difference } - static FieldMergeDiff createFieldMergeDiffPresentOnOneSide(FieldDiff caSourceFieldDiff) { + static FieldMergeDiff createFieldMergeDiffPresentOnOneSide(String fullyQualifiedObjectPath, FieldDiff caSourceFieldDiff) { log.debug('[{}] Processing field difference against common ancestor on one side', caSourceFieldDiff.fieldName) - createBaseFieldMergeDiffPresentOnOneSide(caSourceFieldDiff) + createBaseFieldMergeDiffPresentOnOneSide(fullyQualifiedObjectPath, caSourceFieldDiff) // just use whats in the commonAncestor as no caTarget data denotes no difference between the CA and the target .withTarget(caSourceFieldDiff.left) + .getValidOnly() } - static FieldMergeDiff createBaseFieldMergeDiffPresentOnOneSide(FieldDiff caSourceFieldDiff) { + static FieldMergeDiff createBaseFieldMergeDiffPresentOnOneSide(String fullyQualifiedObjectPath, FieldDiff caSourceFieldDiff) { fieldMergeDiff(caSourceFieldDiff.targetClass) .forFieldName(caSourceFieldDiff.fieldName) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) .withSource(caSourceFieldDiff.right) .withCommonAncestor(caSourceFieldDiff.left) // both diffs have the same LHS } - static Collection> createCreationMergeDiffsPresentOnOneSide( - Collection> caSourceCreatedDiffs) { + static Collection> createCreationMergeDiffsPresentOnOneSide(String fullyQualifiedObjectPath, + Collection> caSourceCreatedDiffs) { caSourceCreatedDiffs.collect {created -> creationMergeDiff(created.targetClass) .whichCreated(created.created) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) } } @@ -345,20 +378,22 @@ class MergeDiff extends TriDirectionalDiff implements Com * Important to note that due to the way diff works the same object cannot exist in more than one of created, deleted, modified in an * ArrayDIff */ - static Collection createCreationMergeDiffsPresentOnBothSides(ArrayDiff caSourceDiff, + static Collection createCreationMergeDiffsPresentOnBothSides(String fullyQualifiedObjectPath, + ArrayDiff caSourceDiff, ArrayDiff caTargetDiff) { - List modificationCreationMergeDiffs = caSourceDiff.modified.collect {caSourceModificationDiff -> + Collection modificationCreationMergeDiffs = caSourceDiff.modified.collect {caSourceModificationDiff -> DeletionDiff caTargetDeletionDiff = caTargetDiff.deleted.find {it.deletedIdentifier == caSourceModificationDiff.leftIdentifier} if (caTargetDeletionDiff) { log.debug('[{}] ca/source modified exists in ca/target deleted.') return creationMergeDiff(caSourceModificationDiff.targetClass) .whichCreated(caSourceModificationDiff.right) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) .withCommonAncestor(caSourceModificationDiff.left) .asMergeConflict() } null - }.findAll() as Collection - List creationMergeDiffs = caSourceDiff.created.collect {diff -> + }.findAll() + Collection creationMergeDiffs = caSourceDiff.created.collect {diff -> if (diff.createdIdentifier in caTargetDiff.created*.createdIdentifier) { // Both sides added : potential conflict and therefore is a modified rather than create or no diff log.trace('[{}] ca/source created exists in ca/target created. Possible merge conflict will be rendered as a modified MergeDiff', diff.createdIdentifier) @@ -374,17 +409,22 @@ class MergeDiff extends TriDirectionalDiff implements Com } // Only added on source side : no conflict log.debug('[{}] ca/source created doesnt exist in ca/target', diff.createdIdentifier) - return creationMergeDiff(diff.targetClass).whichCreated(diff.value) - }.findAll() as Collection - modificationCreationMergeDiffs + creationMergeDiffs + return creationMergeDiff(diff.targetClass) + .whichCreated(diff.value) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) + }.findAll() + + creationMergeDiffs.addAll(modificationCreationMergeDiffs) + creationMergeDiffs } - static Collection> createDeletionMergeDiffsPresentOnOneSide( - Collection> caSourceDeletionDiffs) { + static Collection> createDeletionMergeDiffsPresentOnOneSide(String fullyQualifiedObjectPath, + Collection> caSourceDeletionDiffs) { caSourceDeletionDiffs.collect {deleted -> deletionMergeDiff(deleted.targetClass) .whichDeleted(deleted.deleted) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) } } @@ -395,7 +435,8 @@ class MergeDiff extends TriDirectionalDiff implements Com * ArrayDIff * */ - static Collection createDeletionMergeDiffsPresentOnBothSides(Collection> caSourceDeletedDiffs, + static Collection createDeletionMergeDiffsPresentOnBothSides(String fullyQualifiedObjectPath, + Collection> caSourceDeletedDiffs, ArrayDiff caTargetDiff) { caSourceDeletedDiffs.collect {diff -> if (diff.deletedIdentifier in caTargetDiff.created*.createdIdentifier) { @@ -413,23 +454,29 @@ class MergeDiff extends TriDirectionalDiff implements Com log.debug('[{}] ca/source deleted exists in ca/target modified', diff.deletedIdentifier) return deletionMergeDiff(diff.targetClass) .whichDeleted(diff.value) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) .withMergeModification(caTargetModifiedDiff) // TODO does this work with giving the information needed??? .withCommonAncestor(caTargetModifiedDiff.left) .asMergeConflict() } // Deleted in source but not touched in target : no conflict log.debug('[{}] ca/source deleted doesnt exist in ca/target', diff.deletedIdentifier) - return deletionMergeDiff(diff.targetClass).whichDeleted(diff.value).withNoMergeModification() + return deletionMergeDiff(diff.targetClass) + .whichDeleted(diff.value) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) + .withNoMergeModification() }.findAll() } - static Collection> createModifiedMergeDiffsPresentOnOneSide(Collection> caSourceModifiedDiffs) { + static Collection> createModifiedMergeDiffsPresentOnOneSide(String fullyQualifiedObjectPath, + Collection> caSourceModifiedDiffs) { // Modified diffs represent diffs which have modifications down the chain but no changes on the target side // Therefore we can use an empty object diff caSourceModifiedDiffs.collect {objDiff -> Class targetClass = objDiff.left.class as Class mergeDiff(targetClass) .forMergingDiffable(objDiff.right) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) .intoDiffable(objDiff.left) .havingCommonAncestor(objDiff.left) .withCommonAncestorDiffedAgainstSource(objDiff) @@ -437,15 +484,18 @@ class MergeDiff extends TriDirectionalDiff implements Com } } - static Collection> createModifiedMergeDiffsPresentOnBothSides(ArrayDiff caSourceDiff, ArrayDiff caTargetDiff) { + static Collection> createModifiedMergeDiffsPresentOnBothSides(String fullyQualifiedObjectPath, + ArrayDiff caSourceDiff, + ArrayDiff caTargetDiff) { - List> modifiedDiffs = caSourceDiff.modified.collect {caSourceModifiedDiff -> + Collection> modifiedDiffs = caSourceDiff.modified.collect {caSourceModifiedDiff -> ObjectDiff caTargetModifiedDiff = caTargetDiff.modified.find {it.leftIdentifier == caSourceModifiedDiff.leftIdentifier} if (caTargetModifiedDiff) { log.debug('[{}] modified on both sides', caSourceModifiedDiff.leftIdentifier) MergeDiff sourceTargetMergeDiff = mergeDiff(caSourceModifiedDiff.targetClass) .forMergingDiffable(caSourceModifiedDiff.right) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) .intoDiffable(caTargetModifiedDiff.right) .havingCommonAncestor(caSourceModifiedDiff.left) .withCommonAncestorDiffedAgainstSource(caSourceModifiedDiff) @@ -464,6 +514,7 @@ class MergeDiff extends TriDirectionalDiff implements Com log.debug('[{}] only modified on ca/source side', caSourceModifiedDiff.leftIdentifier) mergeDiff(caSourceModifiedDiff.targetClass) .forMergingDiffable(caSourceModifiedDiff.right) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) .intoDiffable(caSourceModifiedDiff.left) .havingCommonAncestor(caSourceModifiedDiff.left) .withCommonAncestorDiffedAgainstSource(caSourceModifiedDiff) @@ -471,7 +522,7 @@ class MergeDiff extends TriDirectionalDiff implements Com }.findAll() - List> createdModifiedDiffs = caSourceDiff.created.collect {caSourceCreationDiff -> + Collection> createdModifiedDiffs = caSourceDiff.created.collect {caSourceCreationDiff -> CreationDiff caTargetCreationDiff = caTargetDiff.created.find {it.createdIdentifier == caSourceCreationDiff.createdIdentifier} if (caTargetCreationDiff) { // Both sides added : potential conflict and therefore is a modified rather than create or no diff @@ -487,6 +538,7 @@ class MergeDiff extends TriDirectionalDiff implements Com return mergeDiff(sourceTargetDiff.targetClass as Class) .forMergingDiffable(caSourceCreationDiff.created) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) .intoDiffable(caTargetCreationDiff.created) .havingCommonAncestor(null) .withSourceDiffedAgainstTarget(sourceTargetDiff) @@ -494,12 +546,7 @@ class MergeDiff extends TriDirectionalDiff implements Com } null }.findAll() - - modifiedDiffs + createdModifiedDiffs - } - - @Override - int compareTo(MergeDiff that) { - this.sourceIdentifier <=> that.sourceIdentifier + modifiedDiffs.addAll(createdModifiedDiffs) + modifiedDiffs } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy index aad1add320..efaddfcfc9 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy @@ -8,6 +8,7 @@ import groovy.transform.CompileStatic @CompileStatic abstract class TriDirectionalDiff extends BiDirectionalDiff { + protected String fullyQualifiedObjectPath private Boolean mergeConflict private T commonAncestor @@ -23,12 +24,17 @@ abstract class TriDirectionalDiff extends BiDirectionalDiff { TriDirectionalDiff diff = (TriDirectionalDiff) o - if (left != diff.left) return false - if (right != diff.right) return false + if (source != diff.source) return false + if (target != diff.target) return false return true } + TriDirectionalDiff insideFullyQualifiedObjectPath(String fullyQualifiedObjectPath) { + this.fullyQualifiedObjectPath = fullyQualifiedObjectPath + this + } + @SuppressWarnings('GrDeprecatedAPIUsage') TriDirectionalDiff withSource(T source) { this.left = source 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 d0a2ee401b..ad7e33e407 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 @@ -439,274 +439,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { } String getExpectedMergeDiffJson() { - '''{ - "sourceId": "${json-unit.matches:id}", - "targetId": "${json-unit.matches:id}", - "label": "Functional Test Model", - "count": 11, - "diffs": [ - { - "branchName": { - "source": "source", - "target": "main", - "commonAncestor": "main", - "isMergeConflict": false - } - }, - { - "description": { - "source": "DescriptionLeft", - "target": "DescriptionRight", - "commonAncestor": null, - "isMergeConflict": true - } - }, - { - "dataClasses": { - "created": [ - { - "created": { - "id": "${json-unit.matches:id}", - "label": "addLeftOnly", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ] - }, - "isMergeConflict": false, - "isSourceModificationAndTargetDeletion": false - }, - { - "created": { - "id": "${json-unit.matches:id}", - "label": "modifyAndDelete", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ] - }, - "isMergeConflict": true, - "isSourceModificationAndTargetDeletion": true - } - ], - "deleted": [ - { - "deleted": { - "id": "${json-unit.matches:id}", - "label": "deleteAndModify", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": true - } - ] - }, - "isMergeConflict": true, - "isSourceDeletionAndTargetModification": true - }, - { - "deleted": { - "id": "${json-unit.matches:id}", - "label": "deleteLeftOnly", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": true - } - ] - }, - "isMergeConflict": false, - "isSourceDeletionAndTargetModification": false - } - ], - "modified": [ - { - "sourceId": "${json-unit.matches:id}", - "targetId": "${json-unit.matches:id}", - "label": "addAndAddReturningDifference", - "sourceBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ], - "targetBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ], - "count": 1, - "diffs": [ - { - "description": { - "source": "DescriptionLeft", - "target": "DescriptionRight", - "commonAncestor": null, - "isMergeConflict": true - } - } - ] - }, - { - "sourceId": "${json-unit.matches:id}", - "targetId": "${json-unit.matches:id}", - "label": "existingClass", - "sourceBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ], - "targetBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ], - "count": 2, - "diffs": [ - { - "dataClasses": { - "created": [ - { - "created": { - "id": "${json-unit.matches:id}", - "label": "addLeftToExistingClass", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - }, - { - "id": "${json-unit.matches:id}", - "label": "existingClass", - "domainType": "DataClass" - } - ] - }, - "isMergeConflict": false, - "isSourceModificationAndTargetDeletion": false - } - ], - "deleted": [ - { - "deleted": { - "id": "${json-unit.matches:id}", - "label": "deleteLeftOnlyFromExistingClass", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": true - }, - { - "id": "${json-unit.matches:id}", - "label": "existingClass", - "domainType": "DataClass" - } - ] - }, - "isMergeConflict": false, - "isSourceDeletionAndTargetModification": false - } - ] - } - } - ] - }, - { - "sourceId": "${json-unit.matches:id}", - "targetId": "${json-unit.matches:id}", - "label": "modifyAndModifyReturningDifference", - "sourceBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ], - "targetBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ], - "count": 1, - "diffs": [ - { - "description": { - "source": "DescriptionLeft", - "target": "DescriptionRight", - "commonAncestor": null, - "isMergeConflict": true - } - } - ] - }, - { - "sourceId": "${json-unit.matches:id}", - "targetId": "${json-unit.matches:id}", - "label": "modifyLeftOnly", - "sourceBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": true - } - ], - "targetBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": true - } - ], - "count": 1, - "diffs": [ - { - "description": { - "source": "Description", - "target": null, - "commonAncestor": null, - "isMergeConflict": false - } - } - ] - } - ] - } - } - ] -}''' + '{}' } void 'test getting DataModel types'() { From 9db0425e36dabdef8d2419b1e3d08e3b87f634ce Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Thu, 24 Jun 2021 18:26:20 +0100 Subject: [PATCH 28/82] Add missing prefixes and add test to ensure all prefixes are unique Also add endpoint to supply all known prefixes --- .../maurodatamapper/core/UrlMappings.groovy | 1 + .../core/path/PathController.groovy | 7 ++++-- .../core/path/PathInterceptor.groovy | 5 +--- .../core/path/PathService.groovy | 22 ++++++++++++++---- .../federation/SubscribedCatalogue.groovy | 5 ++++ .../item/ReferenceDataValue.groovy | 5 ++++ .../security/authentication/ApiKey.groovy | 5 ++++ .../core/path/PathFunctionalSpec.groovy | 23 ++++++++++++++++++- 8 files changed, 62 insertions(+), 11 deletions(-) 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 e3f4b4f4cf..bdb127627d 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 @@ -67,6 +67,7 @@ class UrlMappings { get '/properties'(controller: 'apiProperty', action: 'index') { openAccess = true } + get '/path/prefixMappings'(controller: 'path', action: 'listAllPrefixMappings') group '/importer', { get "/parameters/$ns?/$name?/$version?"(controller: 'importer', action: 'parameters') diff --git a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathController.groovy b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathController.groovy index 8d062ec5d5..c3e5360708 100644 --- a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathController.groovy +++ b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathController.groovy @@ -42,8 +42,11 @@ class PathController extends RestfulController implements MdmCont if (!catalogueItem) return notFound(CatalogueItem, params.path) respond(catalogueItem, [model: [userSecurityPolicyManager: currentUserSecurityPolicyManager, - catalogueItem: catalogueItem], - view: 'show']) + catalogueItem : catalogueItem], + view : 'show']) } + def listAllPrefixMappings() { + respond pathService.listAllPrefixMappings() + } } diff --git a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy index c51b0cae28..84efcf1c04 100644 --- a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy +++ b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy @@ -22,9 +22,6 @@ import uk.ac.ox.softeng.maurodatamapper.core.traits.controller.MdmInterceptor class PathInterceptor implements MdmInterceptor { boolean before() { - // Allow anyone to retrieve by path as returned item will be constrained by what they can read - actionName == 'show' + true } - - } \ No newline at end of file diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy index 396ba281ba..499369c4ed 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy @@ -23,11 +23,14 @@ import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem import uk.ac.ox.softeng.maurodatamapper.core.model.ModelService import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager +import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.PathNode +import grails.core.GrailsApplication import grails.gorm.transactions.Transactional import groovy.util.logging.Slf4j +import org.grails.core.artefact.DomainClassArtefactHandler import org.grails.orm.hibernate.proxy.HibernateProxyHandler import org.springframework.beans.factory.annotation.Autowired @@ -38,18 +41,29 @@ class PathService { @Autowired(required = false) List catalogueItemServices - private static HibernateProxyHandler proxyHandler = new HibernateProxyHandler(); + GrailsApplication grailsApplication + + private static HibernateProxyHandler proxyHandler = new HibernateProxyHandler() + + Map listAllPrefixMappings() { + grailsApplication.getArtefacts(DomainClassArtefactHandler.TYPE) + .findAll {CreatorAware.isAssignableFrom(it.clazz) && !it.isAbstract()} + .collectEntries {grailsClass -> + CreatorAware domain = grailsClass.newInstance() as CreatorAware + [domain.pathPrefix, domain.domainType] + }.sort() + } CatalogueItem findCatalogueItemByPath(UserSecurityPolicyManager userSecurityPolicyManager, Map params) { Path path = new Path(params.path) - CatalogueItemService service = catalogueItemServices.find { it.handles(params.catalogueItemDomainType) } + CatalogueItemService service = catalogueItemServices.find {it.handles(params.catalogueItemDomainType)} CatalogueItem catalogueItem /* Iterate over nodes in the path */ boolean first = true - path.pathNodes.each { PathNode node -> + path.pathNodes.each {PathNode node -> /* On first iteration, if params.catalogueItemId is provided then use this to get the top CatalogueItem by ID. Else if the service handles the typePrefix then use this service to find the top CatalogueItem by label. @@ -88,7 +102,7 @@ class PathService { } else { if (catalogueItem) { //Try to find the child of this catalogue item by prefix and label - service = catalogueItemServices.find { it.handlesPathPrefix(node.typePrefix) } + service = catalogueItemServices.find {it.handlesPathPrefix(node.typePrefix)} //Use the service to find a child CatalogueItem whose parent is catalogueItem and which has the specified label //Missing method exception means the path tried to retrieve a type of parent that is not expected 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 53085f07ca..a1a25c3824 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 @@ -82,6 +82,11 @@ class SubscribedCatalogue implements SecurableResource, EditHistoryAware, Inform SubscribedCatalogue.simpleName } + @Override + String getPathPrefix() { + 'subc' + } + @Override String getEditLabel() { "SubscribedCatalogue:${url}" diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValue.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValue.groovy index 8eaee7abd7..1bdccec4c2 100644 --- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValue.groovy +++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValue.groovy @@ -60,6 +60,11 @@ class ReferenceDataValue implements CreatorAware, Diffable { ReferenceDataValue.simpleName } + @Override + String getPathPrefix() { + 'rdv' + } + @Override String getDiffIdentifier() { this.id diff --git a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/authentication/ApiKey.groovy b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/authentication/ApiKey.groovy index d796100a82..50edf5d22e 100644 --- a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/authentication/ApiKey.groovy +++ b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/authentication/ApiKey.groovy @@ -53,6 +53,11 @@ class ApiKey implements CreatorAware { ApiKey.simpleName } + @Override + String getPathPrefix() { + 'ak' + } + ApiKey() { refreshable = false name = DEFAULT_NAME diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy index 934ae19d15..20f58de918 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy @@ -1164,5 +1164,26 @@ class PathFunctionalSpec extends FunctionalSpec { then: "The response is OK" verifyJsonResponse OK, getExpectedReferenceTypeJson() - } + } + + void 'Confirm all path prefixes are unique'() { + when: + GET('path/prefixMappings', STRING_ARG) + log.debug('{}', jsonResponseBody()) + + and: + GET('path/prefixMappings') + + then: + verifyResponse(OK, response) + + when: + Map> grouped = (responseBody() as Map).groupBy {it.key} + + then: + grouped.each { + log.debug('Checking {}', it.key) + assert it.value.size() == 1 + } + } } From 476f47212bccde86c948eca2d4f14cd8017a4b4c Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Fri, 25 Jun 2021 10:02:23 +0100 Subject: [PATCH 29/82] Change the mergeDiff parameter to isLegacy boolean true/false --- .../core/controller/ModelController.groovy | 5 +---- .../datamodel/DataModelFunctionalSpec.groovy | 10 +++++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy index f156551b97..8ebf586503 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy @@ -270,11 +270,8 @@ abstract class ModelController extends CatalogueItemController< T target = queryForResource params.otherModelId if (!target) return notFound(params.otherModelId) - String view = 'legacyMergeDiff' // default to legacy until UI is updated - if (params.style == 'legacy') view = 'legacyMergeDiff' - if (params.style == 'new') view = 'mergeDiff' - + String view = params.boolean('isLegacy', true) ? 'legacyMergeDiff' : 'mergeDiff' respond modelService.getMergeDiffForModels(source, target), view: view } 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 ad7e33e407..e93e897a2b 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 @@ -1425,9 +1425,9 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { String rightId = responseBody().id when: - GET("$leftId/mergeDiff/$mainId?style=new", STRING_ARG) + GET("$leftId/mergeDiff/$mainId?isLegacy=false", STRING_ARG) log.debug('{}', jsonResponseBody()) - GET("$leftId/mergeDiff/$mainId?style=new") + GET("$leftId/mergeDiff/$mainId?isLegacy=false") then: verifyResponse OK, response @@ -1435,9 +1435,9 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { responseBody().sourceId == leftId when: - GET("$rightId/mergeDiff/$mainId?style=new", STRING_ARG) + GET("$rightId/mergeDiff/$mainId?isLegacy=false", STRING_ARG) log.debug('{}', jsonResponseBody()) - GET("$rightId/mergeDiff/$mainId?style=new") + GET("$rightId/mergeDiff/$mainId?isLegacy=false") then: verifyResponse OK, response @@ -1456,7 +1456,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { Map mergeData = buildComplexDataModelsForMerging() when: - GET("$mergeData.source/mergeDiff/$mergeData.target?style=new", STRING_ARG) + GET("$mergeData.source/mergeDiff/$mergeData.target?isLegacy=false", STRING_ARG) then: verifyJsonResponse OK, expectedMergeDiffJson From 159b36119055b1829d3d97650df9d983b871b5ad Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Fri, 25 Jun 2021 10:55:30 +0100 Subject: [PATCH 30/82] Flatten the mergeDiff json style and add the "type" field to the json --- .../creationMergeDiff/_creationMergeDiff.gson | 2 +- .../deletionMergeDiff/_deletionMergeDiff.gson | 2 +- .../views/fieldMergeDiff/_fieldMergeDiff.gson | 7 +- .../views/mergeDiff/_mergeDiff.gson | 9 +- .../diff/tridirectional/ArrayMergeDiff.groovy | 13 ++- .../core/diff/tridirectional/MergeDiff.groovy | 10 ++ .../datamodel/DataModelFunctionalSpec.groovy | 91 ++++++++++++++++++- 7 files changed, 118 insertions(+), 16 deletions(-) diff --git a/mdm-core/grails-app/views/creationMergeDiff/_creationMergeDiff.gson b/mdm-core/grails-app/views/creationMergeDiff/_creationMergeDiff.gson index 684dd3b189..3dbf599c6e 100644 --- a/mdm-core/grails-app/views/creationMergeDiff/_creationMergeDiff.gson +++ b/mdm-core/grails-app/views/creationMergeDiff/_creationMergeDiff.gson @@ -6,7 +6,7 @@ model { json { path creationMergeDiff.fullyQualifiedPath - // objectCreated tmpl.'/diffable/diffable'(diffable: creationMergeDiff.created, renderBreadcrumbs: false) isMergeConflict creationMergeDiff.isMergeConflict() isSourceModificationAndTargetDeletion creationMergeDiff.isSourceModificationAndTargetDeletion() + type 'creation' } \ No newline at end of file diff --git a/mdm-core/grails-app/views/deletionMergeDiff/_deletionMergeDiff.gson b/mdm-core/grails-app/views/deletionMergeDiff/_deletionMergeDiff.gson index c30386b148..c5a72090ea 100644 --- a/mdm-core/grails-app/views/deletionMergeDiff/_deletionMergeDiff.gson +++ b/mdm-core/grails-app/views/deletionMergeDiff/_deletionMergeDiff.gson @@ -6,7 +6,7 @@ model { json { path deletionMergeDiff.fullyQualifiedPath - // objectDeleted tmpl.'/diffable/diffable'(diffable: deletionMergeDiff.deleted, renderBreadcrumbs: false) isMergeConflict deletionMergeDiff.isMergeConflict() isSourceDeletionAndTargetModification deletionMergeDiff.isSourceDeletionAndTargetModification() + type 'deletion' } \ No newline at end of file diff --git a/mdm-core/grails-app/views/fieldMergeDiff/_fieldMergeDiff.gson b/mdm-core/grails-app/views/fieldMergeDiff/_fieldMergeDiff.gson index a9c5f9f19c..5ebe53cd06 100644 --- a/mdm-core/grails-app/views/fieldMergeDiff/_fieldMergeDiff.gson +++ b/mdm-core/grails-app/views/fieldMergeDiff/_fieldMergeDiff.gson @@ -7,8 +7,9 @@ model { json { fieldName fieldMergeDiff.fieldName path fieldMergeDiff.fullyQualifiedPath - source fieldMergeDiff.getSource() - target fieldMergeDiff.getTarget() - commonAncestor fieldMergeDiff.commonAncestor + sourceValue fieldMergeDiff.getSource() + targetValue fieldMergeDiff.getTarget() + commonAncestorValue fieldMergeDiff.commonAncestor isMergeConflict fieldMergeDiff.mergeConflict + type 'modification' } \ No newline at end of file diff --git a/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson b/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson index 354a6fc6d9..8bbbceac82 100644 --- a/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson +++ b/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson @@ -22,13 +22,6 @@ json { key(((Metadata) mergeDiff.getTarget()).getKey()) } - // if (mergeDiff.getTarget() instanceof ModelItem && mergeDiff.getSource() instanceof ModelItem) { - // ModelItem source = mergeDiff.getTarget() as ModelItem - // ModelItem target = mergeDiff.getTarget() as ModelItem - // sourceBreadcrumbs g.render(source.getBreadcrumbs()) - // targetBreadcrumbs g.render(target.getBreadcrumbs()) - // } - count mergeDiff.getNumberOfDiffs() - diffs g.render(mergeDiff.getDiffs()) + diffs g.render(mergeDiff.getFlattenedDiffs()) } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy index 36ee1ecbfb..eb3994de00 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy @@ -94,13 +94,22 @@ class ArrayMergeDiff extends FieldMergeDiff> { @Override Integer getNumberOfDiffs() { - created.size() + deleted.size() + ((modified.sum {it.getNumberOfDiffs()} ?: 0) as Integer) + created.size() + deleted.size() + ((modified.sum { it.getNumberOfDiffs() } ?: 0) as Integer) + } + + List getFlattenedDiffs() { + List flattenedDiffs = new ArrayList<>(numberOfDiffs) + flattenedDiffs.addAll(created.sort()) + flattenedDiffs.addAll(deleted.sort()) + flattenedDiffs.addAll(modified.sort().collectMany { it.getFlattenedDiffs() }) + flattenedDiffs } @Override String toString() { String diffIdentifier = source?.first()?.diffIdentifier ?: target?.first()?.diffIdentifier ?: commonAncestor?.first()?.diffIdentifier - StringBuilder stringBuilder = new StringBuilder("${diffIdentifier}.${fieldName} :: ${source.size()} <> ${target.size()} :: ${commonAncestor.size()}") + StringBuilder stringBuilder = new StringBuilder( + "${diffIdentifier}.${fieldName} :: ${source.size()} <> ${target.size()} :: ${commonAncestor.size()}") if (created) { stringBuilder.append('\n').append(created.join('\n')) diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy index 96ecb18ef0..98a62da371 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy @@ -101,6 +101,16 @@ class MergeDiff extends TriDirectionalDiff implements Com diffs.sort() } + List getFlattenedDiffs() { + diffs.sort().collectMany { diff -> + if (diff.diffType == FieldMergeDiff.simpleName) return [diff] + if (diff.diffType == ArrayMergeDiff.simpleName) { + return (diff as ArrayMergeDiff).getFlattenedDiffs() + } + [] + } as List + } + @Override String toString() { String str = "${sourceIdentifier} --> ${targetIdentifier}" 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 e93e897a2b..a68ddf10a4 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 @@ -439,7 +439,96 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { } String getExpectedMergeDiffJson() { - '{}' + '''{ + "sourceId": "${json-unit.matches:id}", + "targetId": "${json-unit.matches:id}", + "path": "dm:Functional Test Model", + "label": "Functional Test Model", + "count": 11, + "diffs": [ + { + "fieldName": "branchName", + "path": "dm:Functional Test Model:branchName", + "sourceValue": "source", + "targetValue": "main", + "commonAncestorValue": "main", + "isMergeConflict": false, + "type": "modification" + }, + { + "fieldName": "description", + "path": "dm:Functional Test Model:description", + "sourceValue": "DescriptionLeft", + "targetValue": "DescriptionRight", + "commonAncestorValue": null, + "isMergeConflict": true, + "type": "modification" + }, + { + "path": "dm:Functional Test Model|dc:addLeftOnly", + "isMergeConflict": false, + "isSourceModificationAndTargetDeletion": false, + "type": "creation" + }, + { + "path": "dm:Functional Test Model|dc:modifyAndDelete", + "isMergeConflict": true, + "isSourceModificationAndTargetDeletion": true, + "type": "creation" + }, + { + "path": "dm:Functional Test Model|dc:deleteAndModify", + "isMergeConflict": true, + "isSourceDeletionAndTargetModification": true, + "type": "deletion" + }, + { + "path": "dm:Functional Test Model|dc:deleteLeftOnly", + "isMergeConflict": false, + "isSourceDeletionAndTargetModification": false, + "type": "deletion" + }, + { + "fieldName": "description", + "path": "dm:Functional Test Model|dc:addAndAddReturningDifference:description", + "sourceValue": "DescriptionLeft", + "targetValue": "DescriptionRight", + "commonAncestorValue": null, + "isMergeConflict": true, + "type": "modification" + }, + { + "path": "dm:Functional Test Model|dc:existingClass|dc:addLeftToExistingClass", + "isMergeConflict": false, + "isSourceModificationAndTargetDeletion": false, + "type": "creation" + }, + { + "path": "dm:Functional Test Model|dc:existingClass|dc:deleteLeftOnlyFromExistingClass", + "isMergeConflict": false, + "isSourceDeletionAndTargetModification": false, + "type": "deletion" + }, + { + "fieldName": "description", + "path": "dm:Functional Test Model|dc:modifyAndModifyReturningDifference:description", + "sourceValue": "DescriptionLeft", + "targetValue": "DescriptionRight", + "commonAncestorValue": null, + "isMergeConflict": true, + "type": "modification" + }, + { + "fieldName": "description", + "path": "dm:Functional Test Model|dc:modifyLeftOnly:description", + "sourceValue": "Description", + "targetValue": null, + "commonAncestorValue": null, + "isMergeConflict": false, + "type": "modification" + } + ] +}''' } void 'test getting DataModel types'() { From 83c7015143a6f9d08c3cfa952e8e5d7c56be38ac Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Fri, 25 Jun 2021 11:27:22 +0100 Subject: [PATCH 31/82] Add all the missing headers --- .../core/diff/DiffBuilder.groovy | 17 +++++++++++++++++ .../diff/bidirectional/BiDirectionalDiff.groovy | 17 +++++++++++++++++ .../tridirectional/CreationMergeDiff.groovy | 17 +++++++++++++++++ .../tridirectional/DeletionMergeDiff.groovy | 17 +++++++++++++++++ .../core/diff/tridirectional/MergeDiff.groovy | 17 +++++++++++++++++ .../tridirectional/TriDirectionalDiff.groovy | 17 +++++++++++++++++ .../diff/unidirectional/CreationDiff.groovy | 17 +++++++++++++++++ .../diff/unidirectional/DeletionDiff.groovy | 17 +++++++++++++++++ 8 files changed, 136 insertions(+) diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy index 802e22fab6..aea074c9db 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy @@ -1,3 +1,20 @@ +/* + * 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.diff import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ArrayDiff diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/BiDirectionalDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/BiDirectionalDiff.groovy index c96c827460..dbcaff0f58 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/BiDirectionalDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/BiDirectionalDiff.groovy @@ -1,3 +1,20 @@ +/* + * 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.diff.bidirectional import uk.ac.ox.softeng.maurodatamapper.core.diff.Diff diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy index c0af812afb..9af9ccd8f0 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy @@ -1,3 +1,20 @@ +/* + * 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.diff.tridirectional import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy index a2142a8114..a1b2c743c4 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy @@ -1,3 +1,20 @@ +/* + * 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.diff.tridirectional import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy index 98a62da371..a99d416e1a 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy @@ -1,3 +1,20 @@ +/* + * 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.diff.tridirectional diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy index efaddfcfc9..a4e2260a0b 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy @@ -1,3 +1,20 @@ +/* + * 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.diff.tridirectional diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/CreationDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/CreationDiff.groovy index 65586df513..afa167c299 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/CreationDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/CreationDiff.groovy @@ -1,3 +1,20 @@ +/* + * 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.diff.unidirectional import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/DeletionDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/DeletionDiff.groovy index 7690ae5790..a948998695 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/DeletionDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/DeletionDiff.groovy @@ -1,3 +1,20 @@ +/* + * 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.diff.unidirectional import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable From b7464094da8747ff80f79a5f4411ae48da06f8dc Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Fri, 25 Jun 2021 12:26:42 +0100 Subject: [PATCH 32/82] Fix all diff and merge diff tests * Handle the removal of dateleemnts from dm diff * Expect the deleted objects to be breadcrumb'd from the CA in the legacy style --- .../datamodel/DataModelFunctionalSpec.groovy | 463 ++++++++---------- .../datamodel/DataModelSpec.groovy | 4 +- .../datamodel/item/DataElementSpec.groovy | 2 +- .../terminology/CodeSetFunctionalSpec.groovy | 416 ++++++++-------- .../TerminologyFunctionalSpec.groovy | 298 +++++------ .../TerminologyServiceIntegrationSpec.groovy | 15 +- .../datamodel/DataModelFunctionalSpec.groovy | 69 +-- 7 files changed, 573 insertions(+), 694 deletions(-) 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 a68ddf10a4..81a5ec9329 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 @@ -3840,277 +3840,214 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { then: verifyJsonResponse OK, '''{ - "leftId": "${json-unit.matches:id}", - "rightId": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "count": 20, - "diffs": [ - { - "label": { - "left": "Complex Test DataModel", - "right": "Simple Test DataModel" + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "Complex Test DataModel", + "count": 17, + "diffs": [ + { + "label": { + "left": "Complex Test DataModel", + "right": "Simple Test DataModel" + } + }, + { + "metadata": { + "deleted": [ + { + "value": { + "id": "${json-unit.matches:id}", + "namespace": "test.com/test", + "key": "mdk1", + "value": "mdv2" } - }, - { - "metadata": { - "deleted": [ - { - "value": { - "id": "${json-unit.matches:id}", - "namespace": "test.com", - "key": "mdk1", - "value": "mdv1" - } - }, - { - "value": { - "id": "${json-unit.matches:id}", - "namespace": "test.com/test", - "key": "mdk1", - "value": "mdv2" - } - } - ], - "created": [ - { - "value": { - "id": "${json-unit.matches:id}", - "namespace": "test.com/simple", - "key": "mdk1", - "value": "mdv1" - } - }, - { - "value": { - "id": "${json-unit.matches:id}", - "namespace": "test.com/simple", - "key": "mdk2", - "value": "mdv2" - } - } - ] + }, + { + "value": { + "id": "${json-unit.matches:id}", + "namespace": "test.com", + "key": "mdk1", + "value": "mdv1" } - }, - { - "annotations": { - "deleted": [ - { - "value": { - "id": "${json-unit.matches:id}", - "label": "test annotation 1" - } - }, - { - "value": { - "id": "${json-unit.matches:id}", - "label": "test annotation 2" - } - } - ] + } + ], + "created": [ + { + "value": { + "id": "${json-unit.matches:id}", + "namespace": "test.com/simple", + "key": "mdk1", + "value": "mdv1" } - }, - { - "author": { - "left": "admin person", - "right": null + }, + { + "value": { + "id": "${json-unit.matches:id}", + "namespace": "test.com/simple", + "key": "mdk2", + "value": "mdv2" } - }, - { - "organisation": { - "left": "brc", - "right": null + } + ] + } + }, + { + "annotations": { + "deleted": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "test annotation 1" } - }, - { - "dataTypes": { - "deleted": [ - { - "value": { - "id": "${json-unit.matches:id}", - "label": "string", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - } - ] - } - }, - { - "value": { - "id": "${json-unit.matches:id}", - "label": "integer", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - } - ] - } - }, - { - "value": { - "id": "${json-unit.matches:id}", - "label": "yesnounknown", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - } - ] - } - }, - { - "value": { - "id": "${json-unit.matches:id}", - "label": "child", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - } - ] - } - } - ] + }, + { + "value": { + "id": "${json-unit.matches:id}", + "label": "test annotation 2" } - }, - { - "dataClasses": { - "deleted": [ - { - "value": { - "id": "${json-unit.matches:id}", - "label": "content", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - } - ] - } - }, - { - "value": { - "id": "${json-unit.matches:id}", - "label": "emptyclass", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - } - ] - } - }, - { - "value": { - "id": "${json-unit.matches:id}", - "label": "parent", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - } - ] - } - } - ], - "created": [ - { - "value": { - "id": "${json-unit.matches:id}", - "label": "simple", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Simple Test DataModel", - "domainType": "DataModel", - "finalised": false - } - ] - } - } - ] + } + ] + } + }, + { + "author": { + "left": "admin person", + "right": null + } + }, + { + "organisation": { + "left": "brc", + "right": null + } + }, + { + "dataTypes": { + "deleted": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "string", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Complex Test DataModel", + "domainType": "DataModel", + "finalised": false + } + ] } - }, - { - "dataElements": { - "deleted": [ - { - "value": { - "id": "${json-unit.matches:id}", - "label": "element2", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - }, - { - "id": "${json-unit.matches:id}", - "label": "content", - "domainType": "DataClass" - } - ] - } - }, - { - "value": { - "id": "${json-unit.matches:id}", - "label": "child", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - }, - { - "id": "${json-unit.matches:id}", - "label": "parent", - "domainType": "DataClass" - } - ] - } - }, - { - "value": { - "id": "${json-unit.matches:id}", - "label": "ele1", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - }, - { - "id": "${json-unit.matches:id}", - "label": "content", - "domainType": "DataClass" - } - ] - } - } - ] + }, + { + "value": { + "id": "${json-unit.matches:id}", + "label": "integer", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Complex Test DataModel", + "domainType": "DataModel", + "finalised": false + } + ] } - } - ] + }, + { + "value": { + "id": "${json-unit.matches:id}", + "label": "yesnounknown", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Complex Test DataModel", + "domainType": "DataModel", + "finalised": false + } + ] + } + }, + { + "value": { + "id": "${json-unit.matches:id}", + "label": "child", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Complex Test DataModel", + "domainType": "DataModel", + "finalised": false + } + ] + } + } + ] + } + }, + { + "dataClasses": { + "deleted": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "content", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Complex Test DataModel", + "domainType": "DataModel", + "finalised": false + } + ] + } + }, + { + "value": { + "id": "${json-unit.matches:id}", + "label": "emptyclass", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Complex Test DataModel", + "domainType": "DataModel", + "finalised": false + } + ] + } + }, + { + "value": { + "id": "${json-unit.matches:id}", + "label": "parent", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Complex Test DataModel", + "domainType": "DataModel", + "finalised": false + } + ] + } + } + ], + "created": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "simple", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Simple Test DataModel", + "domainType": "DataModel", + "finalised": false + } + ] + } + } + ] + } + } + ] }''' cleanup: diff --git a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelSpec.groovy b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelSpec.groovy index 5ff9c09e7c..a8af43c25a 100644 --- a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelSpec.groovy +++ b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelSpec.groovy @@ -286,14 +286,14 @@ class DataModelSpec extends ModelSpec implements DomainUnitTest diff = dm1.diff(dm2) then: - diff.getNumberOfDiffs() == 4 + diff.getNumberOfDiffs() == 2 when: dm2.label = "test model 2" diff = dm1.diff(dm2) then: - diff.getNumberOfDiffs() == 5 + diff.getNumberOfDiffs() == 3 } diff --git a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementSpec.groovy b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementSpec.groovy index abff35a1fc..f7a83e7d79 100644 --- a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementSpec.groovy +++ b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementSpec.groovy @@ -17,7 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.datamodel.item -import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.facet.Rule import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel diff --git a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetFunctionalSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetFunctionalSpec.groovy index 74208c0a12..7bbd7d9071 100644 --- a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetFunctionalSpec.groovy +++ b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetFunctionalSpec.groovy @@ -167,216 +167,216 @@ class CodeSetFunctionalSpec extends ResourceFunctionalSpec { String getExpectedMergeDiffJson() { '''{ - "leftId": "${json-unit.matches:id}", - "rightId": "${json-unit.matches:id}", - "label": "Functional Test Model", - "count": 9, - "diffs": [ - { - "description": { - "left": "DescriptionRight", - "right": "DescriptionLeft", - "isMergeConflict": true, - "commonAncestorValue": null - } - }, - { - "branchName": { - "left": "main", - "right": "source", - "isMergeConflict": false + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "Functional Test Model", + "count": 9, + "diffs": [ + { + "branchName": { + "left": "main", + "right": "source", + "isMergeConflict": false + } + }, + { + "description": { + "left": "DescriptionRight", + "right": "DescriptionLeft", + "isMergeConflict": true, + "commonAncestorValue": null + } + }, + { + "terms": { + "deleted": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "DAM: deleteAndModify", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "Terminology", + "finalised": true + } + ] + }, + "isMergeConflict": true, + "commonAncestorValue": { + "id": "${json-unit.matches:id}", + "label": "DAM: deleteAndModify", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "Terminology", + "finalised": true + } + ] } - }, - { - "terms": { - "deleted": [ - { - "value": { - "id": "${json-unit.matches:id}", - "label": "DAM: deleteAndModify", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "Terminology", - "finalised": false - } - ] - }, - "isMergeConflict": true, - "commonAncestorValue": { - "id": "${json-unit.matches:id}", - "label": "DAM: deleteAndModify", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "Terminology", - "finalised": true - } - ] - } - }, - { - "value": { - "id": "${json-unit.matches:id}", - "label": "DLO: deleteLeftOnly", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "Terminology", - "finalised": false - } - ] - }, - "isMergeConflict": false - } - ], - "created": [ - { - "value": { - "id": "${json-unit.matches:id}", - "label": "MAD: modifyAndDelete", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "Terminology", - "finalised": false - } - ] - }, - "isMergeConflict": true, - "commonAncestorValue": { - "id": "${json-unit.matches:id}", - "label": "MAD: modifyAndDelete", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "Terminology", - "finalised": true - } - ] - } - }, - { - "value": { - "id": "${json-unit.matches:id}", - "label": "ALO: addLeftOnly", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "Terminology", - "finalised": false - } - ] - }, - "isMergeConflict": false - } - ], - "modified": [ - { - "leftId": "${json-unit.matches:id}", - "rightId": "${json-unit.matches:id}", - "label": "MAMRD: modifyAndModifyReturningDifference", - "leftBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "Terminology", - "finalised": false - } - ], - "rightBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "Terminology", - "finalised": false - } - ], - "count": 1, - "diffs": [ - { - "description": { - "left": "DescriptionRight", - "right": "DescriptionLeft", - "isMergeConflict": true, - "commonAncestorValue": null - } - } - ] - }, - { - "leftId": "${json-unit.matches:id}", - "rightId": "${json-unit.matches:id}", - "label": "MLO: modifyLeftOnly", - "leftBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "Terminology", - "finalised": false - } - ], - "rightBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "Terminology", - "finalised": false - } - ], - "count": 1, - "diffs": [ - { - "description": { - "left": null, - "right": "Description", - "isMergeConflict": false - } - } - ] - }, - { - "leftId": "${json-unit.matches:id}", - "rightId": "${json-unit.matches:id}", - "label": "AAARD: addAndAddReturningDifference", - "leftBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "Terminology", - "finalised": false - } - ], - "rightBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "Terminology", - "finalised": false - } - ], - "count": 1, - "diffs": [ - { - "description": { - "left": "DescriptionRight", - "right": "DescriptionLeft", - "isMergeConflict": true, - "commonAncestorValue": null - } - } - ] - } - ] + }, + { + "value": { + "id": "${json-unit.matches:id}", + "label": "DLO: deleteLeftOnly", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "Terminology", + "finalised": true + } + ] + }, + "isMergeConflict": false + } + ], + "created": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "ALO: addLeftOnly", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "Terminology", + "finalised": false + } + ] + }, + "isMergeConflict": false + }, + { + "value": { + "id": "${json-unit.matches:id}", + "label": "MAD: modifyAndDelete", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "Terminology", + "finalised": false + } + ] + }, + "isMergeConflict": true, + "commonAncestorValue": { + "id": "${json-unit.matches:id}", + "label": "MAD: modifyAndDelete", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "Terminology", + "finalised": true + } + ] } - } - ] + } + ], + "modified": [ + { + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "MLO: modifyLeftOnly", + "leftBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "Terminology", + "finalised": true + } + ], + "rightBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "Terminology", + "finalised": true + } + ], + "count": 1, + "diffs": [ + { + "description": { + "left": null, + "right": "Description", + "isMergeConflict": false + } + } + ] + }, + { + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "MAMRD: modifyAndModifyReturningDifference", + "leftBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "Terminology", + "finalised": false + } + ], + "rightBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "Terminology", + "finalised": false + } + ], + "count": 1, + "diffs": [ + { + "description": { + "left": "DescriptionRight", + "right": "DescriptionLeft", + "isMergeConflict": true, + "commonAncestorValue": null + } + } + ] + }, + { + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "AAARD: addAndAddReturningDifference", + "leftBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "Terminology", + "finalised": false + } + ], + "rightBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "Terminology", + "finalised": false + } + ], + "count": 1, + "diffs": [ + { + "description": { + "left": "DescriptionRight", + "right": "DescriptionLeft", + "isMergeConflict": true, + "commonAncestorValue": null + } + } + ] + } + ] + } + } + ] }''' } 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 c9ae3b944d..374112af38 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 @@ -167,6 +167,13 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { "label": "Functional Test Model", "count": 18, "diffs": [ + { + "branchName": { + "left": "main", + "right": "source", + "isMergeConflict": false + } + }, { "description": { "left": "DescriptionRight", @@ -176,10 +183,74 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { } }, { - "branchName": { - "left": "main", - "right": "source", - "isMergeConflict": false + "termRelationshipTypes": { + "deleted": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "oppositeActionTo", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "Terminology", + "finalised": true + } + ] + }, + "isMergeConflict": false + } + ], + "created": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "sameActionAs", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "Terminology", + "finalised": false + } + ] + }, + "isMergeConflict": false + } + ], + "modified": [ + { + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "inverseOf", + "leftBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "Terminology", + "finalised": true + } + ], + "rightBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "Terminology", + "finalised": true + } + ], + "count": 1, + "diffs": [ + { + "description": { + "left": null, + "right": "inverseOf(Modified)", + "isMergeConflict": false + } + } + ] + } + ] } }, { @@ -194,7 +265,7 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "Terminology", - "finalised": false + "finalised": true } ] }, @@ -221,7 +292,7 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "Terminology", - "finalised": false + "finalised": true } ] }, @@ -232,7 +303,7 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { { "value": { "id": "${json-unit.matches:id}", - "label": "ALO: addLeftOnly", + "label": "SALO: secondAddLeftOnly", "breadcrumbs": [ { "id": "${json-unit.matches:id}", @@ -247,7 +318,7 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { { "value": { "id": "${json-unit.matches:id}", - "label": "MAD: modifyAndDelete", + "label": "ALO: addLeftOnly", "breadcrumbs": [ { "id": "${json-unit.matches:id}", @@ -257,8 +328,10 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { } ] }, - "isMergeConflict": true, - "commonAncestorValue": { + "isMergeConflict": false + }, + { + "value": { "id": "${json-unit.matches:id}", "label": "MAD: modifyAndDelete", "breadcrumbs": [ @@ -266,38 +339,36 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "Terminology", - "finalised": true + "finalised": false } ] - } - }, - { - "value": { + }, + "isMergeConflict": true, + "commonAncestorValue": { "id": "${json-unit.matches:id}", - "label": "SALO: secondAddLeftOnly", + "label": "MAD: modifyAndDelete", "breadcrumbs": [ { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "Terminology", - "finalised": false + "finalised": true } ] - }, - "isMergeConflict": false + } } ], "modified": [ { "leftId": "${json-unit.matches:id}", "rightId": "${json-unit.matches:id}", - "label": "AAARD: addAndAddReturningDifference", + "label": "MLO: modifyLeftOnly", "leftBreadcrumbs": [ { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "Terminology", - "finalised": false + "finalised": true } ], "rightBreadcrumbs": [ @@ -305,22 +376,21 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "Terminology", - "finalised": false + "finalised": true } ], - "count": 2, + "count": 3, "diffs": [ { "description": { - "left": "DescriptionRight", - "right": "DescriptionLeft", - "isMergeConflict": true, - "commonAncestorValue": null + "left": null, + "right": "Description", + "isMergeConflict": false } }, { - "targetTermRelationships": { - "created": [ + "sourceTermRelationships": { + "deleted": [ { "value": { "id": "${json-unit.matches:id}", @@ -330,45 +400,22 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "Terminology", - "finalised": false + "finalised": true }, { "id": "${json-unit.matches:id}", - "label": "ALO: addLeftOnly", + "label": "MLO: modifyLeftOnly", "domainType": "Term" } ] - } + }, + "isMergeConflict": false } ] } - } - ] - }, - { - "leftId": "${json-unit.matches:id}", - "rightId": "${json-unit.matches:id}", - "label": "SMLO: secondModifyLeftOnly", - "leftBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "Terminology", - "finalised": false - } - ], - "rightBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "Terminology", - "finalised": false - } - ], - "count": 1, - "diffs": [ + }, { - "sourceTermRelationships": { + "targetTermRelationships": { "modified": [ { "leftId": "${json-unit.matches:id}", @@ -379,7 +426,7 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "Terminology", - "finalised": false + "finalised": true }, { "id": "${json-unit.matches:id}", @@ -392,7 +439,7 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "Terminology", - "finalised": false + "finalised": true }, { "id": "${json-unit.matches:id}", @@ -405,7 +452,8 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { { "description": { "left": null, - "right": "NewDescription" + "right": "NewDescription", + "isMergeConflict": false } } ] @@ -418,13 +466,13 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { { "leftId": "${json-unit.matches:id}", "rightId": "${json-unit.matches:id}", - "label": "MLO: modifyLeftOnly", + "label": "SMLO: secondModifyLeftOnly", "leftBreadcrumbs": [ { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "Terminology", - "finalised": false + "finalised": true } ], "rightBreadcrumbs": [ @@ -432,45 +480,13 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "Terminology", - "finalised": false + "finalised": true } ], - "count": 3, + "count": 1, "diffs": [ - { - "description": { - "left": null, - "right": "Description", - "isMergeConflict": false - } - }, { "sourceTermRelationships": { - "deleted": [ - { - "value": { - "id": "${json-unit.matches:id}", - "label": "similarSourceAction", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "Terminology", - "finalised": false - }, - { - "id": "${json-unit.matches:id}", - "label": "MLO: modifyLeftOnly", - "domainType": "Term" - } - ] - } - } - ] - } - }, - { - "targetTermRelationships": { "modified": [ { "leftId": "${json-unit.matches:id}", @@ -481,7 +497,7 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "Terminology", - "finalised": false + "finalised": true }, { "id": "${json-unit.matches:id}", @@ -494,7 +510,7 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "Terminology", - "finalised": false + "finalised": true }, { "id": "${json-unit.matches:id}", @@ -507,7 +523,8 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { { "description": { "left": null, - "right": "NewDescription" + "right": "NewDescription", + "isMergeConflict": false } } ] @@ -559,7 +576,7 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "Terminology", - "finalised": false + "finalised": true }, { "id": "${json-unit.matches:id}", @@ -574,51 +591,11 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { } } ] - } - ] - } - }, - { - "termRelationshipTypes": { - "deleted": [ - { - "value": { - "id": "${json-unit.matches:id}", - "label": "oppositeActionTo", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "Terminology", - "finalised": false - } - ] - }, - "isMergeConflict": false - } - ], - "created": [ - { - "value": { - "id": "${json-unit.matches:id}", - "label": "sameActionAs", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "Terminology", - "finalised": false - } - ] - }, - "isMergeConflict": false - } - ], - "modified": [ + }, { "leftId": "${json-unit.matches:id}", "rightId": "${json-unit.matches:id}", - "label": "inverseOf", + "label": "AAARD: addAndAddReturningDifference", "leftBreadcrumbs": [ { "id": "${json-unit.matches:id}", @@ -635,13 +612,40 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { "finalised": false } ], - "count": 1, + "count": 2, "diffs": [ { "description": { - "left": null, - "right": "inverseOf(Modified)", - "isMergeConflict": false + "left": "DescriptionRight", + "right": "DescriptionLeft", + "isMergeConflict": true, + "commonAncestorValue": null + } + }, + { + "targetTermRelationships": { + "deleted": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "similarSourceAction", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "Terminology", + "finalised": false + }, + { + "id": "${json-unit.matches:id}", + "label": "ALO: addLeftOnly", + "domainType": "Term" + } + ] + }, + "isMergeConflict": false + } + ] } } ] diff --git a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy index 693103e982..96c379aa8f 100644 --- a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy +++ b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy @@ -17,6 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.terminology +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints import uk.ac.ox.softeng.maurodatamapper.terminology.test.BaseTerminologyIntegrationSpec import uk.ac.ox.softeng.maurodatamapper.util.GormUtils @@ -357,15 +358,15 @@ class TerminologyServiceIntegrationSpec extends BaseTerminologyIntegrationSpec { right.branchName == 'right' when: - def mergeDiff = terminologyService.getMergeDiffForModels(terminologyService.get(left.id), terminologyService.get(right.id)) + MergeDiff mergeDiff = terminologyService.getMergeDiffForModels(terminologyService.get(left.id), terminologyService.get(right.id)) then: - mergeDiff.diffs.size == 1 - mergeDiff.diffs[0].fieldName == 'branchName' - mergeDiff.diffs[0].left == 'right' - mergeDiff.diffs[0].right == 'left' - mergeDiff.diffs[0].isMergeConflict - mergeDiff.diffs[0].commonAncestor == VersionAwareConstraints.DEFAULT_BRANCH_NAME + mergeDiff.size() == 1 + mergeDiff.first().fieldName == 'branchName' + mergeDiff.first().target == 'right' + mergeDiff.first().source == 'left' + mergeDiff.first().isMergeConflict() + mergeDiff.first().commonAncestor == VersionAwareConstraints.DEFAULT_BRANCH_NAME } } 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 2156f53571..ac703a4bd2 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 @@ -3009,7 +3009,7 @@ class DataModelFunctionalSpec extends ModelUserAccessPermissionChangingAndVersio "leftId": "${json-unit.matches:id}", "rightId": "${json-unit.matches:id}", "label": "Complex Test DataModel", - "count": 21, + "count": 18, "diffs": [ { "label": { @@ -3063,13 +3063,13 @@ class DataModelFunctionalSpec extends ModelUserAccessPermissionChangingAndVersio { "value": { "id": "${json-unit.matches:id}", - "label": "test annotation 2" + "label": "test annotation 1" } }, { "value": { "id": "${json-unit.matches:id}", - "label": "test annotation 1" + "label": "test annotation 2" } } ] @@ -3223,69 +3223,6 @@ class DataModelFunctionalSpec extends ModelUserAccessPermissionChangingAndVersio } ] } - }, - { - "dataElements": { - "deleted": [ - { - "value": { - "id": "${json-unit.matches:id}", - "label": "child", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - }, - { - "id": "${json-unit.matches:id}", - "label": "parent", - "domainType": "DataClass" - } - ] - } - }, - { - "value": { - "id": "${json-unit.matches:id}", - "label": "ele1", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - }, - { - "id": "${json-unit.matches:id}", - "label": "content", - "domainType": "DataClass" - } - ] - } - }, - { - "value": { - "id": "${json-unit.matches:id}", - "label": "element2", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - }, - { - "id": "${json-unit.matches:id}", - "label": "content", - "domainType": "DataClass" - } - ] - } - } - ] - } } ] }''' From 2a9756382eb5579eafefc6b01a79a71899588fcc Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Tue, 29 Jun 2021 15:17:24 +0100 Subject: [PATCH 33/82] Ensure the existing format mergeintos all work in the tests --- .../transport/merge/ObjectPatchData.groovy | 19 +- .../datamodel/DataModelFunctionalSpec.groovy | 228 +++++++++--------- .../TerminologyFunctionalSpec.groovy | 8 +- 3 files changed, 134 insertions(+), 121 deletions(-) diff --git a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/ObjectPatchData.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/ObjectPatchData.groovy index 37836647c0..6d34da9cb9 100644 --- a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/ObjectPatchData.groovy +++ b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/ObjectPatchData.groovy @@ -17,7 +17,6 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge - import grails.validation.Validateable /** @@ -30,6 +29,9 @@ class ObjectPatchData implements Validateable { String label private List patches + @Deprecated + List diffs + static constraints = { sourceId nullable: false targetId nullable: false @@ -39,14 +41,15 @@ class ObjectPatchData implements Validateable { ObjectPatchData() { patches = [] + diffs = [] } boolean hasPatches() { - patches.any {it.hasPatches()} + getPatches().any {it.hasPatches()} } List getPatches() { - patches.findAll {it.hasPatches()} + patches.findAll {it.hasPatches()} + diffs.findAll {it.hasPatches()} } @Deprecated @@ -58,4 +61,14 @@ class ObjectPatchData implements Validateable { void setRightId(UUID rightId) { this.sourceId = rightId } + + @Deprecated + UUID getLeftId() { + this.targetId + } + + @Deprecated + UUID getRightId() { + this.sourceId + } } 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 81a5ec9329..0d264542c6 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 @@ -2172,123 +2172,123 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { then: verifyJsonResponse OK, ''' { - "leftId": "${json-unit.matches:id}", - "rightId": "${json-unit.matches:id}", - "label": "Functional Test Model", - "count": 7, - "diffs": [ - { - "description": { - "left": "DescriptionRight", - "right": "DescriptionLeft", - "isMergeConflict": true, - "commonAncestorValue": null - } - }, - { - "metadata": { - "deleted": [ - { - "value": { - "id": "${json-unit.matches:id}", - "namespace": "functional.test.namespace", - "key": "deleteMetadataSource", - "value": "original" - }, - "isMergeConflict": false - } - ], - "created": [ - { - "value": { - "id": "${json-unit.matches:id}", - "namespace": "functional.test.namespace", - "key": "addMetadataSource", - "value": "original" - }, - "isMergeConflict": false - } - ], - "modified": [ + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "Functional Test Model", + "count": 7, + "diffs": [ + { + "branchName": { + "left": "main", + "right": "source", + "isMergeConflict": false + } + }, + { + "description": { + "left": "DescriptionRight", + "right": "DescriptionLeft", + "isMergeConflict": true, + "commonAncestorValue": null + } + }, + { + "dataClasses": { + "modified": [ + { + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "modifyLeftOnly", + "leftBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": true + } + ], + "rightBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": true + } + ], + "count": 2, + "diffs": [ + { + "description": { + "left": null, + "right": "Description", + "isMergeConflict": false + } + }, + { + "metadata": { + "created": [ { - "leftId": "${json-unit.matches:id}", - "rightId": "${json-unit.matches:id}", + "value": { + "id": "${json-unit.matches:id}", "namespace": "functional.test.namespace", - "key": "modifyMetadataSource", - "count": 1, - "diffs": [ - { - "value": { - "left": "original", - "right": "Modified Description", - "isMergeConflict": false - } - } - ] - } - ] - } - }, - { - "branchName": { - "left": "main", - "right": "source", - "isMergeConflict": false - } - }, - { - "dataClasses": { - "modified": [ - { - "leftId": "${json-unit.matches:id}", - "rightId": "${json-unit.matches:id}", - "label": "modifyLeftOnly", - "leftBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ], - "rightBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ], - "count": 2, - "diffs": [ - { - "description": { - "left": null, - "right": "Description", - "isMergeConflict": false - } - }, - { - "metadata": { - "created": [ - { - "value": { - "id": "${json-unit.matches:id}", - "namespace": "functional.test.namespace", - "key": "addMetadataModifyLeftOnly", - "value": "original" - }, - "isMergeConflict": false - } - ] - } - } - ] + "key": "addMetadataModifyLeftOnly", + "value": "original" + }, + "isMergeConflict": false } - ] - } - } - ] + ] + } + } + ] + } + ] + } + }, + { + "metadata": { + "deleted": [ + { + "value": { + "id": "${json-unit.matches:id}", + "namespace": "functional.test.namespace", + "key": "deleteMetadataSource", + "value": "original" + }, + "isMergeConflict": false + } + ], + "created": [ + { + "value": { + "id": "${json-unit.matches:id}", + "namespace": "functional.test.namespace", + "key": "addMetadataSource", + "value": "original" + }, + "isMergeConflict": false + } + ], + "modified": [ + { + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "namespace": "functional.test.namespace", + "key": "modifyMetadataSource", + "count": 1, + "diffs": [ + { + "value": { + "left": "original", + "right": "Modified Description", + "isMergeConflict": false + } + } + ] + } + ] + } + } + ] }''' when: 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 374112af38..3aa584f29e 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 @@ -1315,7 +1315,7 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { cleanUpData(id) } - void 'VB08 : test finding merge difference of two Model (as editor)'() { + void 'MD01 : test finding merge difference of two Model (as editor)'() { given: String id = createNewItem(validJson) PUT("$id/finalise", [versionChangeType: "Major"]) @@ -1361,7 +1361,7 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { cleanUpData(id) } - void 'VB09a : test merging diff with no patch data'() { + void 'MP01 : test merging diff with no patch data'() { given: String id = createNewItem(validJson) @@ -1388,7 +1388,7 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { cleanUpData(id) } - void 'VB09b : test merging diff with URI id not matching body id'() { + void 'MP02: test merging diff with URI id not matching body id'() { given: String id = createNewItem(validJson) @@ -1437,7 +1437,7 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { cleanUpData(id) } - void 'VB09c : test merging diff into draft model'() { + void 'MP04 : test merging diff into draft model'() { given: String id = createNewItem(validJson) From 3b471b7601410943e91966488182194c6ab8ad8b Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Wed, 30 Jun 2021 15:11:41 +0100 Subject: [PATCH 34/82] Get legacy merge into working using the build method --- .../datamodel/DataModelFunctionalSpec.groovy | 551 +++++++----------- 1 file changed, 217 insertions(+), 334 deletions(-) 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 0d264542c6..877fc5f468 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 @@ -1556,168 +1556,6 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { cleanUpData(mergeData.id) } - Map buildComplexDataModelsForMerging() { - - String id = createNewItem(validJson) - - POST("$id/dataClasses", [label: 'deleteLeftOnly']) - verifyResponse CREATED, response - POST("$id/dataClasses", [label: 'deleteRightOnly']) - verifyResponse CREATED, response - POST("$id/dataClasses", [label: 'modifyLeftOnly']) - verifyResponse CREATED, response - POST("$id/dataClasses", [label: 'modifyRightOnly']) - verifyResponse CREATED, response - POST("$id/dataClasses", [label: 'deleteAndDelete']) - verifyResponse CREATED, response - POST("$id/dataClasses", [label: 'deleteAndModify']) - verifyResponse CREATED, response - POST("$id/dataClasses", [label: 'modifyAndDelete']) - verifyResponse CREATED, response - POST("$id/dataClasses", [label: 'modifyAndModifyReturningNoDifference']) - verifyResponse CREATED, response - POST("$id/dataClasses", [label: 'modifyAndModifyReturningDifference']) - verifyResponse CREATED, response - POST("$id/dataClasses", [label: 'existingClass']) - verifyResponse CREATED, response - String existingClass = responseBody().id - POST("$id/dataClasses/$existingClass/dataClasses", [label: 'deleteLeftOnlyFromExistingClass']) - verifyResponse CREATED, response - POST("$id/dataClasses/$existingClass/dataClasses", [label: 'deleteRightOnlyFromExistingClass']) - verifyResponse CREATED, response - - PUT("$id/finalise", [versionChangeType: 'Major']) - verifyResponse OK, response - PUT("$id/newBranchModelVersion", [branchName: VersionAwareConstraints.DEFAULT_BRANCH_NAME]) - verifyResponse CREATED, response - String target = responseBody().id - PUT("$id/newBranchModelVersion", [branchName: 'source']) - verifyResponse CREATED, response - String source = responseBody().id - - GET("$source/path/dm%3A%7Cdc%3AexistingClass") - verifyResponse OK, response - existingClass = responseBody().id - GET("dataClasses/$existingClass/path/dc%3A%7Cdc%3AdeleteLeftOnlyFromExistingClass", MAP_ARG, true) - verifyResponse OK, response - String deleteLeftOnlyFromExistingClass = responseBody().id - GET("$source/path/dm%3A%7Cdc%3AdeleteLeftOnly") - verifyResponse OK, response - String deleteLeftOnly = responseBody().id - GET("$source/path/dm%3A%7Cdc%3AdeleteAndDelete") - verifyResponse OK, response - String deleteAndDelete = responseBody().id - GET("$source/path/dm%3A%7Cdc%3AdeleteAndModify") - verifyResponse OK, response - String deleteAndModify = responseBody().id - - GET("$source/path/dm%3A%7Cdc%3AmodifyLeftOnly") - verifyResponse OK, response - String modifyLeftOnly = responseBody().id - GET("$source/path/dm%3A%7Cdc%3AmodifyAndDelete") - verifyResponse OK, response - String modifyAndDelete = responseBody().id - GET("$source/path/dm%3A%7Cdc%3AmodifyAndModifyReturningNoDifference") - verifyResponse OK, response - String modifyAndModifyReturningNoDifference = responseBody().id - GET("$source/path/dm%3A%7Cdc%3AmodifyAndModifyReturningDifference") - verifyResponse OK, response - String modifyAndModifyReturningDifference = responseBody().id - - DELETE("$source/dataClasses/$deleteAndDelete") - verifyResponse NO_CONTENT, response - DELETE("$source/dataClasses/$existingClass/dataClasses/$deleteLeftOnlyFromExistingClass") - verifyResponse NO_CONTENT, response - DELETE("$source/dataClasses/$deleteLeftOnly") - verifyResponse NO_CONTENT, response - DELETE("$source/dataClasses/$deleteAndModify") - verifyResponse NO_CONTENT, response - - PUT("$source/dataClasses/$modifyLeftOnly", [description: 'Description']) - verifyResponse OK, response - PUT("$source/dataClasses/$modifyAndDelete", [description: 'Description']) - verifyResponse OK, response - PUT("$source/dataClasses/$modifyAndModifyReturningNoDifference", [description: 'Description']) - verifyResponse OK, response - PUT("$source/dataClasses/$modifyAndModifyReturningDifference", [description: 'DescriptionLeft']) - verifyResponse OK, response - - POST("$source/dataClasses/$existingClass/dataClasses", [label: 'addLeftToExistingClass']) - verifyResponse CREATED, response - POST("$source/dataClasses", [label: 'addLeftOnly']) - verifyResponse CREATED, response - POST("$source/dataClasses", [label: 'addAndAddReturningNoDifference']) - verifyResponse CREATED, response - POST("$source/dataClasses", [label: 'addAndAddReturningDifference', description: 'DescriptionLeft']) - verifyResponse CREATED, response - - PUT("$source", [description: 'DescriptionLeft']) - verifyResponse OK, response - - GET("$target/path/dm%3A%7Cdc%3AexistingClass") - verifyResponse OK, response - existingClass = responseBody().id - GET("dataClasses/$existingClass/path/dc%3A%7Cdc%3AdeleteRightOnlyFromExistingClass", MAP_ARG, true) - verifyResponse OK, response - String deleteRightOnlyFromExistingClass = responseBody().id - GET("$target/path/dm%3A%7Cdc%3AdeleteRightOnly") - verifyResponse OK, response - String deleteRightOnly = responseBody().id - GET("$target/path/dm%3A%7Cdc%3AdeleteAndDelete") - verifyResponse OK, response - deleteAndDelete = responseBody().id - GET("$target/path/dm%3A%7Cdc%3AmodifyAndDelete") - verifyResponse OK, response - modifyAndDelete = responseBody().id - - GET("$target/path/dm%3A%7Cdc%3AmodifyRightOnly") - verifyResponse OK, response - String modifyRightOnly = responseBody().id - GET("$target/path/dm%3A%7Cdc%3AdeleteAndModify") - verifyResponse OK, response - deleteAndModify = responseBody().id - GET("$target/path/dm%3A%7Cdc%3AmodifyAndModifyReturningNoDifference") - verifyResponse OK, response - modifyAndModifyReturningNoDifference = responseBody().id - GET("$target/path/dm%3A%7Cdc%3AmodifyAndModifyReturningDifference") - verifyResponse OK, response - modifyAndModifyReturningDifference = responseBody().id - - DELETE("$target/dataClasses/$existingClass/dataClasses/$deleteRightOnlyFromExistingClass") - verifyResponse NO_CONTENT, response - DELETE("$target/dataClasses/$deleteRightOnly") - verifyResponse NO_CONTENT, response - DELETE("$target/dataClasses/$deleteAndDelete") - verifyResponse NO_CONTENT, response - DELETE("$target/dataClasses/$modifyAndDelete") - verifyResponse NO_CONTENT, response - - PUT("$target/dataClasses/$modifyRightOnly", [description: 'Description']) - verifyResponse OK, response - PUT("$target/dataClasses/$deleteAndModify", [description: 'Description']) - verifyResponse OK, response - PUT("$target/dataClasses/$modifyAndModifyReturningNoDifference", [description: 'Description']) - verifyResponse OK, response - PUT("$target/dataClasses/$modifyAndModifyReturningDifference", [description: 'DescriptionRight']) - verifyResponse OK, response - - POST("$target/dataClasses/$existingClass/dataClasses", [label: 'addRightToExistingClass']) - verifyResponse CREATED, response - POST("$target/dataClasses", [label: 'addRightOnly']) - verifyResponse CREATED, response - POST("$target/dataClasses", [label: 'addAndAddReturningNoDifference']) - verifyResponse CREATED, response - POST("$target/dataClasses", [label: 'addAndAddReturningDifference', description: 'DescriptionRight']) - verifyResponse CREATED, response - - PUT("$target", [description: 'DescriptionRight']) - verifyResponse OK, response - - [id : id, - source: source, - target: target] - } - void 'MP01 : test merging diff with no patch data'() { given: String id = createNewItem(validJson) @@ -1796,159 +1634,10 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { void 'MP03 : test merging diff into draft model'() { given: - String id = createNewItem(validJson) - - POST("$id/dataClasses", [label: 'deleteLeftOnly']) - verifyResponse CREATED, response - POST("$id/dataClasses", [label: 'modifyLeftOnly']) - verifyResponse CREATED, response - POST("$id/dataClasses", [label: 'deleteAndModify']) - verifyResponse CREATED, response - POST("$id/dataClasses", [label: 'modifyAndDelete']) - verifyResponse CREATED, response - POST("$id/dataClasses", [label: 'modifyAndModifyReturningDifference']) - verifyResponse CREATED, response - POST("$id/dataClasses", [label: 'existingClass']) - verifyResponse CREATED, response - String existingClass = responseBody().id - POST("$id/dataClasses/$existingClass/dataClasses", [label: 'deleteLeftOnlyFromExistingClass']) - verifyResponse CREATED, response - - // POST("$id/dataTypes", [label: 'deleteDataTypeSource', domainType: 'PrimitiveType']) - // verifyResponse CREATED, response - // POST("$id/dataTypes", [label: 'modifyDataTypeSource', domainType: 'PrimitiveType']) - // verifyResponse CREATED, response - - PUT("$id/finalise", [versionChangeType: 'Major']) - verifyResponse OK, response - PUT("$id/newBranchModelVersion", [branchName: VersionAwareConstraints.DEFAULT_BRANCH_NAME]) - verifyResponse CREATED, response - String target = responseBody().id - PUT("$id/newBranchModelVersion", [branchName: 'source']) - verifyResponse CREATED, response - String source = responseBody().id - - when: - //to delete - GET("$source/path/dm%3A%7Cdc%3AexistingClass") - verifyResponse OK, response - existingClass = responseBody().id - GET("dataClasses/$existingClass/path/dc%3A%7Cdc%3AdeleteLeftOnlyFromExistingClass", MAP_ARG, true) - verifyResponse OK, response - String deleteLeftOnlyFromExistingClass = responseBody().id - GET("$source/path/dm%3A%7Cdc%3AdeleteLeftOnly") - verifyResponse OK, response - String deleteLeftOnly = responseBody().id - GET("$source/path/dm%3A%7Cdc%3AdeleteAndModify") - verifyResponse OK, response - String deleteAndModify = responseBody().id - //to modify - GET("$source/path/dm%3A%7Cdc%3AmodifyLeftOnly") - verifyResponse OK, response - String modifyLeftOnly = responseBody().id - GET("$source/path/dm%3A%7Cdc%3AmodifyAndDelete") - verifyResponse OK, response - String sourceModifyAndDelete = responseBody().id - GET("$source/path/dm%3A%7Cdc%3AmodifyAndModifyReturningDifference") - verifyResponse OK, response - String modifyAndModifyReturningDifference = responseBody().id - - // GET("$source/path/dm%3A%7Cdt%3AdeleteDataTypeSource") - // verifyResponse OK, response - // String deleteDataTypeSource = responseBody().id - // GET("$source/path/dm%3A%7Cdt%3AmodifyDataTypeSource") - // verifyResponse OK, response - // String modifyDataTypeSource = responseBody().id - - then: - //dataModel description - PUT("$source", [description: 'DescriptionLeft']) - verifyResponse OK, response - - //dataClasses - DELETE("$source/dataClasses/$deleteLeftOnly") - verifyResponse NO_CONTENT, response - DELETE("$source/dataClasses/$deleteAndModify") - verifyResponse NO_CONTENT, response - DELETE("$source/dataClasses/$existingClass/dataClasses/$deleteLeftOnlyFromExistingClass") - verifyResponse NO_CONTENT, response - - PUT("$source/dataClasses/$modifyLeftOnly", [description: 'Description']) - verifyResponse OK, response - PUT("$source/dataClasses/$sourceModifyAndDelete", [description: 'Description']) - verifyResponse OK, response - PUT("$source/dataClasses/$modifyAndModifyReturningDifference", [description: 'DescriptionLeft']) - verifyResponse OK, response - - POST("$source/dataClasses/$existingClass/dataClasses", [label: 'addLeftToExistingClass']) - verifyResponse CREATED, response - String addLeftToExistingClass = responseBody().id - POST("$source/dataClasses", [label: 'addLeftOnly']) - verifyResponse CREATED, response - String addLeftOnly = responseBody().id - POST("$source/dataClasses", [label: 'addAndAddReturningDifference', description: 'DescriptionLeft']) - verifyResponse CREATED, response - - //dataTypes - // DELETE("$source/dataTypes/$deleteDataTypeSource") - // verifyResponse NO_CONTENT, response - // - // PUT("$source/dataClasses/$modifyDataTypeSource", [description: 'Description']) - // verifyResponse OK, response - // - // POST("$source/dataTypes", [label: 'addDataTypeSource', domainType: 'PrimitiveType']) - // verifyResponse CREATED, response - // String addDataTypeSource = responseBody().id - - when: - // for mergeInto json - GET("$target/path/dm%3A%7Cdc%3AexistingClass") - verifyResponse OK, response - existingClass = responseBody().id - GET("$target/path/dm%3A%7Cdc%3AmodifyAndDelete") - verifyResponse OK, response - String targetModifyAndDelete = responseBody().id - - GET("$target/path/dm%3A%7Cdc%3AdeleteAndModify") - verifyResponse OK, response - deleteAndModify = responseBody().id - GET("$target/path/dm%3A%7Cdc%3AmodifyAndModifyReturningDifference") - verifyResponse OK, response - modifyAndModifyReturningDifference = responseBody().id - - then: - //dataModel description - PUT("$target", [description: 'DescriptionRight']) - verifyResponse OK, response - - //dataClasses - DELETE("$target/dataClasses/$targetModifyAndDelete") - verifyResponse NO_CONTENT, response - - PUT("$target/dataClasses/$deleteAndModify", [description: 'Description']) - verifyResponse OK, response - PUT("$target/dataClasses/$modifyAndModifyReturningDifference", [description: 'DescriptionRight']) - verifyResponse OK, response - - POST("$target/dataClasses/$existingClass/dataClasses", [label: 'addRightToExistingClass']) - verifyResponse CREATED, response - POST("$target/dataClasses", [label: 'addAndAddReturningDifference', description: 'DescriptionRight']) - verifyResponse CREATED, response - String addAndAddReturningDifference = responseBody().id + Map mergeData = buildComplexDataModelsForMerging() when: - // for mergeInto json - GET("$target/path/dm%3A%7Cdc%3AdeleteLeftOnly") - verifyResponse OK, response - deleteLeftOnly = responseBody().id - GET("$target/path/dm%3A%7Cdc%3AmodifyLeftOnly") - verifyResponse OK, response - modifyLeftOnly = responseBody().id - GET("dataClasses/$existingClass/path/dc%3A%7Cdc%3AdeleteLeftOnlyFromExistingClass", MAP_ARG, true) - verifyResponse OK, response - deleteLeftOnlyFromExistingClass = responseBody().id - - GET("$source/mergeDiff/$target", STRING_ARG) + GET("$mergeData.source/mergeDiff/$mergeData.target", STRING_ARG) then: verifyJsonResponse OK, expectedLegacyMergeDiffJson @@ -1959,8 +1648,8 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { def requestBody = [ changeNotice: 'Functional Test Merge Change Notice', patch : [ - leftId : target, - rightId: source, + leftId : mergeData.target, + rightId: mergeData.source, label : "Functional Test Model", diffs : [ [ @@ -1972,27 +1661,27 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { deleted : [ [ - id : deleteAndModify, + id : mergeData.deleteAndModify, label: "deleteAndModify" ], [ - id : deleteLeftOnly, + id : mergeData.deleteLeftOnly, label: "deleteLeftOnly" ] ], created : [ [ - id : addLeftOnly, + id : mergeData.addLeftOnly, label: "addLeftOnly" ], [ - id : sourceModifyAndDelete, + id : mergeData.modifyAndDelete, label: "modifyAndDelete" ] ], modified : [ [ - leftId: addAndAddReturningDifference, + leftId: mergeData.addAndAddReturningDifference, label : "addAndAddReturningDifference", diffs : [ [ @@ -2002,7 +1691,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { ] ], [ - leftId: existingClass, + leftId: mergeData.existingClass, label : "existingClass", diffs : [ [ @@ -2010,13 +1699,13 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { deleted : [ [ - id : deleteLeftOnlyFromExistingClass, + id : mergeData.deleteLeftOnlyFromExistingClass, label: "deleteLeftOnlyFromExistingClass" ] ], created : [ [ - id : addLeftToExistingClass, + id : mergeData.addLeftToExistingClass, label: "addLeftToExistingClass" ] ] @@ -2025,7 +1714,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { ] ], [ - leftId: modifyAndModifyReturningDifference, + leftId: mergeData.modifyAndModifyReturningDifference, label : "modifyAndModifyReturningDifference", diffs : [ [ @@ -2035,7 +1724,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { ] ], [ - leftId: modifyLeftOnly, + leftId: mergeData.modifyLeftOnly, label : "modifyLeftOnly", diffs : [ [ @@ -2051,32 +1740,33 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { ] - PUT("$source/mergeInto/$target", requestBody) + PUT("$mergeData.source/mergeInto/$mergeData.target", requestBody) then: verifyResponse OK, response - responseBody().id == target + responseBody().id == mergeData.target responseBody().description == modifiedDescriptionSource when: - GET("$target/dataClasses") + GET("$mergeData.target/dataClasses") then: responseBody().items.label as Set == ['existingClass', 'modifyAndModifyReturningDifference', 'modifyLeftOnly', - 'addAndAddReturningDifference', 'modifyAndDelete', 'addLeftOnly'] as Set + 'addAndAddReturningDifference', 'modifyAndDelete', 'addLeftOnly', + 'modifyRightOnly', 'addRightOnly', 'modifyAndModifyReturningNoDifference', 'addAndAddReturningNoDifference'] as Set responseBody().items.find {dataClass -> dataClass.label == 'modifyAndDelete'}.description == 'Description' responseBody().items.find {dataClass -> dataClass.label == 'addAndAddReturningDifference'}.description == 'addedDescriptionSource' responseBody().items.find {dataClass -> dataClass.label == 'modifyAndModifyReturningDifference'}.description == modifiedDescriptionSource responseBody().items.find {dataClass -> dataClass.label == 'modifyLeftOnly'}.description == 'modifiedDescriptionSourceOnly' when: - GET("$target/dataClasses/$existingClass/dataClasses") + GET("$mergeData.target/dataClasses/$mergeData.existingClass/dataClasses") then: responseBody().items.label as Set == ['addRightToExistingClass', 'addLeftToExistingClass'] as Set when: 'List edits for the Target DataModel' - GET("$target/edits", MAP_ARG) + GET("$mergeData.target/edits", MAP_ARG) then: 'The response is OK' verifyResponse OK, response @@ -2087,9 +1777,9 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { } cleanup: - cleanUpData(source) - cleanUpData(target) - cleanUpData(id) + cleanUpData(mergeData.source) + cleanUpData(mergeData.target) + cleanUpData(mergeData.id) } void 'MP04 : test merging metadata diff into draft model'() { @@ -2535,6 +2225,199 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { cleanUpData(id) } + Map buildComplexDataModelsForMerging() { + + String id = createNewItem(validJson) + + POST("$id/dataClasses", [label: 'deleteLeftOnly']) + verifyResponse CREATED, response + POST("$id/dataClasses", [label: 'deleteRightOnly']) + verifyResponse CREATED, response + POST("$id/dataClasses", [label: 'modifyLeftOnly']) + verifyResponse CREATED, response + POST("$id/dataClasses", [label: 'modifyRightOnly']) + verifyResponse CREATED, response + POST("$id/dataClasses", [label: 'deleteAndDelete']) + verifyResponse CREATED, response + POST("$id/dataClasses", [label: 'deleteAndModify']) + verifyResponse CREATED, response + POST("$id/dataClasses", [label: 'modifyAndDelete']) + verifyResponse CREATED, response + POST("$id/dataClasses", [label: 'modifyAndModifyReturningNoDifference']) + verifyResponse CREATED, response + POST("$id/dataClasses", [label: 'modifyAndModifyReturningDifference']) + verifyResponse CREATED, response + POST("$id/dataClasses", [label: 'existingClass']) + verifyResponse CREATED, response + String existingClass = responseBody().id + POST("$id/dataClasses/$existingClass/dataClasses", [label: 'deleteLeftOnlyFromExistingClass']) + verifyResponse CREATED, response + POST("$id/dataClasses/$existingClass/dataClasses", [label: 'deleteRightOnlyFromExistingClass']) + verifyResponse CREATED, response + + PUT("$id/finalise", [versionChangeType: 'Major']) + verifyResponse OK, response + PUT("$id/newBranchModelVersion", [branchName: VersionAwareConstraints.DEFAULT_BRANCH_NAME]) + verifyResponse CREATED, response + String target = responseBody().id + PUT("$id/newBranchModelVersion", [branchName: 'source']) + verifyResponse CREATED, response + String source = responseBody().id + + GET("$source/path/dm%3A%7Cdc%3AexistingClass") + verifyResponse OK, response + existingClass = responseBody().id + GET("dataClasses/$existingClass/path/dc%3A%7Cdc%3AdeleteLeftOnlyFromExistingClass", MAP_ARG, true) + verifyResponse OK, response + String deleteLeftOnlyFromExistingClass = responseBody().id + GET("$source/path/dm%3A%7Cdc%3AdeleteLeftOnly") + verifyResponse OK, response + String deleteLeftOnly = responseBody().id + GET("$source/path/dm%3A%7Cdc%3AdeleteAndDelete") + verifyResponse OK, response + String deleteAndDelete = responseBody().id + GET("$source/path/dm%3A%7Cdc%3AdeleteAndModify") + verifyResponse OK, response + String deleteAndModify = responseBody().id + + GET("$source/path/dm%3A%7Cdc%3AmodifyLeftOnly") + verifyResponse OK, response + String modifyLeftOnly = responseBody().id + GET("$source/path/dm%3A%7Cdc%3AmodifyAndDelete") + verifyResponse OK, response + String modifyAndDelete = responseBody().id + GET("$source/path/dm%3A%7Cdc%3AmodifyAndModifyReturningNoDifference") + verifyResponse OK, response + String modifyAndModifyReturningNoDifference = responseBody().id + GET("$source/path/dm%3A%7Cdc%3AmodifyAndModifyReturningDifference") + verifyResponse OK, response + String modifyAndModifyReturningDifference = responseBody().id + + DELETE("$source/dataClasses/$deleteAndDelete") + verifyResponse NO_CONTENT, response + DELETE("$source/dataClasses/$existingClass/dataClasses/$deleteLeftOnlyFromExistingClass") + verifyResponse NO_CONTENT, response + DELETE("$source/dataClasses/$deleteLeftOnly") + verifyResponse NO_CONTENT, response + DELETE("$source/dataClasses/$deleteAndModify") + verifyResponse NO_CONTENT, response + + PUT("$source/dataClasses/$modifyLeftOnly", [description: 'Description']) + verifyResponse OK, response + PUT("$source/dataClasses/$modifyAndDelete", [description: 'Description']) + verifyResponse OK, response + PUT("$source/dataClasses/$modifyAndModifyReturningNoDifference", [description: 'Description']) + verifyResponse OK, response + PUT("$source/dataClasses/$modifyAndModifyReturningDifference", [description: 'DescriptionLeft']) + verifyResponse OK, response + + POST("$source/dataClasses/$existingClass/dataClasses", [label: 'addLeftToExistingClass']) + verifyResponse CREATED, response + String addLeftToExistingClass = responseBody().id + POST("$source/dataClasses", [label: 'addLeftOnly']) + verifyResponse CREATED, response + String addLeftOnly = responseBody().id + POST("$source/dataClasses", [label: 'addAndAddReturningNoDifference']) + verifyResponse CREATED, response + POST("$source/dataClasses", [label: 'addAndAddReturningDifference', description: 'DescriptionLeft']) + verifyResponse CREATED, response + String addAndAddReturningDifference = responseBody().id + + PUT("$source", [description: 'DescriptionLeft']) + verifyResponse OK, response + + GET("$target/path/dm%3A%7Cdc%3AexistingClass") + verifyResponse OK, response + existingClass = responseBody().id + GET("dataClasses/$existingClass/path/dc%3A%7Cdc%3AdeleteRightOnlyFromExistingClass", MAP_ARG, true) + verifyResponse OK, response + String deleteRightOnlyFromExistingClass = responseBody().id + GET("$target/path/dm%3A%7Cdc%3AdeleteRightOnly") + verifyResponse OK, response + String deleteRightOnly = responseBody().id + GET("$target/path/dm%3A%7Cdc%3AdeleteAndDelete") + verifyResponse OK, response + deleteAndDelete = responseBody().id + GET("$target/path/dm%3A%7Cdc%3AmodifyAndDelete") + verifyResponse OK, response + String targetModifyAndDelete = responseBody().id + + GET("$target/path/dm%3A%7Cdc%3AmodifyRightOnly") + verifyResponse OK, response + String modifyRightOnly = responseBody().id + GET("$target/path/dm%3A%7Cdc%3AdeleteAndModify") + verifyResponse OK, response + deleteAndModify = responseBody().id + GET("$target/path/dm%3A%7Cdc%3AmodifyAndModifyReturningNoDifference") + verifyResponse OK, response + modifyAndModifyReturningNoDifference = responseBody().id + GET("$target/path/dm%3A%7Cdc%3AmodifyAndModifyReturningDifference") + verifyResponse OK, response + modifyAndModifyReturningDifference = responseBody().id + + DELETE("$target/dataClasses/$existingClass/dataClasses/$deleteRightOnlyFromExistingClass") + verifyResponse NO_CONTENT, response + DELETE("$target/dataClasses/$deleteRightOnly") + verifyResponse NO_CONTENT, response + DELETE("$target/dataClasses/$deleteAndDelete") + verifyResponse NO_CONTENT, response + DELETE("$target/dataClasses/$targetModifyAndDelete") + verifyResponse NO_CONTENT, response + + PUT("$target/dataClasses/$modifyRightOnly", [description: 'Description']) + verifyResponse OK, response + PUT("$target/dataClasses/$deleteAndModify", [description: 'Description']) + verifyResponse OK, response + PUT("$target/dataClasses/$modifyAndModifyReturningNoDifference", [description: 'Description']) + verifyResponse OK, response + PUT("$target/dataClasses/$modifyAndModifyReturningDifference", [description: 'DescriptionRight']) + verifyResponse OK, response + + POST("$target/dataClasses/$existingClass/dataClasses", [label: 'addRightToExistingClass']) + verifyResponse CREATED, response + POST("$target/dataClasses", [label: 'addRightOnly']) + verifyResponse CREATED, response + POST("$target/dataClasses", [label: 'addAndAddReturningNoDifference']) + verifyResponse CREATED, response + POST("$target/dataClasses", [label: 'addAndAddReturningDifference', description: 'DescriptionRight']) + verifyResponse CREATED, response + addAndAddReturningDifference = responseBody().id + + PUT("$target", [description: 'DescriptionRight']) + verifyResponse OK, response + + // for mergeInto json + GET("$target/path/dm%3A%7Cdc%3AdeleteLeftOnly") + verifyResponse OK, response + deleteLeftOnly = responseBody().id + GET("$target/path/dm%3A%7Cdc%3AmodifyLeftOnly") + verifyResponse OK, response + modifyLeftOnly = responseBody().id + GET("dataClasses/$existingClass/path/dc%3A%7Cdc%3AdeleteLeftOnlyFromExistingClass", MAP_ARG, true) + verifyResponse OK, response + deleteLeftOnlyFromExistingClass = responseBody().id + + [id : id, + source : source, + target : target, + // For legacy testing + existingClass : existingClass, + deleteLeftOnlyFromExistingClass : deleteLeftOnlyFromExistingClass, + deleteLeftOnly : deleteLeftOnly, + deleteAndDelete : deleteAndDelete, + deleteAndModify : deleteAndModify, + modifyLeftOnly : modifyLeftOnly, + modifyAndDelete : modifyAndDelete, + modifyAndModifyReturningNoDifference: modifyAndModifyReturningNoDifference, + modifyAndModifyReturningDifference : modifyAndModifyReturningDifference, + deleteRightOnlyFromExistingClass : deleteRightOnlyFromExistingClass, + deleteRightOnly : deleteRightOnly, + modifyRightOnly : modifyRightOnly, + addLeftOnly : addLeftOnly, + addAndAddReturningDifference : addAndAddReturningDifference, + addLeftToExistingClass : addLeftToExistingClass] + } + void 'test changing folder from DataModel context'() { given: 'The save action is executed with valid data' String id = createNewItem(validJson) From 50414a34983d93a578fe1091fd0e6e8183a4e93d Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Thu, 1 Jul 2021 12:50:36 +0100 Subject: [PATCH 35/82] Add pathIdentifier and service methods to find by this to all CreatorAware domains * This will allow a complete coverage of our system by path alone * This does mean there are now some redundent methods in the services which are currently used by the existing path service --- .../traits/domain/CreatorAware.groovy | 10 ++- .../core/admin/ApiProperty.groovy | 5 ++ .../core/authority/Authority.groovy | 4 + .../core/container/Classifier.groovy | 7 +- .../core/container/Folder.groovy | 5 ++ .../core/container/VersionedFolder.groovy | 13 ++- .../core/facet/Annotation.groovy | 10 ++- .../maurodatamapper/core/facet/Edit.groovy | 5 ++ .../core/facet/Metadata.groovy | 8 +- .../core/facet/ReferenceFile.groovy | 2 +- .../maurodatamapper/core/facet/Rule.groovy | 10 ++- .../core/facet/SemanticLink.groovy | 11 ++- .../core/facet/VersionLink.groovy | 11 ++- .../core/facet/rule/RuleRepresentation.groovy | 5 ++ .../core/container/ClassifierService.groovy | 32 +++---- .../core/container/FolderService.groovy | 24 ++--- .../container/VersionedFolderService.groovy | 50 +++++++---- .../core/facet/AnnotationService.groovy | 5 ++ .../core/facet/MetadataService.groovy | 8 ++ .../core/facet/ReferenceFileService.groovy | 4 + .../core/facet/RuleService.groovy | 8 +- .../core/facet/SemanticLinkService.groovy | 9 ++ .../core/facet/VersionLinkService.groovy | 9 ++ .../core/model/CatalogueItem.groovy | 5 ++ .../core/model/CatalogueItemService.groovy | 31 +++---- .../core/model/Container.groovy | 5 ++ .../core/model/ContainerService.groovy | 11 ++- .../maurodatamapper/core/model/Model.groovy | 5 ++ .../core/model/ModelItemService.groovy | 11 +-- .../core/model/ModelService.groovy | 28 +++++- .../core/model/file/CatalogueFile.groovy | 7 +- .../traits/domain/MultiFacetItemAware.groovy | 5 +- .../core/traits/service/DomainService.groovy | 36 +++++++- .../service/MultiFacetItemAwareService.groovy | 22 ++--- .../datamodel/facet/SummaryMetadata.groovy | 5 ++ .../SummaryMetadataReport.groovy | 9 ++ .../datamodel/item/DataClass.groovy | 4 + .../facet/SummaryMetadataService.groovy | 5 ++ .../SummaryMetadataReportService.groovy | 11 ++- .../datamodel/item/DataClassService.groovy | 90 ++++++++++--------- .../datamodel/item/DataElementService.groovy | 27 +++--- .../item/datatype/DataTypeService.groovy | 29 +++--- .../EnumerationValueService.groovy | 5 ++ .../DataModelServiceIntegrationSpec.groovy | 3 - .../federation/SubscribedCatalogue.groovy | 9 +- .../federation/SubscribedModel.groovy | 7 +- .../facet/ReferenceSummaryMetadata.groovy | 8 +- .../ReferenceSummaryMetadataReport.groovy | 11 ++- .../item/ReferenceDataValue.groovy | 5 ++ .../ReferenceSummaryMetadataService.groovy | 5 ++ ...ferenceSummaryMetadataReportService.groovy | 11 ++- .../item/ReferenceDataValueService.groovy | 10 ++- .../terminology/item/Term.groovy | 5 ++ .../item/term/TermRelationship.groovy | 20 ++++- .../terminology/CodeSetService.groovy | 9 +- .../item/TermRelationshipTypeService.groovy | 5 ++ .../terminology/item/TermService.groovy | 18 +++- .../item/term/TermRelationshipService.groovy | 13 ++- .../security/CatalogueUser.groovy | 9 +- .../maurodatamapper/security/UserGroup.groovy | 5 ++ .../security/authentication/ApiKey.groovy | 5 -- .../security/role/GroupRole.groovy | 5 ++ .../role/SecurableResourceGroupRole.groovy | 5 -- .../security/UserGroupService.groovy | 10 ++- .../security/role/GroupRoleService.groovy | 10 ++- 65 files changed, 569 insertions(+), 225 deletions(-) diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/traits/domain/CreatorAware.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/traits/domain/CreatorAware.groovy index a99ce007b5..5c995ddeff 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/traits/domain/CreatorAware.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/traits/domain/CreatorAware.groovy @@ -45,7 +45,15 @@ trait CreatorAware { abstract String getDomainType() - abstract String getPathPrefix() + // Allow domains to not be "pathed". Also provides compatability + String getPathPrefix() { + null + } + + // Allow domains to not be "pathed". Also provides compatability + String getPathIdentifier() { + null + } @Deprecated(forRemoval = true) void setCreatedByUser(User user) { diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/admin/ApiProperty.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/admin/ApiProperty.groovy index 0d92fcfeb0..42693cb5da 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/admin/ApiProperty.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/admin/ApiProperty.groovy @@ -70,6 +70,11 @@ class ApiProperty implements CreatorAware { 'api' } + @Override + String getPathIdentifier() { + "${category}.${key}" + } + def beforeValidate() { if (!lastUpdatedBy) lastUpdatedBy = createdBy } diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/authority/Authority.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/authority/Authority.groovy index 6b96e6cc2f..09485a48ca 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/authority/Authority.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/authority/Authority.groovy @@ -60,6 +60,10 @@ class Authority implements InformationAware, CreatorAware, SecurableResource { 'auth' } + @Override + String getPathIdentifier() { + "${label}@${url}" + } @Override String toString() { diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Classifier.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Classifier.groovy index 9c9473f99a..9c55f21ae9 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Classifier.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Classifier.groovy @@ -99,6 +99,11 @@ class Classifier implements Container { 'cl' } + @Override + String getPathIdentifier() { + label + } + @Override Classifier getPathParent() { parentClassifier @@ -107,7 +112,7 @@ class Classifier implements Container { @Override def beforeValidate() { buildPath() - childClassifiers.each {it.beforeValidate()} + childClassifiers.each { it.beforeValidate() } } @Override diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Folder.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Folder.groovy index 83c2f14f34..aab56c81aa 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Folder.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Folder.groovy @@ -104,6 +104,11 @@ class Folder implements Container { 'fo' } + @Override + String getPathIdentifier() { + label + } + boolean hasChildFolders() { Folder.countByParentFolder(this) } diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolder.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolder.groovy index f62befd2a8..a3c2b7a31e 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolder.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolder.groovy @@ -44,13 +44,13 @@ class VersionedFolder extends Folder implements VersionAware, VersionLinkAware { CallableConstraints.call(InformationAwareConstraints, delegate) CallableConstraints.call(VersionAwareConstraints, delegate) - label validator: {val, obj -> new VersionedFolderLabelValidator(obj).isValid(val)} + label validator: { val, obj -> new VersionedFolderLabelValidator(obj).isValid(val) } parentFolder nullable: true - childFolders validator: {val, obj -> + childFolders validator: { val, obj -> if (obj.ident()) { return VersionedFolder.countByParentFolder(obj) ? ['Cannot have any VersionedFolders inside a VersionedFolder'] : true } - val.any {it.domainType == VersionedFolder.simpleName} ? ['Cannot have any VersionedFolders inside a VersionedFolder'] : true + val.any { it.domainType == VersionedFolder.simpleName } ? ['Cannot have any VersionedFolders inside a VersionedFolder'] : true } } @@ -74,6 +74,11 @@ class VersionedFolder extends Folder implements VersionAware, VersionLinkAware { 'vf' } + @Override + String getPathIdentifier() { + "${label}.${modelVersion ?: branchName}" + } + static DetachedCriteria by() { new DetachedCriteria(VersionedFolder) } @@ -123,7 +128,7 @@ class VersionedFolder extends Folder implements VersionAware, VersionLinkAware { by() .isNotNull('path') .ne('path', '') - .findAll {f -> + .findAll { f -> ids.any { it in f.path.split('/') } diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy index f80334e1f0..185226837d 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy @@ -26,14 +26,13 @@ import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.MultiFacetItemAware import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.PathAware import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CallableConstraints import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CreatorAwareConstraints -import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware import uk.ac.ox.softeng.maurodatamapper.util.Utils import grails.gorm.DetachedCriteria import grails.rest.Resource @Resource(readOnly = false, formats = ['json', 'xml']) -class Annotation implements MultiFacetItemAware, PathAware, InformationAware, CreatorAware, Diffable { +class Annotation implements MultiFacetItemAware, PathAware, InformationAware, Diffable { UUID id Annotation parentAnnotation @@ -82,6 +81,11 @@ class Annotation implements MultiFacetItemAware, PathAware, InformationAware, Cr 'ann' } + @Override + String getPathIdentifier() { + label + } + @Override Annotation getPathParent() { parentAnnotation @@ -89,7 +93,7 @@ class Annotation implements MultiFacetItemAware, PathAware, InformationAware, Cr def beforeValidate() { buildPath() - childAnnotations.eachWithIndex {ann, i -> + childAnnotations.eachWithIndex { ann, i -> if (!ann.label) ann.label = "$label [$i]" if (multiFacetAwareItem) { ann.setMultiFacetAwareItem(this.multiFacetAwareItem) diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Edit.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Edit.groovy index 44724ce3d8..3818abd586 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Edit.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Edit.groovy @@ -54,6 +54,11 @@ class Edit implements CreatorAware { 'ed' } + @Override + String getPathIdentifier() { + title + } + @SuppressWarnings("UnnecessaryQualifiedReference") static List findAllByResource(String resourceDomainType, UUID resourceId, Map pagination = [:]) { Edit.findAllByResourceDomainTypeAndResourceId(resourceDomainType, resourceId, pagination) diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy index d9ada1f6a8..871bf2b814 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy @@ -23,14 +23,13 @@ import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.MultiFacetItemAware import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CallableConstraints import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CreatorAwareConstraints -import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware import uk.ac.ox.softeng.maurodatamapper.util.Utils import grails.gorm.DetachedCriteria import grails.rest.Resource @Resource(readOnly = false, formats = ['json', 'xml']) -class Metadata implements MultiFacetItemAware, CreatorAware, Diffable { +class Metadata implements MultiFacetItemAware, Diffable { public final static Integer BATCH_SIZE = 5000 @@ -108,6 +107,11 @@ class Metadata implements MultiFacetItemAware, CreatorAware, Diffable "${this.namespace}.${this.key}" } + @Override + String getPathIdentifier() { + "${this.namespace}.${this.key}" + } + static DetachedCriteria distinctNamespacesKeys() { new DetachedCriteria(Metadata) .projections { diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/ReferenceFile.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/ReferenceFile.groovy index 6df5d3f391..f064a2c641 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/ReferenceFile.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/ReferenceFile.groovy @@ -33,7 +33,7 @@ class ReferenceFile implements CatalogueFile, MultiFacetItemAware { static constraints = { CallableConstraints.call(CatalogueFileConstraints, delegate) - multiFacetAwareItemId nullable: true, validator: {val, obj -> + multiFacetAwareItemId nullable: true, validator: { val, obj -> if (val) return true if (!val && obj.multiFacetAwareItem && !obj.multiFacetAwareItem.ident()) return true ['default.null.message'] diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy index 4183fd5d51..30eaab27e4 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy @@ -24,14 +24,13 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.rule.RuleRepresentation import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.MultiFacetItemAware import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CallableConstraints import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CreatorAwareConstraints -import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware import uk.ac.ox.softeng.maurodatamapper.util.Utils import grails.gorm.DetachedCriteria import grails.rest.Resource @Resource(readOnly = false, formats = ['json', 'xml']) -class Rule implements MultiFacetItemAware, CreatorAware, Diffable { +class Rule implements MultiFacetItemAware, Diffable { UUID id @@ -81,6 +80,11 @@ class Rule implements MultiFacetItemAware, CreatorAware, Diffable { 'ru' } + @Override + String getPathIdentifier() { + name + } + @Override String toString() { "${getClass().getName()} : ${name}/${description} : ${id ?: '(unsaved)'}" @@ -103,7 +107,7 @@ class Rule implements MultiFacetItemAware, CreatorAware, Diffable { @Override String getDiffIdentifier() { - "${this.name}" + this.name } static DetachedCriteria by() { diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/SemanticLink.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/SemanticLink.groovy index 52080b3c3e..117c5c62e2 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/SemanticLink.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/SemanticLink.groovy @@ -17,12 +17,10 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.facet - import uk.ac.ox.softeng.maurodatamapper.core.model.facet.MultiFacetAware import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.MultiFacetItemAware import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CallableConstraints import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CreatorAwareConstraints -import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware import uk.ac.ox.softeng.maurodatamapper.util.Utils import grails.databinding.BindUsing @@ -30,11 +28,11 @@ import grails.gorm.DetachedCriteria import grails.rest.Resource @Resource(readOnly = false, formats = ['json', 'xml']) -class SemanticLink implements MultiFacetItemAware, CreatorAware { +class SemanticLink implements MultiFacetItemAware { UUID id - @BindUsing({obj, source -> SemanticLinkType.findFromMap(source)}) + @BindUsing({ obj, source -> SemanticLinkType.findFromMap(source) }) SemanticLinkType linkType MultiFacetAware targetMultiFacetAwareItem UUID targetMultiFacetAwareItemId @@ -79,6 +77,11 @@ class SemanticLink implements MultiFacetItemAware, CreatorAware { 'sl' } + @Override + String getPathIdentifier() { + "${linkType}.${targetMultiFacetAwareItemId}" + } + @Override String getEditLabel() { "SemanticLink:${linkType}:${targetMultiFacetAwareItemId}" diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/VersionLink.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/VersionLink.groovy index 4ced12e44f..db19582c8c 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/VersionLink.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/VersionLink.groovy @@ -17,14 +17,12 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.facet - import uk.ac.ox.softeng.maurodatamapper.core.model.facet.MultiFacetAware import uk.ac.ox.softeng.maurodatamapper.core.model.facet.VersionLinkAware import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.MultiFacetItemAware import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.VersionAware import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CallableConstraints import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CreatorAwareConstraints -import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware import uk.ac.ox.softeng.maurodatamapper.util.Utils import grails.databinding.BindUsing @@ -32,11 +30,11 @@ import grails.gorm.DetachedCriteria import grails.rest.Resource @Resource(readOnly = false, formats = ['json', 'xml']) -class VersionLink implements MultiFacetItemAware, CreatorAware { +class VersionLink implements MultiFacetItemAware { UUID id - @BindUsing({obj, source -> VersionLinkType.findFromMap(source)}) + @BindUsing({ obj, source -> VersionLinkType.findFromMap(source) }) VersionLinkType linkType VersionLinkAware targetModel UUID targetModelId @@ -79,6 +77,11 @@ class VersionLink implements MultiFacetItemAware, CreatorAware { 'vl' } + @Override + String getPathIdentifier() { + "${linkType}.${targetModelId}" + } + @Override String getEditLabel() { "VersionLink:${linkType}:${targetModelId}" diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy index 9bdcc6b7ce..c52c6c5c2a 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy @@ -77,6 +77,11 @@ class RuleRepresentation implements Diffable, EditHistoryAwa 'rr' } + @Override + String getPathIdentifier() { + language + } + @Override String toString() { "${getClass().getName()} : ${language}/${representation} : ${id ?: '(unsaved)'}" diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/ClassifierService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/ClassifierService.groovy index d91206e368..6571c5cacd 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/ClassifierService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/ClassifierService.groovy @@ -66,7 +66,7 @@ class ClassifierService extends ContainerService { @Override List getAll(Collection containerIds) { - Classifier.getAll(containerIds).findAll().collect {unwrapIfProxy(it)} + Classifier.getAll(containerIds).findAll().collect { unwrapIfProxy(it) } } @Override @@ -91,7 +91,7 @@ class ClassifierService extends ContainerService { @Override List list() { - Classifier.list().collect {unwrapIfProxy(it)} + Classifier.list().collect { unwrapIfProxy(it) } } @Override @@ -105,7 +105,7 @@ class ClassifierService extends ContainerService { } @Override - Classifier findDomainByParentIdAndLabel(UUID parentId, String label) { + Classifier findByParentIdAndLabel(UUID parentId, String label) { return null } @@ -128,7 +128,7 @@ class ClassifierService extends ContainerService { List findAllReadableContainersBySearchTerm(UserSecurityPolicyManager userSecurityPolicyManager, String searchTerm) { log.debug('Searching readable classifiers for search term in label') List readableIds = userSecurityPolicyManager.listReadableSecuredResourceIds(Classifier) - Classifier.luceneTreeLabelSearch(readableIds.collect {it.toString()}, searchTerm) + Classifier.luceneTreeLabelSearch(readableIds.collect { it.toString() }, searchTerm) } @Override @@ -166,8 +166,8 @@ class ClassifierService extends ContainerService { def saveAll(Collection classifiers) { - Collection alreadySaved = classifiers.findAll {it.ident() && it.isDirty()} - Collection notSaved = classifiers.findAll {!it.ident()} + Collection alreadySaved = classifiers.findAll { it.ident() && it.isDirty() } + Collection notSaved = classifiers.findAll { !it.ident() } if (alreadySaved) { log.debug('Straight saving {} classifiers', alreadySaved.size()) @@ -179,7 +179,7 @@ class ClassifierService extends ContainerService { List batch = [] int count = 0 - notSaved.each {de -> + notSaved.each { de -> batch += de count++ @@ -273,7 +273,7 @@ class ClassifierService extends ContainerService { // Filter out all the classifiers which the user can't read Collection allClassifiersInItem = catalogueItem.classifiers List readableIds = userSecurityPolicyManager.listReadableSecuredResourceIds(Classifier) - new PaginatedResultList(allClassifiersInItem.findAll {it.id in readableIds}.toList(), pagination) + new PaginatedResultList(allClassifiersInItem.findAll { it.id in readableIds }.toList(), pagination) } List findAllByParentClassifierId(UUID parentClassifierId, Map pagination = [:]) { @@ -288,13 +288,13 @@ class ClassifierService extends ContainerService { } def Classifier addClassifierToCatalogueItem(Class catalogueItemClass, UUID catalogueItemId, Classifier classifier) { - CatalogueItemService service = catalogueItemServices.find {it.handles(catalogueItemClass)} + CatalogueItemService service = catalogueItemServices.find { it.handles(catalogueItemClass) } service.addClassifierToCatalogueItem(catalogueItemId, classifier) classifier } def void removeClassifierFromCatalogueItem(Class catalogueItemClass, UUID catalogueItemId, Classifier classifier) { - CatalogueItemService service = catalogueItemServices.find {it.handles(catalogueItemClass)} + CatalogueItemService service = catalogueItemServices.find { it.handles(catalogueItemClass) } service.removeClassifierFromCatalogueItem(catalogueItemId, classifier) classifier } @@ -302,7 +302,7 @@ class ClassifierService extends ContainerService { void checkClassifiers(User catalogueUser, def classifiedItem) { if (!classifiedItem.classifiers) return - classifiedItem.classifiers.each {it -> + classifiedItem.classifiers.each { it -> it.createdBy = it.createdBy ?: classifiedItem.createdBy } @@ -311,13 +311,13 @@ class ClassifierService extends ContainerService { classifiedItem.classifiers?.clear() - List foundOrCreated = classifiers.collect {cls -> + List foundOrCreated = classifiers.collect { cls -> findOrCreateClassifier(catalogueUser, cls) } batchSave(foundOrCreated) - foundOrCreated.each {cls -> + foundOrCreated.each { cls -> classifiedItem.addToClassifiers(cls) } } @@ -325,13 +325,13 @@ class ClassifierService extends ContainerService { List findAllReadableCatalogueItemsByClassifierId(UserSecurityPolicyManager userSecurityPolicyManager, UUID classifierId, Map pagination = [:]) { Classifier classifier = get(classifierId) - catalogueItemServices.collect {service -> + catalogueItemServices.collect { service -> service.findAllReadableByClassifier(userSecurityPolicyManager, classifier) }.findAll().flatten() } private void cleanoutClassifier(Classifier classifier) { - classifier.childClassifiers.each {cleanoutClassifier(it)} - catalogueItemServices.each {it.removeAllFromClassifier(classifier)} + classifier.childClassifiers.each { cleanoutClassifier(it) } + catalogueItemServices.each { it.removeAllFromClassifier(classifier) } } } 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 40e010fc8b..1a683791d7 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 @@ -68,14 +68,14 @@ class FolderService extends ContainerService { @Override List getAll(Collection containerIds) { - Folder.getAll(containerIds).findAll().collect {unwrapIfProxy(it)} + Folder.getAll(containerIds).findAll().collect { unwrapIfProxy(it) } } @Override List findAllReadableContainersBySearchTerm(UserSecurityPolicyManager userSecurityPolicyManager, String searchTerm) { log.debug('Searching readable folders for search term in label') List readableIds = userSecurityPolicyManager.listReadableSecuredResourceIds(Folder) - Folder.luceneTreeLabelSearch(readableIds.collect {it.toString()}, searchTerm) + Folder.luceneTreeLabelSearch(readableIds.collect { it.toString() }, searchTerm) } @Override @@ -89,7 +89,7 @@ class FolderService extends ContainerService { } @Override - Folder findDomainByParentIdAndLabel(UUID parentId, String label) { + Folder findByParentIdAndLabel(UUID parentId, String label) { Folder.byParentFolderIdAndLabel(parentId, label.trim()).get() } @@ -127,7 +127,7 @@ class FolderService extends ContainerService { @Override List list() { - Folder.list().collect {unwrapIfProxy(it)} + Folder.list().collect { unwrapIfProxy(it) } } Long count() { @@ -148,15 +148,15 @@ class FolderService extends ContainerService { return } if (permanent) { - folder.childFolders.each {delete(it, permanent, false)} - modelServices.each {it.deleteAllInContainer(folder)} + folder.childFolders.each { delete(it, permanent, false) } + modelServices.each { it.deleteAllInContainer(folder) } if (securityPolicyManagerService) { securityPolicyManagerService.removeSecurityForSecurableResource(folder, null) } folder.trackChanges() folder.delete(flush: flush) } else { - folder.childFolders.each {delete(it)} + folder.childFolders.each { delete(it) } delete(folder) } } @@ -218,7 +218,7 @@ class FolderService extends ContainerService { @Deprecated Folder findFolder(Folder parentFolder, String label) { - findDomainByParentIdAndLabel(parentFolder.id, label) + findByParentIdAndLabel(parentFolder.id, label) } @Deprecated @@ -252,9 +252,9 @@ class FolderService extends ContainerService { copy.description = original.description metadataService.findAllByMultiFacetAwareItemId(original.id).each {copy.addToMetadata(it.namespace, it.key, it.value, copier.emailAddress)} - ruleService.findAllByMultiFacetAwareItemId(original.id).each {rule -> + ruleService.findAllByMultiFacetAwareItemId(original.id).each { rule -> Rule copiedRule = new Rule(name: rule.name, description: rule.description, createdBy: copier.emailAddress) - rule.ruleRepresentations.each {ruleRepresentation -> + rule.ruleRepresentations.each { ruleRepresentation -> copiedRule.addToRuleRepresentations(language: ruleRepresentation.language, representation: ruleRepresentation.representation, createdBy: copier.emailAddress) @@ -262,7 +262,7 @@ class FolderService extends ContainerService { copy.addToRules(copiedRule) } - semanticLinkService.findAllBySourceMultiFacetAwareItemId(original.id).each {link -> + semanticLinkService.findAllBySourceMultiFacetAwareItemId(original.id).each { link -> copy.addToSemanticLinks(createdBy: copier.emailAddress, linkType: link.linkType, targetMultiFacetAwareItemId: link.targetMultiFacetAwareItemId, targetMultiFacetAwareItemDomainType: link.targetMultiFacetAwareItemDomainType, @@ -274,7 +274,7 @@ class FolderService extends ContainerService { List findAllModelsInFolder(Folder folder) { if (!modelServices) return [] - modelServices.collectMany {service -> + modelServices.collectMany { service -> service.findAllByFolderId(folder.id) } as List } diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy index a24c666434..7ec5e5b0b2 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy @@ -102,14 +102,14 @@ class VersionedFolderService extends ContainerService implement @Override List getAll(Collection containerIds) { - VersionedFolder.getAll(containerIds).findAll().collect {unwrapIfProxy(it)} + VersionedFolder.getAll(containerIds).findAll().collect { unwrapIfProxy(it) } } @Override List findAllReadableContainersBySearchTerm(UserSecurityPolicyManager userSecurityPolicyManager, String searchTerm) { log.debug('Searching readable folders for search term in label') List readableIds = userSecurityPolicyManager.listReadableSecuredResourceIds(Folder) - VersionedFolder.luceneTreeLabelSearch(readableIds.collect {it.toString()}, searchTerm) + VersionedFolder.luceneTreeLabelSearch(readableIds.collect { it.toString() }, searchTerm) } @Override @@ -123,10 +123,23 @@ class VersionedFolderService extends ContainerService implement } @Override - VersionedFolder findDomainByParentIdAndLabel(UUID parentId, String label) { + VersionedFolder findByParentIdAndLabel(UUID parentId, String label) { VersionedFolder.byParentFolderIdAndLabel(parentId, label.trim()).get() } + @Override + VersionedFolder findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + String split = pathIdentifier.split(/\./) + DetachedCriteria criteria = VersionedFolder.byParentFolderId(parentId).eq('label', split[0]) + + if (Version.isVersionable(split[1])) { + criteria.eq('modelVersion', Version.from(split[1])) + } else { + criteria.eq('branchName', split[1]) + } + criteria.get() + } + @Override List findAllByParentId(UUID parentId, Map pagination = [:]) { VersionedFolder.byParentFolderId(parentId).list(pagination) @@ -181,16 +194,16 @@ class VersionedFolderService extends ContainerService implement long start = System.currentTimeMillis() log.debug('Finalising models inside folder') - modelServices.each {service -> + modelServices.each { service -> Collection modelsInFolder = service.findAllByFolderId(folder.id) - modelsInFolder.each {model -> + modelsInFolder.each { model -> service.finaliseModel(model as Model, user, folderVersion, null, folderVersionTag) } } List folders = findAllByParentId(folder.id) log.debug('Finalising {} sub folders inside folder', folders.size()) - folders.each {childFolder -> + folders.each { childFolder -> finaliseFolderContents(childFolder, user, folderVersion, folderVersionTag) } @@ -282,7 +295,7 @@ class VersionedFolderService extends ContainerService implement @Override List list() { - VersionedFolder.list().collect {unwrapIfProxy(it)} + VersionedFolder.list().collect { unwrapIfProxy(it) } } Long count() { @@ -502,9 +515,9 @@ class VersionedFolderService extends ContainerService implement String labelSuffix = folderCopy.label == original.label ? '' : " (${folderCopy.label})" log.debug('Copying models from original folder into copied folder') - modelServices.each {service -> + modelServices.each { service -> List originalModels = service.findAllByContainerId(original.id) as List - List copiedModels = originalModels.collect {Model model -> + List copiedModels = originalModels.collect { Model model -> service.copyModel(model, folderCopy, copier, copyPermissions, "${model.label}${labelSuffix}", @@ -512,7 +525,7 @@ class VersionedFolderService extends ContainerService implement userSecurityPolicyManager) } // We can't save until after all copied as the save clears the sessions - copiedModels.each {copy -> + copiedModels.each { copy -> log.debug('Validating and saving model copy') service.validate(copy) if (copy.hasErrors()) { @@ -524,7 +537,7 @@ class VersionedFolderService extends ContainerService implement List folders = findAllByParentId(original.id) log.debug('Copying {} sub folders inside folder', folders.size()) - folders.each {childFolder -> + folders.each { childFolder -> Folder childCopy = new Folder(parentFolder: folderCopy, deleted: false) childCopy = folderService.copyBasicFolderInformation(childFolder, childCopy, copier) folderService.validate(childCopy) @@ -579,7 +592,8 @@ class VersionedFolderService extends ContainerService implement VersionedFolder findOldestAncestor(VersionedFolder versionedFolder) { // Look for model version or doc version only VersionLink versionLink = versionLinkService.findBySourceModelIdAndLinkType(versionedFolder.id, VersionLinkType.NEW_MODEL_VERSION_OF) - versionLink = versionLink ?: versionLinkService.findBySourceModelIdAndLinkType(versionedFolder.id, VersionLinkType.NEW_DOCUMENTATION_VERSION_OF) + versionLink = + versionLink ?: versionLinkService.findBySourceModelIdAndLinkType(versionedFolder.id, VersionLinkType.NEW_DOCUMENTATION_VERSION_OF) // If no versionlink then we're at the oldest ancestor if (!versionLink) { @@ -602,7 +616,7 @@ class VersionedFolderService extends ContainerService implement List versionLinks = versionLinkService.findAllByTargetModelId(instance.id) - versionLinks.each {link -> + versionLinks.each { link -> VersionedFolder linkedModel = get(link.multiFacetAwareItemId) versionTreeModelList. addAll(buildModelVersionTree(linkedModel, link.linkType, rootVersionTreeModel, includeForks, userSecurityPolicyManager)) @@ -613,8 +627,10 @@ class VersionedFolderService extends ContainerService implement VersionedFolder findCommonAncestorBetweenModels(VersionedFolder leftModel, VersionedFolder rightModel) { if (leftModel.label != rightModel.label) { - throw new ApiBadRequestException('VFS03', "VersionedFolder [${leftModel.id}] does not share its label with [${rightModel.id}] therefore they cannot have a " + - "common ancestor") + throw new ApiBadRequestException('VFS03', + "VersionedFolder [${leftModel.id}] does not share its label with [${rightModel.id}] therefore they " + + "cannot have a " + + "common ancestor") } VersionedFolder finalisedLeftParent = getFinalisedParent(leftModel) @@ -665,7 +681,7 @@ class VersionedFolderService extends ContainerService implement } boolean doesDepthTreeContainVersionedFolder(Folder folder) { - folder.instanceOf(VersionedFolder) || folderService.findAllByParentId(folder.id).any {doesDepthTreeContainVersionedFolder(it)} + folder.instanceOf(VersionedFolder) || folderService.findAllByParentId(folder.id).any { doesDepthTreeContainVersionedFolder(it) } } boolean isVersionedFolderFamily(Folder folder) { @@ -674,6 +690,6 @@ class VersionedFolderService extends ContainerService implement boolean doesDepthTreeContainFinalisedModel(Folder folder) { List models = folderService.findAllModelsInFolder(folder) - models.any {it.finalised} || findAllByParentId(folder.id).any {doesDepthTreeContainFinalisedModel(it)} + models.any { it.finalised } || findAllByParentId(folder.id).any { doesDepthTreeContainFinalisedModel(it) } } } diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/AnnotationService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/AnnotationService.groovy index a0782b4078..152f3e3365 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/AnnotationService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/AnnotationService.groovy @@ -114,4 +114,9 @@ class AnnotationService implements MultiFacetItemAwareService { Number countWhereRootAnnotationOfMultiFacetAwareItemId(UUID multiFacetAwareItemId) { Annotation.whereRootAnnotationOfMultiFacetAwareItemId(multiFacetAwareItemId).count() } + + @Override + Annotation findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + Annotation.byMultiFacetAwareItemId(parentId).eq('label', pathIdentifier).get() + } } \ No newline at end of file diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy index 37948f540d..121b783134 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy @@ -265,4 +265,12 @@ class MetadataService implements MultiFacetItemAwareService { log.trace('Batch save took {}', Utils.getTimeString(System.currentTimeMillis() - start)) } + + @Override + Metadata findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + Metadata.byMultiFacetAwareItemId(parentId) + .eq('namespace', pathIdentifier.split(/\./)[0]) + .eq('key', pathIdentifier.split(/\./)[1]) + .get() + } } \ No newline at end of file diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/ReferenceFileService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/ReferenceFileService.groovy index 3581f133f8..8b1078b1b7 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/ReferenceFileService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/ReferenceFileService.groovy @@ -93,4 +93,8 @@ class ReferenceFileService implements CatalogueFileService, Multi ReferenceFile.by() } + @Override + ReferenceFile findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + ReferenceFile.byMultiFacetAwareItemId(parentId).eq('fileName', pathIdentifier).get() + } } \ No newline at end of file diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/RuleService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/RuleService.groovy index 8fa6bc041f..bb039089ad 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/RuleService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/RuleService.groovy @@ -142,7 +142,13 @@ class RuleService implements MultiFacetItemAwareService { UUID multiFacetAwareItemId) { EditHistoryAware multiFacetAwareItem = findMultiFacetAwareItemByDomainTypeAndId(multiFacetAwareItemDomainType, multiFacetAwareItemId) as EditHistoryAware - multiFacetAwareItem.addToEditsTransactionally EditTitle.DELETE,deleter, "[$domain.editLabel] removed from component [${multiFacetAwareItem.editLabel}]" + multiFacetAwareItem.addToEditsTransactionally(EditTitle.DELETE, deleter, "[$domain.editLabel] removed from component " + + "[${multiFacetAwareItem.editLabel}]") domain } + + @Override + Rule findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + Rule.byMultiFacetAwareItemId(parentId).eq('name', pathIdentifier).get() + } } \ No newline at end of file diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/SemanticLinkService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/SemanticLinkService.groovy index 03d1e30c93..7368242af1 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/SemanticLinkService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/SemanticLinkService.groovy @@ -54,6 +54,15 @@ class SemanticLinkService implements MultiFacetItemAwareService { semanticLink.save(flush: true) } + @Override + SemanticLink findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + String[] split = pathIdentifier + SemanticLink.byMultiFacetAwareItemId(parentId) + .eq('linkType', SemanticLinkType.findForLabel(split[0])) + .eq('targetMultiFacetAwareItemId', Utils.toUuid(split[1])) + .get() + } + void delete(SemanticLink semanticLink, boolean flush = false) { if (!semanticLink) return MultiFacetAwareService service = findServiceForMultiFacetAwareDomainType(semanticLink.multiFacetAwareItemDomainType) diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/VersionLinkService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/VersionLinkService.groovy index 8781e36309..7234f3550a 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/VersionLinkService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/VersionLinkService.groovy @@ -83,6 +83,15 @@ class VersionLinkService implements MultiFacetItemAwareService { service.save(versionLink.model) } + @Override + VersionLink findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + String[] split = pathIdentifier + VersionLink.byModelId(parentId) + .eq('linkType', SemanticLinkType.findForLabel(split[0])) + .eq('targetModelId', Utils.toUuid(split[1])) + .get() + } + @Override void addFacetToDomain(VersionLink facet, String domainType, UUID domainId) { if (!facet) return diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy index a1e6f8c48e..7cf3cc8964 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy @@ -88,6 +88,11 @@ trait CatalogueItem implements InformationAware, EditHistory label } + @Override + String getPathIdentifier() { + label + } + void updateChildIndexes(ModelItem updated, Integer oldIndex) { // no-op } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy index 7a23126412..b0d1954d5b 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy @@ -17,7 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.model -import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException + import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier import uk.ac.ox.softeng.maurodatamapper.core.container.ClassifierService import uk.ac.ox.softeng.maurodatamapper.core.facet.AnnotationService @@ -34,10 +34,8 @@ import uk.ac.ox.softeng.maurodatamapper.core.traits.service.DomainService import uk.ac.ox.softeng.maurodatamapper.core.traits.service.MultiFacetAwareService import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.util.Utils import grails.core.GrailsApplication -import grails.core.GrailsClass import groovy.util.logging.Slf4j import org.grails.datastore.gorm.GormEntity import org.hibernate.SessionFactory @@ -63,18 +61,6 @@ abstract class CatalogueItemService implements DomainSe getCatalogueItemClass() } - boolean handles(Class clazz) { - clazz == getCatalogueItemClass() - } - - boolean handles(String domainType) { - GrailsClass grailsClass = Utils.lookupGrailsDomain(grailsApplication, domainType) - if (!grailsClass) { - throw new ApiBadRequestException('CISXX', "Unrecognised domain class resource [${domainType}]") - } - handles(grailsClass.clazz) - } - boolean handlesPathPrefix(String pathPrefix) { false } @@ -189,25 +175,34 @@ abstract class CatalogueItemService implements DomainSe */ K findByParentAndLabel(CatalogueItem parentCatalogueItem, String label) { + findByParentIdAndLabel(parentCatalogueItem.id, label) + } + + K findByParentIdAndLabel(UUID parentId, String label) { null } + @Override + K findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + findByParentIdAndLabel(parentId, pathIdentifier) + } + void mergeMetadataIntoCatalogueItem(FieldPatchData fieldPatchData, K targetCatalogueItem, UserSecurityPolicyManager userSecurityPolicyManager) { log.debug('Merging Metadata into Catalogue Item') // call metadataService version of below - fieldPatchData.deleted.each {deletedItemPatchData -> + fieldPatchData.deleted.each { deletedItemPatchData -> Metadata metadata = metadataService.get(deletedItemPatchData.id) metadataService.delete(metadata) } // copy additions from source to target object - fieldPatchData.created.each {createdItemPatchData -> + fieldPatchData.created.each { createdItemPatchData -> Metadata metadata = metadataService.get(createdItemPatchData.id) metadataService.copy(targetCatalogueItem, metadata, userSecurityPolicyManager) } // for modifications, recursively call this method - fieldPatchData.modified.each {modifiedObjectPatchData -> + fieldPatchData.modified.each { modifiedObjectPatchData -> metadataService.mergeMetadataIntoCatalogueItem(targetCatalogueItem, modifiedObjectPatchData) } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Container.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Container.groovy index a92fc94739..10c2fa2a42 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Container.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Container.groovy @@ -35,4 +35,9 @@ trait Container implements PathAware, InformationAware, SecurableResource, EditH abstract boolean hasChildren() abstract Boolean getDeleted() + + // @Override + // String getPathIdentifier() { + // label + // } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ContainerService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ContainerService.groovy index 9a49168db5..8c2bee06d5 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ContainerService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ContainerService.groovy @@ -56,7 +56,7 @@ abstract class ContainerService implements SecurableResourc abstract K findDomainByLabel(String label) - abstract K findDomainByParentIdAndLabel(UUID parentId, String label) + abstract K findByParentIdAndLabel(UUID parentId, String label) abstract List findAllByParentId(UUID parentId) @@ -74,6 +74,11 @@ abstract class ContainerService implements SecurableResourc getContainerClass() } + @Override + K findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + findByParentIdAndLabel(parentId, pathIdentifier) + } + K findByPath(String path) { List paths if (path.contains('/')) paths = path.split('/').findAll() ?: [] @@ -94,11 +99,11 @@ abstract class ContainerService implements SecurableResourc K findByPath(K parent, List pathLabels) { if (pathLabels.size() == 1) { - return findDomainByParentIdAndLabel(parent.id, pathLabels[0]) + return findByParentIdAndLabel(parent.id, pathLabels[0]) } String nextParentLabel = pathLabels.remove(0) - K nextParent = findDomainByParentIdAndLabel(parent.id, nextParentLabel) + K nextParent = findByParentIdAndLabel(parent.id, nextParentLabel) findByPath(nextParent, pathLabels) } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Model.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Model.groovy index 9eff3653af..a95ab3df4e 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Model.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Model.groovy @@ -56,6 +56,11 @@ trait Model extends CatalogueItem implements SecurableRes static mapping = { } + @Override + String getPathIdentifier() { + "${label}.${modelVersion ?: branchName}" + } + @Override int compareTo(D that) { int res = 0 diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy index 8d8bd2f9da..397a7f27e3 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy @@ -19,9 +19,9 @@ package uk.ac.ox.softeng.maurodatamapper.core.model import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiNotYetImplementedException import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier -import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.CopyInformation import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.FieldPatchData import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.ObjectPatchData +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.CopyInformation import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.util.Utils @@ -64,7 +64,7 @@ abstract class ModelItemService extends CatalogueItemServic //TODO validation on saving merges if (!objectPatchData.hasPatches()) return targetModel log.debug('Merging {} diffs into modelItem [{}]', objectPatchData.getPatches().size(), targetModelItem.label) - objectPatchData.getPatches().each {mergeFieldDiff -> + objectPatchData.getPatches().each { mergeFieldDiff -> log.debug('{}', mergeFieldDiff.summary) if (mergeFieldDiff.isFieldChange()) { @@ -95,15 +95,16 @@ abstract class ModelItemService extends CatalogueItemServic targetModel } - void processFieldPatchData(FieldPatchData fieldPatchData, Model targetModel, UserSecurityPolicyManager userSecurityPolicyManager, UUID parentId = null) { + void processFieldPatchData(FieldPatchData fieldPatchData, Model targetModel, UserSecurityPolicyManager userSecurityPolicyManager, + UUID parentId = null) { // apply deletions of children to target object - fieldPatchData.deleted.each {deletedItemPatchData -> + fieldPatchData.deleted.each { deletedItemPatchData -> ModelItem modelItem = get(deletedItemPatchData.id) as ModelItem delete(modelItem) } // copy additions from source to target object - fieldPatchData.created.each {createdItemPatchData -> + fieldPatchData.created.each { createdItemPatchData -> ModelItem modelItem = get(createdItemPatchData.id) as ModelItem ModelItem copyModelItem if (parentId) { diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy index 9610f452b4..dd3fcf3e1a 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy @@ -34,12 +34,14 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLink import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkService import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints +import uk.ac.ox.softeng.maurodatamapper.core.path.PathService import uk.ac.ox.softeng.maurodatamapper.core.provider.dataloader.DataLoaderProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.ModelImporterProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.ModelImporterProviderServiceParameters import uk.ac.ox.softeng.maurodatamapper.core.rest.converter.json.OffsetDateTimeConverter import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.ObjectPatchData import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.VersionTreeModel +import uk.ac.ox.softeng.maurodatamapper.core.traits.service.DomainService import uk.ac.ox.softeng.maurodatamapper.core.traits.service.VersionLinkAwareService import uk.ac.ox.softeng.maurodatamapper.security.SecurableResourceService import uk.ac.ox.softeng.maurodatamapper.security.SecurityPolicyManagerService @@ -49,6 +51,7 @@ import uk.ac.ox.softeng.maurodatamapper.util.Utils import uk.ac.ox.softeng.maurodatamapper.util.Version import uk.ac.ox.softeng.maurodatamapper.util.VersionChangeType +import grails.gorm.DetachedCriteria import groovy.util.logging.Slf4j import org.grails.datastore.gorm.GormValidateable import org.grails.orm.hibernate.proxy.HibernateProxyHandler @@ -69,6 +72,9 @@ abstract class ModelService extends CatalogueItemService imp @Autowired(required = false) Set modelItemServices + @Autowired(required = false) + Set domainServices + @Autowired VersionLinkService versionLinkService @@ -87,6 +93,9 @@ abstract class ModelService extends CatalogueItemService imp @Autowired(required = false) SecurityPolicyManagerService securityPolicyManagerService + @Autowired + PathService pathService + @Override Class getCatalogueItemClass() { getModelClass() @@ -199,6 +208,21 @@ abstract class ModelService extends CatalogueItemService imp findAllReadableModels(constrainedIds, includeDeleted) } + @Override + K findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + String split = pathIdentifier.split(/\./) + DetachedCriteria criteria = parentId ? modelClass.byFolderId(parentId) : modelClass.by() + + criteria.eq('label', split[0]) + + if (Version.isVersionable(split[1])) { + criteria.eq('modelVersion', Version.from(split[1])) + } else { + criteria.eq('branchName', split[1]) + } + criteria.get() as K + } + K finaliseModel(K model, User user, Version requestedModelVersion, VersionChangeType versionChangeType, String versionTag) { log.debug('Finalising model') @@ -396,7 +420,7 @@ abstract class ModelService extends CatalogueItemService imp if (!objectPatchData.hasPatches()) return targetModel log.debug('Merging {} diffs into model {}', objectPatchData.getPatches().size(), targetModel.label) - objectPatchData.getPatches().each {mergeFieldDiff -> + objectPatchData.getPatches().each { mergeFieldDiff -> log.debug('{}', mergeFieldDiff.summary) if (mergeFieldDiff.isFieldChange()) { @@ -404,7 +428,7 @@ abstract class ModelService extends CatalogueItemService imp } else if (mergeFieldDiff.isMetadataChange()) { mergeMetadataIntoCatalogueItem(mergeFieldDiff, targetModel, userSecurityPolicyManager) } else { - ModelItemService modelItemService = modelItemServices.find {it.handles(mergeFieldDiff.fieldName)} + ModelItemService modelItemService = modelItemServices.find { it.handles(mergeFieldDiff.fieldName) } if (modelItemService) { modelItemService.processFieldPatchData(mergeFieldDiff, targetModel, userSecurityPolicyManager) } else { diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/file/CatalogueFile.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/file/CatalogueFile.groovy index e6c2b908f6..349583fd80 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/file/CatalogueFile.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/file/CatalogueFile.groovy @@ -28,7 +28,7 @@ import java.nio.file.Files import java.nio.file.Path @GrailsCompileStatic -trait CatalogueFile implements EditHistoryAware { +trait CatalogueFile extends EditHistoryAware { @BindUsing({ obj, source -> @@ -82,6 +82,11 @@ trait CatalogueFile implements EditHistoryAware { "${getClass().simpleName}:${fileName}" } + @Override + String getPathIdentifier() { + fileName + } + static DetachedCriteria withBaseFilter(DetachedCriteria criteria, Map filters) { if (filters.fileName) criteria = criteria.ilike('fileName', "%${filters.fileName}%") if (filters.fileType) criteria = criteria.ilike('fileType', "%${filters.fileType}%") diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/domain/MultiFacetItemAware.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/domain/MultiFacetItemAware.groovy index 4cf3c522f7..96f0e254cc 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/domain/MultiFacetItemAware.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/domain/MultiFacetItemAware.groovy @@ -17,19 +17,16 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.traits.domain - import uk.ac.ox.softeng.maurodatamapper.core.model.facet.MultiFacetAware import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware import grails.compiler.GrailsCompileStatic -import groovy.transform.SelfType /** * @since 30/01/2020 */ -@SelfType(CreatorAware) @GrailsCompileStatic -trait MultiFacetItemAware { +trait MultiFacetItemAware extends CreatorAware { UUID multiFacetAwareItemId String multiFacetAwareItemDomainType diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/DomainService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/DomainService.groovy index 9dd35fd687..9d1d60525b 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/DomainService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/DomainService.groovy @@ -17,10 +17,21 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.traits.service +import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException +import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware +import uk.ac.ox.softeng.maurodatamapper.util.Utils +import grails.core.GrailsApplication +import grails.core.GrailsClass import org.grails.orm.hibernate.proxy.HibernateProxyHandler +import org.springframework.beans.factory.annotation.Autowired -trait DomainService { +import java.lang.reflect.ParameterizedType + +trait DomainService { + + @Autowired + GrailsApplication grailsApplication final static HibernateProxyHandler HIBERNATE_PROXY_HANDLER = new HibernateProxyHandler() @@ -44,4 +55,27 @@ trait DomainService { K unwrapIfProxy(def ge) { HIBERNATE_PROXY_HANDLER.unwrapIfProxy(ge) as K } + + Class getDomainClass() { + (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0] + } + + boolean handles(Class clazz) { + clazz == getDomainClass() + } + + boolean handles(String domainType) { + GrailsClass grailsClass = Utils.lookupGrailsDomain(grailsApplication, domainType) + if (!grailsClass) { + throw new ApiBadRequestException('CISXX', "Unrecognised domain class resource [${domainType}]") + } + handles(grailsClass.clazz) + } + + boolean handlesPathPrefix(String pathPrefix) { + (getDomainClass().getDeclaredConstructor().newInstance() as CreatorAware).pathPrefix == pathPrefix + } + + abstract K findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) + } \ No newline at end of file diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/MultiFacetItemAwareService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/MultiFacetItemAwareService.groovy index 24f8d3ce1a..4bcfc20037 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/MultiFacetItemAwareService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/MultiFacetItemAwareService.groovy @@ -36,7 +36,7 @@ import org.springframework.beans.factory.annotation.Autowired * @since 31/01/2020 */ @Slf4j -trait MultiFacetItemAwareService extends DomainService { +trait MultiFacetItemAwareService extends DomainService { @Autowired(required = false) List catalogueItemServices @@ -44,19 +44,19 @@ trait MultiFacetItemAwareService extends DomainService { @Autowired(required = false) List containerServices - abstract K findByMultiFacetAwareItemIdAndId(UUID multiFacetAwareItemId, Serializable id) + abstract M findByMultiFacetAwareItemIdAndId(UUID multiFacetAwareItemId, Serializable id) - abstract List findAllByMultiFacetAwareItemId(UUID multiFacetAwareItemId, Map pagination) + abstract List findAllByMultiFacetAwareItemId(UUID multiFacetAwareItemId, Map pagination) - abstract DetachedCriteria getBaseDeleteCriteria() + abstract DetachedCriteria getBaseDeleteCriteria() - abstract void saveMultiFacetAwareItem(K facet) + abstract void saveMultiFacetAwareItem(M facet) - abstract void delete(K facet, boolean flush) + abstract void delete(M facet, boolean flush) - abstract void addFacetToDomain(K facet, String domainType, UUID domainId) + abstract void addFacetToDomain(M facet, String domainType, UUID domainId) - K addCreatedEditToMultiFacetAwareItem(User creator, K domain, String multiFacetAwareItemDomainType, UUID multiFacetAwareItemId) { + M addCreatedEditToMultiFacetAwareItem(User creator, M domain, String multiFacetAwareItemDomainType, UUID multiFacetAwareItemId) { EditHistoryAware multiFacetAwareItem = findMultiFacetAwareItemByDomainTypeAndId(multiFacetAwareItemDomainType, multiFacetAwareItemId) as EditHistoryAware multiFacetAwareItem.addToEditsTransactionally EditTitle.CREATE, creator, "[$domain.editLabel] added to component " + @@ -64,15 +64,15 @@ trait MultiFacetItemAwareService extends DomainService { domain } - K addUpdatedEditToMultiFacetAwareItem(User editor, K domain, String multiFacetAwareItemDomainType, UUID multiFacetAwareItemId, + M addUpdatedEditToMultiFacetAwareItem(User editor, M domain, String multiFacetAwareItemDomainType, UUID multiFacetAwareItemId, List dirtyPropertyNames) { EditHistoryAware multiFacetAwareItem = findMultiFacetAwareItemByDomainTypeAndId(multiFacetAwareItemDomainType, multiFacetAwareItemId) as EditHistoryAware - multiFacetAwareItem.addToEditsTransactionally EditTitle.UPDATE,editor, domain.editLabel, dirtyPropertyNames + multiFacetAwareItem.addToEditsTransactionally EditTitle.UPDATE, editor, domain.editLabel, dirtyPropertyNames domain } - K addDeletedEditToMultiFacetAwareItem(User deleter, K domain, String multiFacetAwareItemDomainType, UUID multiFacetAwareItemId) { + M addDeletedEditToMultiFacetAwareItem(User deleter, M domain, String multiFacetAwareItemDomainType, UUID multiFacetAwareItemId) { EditHistoryAware multiFacetAwareItem = findMultiFacetAwareItemByDomainTypeAndId(multiFacetAwareItemDomainType, multiFacetAwareItemId) as EditHistoryAware multiFacetAwareItem.addToEditsTransactionally EditTitle.DELETE, deleter, "[$domain.editLabel] removed from component " + diff --git a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/SummaryMetadata.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/SummaryMetadata.groovy index 5fd4f440d9..b67b910723 100644 --- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/SummaryMetadata.groovy +++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/SummaryMetadata.groovy @@ -69,6 +69,11 @@ class SummaryMetadata implements MultiFacetItemAware, InformationAware, CreatorA 'sm' } + @Override + String getPathIdentifier() { + label + } + String toString() { "${getClass().getName()} : ${label} : ${id ?: '(unsaved)'}" } diff --git a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/summarymetadata/SummaryMetadataReport.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/summarymetadata/SummaryMetadataReport.groovy index 4a35e44b12..f71b0ea8ef 100644 --- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/summarymetadata/SummaryMetadataReport.groovy +++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/summarymetadata/SummaryMetadataReport.groovy @@ -27,10 +27,14 @@ import grails.gorm.DetachedCriteria import grails.rest.Resource import java.time.OffsetDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter @Resource(readOnly = false, formats = ['json', 'xml']) class SummaryMetadataReport implements CreatorAware { + static final DateTimeFormatter PATH_FORMATTER = DateTimeFormatter.ofPattern('yyyyMMddHHmmssSSSSSSX') + UUID id OffsetDateTime reportDate String reportValue @@ -60,6 +64,11 @@ class SummaryMetadataReport implements CreatorAware { 'smr' } + @Override + String getPathIdentifier() { + reportDate.withOffsetSameInstant(ZoneOffset.UTC).format(PATH_FORMATTER) + } + String getEditLabel() { "Summary Metadata Report:${reportDate}" } diff --git a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClass.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClass.groovy index 6ff901e9d7..ef9b52254e 100644 --- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClass.groovy +++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClass.groovy @@ -298,6 +298,10 @@ class DataClass implements ModelItem, MultiplicityAware, S byDataModelId(dataModelId).eq('parentDataClass.id', dataClassId) } + static DetachedCriteria byParentDataClassId(UUID dataClassId) { + new DetachedCriteria(DataClass).eq('parentDataClass.id', dataClassId) + } + static DetachedCriteria byDataModelIdAndParentDataClassIdIncludingImported(UUID dataModelId, UUID dataClassId) { new DetachedCriteria(DataClass).or { and { diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/SummaryMetadataService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/SummaryMetadataService.groovy index 6feb9d051e..7a3a952136 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/SummaryMetadataService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/SummaryMetadataService.groovy @@ -88,4 +88,9 @@ class SummaryMetadataService implements MultiFacetItemAwareService getBaseDeleteCriteria() { SummaryMetadata.by() } + + @Override + SummaryMetadata findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + SummaryMetadata.byMultiFacetAwareItemId(parentId).eq('label', pathIdentifier).get() + } } \ No newline at end of file diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/summarymetadata/SummaryMetadataReportService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/summarymetadata/SummaryMetadataReportService.groovy index 3f4c5eb306..a0d145213c 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/summarymetadata/SummaryMetadataReportService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/summarymetadata/SummaryMetadataReportService.groovy @@ -21,15 +21,18 @@ import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException import uk.ac.ox.softeng.maurodatamapper.core.facet.EditTitle import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItemService +import uk.ac.ox.softeng.maurodatamapper.core.traits.service.DomainService import uk.ac.ox.softeng.maurodatamapper.security.User import grails.gorm.transactions.Transactional import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired +import java.time.OffsetDateTime + @Slf4j @Transactional -class SummaryMetadataReportService { +class SummaryMetadataReportService implements DomainService { @Autowired(required = false) List catalogueItemServices @@ -51,6 +54,12 @@ class SummaryMetadataReportService { summaryMetadataReport.delete() } + @Override + SummaryMetadataReport findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + OffsetDateTime reportDate = OffsetDateTime.parse(pathIdentifier, SummaryMetadataReport.PATH_FORMATTER) + SummaryMetadataReport.bySummaryMetadataId(parentId).eq('reportDate', reportDate).get() + } + SummaryMetadataReport findBySummaryMetadataIdAndId(UUID summaryMetadataId, Serializable id) { SummaryMetadataReport.bySummaryMetadataIdAndId(summaryMetadataId, id).get() } diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassService.groovy index a8c2f2ea3f..a3344f0fa8 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassService.groovy @@ -27,6 +27,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItemService +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.CopyInformation import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModelService import uk.ac.ox.softeng.maurodatamapper.datamodel.facet.SummaryMetadata @@ -35,7 +36,6 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.DataType import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.DataTypeService import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.ReferenceType import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.ReferenceTypeService -import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.CopyInformation import uk.ac.ox.softeng.maurodatamapper.datamodel.traits.service.SummaryMetadataAwareService import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager @@ -102,13 +102,13 @@ class DataClassService extends ModelItemService implements SummaryMet Set dataTypes = extractAllUsedNewOrDirtyDataTypes(dataClass) log.debug('{} new or dirty used datatypes inside dataclass', dataTypes.size()) // Validation should have already been done - dataTypes.each {it.skipValidation(true)} + dataTypes.each { it.skipValidation(true) } dataTypeService.saveAll(dataTypes, false) } Set extractAllUsedNewOrDirtyDataTypes(DataClass dataClass) { - Set dataTypes = dataClass.dataElements.collect {it.dataType}.findAll {it.isDirty() || !it.ident()}.toSet() - dataTypes.addAll(dataClass.dataClasses.collect {extractAllUsedNewOrDirtyDataTypes(it)}.flatten().toSet() as Collection) + Set dataTypes = dataClass.dataElements.collect { it.dataType }.findAll { it.isDirty() || !it.ident() }.toSet() + dataTypes.addAll(dataClass.dataClasses.collect { extractAllUsedNewOrDirtyDataTypes(it) }.flatten().toSet() as Collection) dataTypes } @@ -133,7 +133,7 @@ class DataClassService extends ModelItemService implements SummaryMet dataClass.save(flush: false, validate: false) // Recurse through the hierarchy - dataClasses?.each {dc -> + dataClasses?.each { dc -> saveDataClassHierarchy(dc) } @@ -242,14 +242,14 @@ class DataClassService extends ModelItemService implements SummaryMet Collection saveAllAndGetDataElements(Collection dataClasses) { - List classifiers = dataClasses.collectMany {it.classifiers ?: []} as List + List classifiers = dataClasses.collectMany { it.classifiers ?: [] } as List if (classifiers) { log.trace('Saving {} classifiers') classifierService.saveAll(classifiers) } - Collection alreadySaved = dataClasses.findAll {it.ident() && it.isDirty()} - Collection notSaved = dataClasses.findAll {!it.ident()} + Collection alreadySaved = dataClasses.findAll { it.ident() && it.isDirty() } + Collection notSaved = dataClasses.findAll { !it.ident() } Collection dataElements = [] @@ -264,10 +264,10 @@ class DataClassService extends ModelItemService implements SummaryMet int count = 0 // Find all DCs which are either top level or have their parent DC already saved - Collection parentIsSaved = notSaved.findAll {!it.parentDataClass || it.parentDataClass.id} + Collection parentIsSaved = notSaved.findAll { !it.parentDataClass || it.parentDataClass.id } log.trace('Ready to save on first run {}', parentIsSaved.size()) while (parentIsSaved) { - parentIsSaved.each {dc -> + parentIsSaved.each { dc -> dataElements.addAll dc.dataElements ?: [] dc.dataClasses?.clear() @@ -285,7 +285,7 @@ class DataClassService extends ModelItemService implements SummaryMet batch.clear() // Find all DCs which have a saved parent DC notSaved.removeAll(parentIsSaved) - parentIsSaved = notSaved.findAll {it.parentDataClass && it.parentDataClass.id} + parentIsSaved = notSaved.findAll { it.parentDataClass && it.parentDataClass.id } log.trace('Ready to save on subsequent run {}', parentIsSaved.size()) } } @@ -298,29 +298,29 @@ class DataClassService extends ModelItemService implements SummaryMet dataClass.breadcrumbTree.removeFromParent() dataClass.dataModel.removeFromDataClasses(dataClass) dataClass.extendedDataClasses - dataClass.dataClasses?.each {removeAssociations(it)} + dataClass.dataClasses?.each { removeAssociations(it) } } private void removeSemanticLinks(DataClass dataClass) { List semanticLinks = semanticLinkService.findAllByMultiFacetAwareItemId(dataClass.id) - semanticLinks.each {semanticLinkService.delete(it)} + semanticLinks.each { semanticLinkService.delete(it) } } private void removeReferenceTypes(DataClass dataClass) { List referenceTypes = new ArrayList<>(dataClass.referenceTypes.findAll()) - referenceTypes.each {dataTypeService.delete(it)} + referenceTypes.each { dataTypeService.delete(it) } } private void removeAllDataElementsWithNoLabel(DataClass dataClass) { - List dataElements = new ArrayList<>(dataClass.dataElements.findAll {!it.label}) - dataElements.each {dataElementService.delete(it)} + List dataElements = new ArrayList<>(dataClass.dataElements.findAll { !it.label }) + dataElements.each { dataElementService.delete(it) } } private void removeAllDataElementsWithSameLabel(DataClass dataClass) { if (dataClass.dataElements) { - Map> identicalDataElements = dataClass.dataElements.groupBy {it.label}.findAll {it.value.size() > 1} - identicalDataElements.each {label, dataElements -> + Map> identicalDataElements = dataClass.dataElements.groupBy { it.label }.findAll { it.value.size() > 1 } + identicalDataElements.each { label, dataElements -> for (int i = 1; i < dataElements.size(); i++) { dataElementService.delete(dataElements[i]) } @@ -330,8 +330,8 @@ class DataClassService extends ModelItemService implements SummaryMet private void ensureChildDataClassesHaveUniqueNames(DataClass dataClass) { if (dataClass.dataClasses) { - dataClass.dataClasses.groupBy {it.label}.findAll {it.value.size() > 1}.each {label, dataClasses -> - dataClasses.eachWithIndex {DataClass child, int i -> + dataClass.dataClasses.groupBy { it.label }.findAll { it.value.size() > 1 }.each { label, dataClasses -> + dataClasses.eachWithIndex { DataClass child, int i -> child.label = "${child.label}-$i" } } @@ -341,10 +341,10 @@ class DataClassService extends ModelItemService implements SummaryMet private void collapseReferenceTypes(DataClass dataClass) { if (!dataClass.referenceTypes || dataClass.referenceTypes.size() == 1) return DataModel dataModel = dataClass.dataModel - Map> labelGroupedReferenceTypes = dataClass.referenceTypes.groupBy {it.label} + Map> labelGroupedReferenceTypes = dataClass.referenceTypes.groupBy { it.label } - labelGroupedReferenceTypes.findAll {it.value.size() > 1}.each {label, labelReferenceTypes -> - Map> dmGrouped = labelReferenceTypes.groupBy {it.dataModel ? 'dataModel' : 'noDataModel'} + labelGroupedReferenceTypes.findAll { it.value.size() > 1 }.each { label, labelReferenceTypes -> + Map> dmGrouped = labelReferenceTypes.groupBy { it.dataModel ? 'dataModel' : 'noDataModel' } // There will be only 1 datamodel owned type as we've already merged datamodel owned datatypes if (dmGrouped.dataModel) { @@ -362,11 +362,11 @@ class DataClassService extends ModelItemService implements SummaryMet private void setCreatedBy(User creator, DataClass dataClass) { dataClass.createdBy = creator.emailAddress - dataClass.dataClasses?.each {dc -> + dataClass.dataClasses?.each { dc -> setCreatedBy(creator, dc) } - dataClass.dataElements?.each {de -> + dataClass.dataElements?.each { de -> de.createdBy = creator.emailAddress } } @@ -378,13 +378,13 @@ class DataClassService extends ModelItemService implements SummaryMet checkFacetsAfterImportingCatalogueItem(dataClass) if (dataClass.dataClasses) { dataClass.fullSortOfChildren(dataClass.dataClasses) - dataClass.dataClasses.each {dc -> + dataClass.dataClasses.each { dc -> checkImportedDataClassAssociations(importingUser, dataModel, dc, matchDataTypes) } } if (dataClass.dataElements) { dataClass.fullSortOfChildren(dataClass.dataElements) - dataClass.dataElements.each {de -> + dataClass.dataElements.each { de -> de.createdBy = importingUser.emailAddress de.buildPath() dataElementService.checkFacetsAfterImportingCatalogueItem(de) @@ -394,7 +394,7 @@ class DataClassService extends ModelItemService implements SummaryMet } DataClass findSameLabelTree(DataModel dataModel, DataClass searchFor) { - dataModel.dataClasses.find {hasSameLabelTree(it, searchFor)} + dataModel.dataClasses.find { hasSameLabelTree(it, searchFor) } } private boolean hasSameLabelTree(DataClass left, DataClass right) { @@ -548,11 +548,11 @@ class DataClassService extends ModelItemService implements SummaryMet } DataClass findDataClass(DataModel dataModel, String label) { - dataModel.dataClasses.find {!it.parentDataClass && it.label == label.trim()} + dataModel.dataClasses.find { !it.parentDataClass && it.label == label.trim() } } DataClass findDataClass(DataClass parentDataClass, String label) { - parentDataClass.dataClasses.find {it.label == label.trim()} + parentDataClass.dataClasses.find { it.label == label.trim() } } DataClass findDataClassByPath(DataModel dataModel, List pathLabels) { @@ -622,8 +622,8 @@ class DataClassService extends ModelItemService implements SummaryMet if (!copy.validate()) //save(validate: false, copy) else throw new ApiInvalidModelException('DCS01', 'Copied DataClass is invalid', copy.errors, messageSource) - original.referenceTypes.each {refType -> - ReferenceType referenceType = copiedDataModel.referenceTypes.find {it.label == refType.label } + original.referenceTypes.each { refType -> + ReferenceType referenceType = copiedDataModel.referenceTypes.find { it.label == refType.label } if (!referenceType) { referenceType = new ReferenceType(createdBy: copier.emailAddress, label: refType.label) copiedDataModel.addToDataTypes(referenceType) @@ -632,11 +632,11 @@ class DataClassService extends ModelItemService implements SummaryMet } copy.dataClasses = [] - original.dataClasses.each {child -> + original.dataClasses.each { child -> copy.addToDataClasses(copyDataClass(copiedDataModel, child, copier, userSecurityPolicyManager)) } copy.dataElements = [] - original.dataElements.each {element -> + original.dataElements.each { element -> copy.addToDataElements(dataElementService.copyDataElement(copiedDataModel, element, copier, userSecurityPolicyManager)) } @@ -674,7 +674,7 @@ class DataClassService extends ModelItemService implements SummaryMet if (!emptyReferenceTypes) return log.debug('Found {} empty reference types', emptyReferenceTypes.size()) // Copy all the missing reference classes - emptyReferenceTypes.each {rt -> + emptyReferenceTypes.each { rt -> ReferenceType ort = originalDataModel.findDataTypeByLabel(rt.label) as ReferenceType String originalDataClassPath = buildPath(ort.referenceClass) DataClass copiedDataClass = findDataClassByPath(copiedDataModel, originalDataClassPath.split(/\|/).toList()) @@ -693,7 +693,7 @@ class DataClassService extends ModelItemService implements SummaryMet private Set getAllNestedReferenceTypes(DataClass dataClass) { Set referenceTypes = [] referenceTypes.addAll(dataClass.referenceTypes ?: []) - referenceTypes.addAll(dataClass.dataElements.dataType.findAll {it.instanceOf(ReferenceType) }) + referenceTypes.addAll(dataClass.dataElements.dataType.findAll { it.instanceOf(ReferenceType) }) dataClass.dataClasses.each { referenceTypes.addAll(getAllNestedReferenceTypes(it)) } @@ -702,7 +702,7 @@ class DataClassService extends ModelItemService implements SummaryMet private Set findAllEmptyReferenceTypes(DataModel dataModel) { - dataModel.referenceTypes.findAll {!(it as ReferenceType).referenceClass } as Set + dataModel.referenceTypes.findAll { !(it as ReferenceType).referenceClass } as Set } @@ -741,7 +741,7 @@ class DataClassService extends ModelItemService implements SummaryMet @Override List findAllReadableByClassifier(UserSecurityPolicyManager userSecurityPolicyManager, Classifier classifier) { - DataClass.byClassifierId(classifier.id).list().findAll {userSecurityPolicyManager.userCanReadSecuredResourceId(DataModel, it.model.id) } + DataClass.byClassifierId(classifier.id).list().findAll { userSecurityPolicyManager.userCanReadSecuredResourceId(DataModel, it.model.id) } } @Override @@ -779,10 +779,10 @@ class DataClassService extends ModelItemService implements SummaryMet List alreadyExistingLinks = semanticLinkService.findAllBySourceMultiFacetAwareItemIdInListAndTargetMultiFacetAwareItemIdInListAndLinkType( dataClasses*.id, fromDataClasses*.id, SemanticLinkType.IS_FROM) - dataClasses.each {de -> - fromDataClasses.each {fde -> + dataClasses.each { de -> + fromDataClasses.each { fde -> // If no link already exists then add a new one - if (!alreadyExistingLinks.any {it.multiFacetAwareItemId == de.id && it.targetMultiFacetAwareItemId == fde.id }) { + if (!alreadyExistingLinks.any { it.multiFacetAwareItemId == de.id && it.targetMultiFacetAwareItemId == fde.id }) { setDataClassIsFromDataClass(de, fde, user) } } @@ -815,8 +815,12 @@ class DataClassService extends ModelItemService implements SummaryMet * @param label The label of the DataClass being sought */ @Override - DataClass findByParentAndLabel(CatalogueItem parentCatalogueItem, String label) { - findDataClass(parentCatalogueItem, label) + DataClass findByParentIdAndLabel(UUID parentId, String label) { + DataClass dataClass = findByDataModelIdAndId(parentId, label) + if (!dataClass) { + dataClass = DataClass.byParentDataClassId(parentId).eq('label', label).get() + } + dataClass } /** diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementService.groovy index eb7067cd56..aba5e8e119 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementService.groovy @@ -21,7 +21,6 @@ import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLink import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkType -import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItemService import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.CopyInformation @@ -82,7 +81,7 @@ class DataElementService extends ModelItemService implements Summar @Override void deleteAll(Collection dataElements) { - dataElements.each {delete(it)} + dataElements.each { delete(it) } } void delete(UUID id) { @@ -179,7 +178,7 @@ class DataElementService extends ModelItemService implements Summar @Override List findAllReadableByClassifier(UserSecurityPolicyManager userSecurityPolicyManager, Classifier classifier) { - DataElement.byClassifierId(classifier.id).list().findAll {userSecurityPolicyManager.userCanReadSecuredResourceId(DataModel, it.model.id)} + DataElement.byClassifierId(classifier.id).list().findAll { userSecurityPolicyManager.userCanReadSecuredResourceId(DataModel, it.model.id) } } @Override @@ -213,13 +212,13 @@ class DataElementService extends ModelItemService implements Summar void matchUpDataTypes(DataModel dataModel, Collection dataElements) { if (dataElements) { log.debug("Matching up {} DataElements to a possible {} DataTypes", dataElements.size(), dataModel.dataTypes.size()) - def grouped = dataElements.groupBy {it.dataType.label}.sort {a, b -> + def grouped = dataElements.groupBy { it.dataType.label }.sort { a, b -> def res = a.value.size() <=> b.value.size() if (res == 0) res = a.key <=> b.key res } log.debug('Grouped {} DataElements by DataType label', grouped.size()) - grouped.each {label, elements -> + grouped.each { label, elements -> log.trace('Matching {} elements to DataType label {}', elements.size(), label) DataType dataType = dataModel.findDataTypeByLabel(label) @@ -230,7 +229,7 @@ class DataElementService extends ModelItemService implements Summar dataType.createdBy = dataElement.createdBy dataModel.addToDataTypes(dataType) } - elements.each {dataType.addToDataElements(it)} + elements.each { dataType.addToDataElements(it) } } } } @@ -325,7 +324,7 @@ class DataElementService extends ModelItemService implements Summar //Put the dataClass lookup in this method for use when merging DataElement copy(Model copiedDataModel, DataElement original, UserSecurityPolicyManager userSecurityPolicyManager) { DataElement copy = copyDataElement(copiedDataModel as DataModel, original, userSecurityPolicyManager.user, userSecurityPolicyManager) - DataClass dataClass = copiedDataModel.getDataClasses()?.find {it.label == original.dataClass.label} + DataClass dataClass = copiedDataModel.getDataClasses()?.find { it.label == original.dataClass.label } if (dataClass) { dataClass.addToDataElements(copy) } @@ -358,7 +357,7 @@ class DataElementService extends ModelItemService implements Summar DataElement copy, User copier, UserSecurityPolicyManager userSecurityPolicyManager, - boolean copySummaryMetadata,CopyInformation copyInformation) { + boolean copySummaryMetadata, CopyInformation copyInformation) { copy = super.copyCatalogueItemInformation(original, copy, copier, userSecurityPolicyManager, copyInformation) if (copySummaryMetadata) { summaryMetadataService.findAllByMultiFacetAwareItemId(original.id).each { @@ -435,10 +434,10 @@ class DataElementService extends ModelItemService implements Summar List alreadyExistingLinks = semanticLinkService.findAllBySourceMultiFacetAwareItemIdInListAndTargetMultiFacetAwareItemIdInListAndLinkType( dataElements*.id, fromDataElements*.id, SemanticLinkType.IS_FROM) - dataElements.each {de -> - fromDataElements.each {fde -> + dataElements.each { de -> + fromDataElements.each { fde -> // If no link already exists then add a new one - if (!alreadyExistingLinks.any {it.multiFacetAwareItemId == de.id && it.targetMultiFacetAwareItemId == fde.id}) { + if (!alreadyExistingLinks.any { it.multiFacetAwareItemId == de.id && it.targetMultiFacetAwareItemId == fde.id }) { setDataElementIsFromDataElement(de, fde, user) } } @@ -470,7 +469,7 @@ class DataElementService extends ModelItemService implements Summar * @param label The label of the DataElement being sought */ DataElement findDataElement(DataClass dataClass, String label) { - dataClass.dataElements.find {it.label == label.trim()} + dataClass.dataElements.find { it.label == label.trim() } } /** @@ -480,8 +479,8 @@ class DataElementService extends ModelItemService implements Summar * @param label The label of the DataElement being sought */ @Override - DataElement findByParentAndLabel(CatalogueItem parentCatalogueItem, String label) { - findDataElement(parentCatalogueItem, label) + DataElement findByParentIdAndLabel(UUID parentId, String label) { + findByDataClassIdAndLabel(parentId, label) } @Override diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataTypeService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataTypeService.groovy index 254b799917..1d4eaa519d 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataTypeService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataTypeService.groovy @@ -20,7 +20,6 @@ package uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata -import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItemService @@ -92,7 +91,7 @@ class DataTypeService extends ModelItemService implements DefaultDataT dataType.breadcrumbTree.removeFromParent() List dataElements = dataElementService.findAllByDataType(dataType) - dataElements.each {dataElementService.delete(it)} + dataElements.each { dataElementService.delete(it) } switch (dataType.domainType) { case DataType.PRIMITIVE_DOMAIN_TYPE: @@ -204,7 +203,7 @@ class DataTypeService extends ModelItemService implements DefaultDataT new PrimitiveType(label: 'Timestamp', description: 'A timestamp'), new PrimitiveType(label: 'Boolean', description: 'A true or false value'), new PrimitiveType(label: 'Duration', description: 'A time period in arbitrary units') - ].collect {new DefaultDataType(it)} + ].collect { new DefaultDataType(it) } } @Override @@ -284,7 +283,7 @@ class DataTypeService extends ModelItemService implements DefaultDataT if (dataType.instanceOf(EnumerationType)) { EnumerationType enumerationType = (dataType as EnumerationType) enumerationType.fullSortOfChildren(enumerationType.enumerationValues) - enumerationType.enumerationValues.each {ev -> + enumerationType.enumerationValues.each { ev -> ev.createdBy = importingUser.emailAddress ev.buildPath() } @@ -309,8 +308,8 @@ class DataTypeService extends ModelItemService implements DefaultDataT } void matchReferenceClasses(DataModel dataModel, Collection referenceTypes, Collection bindingMaps = []) { - referenceTypes.sort {it.label}.each {rdt -> - Map dataTypeBindingMap = bindingMaps.find {it.label == rdt.label} ?: [:] + referenceTypes.sort { it.label }.each { rdt -> + Map dataTypeBindingMap = bindingMaps.find { it.label == rdt.label } ?: [:] Map refClassBindingMap = dataTypeBindingMap.referenceClass ?: [:] matchReferenceClass(dataModel, rdt, refClassBindingMap) } @@ -329,7 +328,7 @@ class DataTypeService extends ModelItemService implements DefaultDataT else { log. trace('No referenceClass could be found to match label tree for {}, attempting no label tree', referenceType.referenceClass.label) - def possibles = dataModel.dataClasses.findAll {it.label == referenceType.referenceClass.label} + def possibles = dataModel.dataClasses.findAll { it.label == referenceType.referenceClass.label } if (possibles.size() == 1) { log.trace('Single possible referenceClass found, safely using') possibles.first().addToReferenceTypes(referenceType) @@ -343,7 +342,7 @@ class DataTypeService extends ModelItemService implements DefaultDataT } } else { log.trace('Making best guess for matching reference class as no path nor bound class') - DataClass dataClass = dataModel.dataClasses.find {it.label == bindingMap.referenceClass.label} + DataClass dataClass = dataModel.dataClasses.find { it.label == bindingMap.referenceClass.label } if (dataClass) dataClass.addToReferenceTypes(referenceType) } } @@ -364,7 +363,7 @@ class DataTypeService extends ModelItemService implements DefaultDataT break case DataType.ENUMERATION_DOMAIN_TYPE: copy = new EnumerationType() - original.enumerationValues.each {ev -> + original.enumerationValues.each { ev -> copy.addToEnumerationValues(key: ev.key, value: ev.value, category: ev.category) } break @@ -392,7 +391,7 @@ class DataTypeService extends ModelItemService implements DefaultDataT User copier, UserSecurityPolicyManager userSecurityPolicyManager, boolean copySummaryMetadata, - copyInformation = new CopyInformation()) { + copyInformation = new CopyInformation()) { copy = super.copyCatalogueItemInformation(original, copy, copier, userSecurityPolicyManager, copyInformation) if (copySummaryMetadata) { summaryMetadataService.findAllByMultiFacetAwareItemId(original.id).each { @@ -444,19 +443,19 @@ class DataTypeService extends ModelItemService implements DefaultDataT } private void mergeDataTypes(DataType keep, DataType replace) { - replace.dataElements?.each {de -> + replace.dataElements?.each { de -> keep.addToDataElements(de) } List mds = [] mds += replace.metadata ?: [] - mds.findAll {!keep.findMetadataByNamespaceAndKey(it.namespace, it.key)}.each {md -> + mds.findAll { !keep.findMetadataByNamespaceAndKey(it.namespace, it.key) }.each { md -> replace.removeFromMetadata(md) keep.addToMetadata(md.namespace, md.key, md.value, md.createdBy) } } DataType findDataType(DataModel dataModel, String label) { - dataModel.dataTypes.find {it.label == label.trim()} + dataModel.dataTypes.find { it.label == label.trim() } } /* @@ -467,8 +466,8 @@ class DataTypeService extends ModelItemService implements DefaultDataT */ @Override - DataType findByParentAndLabel(CatalogueItem parentCatalogueItem, String label) { - findDataType(parentCatalogueItem, label) + DataType findByParentIdAndLabel(UUID parentId, String label) { + DataType.byDataModelId(parentId).eq('label', label).get() } @Override diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValueService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValueService.groovy index 010d67836d..ab1d1edf6e 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValueService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValueService.groovy @@ -67,6 +67,11 @@ class EnumerationValueService extends ModelItemService impleme } } + @Override + EnumerationValue findByParentIdAndLabel(UUID parentId, String label) { + EnumerationValue.byEnumerationType(parentId).eq('label', label).get() + } + void deleteAllByModelId(UUID dataModelId) { //Assume DataElements gone by this point List enumerationValueIds = EnumerationValue.by().where { diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy index 1628f91617..8ad4957eb2 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy @@ -17,7 +17,6 @@ */ package uk.ac.ox.softeng.maurodatamapper.datamodel - import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.ArrayMergeDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.CreationMergeDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.DeletionMergeDiff @@ -47,12 +46,10 @@ import grails.testing.mixin.integration.Integration import groovy.util.logging.Slf4j import org.spockframework.util.Assert import spock.lang.PendingFeature -import spock.lang.Stepwise @Slf4j @Integration @Rollback -@Stepwise class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { DataModel complexDataModel 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 a1a25c3824..8b922a9b43 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 @@ -50,7 +50,7 @@ class SubscribedCatalogue implements SecurableResource, EditHistoryAware, Inform static constraints = { CallableConstraints.call(InformationAwareConstraints, delegate) - url blank: false, validator: {val -> + url blank: false, validator: { val -> new UrlValidator(UrlValidator.ALLOW_LOCAL_URLS).isValid(val) ?: ['default.invalid.url.message'] } label unique: true @@ -84,7 +84,12 @@ class SubscribedCatalogue implements SecurableResource, EditHistoryAware, Inform @Override String getPathPrefix() { - 'subc' + null + } + + @Override + String getPathIdentifier() { + null } @Override 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 6ccaf59a6e..3ce7c30558 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 @@ -67,7 +67,12 @@ class SubscribedModel implements SecurableResource, EditHistoryAware { @Override String getPathPrefix() { - 'subm' + null + } + + @Override + String getPathIdentifier() { + null } @Override diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadata.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadata.groovy index 3dff2bc83c..7d2867339a 100644 --- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadata.groovy +++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadata.groovy @@ -24,14 +24,13 @@ import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CallableConstra import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CreatorAwareConstraints import uk.ac.ox.softeng.maurodatamapper.referencedata.facet.summarymetadata.ReferenceSummaryMetadataReport import uk.ac.ox.softeng.maurodatamapper.referencedata.gorm.constraint.validator.ReferenceSummaryMetadataLabelValidator -import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware import uk.ac.ox.softeng.maurodatamapper.util.Utils import grails.gorm.DetachedCriteria import grails.rest.Resource @Resource(readOnly = false, formats = ['json', 'xml']) -class ReferenceSummaryMetadata implements MultiFacetItemAware, InformationAware, CreatorAware { +class ReferenceSummaryMetadata implements MultiFacetItemAware, InformationAware { public final static Integer BATCH_SIZE = 5000 @@ -69,6 +68,11 @@ class ReferenceSummaryMetadata implements MultiFacetItemAware, InformationAware, 'rsm' } + @Override + String getPathIdentifier() { + label + } + String toString() { "${getClass().getName()} : ${label} : ${id ?: '(unsaved)'}" } diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/summarymetadata/ReferenceSummaryMetadataReport.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/summarymetadata/ReferenceSummaryMetadataReport.groovy index e1df51b721..6760da76b3 100644 --- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/summarymetadata/ReferenceSummaryMetadataReport.groovy +++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/summarymetadata/ReferenceSummaryMetadataReport.groovy @@ -27,17 +27,21 @@ import grails.gorm.DetachedCriteria import grails.rest.Resource import java.time.OffsetDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter @Resource(readOnly = false, formats = ['json', 'xml']) class ReferenceSummaryMetadataReport implements CreatorAware { + static final DateTimeFormatter PATH_FORMATTER = DateTimeFormatter.ofPattern('yyyyMMddHHmmssSSSSSSX') + UUID id OffsetDateTime reportDate String reportValue ReferenceSummaryMetadata summaryMetadata static belongsTo = [ - ReferenceSummaryMetadata + ReferenceSummaryMetadata ] static constraints = { @@ -60,6 +64,11 @@ class ReferenceSummaryMetadataReport implements CreatorAware { 'rsmr' } + @Override + String getPathIdentifier() { + reportDate.withOffsetSameInstant(ZoneOffset.UTC).format(PATH_FORMATTER) + } + String getEditLabel() { "Summary Metadata Report:${reportDate}" } diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValue.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValue.groovy index 1bdccec4c2..b542fe1c29 100644 --- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValue.groovy +++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValue.groovy @@ -65,6 +65,11 @@ class ReferenceDataValue implements CreatorAware, Diffable { 'rdv' } + @Override + String getPathIdentifier() { + rowNumber + } + @Override String getDiffIdentifier() { this.id diff --git a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadataService.groovy b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadataService.groovy index b34d519deb..79abbe04eb 100644 --- a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadataService.groovy +++ b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadataService.groovy @@ -89,4 +89,9 @@ class ReferenceSummaryMetadataService implements MultiFacetItemAwareService getBaseDeleteCriteria() { ReferenceSummaryMetadata.by() } + + @Override + ReferenceSummaryMetadata findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + ReferenceSummaryMetadata.byMultiFacetAwareItemId(parentId).eq('label', pathIdentifier).get() + } } \ No newline at end of file diff --git a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/summarymetadata/ReferenceSummaryMetadataReportService.groovy b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/summarymetadata/ReferenceSummaryMetadataReportService.groovy index 0aab90eba9..c7458ad926 100644 --- a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/summarymetadata/ReferenceSummaryMetadataReportService.groovy +++ b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/summarymetadata/ReferenceSummaryMetadataReportService.groovy @@ -21,15 +21,18 @@ import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException import uk.ac.ox.softeng.maurodatamapper.core.facet.EditTitle import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItemService +import uk.ac.ox.softeng.maurodatamapper.core.traits.service.DomainService import uk.ac.ox.softeng.maurodatamapper.security.User import grails.gorm.transactions.Transactional import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired +import java.time.OffsetDateTime + @Slf4j @Transactional -class ReferenceSummaryMetadataReportService { +class ReferenceSummaryMetadataReportService implements DomainService { @Autowired(required = false) List catalogueItemServices @@ -51,6 +54,12 @@ class ReferenceSummaryMetadataReportService { summaryMetadataReport.delete() } + @Override + ReferenceSummaryMetadataReport findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + OffsetDateTime reportDate = OffsetDateTime.parse(pathIdentifier, ReferenceSummaryMetadataReport.PATH_FORMATTER) + ReferenceSummaryMetadataReport.byReferenceSummaryMetadataId(parentId).eq('reportDate', reportDate).get() + } + ReferenceSummaryMetadataReport findByReferenceSummaryMetadataIdAndId(UUID referenceSummaryMetadataId, Serializable id) { ReferenceSummaryMetadataReport.byReferenceSummaryMetadataIdAndId(referenceSummaryMetadataId, id).get() } diff --git a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValueService.groovy b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValueService.groovy index 15fc9baaf8..414db31325 100644 --- a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValueService.groovy +++ b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataValueService.groovy @@ -122,7 +122,7 @@ class ReferenceDataValueService implements DomainService { List findAllByReferenceDataModelIdAndRowNumberIn(Serializable referenceDataModelId, List rowNumbers, Map params = [:]) { ReferenceDataValue.withFilter(ReferenceDataValue.byReferenceDataModelIdAndRowNumberIn(referenceDataModelId, rowNumbers), params).list(params) - } + } Integer countRowsByReferenceDataModelId(Serializable referenceDataModelId) { ReferenceDataValue.countByReferenceDataModelId(referenceDataModelId).list()[0] @@ -141,7 +141,8 @@ class ReferenceDataValueService implements DomainService { referenceDataValue.createdBy = importingUser.emailAddress //Get the reference data element for this value by getting the matching reference data element for the model - referenceDataValue.referenceDataElement = referenceDataModel.referenceDataElements.find {it.label == referenceDataValue.referenceDataElement.label} + referenceDataValue.referenceDataElement = + referenceDataModel.referenceDataElements.find { it.label == referenceDataValue.referenceDataElement.label } } List findAllByMetadataNamespaceAndKey(String namespace, String key, Map pagination) { @@ -151,4 +152,9 @@ class ReferenceDataValueService implements DomainService { List findAllByMetadataNamespace(String namespace, Map pagination) { ReferenceDataValue.byMetadataNamespace(namespace).list(pagination) } + + @Override + ReferenceDataValue findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + ReferenceDataValue.byReferenceDataModelId(parentId).eq('rowNumber', pathIdentifier.toInteger()).get() + } } \ No newline at end of file diff --git a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/Term.groovy b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/Term.groovy index ddf5fd81a6..d0bcee1754 100644 --- a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/Term.groovy +++ b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/Term.groovy @@ -121,6 +121,11 @@ class Term implements ModelItem { Term.simpleName } + @Override + String getPathIdentifier() { + code + } + @Override String getPathPrefix() { 'tm' diff --git a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/term/TermRelationship.groovy b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/term/TermRelationship.groovy index 2781502794..37c478de46 100644 --- a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/term/TermRelationship.groovy +++ b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/item/term/TermRelationship.groovy @@ -17,7 +17,6 @@ */ package uk.ac.ox.softeng.maurodatamapper.terminology.item.term - import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation @@ -78,6 +77,11 @@ class TermRelationship implements ModelItem { 'tr' } + @Override + String getPathIdentifier() { + "${sourceTerm.code}-${relationshipType.label}-${targetTerm.code}" + } + def beforeValidate() { label = relationshipType?.label beforeValidateModelItem() @@ -181,6 +185,20 @@ class TermRelationship implements ModelItem { criteria } + static DetachedCriteria byPathIdentifierFields(String sourceTermCode, String relationshipTypeLabel, String targetTermCode) { + where { + sourceTerm { + eq 'code', sourceTermCode + } + targetTerm { + eq 'code', targetTermCode + } + relationshipType { + eq 'label', relationshipTypeLabel + } + } + } + static DetachedCriteria byTermIdIsParent(UUID termId) { by().or { and { diff --git a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy index a6abe1871b..e71b866e5a 100644 --- a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy +++ b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy @@ -35,12 +35,12 @@ import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.ModelIm import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.ObjectPatchData import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.security.basic.PublicAccessSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.terminology.item.Term import uk.ac.ox.softeng.maurodatamapper.terminology.item.TermRelationshipTypeService import uk.ac.ox.softeng.maurodatamapper.terminology.item.TermService import uk.ac.ox.softeng.maurodatamapper.terminology.item.term.TermRelationshipService import uk.ac.ox.softeng.maurodatamapper.terminology.provider.importer.CodeSetJsonImporterService +import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.Utils import uk.ac.ox.softeng.maurodatamapper.util.Version @@ -54,8 +54,8 @@ class CodeSetService extends ModelService { TermRelationshipTypeService termRelationshipTypeService TermService termService TermRelationshipService termRelationshipService - PathService pathService CodeSetJsonImporterService codeSetJsonImporterService + PathService pathService @Override CodeSet get(Serializable id) { @@ -447,14 +447,13 @@ class CodeSetService extends ModelService { //Here we check that each path does retrieve a known term. if (bindingMap.termPaths) { bindingMap.termPaths.each { - String path = it.termPath - Map pathParams = [path: path, catalogueItemDomainType: Terminology.simpleName] + Path path = Path.from(it.termPath) //pathService requires a UserSecurityPolicyManager. //Assumption is that if we got this far then it is OK to read the Terms because either (i) we came via a controller in which case //the user's ability to import a CodeSet has already been tested, or (ii) we are calling this method from a service test spec in which //case it is OK to read. - Term term = pathService.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, pathParams) + Term term = pathService.findResourceByPathFromRootClass(Terminology, path) as Term if (term) { codeSet.addToTerms(term) diff --git a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermRelationshipTypeService.groovy b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermRelationshipTypeService.groovy index 3b1c7e186d..91fd69bbb7 100644 --- a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermRelationshipTypeService.groovy +++ b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermRelationshipTypeService.groovy @@ -187,4 +187,9 @@ class TermRelationshipTypeService extends ModelItemService List findAllByMetadataNamespace(String namespace, Map pagination) { TermRelationshipType.byMetadataNamespace(namespace).list(pagination) } + + @Override + TermRelationshipType findByParentIdAndLabel(UUID parentId, String label) { + TermRelationshipType.byTerminologyId(parentId).eq('label', label).get() + } } \ No newline at end of file diff --git a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermService.groovy b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermService.groovy index 54dd0d88f6..dc6a9cf0e4 100644 --- a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermService.groovy +++ b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermService.groovy @@ -19,7 +19,6 @@ package uk.ac.ox.softeng.maurodatamapper.terminology.item import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier -import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItemService @@ -453,8 +452,21 @@ class TermService extends ModelItemService { */ @Override - Term findByParentAndLabel(CatalogueItem parentCatalogueItem, String label) { - findTerm(parentCatalogueItem, label) + Term findByParentIdAndLabel(UUID parentId, String label) { + Term term = Term.byCodeSetId(parentId).eq('label', label).get() + if (!term) { + term = Term.byTerminologyId(parentId).eq('label', label).get() + } + term + } + + @Override + Term findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + Term term = Term.byCodeSetId(parentId).eq('code', pathIdentifier).get() + if (!term) { + term = Term.byTerminologyId(parentId).eq('code', pathIdentifier).get() + } + term } @Override diff --git a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/term/TermRelationshipService.groovy b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/term/TermRelationshipService.groovy index 9ab9bb4575..6b9a6d1666 100644 --- a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/term/TermRelationshipService.groovy +++ b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/term/TermRelationshipService.groovy @@ -17,6 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.terminology.item.term +import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItemService @@ -86,7 +87,7 @@ class TermRelationshipService extends ModelItemService { log.trace('Removing {} TermRelationships', termRelationshipIds.size()) sessionFactory.currentSession - .createSQLQuery('delete from terminology.term_relationship where id in :ids') + .createSQLQuery('DELETE FROM terminology.term_relationship WHERE id IN :ids') .setParameter('ids', termRelationshipIds) .executeUpdate() @@ -203,4 +204,14 @@ class TermRelationshipService extends ModelItemService { List findAllByMetadataNamespace(String namespace, Map pagination) { TermRelationship.byMetadataNamespace(namespace).list(pagination) } + + @Override + TermRelationship findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + String[] split = pathIdentifier.split(/-/) + if (split.size() != 3) throw new ApiBadRequestException('TRS01', "TermRelationship Path identifier is invalid [${pathIdentifier}]") + TermRelationship.byPathIdentifierFields(split[0], split[1], split[2]).or { + eq 'sourceTerm.id', parentId + eq 'targetTerm.id', parentId + }.get() + } } \ No newline at end of file diff --git a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/CatalogueUser.groovy b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/CatalogueUser.groovy index 301ed94119..a34df0d37c 100644 --- a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/CatalogueUser.groovy +++ b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/CatalogueUser.groovy @@ -34,7 +34,7 @@ import java.security.Principal import java.time.OffsetDateTime @Resource(readOnly = false, formats = ['json', 'xml']) -class CatalogueUser implements Principal, EditHistoryAware, CreatorAware, User { +class CatalogueUser implements Principal, EditHistoryAware, User { UUID id String emailAddress @@ -43,7 +43,7 @@ class CatalogueUser implements Principal, EditHistoryAware, CreatorAware, User { OffsetDateTime lastLogin String organisation - @BindUsing({obj, source -> + @BindUsing({ obj, source -> SecurityUtils.getHash(source['password'] as String, obj.salt) }) byte[] password @@ -143,6 +143,11 @@ class CatalogueUser implements Principal, EditHistoryAware, CreatorAware, User { 'cu' } + @Override + String getPathIdentifier() { + emailAddress + } + boolean isDisabled() { disabled } diff --git a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/UserGroup.groovy b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/UserGroup.groovy index ef748aa62e..adbf35d242 100644 --- a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/UserGroup.groovy +++ b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/UserGroup.groovy @@ -77,6 +77,11 @@ class UserGroup implements EditHistoryAware, SecurableResource, Principal { 'ug' } + @Override + String getPathIdentifier() { + name + } + @Override Boolean getReadableByEveryone() { false diff --git a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/authentication/ApiKey.groovy b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/authentication/ApiKey.groovy index 50edf5d22e..d796100a82 100644 --- a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/authentication/ApiKey.groovy +++ b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/authentication/ApiKey.groovy @@ -53,11 +53,6 @@ class ApiKey implements CreatorAware { ApiKey.simpleName } - @Override - String getPathPrefix() { - 'ak' - } - ApiKey() { refreshable = false name = DEFAULT_NAME diff --git a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/GroupRole.groovy b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/GroupRole.groovy index a5407776c7..20da131166 100644 --- a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/GroupRole.groovy +++ b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/GroupRole.groovy @@ -119,6 +119,11 @@ class GroupRole implements EditHistoryAware, PathAware, SecurableResource, Compa 'gr' } + @Override + String getPathIdentifier() { + name + } + @Override Boolean getReadableByEveryone() { false diff --git a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/SecurableResourceGroupRole.groovy b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/SecurableResourceGroupRole.groovy index edb3c89662..8b70f5f6a3 100644 --- a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/SecurableResourceGroupRole.groovy +++ b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/SecurableResourceGroupRole.groovy @@ -76,11 +76,6 @@ class SecurableResourceGroupRole implements EditHistoryAware { SecurableResourceGroupRole.simpleName } - @Override - String getPathPrefix() { - 'srgr' - } - void setSecurableResource(SecurableResource securableResource, boolean justLoad) { this.securableResource = securableResource if (!justLoad) { diff --git a/mdm-security/grails-app/services/uk/ac/ox/softeng/maurodatamapper/security/UserGroupService.groovy b/mdm-security/grails-app/services/uk/ac/ox/softeng/maurodatamapper/security/UserGroupService.groovy index 4fbfa6df43..0cc63e1614 100644 --- a/mdm-security/grails-app/services/uk/ac/ox/softeng/maurodatamapper/security/UserGroupService.groovy +++ b/mdm-security/grails-app/services/uk/ac/ox/softeng/maurodatamapper/security/UserGroupService.groovy @@ -17,6 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.security +import uk.ac.ox.softeng.maurodatamapper.core.traits.service.DomainService import uk.ac.ox.softeng.maurodatamapper.security.role.GroupRole import uk.ac.ox.softeng.maurodatamapper.security.role.GroupRoleService @@ -24,7 +25,7 @@ import grails.gorm.transactions.Transactional import grails.validation.ValidationException @Transactional -class UserGroupService { +class UserGroupService implements DomainService { GroupRoleService groupRoleService @@ -55,13 +56,18 @@ class UserGroupService { group.delete(flush: true) } + @Override + UserGroup findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + findByName(pathIdentifier) + } + UserGroup findByName(String name) { UserGroup.findByName(name) } UserGroup createNewGroup(CatalogueUser createdBy, String name, String description = null, List members = []) { UserGroup group = new UserGroup(createdBy: createdBy.emailAddress, name: name, description: description) - members.each {group.addToGroupMembers(it)} + members.each { group.addToGroupMembers(it) } group.addToGroupMembers(createdBy) } diff --git a/mdm-security/grails-app/services/uk/ac/ox/softeng/maurodatamapper/security/role/GroupRoleService.groovy b/mdm-security/grails-app/services/uk/ac/ox/softeng/maurodatamapper/security/role/GroupRoleService.groovy index 68e089a53d..1095489ec5 100644 --- a/mdm-security/grails-app/services/uk/ac/ox/softeng/maurodatamapper/security/role/GroupRoleService.groovy +++ b/mdm-security/grails-app/services/uk/ac/ox/softeng/maurodatamapper/security/role/GroupRoleService.groovy @@ -21,6 +21,7 @@ import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException import uk.ac.ox.softeng.maurodatamapper.core.model.Container import uk.ac.ox.softeng.maurodatamapper.core.model.Model +import uk.ac.ox.softeng.maurodatamapper.core.traits.service.DomainService import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.util.Utils @@ -29,7 +30,7 @@ import org.grails.plugin.cache.GrailsCacheManager import org.springframework.cache.Cache @Transactional -class GroupRoleService { +class GroupRoleService implements DomainService { public static final String GROUP_ROLES_CACHE_NAME = 'mdmSecurityGroupRoles' @@ -39,7 +40,7 @@ class GroupRoleService { void refreshCacheGroupRoles() { grailsCacheManager.destroyCache(GROUP_ROLES_CACHE_NAME) Cache cache = grailsCacheManager.getCache(GROUP_ROLES_CACHE_NAME) - GroupRole.list().each {gr -> + GroupRole.list().each { gr -> cache.put(gr.name, new VirtualGroupRole(groupRole: gr, allowedRoles: gr.extractAllowedRoles())) } } @@ -75,6 +76,11 @@ class GroupRoleService { groupRole } + @Override + GroupRole findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + getFromCache(pathIdentifier).groupRole + } + void delete(Serializable id) { delete(get(id)) } From 33d42527cd4b5faff0f9d19897769c7f1d356d5e Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Thu, 1 Jul 2021 14:41:46 +0100 Subject: [PATCH 36/82] Update path system * Update path service to use the new path identifer methods * Update path controller to use the new path service methods * Use the path interceptor to properly check security and to map params where possible --- .../softeng/maurodatamapper/util/Path.groovy | 41 ++++++- .../maurodatamapper/util/PathNode.groovy | 8 +- .../maurodatamapper/core/UrlMappings.groovy | 32 ++--- .../core/path/PathController.groovy | 31 ++++- .../core/path/PathInterceptor.groovy | 9 ++ .../core/path/PathService.groovy | 113 ++++++++++++++++-- mdm-core/grails-app/views/path/show.gson | 24 ++-- .../core/path/PathFunctionalSpec.groovy | 4 +- 8 files changed, 213 insertions(+), 49 deletions(-) diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Path.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Path.groovy index a4c97489d1..eef58b053a 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Path.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Path.groovy @@ -17,6 +17,9 @@ */ package uk.ac.ox.softeng.maurodatamapper.util +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType + /** * @since 28/08/2020 */ @@ -30,13 +33,18 @@ class Path { List pathNodes + Path() { + + } + /* * Make a list of PathNode from the provided path string. The path string is like dm:|dc:class-label|de:element-label * which means 'The DataElement labelled element-label which belongs to the DataClass labelled class-label which * belongs to the current DataModel' * @param path The path */ - Path (String path) { + + Path(String path) { pathNodes = [] if (path) { @@ -48,4 +56,35 @@ class Path { } } + int getSize() { + pathNodes.size() + } + + PathNode getAt(int i) { + pathNodes[i] + } + + PathNode last() { + pathNodes.last() + } + + PathNode first() { + pathNodes.first() + } + + boolean isEmpty() { + pathNodes.isEmpty() + } + + void each(@DelegatesTo(List) @ClosureParams(value = SimpleType, options = 'uk.ac.ox.softeng.maurodatamapper.util.PathNode') Closure closure) { + pathNodes.each closure + } + + Path getChildPath() { + new Path(pathNodes: pathNodes[1..size - 1]) + } + + String toString() { + pathNodes.join('|') + } } diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/PathNode.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/PathNode.groovy index 4ecf504019..b0051a0862 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/PathNode.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/PathNode.groovy @@ -44,10 +44,16 @@ class PathNode { } //If there are characters after the : then extract these as the label - if (index < node.length() -1) { + if (index < node.length() - 1) { label = node.substring(index + 1) } + } + boolean hasTypePrefix() { + typePrefix } + String toString() { + "${typePrefix}:${label}" + } } 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 bdb127627d..e74b375bf8 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 @@ -103,7 +103,7 @@ class UrlMappings { post '/search'(controller: 'versionedFolder', action: 'search') get '/search'(controller: 'versionedFolder', action: 'search') - + get "/commonAncestor/$otherVersionedFolderId"(controller: 'VersionedFolder', action: 'commonAncestor') get '/latestFinalisedModel'(controller: 'VersionedFolder', action: 'latestFinalisedModel') get '/latestModelVersion'(controller: 'VersionedFolder', action: 'latestModelVersion') @@ -166,11 +166,6 @@ class UrlMappings { */ '/referenceFiles'(resources: 'referenceFile', excludes: DEFAULT_EXCLUDES) - /* - Get Catalogue Item by path where is ID of top Catalogue Item is provided - */ - get "/path/$path"(controller: 'path', action: 'show') - /* Rules */ @@ -217,10 +212,24 @@ class UrlMappings { } } + + group "/$resourceDomainType/$resourceId", { + /* + Edits + */ + get '/edits'(controller: 'edit', action: 'index') + } + + group "/$securableResourceDomainType/$securableResourceId", { + /* + Get resource by path where securableResourceId is the parent resource containing the path + */ + get "/path/$path"(controller: 'path', action: 'show') + } /* - Edits - */ - get "/$resourceDomainType/$resourceId/edits"(controller: 'edit', action: 'index') + Get by path where is ID of top resource is not provided + */ + get "/$securableResourceDomainType/path/$path"(controller: 'path', action: 'show') /* Changelogs @@ -228,11 +237,6 @@ class UrlMappings { get "/$resourceDomainType/$resourceId/changelogs"(controller: 'changelog', action: 'index') post "/$resourceDomainType/$resourceId/changelogs"(controller: 'changelog', action: 'save') - /* - Get Catalogue Item by path where is ID of top Catalogue Item is not provided - */ - get "/$catalogueItemDomainType/path/$path"(controller: 'path', action: 'show') - /* Tree */ diff --git a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathController.groovy b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathController.groovy index c3e5360708..6916c9eca4 100644 --- a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathController.groovy +++ b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathController.groovy @@ -17,9 +17,12 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.path +import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.traits.controller.MdmController +import uk.ac.ox.softeng.maurodatamapper.security.SecurableResource import uk.ac.ox.softeng.maurodatamapper.security.SecurityPolicyManagerService +import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware import grails.rest.RestfulController import org.springframework.beans.factory.annotation.Autowired @@ -38,12 +41,30 @@ class PathController extends RestfulController implements MdmCont } def show() { - CatalogueItem catalogueItem = pathService.findCatalogueItemByPath(currentUserSecurityPolicyManager, params) - if (!catalogueItem) return notFound(CatalogueItem, params.path) + CreatorAware pathedResource + if (params.securableResourceId) { + SecurableResource resource = pathService.findSecurableResourceByDomainClassAndId(params.securableResourceClass, + params.securableResourceId) - respond(catalogueItem, [model: [userSecurityPolicyManager: currentUserSecurityPolicyManager, - catalogueItem : catalogueItem], - view : 'show']) + if (!resource) { + return notFound(params.securableResourceClass, params.securableResourceId) + } + + if (!(resource instanceof CreatorAware)) { + throw new ApiBadRequestException('PC01', "[${params.securableResourceDomainType}] is not a pathable resource") + } + + // Permissions have been checked as part of the interceptor + pathedResource = pathService.findResourceByPathFromRootResource(resource as CreatorAware, params.path) + } else { + pathedResource = pathService.findResourceByPathFromRootClass(params.securableResourceClass, params.path) + } + + if (!pathedResource) return notFound(CreatorAware, params.path) + + respond(pathedResource, [model: [userSecurityPolicyManager: currentUserSecurityPolicyManager, + pathedResource : pathedResource], + view : 'show']) } def listAllPrefixMappings() { diff --git a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy index 84efcf1c04..fb8fbe9ac4 100644 --- a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy +++ b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy @@ -18,10 +18,19 @@ package uk.ac.ox.softeng.maurodatamapper.core.path import uk.ac.ox.softeng.maurodatamapper.core.traits.controller.MdmInterceptor +import uk.ac.ox.softeng.maurodatamapper.util.Path class PathInterceptor implements MdmInterceptor { boolean before() { + mapDomainTypeToClass('securableResource', true) + params.path = new Path(params.path) + + if (params.containsKey('securableResourceId')) { + return currentUserSecurityPolicyManager.userCanReadSecuredResourceId(params.securableResourceClass, params.securableResourceId) ?: + notFound(params.securableResourceClass, params.securableResourceId) + } + true } } \ No newline at end of file diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy index 499369c4ed..20946c3d79 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy @@ -17,18 +17,25 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.path +import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItemService import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem import uk.ac.ox.softeng.maurodatamapper.core.model.ModelService +import uk.ac.ox.softeng.maurodatamapper.core.traits.service.DomainService +import uk.ac.ox.softeng.maurodatamapper.security.SecurableResource +import uk.ac.ox.softeng.maurodatamapper.security.SecurableResourceService import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.PathNode +import uk.ac.ox.softeng.maurodatamapper.util.Utils import grails.core.GrailsApplication +import grails.core.GrailsClass import grails.gorm.transactions.Transactional +import grails.web.servlet.mvc.GrailsParameterMap import groovy.util.logging.Slf4j import org.grails.core.artefact.DomainClassArtefactHandler import org.grails.orm.hibernate.proxy.HibernateProxyHandler @@ -41,29 +48,102 @@ class PathService { @Autowired(required = false) List catalogueItemServices + @Autowired(required = false) + List domainServices + + @Autowired(required = false) + List securableResourceServices + GrailsApplication grailsApplication private static HibernateProxyHandler proxyHandler = new HibernateProxyHandler() + SecurableResource findSecurableResourceByDomainClassAndId(Class resourceClass, UUID resourceId) { + SecurableResourceService securableResourceService = securableResourceServices.find { it.handles(resourceClass) } + if (!securableResourceService) throw new ApiBadRequestException('PS03', "No service available to handle [${resourceClass.simpleName}]") + securableResourceService.get(resourceId) + } + Map listAllPrefixMappings() { grailsApplication.getArtefacts(DomainClassArtefactHandler.TYPE) - .findAll {CreatorAware.isAssignableFrom(it.clazz) && !it.isAbstract()} - .collectEntries {grailsClass -> - CreatorAware domain = grailsClass.newInstance() as CreatorAware - [domain.pathPrefix, domain.domainType] - }.sort() + .findAll { CreatorAware.isAssignableFrom(it.clazz) && !it.isAbstract() } + .collectEntries { grailsClass -> + def domain = grailsClass.newInstance() + // Allow unqualified path domains to exist without breaking the system + if (domain instanceof CreatorAware && domain.pathPrefix) { + [domain.pathPrefix, domain.domainType] + } + null + }.findAll().sort() as Map + } + + CreatorAware findResourceByPathFromRootResource(CreatorAware rootResourceOfPath, Path path) { + if (path.isEmpty()) { + throw new ApiBadRequestException('PS06', 'Must have a path to search') + } + + if (path.first().label != rootResourceOfPath.pathIdentifier) { + throw new ApiBadRequestException('PS01', 'Path cannot exist inside resource as first path node is not the resource node') + } + // Confirmed the path is inside the model + // If only one node then return the model + if (path.size == 1) return rootResourceOfPath as CreatorAware + + // Only 2 nodes in path, first is model + // Last part of path is a field access as has no type prefix so return the model + if (path.size == 2 && !path.last().hasTypePrefix()) return rootResourceOfPath as CreatorAware + + // Find the first child in the path + Path childPath = path.childPath + PathNode childNode = childPath.first() + + DomainService domainService = domainServices.find { service -> + service.handlesPathPrefix(childNode.typePrefix) + } + + log.debug('Found service [{}] to handle [{}]', domainService.class.simpleName, childNode.typePrefix) + CreatorAware child = domainService.findByParentIdAndPathIdentifier(rootResourceOfPath.id, childNode.label) + + if (!child) { + log.debug("Child [${childNode}] does not exist in path [${path}]") + throw new ApiBadRequestException('PS02', "Child [${childNode}] in path cannot be found inside domain") + } + + // Recurse down the path for that child + findResourceByPathFromRootResource(child, childPath) } + CreatorAware findResourceByPathFromRootClass(Class rootClass, Path path) { + if (path.isEmpty()) { + throw new ApiBadRequestException('PS05', 'Must have a path to search') + } + + PathNode rootNode = path.first() + + SecurableResourceService securableResourceService = securableResourceServices.find { it.handles(rootClass) } + if (!securableResourceService) { + throw new ApiBadRequestException('PS03', "No service available to handle [${rootClass.simpleName}]") + } + if (!(securableResourceService instanceof DomainService)) { + throw new ApiBadRequestException('PS04', "[${rootClass.simpleName}] is not a pathable resource") + } + + CreatorAware rootResource = securableResourceService.findByParentIdAndPathIdentifier(null, rootNode.label) + if (!rootResource) return null + findResourceByPathFromRootResource(rootResource, path) + } + + @Deprecated CatalogueItem findCatalogueItemByPath(UserSecurityPolicyManager userSecurityPolicyManager, Map params) { Path path = new Path(params.path) - CatalogueItemService service = catalogueItemServices.find {it.handles(params.catalogueItemDomainType)} + CatalogueItemService service = catalogueItemServices.find { it.handles(params.catalogueItemDomainType) } CatalogueItem catalogueItem /* Iterate over nodes in the path */ boolean first = true - path.pathNodes.each {PathNode node -> + path.pathNodes.each { PathNode node -> /* On first iteration, if params.catalogueItemId is provided then use this to get the top CatalogueItem by ID. Else if the service handles the typePrefix then use this service to find the top CatalogueItem by label. @@ -83,7 +163,8 @@ class PathService { } /* - Only return anything if the first item retrieved is a model which is securable and readable, or it belongs to a model which is securable and readable + Only return anything if the first item retrieved is a model which is securable and readable, or it belongs to a model which is + securable and readable */ boolean readable = false if (catalogueItem instanceof Model) { @@ -102,7 +183,7 @@ class PathService { } else { if (catalogueItem) { //Try to find the child of this catalogue item by prefix and label - service = catalogueItemServices.find {it.handlesPathPrefix(node.typePrefix)} + service = catalogueItemServices.find { it.handlesPathPrefix(node.typePrefix) } //Use the service to find a child CatalogueItem whose parent is catalogueItem and which has the specified label //Missing method exception means the path tried to retrieve a type of parent that is not expected @@ -119,4 +200,18 @@ class PathService { catalogueItem } + GrailsClass getGrailsResource(GrailsParameterMap params, String resourceParam) { + String lookup = params[resourceParam] + + if (!lookup) { + throw new ApiBadRequestException('MCI01', "No domain class resource provided") + } + + GrailsClass grailsClass = Utils.lookupGrailsDomain(grailsApplication, lookup) + if (!grailsClass) { + throw new ApiBadRequestException('MCI02', "Unrecognised domain class resource [${params[resourceParam]}]") + } + grailsClass + } + } diff --git a/mdm-core/grails-app/views/path/show.gson b/mdm-core/grails-app/views/path/show.gson index a893536139..2e1feb10cb 100644 --- a/mdm-core/grails-app/views/path/show.gson +++ b/mdm-core/grails-app/views/path/show.gson @@ -1,26 +1,16 @@ -import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem -import uk.ac.ox.softeng.maurodatamapper.core.model.Model -import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager +import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware import java.beans.Introspector model { - CatalogueItem catalogueItem + CreatorAware pathedResource UserSecurityPolicyManager userSecurityPolicyManager } -//Path controller, service and view are in mdm-core and need to return details of classes which are -//unknown because they are defined in plugin projects. -//For example, when rendering an EnumerationType, it is required to display EnumerationValues, -//but this cannot be easily done from within a CatalogueItem. -//(Possibilities in this example would be g.render(catalogueItem, [deep: true])) which renders too much -//information, or g.render(catalogueItem, [expand:['enumerationValues']]) which requires knowledge of the properties -//to be rendered. -//So instead here we define a template based on the domainType of the catalogueItem. It is up to plugins to ensure -//that a suitable template is available. -//Note: using a template called 'path' does not work, hence 'showPath'. +String modelName = Introspector.decapitalize(pathedResource.domainType) +Map modelMap = new HashMap<>() +modelMap.userSecurityPolicyManager = userSecurityPolicyManager +modelMap.put(modelName, pathedResource) -String template = "/${Introspector.decapitalize(catalogueItem.domainType)}/showPath" -json g.render(template: template, model: [catalogueItem: catalogueItem, - userSecurityPolicyManager: userSecurityPolicyManager]) \ No newline at end of file +json g.render(model: modelMap) \ No newline at end of file diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy index 20f58de918..40f384557b 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy @@ -499,7 +499,7 @@ class PathFunctionalSpec extends FunctionalSpec { "parentDataClass": "${json-unit.matches:id}" } }''' - } + } void 'Get Terminology by path and ID when not logged in'() { String node @@ -803,7 +803,7 @@ class PathFunctionalSpec extends FunctionalSpec { verifyJsonResponse OK, getExpectedSimpleTermJson() } - void 'Get DataModel by path and ID when not logged in'() { + void 'DM01 : Get DataModel by path and ID when not logged in'() { String node //No ID From 7d28d5b73c2224b6ae624f3a68b14c9e08da4f68 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Tue, 13 Jul 2021 13:26:11 +0100 Subject: [PATCH 37/82] Update all views to use the more friendly "full" qualifier We want to be able to resolve the full templates in a more reliable way. There are qualifiers in template resolution, therefore if we rename all the templates as such then we gain access to the use of the "full" qualifier --- ...ogueItem.gson => _catalogueItem_full.gson} | 1 - ...lClassifier.gson => _classifier_full.gson} | 0 .../grails-app/views/classifier/show.gson | 2 +- .../grails-app/views/classifier/update.gson | 2 +- .../{_fullFolder.gson => _folder_full.gson} | 0 mdm-core/grails-app/views/folder/show.gson | 2 +- mdm-core/grails-app/views/folder/update.gson | 2 +- mdm-core/grails-app/views/path/show.gson | 20 ++++++++++++++----- ...Folder.gson => _versionedFolder_full.gson} | 2 +- .../views/versionedFolder/show.gson | 2 +- .../views/versionedFolder/update.gson | 2 +- .../model/CatalogueItemRenderingSpec.groovy | 12 +++++------ ...ent.gson => _dataClassComponent_full.gson} | 8 ++++---- .../views/dataClassComponent/show.gson | 2 +- .../views/dataClassComponent/update.gson | 2 +- ...t.gson => _dataElementComponent_full.gson} | 8 ++++---- .../views/dataElementComponent/show.gson | 2 +- .../views/dataElementComponent/update.gson | 2 +- .../views/dataFlow/_dataFlow_full.gson | 19 ++++++++++++++++++ .../views/dataFlow/_fullDataFlow.gson | 19 ------------------ .../grails-app/views/dataFlow/show.gson | 2 +- .../grails-app/views/dataFlow/update.gson | 2 +- ...ullDataClass.gson => _dataClass_full.gson} | 8 ++++---- .../views/dataClass/_deepDataClass.gson | 4 ++-- .../grails-app/views/dataClass/_showPath.gson | 13 ------------ .../grails-app/views/dataClass/show.gson | 2 +- .../grails-app/views/dataClass/update.gson | 2 +- ...ataElement.gson => _dataElement_full.gson} | 8 ++++---- .../views/dataElement/_showPath.gson | 13 ------------ .../grails-app/views/dataElement/show.gson | 2 +- .../grails-app/views/dataElement/update.gson | 2 +- ...ullDataModel.gson => _dataModel_full.gson} | 4 ++-- .../grails-app/views/dataModel/_showPath.gson | 13 ------------ .../grails-app/views/dataModel/hierarchy.gson | 10 +++++----- .../grails-app/views/dataModel/show.gson | 2 +- .../grails-app/views/dataModel/update.gson | 2 +- ..._fullDataType.gson => _dataType_full.gson} | 8 ++++---- .../grails-app/views/dataType/show.gson | 2 +- .../grails-app/views/dataType/update.gson | 2 +- .../enumerationType/_enumerationType.gson | 11 ++++++++++ ...owPath.gson => _enumerationType_full.gson} | 4 ++-- .../views/primitiveType/_primitiveType.gson | 11 ++++++++++ ...showPath.gson => _primitiveType_full.gson} | 4 ++-- .../views/referenceType/_referenceType.gson | 11 ++++++++++ ...showPath.gson => _referenceType_full.gson} | 4 ++-- ...ue.gson => _subscribedCatalogue_full.gson} | 0 .../views/subscribedCatalogue/show.gson | 2 +- ...dModel.gson => _subscribedModel_full.gson} | 0 .../views/subscribedModel/show.gson | 2 +- ...t.gson => _referenceDataElement_full.gson} | 10 +++++----- .../views/referenceDataElement/show.gson | 2 +- .../views/referenceDataElement/update.gson | 2 +- ...del.gson => _referenceDataModel_full.gson} | 4 ++-- .../views/referenceDataModel/hierarchy.gson | 10 +++++----- .../views/referenceDataModel/show.gson | 2 +- .../views/referenceDataModel/update.gson | 2 +- ...Type.gson => _referenceDataType_full.gson} | 8 ++++---- .../views/referenceDataType/show.gson | 2 +- .../views/referenceDataType/update.gson | 2 +- .../{_fullCodeSet.gson => _codeSet_full.gson} | 4 ++-- .../grails-app/views/codeSet/_showPath.gson | 13 ------------ .../grails-app/views/codeSet/show.gson | 2 +- .../grails-app/views/codeSet/update.gson | 2 +- .../grails-app/views/term/_fullTerm.gson | 18 ----------------- .../grails-app/views/term/_showPath.gson | 13 ------------ .../grails-app/views/term/_term_full.gson | 18 +++++++++++++++++ .../grails-app/views/term/show.gson | 2 +- .../grails-app/views/term/update.gson | 2 +- ...nship.gson => _termRelationship_full.gson} | 8 ++++---- .../views/termRelationship/show.gson | 2 +- .../views/termRelationship/update.gson | 2 +- .../_fullTermRelationshipType.gson | 17 ---------------- .../_termRelationshipType_full.gson | 17 ++++++++++++++++ .../views/termRelationshipType/show.gson | 2 +- .../views/termRelationshipType/update.gson | 2 +- .../views/terminology/_showPath.gson | 13 ------------ ...erminology.gson => _terminology_full.gson} | 4 ++-- .../grails-app/views/terminology/show.gson | 2 +- .../grails-app/views/terminology/update.gson | 2 +- ...ogueUser.gson => _catalogueUser_full.gson} | 0 .../grails-app/views/catalogueUser/index.gson | 2 +- .../views/catalogueUser/pending.gson | 2 +- .../grails-app/views/catalogueUser/show.gson | 2 +- .../views/catalogueUser/update.gson | 2 +- ...ullGroupRole.gson => _groupRole_full.gson} | 0 .../grails-app/views/groupRole/index.gson | 2 +- ...roupRolesAvailableToSecurableResource.gson | 2 +- .../grails-app/views/groupRole/show.gson | 2 +- .../grails-app/views/groupRole/update.gson | 2 +- ...ullUserGroup.gson => _userGroup_full.gson} | 0 .../grails-app/views/userGroup/index.gson | 2 +- .../grails-app/views/userGroup/show.gson | 2 +- .../grails-app/views/userGroup/update.gson | 2 +- 93 files changed, 217 insertions(+), 253 deletions(-) rename mdm-core/grails-app/views/catalogueItem/{_fullCatalogueItem.gson => _catalogueItem_full.gson} (93%) rename mdm-core/grails-app/views/classifier/{_fullClassifier.gson => _classifier_full.gson} (100%) rename mdm-core/grails-app/views/folder/{_fullFolder.gson => _folder_full.gson} (100%) rename mdm-core/grails-app/views/versionedFolder/{_fullVersionedFolder.gson => _versionedFolder_full.gson} (85%) rename mdm-plugin-dataflow/grails-app/views/dataClassComponent/{_fullDataClassComponent.gson => _dataClassComponent_full.gson} (54%) rename mdm-plugin-dataflow/grails-app/views/dataElementComponent/{_fullDataElementComponent.gson => _dataElementComponent_full.gson} (58%) create mode 100644 mdm-plugin-dataflow/grails-app/views/dataFlow/_dataFlow_full.gson delete mode 100644 mdm-plugin-dataflow/grails-app/views/dataFlow/_fullDataFlow.gson rename mdm-plugin-datamodel/grails-app/views/dataClass/{_fullDataClass.gson => _dataClass_full.gson} (59%) delete mode 100644 mdm-plugin-datamodel/grails-app/views/dataClass/_showPath.gson rename mdm-plugin-datamodel/grails-app/views/dataElement/{_fullDataElement.gson => _dataElement_full.gson} (56%) delete mode 100644 mdm-plugin-datamodel/grails-app/views/dataElement/_showPath.gson rename mdm-plugin-datamodel/grails-app/views/dataModel/{_fullDataModel.gson => _dataModel_full.gson} (81%) delete mode 100644 mdm-plugin-datamodel/grails-app/views/dataModel/_showPath.gson rename mdm-plugin-datamodel/grails-app/views/dataType/{_fullDataType.gson => _dataType_full.gson} (74%) create mode 100644 mdm-plugin-datamodel/grails-app/views/enumerationType/_enumerationType.gson rename mdm-plugin-datamodel/grails-app/views/enumerationType/{_showPath.gson => _enumerationType_full.gson} (57%) create mode 100644 mdm-plugin-datamodel/grails-app/views/primitiveType/_primitiveType.gson rename mdm-plugin-datamodel/grails-app/views/primitiveType/{_showPath.gson => _primitiveType_full.gson} (58%) create mode 100644 mdm-plugin-datamodel/grails-app/views/referenceType/_referenceType.gson rename mdm-plugin-datamodel/grails-app/views/referenceType/{_showPath.gson => _referenceType_full.gson} (58%) rename mdm-plugin-federation/grails-app/views/subscribedCatalogue/{_fullSubscribedCatalogue.gson => _subscribedCatalogue_full.gson} (100%) rename mdm-plugin-federation/grails-app/views/subscribedModel/{_fullSubscribedModel.gson => _subscribedModel_full.gson} (100%) rename mdm-plugin-referencedata/grails-app/views/referenceDataElement/{_fullReferenceDataElement.gson => _referenceDataElement_full.gson} (58%) rename mdm-plugin-referencedata/grails-app/views/referenceDataModel/{_fullReferenceDataModel.gson => _referenceDataModel_full.gson} (83%) rename mdm-plugin-referencedata/grails-app/views/referenceDataType/{_fullReferenceDataType.gson => _referenceDataType_full.gson} (70%) rename mdm-plugin-terminology/grails-app/views/codeSet/{_fullCodeSet.gson => _codeSet_full.gson} (81%) delete mode 100644 mdm-plugin-terminology/grails-app/views/codeSet/_showPath.gson delete mode 100644 mdm-plugin-terminology/grails-app/views/term/_fullTerm.gson delete mode 100644 mdm-plugin-terminology/grails-app/views/term/_showPath.gson create mode 100644 mdm-plugin-terminology/grails-app/views/term/_term_full.gson rename mdm-plugin-terminology/grails-app/views/termRelationship/{_fullTermRelationship.gson => _termRelationship_full.gson} (52%) delete mode 100644 mdm-plugin-terminology/grails-app/views/termRelationshipType/_fullTermRelationshipType.gson create mode 100644 mdm-plugin-terminology/grails-app/views/termRelationshipType/_termRelationshipType_full.gson delete mode 100644 mdm-plugin-terminology/grails-app/views/terminology/_showPath.gson rename mdm-plugin-terminology/grails-app/views/terminology/{_fullTerminology.gson => _terminology_full.gson} (82%) rename mdm-security/grails-app/views/catalogueUser/{_fullCatalogueUser.gson => _catalogueUser_full.gson} (100%) rename mdm-security/grails-app/views/groupRole/{_fullGroupRole.gson => _groupRole_full.gson} (100%) rename mdm-security/grails-app/views/userGroup/{_fullUserGroup.gson => _userGroup_full.gson} (100%) diff --git a/mdm-core/grails-app/views/catalogueItem/_fullCatalogueItem.gson b/mdm-core/grails-app/views/catalogueItem/_catalogueItem_full.gson similarity index 93% rename from mdm-core/grails-app/views/catalogueItem/_fullCatalogueItem.gson rename to mdm-core/grails-app/views/catalogueItem/_catalogueItem_full.gson index df620cf70e..c15ef6c910 100644 --- a/mdm-core/grails-app/views/catalogueItem/_fullCatalogueItem.gson +++ b/mdm-core/grails-app/views/catalogueItem/_catalogueItem_full.gson @@ -1,4 +1,3 @@ -import uk.ac.ox.softeng.maurodatamapper.core.container.Folder import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager diff --git a/mdm-core/grails-app/views/classifier/_fullClassifier.gson b/mdm-core/grails-app/views/classifier/_classifier_full.gson similarity index 100% rename from mdm-core/grails-app/views/classifier/_fullClassifier.gson rename to mdm-core/grails-app/views/classifier/_classifier_full.gson diff --git a/mdm-core/grails-app/views/classifier/show.gson b/mdm-core/grails-app/views/classifier/show.gson index b1393cff13..747086801b 100644 --- a/mdm-core/grails-app/views/classifier/show.gson +++ b/mdm-core/grails-app/views/classifier/show.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullClassifier(classifier: classifier, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.classifier_full(classifier: classifier, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-core/grails-app/views/classifier/update.gson b/mdm-core/grails-app/views/classifier/update.gson index b1393cff13..747086801b 100644 --- a/mdm-core/grails-app/views/classifier/update.gson +++ b/mdm-core/grails-app/views/classifier/update.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullClassifier(classifier: classifier, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.classifier_full(classifier: classifier, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-core/grails-app/views/folder/_fullFolder.gson b/mdm-core/grails-app/views/folder/_folder_full.gson similarity index 100% rename from mdm-core/grails-app/views/folder/_fullFolder.gson rename to mdm-core/grails-app/views/folder/_folder_full.gson diff --git a/mdm-core/grails-app/views/folder/show.gson b/mdm-core/grails-app/views/folder/show.gson index cedef0bfe5..eafb3bc401 100644 --- a/mdm-core/grails-app/views/folder/show.gson +++ b/mdm-core/grails-app/views/folder/show.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullFolder(folder: folder, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.folder_full(folder: folder, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-core/grails-app/views/folder/update.gson b/mdm-core/grails-app/views/folder/update.gson index cedef0bfe5..eafb3bc401 100644 --- a/mdm-core/grails-app/views/folder/update.gson +++ b/mdm-core/grails-app/views/folder/update.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullFolder(folder: folder, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.folder_full(folder: folder, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-core/grails-app/views/path/show.gson b/mdm-core/grails-app/views/path/show.gson index 2e1feb10cb..4a20c41a46 100644 --- a/mdm-core/grails-app/views/path/show.gson +++ b/mdm-core/grails-app/views/path/show.gson @@ -1,6 +1,8 @@ import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware +import grails.plugin.json.view.template.JsonViewTemplate + import java.beans.Introspector model { @@ -8,9 +10,17 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -String modelName = Introspector.decapitalize(pathedResource.domainType) -Map modelMap = new HashMap<>() -modelMap.userSecurityPolicyManager = userSecurityPolicyManager -modelMap.put(modelName, pathedResource) +def template = (JsonViewTemplate) templateEngine.resolveTemplate(pathedResource.class, locale, 'full') + +// If we find a "full" template then use that otherwise use the standard grails template resolution whcih should find the "basic" template we've defined +if (template) { + String modelName = Introspector.decapitalize(pathedResource.domainType) + Map modelMap = new HashMap<>() + modelMap.userSecurityPolicyManager = userSecurityPolicyManager + modelMap[modelName] = pathedResource -json g.render(model: modelMap) \ No newline at end of file + String templatePath = template.templatePath.find(/(.+?\/)_(.+?).gson/) {match, pp, tp -> "$pp$tp"} + json g.render(template: templatePath, model: modelMap) +} else { + json g.render(pathedResource) +} diff --git a/mdm-core/grails-app/views/versionedFolder/_fullVersionedFolder.gson b/mdm-core/grails-app/views/versionedFolder/_versionedFolder_full.gson similarity index 85% rename from mdm-core/grails-app/views/versionedFolder/_fullVersionedFolder.gson rename to mdm-core/grails-app/views/versionedFolder/_versionedFolder_full.gson index 7c71b08afc..12ffa90393 100644 --- a/mdm-core/grails-app/views/versionedFolder/_fullVersionedFolder.gson +++ b/mdm-core/grails-app/views/versionedFolder/_versionedFolder_full.gson @@ -1,7 +1,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.container.VersionedFolder import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -inherits(template: '/folder/fullFolder', model: [folder: versionedFolder, userSecurityPolicyManager: userSecurityPolicyManager]) +inherits(template: '/folder/folder_full', model: [folder: versionedFolder, userSecurityPolicyManager: userSecurityPolicyManager]) model { VersionedFolder versionedFolder diff --git a/mdm-core/grails-app/views/versionedFolder/show.gson b/mdm-core/grails-app/views/versionedFolder/show.gson index 8f8a6c4940..4a0c2edc7c 100644 --- a/mdm-core/grails-app/views/versionedFolder/show.gson +++ b/mdm-core/grails-app/views/versionedFolder/show.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullVersionedFolder(versionedFolder: versionedFolder, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.versionedFolder_full(versionedFolder: versionedFolder, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-core/grails-app/views/versionedFolder/update.gson b/mdm-core/grails-app/views/versionedFolder/update.gson index 8f8a6c4940..4a0c2edc7c 100644 --- a/mdm-core/grails-app/views/versionedFolder/update.gson +++ b/mdm-core/grails-app/views/versionedFolder/update.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullVersionedFolder(versionedFolder: versionedFolder, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.versionedFolder_full(versionedFolder: versionedFolder, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemRenderingSpec.groovy b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemRenderingSpec.groovy index d69a931099..779712c370 100644 --- a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemRenderingSpec.groovy +++ b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemRenderingSpec.groovy @@ -124,9 +124,9 @@ class CatalogueItemRenderingSpec extends BaseUnitSpec implements JsonViewTest, J void 'test rendering model full catalogue item'() { when: - def json = render(template: "/catalogueItem/fullCatalogueItem", model: [catalogueItem : basicModel, - userSecurityPolicyManager: - PublicAccessSecurityPolicyManager.instance]) + def json = render(template: "/catalogueItem/catalogueItem_full", model: [catalogueItem : basicModel, + userSecurityPolicyManager: + PublicAccessSecurityPolicyManager.instance]) then: verifyJson('''{ @@ -141,9 +141,9 @@ class CatalogueItemRenderingSpec extends BaseUnitSpec implements JsonViewTest, J void 'test rendering of model item full catalogue item'() { when: - def json = render(template: "/catalogueItem/fullCatalogueItem", model: [catalogueItem : basicModelItem, - userSecurityPolicyManager: - PublicAccessSecurityPolicyManager.instance]) + def json = render(template: "/catalogueItem/catalogueItem_full", model: [catalogueItem : basicModelItem, + userSecurityPolicyManager: + PublicAccessSecurityPolicyManager.instance]) then: verifyJson('''{ diff --git a/mdm-plugin-dataflow/grails-app/views/dataClassComponent/_fullDataClassComponent.gson b/mdm-plugin-dataflow/grails-app/views/dataClassComponent/_dataClassComponent_full.gson similarity index 54% rename from mdm-plugin-dataflow/grails-app/views/dataClassComponent/_fullDataClassComponent.gson rename to mdm-plugin-dataflow/grails-app/views/dataClassComponent/_dataClassComponent_full.gson index 839a394dac..e4b3484e6d 100644 --- a/mdm-plugin-dataflow/grails-app/views/dataClassComponent/_fullDataClassComponent.gson +++ b/mdm-plugin-dataflow/grails-app/views/dataClassComponent/_dataClassComponent_full.gson @@ -2,10 +2,10 @@ import uk.ac.ox.softeng.maurodatamapper.dataflow.component.DataClassComponent import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -inherits template: '/catalogueItem/fullCatalogueItem', model: [catalogueItem : dataClassComponent, - userSecurityPolicyManager : userSecurityPolicyManager, - owningSecurableResourceId : dataClassComponent.model.id, - owningSecurableResourceClass: DataModel] +inherits template: '/catalogueItem/catalogueItem_full', model: [catalogueItem : dataClassComponent, + userSecurityPolicyManager : userSecurityPolicyManager, + owningSecurableResourceId : dataClassComponent.model.id, + owningSecurableResourceClass: DataModel] model { DataClassComponent dataClassComponent UserSecurityPolicyManager userSecurityPolicyManager diff --git a/mdm-plugin-dataflow/grails-app/views/dataClassComponent/show.gson b/mdm-plugin-dataflow/grails-app/views/dataClassComponent/show.gson index e3293c8a24..e2c1f89ca8 100644 --- a/mdm-plugin-dataflow/grails-app/views/dataClassComponent/show.gson +++ b/mdm-plugin-dataflow/grails-app/views/dataClassComponent/show.gson @@ -7,4 +7,4 @@ model { } -json tmpl.fullDataClassComponent(dataClassComponent: dataClassComponent, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.dataClassComponent_full(dataClassComponent: dataClassComponent, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-dataflow/grails-app/views/dataClassComponent/update.gson b/mdm-plugin-dataflow/grails-app/views/dataClassComponent/update.gson index e3293c8a24..e2c1f89ca8 100644 --- a/mdm-plugin-dataflow/grails-app/views/dataClassComponent/update.gson +++ b/mdm-plugin-dataflow/grails-app/views/dataClassComponent/update.gson @@ -7,4 +7,4 @@ model { } -json tmpl.fullDataClassComponent(dataClassComponent: dataClassComponent, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.dataClassComponent_full(dataClassComponent: dataClassComponent, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-dataflow/grails-app/views/dataElementComponent/_fullDataElementComponent.gson b/mdm-plugin-dataflow/grails-app/views/dataElementComponent/_dataElementComponent_full.gson similarity index 58% rename from mdm-plugin-dataflow/grails-app/views/dataElementComponent/_fullDataElementComponent.gson rename to mdm-plugin-dataflow/grails-app/views/dataElementComponent/_dataElementComponent_full.gson index c4ac768f19..fb3ec5c683 100644 --- a/mdm-plugin-dataflow/grails-app/views/dataElementComponent/_fullDataElementComponent.gson +++ b/mdm-plugin-dataflow/grails-app/views/dataElementComponent/_dataElementComponent_full.gson @@ -2,10 +2,10 @@ import uk.ac.ox.softeng.maurodatamapper.dataflow.component.DataElementComponent import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -inherits template: '/catalogueItem/fullCatalogueItem', model: [catalogueItem : dataElementComponent, - userSecurityPolicyManager : userSecurityPolicyManager, - owningSecurableResourceId : dataElementComponent.model.id, - owningSecurableResourceClass: DataModel] +inherits template: '/catalogueItem/catalogueItem_full', model: [catalogueItem : dataElementComponent, + userSecurityPolicyManager : userSecurityPolicyManager, + owningSecurableResourceId : dataElementComponent.model.id, + owningSecurableResourceClass: DataModel] model { DataElementComponent dataElementComponent UserSecurityPolicyManager userSecurityPolicyManager diff --git a/mdm-plugin-dataflow/grails-app/views/dataElementComponent/show.gson b/mdm-plugin-dataflow/grails-app/views/dataElementComponent/show.gson index eaac40773b..a613f892a3 100644 --- a/mdm-plugin-dataflow/grails-app/views/dataElementComponent/show.gson +++ b/mdm-plugin-dataflow/grails-app/views/dataElementComponent/show.gson @@ -7,4 +7,4 @@ model { } -json tmpl.fullDataElementComponent(dataElementComponent: dataElementComponent, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.dataElementComponent_full(dataElementComponent: dataElementComponent, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-dataflow/grails-app/views/dataElementComponent/update.gson b/mdm-plugin-dataflow/grails-app/views/dataElementComponent/update.gson index eaac40773b..a613f892a3 100644 --- a/mdm-plugin-dataflow/grails-app/views/dataElementComponent/update.gson +++ b/mdm-plugin-dataflow/grails-app/views/dataElementComponent/update.gson @@ -7,4 +7,4 @@ model { } -json tmpl.fullDataElementComponent(dataElementComponent: dataElementComponent, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.dataElementComponent_full(dataElementComponent: dataElementComponent, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-dataflow/grails-app/views/dataFlow/_dataFlow_full.gson b/mdm-plugin-dataflow/grails-app/views/dataFlow/_dataFlow_full.gson new file mode 100644 index 0000000000..4d7f46b02e --- /dev/null +++ b/mdm-plugin-dataflow/grails-app/views/dataFlow/_dataFlow_full.gson @@ -0,0 +1,19 @@ +import uk.ac.ox.softeng.maurodatamapper.dataflow.DataFlow +import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel +import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager + +inherits template: '/catalogueItem/catalogueItem_full', model: [catalogueItem : dataFlow, + userSecurityPolicyManager : userSecurityPolicyManager, + owningSecurableResourceId : dataFlow.model.id, + owningSecurableResourceClass: DataModel] +model { + DataFlow dataFlow + UserSecurityPolicyManager userSecurityPolicyManager +} + +json { + definition dataFlow.definition + source g.render(dataFlow.source) + target g.render(dataFlow.target) + diagramLayout dataFlow.diagramLayout +} diff --git a/mdm-plugin-dataflow/grails-app/views/dataFlow/_fullDataFlow.gson b/mdm-plugin-dataflow/grails-app/views/dataFlow/_fullDataFlow.gson deleted file mode 100644 index cacff99da1..0000000000 --- a/mdm-plugin-dataflow/grails-app/views/dataFlow/_fullDataFlow.gson +++ /dev/null @@ -1,19 +0,0 @@ -import uk.ac.ox.softeng.maurodatamapper.dataflow.DataFlow -import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel -import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager - -inherits template: '/catalogueItem/fullCatalogueItem', model: [catalogueItem : dataFlow, - userSecurityPolicyManager : userSecurityPolicyManager, - owningSecurableResourceId : dataFlow.model.id, - owningSecurableResourceClass: DataModel] -model { - DataFlow dataFlow - UserSecurityPolicyManager userSecurityPolicyManager -} - -json { - definition dataFlow.definition - source g.render(dataFlow.source) - target g.render(dataFlow.target) - diagramLayout dataFlow.diagramLayout -} diff --git a/mdm-plugin-dataflow/grails-app/views/dataFlow/show.gson b/mdm-plugin-dataflow/grails-app/views/dataFlow/show.gson index fdde6e3883..e4e5f547de 100644 --- a/mdm-plugin-dataflow/grails-app/views/dataFlow/show.gson +++ b/mdm-plugin-dataflow/grails-app/views/dataFlow/show.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullDataFlow(dataFlow: dataFlow, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.dataFlow_full(dataFlow: dataFlow, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-dataflow/grails-app/views/dataFlow/update.gson b/mdm-plugin-dataflow/grails-app/views/dataFlow/update.gson index fdde6e3883..e4e5f547de 100644 --- a/mdm-plugin-dataflow/grails-app/views/dataFlow/update.gson +++ b/mdm-plugin-dataflow/grails-app/views/dataFlow/update.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullDataFlow(dataFlow: dataFlow, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.dataFlow_full(dataFlow: dataFlow, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-datamodel/grails-app/views/dataClass/_fullDataClass.gson b/mdm-plugin-datamodel/grails-app/views/dataClass/_dataClass_full.gson similarity index 59% rename from mdm-plugin-datamodel/grails-app/views/dataClass/_fullDataClass.gson rename to mdm-plugin-datamodel/grails-app/views/dataClass/_dataClass_full.gson index 90d583d453..fd270a6128 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataClass/_fullDataClass.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataClass/_dataClass_full.gson @@ -2,10 +2,10 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClass import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -inherits template: '/catalogueItem/fullCatalogueItem', model: [catalogueItem : dataClass, - userSecurityPolicyManager : userSecurityPolicyManager, - owningSecurableResourceId : dataClass.modelId, - owningSecurableResourceClass: DataModel] +inherits template: '/catalogueItem/catalogueItem_full', model: [catalogueItem : dataClass, + userSecurityPolicyManager : userSecurityPolicyManager, + owningSecurableResourceId : dataClass.modelId, + owningSecurableResourceClass: DataModel] model { DataClass dataClass UserSecurityPolicyManager userSecurityPolicyManager diff --git a/mdm-plugin-datamodel/grails-app/views/dataClass/_deepDataClass.gson b/mdm-plugin-datamodel/grails-app/views/dataClass/_deepDataClass.gson index 61bb6340ee..be4247ee35 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataClass/_deepDataClass.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataClass/_deepDataClass.gson @@ -1,7 +1,7 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClass import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -inherits template: '/dataClass/fullDataClass', model: [dataClass: deepDataClass, userSecurityPolicyManager: userSecurityPolicyManager] +inherits template: '/dataClass/dataClass_full', model: [dataClass: deepDataClass, userSecurityPolicyManager: userSecurityPolicyManager] model { DataClass deepDataClass UserSecurityPolicyManager userSecurityPolicyManager @@ -9,7 +9,7 @@ model { json { dataClasses tmpl.deepDataClass(deepDataClass.dataClasses ?: [], [userSecurityPolicyManager: userSecurityPolicyManager]) - dataElements tmpl.'/dataElement/fullDataElement'(deepDataClass.dataElements ?: [], [userSecurityPolicyManager: userSecurityPolicyManager]) + dataElements tmpl.'/dataElement/dataElement_full'(deepDataClass.dataElements ?: [], [userSecurityPolicyManager: userSecurityPolicyManager]) if (deepDataClass.parentDataClass) parentDataClass deepDataClass.parentDataClassId } \ No newline at end of file diff --git a/mdm-plugin-datamodel/grails-app/views/dataClass/_showPath.gson b/mdm-plugin-datamodel/grails-app/views/dataClass/_showPath.gson deleted file mode 100644 index 1f64b7a7ce..0000000000 --- a/mdm-plugin-datamodel/grails-app/views/dataClass/_showPath.gson +++ /dev/null @@ -1,13 +0,0 @@ -import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClass -import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager - -inherits template: '/dataClass/fullDataClass', model: [dataClass: catalogueItem, userSecurityPolicyManager: userSecurityPolicyManager] - -model { - DataClass catalogueItem - UserSecurityPolicyManager userSecurityPolicyManager -} - -json { - -} \ No newline at end of file diff --git a/mdm-plugin-datamodel/grails-app/views/dataClass/show.gson b/mdm-plugin-datamodel/grails-app/views/dataClass/show.gson index e599aa59a3..017bc6f84d 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataClass/show.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataClass/show.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullDataClass(dataClass: dataClass, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.dataClass_full(dataClass: dataClass, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-datamodel/grails-app/views/dataClass/update.gson b/mdm-plugin-datamodel/grails-app/views/dataClass/update.gson index e599aa59a3..017bc6f84d 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataClass/update.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataClass/update.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullDataClass(dataClass: dataClass, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.dataClass_full(dataClass: dataClass, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-datamodel/grails-app/views/dataElement/_fullDataElement.gson b/mdm-plugin-datamodel/grails-app/views/dataElement/_dataElement_full.gson similarity index 56% rename from mdm-plugin-datamodel/grails-app/views/dataElement/_fullDataElement.gson rename to mdm-plugin-datamodel/grails-app/views/dataElement/_dataElement_full.gson index 5db8f8bd14..f16071c476 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataElement/_fullDataElement.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataElement/_dataElement_full.gson @@ -2,10 +2,10 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataElement import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -inherits template: '/catalogueItem/fullCatalogueItem', model: [catalogueItem : dataElement, - userSecurityPolicyManager : userSecurityPolicyManager, - owningSecurableResourceId : dataElement.modelId, - owningSecurableResourceClass: DataModel] +inherits template: '/catalogueItem/catalogueItem_full', model: [catalogueItem : dataElement, + userSecurityPolicyManager : userSecurityPolicyManager, + owningSecurableResourceId : dataElement.modelId, + owningSecurableResourceClass: DataModel] model { DataElement dataElement UserSecurityPolicyManager userSecurityPolicyManager diff --git a/mdm-plugin-datamodel/grails-app/views/dataElement/_showPath.gson b/mdm-plugin-datamodel/grails-app/views/dataElement/_showPath.gson deleted file mode 100644 index 6ecef5ee80..0000000000 --- a/mdm-plugin-datamodel/grails-app/views/dataElement/_showPath.gson +++ /dev/null @@ -1,13 +0,0 @@ -import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataElement -import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager - -inherits template: '/dataElement/fullDataElement', model: [dataElement: catalogueItem, userSecurityPolicyManager: userSecurityPolicyManager] - -model { - DataElement catalogueItem - UserSecurityPolicyManager userSecurityPolicyManager -} - -json { - -} \ No newline at end of file diff --git a/mdm-plugin-datamodel/grails-app/views/dataElement/show.gson b/mdm-plugin-datamodel/grails-app/views/dataElement/show.gson index 044e485486..33a0b7e5a4 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataElement/show.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataElement/show.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullDataElement(dataElement: dataElement, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.dataElement_full(dataElement: dataElement, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-datamodel/grails-app/views/dataElement/update.gson b/mdm-plugin-datamodel/grails-app/views/dataElement/update.gson index b878b5fd20..255d5d72a6 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataElement/update.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataElement/update.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullDataElement(dataElement: dataElement, userSecurityPolicyManager: userSecurityPolicyManager) \ No newline at end of file +json tmpl.dataElement_full(dataElement: dataElement, userSecurityPolicyManager: userSecurityPolicyManager) \ No newline at end of file diff --git a/mdm-plugin-datamodel/grails-app/views/dataModel/_fullDataModel.gson b/mdm-plugin-datamodel/grails-app/views/dataModel/_dataModel_full.gson similarity index 81% rename from mdm-plugin-datamodel/grails-app/views/dataModel/_fullDataModel.gson rename to mdm-plugin-datamodel/grails-app/views/dataModel/_dataModel_full.gson index 64335cf910..0eb0528613 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataModel/_fullDataModel.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataModel/_dataModel_full.gson @@ -1,8 +1,8 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -inherits template: '/catalogueItem/fullCatalogueItem', model: [catalogueItem : dataModel, - userSecurityPolicyManager: userSecurityPolicyManager] +inherits template: '/catalogueItem/catalogueItem_full', model: [catalogueItem : dataModel, + userSecurityPolicyManager: userSecurityPolicyManager] model { DataModel dataModel diff --git a/mdm-plugin-datamodel/grails-app/views/dataModel/_showPath.gson b/mdm-plugin-datamodel/grails-app/views/dataModel/_showPath.gson deleted file mode 100644 index b1dc472387..0000000000 --- a/mdm-plugin-datamodel/grails-app/views/dataModel/_showPath.gson +++ /dev/null @@ -1,13 +0,0 @@ -import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel -import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager - -inherits template: '/dataModel/fullDataModel', model: [dataModel: catalogueItem, userSecurityPolicyManager: userSecurityPolicyManager] - -model { - DataModel catalogueItem - UserSecurityPolicyManager userSecurityPolicyManager -} - -json { - -} \ No newline at end of file diff --git a/mdm-plugin-datamodel/grails-app/views/dataModel/hierarchy.gson b/mdm-plugin-datamodel/grails-app/views/dataModel/hierarchy.gson index 2e922e6d7d..5951aaf635 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataModel/hierarchy.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataModel/hierarchy.gson @@ -2,8 +2,8 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClass import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -inherits template: '/dataModel/fullDataModel', model: [dataModel : dataModel, - userSecurityPolicyManager: userSecurityPolicyManager] +inherits template: '/dataModel/dataModel_full', model: [dataModel : dataModel, + userSecurityPolicyManager: userSecurityPolicyManager] model { DataModel dataModel @@ -13,9 +13,9 @@ model { List dataClasses = dataModel.getChildDataClasses() json { - dataTypes tmpl.'/dataType/fullDataType'('dataType', - dataModel.getSortedDataTypes(), - [userSecurityPolicyManager: userSecurityPolicyManager] + dataTypes tmpl.'/dataType/dataType_full'('dataType', + dataModel.getSortedDataTypes(), + [userSecurityPolicyManager: userSecurityPolicyManager] ) childDataClasses tmpl.'/dataClass/deepDataClass'( dataClasses, diff --git a/mdm-plugin-datamodel/grails-app/views/dataModel/show.gson b/mdm-plugin-datamodel/grails-app/views/dataModel/show.gson index 56beb2871a..0dc0f529cd 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataModel/show.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataModel/show.gson @@ -6,5 +6,5 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullDataModel(dataModel: dataModel, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.dataModel_full(dataModel: dataModel, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-datamodel/grails-app/views/dataModel/update.gson b/mdm-plugin-datamodel/grails-app/views/dataModel/update.gson index 56beb2871a..0dc0f529cd 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataModel/update.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataModel/update.gson @@ -6,5 +6,5 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullDataModel(dataModel: dataModel, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.dataModel_full(dataModel: dataModel, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-datamodel/grails-app/views/dataType/_fullDataType.gson b/mdm-plugin-datamodel/grails-app/views/dataType/_dataType_full.gson similarity index 74% rename from mdm-plugin-datamodel/grails-app/views/dataType/_fullDataType.gson rename to mdm-plugin-datamodel/grails-app/views/dataType/_dataType_full.gson index 6c4413b800..539a90b3b3 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataType/_fullDataType.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataType/_dataType_full.gson @@ -6,10 +6,10 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.PrimitiveType import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.ReferenceType import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -inherits template: '/catalogueItem/fullCatalogueItem', model: [catalogueItem : dataType, - userSecurityPolicyManager : userSecurityPolicyManager, - owningSecurableResourceId : dataType.modelId, - owningSecurableResourceClass: DataModel] +inherits template: '/catalogueItem/catalogueItem_full', model: [catalogueItem : dataType, + userSecurityPolicyManager : userSecurityPolicyManager, + owningSecurableResourceId : dataType.modelId, + owningSecurableResourceClass: DataModel] model { DataType dataType UserSecurityPolicyManager userSecurityPolicyManager diff --git a/mdm-plugin-datamodel/grails-app/views/dataType/show.gson b/mdm-plugin-datamodel/grails-app/views/dataType/show.gson index a85bf5feea..b73eca833c 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataType/show.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataType/show.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullDataType(dataType: dataType, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.dataType_full(dataType: dataType, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-datamodel/grails-app/views/dataType/update.gson b/mdm-plugin-datamodel/grails-app/views/dataType/update.gson index a85bf5feea..b73eca833c 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataType/update.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataType/update.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullDataType(dataType: dataType, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.dataType_full(dataType: dataType, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-datamodel/grails-app/views/enumerationType/_enumerationType.gson b/mdm-plugin-datamodel/grails-app/views/enumerationType/_enumerationType.gson new file mode 100644 index 0000000000..16db488c6a --- /dev/null +++ b/mdm-plugin-datamodel/grails-app/views/enumerationType/_enumerationType.gson @@ -0,0 +1,11 @@ +import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.EnumerationType + +inherits template: '/dataType/dataType', model: [dataType: enumerationType] + +model { + EnumerationType enumerationType +} + +json { + +} \ No newline at end of file diff --git a/mdm-plugin-datamodel/grails-app/views/enumerationType/_showPath.gson b/mdm-plugin-datamodel/grails-app/views/enumerationType/_enumerationType_full.gson similarity index 57% rename from mdm-plugin-datamodel/grails-app/views/enumerationType/_showPath.gson rename to mdm-plugin-datamodel/grails-app/views/enumerationType/_enumerationType_full.gson index 2b4e9ad913..becb36dd59 100644 --- a/mdm-plugin-datamodel/grails-app/views/enumerationType/_showPath.gson +++ b/mdm-plugin-datamodel/grails-app/views/enumerationType/_enumerationType_full.gson @@ -1,10 +1,10 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.EnumerationType import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -inherits template: '/dataType/fullDataType', model: [dataType: catalogueItem, userSecurityPolicyManager: userSecurityPolicyManager] +inherits template: '/dataType/dataType_full', model: [dataType: enumerationType, userSecurityPolicyManager: userSecurityPolicyManager] model { - EnumerationType catalogueItem + EnumerationType enumerationType UserSecurityPolicyManager userSecurityPolicyManager } diff --git a/mdm-plugin-datamodel/grails-app/views/primitiveType/_primitiveType.gson b/mdm-plugin-datamodel/grails-app/views/primitiveType/_primitiveType.gson new file mode 100644 index 0000000000..f0e1af3164 --- /dev/null +++ b/mdm-plugin-datamodel/grails-app/views/primitiveType/_primitiveType.gson @@ -0,0 +1,11 @@ +import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.PrimitiveType + +inherits template: '/dataType/dataType', model: [dataType: primitiveType] + +model { + PrimitiveType primitiveType +} + +json { + +} \ No newline at end of file diff --git a/mdm-plugin-datamodel/grails-app/views/primitiveType/_showPath.gson b/mdm-plugin-datamodel/grails-app/views/primitiveType/_primitiveType_full.gson similarity index 58% rename from mdm-plugin-datamodel/grails-app/views/primitiveType/_showPath.gson rename to mdm-plugin-datamodel/grails-app/views/primitiveType/_primitiveType_full.gson index d4690b04eb..9ac494f3ad 100644 --- a/mdm-plugin-datamodel/grails-app/views/primitiveType/_showPath.gson +++ b/mdm-plugin-datamodel/grails-app/views/primitiveType/_primitiveType_full.gson @@ -1,10 +1,10 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.PrimitiveType import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -inherits template: '/dataType/fullDataType', model: [dataType: catalogueItem, userSecurityPolicyManager: userSecurityPolicyManager] +inherits template: '/dataType/dataType_full', model: [dataType: primitiveType, userSecurityPolicyManager: userSecurityPolicyManager] model { - PrimitiveType catalogueItem + PrimitiveType primitiveType UserSecurityPolicyManager userSecurityPolicyManager } diff --git a/mdm-plugin-datamodel/grails-app/views/referenceType/_referenceType.gson b/mdm-plugin-datamodel/grails-app/views/referenceType/_referenceType.gson new file mode 100644 index 0000000000..7b213d8347 --- /dev/null +++ b/mdm-plugin-datamodel/grails-app/views/referenceType/_referenceType.gson @@ -0,0 +1,11 @@ +import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.ReferenceType + +inherits template: '/dataType/dataType', model: [dataType: referenceType] + +model { + ReferenceType referenceType +} + +json { + +} \ No newline at end of file diff --git a/mdm-plugin-datamodel/grails-app/views/referenceType/_showPath.gson b/mdm-plugin-datamodel/grails-app/views/referenceType/_referenceType_full.gson similarity index 58% rename from mdm-plugin-datamodel/grails-app/views/referenceType/_showPath.gson rename to mdm-plugin-datamodel/grails-app/views/referenceType/_referenceType_full.gson index f80462f818..2a4454bee2 100644 --- a/mdm-plugin-datamodel/grails-app/views/referenceType/_showPath.gson +++ b/mdm-plugin-datamodel/grails-app/views/referenceType/_referenceType_full.gson @@ -1,10 +1,10 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.ReferenceType import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -inherits template: '/dataType/fullDataType', model: [dataType: catalogueItem, userSecurityPolicyManager: userSecurityPolicyManager] +inherits template: '/dataType/dataType_full', model: [dataType: referenceType, userSecurityPolicyManager: userSecurityPolicyManager] model { - ReferenceType catalogueItem + ReferenceType referenceType UserSecurityPolicyManager userSecurityPolicyManager } diff --git a/mdm-plugin-federation/grails-app/views/subscribedCatalogue/_fullSubscribedCatalogue.gson b/mdm-plugin-federation/grails-app/views/subscribedCatalogue/_subscribedCatalogue_full.gson similarity index 100% rename from mdm-plugin-federation/grails-app/views/subscribedCatalogue/_fullSubscribedCatalogue.gson rename to mdm-plugin-federation/grails-app/views/subscribedCatalogue/_subscribedCatalogue_full.gson diff --git a/mdm-plugin-federation/grails-app/views/subscribedCatalogue/show.gson b/mdm-plugin-federation/grails-app/views/subscribedCatalogue/show.gson index 903993c336..68089c754b 100644 --- a/mdm-plugin-federation/grails-app/views/subscribedCatalogue/show.gson +++ b/mdm-plugin-federation/grails-app/views/subscribedCatalogue/show.gson @@ -4,4 +4,4 @@ model { SubscribedCatalogue subscribedCatalogue } -json tmpl.fullSubscribedCatalogue(subscribedCatalogue) +json tmpl.subscribedCatalogue_full(subscribedCatalogue) diff --git a/mdm-plugin-federation/grails-app/views/subscribedModel/_fullSubscribedModel.gson b/mdm-plugin-federation/grails-app/views/subscribedModel/_subscribedModel_full.gson similarity index 100% rename from mdm-plugin-federation/grails-app/views/subscribedModel/_fullSubscribedModel.gson rename to mdm-plugin-federation/grails-app/views/subscribedModel/_subscribedModel_full.gson diff --git a/mdm-plugin-federation/grails-app/views/subscribedModel/show.gson b/mdm-plugin-federation/grails-app/views/subscribedModel/show.gson index 8ecedd9f67..44a71dfc70 100644 --- a/mdm-plugin-federation/grails-app/views/subscribedModel/show.gson +++ b/mdm-plugin-federation/grails-app/views/subscribedModel/show.gson @@ -4,4 +4,4 @@ model { SubscribedModel subscribedModel } -json tmpl.fullSubscribedModel(subscribedModel) \ No newline at end of file +json tmpl.subscribedModel_full(subscribedModel) \ No newline at end of file diff --git a/mdm-plugin-referencedata/grails-app/views/referenceDataElement/_fullReferenceDataElement.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataElement/_referenceDataElement_full.gson similarity index 58% rename from mdm-plugin-referencedata/grails-app/views/referenceDataElement/_fullReferenceDataElement.gson rename to mdm-plugin-referencedata/grails-app/views/referenceDataElement/_referenceDataElement_full.gson index 4dd465c25f..c4559b7821 100644 --- a/mdm-plugin-referencedata/grails-app/views/referenceDataElement/_fullReferenceDataElement.gson +++ b/mdm-plugin-referencedata/grails-app/views/referenceDataElement/_referenceDataElement_full.gson @@ -1,11 +1,11 @@ +import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModel import uk.ac.ox.softeng.maurodatamapper.referencedata.item.ReferenceDataElement import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModel -inherits template: '/catalogueItem/fullCatalogueItem', model: [catalogueItem : referenceDataElement, - userSecurityPolicyManager : userSecurityPolicyManager, - owningSecurableResourceId : referenceDataElement.modelId, - owningSecurableResourceClass: ReferenceDataModel] +inherits template: '/catalogueItem/catalogueItem_full', model: [catalogueItem : referenceDataElement, + userSecurityPolicyManager : userSecurityPolicyManager, + owningSecurableResourceId : referenceDataElement.modelId, + owningSecurableResourceClass: ReferenceDataModel] model { ReferenceDataElement referenceDataElement UserSecurityPolicyManager userSecurityPolicyManager diff --git a/mdm-plugin-referencedata/grails-app/views/referenceDataElement/show.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataElement/show.gson index 4b2baf032f..3b9a1c39fc 100644 --- a/mdm-plugin-referencedata/grails-app/views/referenceDataElement/show.gson +++ b/mdm-plugin-referencedata/grails-app/views/referenceDataElement/show.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullReferenceDataElement(referenceDataElement: referenceDataElement, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.referenceDataElement_full(referenceDataElement: referenceDataElement, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-referencedata/grails-app/views/referenceDataElement/update.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataElement/update.gson index bbd1094806..8059239d24 100644 --- a/mdm-plugin-referencedata/grails-app/views/referenceDataElement/update.gson +++ b/mdm-plugin-referencedata/grails-app/views/referenceDataElement/update.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullReferenceDataElement(referenceDataElement: referenceDataElement, userSecurityPolicyManager: userSecurityPolicyManager) \ No newline at end of file +json tmpl.referenceDataElement_full(referenceDataElement: referenceDataElement, userSecurityPolicyManager: userSecurityPolicyManager) \ No newline at end of file diff --git a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/_fullReferenceDataModel.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/_referenceDataModel_full.gson similarity index 83% rename from mdm-plugin-referencedata/grails-app/views/referenceDataModel/_fullReferenceDataModel.gson rename to mdm-plugin-referencedata/grails-app/views/referenceDataModel/_referenceDataModel_full.gson index a25a8958df..08f424e9ed 100644 --- a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/_fullReferenceDataModel.gson +++ b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/_referenceDataModel_full.gson @@ -1,8 +1,8 @@ import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModel import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -inherits template: '/catalogueItem/fullCatalogueItem', model: [catalogueItem : referenceDataModel, - userSecurityPolicyManager: userSecurityPolicyManager] +inherits template: '/catalogueItem/catalogueItem_full', model: [catalogueItem : referenceDataModel, + userSecurityPolicyManager: userSecurityPolicyManager] model { ReferenceDataModel referenceDataModel diff --git a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/hierarchy.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/hierarchy.gson index 8f79ba6630..9025b92ce0 100644 --- a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/hierarchy.gson +++ b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/hierarchy.gson @@ -1,8 +1,8 @@ import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModel import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -inherits template: '/dataModel/fullDataModel', model: [dataModel : referenceDataModel, - userSecurityPolicyManager: userSecurityPolicyManager] +inherits template: '/dataModel/dataModel_full', model: [dataModel : referenceDataModel, + userSecurityPolicyManager: userSecurityPolicyManager] model { ReferenceDataModel referenceDataModel @@ -10,9 +10,9 @@ model { } json { - dataTypes tmpl.'/dataType/fullDataType'('dataType', - referenceDataModel.getSortedReferenceDataTypes(), - [userSecurityPolicyManager: userSecurityPolicyManager] + dataTypes tmpl.'/dataType/dataType_full'('dataType', + referenceDataModel.getSortedReferenceDataTypes(), + [userSecurityPolicyManager: userSecurityPolicyManager] ) } diff --git a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/show.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/show.gson index 5f4d797442..b2f0ae7ee9 100644 --- a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/show.gson +++ b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/show.gson @@ -6,5 +6,5 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullReferenceDataModel(referenceDataModel: referenceDataModel, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.referenceDataModel_full(referenceDataModel: referenceDataModel, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/update.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/update.gson index 5f4d797442..b2f0ae7ee9 100644 --- a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/update.gson +++ b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/update.gson @@ -6,5 +6,5 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullReferenceDataModel(referenceDataModel: referenceDataModel, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.referenceDataModel_full(referenceDataModel: referenceDataModel, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-referencedata/grails-app/views/referenceDataType/_fullReferenceDataType.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataType/_referenceDataType_full.gson similarity index 70% rename from mdm-plugin-referencedata/grails-app/views/referenceDataType/_fullReferenceDataType.gson rename to mdm-plugin-referencedata/grails-app/views/referenceDataType/_referenceDataType_full.gson index 87ddfefc3f..6460838223 100644 --- a/mdm-plugin-referencedata/grails-app/views/referenceDataType/_fullReferenceDataType.gson +++ b/mdm-plugin-referencedata/grails-app/views/referenceDataType/_referenceDataType_full.gson @@ -4,10 +4,10 @@ import uk.ac.ox.softeng.maurodatamapper.referencedata.item.datatype.ReferenceEnu import uk.ac.ox.softeng.maurodatamapper.referencedata.item.datatype.ReferencePrimitiveType import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -inherits template: '/catalogueItem/fullCatalogueItem', model: [catalogueItem : referenceDataType, - userSecurityPolicyManager : userSecurityPolicyManager, - owningSecurableResourceId : referenceDataType.modelId, - owningSecurableResourceClass: ReferenceDataModel] +inherits template: '/catalogueItem/catalogueItem_full', model: [catalogueItem : referenceDataType, + userSecurityPolicyManager : userSecurityPolicyManager, + owningSecurableResourceId : referenceDataType.modelId, + owningSecurableResourceClass: ReferenceDataModel] model { ReferenceDataType referenceDataType UserSecurityPolicyManager userSecurityPolicyManager diff --git a/mdm-plugin-referencedata/grails-app/views/referenceDataType/show.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataType/show.gson index 2770fec51b..cd07fd8890 100644 --- a/mdm-plugin-referencedata/grails-app/views/referenceDataType/show.gson +++ b/mdm-plugin-referencedata/grails-app/views/referenceDataType/show.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullReferenceDataType(referenceDataType: referenceDataType, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.referenceDataType_full(referenceDataType: referenceDataType, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-referencedata/grails-app/views/referenceDataType/update.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataType/update.gson index 2770fec51b..cd07fd8890 100644 --- a/mdm-plugin-referencedata/grails-app/views/referenceDataType/update.gson +++ b/mdm-plugin-referencedata/grails-app/views/referenceDataType/update.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullReferenceDataType(referenceDataType: referenceDataType, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.referenceDataType_full(referenceDataType: referenceDataType, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-terminology/grails-app/views/codeSet/_fullCodeSet.gson b/mdm-plugin-terminology/grails-app/views/codeSet/_codeSet_full.gson similarity index 81% rename from mdm-plugin-terminology/grails-app/views/codeSet/_fullCodeSet.gson rename to mdm-plugin-terminology/grails-app/views/codeSet/_codeSet_full.gson index cdb65b8ce7..c642b67af9 100644 --- a/mdm-plugin-terminology/grails-app/views/codeSet/_fullCodeSet.gson +++ b/mdm-plugin-terminology/grails-app/views/codeSet/_codeSet_full.gson @@ -1,8 +1,8 @@ import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.terminology.CodeSet -inherits template: '/catalogueItem/fullCatalogueItem', model: [catalogueItem : codeSet, - userSecurityPolicyManager: userSecurityPolicyManager] +inherits template: '/catalogueItem/catalogueItem_full', model: [catalogueItem : codeSet, + userSecurityPolicyManager: userSecurityPolicyManager] model { CodeSet codeSet UserSecurityPolicyManager userSecurityPolicyManager diff --git a/mdm-plugin-terminology/grails-app/views/codeSet/_showPath.gson b/mdm-plugin-terminology/grails-app/views/codeSet/_showPath.gson deleted file mode 100644 index 9019063f65..0000000000 --- a/mdm-plugin-terminology/grails-app/views/codeSet/_showPath.gson +++ /dev/null @@ -1,13 +0,0 @@ -import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.terminology.CodeSet - -inherits template: '/codeSet/fullCodeSet', model: [codeSet: catalogueItem, userSecurityPolicyManager: userSecurityPolicyManager] - -model { - CodeSet catalogueItem - UserSecurityPolicyManager userSecurityPolicyManager -} - -json { - -} \ No newline at end of file diff --git a/mdm-plugin-terminology/grails-app/views/codeSet/show.gson b/mdm-plugin-terminology/grails-app/views/codeSet/show.gson index aa49f96f15..1fbb6fba78 100644 --- a/mdm-plugin-terminology/grails-app/views/codeSet/show.gson +++ b/mdm-plugin-terminology/grails-app/views/codeSet/show.gson @@ -6,5 +6,5 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullCodeSet(codeSet: codeSet, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.codeSet_full(codeSet: codeSet, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-terminology/grails-app/views/codeSet/update.gson b/mdm-plugin-terminology/grails-app/views/codeSet/update.gson index aa49f96f15..1fbb6fba78 100644 --- a/mdm-plugin-terminology/grails-app/views/codeSet/update.gson +++ b/mdm-plugin-terminology/grails-app/views/codeSet/update.gson @@ -6,5 +6,5 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullCodeSet(codeSet: codeSet, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.codeSet_full(codeSet: codeSet, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-terminology/grails-app/views/term/_fullTerm.gson b/mdm-plugin-terminology/grails-app/views/term/_fullTerm.gson deleted file mode 100644 index ff93ec460b..0000000000 --- a/mdm-plugin-terminology/grails-app/views/term/_fullTerm.gson +++ /dev/null @@ -1,18 +0,0 @@ -import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.terminology.Terminology -import uk.ac.ox.softeng.maurodatamapper.terminology.item.Term - -inherits template: '/catalogueItem/fullCatalogueItem', model: [catalogueItem : term, - userSecurityPolicyManager : userSecurityPolicyManager, - owningSecurableResourceId : term.modelId, - owningSecurableResourceClass: Terminology] -model { - Term term - UserSecurityPolicyManager userSecurityPolicyManager -} - -json { - code term.getCode() - definition term.getDefinition() - if (term.url) url term.getUrl() -} diff --git a/mdm-plugin-terminology/grails-app/views/term/_showPath.gson b/mdm-plugin-terminology/grails-app/views/term/_showPath.gson deleted file mode 100644 index ea4e67ea51..0000000000 --- a/mdm-plugin-terminology/grails-app/views/term/_showPath.gson +++ /dev/null @@ -1,13 +0,0 @@ -import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.terminology.item.Term - -inherits template: '/term/fullTerm', model: [term: catalogueItem, userSecurityPolicyManager: userSecurityPolicyManager] - -model { - Term catalogueItem - UserSecurityPolicyManager userSecurityPolicyManager -} - -json { - -} \ No newline at end of file diff --git a/mdm-plugin-terminology/grails-app/views/term/_term_full.gson b/mdm-plugin-terminology/grails-app/views/term/_term_full.gson new file mode 100644 index 0000000000..98a9103de9 --- /dev/null +++ b/mdm-plugin-terminology/grails-app/views/term/_term_full.gson @@ -0,0 +1,18 @@ +import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager +import uk.ac.ox.softeng.maurodatamapper.terminology.Terminology +import uk.ac.ox.softeng.maurodatamapper.terminology.item.Term + +inherits template: '/catalogueItem/catalogueItem_full', model: [catalogueItem : term, + userSecurityPolicyManager : userSecurityPolicyManager, + owningSecurableResourceId : term.modelId, + owningSecurableResourceClass: Terminology] +model { + Term term + UserSecurityPolicyManager userSecurityPolicyManager +} + +json { + code term.getCode() + definition term.getDefinition() + if (term.url) url term.getUrl() +} diff --git a/mdm-plugin-terminology/grails-app/views/term/show.gson b/mdm-plugin-terminology/grails-app/views/term/show.gson index 7099b0adb3..85c31b6beb 100644 --- a/mdm-plugin-terminology/grails-app/views/term/show.gson +++ b/mdm-plugin-terminology/grails-app/views/term/show.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullTerm(term: term, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.term_full(term: term, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-terminology/grails-app/views/term/update.gson b/mdm-plugin-terminology/grails-app/views/term/update.gson index 7099b0adb3..85c31b6beb 100644 --- a/mdm-plugin-terminology/grails-app/views/term/update.gson +++ b/mdm-plugin-terminology/grails-app/views/term/update.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullTerm(term: term, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.term_full(term: term, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-terminology/grails-app/views/termRelationship/_fullTermRelationship.gson b/mdm-plugin-terminology/grails-app/views/termRelationship/_termRelationship_full.gson similarity index 52% rename from mdm-plugin-terminology/grails-app/views/termRelationship/_fullTermRelationship.gson rename to mdm-plugin-terminology/grails-app/views/termRelationship/_termRelationship_full.gson index c8746c6bb3..016457869b 100644 --- a/mdm-plugin-terminology/grails-app/views/termRelationship/_fullTermRelationship.gson +++ b/mdm-plugin-terminology/grails-app/views/termRelationship/_termRelationship_full.gson @@ -2,10 +2,10 @@ import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.terminology.Terminology import uk.ac.ox.softeng.maurodatamapper.terminology.item.term.TermRelationship -inherits template: '/catalogueItem/fullCatalogueItem', model: [catalogueItem : termRelationship, - userSecurityPolicyManager : userSecurityPolicyManager, - owningSecurableResourceId : termRelationship.model.id, - owningSecurableResourceClass: Terminology] +inherits template: '/catalogueItem/catalogueItem_full', model: [catalogueItem : termRelationship, + userSecurityPolicyManager : userSecurityPolicyManager, + owningSecurableResourceId : termRelationship.model.id, + owningSecurableResourceClass: Terminology] model { TermRelationship termRelationship diff --git a/mdm-plugin-terminology/grails-app/views/termRelationship/show.gson b/mdm-plugin-terminology/grails-app/views/termRelationship/show.gson index 7153b6b1a0..cddb79a9ba 100644 --- a/mdm-plugin-terminology/grails-app/views/termRelationship/show.gson +++ b/mdm-plugin-terminology/grails-app/views/termRelationship/show.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullTermRelationship(termRelationship: termRelationship, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.termRelationship_full(termRelationship: termRelationship, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-terminology/grails-app/views/termRelationship/update.gson b/mdm-plugin-terminology/grails-app/views/termRelationship/update.gson index 7153b6b1a0..cddb79a9ba 100644 --- a/mdm-plugin-terminology/grails-app/views/termRelationship/update.gson +++ b/mdm-plugin-terminology/grails-app/views/termRelationship/update.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullTermRelationship(termRelationship: termRelationship, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.termRelationship_full(termRelationship: termRelationship, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-terminology/grails-app/views/termRelationshipType/_fullTermRelationshipType.gson b/mdm-plugin-terminology/grails-app/views/termRelationshipType/_fullTermRelationshipType.gson deleted file mode 100644 index deae69d0c7..0000000000 --- a/mdm-plugin-terminology/grails-app/views/termRelationshipType/_fullTermRelationshipType.gson +++ /dev/null @@ -1,17 +0,0 @@ -import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.terminology.Terminology -import uk.ac.ox.softeng.maurodatamapper.terminology.item.TermRelationshipType - -inherits template: '/catalogueItem/fullCatalogueItem', model: [catalogueItem : termRelationshipType, - userSecurityPolicyManager : userSecurityPolicyManager, - owningSecurableResourceId : termRelationshipType.modelId, - owningSecurableResourceClass: Terminology] - -model { - TermRelationshipType termRelationshipType - UserSecurityPolicyManager userSecurityPolicyManager -} - -json { - displayLabel termRelationshipType.displayLabel -} diff --git a/mdm-plugin-terminology/grails-app/views/termRelationshipType/_termRelationshipType_full.gson b/mdm-plugin-terminology/grails-app/views/termRelationshipType/_termRelationshipType_full.gson new file mode 100644 index 0000000000..5b06a8e168 --- /dev/null +++ b/mdm-plugin-terminology/grails-app/views/termRelationshipType/_termRelationshipType_full.gson @@ -0,0 +1,17 @@ +import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager +import uk.ac.ox.softeng.maurodatamapper.terminology.Terminology +import uk.ac.ox.softeng.maurodatamapper.terminology.item.TermRelationshipType + +inherits template: '/catalogueItem/catalogueItem_full', model: [catalogueItem : termRelationshipType, + userSecurityPolicyManager : userSecurityPolicyManager, + owningSecurableResourceId : termRelationshipType.modelId, + owningSecurableResourceClass: Terminology] + +model { + TermRelationshipType termRelationshipType + UserSecurityPolicyManager userSecurityPolicyManager +} + +json { + displayLabel termRelationshipType.displayLabel +} diff --git a/mdm-plugin-terminology/grails-app/views/termRelationshipType/show.gson b/mdm-plugin-terminology/grails-app/views/termRelationshipType/show.gson index 4eaeb799ba..343ce2e923 100644 --- a/mdm-plugin-terminology/grails-app/views/termRelationshipType/show.gson +++ b/mdm-plugin-terminology/grails-app/views/termRelationshipType/show.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullTermRelationshipType(termRelationshipType: termRelationshipType, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.termRelationshipType_full(termRelationshipType: termRelationshipType, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-terminology/grails-app/views/termRelationshipType/update.gson b/mdm-plugin-terminology/grails-app/views/termRelationshipType/update.gson index 4eaeb799ba..343ce2e923 100644 --- a/mdm-plugin-terminology/grails-app/views/termRelationshipType/update.gson +++ b/mdm-plugin-terminology/grails-app/views/termRelationshipType/update.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullTermRelationshipType(termRelationshipType: termRelationshipType, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.termRelationshipType_full(termRelationshipType: termRelationshipType, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-terminology/grails-app/views/terminology/_showPath.gson b/mdm-plugin-terminology/grails-app/views/terminology/_showPath.gson deleted file mode 100644 index 826bc8578c..0000000000 --- a/mdm-plugin-terminology/grails-app/views/terminology/_showPath.gson +++ /dev/null @@ -1,13 +0,0 @@ -import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.terminology.Terminology - -inherits template: '/terminology/fullTerminology', model: [terminology: catalogueItem, userSecurityPolicyManager: userSecurityPolicyManager] - -model { - Terminology catalogueItem - UserSecurityPolicyManager userSecurityPolicyManager -} - -json { - -} \ No newline at end of file diff --git a/mdm-plugin-terminology/grails-app/views/terminology/_fullTerminology.gson b/mdm-plugin-terminology/grails-app/views/terminology/_terminology_full.gson similarity index 82% rename from mdm-plugin-terminology/grails-app/views/terminology/_fullTerminology.gson rename to mdm-plugin-terminology/grails-app/views/terminology/_terminology_full.gson index 8b6a8f215d..6a0c6319da 100644 --- a/mdm-plugin-terminology/grails-app/views/terminology/_fullTerminology.gson +++ b/mdm-plugin-terminology/grails-app/views/terminology/_terminology_full.gson @@ -1,8 +1,8 @@ import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.terminology.Terminology -inherits template: '/catalogueItem/fullCatalogueItem', model: [catalogueItem : terminology, - userSecurityPolicyManager: userSecurityPolicyManager] +inherits template: '/catalogueItem/catalogueItem_full', model: [catalogueItem : terminology, + userSecurityPolicyManager: userSecurityPolicyManager] model { Terminology terminology diff --git a/mdm-plugin-terminology/grails-app/views/terminology/show.gson b/mdm-plugin-terminology/grails-app/views/terminology/show.gson index d037d3751d..67053f6fb4 100644 --- a/mdm-plugin-terminology/grails-app/views/terminology/show.gson +++ b/mdm-plugin-terminology/grails-app/views/terminology/show.gson @@ -6,5 +6,5 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullTerminology(terminology: terminology, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.terminology_full(terminology: terminology, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-plugin-terminology/grails-app/views/terminology/update.gson b/mdm-plugin-terminology/grails-app/views/terminology/update.gson index d037d3751d..67053f6fb4 100644 --- a/mdm-plugin-terminology/grails-app/views/terminology/update.gson +++ b/mdm-plugin-terminology/grails-app/views/terminology/update.gson @@ -6,5 +6,5 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullTerminology(terminology: terminology, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.terminology_full(terminology: terminology, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-security/grails-app/views/catalogueUser/_fullCatalogueUser.gson b/mdm-security/grails-app/views/catalogueUser/_catalogueUser_full.gson similarity index 100% rename from mdm-security/grails-app/views/catalogueUser/_fullCatalogueUser.gson rename to mdm-security/grails-app/views/catalogueUser/_catalogueUser_full.gson diff --git a/mdm-security/grails-app/views/catalogueUser/index.gson b/mdm-security/grails-app/views/catalogueUser/index.gson index 2fae998587..6e25536960 100644 --- a/mdm-security/grails-app/views/catalogueUser/index.gson +++ b/mdm-security/grails-app/views/catalogueUser/index.gson @@ -12,5 +12,5 @@ json { count catalogueUserList instanceof PagedResultList ? ((PagedResultList) catalogueUserList).getTotalCount() : catalogueUserList?.size() ?: 0 if (params.boolean('groupsContent', false)) { items tmpl.catalogueUser(catalogueUserList ?: []) - } else items tmpl.fullCatalogueUser(catalogueUserList ?: [], [userSecurityPolicyManager: userSecurityPolicyManager]) + } else items tmpl.catalogueUser_full(catalogueUserList ?: [], [userSecurityPolicyManager: userSecurityPolicyManager]) } \ No newline at end of file diff --git a/mdm-security/grails-app/views/catalogueUser/pending.gson b/mdm-security/grails-app/views/catalogueUser/pending.gson index 30fcf59f29..7228296875 100644 --- a/mdm-security/grails-app/views/catalogueUser/pending.gson +++ b/mdm-security/grails-app/views/catalogueUser/pending.gson @@ -10,5 +10,5 @@ model { json { count catalogueUserList instanceof PagedResultList ? ((PagedResultList) catalogueUserList).getTotalCount() : catalogueUserList?.size() ?: 0 - items tmpl.fullCatalogueUser(catalogueUserList ?: [], [userSecurityPolicyManager: userSecurityPolicyManager]) + items tmpl.catalogueUser_full(catalogueUserList ?: [], [userSecurityPolicyManager: userSecurityPolicyManager]) } \ No newline at end of file diff --git a/mdm-security/grails-app/views/catalogueUser/show.gson b/mdm-security/grails-app/views/catalogueUser/show.gson index ef248c690d..f9c4f2f9d3 100644 --- a/mdm-security/grails-app/views/catalogueUser/show.gson +++ b/mdm-security/grails-app/views/catalogueUser/show.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullCatalogueUser(catalogueUser: catalogueUser, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.catalogueUser_full(catalogueUser: catalogueUser, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-security/grails-app/views/catalogueUser/update.gson b/mdm-security/grails-app/views/catalogueUser/update.gson index ef248c690d..f9c4f2f9d3 100644 --- a/mdm-security/grails-app/views/catalogueUser/update.gson +++ b/mdm-security/grails-app/views/catalogueUser/update.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullCatalogueUser(catalogueUser: catalogueUser, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.catalogueUser_full(catalogueUser: catalogueUser, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-security/grails-app/views/groupRole/_fullGroupRole.gson b/mdm-security/grails-app/views/groupRole/_groupRole_full.gson similarity index 100% rename from mdm-security/grails-app/views/groupRole/_fullGroupRole.gson rename to mdm-security/grails-app/views/groupRole/_groupRole_full.gson diff --git a/mdm-security/grails-app/views/groupRole/index.gson b/mdm-security/grails-app/views/groupRole/index.gson index 66eceb4b0a..1f8a2c398f 100644 --- a/mdm-security/grails-app/views/groupRole/index.gson +++ b/mdm-security/grails-app/views/groupRole/index.gson @@ -10,5 +10,5 @@ model { json { count groupRoleList instanceof PagedResultList ? ((PagedResultList) groupRoleList).getTotalCount() : groupRoleList?.size() ?: 0 - items tmpl.fullGroupRole(groupRoleList ?: [], [userSecurityPolicyManager: userSecurityPolicyManager]) + items tmpl.groupRole_full(groupRoleList ?: [], [userSecurityPolicyManager: userSecurityPolicyManager]) } \ No newline at end of file diff --git a/mdm-security/grails-app/views/groupRole/listGroupRolesAvailableToSecurableResource.gson b/mdm-security/grails-app/views/groupRole/listGroupRolesAvailableToSecurableResource.gson index 8535cdf4a7..18a441e273 100644 --- a/mdm-security/grails-app/views/groupRole/listGroupRolesAvailableToSecurableResource.gson +++ b/mdm-security/grails-app/views/groupRole/listGroupRolesAvailableToSecurableResource.gson @@ -8,5 +8,5 @@ model { json { count groupRoleSet.size() - items tmpl.fullGroupRole(groupRoleSet, [userSecurityPolicyManager: userSecurityPolicyManager]) + items tmpl.groupRole_full(groupRoleSet, [userSecurityPolicyManager: userSecurityPolicyManager]) } \ No newline at end of file diff --git a/mdm-security/grails-app/views/groupRole/show.gson b/mdm-security/grails-app/views/groupRole/show.gson index 5aa60a6dac..419dedda61 100644 --- a/mdm-security/grails-app/views/groupRole/show.gson +++ b/mdm-security/grails-app/views/groupRole/show.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullGroupRole(groupRole: groupRole, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.groupRole_full(groupRole: groupRole, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-security/grails-app/views/groupRole/update.gson b/mdm-security/grails-app/views/groupRole/update.gson index 5aa60a6dac..419dedda61 100644 --- a/mdm-security/grails-app/views/groupRole/update.gson +++ b/mdm-security/grails-app/views/groupRole/update.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullGroupRole(groupRole: groupRole, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.groupRole_full(groupRole: groupRole, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-security/grails-app/views/userGroup/_fullUserGroup.gson b/mdm-security/grails-app/views/userGroup/_userGroup_full.gson similarity index 100% rename from mdm-security/grails-app/views/userGroup/_fullUserGroup.gson rename to mdm-security/grails-app/views/userGroup/_userGroup_full.gson diff --git a/mdm-security/grails-app/views/userGroup/index.gson b/mdm-security/grails-app/views/userGroup/index.gson index b9fdbe1b2a..587984ae47 100644 --- a/mdm-security/grails-app/views/userGroup/index.gson +++ b/mdm-security/grails-app/views/userGroup/index.gson @@ -10,5 +10,5 @@ model { json { count userGroupList instanceof PagedResultList ? ((PagedResultList) userGroupList).getTotalCount() : userGroupList?.size() ?: 0 - items tmpl.fullUserGroup(userGroupList ?: [], [userSecurityPolicyManager: userSecurityPolicyManager]) + items tmpl.userGroup_full(userGroupList ?: [], [userSecurityPolicyManager: userSecurityPolicyManager]) } \ No newline at end of file diff --git a/mdm-security/grails-app/views/userGroup/show.gson b/mdm-security/grails-app/views/userGroup/show.gson index 406140e07a..404340a141 100644 --- a/mdm-security/grails-app/views/userGroup/show.gson +++ b/mdm-security/grails-app/views/userGroup/show.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullUserGroup(userGroup: userGroup, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.userGroup_full(userGroup: userGroup, userSecurityPolicyManager: userSecurityPolicyManager) diff --git a/mdm-security/grails-app/views/userGroup/update.gson b/mdm-security/grails-app/views/userGroup/update.gson index 406140e07a..404340a141 100644 --- a/mdm-security/grails-app/views/userGroup/update.gson +++ b/mdm-security/grails-app/views/userGroup/update.gson @@ -6,4 +6,4 @@ model { UserSecurityPolicyManager userSecurityPolicyManager } -json tmpl.fullUserGroup(userGroup: userGroup, userSecurityPolicyManager: userSecurityPolicyManager) +json tmpl.userGroup_full(userGroup: userGroup, userSecurityPolicyManager: userSecurityPolicyManager) From c03171c9a24c965a7b73741916192264e348a19e Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Tue, 13 Jul 2021 22:37:38 +0100 Subject: [PATCH 38/82] Try something new with role cleanup --- ...AccessWithoutUpdatingFunctionalSpec.groovy | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessWithoutUpdatingFunctionalSpec.groovy b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessWithoutUpdatingFunctionalSpec.groovy index dd969c3c05..613e38eb3b 100644 --- a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessWithoutUpdatingFunctionalSpec.groovy +++ b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessWithoutUpdatingFunctionalSpec.groovy @@ -18,6 +18,8 @@ package uk.ac.ox.softeng.maurodatamapper.testing.functional import uk.ac.ox.softeng.maurodatamapper.security.CatalogueUser +import uk.ac.ox.softeng.maurodatamapper.security.SecurableResource +import uk.ac.ox.softeng.maurodatamapper.security.SecurableResourceService import uk.ac.ox.softeng.maurodatamapper.security.UserGroup import uk.ac.ox.softeng.maurodatamapper.security.role.GroupRole import uk.ac.ox.softeng.maurodatamapper.security.role.SecurableResourceGroupRole @@ -30,6 +32,7 @@ import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus import org.apache.commons.lang3.NotImplementedException import org.junit.Assert +import org.springframework.beans.factory.annotation.Autowired import spock.lang.Stepwise import static uk.ac.ox.softeng.maurodatamapper.util.GormUtils.checkAndSave @@ -274,11 +277,7 @@ abstract class UserAccessWithoutUpdatingFunctionalSpec extends ReadOnlyUserAcces List rolesLeftOver = SecurableResourceGroupRole.byUserGroupIds(groupsToDelete*.id).list() if (rolesLeftOver) { log.warn('Roles not cleaned up : {}', rolesLeftOver.size()) - rolesLeftOver.each { role -> - log.warn('Left over role resource {}:{}:{}:{}', role.groupRole.name, role.userGroup.name, role.securableResourceDomainType, role.securableResourceId) - } - Assert.fail('Roles remaining these need to be cleaned up from another test.' + - '\nSee logs to find out what roles and resources havent been cleaned') + cleanupOrphanedRoles(rolesLeftOver) } UserGroup.byNameNotInList(getPermanentGroupNames()).deleteAll() } @@ -287,6 +286,30 @@ abstract class UserAccessWithoutUpdatingFunctionalSpec extends ReadOnlyUserAcces assert UserGroup.count() == getPermanentGroupNames().size() } + @Autowired(required = false) + List securableResourceServices + + void cleanupOrphanedRoles(List rolesLeftOver) { + + rolesLeftOver.each {srgr -> + log.warn('Left over role resource {}:{}:{}:{}', srgr.groupRole.name, srgr.userGroup.name, srgr.securableResourceDomainType, srgr.securableResourceId) + SecurableResourceService service = securableResourceServices.find {it.handles(srgr.securableResourceDomainType)} + + if (!service) { + Assert.fail('Roles remaining these need to be cleaned up from another test and cannot remote clean them as no service to handle securable resource.' + + '\nSee logs to find out what roles and resources havent been cleaned') + } + SecurableResource resource = service.get(srgr.securableResourceId) + if (resource) { + log.warn('Resource {}:{} was not cleaned up', resource.domainType, resource.resourceId) + service.delete(resource) + } + } + SecurableResourceGroupRole.deleteAll(rolesLeftOver) + sessionFactory.currentSession.flush() + } + + List getPermanentGroupNames() { ['validIdGroup', 'administrators', 'readers', 'editors'] } From 36ffff7077faf0a339f5ed297f85e18dabc43838 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Tue, 13 Jul 2021 22:44:48 +0100 Subject: [PATCH 39/82] Update all the pathing code to use the new methods only * Better use of Path and PathNode * Match and path anything not just the old service defined entities * Update all tests, mostly just to make the correct call, the "context" of the tests remains the same so the functionality that used to exist still exists --- .../security/SecurableResourceService.groovy | 2 + .../softeng/maurodatamapper/util/Path.groovy | 20 +- .../maurodatamapper/util/PathNode.groovy | 48 +- .../core/path/PathController.groovy | 6 +- .../core/path/PathInterceptor.groovy | 2 + .../core/path/PathService.groovy | 152 +- .../maurodatamapper/core/model/Model.groovy | 2 +- .../core/model/ModelService.groovy | 18 +- .../core/traits/service/DomainService.groovy | 6 +- .../dataflow/DataFlowService.groovy | 36 +- .../DataClassComponentService.groovy | 20 +- .../DataElementComponentService.groovy | 20 +- .../datamodel/item/DataClassService.groovy | 2 +- .../path/DataModelPathServiceSpec.groovy | 210 +-- .../terminology/item/TermService.groovy | 7 +- .../path/TerminologyPathServiceSpec.groovy | 361 ++-- .../role/SecurableResourceGroupRole.groovy | 2 +- .../core/path/PathFunctionalSpec.groovy | 1534 ++++++++--------- 18 files changed, 1230 insertions(+), 1218 deletions(-) rename mdm-plugin-datamodel/src/{test/groovy/uk/ac/ox/softeng/maurodatamapper => integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel}/path/DataModelPathServiceSpec.groovy (50%) rename mdm-plugin-terminology/src/{test/groovy/uk/ac/ox/softeng/maurodatamapper => integration-test/groovy}/path/TerminologyPathServiceSpec.groovy (52%) diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/SecurableResourceService.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/SecurableResourceService.groovy index e711453311..5ac926293a 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/SecurableResourceService.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/SecurableResourceService.groovy @@ -21,6 +21,8 @@ interface SecurableResourceService { K get(Serializable id) + void delete(K domain) + boolean handles(Class clazz) boolean handles(String domainType) diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Path.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Path.groovy index eef58b053a..d450f95844 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Path.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Path.groovy @@ -17,6 +17,8 @@ */ package uk.ac.ox.softeng.maurodatamapper.util +import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware + import groovy.transform.stc.ClosureParams import groovy.transform.stc.SimpleType @@ -26,7 +28,7 @@ import groovy.transform.stc.SimpleType class Path { //Need to escape the vertical bar which we are using as the split delimiter - static String PATH_DELIMITER = "\\|" + static String PATH_DELIMITER = '\\|' //Arbitrary maximum number of nodes, to avoid unexpectedly long iteration static int MAX_NODES = 10 @@ -87,4 +89,20 @@ class Path { String toString() { pathNodes.join('|') } + + static Path from(String path) { + new Path(path) + } + + static Path from(String prefix, String pathIdentifier) { + new Path().tap { + pathNodes = [new PathNode(prefix, pathIdentifier)] + } + } + + static Path from(CreatorAware... domains) { + new Path().tap { + pathNodes = domains.collect {new PathNode(it.pathPrefix, it.pathIdentifier)} + } + } } diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/PathNode.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/PathNode.groovy index b0051a0862..6680ffd53f 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/PathNode.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/PathNode.groovy @@ -17,9 +17,14 @@ */ package uk.ac.ox.softeng.maurodatamapper.util +import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware + +import groovy.util.logging.Slf4j + /** * @since 28/08/2020 */ +@Slf4j class PathNode { static String NODE_DELIMITER = ":" @@ -27,6 +32,11 @@ class PathNode { String typePrefix String label + PathNode(String prefix, String label) { + this.typePrefix = prefix + this.label = label + } + /* Parse a string into a type prefix and label. The string is imagined to be of the format tp:label @@ -34,7 +44,8 @@ class PathNode { dm:my-data-model (type prefix = dm, label = my-data-model) te:my-code:my-definition (type prefix = te, label = my-code:my-definition) */ - PathNode (String node) { + + PathNode(String node) { //Look for the first : int index = node.indexOf(NODE_DELIMITER) @@ -56,4 +67,39 @@ class PathNode { String toString() { "${typePrefix}:${label}" } + + boolean matches(CreatorAware creatorAware) { + matches(creatorAware.pathIdentifier, creatorAware.pathPrefix) + } + + boolean matches(String pathIdentifier, String pathPrefix) { + matchesLabel(pathIdentifier) && matchesPrefix(pathPrefix) + } + + boolean matchesLabel(String pathIdentifier) { + + // No label suggests we're at the top of the path and we've come in via an id tracking path + // confirmation will come via the prefix check + if (!label) return true + + if (label == pathIdentifier) return true + + // Allow for the possibility of a label which has been defaulted (models can be path'd without the branch or version) + String[] pathIdentifierSplit = pathIdentifier.split(/:/) + if (label == pathIdentifierSplit[0]) return true + + // Some of the legacy paths included : so we handle submissions of this format + String[] labelSplit = label.split(/:/) + if (labelSplit[0] == pathIdentifier) return true + log.warn("Resource identifier [{}] does not match the path node [{}]", pathIdentifier, this) + false + } + + boolean matchesPrefix(String pathPrefix) { + if (pathPrefix != typePrefix) { + log.warn("Resource prefix [{}] does not match the path node [{}]", typePrefix, this) + return false + } + true + } } diff --git a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathController.groovy b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathController.groovy index 6916c9eca4..8e701cd63e 100644 --- a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathController.groovy +++ b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathController.groovy @@ -24,6 +24,7 @@ import uk.ac.ox.softeng.maurodatamapper.security.SecurableResource import uk.ac.ox.softeng.maurodatamapper.security.SecurityPolicyManagerService import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware +import grails.artefact.DomainClass import grails.rest.RestfulController import org.springframework.beans.factory.annotation.Autowired @@ -57,10 +58,10 @@ class PathController extends RestfulController implements MdmCont // Permissions have been checked as part of the interceptor pathedResource = pathService.findResourceByPathFromRootResource(resource as CreatorAware, params.path) } else { - pathedResource = pathService.findResourceByPathFromRootClass(params.securableResourceClass, params.path) + pathedResource = pathService.findResourceByPathFromRootClass(params.securableResourceClass, params.path, currentUserSecurityPolicyManager) } - if (!pathedResource) return notFound(CreatorAware, params.path) + if (!pathedResource) return notFound(DomainClass, params.path) respond(pathedResource, [model: [userSecurityPolicyManager: currentUserSecurityPolicyManager, pathedResource : pathedResource], @@ -69,5 +70,6 @@ class PathController extends RestfulController implements MdmCont def listAllPrefixMappings() { respond pathService.listAllPrefixMappings() + } } diff --git a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy index fb8fbe9ac4..e0914f9c21 100644 --- a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy +++ b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy @@ -23,6 +23,8 @@ import uk.ac.ox.softeng.maurodatamapper.util.Path class PathInterceptor implements MdmInterceptor { boolean before() { + if (actionName == 'listAllPrefixMappings') return true + mapDomainTypeToClass('securableResource', true) params.path = new Path(params.path) diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy index 20946c3d79..c79bbfd156 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy @@ -18,11 +18,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.path import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException -import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItemService -import uk.ac.ox.softeng.maurodatamapper.core.model.Model -import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem -import uk.ac.ox.softeng.maurodatamapper.core.model.ModelService import uk.ac.ox.softeng.maurodatamapper.core.traits.service.DomainService import uk.ac.ox.softeng.maurodatamapper.security.SecurableResource import uk.ac.ox.softeng.maurodatamapper.security.SecurableResourceService @@ -30,12 +26,9 @@ import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.PathNode -import uk.ac.ox.softeng.maurodatamapper.util.Utils import grails.core.GrailsApplication -import grails.core.GrailsClass import grails.gorm.transactions.Transactional -import grails.web.servlet.mvc.GrailsParameterMap import groovy.util.logging.Slf4j import org.grails.core.artefact.DomainClassArtefactHandler import org.grails.orm.hibernate.proxy.HibernateProxyHandler @@ -59,22 +52,23 @@ class PathService { private static HibernateProxyHandler proxyHandler = new HibernateProxyHandler() SecurableResource findSecurableResourceByDomainClassAndId(Class resourceClass, UUID resourceId) { - SecurableResourceService securableResourceService = securableResourceServices.find { it.handles(resourceClass) } + SecurableResourceService securableResourceService = securableResourceServices.find {it.handles(resourceClass)} if (!securableResourceService) throw new ApiBadRequestException('PS03', "No service available to handle [${resourceClass.simpleName}]") securableResourceService.get(resourceId) } Map listAllPrefixMappings() { - grailsApplication.getArtefacts(DomainClassArtefactHandler.TYPE) - .findAll { CreatorAware.isAssignableFrom(it.clazz) && !it.isAbstract() } - .collectEntries { grailsClass -> - def domain = grailsClass.newInstance() + List domains = grailsApplication.getArtefacts(DomainClassArtefactHandler.TYPE) + .findAll {CreatorAware.isAssignableFrom(it.clazz) && !it.isAbstract()} + .collect {grailsClass -> // Allow unqualified path domains to exist without breaking the system - if (domain instanceof CreatorAware && domain.pathPrefix) { - [domain.pathPrefix, domain.domainType] - } - null - }.findAll().sort() as Map + CreatorAware domain = grailsClass.newInstance() as CreatorAware + domain.pathPrefix ? domain : null + }.findAll() + + domains.collectEntries {domain -> + [domain.pathPrefix, domain.domainType] + }.sort() as Map } CreatorAware findResourceByPathFromRootResource(CreatorAware rootResourceOfPath, Path path) { @@ -82,45 +76,60 @@ class PathService { throw new ApiBadRequestException('PS06', 'Must have a path to search') } - if (path.first().label != rootResourceOfPath.pathIdentifier) { - throw new ApiBadRequestException('PS01', 'Path cannot exist inside resource as first path node is not the resource node') + if (!(path.first().matches(rootResourceOfPath))) { + log.warn('Path cannot exist inside resource as first path node is not the resource node') + return null } + // Confirmed the path is inside the model // If only one node then return the model - if (path.size == 1) return rootResourceOfPath as CreatorAware + if (path.size == 1) return rootResourceOfPath // Only 2 nodes in path, first is model // Last part of path is a field access as has no type prefix so return the model - if (path.size == 2 && !path.last().hasTypePrefix()) return rootResourceOfPath as CreatorAware + if (path.size == 2 && !path.last().hasTypePrefix()) return rootResourceOfPath // Find the first child in the path Path childPath = path.childPath PathNode childNode = childPath.first() - DomainService domainService = domainServices.find { service -> + DomainService domainService = domainServices.find {service -> service.handlesPathPrefix(childNode.typePrefix) } + if (!domainService) { + log.warn("Unknown path prefix [${childNode.typePrefix}] in path") + return null + } + log.debug('Found service [{}] to handle [{}]', domainService.class.simpleName, childNode.typePrefix) - CreatorAware child = domainService.findByParentIdAndPathIdentifier(rootResourceOfPath.id, childNode.label) + def child = domainService.findByParentIdAndPathIdentifier(rootResourceOfPath.id, childNode.label) if (!child) { - log.debug("Child [${childNode}] does not exist in path [${path}]") - throw new ApiBadRequestException('PS02', "Child [${childNode}] in path cannot be found inside domain") + log.warn("Child [${childNode}] does not exist in path [${path}]") + return null } // Recurse down the path for that child findResourceByPathFromRootResource(child, childPath) } + CreatorAware findResourceByPathFromRootClass(Class rootClass, String path) { + findResourceByPathFromRootClass(rootClass, Path.from(path)) + } + CreatorAware findResourceByPathFromRootClass(Class rootClass, Path path) { + findResourceByPathFromRootClass(rootClass, path, null) + } + + CreatorAware findResourceByPathFromRootClass(Class rootClass, Path path, UserSecurityPolicyManager userSecurityPolicyManager) { if (path.isEmpty()) { throw new ApiBadRequestException('PS05', 'Must have a path to search') } PathNode rootNode = path.first() - SecurableResourceService securableResourceService = securableResourceServices.find { it.handles(rootClass) } + SecurableResourceService securableResourceService = securableResourceServices.find {it.handles(rootClass)} if (!securableResourceService) { throw new ApiBadRequestException('PS03', "No service available to handle [${rootClass.simpleName}]") } @@ -130,88 +139,23 @@ class PathService { CreatorAware rootResource = securableResourceService.findByParentIdAndPathIdentifier(null, rootNode.label) if (!rootResource) return null - findResourceByPathFromRootResource(rootResource, path) - } - @Deprecated - CatalogueItem findCatalogueItemByPath(UserSecurityPolicyManager userSecurityPolicyManager, Map params) { - Path path = new Path(params.path) - CatalogueItemService service = catalogueItemServices.find { it.handles(params.catalogueItemDomainType) } - CatalogueItem catalogueItem - - /* - Iterate over nodes in the path - */ - boolean first = true - path.pathNodes.each { PathNode node -> - /* - On first iteration, if params.catalogueItemId is provided then use this to get the top CatalogueItem by ID. - Else if the service handles the typePrefix then use this service to find the top CatalogueItem by label. - */ - if (first) { - if (params.catalogueItemId) { - catalogueItem = service.get(params.catalogueItemId) - } else { - if (service.handlesPathPrefix(node.typePrefix) && node.label) { - catalogueItem = service.findByLabel(node.label) - if (service instanceof ModelService) { - catalogueItem = service.findLatestModelByLabel(node.label) - } else { - catalogueItem = service.findByLabel(node.label) - } - } - } - - /* - Only return anything if the first item retrieved is a model which is securable and readable, or it belongs to a model which is - securable and readable - */ - boolean readable = false - if (catalogueItem instanceof Model) { - readable = userSecurityPolicyManager.userCanReadSecuredResourceId(catalogueItem.getClass(), catalogueItem.id) - } else if (catalogueItem instanceof ModelItem) { - CatalogueItem model = proxyHandler.unwrapIfProxy(catalogueItem.getModel()) - readable = userSecurityPolicyManager.userCanReadResourceId(catalogueItem.getClass(), catalogueItem.id, model.getClass(), model.id) - } - - if (!readable) { - catalogueItem = null - } - - - first = false - } else { - if (catalogueItem) { - //Try to find the child of this catalogue item by prefix and label - service = catalogueItemServices.find { it.handlesPathPrefix(node.typePrefix) } - - //Use the service to find a child CatalogueItem whose parent is catalogueItem and which has the specified label - //Missing method exception means the path tried to retrieve a type of parent that is not expected - try { - catalogueItem = service.findByParentAndLabel(catalogueItem, node.label) - } catch (groovy.lang.MissingMethodException ex) { - catalogueItem = null - } - - } - } + // Confirm root resource exists and its prefix matches the pathed prefix + // We dont need to check the prefix in the findResourceByPathFromRootResource method as we "have" a resource at this point + // And all subsequent calls in that method use the prefix to find the domain service + if (rootResource.pathPrefix != rootNode.typePrefix) { + log.warn("Root resource prefix [${rootNode.typePrefix}] does not match the root class to search") + return null } - catalogueItem - } - - GrailsClass getGrailsResource(GrailsParameterMap params, String resourceParam) { - String lookup = params[resourceParam] - - if (!lookup) { - throw new ApiBadRequestException('MCI01', "No domain class resource provided") + // Check readabliity if possible + // If no policymanager then assume readability has already been performed + // Cannot read root then return null + if ( + userSecurityPolicyManager && !userSecurityPolicyManager.userCanReadSecuredResourceId(rootResource.getClass() as Class, rootResource.id)) { + return null } - GrailsClass grailsClass = Utils.lookupGrailsDomain(grailsApplication, lookup) - if (!grailsClass) { - throw new ApiBadRequestException('MCI02', "Unrecognised domain class resource [${params[resourceParam]}]") - } - grailsClass + findResourceByPathFromRootResource(rootResource, path) } - } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Model.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Model.groovy index a95ab3df4e..3cd816836a 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Model.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Model.groovy @@ -58,7 +58,7 @@ trait Model extends CatalogueItem implements SecurableRes @Override String getPathIdentifier() { - "${label}.${modelVersion ?: branchName}" + "${label}:${modelVersion ?: branchName}" } @Override diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy index dd3fcf3e1a..769ec80573 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy @@ -34,7 +34,6 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLink import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkService import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints -import uk.ac.ox.softeng.maurodatamapper.core.path.PathService import uk.ac.ox.softeng.maurodatamapper.core.provider.dataloader.DataLoaderProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.ModelImporterProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.ModelImporterProviderServiceParameters @@ -93,9 +92,6 @@ abstract class ModelService extends CatalogueItemService imp @Autowired(required = false) SecurityPolicyManagerService securityPolicyManagerService - @Autowired - PathService pathService - @Override Class getCatalogueItemClass() { getModelClass() @@ -210,15 +206,19 @@ abstract class ModelService extends CatalogueItemService imp @Override K findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { - String split = pathIdentifier.split(/\./) + String[] split = pathIdentifier.split(/:/) + String label = split[0] + // Default to main branch + String identity = split.size() == 1 ? 'main' : split[1] + DetachedCriteria criteria = parentId ? modelClass.byFolderId(parentId) : modelClass.by() - criteria.eq('label', split[0]) + criteria.eq('label', label) - if (Version.isVersionable(split[1])) { - criteria.eq('modelVersion', Version.from(split[1])) + if (Version.isVersionable(identity)) { + criteria.eq('modelVersion', Version.from(identity)) } else { - criteria.eq('branchName', split[1]) + criteria.eq('branchName', identity) } criteria.get() as K } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/DomainService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/DomainService.groovy index 9d1d60525b..a73a013bce 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/DomainService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/DomainService.groovy @@ -57,7 +57,11 @@ trait DomainService { } Class getDomainClass() { - (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0] + ParameterizedType parameterizedType = this.getClass().getGenericInterfaces().find {it instanceof ParameterizedType} + if (!parameterizedType) { + parameterizedType = this.getClass().getGenericSuperclass() + } + (Class) parameterizedType.getActualTypeArguments()[0] } boolean handles(Class clazz) { diff --git a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowService.groovy b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowService.groovy index 5c53b84ba0..fd370ce9db 100644 --- a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowService.groovy +++ b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowService.groovy @@ -28,7 +28,7 @@ import uk.ac.ox.softeng.maurodatamapper.dataflow.component.DataClassComponentSer import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.security.basic.PublicAccessSecurityPolicyManager +import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.Utils import grails.gorm.transactions.Transactional @@ -108,7 +108,7 @@ class DataFlowService extends ModelItemService { log.trace('Removing {} DataFlows', dataFlowIds.size()) sessionFactory.currentSession - .createSQLQuery('delete from dataflow.data_flow where source_id = :id or target_id = :id') + .createSQLQuery('DELETE FROM dataflow.data_flow WHERE source_id = :id OR target_id = :id') .setParameter('id', modelId) .executeUpdate() @@ -152,17 +152,17 @@ class DataFlowService extends ModelItemService { Set dataFlows = [] as Set - dataFlows.addAll buildTargetChain(allReadableDataFlows, allReadableDataFlows.findAll { it.refersToDataModelId(dataModelId) }) - dataFlows.addAll buildSourceChain(allReadableDataFlows, allReadableDataFlows.findAll { it.refersToDataModelId(dataModelId) }) + dataFlows.addAll buildTargetChain(allReadableDataFlows, allReadableDataFlows.findAll {it.refersToDataModelId(dataModelId)}) + dataFlows.addAll buildSourceChain(allReadableDataFlows, allReadableDataFlows.findAll {it.refersToDataModelId(dataModelId)}) dataFlows.toList() as List } def buildTargetChain(List readableDataFlows, Collection dataFlowChain) { - Set targets = dataFlowChain.collect { it.target }.toSet() + Set targets = dataFlowChain.collect {it.target}.toSet() - Set targetDataFlows = readableDataFlows.findAll { it.source in targets }.toSet() + Set targetDataFlows = readableDataFlows.findAll {it.source in targets}.toSet() if (targetDataFlows) { targetDataFlows = buildTargetChain(readableDataFlows - targetDataFlows, targetDataFlows) @@ -173,9 +173,9 @@ class DataFlowService extends ModelItemService { def buildSourceChain(List readableDataFlows, Collection dataFlowChain) { - Set sources = dataFlowChain.collect { it.source }.toSet() + Set sources = dataFlowChain.collect {it.source}.toSet() - Set sourceDataFlows = readableDataFlows.findAll { it.target in sources }.toSet() + Set sourceDataFlows = readableDataFlows.findAll {it.target in sources}.toSet() if (sourceDataFlows) { sourceDataFlows = buildSourceChain(readableDataFlows - sourceDataFlows, sourceDataFlows) @@ -275,7 +275,7 @@ class DataFlowService extends ModelItemService { @Override List findAllReadableByClassifier(UserSecurityPolicyManager userSecurityPolicyManager, Classifier classifier) { - DataFlow.byClassifierId(classifier.id).list().findAll { userSecurityPolicyManager.userCanReadSecuredResourceId(DataModel, it.model.id) } + DataFlow.byClassifierId(classifier.id).list().findAll {userSecurityPolicyManager.userCanReadSecuredResourceId(DataModel, it.model.id)} } @Override @@ -293,7 +293,7 @@ class DataFlowService extends ModelItemService { thisDataFlow.diff(otherDataFlow) } - /** + /** * When importing a DataFlow, do checks and setting of required values as follows: * (1) Set the createdBy of the DataFlow to be the importing user * (2) Check facets @@ -315,11 +315,8 @@ class DataFlowService extends ModelItemService { checkFacetsAfterImportingCatalogueItem(dataFlow) //source and target data model are imported by use of a path like "dm:my-data-model" - String sourcePath = "dm:${bindingMap.source.label}" - DataModel sourceDataModel = pathService.findCatalogueItemByPath( - PublicAccessSecurityPolicyManager.instance, - [path: sourcePath, catalogueItemDomainType: DataModel.simpleName] - ) + Path sourcePath = Path.from('dm', bindingMap.source.label) + DataModel sourceDataModel = pathService.findResourceByPathFromRootClass(DataModel, sourcePath) as DataModel if (sourceDataModel) { dataFlow.source = sourceDataModel @@ -327,11 +324,8 @@ class DataFlowService extends ModelItemService { throw new ApiBadRequestException('DFI01', "Source DataModel retrieval for ${sourcePath} failed") } - String targetPath = "dm:${bindingMap.target.label}" - DataModel targetDataModel = pathService.findCatalogueItemByPath( - PublicAccessSecurityPolicyManager.instance, - [path: targetPath, catalogueItemDomainType: DataModel.simpleName] - ) + Path targetPath = Path.from('dm', bindingMap.target.label) + DataModel targetDataModel = pathService.findResourceByPathFromRootClass(DataModel, targetPath) as DataModel if (targetDataModel) { dataFlow.target = targetDataModel @@ -341,7 +335,7 @@ class DataFlowService extends ModelItemService { //Check associations for the dataClassComponents if (dataFlow.dataClassComponents) { - dataFlow.dataClassComponents.each { dcc -> + dataFlow.dataClassComponents.each {dcc -> dataClassComponentService.checkImportedDataClassComponentAssociations(importingUser, dataFlow, dcc) } diff --git a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponentService.groovy b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponentService.groovy index c4c7a7f376..ac1e667837 100644 --- a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponentService.groovy +++ b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponentService.groovy @@ -27,7 +27,7 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClass import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.security.basic.PublicAccessSecurityPolicyManager +import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.Utils import grails.gorm.transactions.Transactional @@ -210,12 +210,9 @@ class DataClassComponentService extends ModelItemService { Set resolvedSourceDataClasses = [] if (rawSourceDataClasses) { - rawSourceDataClasses.each { sdc -> - String path = "dm:${dataFlow.source.label}|dc:${sdc.label}" - DataClass sourceDataClass = pathService.findCatalogueItemByPath( - PublicAccessSecurityPolicyManager.instance, - [path: path, catalogueItemDomainType: DataModel.simpleName] - ) + rawSourceDataClasses.each {sdc -> + Path path = Path.from(dataFlow.source, sdc) + DataClass sourceDataClass = pathService.findResourceByPathFromRootClass(DataModel, path) as DataClass if (sourceDataClass) { resolvedSourceDataClasses.add(sourceDataClass) @@ -232,12 +229,9 @@ class DataClassComponentService extends ModelItemService { Set resolvedTargetDataClasses = [] if (rawTargetDataClasses) { - rawTargetDataClasses.each { tdc -> - String path = "dm:${dataFlow.target.label}|dc:${tdc.label}" - DataClass targetDataClass = pathService.findCatalogueItemByPath( - PublicAccessSecurityPolicyManager.instance, - [path: path, catalogueItemDomainType: DataModel.simpleName] - ) + rawTargetDataClasses.each {tdc -> + Path path = Path.from(dataFlow.target, tdc) + DataClass targetDataClass = pathService.findResourceByPathFromRootClass(DataModel, path) as DataClass if (targetDataClass) { resolvedTargetDataClasses.add(targetDataClass) diff --git a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponentService.groovy b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponentService.groovy index 9dd63dc374..16c7bad105 100644 --- a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponentService.groovy +++ b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponentService.groovy @@ -27,7 +27,7 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataElement import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.security.basic.PublicAccessSecurityPolicyManager +import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.Utils import grails.gorm.transactions.Transactional @@ -241,12 +241,9 @@ TODO data flow copying def rawSourceDataElements = dataElementComponent.sourceDataElements Set resolvedSourceDataElements = [] - rawSourceDataElements.each { sde -> - String path = "dm:${dataFlow.source.label}|dc:${sde.dataClass.label}|de:${sde.label}" - DataElement sourceDataElement = pathService.findCatalogueItemByPath( - PublicAccessSecurityPolicyManager.instance, - [path: path, catalogueItemDomainType: DataModel.simpleName] - ) + rawSourceDataElements.each {sde -> + Path path = Path.from(dataFlow.source, sde.dataClass, sde) + DataElement sourceDataElement = pathService.findResourceByPathFromRootClass(DataModel, path) as DataElement if (sourceDataElement) { resolvedSourceDataElements.add(sourceDataElement) @@ -260,12 +257,9 @@ TODO data flow copying def rawTargetDataElements = dataElementComponent.targetDataElements Set resolvedTargetDataElements = [] - rawTargetDataElements.each { tde -> - String path = "dm:${dataFlow.target.label}|dc:${tde.dataClass.label}|de:${tde.label}" - DataElement targetDataElement = pathService.findCatalogueItemByPath( - PublicAccessSecurityPolicyManager.instance, - [path: path, catalogueItemDomainType: DataModel.simpleName] - ) + rawTargetDataElements.each {tde -> + Path path = Path.from(dataFlow.target, tde.dataClass, tde) + DataElement targetDataElement = pathService.findResourceByPathFromRootClass(DataModel, path) as DataElement if (targetDataElement) { resolvedTargetDataElements.add(targetDataElement) diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassService.groovy index a3344f0fa8..e6d697326d 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassService.groovy @@ -816,7 +816,7 @@ class DataClassService extends ModelItemService implements SummaryMet */ @Override DataClass findByParentIdAndLabel(UUID parentId, String label) { - DataClass dataClass = findByDataModelIdAndId(parentId, label) + DataClass dataClass = findByDataModelIdAndLabel(parentId, label) if (!dataClass) { dataClass = DataClass.byParentDataClassId(parentId).eq('label', label).get() } diff --git a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/DataModelPathServiceSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/path/DataModelPathServiceSpec.groovy similarity index 50% rename from mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/DataModelPathServiceSpec.groovy rename to mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/path/DataModelPathServiceSpec.groovy index e1469f17be..1c8f61c975 100644 --- a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/DataModelPathServiceSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/path/DataModelPathServiceSpec.groovy @@ -15,32 +15,27 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package uk.ac.ox.softeng.maurodatamapper.path +package uk.ac.ox.softeng.maurodatamapper.datamodel.path -import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTreeService -import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.path.PathService import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel -import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModelService import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClass -import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClassService import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataElement -import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataElementService import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.DataType -import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.DataTypeService import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.PrimitiveType -import uk.ac.ox.softeng.maurodatamapper.security.basic.PublicAccessSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.test.unit.service.CatalogueItemServiceSpec +import uk.ac.ox.softeng.maurodatamapper.datamodel.test.BaseDataModelIntegrationSpec +import uk.ac.ox.softeng.maurodatamapper.util.Path -import grails.testing.services.ServiceUnitTest +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration import groovy.util.logging.Slf4j -import spock.lang.Stepwise @Slf4j -@Stepwise -class DataModelPathServiceSpec extends CatalogueItemServiceSpec implements ServiceUnitTest { +@Integration +@Rollback +class DataModelPathServiceSpec extends BaseDataModelIntegrationSpec { DataModel dataModel1 DataClass dataClass1_1 @@ -55,6 +50,8 @@ class DataModelPathServiceSpec extends CatalogueItemServiceSpec implements Servi DataClass dataClass2_3 DataClass dataClass2_4 + PathService pathService + /* Set up test data like: @@ -71,25 +68,9 @@ class DataModelPathServiceSpec extends CatalogueItemServiceSpec implements Servi -> "data class 3" -> "data class 4" */ - def setup() { - log.debug('Setting up PathServiceSpec Unit') - mockArtefact(BreadcrumbTreeService) - mockArtefact(DataTypeService) - mockArtefact(DataClassService) - mockArtefact(DataElementService) - //The Metadata is required - mockDomains(DataModel, Metadata, DataClass, DataType, DataElement, PrimitiveType) - mockArtefact(DataModelService) - - // service.breadcrumbTreeService = Stub(BreadcrumbTreeService){ - // finalise(_) >> { - // BreadcrumbTree bt -> - // bt.finalised = true - // bt.buildTree() - // - // } - // } + @Override + void setupDomainData() { dataModel1 = new DataModel(createdByUser: admin, label: 'data model 1', folder: testFolder, authority: testAuthority) checkAndSave(dataModel1) @@ -127,109 +108,116 @@ class DataModelPathServiceSpec extends CatalogueItemServiceSpec implements Servi dataModel2.addToDataClasses(dataClass2_4) checkAndSave(dataModel2) - [dataModel1, dataClass1_1, dataClass1_2, dataClass1_3, dataModel2, dataClass2_1, dataClass2_2, dataClass2_3, dataClass2_4, dataElement2_1].each{ + [dataModel1, dataClass1_1, dataClass1_2, dataClass1_3, dataModel2, dataClass2_1, dataClass2_2, dataClass2_3, dataClass2_4, dataElement2_1].each { log.debug("${it.label} ${it.id}") } } - /* Get each of the three DataClasses in data model 1, by specifying the data class ID and label */ + void "test get of data classes in data model 1 by data class ID"() { - Map params + given: + setupData() CatalogueItem catalogueItem when: - params = ['catalogueItemDomainType': 'dataClasses', 'catalogueItemId': dataClass1_1.id.toString(), 'path': "dc:data class 1"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + Path path = Path.from(dataModel1, dataClass1_1) + catalogueItem = pathService.findResourceByPathFromRootClass(DataModel, path) as CatalogueItem then: catalogueItem.label == "data class 1" catalogueItem.domainType == "DataClass" - catalogueItem.model.id.equals(dataModel1.id) + catalogueItem.model.id == dataModel1.id //This one (data class 1_1) is nested inside data class 1 when: - params = ['catalogueItemDomainType': 'dataClasses', 'catalogueItemId': dataClass1_1_1.id.toString(), 'path': "dc:data class 1_1"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from(dataModel1, dataClass1_1, dataClass1_1_1) + catalogueItem = pathService.findResourceByPathFromRootClass(DataModel, path) as CatalogueItem then: catalogueItem.label == "data class 1_1" catalogueItem.domainType == "DataClass" - catalogueItem.model.id.equals(dataModel1.id) + catalogueItem.model.id == dataModel1.id when: - params = ['catalogueItemDomainType': 'dataClasses', 'catalogueItemId': dataClass1_2.id.toString(), 'path': "dc:data class 2"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from(dataModel1, dataClass1_2) + catalogueItem = pathService.findResourceByPathFromRootClass(DataModel, path) as CatalogueItem then: catalogueItem.label == "data class 2" catalogueItem.domainType == "DataClass" - catalogueItem.model.id.equals(dataModel1.id) + catalogueItem.model.id == dataModel1.id when: - params = ['catalogueItemDomainType': 'dataClasses', 'catalogueItemId': dataClass1_3.id.toString(), 'path': "dc:data class 3"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from(dataModel1, dataClass1_3) + catalogueItem = pathService.findResourceByPathFromRootClass(DataModel, path) as CatalogueItem then: catalogueItem.label == "data class 3" catalogueItem.domainType == "DataClass" - catalogueItem.model.id.equals(dataModel1.id) + catalogueItem.model.id == dataModel1.id } /* Get each of the four DataClasses in data model 2, by specifying the data class ID and label */ + void "test get of data classes in data model 2 by data class ID"() { - Map params + given: + setupData() CatalogueItem catalogueItem when: - params = ['catalogueItemDomainType': 'dataClasses', 'catalogueItemId': dataClass2_1.id.toString(), 'path': "dc:data class 1"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + Path path = Path.from(dataModel2, dataClass2_1) + catalogueItem = pathService.findResourceByPathFromRootClass(DataModel, path) as CatalogueItem then: catalogueItem.label == "data class 1" catalogueItem.domainType == "DataClass" - catalogueItem.model.id.equals(dataModel2.id) + catalogueItem.model.id == dataModel2.id when: - params = ['catalogueItemDomainType': 'dataClasses', 'catalogueItemId': dataClass2_2.id.toString(), 'path': "dc:data class 2"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from(dataModel2, dataClass2_2) + catalogueItem = pathService.findResourceByPathFromRootClass(DataModel, path) as CatalogueItem then: catalogueItem.label == "data class 2" catalogueItem.domainType == "DataClass" - catalogueItem.model.id.equals(dataModel2.id) + catalogueItem.model.id == dataModel2.id when: - params = ['catalogueItemDomainType': 'dataClasses', 'catalogueItemId': dataClass2_3.id.toString(), 'path': "dc:data class 3"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from(dataModel2, dataClass2_3) + catalogueItem = pathService.findResourceByPathFromRootClass(DataModel, path) as CatalogueItem then: catalogueItem.label == "data class 3" catalogueItem.domainType == "DataClass" - catalogueItem.model.id.equals(dataModel2.id) + catalogueItem.model.id == dataModel2.id when: - params = ['catalogueItemDomainType': 'dataClasses', 'catalogueItemId': dataClass2_4.id.toString(), 'path': "dc:data class 4"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from(dataModel2, dataClass2_4) + catalogueItem = pathService.findResourceByPathFromRootClass(DataModel, path) as CatalogueItem then: catalogueItem.label == "data class 4" catalogueItem.domainType == "DataClass" - catalogueItem.model.id.equals(dataModel2.id) + catalogueItem.model.id == dataModel2.id } /* Get data model 1 by specifying its ID and label */ + void "test get of data model 1"() { + given: + setupData() + when: - Map params = ['catalogueItemDomainType': 'dataModels', 'catalogueItemId': dataModel1.id.toString(), 'path': "dm:data model 1"] - CatalogueItem catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + Path path = Path.from(dataModel1) + CatalogueItem catalogueItem = pathService.findResourceByPathFromRootClass(DataModel, path) as CatalogueItem then: catalogueItem.label == "data model 1" @@ -239,10 +227,14 @@ class DataModelPathServiceSpec extends CatalogueItemServiceSpec implements Servi /* Get data model 2 by specifying its ID and label */ + void "test get of data model 2"() { + given: + setupData() + when: - Map params = ['catalogueItemDomainType': 'dataModels', 'catalogueItemId': dataModel2.id.toString(), 'path': "dm:data model 2"] - CatalogueItem catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + Path path = Path.from(dataModel2) + CatalogueItem catalogueItem = pathService.findResourceByPathFromRootClass(DataModel, path) as CatalogueItem then: catalogueItem.label == "data model 2" @@ -252,50 +244,53 @@ class DataModelPathServiceSpec extends CatalogueItemServiceSpec implements Servi /* Get the data classes in data model 1 by specifying the ID of the data model and the label of the data class */ + void "test get of data classes by label in data model 1"() { + given: + setupData() Map params CatalogueItem catalogueItem when: - params = ['catalogueItemDomainType': 'dataModels', 'catalogueItemId': dataModel1.id.toString(), 'path': "dm:|dc:data class 1"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + Path path = Path.from('dm:|dc:data class 1') + catalogueItem = pathService.findResourceByPathFromRootResource(dataModel1, path) as CatalogueItem then: catalogueItem.label == "data class 1" catalogueItem.domainType == "DataClass" - catalogueItem.model.id.equals(dataModel1.id) + catalogueItem.model.id == dataModel1.id //This is the nested data class when: - params = ['catalogueItemDomainType': 'dataModels', 'catalogueItemId': dataModel1.id.toString(), 'path': "dm:|dc:data class 1|dc:data class 1_1"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from('dm:|dc:data class 1|dc:data class 1_1') + catalogueItem = pathService.findResourceByPathFromRootResource(dataModel1, path) as CatalogueItem then: catalogueItem.label == "data class 1_1" catalogueItem.domainType == "DataClass" - catalogueItem.model.id.equals(dataModel1.id) + catalogueItem.model.id == dataModel1.id when: - params = ['catalogueItemDomainType': 'dataModels', 'catalogueItemId': dataModel1.id.toString(), 'path': "dm:|dc:data class 2"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from('dm:|dc:data class 2') + catalogueItem = pathService.findResourceByPathFromRootResource(dataModel1, path) as CatalogueItem then: catalogueItem.label == "data class 2" catalogueItem.domainType == "DataClass" - catalogueItem.model.id.equals(dataModel1.id) + catalogueItem.model.id == dataModel1.id when: - params = ['catalogueItemDomainType': 'dataModels', 'catalogueItemId': dataModel1.id.toString(), 'path': "dm:|dc:data class 3"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from('dm:|dc:data class 3') + catalogueItem = pathService.findResourceByPathFromRootResource(dataModel1, path) as CatalogueItem then: catalogueItem.label == "data class 3" catalogueItem.domainType == "DataClass" - catalogueItem.model.id.equals(dataModel1.id) + catalogueItem.model.id == dataModel1.id when: - params = ['catalogueItemDomainType': 'dataModels', 'catalogueItemId': dataModel1.id.toString(), 'path': "dm:|dc:data class 4"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from('dm:|dc:data class 4') + catalogueItem = pathService.findResourceByPathFromRootResource(dataModel1, path) as CatalogueItem then: catalogueItem == null @@ -304,100 +299,109 @@ class DataModelPathServiceSpec extends CatalogueItemServiceSpec implements Servi /* Get the data classes in data model 2 by specifying the ID of the data model and the label of the data class */ + void "test get of data classes by label in data model 2"() { + given: + setupData() Map params CatalogueItem catalogueItem when: - params = ['catalogueItemDomainType': 'dataModels', 'catalogueItemId': dataModel2.id.toString(), 'path': "dm:|dc:data class 1"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + Path path = Path.from('dm:|dc:data class 1') + catalogueItem = pathService.findResourceByPathFromRootResource(dataModel2, path) as CatalogueItem then: catalogueItem.label == "data class 1" catalogueItem.domainType == "DataClass" - catalogueItem.model.id.equals(dataModel2.id) + catalogueItem.model.id == dataModel2.id when: - params = ['catalogueItemDomainType': 'dataModels', 'catalogueItemId': dataModel2.id.toString(), 'path': "dm:|dc:data class 2"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from('dm:|dc:data class 2') + catalogueItem = pathService.findResourceByPathFromRootResource(dataModel2, path) as CatalogueItem then: catalogueItem.label == "data class 2" catalogueItem.domainType == "DataClass" - catalogueItem.model.id.equals(dataModel2.id) + catalogueItem.model.id == dataModel2.id when: - params = ['catalogueItemDomainType': 'dataModels', 'catalogueItemId': dataModel2.id.toString(), 'path': "dm:|dc:data class 3"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from('dm:|dc:data class 3') + catalogueItem = pathService.findResourceByPathFromRootResource(dataModel2, path) as CatalogueItem then: catalogueItem.label == "data class 3" catalogueItem.domainType == "DataClass" - catalogueItem.model.id.equals(dataModel2.id) + catalogueItem.model.id == dataModel2.id when: - params = ['catalogueItemDomainType': 'dataModels', 'catalogueItemId': dataModel2.id.toString(), 'path': "dm:|dc:data class 4"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from('dm:|dc:data class 4') + catalogueItem = pathService.findResourceByPathFromRootResource(dataModel2, path) as CatalogueItem then: catalogueItem.label == "data class 4" catalogueItem.domainType == "DataClass" - catalogueItem.model.id.equals(dataModel2.id) + catalogueItem.model.id == dataModel2.id } /* Get the data element 1 in data model 2 */ + void "test get of data element"() { - Map params + given: + setupData() CatalogueItem catalogueItem //When the data class is root when: - params = ['catalogueItemDomainType': 'dataClasses', 'catalogueItemId': dataClass2_1.id.toString(), 'path': "dc:|de:data element 1"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + Path path = Path.from(dataModel2, dataClass2_1, dataElement2_1) + catalogueItem = pathService.findResourceByPathFromRootClass(DataModel, path) as CatalogueItem then: catalogueItem.label == "data element 1" catalogueItem.domainType == "DataElement" - catalogueItem.model.id.equals(dataModel2.id) + catalogueItem.model.id == dataModel2.id //When the data model is root when: - params = ['catalogueItemDomainType': 'dataModels', 'catalogueItemId': dataModel2.id.toString(), 'path': "dm:|dc:data class 1|de:data element 1"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from('dm:|dc:data class 1|de:data element 1') + catalogueItem = pathService.findResourceByPathFromRootResource(dataModel2, path) as CatalogueItem then: catalogueItem.label == "data element 1" catalogueItem.domainType == "DataElement" - catalogueItem.model.id.equals(dataModel2.id) + catalogueItem.model.id == dataModel2.id } /* Get the data type in data model 2 */ + void "test get of data type"() { - Map params + given: + setupData() CatalogueItem catalogueItem //When the data model ID is provided when: - params = ['catalogueItemDomainType': 'dataModels', 'catalogueItemId': dataModel2.id.toString(), 'path': "dm:|dt: path service test data type"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + Path path = Path.from('dm:|dt:path service test data type') + catalogueItem = pathService.findResourceByPathFromRootResource(dataModel2, path) as CatalogueItem then: catalogueItem.label == "path service test data type" catalogueItem.domainType == "PrimitiveType" - catalogueItem.model.id.equals(dataModel2.id) + catalogueItem.model.id == dataModel2.id //When the data model label is provided when: - params = ['catalogueItemDomainType': 'dataModels', 'path': "dm:data model 2|dt: path service test data type"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from('dm:data model 2|dt:path service test data type') + catalogueItem = pathService.findResourceByPathFromRootClass(DataModel, path) as CatalogueItem then: catalogueItem.label == "path service test data type" catalogueItem.domainType == "PrimitiveType" - catalogueItem.model.id.equals(dataModel2.id) + catalogueItem.model.id == dataModel2.id } + + } diff --git a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermService.groovy b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermService.groovy index dc6a9cf0e4..4fdd096257 100644 --- a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermService.groovy +++ b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermService.groovy @@ -462,9 +462,12 @@ class TermService extends ModelItemService { @Override Term findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { - Term term = Term.byCodeSetId(parentId).eq('code', pathIdentifier).get() + // Older code used the full term label which is not great but we should be able to handle this here + String legacyHandlingPathIdentifier = pathIdentifier.split(':')[0] + + Term term = Term.byCodeSetId(parentId).eq('code', legacyHandlingPathIdentifier).get() if (!term) { - term = Term.byTerminologyId(parentId).eq('code', pathIdentifier).get() + term = Term.byTerminologyId(parentId).eq('code', legacyHandlingPathIdentifier).get() } term } diff --git a/mdm-plugin-terminology/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/TerminologyPathServiceSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/path/TerminologyPathServiceSpec.groovy similarity index 52% rename from mdm-plugin-terminology/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/TerminologyPathServiceSpec.groovy rename to mdm-plugin-terminology/src/integration-test/groovy/path/TerminologyPathServiceSpec.groovy index a38a46e4b5..6c229335f6 100644 --- a/mdm-plugin-terminology/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/TerminologyPathServiceSpec.groovy +++ b/mdm-plugin-terminology/src/integration-test/groovy/path/TerminologyPathServiceSpec.groovy @@ -15,34 +15,33 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package uk.ac.ox.softeng.maurodatamapper.path +package path -import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTreeService import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.path.PathService -import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.security.basic.PublicAccessSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.terminology.CodeSet -import uk.ac.ox.softeng.maurodatamapper.terminology.CodeSetService import uk.ac.ox.softeng.maurodatamapper.terminology.Terminology -import uk.ac.ox.softeng.maurodatamapper.terminology.TerminologyService import uk.ac.ox.softeng.maurodatamapper.terminology.item.Term -import uk.ac.ox.softeng.maurodatamapper.terminology.item.TermService -import uk.ac.ox.softeng.maurodatamapper.test.unit.service.CatalogueItemServiceSpec +import uk.ac.ox.softeng.maurodatamapper.terminology.test.BaseTerminologyIntegrationSpec +import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.Version -import grails.testing.services.ServiceUnitTest +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration import groovy.util.logging.Slf4j +import spock.lang.PendingFeature import spock.lang.Stepwise import java.time.OffsetDateTime import java.time.ZoneOffset @Slf4j +@Integration +@Rollback @Stepwise -class TerminologyPathServiceSpec extends CatalogueItemServiceSpec implements ServiceUnitTest { - - UserSecurityPolicyManager userSecurityPolicyManager +class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { + + PathService pathService Terminology terminology1 Term terminology1_term1 @@ -59,7 +58,7 @@ class TerminologyPathServiceSpec extends CatalogueItemServiceSpec implements Ser Terminology terminology4notFinalised Terminology terminology5first - Terminology terminology5second + Terminology terminology5second CodeSet codeSet1 @@ -93,14 +92,9 @@ class TerminologyPathServiceSpec extends CatalogueItemServiceSpec implements Ser -> "terminology 2 term 1" -> "terminology 2 term 2" */ - def setup() { - log.debug('Setting up TerminologyPathServiceSpec Unit') - mockArtefact(BreadcrumbTreeService) - mockArtefact(TermService) - mockArtefact(TerminologyService) - mockArtefact(CodeSetService) - mockDomains(Terminology, CodeSet, Term) + void setupDomainData() { + log.debug('Setting up TerminologyPathServiceSpec Unit') terminology1 = new Terminology(createdByUser: admin, label: 'terminology 1', folder: testFolder, authority: testAuthority) checkAndSave(terminology1) @@ -123,10 +117,12 @@ class TerminologyPathServiceSpec extends CatalogueItemServiceSpec implements Ser terminology3main = new Terminology(createdByUser: admin, label: 'terminology 3', description: 'terminology 3 on main', folder: testFolder, authority: testAuthority) checkAndSave(terminology3main) - terminology3draft = new Terminology(createdByUser: admin, label: 'terminology 3', description: 'terminology 3 on draft', folder: testFolder, authority: testAuthority, branchName: 'draft') + terminology3draft = new Terminology(createdByUser: admin, label: 'terminology 3', description: 'terminology 3 on draft', folder: testFolder, authority: testAuthority, + branchName: 'draft') checkAndSave(terminology3draft) - terminology4finalised = new Terminology(createdByUser: admin, label: 'terminology 4', description: 'terminology 4 finalised', folder: testFolder, authority: testAuthority) + terminology4finalised = + new Terminology(createdByUser: admin, label: 'terminology 4', description: 'terminology 4 finalised', folder: testFolder, authority: testAuthority) checkAndSave(terminology4finalised) terminology4finalised.finalised = true terminology4finalised.dateFinalised = OffsetDateTime.now().withOffsetSameInstant(ZoneOffset.UTC) @@ -134,8 +130,9 @@ class TerminologyPathServiceSpec extends CatalogueItemServiceSpec implements Ser terminology4finalised.modelVersion = Version.from('1.0.0') checkAndSave(terminology4finalised) - terminology4notFinalised = new Terminology(createdByUser: admin, label: 'terminology 4', description: 'terminology 4 not finalised', folder: testFolder, authority: testAuthority) - checkAndSave(terminology4notFinalised) + terminology4notFinalised = + new Terminology(createdByUser: admin, label: 'terminology 4', description: 'terminology 4 not finalised', folder: testFolder, authority: testAuthority) + checkAndSave(terminology4notFinalised) terminology5first = new Terminology(createdByUser: admin, label: 'terminology 5', description: 'terminology 5 1.0.0', folder: testFolder, authority: testAuthority) checkAndSave(terminology5first) @@ -151,7 +148,7 @@ class TerminologyPathServiceSpec extends CatalogueItemServiceSpec implements Ser terminology5second.dateFinalised = OffsetDateTime.now().withOffsetSameInstant(ZoneOffset.UTC) terminology5second.breadcrumbTree.finalised = true terminology5second.modelVersion = Version.from('2.0.0') - checkAndSave(terminology5second) + checkAndSave(terminology5second) codeSet1 = new CodeSet(createdByUser: admin, label: 'codeset 1', folder: testFolder, authority: testAuthority) checkAndSave(codeSet1) @@ -169,173 +166,187 @@ class TerminologyPathServiceSpec extends CatalogueItemServiceSpec implements Ser } - void "test truth"() { - when: - int a = 42 - - then: - a == 42 - } - void "test getting terminology by ID and path"() { - Map params + given: + setupData() CatalogueItem catalogueItem /* Terminology 1 by ID */ when: - params = ['catalogueItemDomainType': 'terminologies', 'catalogueItemId': terminology1.id.toString(), 'path': "te:"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + Path path = Path.from('te:') + catalogueItem = pathService.findResourceByPathFromRootResource(terminology1, path) as CatalogueItem then: catalogueItem.label == terminology1.label catalogueItem.domainType == "Terminology" - catalogueItem.id.equals(terminology1.id) + catalogueItem.id == terminology1.id /* Terminology 2 by ID */ when: - params = ['catalogueItemDomainType': 'terminologies', 'catalogueItemId': terminology2.id.toString(), 'path': "te:"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from('te:') + catalogueItem = pathService.findResourceByPathFromRootResource(terminology2, path) as CatalogueItem then: catalogueItem.label == terminology2.label catalogueItem.domainType == "Terminology" - catalogueItem.id.equals(terminology2.id) + catalogueItem.id == terminology2.id /* Terminology 1 by ID and path */ when: - params = ['catalogueItemDomainType': 'terminologies', 'catalogueItemId': terminology1.id.toString(), 'path': "te:${terminology1.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from(terminology1) + catalogueItem = pathService.findResourceByPathFromRootResource(terminology1, path) as CatalogueItem then: catalogueItem.label == terminology1.label catalogueItem.domainType == "Terminology" - catalogueItem.id.equals(terminology1.id) + catalogueItem.id == terminology1.id /* Terminology 2 by ID and path */ when: - params = ['catalogueItemDomainType': 'terminologies', 'catalogueItemId': terminology2.id.toString(), 'path': "te:${terminology2.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from(terminology2) + catalogueItem = pathService.findResourceByPathFromRootResource(terminology2, path) as CatalogueItem then: catalogueItem.label == terminology2.label catalogueItem.domainType == "Terminology" - catalogueItem.id.equals(terminology2.id) + catalogueItem.id == terminology2.id /* Terminology 1 by ID and wrong path */ when: - params = ['catalogueItemDomainType': 'terminologies', 'catalogueItemId': terminology1.id.toString(), 'path': "te:${terminology2.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from(terminology2) + catalogueItem = pathService.findResourceByPathFromRootResource(terminology1, path) as CatalogueItem then: - catalogueItem.label == terminology1.label - catalogueItem.domainType == "Terminology" - catalogueItem.id.equals(terminology1.id) + !catalogueItem /* Terminology 2 by ID and wrong path */ when: - params = ['catalogueItemDomainType': 'terminologies', 'catalogueItemId': terminology2.id.toString(), 'path': "te:${terminology1.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from(terminology1) + catalogueItem = pathService.findResourceByPathFromRootResource(terminology2, path) as CatalogueItem then: - catalogueItem.label == terminology2.label - catalogueItem.domainType == "Terminology" - catalogueItem.id.equals(terminology2.id) + !catalogueItem /* Terminology 1 by path */ when: - params = ['catalogueItemDomainType': 'terminologies', 'path': "te:${terminology1.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from(terminology1) + catalogueItem = pathService.findResourceByPathFromRootClass(Terminology, path) as CatalogueItem then: catalogueItem.label == terminology1.label catalogueItem.domainType == "Terminology" - catalogueItem.id.equals(terminology1.id) + catalogueItem.id == terminology1.id /* Terminology 2 by path */ when: - params = ['catalogueItemDomainType': 'terminologies', 'path': "te:${terminology2.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from(terminology2) + catalogueItem = pathService.findResourceByPathFromRootClass(Terminology, path) as CatalogueItem then: catalogueItem.label == terminology2.label catalogueItem.domainType == "Terminology" - catalogueItem.id.equals(terminology2.id) + catalogueItem.id == terminology2.id } void "test getting terms for terminology"() { - Map params + given: + setupData() CatalogueItem catalogueItem /* Terminology 1 Term 1 */ when: - params = ['catalogueItemDomainType': 'terminologies', 'catalogueItemId': terminology1.id.toString(), 'path': "te:|tm:${terminology1_term1.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + Path path = Path.from("te:|tm:${terminology1_term1.pathIdentifier}") + catalogueItem = pathService.findResourceByPathFromRootResource(terminology1, path) as CatalogueItem then: catalogueItem.label == terminology1_term1.label catalogueItem.domainType == "Term" - catalogueItem.id.equals(terminology1_term1.id) + catalogueItem.id == terminology1_term1.id + + /* + Terminology 1 Term 1 + */ + when: 'using the label' + path = Path.from("te:|tm:${terminology1_term1.label}") + catalogueItem = pathService.findResourceByPathFromRootResource(terminology1, path) as CatalogueItem + + then: + catalogueItem.label == terminology1_term1.label + catalogueItem.domainType == "Term" + catalogueItem.id == terminology1_term1.id /* Terminology 1 Term 2 */ when: - params = ['catalogueItemDomainType': 'terminologies', 'catalogueItemId': terminology1.id.toString(), 'path': "te:|tm:${terminology1_term2.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from("te:|tm:${terminology1_term2.pathIdentifier}") + catalogueItem = pathService.findResourceByPathFromRootResource(terminology1, path) as CatalogueItem then: catalogueItem.label == terminology1_term2.label catalogueItem.domainType == "Term" - catalogueItem.id.equals(terminology1_term2.id) + catalogueItem.id == terminology1_term2.id /* Terminology 2 Term 1 */ when: - params = ['catalogueItemDomainType': 'terminologies', 'catalogueItemId': terminology2.id.toString(), 'path': "te:|tm:${terminology2_term1.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from("te:|tm:${terminology2_term1.pathIdentifier}") + catalogueItem = pathService.findResourceByPathFromRootResource(terminology2, path) as CatalogueItem then: catalogueItem.label == terminology2_term1.label catalogueItem.domainType == "Term" - catalogueItem.id.equals(terminology2_term1.id) + catalogueItem.id == terminology2_term1.id /* Terminology 2 Term 2 */ when: - params = ['catalogueItemDomainType': 'terminologies', 'catalogueItemId': terminology2.id.toString(), 'path': "te:|tm:${terminology2_term2.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from("te:|tm:${terminology2_term2.pathIdentifier}") + catalogueItem = pathService.findResourceByPathFromRootResource(terminology2, path) as CatalogueItem then: catalogueItem.label == terminology2_term2.label catalogueItem.domainType == "Term" - catalogueItem.id.equals(terminology2_term2.id) + catalogueItem.id == terminology2_term2.id + + /* + By path alone + */ + when: + path = Path.from(terminology2, terminology2_term2) + catalogueItem = pathService.findResourceByPathFromRootClass(Terminology, path) as CatalogueItem + + then: + catalogueItem.label == terminology2_term2.label + catalogueItem.domainType == "Term" + catalogueItem.id == terminology2_term2.id /* Try to get a term which doesn't exist on Terminology 1 */ when: - params = ['catalogueItemDomainType': 'terminologies', 'catalogueItemId': terminology1.id.toString(), 'path': "te:|tm:${terminology2_term1.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from("te:|tm:${terminology2_term1.pathIdentifier}") + catalogueItem = pathService.findResourceByPathFromRootResource(terminology1, path) as CatalogueItem then: catalogueItem == null @@ -344,172 +355,170 @@ class TerminologyPathServiceSpec extends CatalogueItemServiceSpec implements Ser Try to get a term which doesn't exist on Terminology 2 */ when: - params = ['catalogueItemDomainType': 'terminologies', 'catalogueItemId': terminology2.id.toString(), 'path': "te:|tm:${terminology1_term1.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from("te:|tm:${terminology1_term1.pathIdentifier}") + catalogueItem = pathService.findResourceByPathFromRootResource(terminology2, path) as CatalogueItem then: catalogueItem == null } void "test getting code set by ID and path"() { - Map params + given: + setupData() CatalogueItem catalogueItem /* CodeSet 1 by ID */ when: - params = ['catalogueItemDomainType': 'codeSets', 'catalogueItemId': codeSet1.id.toString(), 'path': "cs:"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + Path path = Path.from('cs:') + catalogueItem = pathService.findResourceByPathFromRootResource(codeSet1, path) as CatalogueItem then: catalogueItem.label == codeSet1.label catalogueItem.domainType == "CodeSet" - catalogueItem.id.equals(codeSet1.id) + catalogueItem.id == codeSet1.id /* CodeSet 2 by ID */ when: - params = ['catalogueItemDomainType': 'codeSets', 'catalogueItemId': codeSet2.id.toString(), 'path': "cs:"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from('cs:') + catalogueItem = pathService.findResourceByPathFromRootResource(codeSet2, path) as CatalogueItem then: catalogueItem.label == codeSet2.label catalogueItem.domainType == "CodeSet" - catalogueItem.id.equals(codeSet2.id) + catalogueItem.id == codeSet2.id /* CodeSet 1 by ID and path */ when: - params = ['catalogueItemDomainType': 'codeSets', 'catalogueItemId': codeSet1.id.toString(), 'path': "cs:${codeSet1.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from(codeSet1) + catalogueItem = pathService.findResourceByPathFromRootResource(codeSet1, path) as CatalogueItem then: catalogueItem.label == codeSet1.label catalogueItem.domainType == "CodeSet" - catalogueItem.id.equals(codeSet1.id) + catalogueItem.id == codeSet1.id /* CodeSet 2 by ID and path */ when: - params = ['catalogueItemDomainType': 'codeSets', 'catalogueItemId': codeSet2.id.toString(), 'path': "cs:${codeSet2.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from(codeSet2) + catalogueItem = pathService.findResourceByPathFromRootResource(codeSet2, path) as CatalogueItem then: catalogueItem.label == codeSet2.label catalogueItem.domainType == "CodeSet" - catalogueItem.id.equals(codeSet2.id) + catalogueItem.id == codeSet2.id /* Code Set 1 by ID and wrong path */ when: - params = ['catalogueItemDomainType': 'codeSets', 'catalogueItemId': codeSet1.id.toString(), 'path': "cs:${codeSet2.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from(codeSet2) + catalogueItem = pathService.findResourceByPathFromRootResource(codeSet1, path) as CatalogueItem then: - catalogueItem.label == codeSet1.label - catalogueItem.domainType == "CodeSet" - catalogueItem.id.equals(codeSet1.id) + !catalogueItem /* CodeSet 2 by ID and wrong path */ when: - params = ['catalogueItemDomainType': 'codeSets', 'catalogueItemId': codeSet2.id.toString(), 'path': "cs:${codeSet1.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from(codeSet1) + catalogueItem = pathService.findResourceByPathFromRootResource(codeSet2, path) as CatalogueItem then: - catalogueItem.label == codeSet2.label - catalogueItem.domainType == "CodeSet" - catalogueItem.id.equals(codeSet2.id) + !catalogueItem /* CodeSet 1 by path */ when: - params = ['catalogueItemDomainType': 'codeSets', 'path': "cs:${codeSet1.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from(codeSet1) + catalogueItem = pathService.findResourceByPathFromRootClass(CodeSet, path) as CatalogueItem then: catalogueItem.label == codeSet1.label catalogueItem.domainType == "CodeSet" - catalogueItem.id.equals(codeSet1.id) + catalogueItem.id == codeSet1.id /* CodeSet 2 by path */ when: - params = ['catalogueItemDomainType': 'codeSets', 'path': "cs:${codeSet2.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from(codeSet2) + catalogueItem = pathService.findResourceByPathFromRootClass(CodeSet, path) as CatalogueItem then: catalogueItem.label == codeSet2.label catalogueItem.domainType == "CodeSet" - catalogueItem.id.equals(codeSet2.id) + catalogueItem.id == codeSet2.id } void "test getting terms for codeset"() { - Map params + given: + setupData() CatalogueItem catalogueItem /* CodeSet 1 Term 1 */ when: - params = ['catalogueItemDomainType': 'codeSets', 'catalogueItemId': codeSet1.id.toString(), 'path': "cs:|tm:${terminology1_term1.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + Path path = Path.from("cs:|tm:${terminology1_term1.pathIdentifier}") + catalogueItem = pathService.findResourceByPathFromRootResource(codeSet1, path) as CatalogueItem then: catalogueItem.label == terminology1_term1.label catalogueItem.domainType == "Term" - catalogueItem.id.equals(terminology1_term1.id) + catalogueItem.id == terminology1_term1.id /* CodeSet 1 Term 2 */ when: - params = ['catalogueItemDomainType': 'codeSets', 'catalogueItemId': codeSet1.id.toString(), 'path': "cs:|tm:${terminology1_term2.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from("cs:|tm:${terminology1_term2.pathIdentifier}") + catalogueItem = pathService.findResourceByPathFromRootResource(codeSet1, path) as CatalogueItem then: catalogueItem.label == terminology1_term2.label catalogueItem.domainType == "Term" - catalogueItem.id.equals(terminology1_term2.id) + catalogueItem.id == terminology1_term2.id /* CodeSet 2 Term 1 */ when: - params = ['catalogueItemDomainType': 'codeSets', 'catalogueItemId': codeSet2.id.toString(), 'path': "cs:|tm:${terminology2_term1.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from("cs:|tm:${terminology2_term1.pathIdentifier}") + catalogueItem = pathService.findResourceByPathFromRootResource(codeSet2, path) as CatalogueItem then: catalogueItem.label == terminology2_term1.label catalogueItem.domainType == "Term" - catalogueItem.id.equals(terminology2_term1.id) + catalogueItem.id == terminology2_term1.id /* CodeSet 2 Term 2 */ when: - params = ['catalogueItemDomainType': 'codeSets', 'catalogueItemId': codeSet2.id.toString(), 'path': "cs:|tm:${terminology2_term2.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from("cs:|tm:${terminology2_term2.pathIdentifier}") + catalogueItem = pathService.findResourceByPathFromRootResource(codeSet2, path) as CatalogueItem then: catalogueItem.label == terminology2_term2.label catalogueItem.domainType == "Term" - catalogueItem.id.equals(terminology2_term2.id) + catalogueItem.id == terminology2_term2.id /* Try to get a term which doesn't exist on CodeSet 1 */ when: - params = ['catalogueItemDomainType': 'codeSets', 'catalogueItemId': codeSet1.id.toString(), 'path': "cs:|tm:${terminology2_term1.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from("cs:|tm:${terminology2_term1.pathIdentifier}") + catalogueItem = pathService.findResourceByPathFromRootResource(codeSet1, path) as CatalogueItem then: catalogueItem == null @@ -518,72 +527,132 @@ class TerminologyPathServiceSpec extends CatalogueItemServiceSpec implements Ser Try to get a term which doesn't exist on CodeSet 2 */ when: - params = ['catalogueItemDomainType': 'codeSets', 'catalogueItemId': codeSet2.id.toString(), 'path': "cs:|tm:${terminology1_term1.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from("cs:|tm:${terminology1_term1.pathIdentifier}") + catalogueItem = pathService.findResourceByPathFromRootResource(codeSet2, path) as CatalogueItem then: catalogueItem == null } void "test get Terminology by path when there is a branch"() { - Map params + given: + setupData() CatalogueItem catalogueItem - + /* Terminology 3 by path. When using the label 'terminology 3' we expect to retrieve the terminology on the main branch, rather than the one on the draft branch */ when: - params = ['catalogueItemDomainType': 'terminologies', 'path': "te:terminology 3"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + Path path = Path.from('te:terminology 3') + catalogueItem = pathService.findResourceByPathFromRootClass(Terminology, path) as CatalogueItem then: catalogueItem.label == 'terminology 3' catalogueItem.domainType == "Terminology" - catalogueItem.id.equals(terminology3main.id) - catalogueItem.description.equals(terminology3main.description) + catalogueItem.id == terminology3main.id + catalogueItem.description == terminology3main.description + + when: 'using the branch name main' + path = Path.from('te:terminology 3:main') + catalogueItem = pathService.findResourceByPathFromRootClass(Terminology, path) as CatalogueItem + + then: + catalogueItem.label == 'terminology 3' + catalogueItem.domainType == "Terminology" + catalogueItem.id == terminology3main.id + + when: 'using the branch name draft' + path = Path.from('te:terminology 3:draft') + catalogueItem = pathService.findResourceByPathFromRootClass(Terminology, path) as CatalogueItem + + then: + catalogueItem.label == 'terminology 3' + catalogueItem.domainType == "Terminology" + catalogueItem.id == terminology3draft.id } + @PendingFeature void "test get Terminology by path when there are finalised and non-finalised versions"() { - Map params + given: + setupData() CatalogueItem catalogueItem - + /* Terminology 4 by path. When using the label 'terminology 4' we expect to retrieve the - non-finalised version. + non-finalised version. which will be the main branch */ when: - params = ['catalogueItemDomainType': 'terminologies', 'path': "te:terminology 4"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + Path path = Path.from('te:terminology 4') + catalogueItem = pathService.findResourceByPathFromRootClass(Terminology, path) as CatalogueItem then: catalogueItem.label == 'terminology 4' catalogueItem.domainType == "Terminology" - catalogueItem.id.equals(terminology4notFinalised.id) - catalogueItem.description.equals(terminology4notFinalised.description) + catalogueItem.id == terminology4notFinalised.id + catalogueItem.description == terminology4notFinalised.description - } + when: 'using the version' + path = Path.from('te:terminology 4:1.0.0') + catalogueItem = pathService.findResourceByPathFromRootClass(Terminology, path) as CatalogueItem + then: + catalogueItem.label == 'terminology 4' + catalogueItem.domainType == "Terminology" + catalogueItem.id == terminology4finalised.id + + } + + @PendingFeature void "test get Terminology by path when there are two finalised versions"() { - Map params + given: + setupData() CatalogueItem catalogueItem - + /* Terminology 5 by path. When using the label 'terminology 5' we expect to retrieve the finalised version 2.0.0. */ when: - params = ['catalogueItemDomainType': 'terminologies', 'path': "te:terminology 5"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + Path path = Path.from('te:terminology 5') + catalogueItem = pathService.findResourceByPathFromRootClass(Terminology, path) as CatalogueItem + + then: + catalogueItem.label == 'terminology 5' + catalogueItem.domainType == "Terminology" + catalogueItem.id == terminology5second.id + catalogueItem.modelVersion.toString() == '2.0.0' + + when: 'using version 2.0.0' + path = Path.from('te:terminology 5:2.0.0') + catalogueItem = pathService.findResourceByPathFromRootClass(Terminology, path) as CatalogueItem then: catalogueItem.label == 'terminology 5' catalogueItem.domainType == "Terminology" - catalogueItem.id.equals(terminology5second.id) - catalogueItem.description.equals(terminology5second.description) + catalogueItem.id == terminology5second.id catalogueItem.modelVersion.toString() == '2.0.0' - } + when: 'using version 2' + path = Path.from('te:terminology 5:2') + catalogueItem = pathService.findResourceByPathFromRootClass(Terminology, path) as CatalogueItem + + then: + catalogueItem.label == 'terminology 5' + catalogueItem.domainType == "Terminology" + catalogueItem.id == terminology5second.id + catalogueItem.modelVersion.toString() == '2.0.0' + + when: 'using version 1' + path = Path.from('te:terminology 5:1') + catalogueItem = pathService.findResourceByPathFromRootClass(Terminology, path) as CatalogueItem + + then: + catalogueItem.label == 'terminology 5' + catalogueItem.domainType == "Terminology" + catalogueItem.id == terminology5second.id + catalogueItem.modelVersion.toString() == '2.0.0' + } } diff --git a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/SecurableResourceGroupRole.groovy b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/SecurableResourceGroupRole.groovy index 8b70f5f6a3..7e84cd8c9d 100644 --- a/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/SecurableResourceGroupRole.groovy +++ b/mdm-security/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/security/role/SecurableResourceGroupRole.groovy @@ -68,7 +68,7 @@ class SecurableResourceGroupRole implements EditHistoryAware { @Override String getEditLabel() { - "SecuredResourceGroupRole:${userGroup.editLabel}:${securableResourceDomainType}:${securableResourceId}" + "SecuredResourceGroupRole:${userGroup?.editLabel}:${securableResourceDomainType}:${securableResourceId}" } @Override diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy index 40f384557b..cd85920ec6 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy @@ -18,17 +18,19 @@ package uk.ac.ox.softeng.maurodatamapper.testing.functional.core.path import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel -import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClass import uk.ac.ox.softeng.maurodatamapper.terminology.CodeSet import uk.ac.ox.softeng.maurodatamapper.terminology.Terminology import uk.ac.ox.softeng.maurodatamapper.terminology.bootstrap.BootstrapModels import uk.ac.ox.softeng.maurodatamapper.testing.functional.FunctionalSpec +import grails.artefact.DomainClass import grails.gorm.transactions.Transactional import grails.testing.mixin.integration.Integration import groovy.util.logging.Slf4j +import io.micronaut.http.HttpResponse + +import java.nio.charset.Charset -import static io.micronaut.http.HttpStatus.NOT_FOUND import static io.micronaut.http.HttpStatus.OK /** @@ -40,8 +42,7 @@ import static io.micronaut.http.HttpStatus.OK * | GET | /api/codeSets/path/$path | Action: show * | GET | /api/dataModels/$dataModelId/path/$path | Action: show * | GET | /api/dataModels/path/$path | Action: show - * | GET | /api/dataClasses/$dataClassId/path/$path | Action: show - * | GET | /api/dataClasses/path/$path | Action: show + * | GET | /api/path/$path | Action: show * * * @@ -62,6 +63,7 @@ class PathFunctionalSpec extends FunctionalSpec { String PRIMITIVE_DATA_TYPE_NAME = 'integer' String ENUMERATION_DATA_TYPE_NAME = 'yesnounknown' String REFERENCE_DATA_TYPE_NAME = 'child' + String node @Override String getResourcePath() { @@ -88,751 +90,387 @@ class PathFunctionalSpec extends FunctionalSpec { DataModel.findByLabel(COMPLEX_DATAMODEL_NAME).id.toString() } - @Transactional - String getParentDataClassId() { - DataClass.findByLabel(PARENT_DATACLASS_NAME).id.toString() + String makePathNode(String prefix, String label) { + prefix + ":" + label } - @Transactional - String getChildDataClassId() { - DataClass.findByLabel(CHILD_DATACLASS_NAME).id.toString() + String makePathNodes(String... pathNodes) { + pathNodes.join('|') } - @Transactional - String getContentDataClassId() { - DataClass.findByLabel(CONTENT_DATACLASS_NAME).id.toString() + String makePath(String node) { + //java.net.URLEncoder.encode turns spaces into +, and these are decoded by grails as +. So do a replace. + URLEncoder.encode(node, Charset.defaultCharset()).replace("+", "%20") } - @Transactional - String getDataElementId() { - DataClass.findByLabel(DATA_ELEMENT_NAME).id.toString() + def cleanup() { + node = null } - String makePathNode(String prefix, String label) { - prefix + ":" + label + void verifyNotFound(HttpResponse response, Object id, Class resourceClass = DomainClass) { + super.verifyNotFound(response, id) + assert response.body().resource == resourceClass.simpleName } - String makePath(List nodes) { - //java.net.URLEncoder.encode turns spaces into +, and these are decoded by grails as +. So do a replace. - URLEncoder.encode(String.join("|", nodes)).replace("+", "%20") + void 'T01: Get Terminology by path and ID when not logged in'() { + //No ID + when: + node = makePathNode('te', SIMPLE_TERMINOLOGY_NAME) + GET("/api/terminologies/path/${makePath(node)}") + + then: "The response is Not Found" + verifyNotFound(response, node) + + //With ID + when: + node = makePathNode('te', SIMPLE_TERMINOLOGY_NAME) + GET("/api/terminologies/${getSimpleTerminologyId()}/path/${makePath(node)}") + + then: "The response is Not Found" + verifyNotFound(response, getSimpleTerminologyId(), Terminology) + + //No ID + when: + node = makePathNode('te', COMPLEX_TERMINOLOGY_NAME) + GET("/api/terminologies/path/${makePath(node)}") + + then: "The response is Not Found" + verifyNotFound(response, node) + + //With ID + when: + node = makePathNode('te', COMPLEX_TERMINOLOGY_NAME) + GET("/api/terminologies/${getComplexTerminologyId()}/path/${makePath(node)}") + + then: "The response is Not Found" + verifyNotFound(response, getComplexTerminologyId(), Terminology) } - String getNotFoundPathJson() { - ''' - { - path: "${json-unit.any-string}", - resource: "CatalogueItem", - id: "${json-unit.any-string}" - }''' + void 'T02 : Get Terminology by path and ID when logged in'() { + given: + loginReader() + + //No ID + when: + node = makePathNode('te', SIMPLE_TERMINOLOGY_NAME) + GET("/api/terminologies/path/${makePath(node)}", STRING_ARG) + + then: "The response is OK" + verifyJsonResponse OK, getExpectedSimpleTerminologyJson() + + //With ID + when: + node = makePathNode('te', SIMPLE_TERMINOLOGY_NAME) + GET("/api/terminologies/${getSimpleTerminologyId()}/path/${makePath(node)}", STRING_ARG) + + then: "The response is OK" + verifyJsonResponse OK, getExpectedSimpleTerminologyJson() + + //No ID + when: + node = makePathNode('te', COMPLEX_TERMINOLOGY_NAME) + GET("/api/terminologies/path/${makePath(node)}", STRING_ARG) + + then: "The response is OK" + verifyJsonResponse OK, getExpectedComplexTerminologyJson() + + //With ID + when: + node = makePathNode('te', COMPLEX_TERMINOLOGY_NAME) + GET("/api/terminologies/${getComplexTerminologyId()}/path/${makePath(node)}", STRING_ARG) + + then: "The response is OK" + verifyJsonResponse OK, getExpectedComplexTerminologyJson() } - String getExpectedSimpleTerminologyJson() { - return '''{ - "id": "${json-unit.matches:id}", - "domainType": "Terminology", - "label": "Simple Test Terminology", - "availableActions": [ - "show", - "comment" - ], - "lastUpdated": "${json-unit.matches:offsetDateTime}", - "classifiers": [ - { - "id": "${json-unit.matches:id}", - "label": "test classifier simple", - "lastUpdated": "${json-unit.matches:offsetDateTime}" - } - ], - "type": "Terminology", - "branchName": "main", - "documentationVersion": "1.0.0", - "finalised": false, - "readableByEveryone": false, - "readableByAuthenticatedUsers": false, - "author": "Test Bootstrap", - "organisation": "Oxford BRC", - "authority": { - "id": "${json-unit.matches:id}", - "url": "http://localhost", - "label": "Mauro Data Mapper" - } - }''' + void 'T03 : get a Terminology but use the wrong prefix when not logged in'() { + //No ID + when: + node = makePathNode('tm', SIMPLE_TERMINOLOGY_NAME) + GET("/api/terminologies/path/${makePath(node)}") + + then: "The response is Not Found" + verifyNotFound(response, node) + + //With ID (this shouldnt work as no read access to simple terminology id) + when: + node = makePathNode('tm', SIMPLE_TERMINOLOGY_NAME) + GET("/api/terminologies/${getSimpleTerminologyId()}/path/${makePath(node)}") + + then: "The response is Not Found" + verifyNotFound(response, getSimpleTerminologyId(), Terminology) } - String getExpectedComplexTerminologyJson() { - return '''{ - "id": "${json-unit.matches:id}", - "domainType": "Terminology", - "label": "Complex Test Terminology", - "availableActions": [ - "show", - "comment" - ], - "lastUpdated": "${json-unit.matches:offsetDateTime}", - "classifiers": [ - { - "id": "${json-unit.matches:id}", - "label": "test classifier", - "lastUpdated": "${json-unit.matches:offsetDateTime}" - }, - { - "id": "${json-unit.matches:id}", - "label": "test classifier2", - "lastUpdated": "${json-unit.matches:offsetDateTime}" - } - ], - "type": "Terminology", - "branchName": "main", - "documentationVersion": "1.0.0", - "finalised": false, - "readableByEveryone": false, - "readableByAuthenticatedUsers": false, - "author": "Test Bootstrap", - "organisation": "Oxford BRC", - "authority": { - "id": "${json-unit.matches:id}", - "url": "http://localhost", - "label": "Mauro Data Mapper" - } - }''' + void 'T04 : get a Terminology but use the wrong prefix when logged in'() { + given: + loginReader() + + //No ID + when: + node = makePathNode('tm', SIMPLE_TERMINOLOGY_NAME) + GET("/api/terminologies/path/${makePath(node)}") + + then: "The response is Not Found" + verifyNotFound(response, node) + + //With ID (shouldnt work as the prefix is wrong) + when: + node = makePathNode('tm', SIMPLE_TERMINOLOGY_NAME) + GET("/api/terminologies/${getSimpleTerminologyId()}/path/${makePath(node)}") + + then: "The response is OK because the ID is used" + verifyNotFound(response, node) } - String getExpectedSimpleCodeSetJson() { - return '''{ - "id": "${json-unit.matches:id}", - "domainType": "CodeSet", - "label": "Simple Test CodeSet", - "availableActions": [ - "show", - "createNewVersions", - "newForkModel", - "comment" - ], - "lastUpdated": "${json-unit.matches:offsetDateTime}", - "classifiers": [ - { - "id": "${json-unit.matches:id}", - "label": "test classifier", - "lastUpdated": "${json-unit.matches:offsetDateTime}" - } - ], - "type": "CodeSet", - "branchName": "main", - "documentationVersion": "1.0.0", - "finalised": true, - "readableByEveryone": false, - "readableByAuthenticatedUsers": false, - "dateFinalised": "${json-unit.matches:offsetDateTime}", - "author": "Test Bootstrap", - "organisation": "Oxford BRC", - "modelVersion": "1.0.0", - "authority": { - "id": "${json-unit.matches:id}", - "url": "http://localhost", - "label": "Mauro Data Mapper" - } - }''' + void 'C01 : Get CodeSet by path and ID when not logged in'() { + //No ID + when: + node = makePathNode('cs', SIMPLE_CODESET_NAME) + GET("/api/codeSets/path/${makePath(node)}") + + then: "The response is Not Found" + verifyNotFound(response, node) + + //With ID + when: + node = makePathNode('cs', SIMPLE_CODESET_NAME) + GET("/api/codeSets/${getSimpleCodeSetId()}/path/${makePath(node)}") + + then: "The response is Not Found" + verifyNotFound(response, getSimpleCodeSetId(), CodeSet) } - String getExpectedSimpleTermJson() { - return '''{ - "id": "${json-unit.matches:id}", - "domainType": "Term", - "label": "STT01: Simple Test Term 01", - "model": "${json-unit.matches:id}", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Simple Test Terminology", - "domainType":"Terminology", - "finalised":false - } - ], - "availableActions": [ - "show", - "comment" - ], - "lastUpdated": "${json-unit.matches:offsetDateTime}", - "code": "STT01", - "definition": "Simple Test Term 01" - }''' + void 'C02 : Get CodeSet by path and ID when logged in'() { + given: + loginReader() + + //No ID + when: + node = makePathNode('cs', SIMPLE_CODESET_NAME) + GET("/api/codeSets/path/${makePath(node)}", STRING_ARG) + + then: "The response is OK" + verifyJsonResponse OK, getExpectedSimpleCodeSetJson() + + //With ID + when: + node = makePathNode('cs', SIMPLE_CODESET_NAME) + GET("/api/codeSets/${getSimpleCodeSetId()}/path/${makePath(node)}", STRING_ARG) + + then: "The response is OK" + verifyJsonResponse OK, getExpectedSimpleCodeSetJson() } - String getExpectedComplexDataModelJson() { - return '''{ - "id": "${json-unit.matches:id}", - "domainType": "DataModel", - "label": "Complex Test DataModel", - "availableActions": [ - "show", - "comment" - ], - "lastUpdated": "${json-unit.matches:offsetDateTime}", - "classifiers": [ - { - "id": "${json-unit.matches:id}", - "label": "test classifier", - "lastUpdated": "${json-unit.matches:offsetDateTime}" - }, - { - "id": "${json-unit.matches:id}", - "label": "test classifier2", - "lastUpdated": "${json-unit.matches:offsetDateTime}" - } - ], - "type": "Data Standard", - "branchName": "main", - "documentationVersion": "1.0.0", - "finalised": false, - "readableByEveryone": false, - "readableByAuthenticatedUsers": false, - "author": "admin person", - "organisation": "brc", - "authority": { - "id": "${json-unit.matches:id}", - "url": "http://localhost", - "label": "Mauro Data Mapper" - } - }''' - } - - String getExpectedParentDataClassJson() { - return '''{ - "id": "${json-unit.matches:id}", - "domainType": "DataClass", - "label": "parent", - "model": "${json-unit.matches:id}", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - } - ], - "availableActions": [ - "show", - "comment" - ], - "lastUpdated": "${json-unit.matches:offsetDateTime}", - "maxMultiplicity": -1, - "minMultiplicity": 1 - }''' - } - - String getExpectedChildDataClassJson() { - return '''{ - "id": "${json-unit.matches:id}", - "domainType": "DataClass", - "label": "child", - "model": "${json-unit.matches:id}", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - }, - { - "id": "${json-unit.matches:id}", - "label": "parent", - "domainType": "DataClass" - } - ], - "availableActions": [ - "show", - "comment" - ], - "lastUpdated": "${json-unit.matches:offsetDateTime}", - "parentDataClass": "${json-unit.matches:id}" - }''' - } - - String getExpectedDataElementJson() { - return '''{ - "id": "${json-unit.matches:id}", - "domainType": "DataElement", - "label": "ele1", - "model": "${json-unit.matches:id}", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - }, - { - "id": "${json-unit.matches:id}", - "label": "content", - "domainType": "DataClass" - } - ], - "availableActions": [ - "show", - "comment" - ], - "lastUpdated": "${json-unit.matches:offsetDateTime}", - "dataClass": "${json-unit.matches:id}", - "dataType": { - "id": "${json-unit.matches:id}", - "domainType": "PrimitiveType", - "label": "string", - "model": "${json-unit.matches:id}", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - } - ] - }, - "maxMultiplicity": 20, - "minMultiplicity": 0 - }''' - } + void 'C03 : get a CodeSet but use the wrong prefix when not logged in'() { + //No ID + when: + node = makePathNode('tm', SIMPLE_CODESET_NAME) + GET("/api/codeSets/path/${makePath(node)}") - String getExpectedPrimitiveTypeJson() { - return '''{ - "id": "${json-unit.matches:id}", - "domainType": "PrimitiveType", - "label": "integer", - "model": "${json-unit.matches:id}", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - } - ], - "availableActions": [ - "show", - "comment" - ], - "lastUpdated": "${json-unit.matches:offsetDateTime}" - }''' - } + then: "The response is Not Found" + verifyNotFound(response, node) - String getExpectedEnumerationTypeJson() { - return '''{ - "id": "${json-unit.matches:id}", - "domainType": "EnumerationType", - "label": "yesnounknown", - "model": "${json-unit.matches:id}", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - } - ], - "availableActions": [ - "show", - "comment" - ], - "lastUpdated": "${json-unit.matches:offsetDateTime}", - "enumerationValues": [ - { - "index": 1, - "id": "${json-unit.matches:id}", - "key": "N", - "value": "No", - "category": null - }, - { - "index": 2, - "id": "${json-unit.matches:id}", - "key": "U", - "value": "Unknown", - "category": null - }, - { - "index": 0, - "id": "${json-unit.matches:id}", - "key": "Y", - "value": "Yes", - "category": null - } - ] - }''' - } + //With ID (no access to the ID'd codeset) + when: + node = makePathNode('tm', SIMPLE_CODESET_NAME) + GET("/api/codeSets/${getSimpleCodeSetId()}/path/${makePath(node)}") - String getExpectedReferenceTypeJson() { - return '''{ - "id": "${json-unit.matches:id}", - "domainType": "ReferenceType", - "label": "child", - "model": "${json-unit.matches:id}", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - } - ], - "availableActions": [ - "show", - "comment" - ], - "lastUpdated": "${json-unit.matches:offsetDateTime}", - "referenceClass": { - "id": "${json-unit.matches:id}", - "domainType": "DataClass", - "label": "child", - "model": "${json-unit.matches:id}", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - }, - { - "id": "${json-unit.matches:id}", - "label": "parent", - "domainType": "DataClass" - } - ], - "parentDataClass": "${json-unit.matches:id}" - } - }''' + then: "The response is Not Found" + verifyNotFound(response, getSimpleCodeSetId(), CodeSet) } - void 'Get Terminology by path and ID when not logged in'() { - String node + void 'C04 : get a CodeSet but use the wrong prefix when logged in'() { + given: + loginReader() //No ID when: - node = makePathNode('te', SIMPLE_TERMINOLOGY_NAME) - GET("/api/terminologies/path/${makePath([node])}", STRING_ARG, true) + node = makePathNode('tm', SIMPLE_CODESET_NAME) + GET("/api/codeSets/path/${makePath(node)}") then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + verifyNotFound(response, node) - //With ID + //With ID (shouldnt work as the prefix is wrong) when: - node = makePathNode('te', SIMPLE_TERMINOLOGY_NAME) - GET("/api/terminologies/${getSimpleTerminologyId()}/path/${makePath([node])}", STRING_ARG, true) + node = makePathNode('tm', SIMPLE_CODESET_NAME) + GET("/api/codeSets/${getSimpleCodeSetId()}/path/${makePath(node)}") + + then: "The response is OK because the ID is used" + verifyNotFound(response, node) + } + + void 'TM01 : get a Term for a Terminology when not logged in'() { + //No Terminology ID + when: + node = makePathNodes(makePathNode('te', SIMPLE_TERMINOLOGY_NAME), + makePathNode('tm', 'STT01: Simple Test Term 01')) + GET("/api/terminologies/path/${makePath(node)}") then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + verifyNotFound(response, node) - //No ID + //With Terminology ID and label when: - node = makePathNode('te', COMPLEX_TERMINOLOGY_NAME) - GET("/api/terminologies/path/${makePath([node])}", STRING_ARG, true) + node = makePathNodes(makePathNode('te', SIMPLE_TERMINOLOGY_NAME), + makePathNode('tm', 'STT01: Simple Test Term 01')) + GET("/api/terminologies/${getSimpleTerminologyId()}/path/${makePath(node)}") then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + verifyNotFound(response, getSimpleTerminologyId(), Terminology) - //With ID + //With Terminology ID and no label when: - node = makePathNode('te', COMPLEX_TERMINOLOGY_NAME) - GET("/api/terminologies/${getComplexTerminologyId()}/path/${makePath([node])}", STRING_ARG, true) + node = makePathNodes(makePathNode('te', ''), + makePathNode('tm', 'STT01: Simple Test Term 01')) + GET("/api/terminologies/${getSimpleTerminologyId()}/path/${makePath(node)}") then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + verifyNotFound(response, getSimpleTerminologyId(), Terminology) } - void 'Get Terminology by path and ID when logged in'() { - String node - + void 'TM02 : get a Term for a Terminology when logged in'() { given: loginReader() - //No ID + //No Terminology ID when: - node = makePathNode('te', SIMPLE_TERMINOLOGY_NAME) - GET("/api/terminologies/path/${makePath([node])}", STRING_ARG, true) + node = makePathNodes(makePathNode('te', SIMPLE_TERMINOLOGY_NAME), + makePathNode('tm', 'STT01: Simple Test Term 01')) + GET("/api/terminologies/path/${makePath(node)}", STRING_ARG) then: "The response is OK" - verifyJsonResponse OK, getExpectedSimpleTerminologyJson() + verifyJsonResponse OK, getExpectedSimpleTermJson() - //With ID + //With Terminology ID and label when: - node = makePathNode('te', SIMPLE_TERMINOLOGY_NAME) - GET("/api/terminologies/${getSimpleTerminologyId()}/path/${makePath([node])}", STRING_ARG, true) + node = makePathNodes(makePathNode('te', SIMPLE_TERMINOLOGY_NAME), + makePathNode('tm', 'STT01: Simple Test Term 01')) + GET("/api/terminologies/${getSimpleTerminologyId()}/path/${makePath(node)}", STRING_ARG) then: "The response is OK" - verifyJsonResponse OK, getExpectedSimpleTerminologyJson() + verifyJsonResponse OK, getExpectedSimpleTermJson() - //No ID - when: - node = makePathNode('te', COMPLEX_TERMINOLOGY_NAME) - GET("/api/terminologies/path/${makePath([node])}", STRING_ARG, true) - - then: "The response is OK" - verifyJsonResponse OK, getExpectedComplexTerminologyJson() - - //With ID - when: - node = makePathNode('te', COMPLEX_TERMINOLOGY_NAME) - GET("/api/terminologies/${getComplexTerminologyId()}/path/${makePath([node])}", STRING_ARG, true) - - then: "The response is OK" - verifyJsonResponse OK, getExpectedComplexTerminologyJson() - } - - void 'get a Terminology but use the wrong prefix when not logged in'() { - String node - - //No ID - when: - node = makePathNode('tm', SIMPLE_TERMINOLOGY_NAME) - GET("/api/terminologies/path/${makePath([node])}", STRING_ARG, true) - - then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() - - //With ID (this should work because the ID is used) - when: - node = makePathNode('tm', COMPLEX_TERMINOLOGY_NAME) - GET("/api/terminologies/${getSimpleTerminologyId()}/path/${makePath([node])}", STRING_ARG, true) - - then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() - } - - void 'get a Terminology but use the wrong prefix when logged in'() { - String node - - given: - loginReader() - - //No ID - when: - node = makePathNode('tm', SIMPLE_TERMINOLOGY_NAME) - GET("/api/terminologies/path/${makePath([node])}", STRING_ARG, true) - - then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() - - //With ID (this should work because the ID is used) - when: - node = makePathNode('tm', COMPLEX_TERMINOLOGY_NAME) - GET("/api/terminologies/${getSimpleTerminologyId()}/path/${makePath([node])}", STRING_ARG, true) - - then: "The response is OK because the ID is used" - verifyJsonResponse OK, getExpectedSimpleTerminologyJson() - } - - void 'Get CodeSet by path and ID when not logged in'() { - String node - - //No ID - when: - node = makePathNode('cs', SIMPLE_CODESET_NAME) - GET("/api/codeSets/path/${makePath([node])}", STRING_ARG, true) - - then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() - - //With ID - when: - node = makePathNode('cs', SIMPLE_CODESET_NAME) - GET("/api/codeSets/${getSimpleCodeSetId()}/path/${makePath([node])}", STRING_ARG, true) - - then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() - } - - void 'Get CodeSet by path and ID when logged in'() { - String node - - given: - loginReader() - - //No ID - when: - node = makePathNode('cs', SIMPLE_CODESET_NAME) - GET("/api/codeSets/path/${makePath([node])}", STRING_ARG, true) - - then: "The response is OK" - verifyJsonResponse OK, getExpectedSimpleCodeSetJson() - - //With ID + //With Terminology ID and no label when: - node = makePathNode('cs', SIMPLE_CODESET_NAME) - GET("/api/codeSets/${getSimpleCodeSetId()}/path/${makePath([node])}", STRING_ARG, true) + node = makePathNodes(makePathNode('te', ''), + makePathNode('tm', 'STT01: Simple Test Term 01')) + GET("/api/terminologies/${getSimpleTerminologyId()}/path/${makePath(node)}", STRING_ARG) then: "The response is OK" - verifyJsonResponse OK, getExpectedSimpleCodeSetJson() - } - - void 'get a CodeSet but use the wrong prefix when not logged in'() { - String node - - //No ID - when: - node = makePathNode('tm', SIMPLE_CODESET_NAME) - GET("/api/codeSets/path/${makePath([node])}", STRING_ARG, true) - - then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() - - //With ID (this should work because the ID is used) - when: - node = makePathNode('tm', SIMPLE_CODESET_NAME) - GET("/api/codeSets/${getSimpleCodeSetId()}/path/${makePath([node])}", STRING_ARG, true) - - then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + verifyJsonResponse OK, getExpectedSimpleTermJson() } - void 'get a CodeSet but use the wrong prefix when logged in'() { - String node - - given: - loginReader() - - //No ID + void 'TM03 : get a Term for a CodeSet when not logged in'() { + //No CodeSet ID when: - node = makePathNode('tm', SIMPLE_CODESET_NAME) - GET("/api/codeSets/path/${makePath([node])}", STRING_ARG, true) + node = makePathNodes(makePathNode('cs', SIMPLE_CODESET_NAME), + makePathNode('tm', 'STT01: Simple Test Term 01')) + GET("/api/codeSets/path/${makePath(node)}") then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() - - //With ID (this should work because the ID is used) - when: - node = makePathNode('tm', SIMPLE_CODESET_NAME) - GET("/api/codeSets/${getSimpleCodeSetId()}/path/${makePath([node])}", STRING_ARG, true) + verifyNotFound(response, node) - then: "The response is OK because the ID is used" - verifyJsonResponse OK, getExpectedSimpleCodeSetJson() - } - - void 'get a Term for a Terminology when not logged in'() { - String node1 - String node2 - - //No Terminology ID + //With CodeSet ID and label when: - node1 = makePathNode('te', SIMPLE_TERMINOLOGY_NAME) - node2 = makePathNode('tm', 'STT01: Simple Test Term 01') - GET("/api/terminologies/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('cs', SIMPLE_CODESET_NAME), + makePathNode('tm', 'STT01: Simple Test Term 01')) + GET("/api/codeSets/${getSimpleCodeSetId()}/path/${makePath(node)}") then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + verifyNotFound(response, getSimpleCodeSetId(), CodeSet) - //With Terminology ID and no label + //With CodeSet ID and no label when: - node1 = makePathNode('te', '') - node2 = makePathNode('tm', 'STT01: Simple Test Term 01') - GET("/api/terminologies/${getSimpleTerminologyId()}/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('cs', ''), + makePathNode('tm', 'STT01: Simple Test Term 01')) + GET("/api/codeSets/${getSimpleCodeSetId()}/path/${makePath(node)}") then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + verifyNotFound(response, getSimpleCodeSetId(), CodeSet) } - void 'get a Term for a Terminology when logged in'() { - String node1 - String node2 - + void 'TM04 : get a Term for a CodeSet when logged in'() { given: loginReader() - //No Terminology ID + //No CodeSet ID when: - node1 = makePathNode('te', SIMPLE_TERMINOLOGY_NAME) - node2 = makePathNode('tm', 'STT01: Simple Test Term 01') - GET("/api/terminologies/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('cs', SIMPLE_CODESET_NAME), + makePathNode('tm', 'STT01: Simple Test Term 01')) + GET("/api/codeSets/path/${makePath(node)}", STRING_ARG) then: "The response is OK" verifyJsonResponse OK, getExpectedSimpleTermJson() - //With Terminology ID and no label + //With CodeSet ID and label when: - node1 = makePathNode('te', '') - node2 = makePathNode('tm', 'STT01: Simple Test Term 01') - GET("/api/terminologies/${getSimpleTerminologyId()}/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('cs', SIMPLE_CODESET_NAME), + makePathNode('tm', 'STT01: Simple Test Term 01')) + GET("/api/codeSets/${getSimpleCodeSetId()}/path/${makePath(node)}", STRING_ARG) then: "The response is OK" verifyJsonResponse OK, getExpectedSimpleTermJson() - } - - void 'get a Term for a CodeSet when not logged in'() { - String node1 - String node2 - - //No CodeSet ID - when: - node1 = makePathNode('cs', SIMPLE_CODESET_NAME) - node2 = makePathNode('tm', 'STT01: Simple Test Term 01') - GET("/api/codeSets/path/${makePath([node1, node2])}", STRING_ARG, true) - - then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() //With CodeSet ID and no label when: - node1 = makePathNode('cs', '') - node2 = makePathNode('tm', 'STT01: Simple Test Term 01') - GET("/api/codeSets/${getSimpleCodeSetId()}/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('cs', ''), + makePathNode('tm', 'STT01: Simple Test Term 01')) + GET("/api/codeSets/${getSimpleCodeSetId()}/path/${makePath(node)}", STRING_ARG) - then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + then: "The response is OK" + verifyJsonResponse OK, getExpectedSimpleTermJson() } - void 'get a Term for a CodeSet when logged in'() { - String node1 - String node2 - + void 'TM05 : get a Term for a Terminology when logged in and term not in terminology'() { given: loginReader() - //No CodeSet ID + //No Terminology ID when: - node1 = makePathNode('cs', SIMPLE_CODESET_NAME) - node2 = makePathNode('tm', 'STT01: Simple Test Term 01') - GET("/api/codeSets/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('te', SIMPLE_TERMINOLOGY_NAME), + makePathNode('tm', 'CTT01')) + GET("/api/terminologies/path/${makePath(node)}") - then: "The response is OK" - verifyJsonResponse OK, getExpectedSimpleTermJson() + then: "The response is Not Found" + verifyNotFound(response, node) - //With CodeSet ID and no label + //With Terminology ID and label when: - node1 = makePathNode('cs', '') - node2 = makePathNode('tm', 'STT01: Simple Test Term 01') - GET("/api/codeSets/${getSimpleCodeSetId()}/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('te', SIMPLE_TERMINOLOGY_NAME), + makePathNode('tm', 'CTT01')) + GET("/api/terminologies/${getSimpleTerminologyId()}/path/${makePath(node)}") - then: "The response is OK" - verifyJsonResponse OK, getExpectedSimpleTermJson() + then: "The response is Not Found" + verifyNotFound(response, node) } void 'DM01 : Get DataModel by path and ID when not logged in'() { - String node - //No ID when: node = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - GET("/api/dataModels/path/${makePath([node])}", STRING_ARG, true) + GET("/api/dataModels/path/${makePath(node)}") then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + verifyNotFound(response, node) //With ID when: node = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath([node])}", STRING_ARG, true) + GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath(node)}") then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + verifyNotFound(response, getComplexDataModelId(), DataModel) } - void 'Get DataModel by path and ID when logged in'() { - String node - + void 'DM02 : Get DataModel by path and ID when logged in'() { given: loginReader() //No ID when: node = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - GET("/api/dataModels/path/${makePath([node])}", STRING_ARG, true) + GET("/api/dataModels/path/${makePath(node)}", STRING_ARG) then: "The response is OK" verifyJsonResponse OK, getExpectedComplexDataModelJson() @@ -840,333 +478,255 @@ class PathFunctionalSpec extends FunctionalSpec { //With ID when: node = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath([node])}", STRING_ARG, true) + GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath(node)}", STRING_ARG) then: "The response is OK" verifyJsonResponse OK, getExpectedComplexDataModelJson() } - void 'Get DataClass with DataModel by path and ID when not logged in'() { - String node1 - String node2 - + void 'DC01 : Get DataClass with DataModel by path and ID when not logged in'() { //No ID when: - node1 = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - node2 = makePathNode('dc', PARENT_DATACLASS_NAME) - GET("/api/dataModels/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dc', PARENT_DATACLASS_NAME)) + GET("/api/dataModels/path/${makePath(node)}") then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + verifyNotFound(response, node) //With ID when: - node1 = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - node2 = makePathNode('dc', PARENT_DATACLASS_NAME) - GET("/api/dataModels/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dc', PARENT_DATACLASS_NAME)) + GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath(node)}") then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + verifyNotFound(response, getComplexDataModelId(), DataModel) } - void 'Get DataClass with DataModel by path and ID when logged in'() { - String node1 - String node2 - + void 'DC02 : Get DataClass with DataModel by path and ID when logged in'() { given: loginReader() //No ID when: - node1 = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - node2 = makePathNode('dc', PARENT_DATACLASS_NAME) - GET("/api/dataModels/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dc', PARENT_DATACLASS_NAME)) + GET("/api/dataModels/path/${makePath(node)}", STRING_ARG) then: "The response is OK" verifyJsonResponse OK, getExpectedParentDataClassJson() //With ID when: - node1 = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - node2 = makePathNode('dc', PARENT_DATACLASS_NAME) - GET("/api/dataModels/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dc', PARENT_DATACLASS_NAME)) + GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath(node)}", STRING_ARG) then: "The response is OK" verifyJsonResponse OK, getExpectedParentDataClassJson() } - void 'Get DataClass by path and ID when not logged in'() { - String node - - //No ID - when: - node = makePathNode('dc', PARENT_DATACLASS_NAME) - GET("/api/dataClasses/path/${makePath([node])}", STRING_ARG, true) - - then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() - - //With ID - when: - node = makePathNode('dc', PARENT_DATACLASS_NAME) - GET("/api/dataClasses/${getParentDataClassId()}/path/${makePath([node])}", STRING_ARG, true) - - then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() - - //No ID - when: - node = makePathNode('dc', CHILD_DATACLASS_NAME) - GET("/api/dataClasses/path/${makePath([node])}", STRING_ARG, true) - - then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() - - //With ID - when: - node = makePathNode('dc', CHILD_DATACLASS_NAME) - GET("/api/dataClasses/${getParentDataClassId()}/path/${makePath([node])}", STRING_ARG, true) - - then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() - } - - void 'Get DataClass by path and ID when logged in'() { - String node - + void 'DC03 : Get non-existent DataClass with DataModel by path and ID when logged in'() { given: loginReader() //No ID when: - node = makePathNode('dc', PARENT_DATACLASS_NAME) - GET("/api/dataClasses/path/${makePath([node])}", STRING_ARG, true) - - then: "The response is OK" - verifyJsonResponse OK, getExpectedParentDataClassJson() - - //With ID - when: - node = makePathNode('dc', PARENT_DATACLASS_NAME) - GET("/api/dataClasses/${getParentDataClassId()}/path/${makePath([node])}", STRING_ARG, true) - - then: "The response is OK" - verifyJsonResponse OK, getExpectedParentDataClassJson() - - //No ID - when: - node = makePathNode('dc', CHILD_DATACLASS_NAME) - GET("/api/dataClasses/path/${makePath([node])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dc', 'simple')) + GET("/api/dataModels/path/${makePath(node)}") then: "The response is OK" - verifyJsonResponse OK, getExpectedChildDataClassJson() + verifyNotFound(response, node) //With ID when: - node = makePathNode('dc', CHILD_DATACLASS_NAME) - GET("/api/dataClasses/${getChildDataClassId()}/path/${makePath([node])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dc', 'simple')) + GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath(node)}") then: "The response is OK" - verifyJsonResponse OK, getExpectedChildDataClassJson() + verifyNotFound(response, node) } - void 'Get DataElement by path and ID when not logged in'() { - String node1 - String node2 - + void 'DE01 : Get DataElement by path and ID when not logged in'() { //No ID when: - node1 = makePathNode('dc', CONTENT_DATACLASS_NAME) - node2 = makePathNode('de', DATA_ELEMENT_NAME) - GET("/api/dataClasses/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dc', CONTENT_DATACLASS_NAME), + makePathNode('de', DATA_ELEMENT_NAME)) + GET("/api/dataModels/path/${makePath(node)}") then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + verifyNotFound(response, node) //With ID when: - node1 = makePathNode('dc', CONTENT_DATACLASS_NAME) - node2 = makePathNode('de', DATA_ELEMENT_NAME) - GET("/api/dataClasses/${getContentDataClassId()}/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dc', CONTENT_DATACLASS_NAME), + makePathNode('de', DATA_ELEMENT_NAME)) + GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath(node)}") then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + verifyNotFound(response, getComplexDataModelId(), DataModel) } - void 'Get DataElement by DataClass, path and ID when logged in'() { - String node1 - String node2 - + void 'DE02 : Get DataElement by DataClass, path and ID when logged in'() { given: loginReader() //No ID when: - node1 = makePathNode('dc', CONTENT_DATACLASS_NAME) - node2 = makePathNode('de', DATA_ELEMENT_NAME) - GET("/api/dataClasses/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dc', CONTENT_DATACLASS_NAME), + makePathNode('de', DATA_ELEMENT_NAME)) + GET("/api/dataModels/path/${makePath(node)}", STRING_ARG) then: "The response is OK" verifyJsonResponse OK, getExpectedDataElementJson() //With ID when: - node1 = makePathNode('dc', CONTENT_DATACLASS_NAME) - node2 = makePathNode('de', DATA_ELEMENT_NAME) - GET("/api/dataClasses/${getContentDataClassId()}/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dc', CONTENT_DATACLASS_NAME), + makePathNode('de', DATA_ELEMENT_NAME)) + GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath(node)}", STRING_ARG) then: "The response is OK" verifyJsonResponse OK, getExpectedDataElementJson() } - void 'Get PrimitiveDataType by DataModel, path and ID when not logged in'() { - String node1 - String node2 - + void 'DT01 : Get PrimitiveDataType by DataModel, path and ID when not logged in'() { //No ID when: - node1 = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - node2 = makePathNode('dt', PRIMITIVE_DATA_TYPE_NAME) - GET("/api/dataModels/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dt', PRIMITIVE_DATA_TYPE_NAME)) + GET("/api/dataModels/path/${makePath(node)}") then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + verifyNotFound(response, node) //With ID when: - node1 = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - node2 = makePathNode('dt', PRIMITIVE_DATA_TYPE_NAME) - GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dt', PRIMITIVE_DATA_TYPE_NAME)) + GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath(node)}") then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + verifyNotFound(response, getComplexDataModelId(), DataModel) } - void 'Get PrimitiveDataType by DataModel, path and ID when logged in'() { - String node1 - String node2 - + void 'DT02 : Get PrimitiveDataType by DataModel, path and ID when logged in'() { given: loginReader() //No ID when: - node1 = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - node2 = makePathNode('dt', PRIMITIVE_DATA_TYPE_NAME) - GET("/api/dataModels/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dt', PRIMITIVE_DATA_TYPE_NAME)) + GET("/api/dataModels/path/${makePath(node)}", STRING_ARG) then: "The response is OK" verifyJsonResponse OK, getExpectedPrimitiveTypeJson() //With ID when: - node1 = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - node2 = makePathNode('dt', PRIMITIVE_DATA_TYPE_NAME) - GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dt', PRIMITIVE_DATA_TYPE_NAME)) + GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath(node)}", STRING_ARG) then: "The response is OK" verifyJsonResponse OK, getExpectedPrimitiveTypeJson() } - void 'Get EnumerationType by DataModel, path and ID when not logged in'() { - String node1 - String node2 - + void 'DT03 : Get EnumerationType by DataModel, path and ID when not logged in'() { //No ID when: - node1 = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - node2 = makePathNode('dt', ENUMERATION_DATA_TYPE_NAME) - GET("/api/dataModels/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dt', ENUMERATION_DATA_TYPE_NAME)) + GET("/api/dataModels/path/${makePath(node)}") then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + verifyNotFound(response, node) //With ID when: - node1 = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - node2 = makePathNode('dt', ENUMERATION_DATA_TYPE_NAME) - GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dt', ENUMERATION_DATA_TYPE_NAME)) + GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath(node)}") then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + verifyNotFound(response, getComplexDataModelId(), DataModel) } - void 'Get EnumerationType by DataModel, path and ID when logged in'() { - String node1 - String node2 - + void 'DT04 : Get EnumerationType by DataModel, path and ID when logged in'() { given: loginReader() //No ID when: - node1 = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - node2 = makePathNode('dt', ENUMERATION_DATA_TYPE_NAME) - GET("/api/dataModels/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dt', ENUMERATION_DATA_TYPE_NAME)) + GET("/api/dataModels/path/${makePath(node)}", STRING_ARG) then: "The response is OK" verifyJsonResponse OK, getExpectedEnumerationTypeJson() //With ID when: - node1 = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - node2 = makePathNode('dt', ENUMERATION_DATA_TYPE_NAME) - GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dt', ENUMERATION_DATA_TYPE_NAME)) + GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath(node)}", STRING_ARG) then: "The response is OK" verifyJsonResponse OK, getExpectedEnumerationTypeJson() } - void 'Get ReferenceType by DataModel, path and ID when not logged in'() { - String node1 - String node2 - + void 'DT05 : Get ReferenceType by DataModel, path and ID when not logged in'() { //No ID when: - node1 = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - node2 = makePathNode('dt', REFERENCE_DATA_TYPE_NAME) - GET("/api/dataModels/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dt', REFERENCE_DATA_TYPE_NAME)) + GET("/api/dataModels/path/${makePath(node)}") then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + verifyNotFound(response, node) //With ID when: - node1 = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - node2 = makePathNode('dt', REFERENCE_DATA_TYPE_NAME) - GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dt', REFERENCE_DATA_TYPE_NAME)) + GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath(node)}") then: "The response is Not Found" - verifyJsonResponse NOT_FOUND, getNotFoundPathJson() + verifyNotFound(response, getComplexDataModelId(), DataModel) } - void 'Get ReferenceType by DataModel, path and ID when logged in'() { - String node1 - String node2 - + void 'DT06 : Get ReferenceType by DataModel, path and ID when logged in'() { given: loginReader() //No ID when: - node1 = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - node2 = makePathNode('dt', REFERENCE_DATA_TYPE_NAME) - GET("/api/dataModels/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dt', REFERENCE_DATA_TYPE_NAME)) + GET("/api/dataModels/path/${makePath(node)}", STRING_ARG) then: "The response is OK" verifyJsonResponse OK, getExpectedReferenceTypeJson() //With ID when: - node1 = makePathNode('dm', COMPLEX_DATAMODEL_NAME) - node2 = makePathNode('dt', REFERENCE_DATA_TYPE_NAME) - GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath([node1, node2])}", STRING_ARG, true) + node = makePathNodes(makePathNode('dm', COMPLEX_DATAMODEL_NAME), + makePathNode('dt', REFERENCE_DATA_TYPE_NAME)) + GET("/api/dataModels/${getComplexDataModelId()}/path/${makePath(node)}", STRING_ARG) then: "The response is OK" verifyJsonResponse OK, getExpectedReferenceTypeJson() } - void 'Confirm all path prefixes are unique'() { + void 'PP : Confirm all path prefixes are unique'() { when: GET('path/prefixMappings', STRING_ARG) log.debug('{}', jsonResponseBody()) @@ -1186,4 +746,380 @@ class PathFunctionalSpec extends FunctionalSpec { assert it.value.size() == 1 } } + + String getExpectedSimpleTerminologyJson() { + return '''{ + "id": "${json-unit.matches:id}", + "domainType": "Terminology", + "label": "Simple Test Terminology", + "availableActions": [ + "show", + "comment" + ], + "lastUpdated": "${json-unit.matches:offsetDateTime}", + "classifiers": [ + { + "id": "${json-unit.matches:id}", + "label": "test classifier simple", + "lastUpdated": "${json-unit.matches:offsetDateTime}" + } + ], + "type": "Terminology", + "branchName": "main", + "documentationVersion": "1.0.0", + "finalised": false, + "readableByEveryone": false, + "readableByAuthenticatedUsers": false, + "author": "Test Bootstrap", + "organisation": "Oxford BRC", + "authority": { + "id": "${json-unit.matches:id}", + "url": "http://localhost", + "label": "Mauro Data Mapper" + } + }''' + } + + String getExpectedComplexTerminologyJson() { + return '''{ + "id": "${json-unit.matches:id}", + "domainType": "Terminology", + "label": "Complex Test Terminology", + "availableActions": [ + "show", + "comment" + ], + "lastUpdated": "${json-unit.matches:offsetDateTime}", + "classifiers": [ + { + "id": "${json-unit.matches:id}", + "label": "test classifier", + "lastUpdated": "${json-unit.matches:offsetDateTime}" + }, + { + "id": "${json-unit.matches:id}", + "label": "test classifier2", + "lastUpdated": "${json-unit.matches:offsetDateTime}" + } + ], + "type": "Terminology", + "branchName": "main", + "documentationVersion": "1.0.0", + "finalised": false, + "readableByEveryone": false, + "readableByAuthenticatedUsers": false, + "author": "Test Bootstrap", + "organisation": "Oxford BRC", + "authority": { + "id": "${json-unit.matches:id}", + "url": "http://localhost", + "label": "Mauro Data Mapper" + } + }''' + } + + String getExpectedSimpleCodeSetJson() { + return '''{ + "id": "${json-unit.matches:id}", + "domainType": "CodeSet", + "label": "Simple Test CodeSet", + "availableActions": [ + "show", + "createNewVersions", + "newForkModel", + "comment" + ], + "lastUpdated": "${json-unit.matches:offsetDateTime}", + "classifiers": [ + { + "id": "${json-unit.matches:id}", + "label": "test classifier", + "lastUpdated": "${json-unit.matches:offsetDateTime}" + } + ], + "type": "CodeSet", + "branchName": "main", + "documentationVersion": "1.0.0", + "finalised": true, + "readableByEveryone": false, + "readableByAuthenticatedUsers": false, + "dateFinalised": "${json-unit.matches:offsetDateTime}", + "author": "Test Bootstrap", + "organisation": "Oxford BRC", + "modelVersion": "1.0.0", + "authority": { + "id": "${json-unit.matches:id}", + "url": "http://localhost", + "label": "Mauro Data Mapper" + } + }''' + } + + String getExpectedSimpleTermJson() { + return '''{ + "id": "${json-unit.matches:id}", + "domainType": "Term", + "label": "STT01: Simple Test Term 01", + "model": "${json-unit.matches:id}", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Simple Test Terminology", + "domainType":"Terminology", + "finalised":false + } + ], + "availableActions": [ + "show", + "comment" + ], + "lastUpdated": "${json-unit.matches:offsetDateTime}", + "code": "STT01", + "definition": "Simple Test Term 01" + }''' + } + + String getExpectedComplexDataModelJson() { + return '''{ + "id": "${json-unit.matches:id}", + "domainType": "DataModel", + "label": "Complex Test DataModel", + "availableActions": [ + "show", + "comment" + ], + "lastUpdated": "${json-unit.matches:offsetDateTime}", + "classifiers": [ + { + "id": "${json-unit.matches:id}", + "label": "test classifier", + "lastUpdated": "${json-unit.matches:offsetDateTime}" + }, + { + "id": "${json-unit.matches:id}", + "label": "test classifier2", + "lastUpdated": "${json-unit.matches:offsetDateTime}" + } + ], + "type": "Data Standard", + "branchName": "main", + "documentationVersion": "1.0.0", + "finalised": false, + "readableByEveryone": false, + "readableByAuthenticatedUsers": false, + "author": "admin person", + "organisation": "brc", + "authority": { + "id": "${json-unit.matches:id}", + "url": "http://localhost", + "label": "Mauro Data Mapper" + } + }''' + } + + String getExpectedParentDataClassJson() { + return '''{ + "id": "${json-unit.matches:id}", + "domainType": "DataClass", + "label": "parent", + "model": "${json-unit.matches:id}", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Complex Test DataModel", + "domainType": "DataModel", + "finalised": false + } + ], + "availableActions": [ + "show", + "comment" + ], + "lastUpdated": "${json-unit.matches:offsetDateTime}", + "maxMultiplicity": -1, + "minMultiplicity": 1 + }''' + } + + String getExpectedChildDataClassJson() { + return '''{ + "id": "${json-unit.matches:id}", + "domainType": "DataClass", + "label": "child", + "model": "${json-unit.matches:id}", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Complex Test DataModel", + "domainType": "DataModel", + "finalised": false + }, + { + "id": "${json-unit.matches:id}", + "label": "parent", + "domainType": "DataClass" + } + ], + "availableActions": [ + "show", + "comment" + ], + "lastUpdated": "${json-unit.matches:offsetDateTime}", + "parentDataClass": "${json-unit.matches:id}" + }''' + } + + String getExpectedDataElementJson() { + return '''{ + "id": "${json-unit.matches:id}", + "domainType": "DataElement", + "label": "ele1", + "model": "${json-unit.matches:id}", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Complex Test DataModel", + "domainType": "DataModel", + "finalised": false + }, + { + "id": "${json-unit.matches:id}", + "label": "content", + "domainType": "DataClass" + } + ], + "availableActions": [ + "show", + "comment" + ], + "lastUpdated": "${json-unit.matches:offsetDateTime}", + "dataClass": "${json-unit.matches:id}", + "dataType": { + "id": "${json-unit.matches:id}", + "domainType": "PrimitiveType", + "label": "string", + "model": "${json-unit.matches:id}", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Complex Test DataModel", + "domainType": "DataModel", + "finalised": false + } + ] + }, + "maxMultiplicity": 20, + "minMultiplicity": 0 + }''' + } + + String getExpectedPrimitiveTypeJson() { + return '''{ + "id": "${json-unit.matches:id}", + "domainType": "PrimitiveType", + "label": "integer", + "model": "${json-unit.matches:id}", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Complex Test DataModel", + "domainType": "DataModel", + "finalised": false + } + ], + "availableActions": [ + "show", + "comment" + ], + "lastUpdated": "${json-unit.matches:offsetDateTime}" + }''' + } + + String getExpectedEnumerationTypeJson() { + return '''{ + "id": "${json-unit.matches:id}", + "domainType": "EnumerationType", + "label": "yesnounknown", + "model": "${json-unit.matches:id}", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Complex Test DataModel", + "domainType": "DataModel", + "finalised": false + } + ], + "availableActions": [ + "show", + "comment" + ], + "lastUpdated": "${json-unit.matches:offsetDateTime}", + "enumerationValues": [ + { + "index": 1, + "id": "${json-unit.matches:id}", + "key": "N", + "value": "No", + "category": null + }, + { + "index": 2, + "id": "${json-unit.matches:id}", + "key": "U", + "value": "Unknown", + "category": null + }, + { + "index": 0, + "id": "${json-unit.matches:id}", + "key": "Y", + "value": "Yes", + "category": null + } + ] + }''' + } + + String getExpectedReferenceTypeJson() { + return '''{ + "id": "${json-unit.matches:id}", + "domainType": "ReferenceType", + "label": "child", + "model": "${json-unit.matches:id}", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Complex Test DataModel", + "domainType": "DataModel", + "finalised": false + } + ], + "availableActions": [ + "show", + "comment" + ], + "lastUpdated": "${json-unit.matches:offsetDateTime}", + "referenceClass": { + "id": "${json-unit.matches:id}", + "domainType": "DataClass", + "label": "child", + "model": "${json-unit.matches:id}", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Complex Test DataModel", + "domainType": "DataModel", + "finalised": false + }, + { + "id": "${json-unit.matches:id}", + "label": "parent", + "domainType": "DataClass" + } + ], + "parentDataClass": "${json-unit.matches:id}" + } + }''' + } + } From 6d823d50efb6489913348bb807a6c36299c92cb5 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Wed, 14 Jul 2021 09:26:53 +0100 Subject: [PATCH 40/82] Fix issues in tests due to pathing changes --- ...DataFlowImporterProviderServiceSpec.groovy | 14 +++++----- .../datamodel/DataModelFunctionalSpec.groovy | 27 +++++++++---------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/mdm-plugin-dataflow/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/test/provider/DataBindDataFlowImporterProviderServiceSpec.groovy b/mdm-plugin-dataflow/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/test/provider/DataBindDataFlowImporterProviderServiceSpec.groovy index 78d4b7eaf3..d8b6990d0b 100644 --- a/mdm-plugin-dataflow/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/test/provider/DataBindDataFlowImporterProviderServiceSpec.groovy +++ b/mdm-plugin-dataflow/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/test/provider/DataBindDataFlowImporterProviderServiceSpec.groovy @@ -29,7 +29,6 @@ import uk.ac.ox.softeng.maurodatamapper.dataflow.provider.importer.DataBindDataF import grails.gorm.transactions.Rollback import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired -import spock.lang.Stepwise /** * @since 11/01/2021 @@ -37,7 +36,6 @@ import spock.lang.Stepwise @Rollback @Slf4j @SuppressWarnings("DuplicatedCode") -@Stepwise abstract class DataBindDataFlowImporterProviderServiceSpec extends BaseImportExportSpec { abstract K getDataFlowImporterService() @@ -269,7 +267,7 @@ abstract class DataBindDataFlowImporterProviderServiceSpec { @Shared @@ -2249,10 +2248,10 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { verifyResponse CREATED, response POST("$id/dataClasses", [label: 'existingClass']) verifyResponse CREATED, response - String existingClass = responseBody().id - POST("$id/dataClasses/$existingClass/dataClasses", [label: 'deleteLeftOnlyFromExistingClass']) + String caExistingClass = responseBody().id + POST("$id/dataClasses/$caExistingClass/dataClasses", [label: 'deleteLeftOnlyFromExistingClass']) verifyResponse CREATED, response - POST("$id/dataClasses/$existingClass/dataClasses", [label: 'deleteRightOnlyFromExistingClass']) + POST("$id/dataClasses/$caExistingClass/dataClasses", [label: 'deleteRightOnlyFromExistingClass']) verifyResponse CREATED, response PUT("$id/finalise", [versionChangeType: 'Major']) @@ -2266,8 +2265,8 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { GET("$source/path/dm%3A%7Cdc%3AexistingClass") verifyResponse OK, response - existingClass = responseBody().id - GET("dataClasses/$existingClass/path/dc%3A%7Cdc%3AdeleteLeftOnlyFromExistingClass", MAP_ARG, true) + String sourceExistingClass = responseBody().id + GET("$source/path/dm%3A%7Cdc%3AexistingClass%7Cdc%3AdeleteLeftOnlyFromExistingClass") verifyResponse OK, response String deleteLeftOnlyFromExistingClass = responseBody().id GET("$source/path/dm%3A%7Cdc%3AdeleteLeftOnly") @@ -2295,7 +2294,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { DELETE("$source/dataClasses/$deleteAndDelete") verifyResponse NO_CONTENT, response - DELETE("$source/dataClasses/$existingClass/dataClasses/$deleteLeftOnlyFromExistingClass") + DELETE("$source/dataClasses/$sourceExistingClass/dataClasses/$deleteLeftOnlyFromExistingClass") verifyResponse NO_CONTENT, response DELETE("$source/dataClasses/$deleteLeftOnly") verifyResponse NO_CONTENT, response @@ -2311,7 +2310,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { PUT("$source/dataClasses/$modifyAndModifyReturningDifference", [description: 'DescriptionLeft']) verifyResponse OK, response - POST("$source/dataClasses/$existingClass/dataClasses", [label: 'addLeftToExistingClass']) + POST("$source/dataClasses/$sourceExistingClass/dataClasses", [label: 'addLeftToExistingClass']) verifyResponse CREATED, response String addLeftToExistingClass = responseBody().id POST("$source/dataClasses", [label: 'addLeftOnly']) @@ -2328,8 +2327,8 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { GET("$target/path/dm%3A%7Cdc%3AexistingClass") verifyResponse OK, response - existingClass = responseBody().id - GET("dataClasses/$existingClass/path/dc%3A%7Cdc%3AdeleteRightOnlyFromExistingClass", MAP_ARG, true) + String targetExistingClass = responseBody().id + GET("$target/path/dm%3A%7Cdc%3AexistingClass%7Cdc%3AdeleteRightOnlyFromExistingClass") verifyResponse OK, response String deleteRightOnlyFromExistingClass = responseBody().id GET("$target/path/dm%3A%7Cdc%3AdeleteRightOnly") @@ -2355,7 +2354,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { verifyResponse OK, response modifyAndModifyReturningDifference = responseBody().id - DELETE("$target/dataClasses/$existingClass/dataClasses/$deleteRightOnlyFromExistingClass") + DELETE("$target/dataClasses/$targetExistingClass/dataClasses/$deleteRightOnlyFromExistingClass") verifyResponse NO_CONTENT, response DELETE("$target/dataClasses/$deleteRightOnly") verifyResponse NO_CONTENT, response @@ -2373,7 +2372,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { PUT("$target/dataClasses/$modifyAndModifyReturningDifference", [description: 'DescriptionRight']) verifyResponse OK, response - POST("$target/dataClasses/$existingClass/dataClasses", [label: 'addRightToExistingClass']) + POST("$target/dataClasses/$targetExistingClass/dataClasses", [label: 'addRightToExistingClass']) verifyResponse CREATED, response POST("$target/dataClasses", [label: 'addRightOnly']) verifyResponse CREATED, response @@ -2393,7 +2392,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { GET("$target/path/dm%3A%7Cdc%3AmodifyLeftOnly") verifyResponse OK, response modifyLeftOnly = responseBody().id - GET("dataClasses/$existingClass/path/dc%3A%7Cdc%3AdeleteLeftOnlyFromExistingClass", MAP_ARG, true) + GET("$target/path/dm%3A%7Cdc%3AexistingClass%7Cdc%3AdeleteLeftOnlyFromExistingClass") verifyResponse OK, response deleteLeftOnlyFromExistingClass = responseBody().id @@ -2401,7 +2400,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { source : source, target : target, // For legacy testing - existingClass : existingClass, + existingClass : targetExistingClass, deleteLeftOnlyFromExistingClass : deleteLeftOnlyFromExistingClass, deleteLeftOnly : deleteLeftOnly, deleteAndDelete : deleteAndDelete, From 067e46ae400859ee2fabb6471408b7718212cbbe Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Wed, 14 Jul 2021 09:27:16 +0100 Subject: [PATCH 41/82] Remove the stepwise annotation from all files --- .../core/rest/transport/tree/TreeItemSpec.groovy | 3 --- .../core/util/test/MultiFacetItemAwareServiceSpec.groovy | 3 --- .../DataBindImportAndDefaultExporterServiceSpec.groovy | 2 -- .../DataBindDataModelImporterProviderServiceSpec.groovy | 2 -- .../DataBindImportAndDefaultExporterServiceSpec.groovy | 2 -- .../datamodel/item/DataClassServiceSpec.groovy | 2 -- .../datamodel/item/DataElementServiceSpec.groovy | 2 -- .../datamodel/item/datatype/DataTypeServiceSpec.groovy | 2 -- .../datamodel/item/datatype/EnumerationTypeServiceSpec.groovy | 2 -- .../datamodel/item/datatype/ModelDataTypeServiceSpec.groovy | 2 -- .../datamodel/item/datatype/PrimitiveTypeServiceSpec.groovy | 2 -- .../datamodel/item/datatype/ReferenceTypeServiceSpec.groovy | 2 -- .../datatype/enumeration/EnumerationValueServiceSpec.groovy | 3 --- .../referencedata/item/ReferenceDataElementServiceSpec.groovy | 2 -- .../item/datatype/ReferenceDataTypeServiceSpec.groovy | 3 --- .../item/datatype/ReferenceEnumerationTypeServiceSpec.groovy | 2 -- .../item/datatype/ReferencePrimitiveTypeServiceSpec.groovy | 2 -- .../enumeration/ReferenceEnumerationValueServiceSpec.groovy | 3 --- .../groovy/path/TerminologyPathServiceSpec.groovy | 2 -- .../terminology/TerminologyServiceIntegrationSpec.groovy | 1 - ...aBindTerminologyImportAndDefaultExporterServiceSpec.groovy | 2 -- .../DataBindTerminologyImporterProviderServiceSpec.groovy | 1 - .../test/unit/core/MultiFacetItemAwareServiceSpec.groovy | 3 --- .../core/container/VersionedFolderFunctionalSpec.groovy | 2 -- .../federation/SubscribedCatalogueFunctionalSpec.groovy | 2 -- .../testing/functional/ModelImportFunctionalSpec.groovy | 2 -- .../functional/ReadOnlyUserAccessFunctionalSpec.groovy | 2 -- .../UserAccessAndCopyingInDataModelsFunctionalSpec.groovy | 2 -- .../UserAccessAndPermissionChangingFunctionalSpec.groovy | 2 -- .../testing/functional/UserAccessFunctionalSpec.groovy | 4 +--- .../functional/UserAccessWithoutUpdatingFunctionalSpec.groovy | 2 -- 31 files changed, 1 insertion(+), 67 deletions(-) diff --git a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/tree/TreeItemSpec.groovy b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/tree/TreeItemSpec.groovy index e85630eed4..efa175647c 100644 --- a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/tree/TreeItemSpec.groovy +++ b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/tree/TreeItemSpec.groovy @@ -27,14 +27,11 @@ import uk.ac.ox.softeng.maurodatamapper.core.util.test.BasicModel import uk.ac.ox.softeng.maurodatamapper.core.util.test.BasicModelItem import uk.ac.ox.softeng.maurodatamapper.test.unit.BaseUnitSpec -import spock.lang.Stepwise - import static uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress.getUNIT_TEST /** * @since 30/10/2017 */ -@Stepwise class TreeItemSpec extends BaseUnitSpec { BasicModel basicModel Folder misc diff --git a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/MultiFacetItemAwareServiceSpec.groovy b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/MultiFacetItemAwareServiceSpec.groovy index 3b073c131f..f50712bf5f 100644 --- a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/MultiFacetItemAwareServiceSpec.groovy +++ b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/util/test/MultiFacetItemAwareServiceSpec.groovy @@ -22,12 +22,9 @@ import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.MultiFacetItemAware import uk.ac.ox.softeng.maurodatamapper.core.traits.service.MultiFacetItemAwareService import uk.ac.ox.softeng.maurodatamapper.test.unit.core.MultiFacetItemAwareServiceSpec as FrameworkMultiFacetItemAwareServiceSpec -import spock.lang.Stepwise - /** * @since 03/02/2020 */ -@Stepwise abstract class MultiFacetItemAwareServiceSpec extends FrameworkMultiFacetItemAwareServiceSpec { diff --git a/mdm-plugin-dataflow/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy b/mdm-plugin-dataflow/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy index dc17490b6f..047221ae34 100644 --- a/mdm-plugin-dataflow/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy +++ b/mdm-plugin-dataflow/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy @@ -28,7 +28,6 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import grails.gorm.transactions.Rollback import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired -import spock.lang.Stepwise import spock.lang.Unroll import java.nio.charset.Charset @@ -38,7 +37,6 @@ import java.nio.charset.Charset */ @Rollback @Slf4j -@Stepwise abstract class DataBindImportAndDefaultExporterServiceSpec extends BaseImportExportSpec { diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/test/provider/DataBindDataModelImporterProviderServiceSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/test/provider/DataBindDataModelImporterProviderServiceSpec.groovy index 811444285b..ffa7a26388 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/test/provider/DataBindDataModelImporterProviderServiceSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/test/provider/DataBindDataModelImporterProviderServiceSpec.groovy @@ -38,7 +38,6 @@ import grails.gorm.transactions.Rollback import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired import spock.lang.Shared -import spock.lang.Stepwise /** * @since 15/11/2017 @@ -46,7 +45,6 @@ import spock.lang.Stepwise @Rollback @Slf4j @SuppressWarnings("DuplicatedCode") -@Stepwise abstract class DataBindDataModelImporterProviderServiceSpec extends BaseImportExportSpec { abstract K getImporterService() diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy index 86dd4f835d..1fa11c84d8 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy @@ -29,7 +29,6 @@ import grails.gorm.transactions.Rollback import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired import spock.lang.Shared -import spock.lang.Stepwise import spock.lang.Unroll import java.nio.charset.Charset @@ -41,7 +40,6 @@ import java.nio.file.Path */ @Rollback @Slf4j -@Stepwise abstract class DataBindImportAndDefaultExporterServiceSpec extends BaseImportExportSpec { diff --git a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassServiceSpec.groovy b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassServiceSpec.groovy index bbc6395eef..9618194622 100644 --- a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassServiceSpec.groovy +++ b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassServiceSpec.groovy @@ -31,10 +31,8 @@ import uk.ac.ox.softeng.maurodatamapper.test.unit.service.CatalogueItemServiceSp import grails.testing.services.ServiceUnitTest import groovy.util.logging.Slf4j -import spock.lang.Stepwise @Slf4j -@Stepwise class DataClassServiceSpec extends CatalogueItemServiceSpec implements ServiceUnitTest { DataModel dataModel diff --git a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementServiceSpec.groovy b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementServiceSpec.groovy index 0b58dbce11..93e78a3b5d 100644 --- a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementServiceSpec.groovy +++ b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementServiceSpec.groovy @@ -30,10 +30,8 @@ import uk.ac.ox.softeng.maurodatamapper.test.unit.service.CatalogueItemServiceSp import grails.testing.services.ServiceUnitTest import groovy.util.logging.Slf4j -import spock.lang.Stepwise @Slf4j -@Stepwise class DataElementServiceSpec extends CatalogueItemServiceSpec implements ServiceUnitTest { UUID simpleId diff --git a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataTypeServiceSpec.groovy b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataTypeServiceSpec.groovy index 4359e6965e..c75c8ddcd1 100644 --- a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataTypeServiceSpec.groovy +++ b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataTypeServiceSpec.groovy @@ -30,10 +30,8 @@ import uk.ac.ox.softeng.maurodatamapper.test.unit.service.CatalogueItemServiceSp import grails.testing.services.ServiceUnitTest import groovy.util.logging.Slf4j import org.spockframework.util.InternalSpockError -import spock.lang.Stepwise @Slf4j -@Stepwise class DataTypeServiceSpec extends CatalogueItemServiceSpec implements ServiceUnitTest { DataModel dataModel diff --git a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/EnumerationTypeServiceSpec.groovy b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/EnumerationTypeServiceSpec.groovy index 0d8097bf8c..cfaca89109 100644 --- a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/EnumerationTypeServiceSpec.groovy +++ b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/EnumerationTypeServiceSpec.groovy @@ -42,10 +42,8 @@ import uk.ac.ox.softeng.maurodatamapper.test.unit.BaseUnitSpec import grails.testing.services.ServiceUnitTest import groovy.util.logging.Slf4j -import spock.lang.Stepwise @Slf4j -@Stepwise class EnumerationTypeServiceSpec extends BaseUnitSpec implements ServiceUnitTest { DataModel dataModel diff --git a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ModelDataTypeServiceSpec.groovy b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ModelDataTypeServiceSpec.groovy index 04496b1090..a2533434a7 100644 --- a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ModelDataTypeServiceSpec.groovy +++ b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ModelDataTypeServiceSpec.groovy @@ -42,10 +42,8 @@ import uk.ac.ox.softeng.maurodatamapper.test.unit.BaseUnitSpec import grails.testing.services.ServiceUnitTest import groovy.util.logging.Slf4j -import spock.lang.Stepwise @Slf4j -@Stepwise class ModelDataTypeServiceSpec extends BaseUnitSpec implements ServiceUnitTest { DataModel dataModel diff --git a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/PrimitiveTypeServiceSpec.groovy b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/PrimitiveTypeServiceSpec.groovy index c314b8a260..cb0649164f 100644 --- a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/PrimitiveTypeServiceSpec.groovy +++ b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/PrimitiveTypeServiceSpec.groovy @@ -42,10 +42,8 @@ import uk.ac.ox.softeng.maurodatamapper.test.unit.BaseUnitSpec import grails.testing.services.ServiceUnitTest import groovy.util.logging.Slf4j -import spock.lang.Stepwise @Slf4j -@Stepwise class PrimitiveTypeServiceSpec extends BaseUnitSpec implements ServiceUnitTest { DataModel dataModel diff --git a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ReferenceTypeServiceSpec.groovy b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ReferenceTypeServiceSpec.groovy index 16c99edfbc..3e10393e0c 100644 --- a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ReferenceTypeServiceSpec.groovy +++ b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ReferenceTypeServiceSpec.groovy @@ -42,10 +42,8 @@ import uk.ac.ox.softeng.maurodatamapper.test.unit.BaseUnitSpec import grails.testing.services.ServiceUnitTest import groovy.util.logging.Slf4j -import spock.lang.Stepwise @Slf4j -@Stepwise class ReferenceTypeServiceSpec extends BaseUnitSpec implements ServiceUnitTest { DataModel dataModel diff --git a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValueServiceSpec.groovy b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValueServiceSpec.groovy index 9967fed2c1..f18df309ec 100644 --- a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValueServiceSpec.groovy +++ b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValueServiceSpec.groovy @@ -17,7 +17,6 @@ */ package uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.enumeration - import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLink import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkType import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel @@ -32,10 +31,8 @@ import uk.ac.ox.softeng.maurodatamapper.test.unit.service.CatalogueItemServiceSp import grails.testing.services.ServiceUnitTest import groovy.util.logging.Slf4j -import spock.lang.Stepwise @Slf4j -@Stepwise class EnumerationValueServiceSpec extends CatalogueItemServiceSpec implements ServiceUnitTest { DataModel dataModel diff --git a/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataElementServiceSpec.groovy b/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataElementServiceSpec.groovy index 3afdc04e2b..f48f750162 100644 --- a/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataElementServiceSpec.groovy +++ b/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataElementServiceSpec.groovy @@ -29,10 +29,8 @@ import uk.ac.ox.softeng.maurodatamapper.test.unit.service.CatalogueItemServiceSp import grails.testing.services.ServiceUnitTest import groovy.util.logging.Slf4j -import spock.lang.Stepwise @Slf4j -@Stepwise class ReferenceDataElementServiceSpec extends CatalogueItemServiceSpec implements ServiceUnitTest { UUID simpleId diff --git a/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferenceDataTypeServiceSpec.groovy b/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferenceDataTypeServiceSpec.groovy index 1a44d2e1ea..54ffa46684 100644 --- a/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferenceDataTypeServiceSpec.groovy +++ b/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferenceDataTypeServiceSpec.groovy @@ -17,7 +17,6 @@ */ package uk.ac.ox.softeng.maurodatamapper.referencedata.item.datatype - import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkType import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModel import uk.ac.ox.softeng.maurodatamapper.referencedata.facet.ReferenceSummaryMetadataService @@ -29,10 +28,8 @@ import uk.ac.ox.softeng.maurodatamapper.test.unit.service.CatalogueItemServiceSp import grails.testing.services.ServiceUnitTest import groovy.util.logging.Slf4j -import spock.lang.Stepwise @Slf4j -@Stepwise class ReferenceDataTypeServiceSpec extends CatalogueItemServiceSpec implements ServiceUnitTest { ReferenceDataModel referenceDataModel diff --git a/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferenceEnumerationTypeServiceSpec.groovy b/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferenceEnumerationTypeServiceSpec.groovy index e07bad412d..5c6d9ae214 100644 --- a/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferenceEnumerationTypeServiceSpec.groovy +++ b/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferenceEnumerationTypeServiceSpec.groovy @@ -41,10 +41,8 @@ import uk.ac.ox.softeng.maurodatamapper.test.unit.BaseUnitSpec import grails.testing.services.ServiceUnitTest import groovy.util.logging.Slf4j -import spock.lang.Stepwise @Slf4j -@Stepwise class ReferenceEnumerationTypeServiceSpec extends BaseUnitSpec implements ServiceUnitTest { ReferenceDataModel referenceDataModel diff --git a/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferencePrimitiveTypeServiceSpec.groovy b/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferencePrimitiveTypeServiceSpec.groovy index 0c5712735a..f6c8f1374f 100644 --- a/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferencePrimitiveTypeServiceSpec.groovy +++ b/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferencePrimitiveTypeServiceSpec.groovy @@ -41,10 +41,8 @@ import uk.ac.ox.softeng.maurodatamapper.test.unit.BaseUnitSpec import grails.testing.services.ServiceUnitTest import groovy.util.logging.Slf4j -import spock.lang.Stepwise @Slf4j -@Stepwise class ReferencePrimitiveTypeServiceSpec extends BaseUnitSpec implements ServiceUnitTest { ReferenceDataModel referenceReferenceDataModel diff --git a/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/enumeration/ReferenceEnumerationValueServiceSpec.groovy b/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/enumeration/ReferenceEnumerationValueServiceSpec.groovy index 5e20818b2c..a4213b85c2 100644 --- a/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/enumeration/ReferenceEnumerationValueServiceSpec.groovy +++ b/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/enumeration/ReferenceEnumerationValueServiceSpec.groovy @@ -17,7 +17,6 @@ */ package uk.ac.ox.softeng.maurodatamapper.referencedata.item.datatype.enumeration - import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModel import uk.ac.ox.softeng.maurodatamapper.referencedata.facet.ReferenceSummaryMetadataService import uk.ac.ox.softeng.maurodatamapper.referencedata.item.ReferenceDataElement @@ -28,10 +27,8 @@ import uk.ac.ox.softeng.maurodatamapper.test.unit.service.CatalogueItemServiceSp import grails.testing.services.ServiceUnitTest import groovy.util.logging.Slf4j -import spock.lang.Stepwise @Slf4j -@Stepwise class ReferenceEnumerationValueServiceSpec extends CatalogueItemServiceSpec implements ServiceUnitTest { ReferenceDataModel referenceDataModel diff --git a/mdm-plugin-terminology/src/integration-test/groovy/path/TerminologyPathServiceSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/path/TerminologyPathServiceSpec.groovy index 6c229335f6..f7ae4bf945 100644 --- a/mdm-plugin-terminology/src/integration-test/groovy/path/TerminologyPathServiceSpec.groovy +++ b/mdm-plugin-terminology/src/integration-test/groovy/path/TerminologyPathServiceSpec.groovy @@ -30,7 +30,6 @@ import grails.gorm.transactions.Rollback import grails.testing.mixin.integration.Integration import groovy.util.logging.Slf4j import spock.lang.PendingFeature -import spock.lang.Stepwise import java.time.OffsetDateTime import java.time.ZoneOffset @@ -38,7 +37,6 @@ import java.time.ZoneOffset @Slf4j @Integration @Rollback -@Stepwise class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { PathService pathService diff --git a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy index 96c379aa8f..73e881a4b8 100644 --- a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy +++ b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy @@ -31,7 +31,6 @@ import org.spockframework.util.Assert @Slf4j @Integration @Rollback -//@Stepwise class TerminologyServiceIntegrationSpec extends BaseTerminologyIntegrationSpec { @Override diff --git a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/test/provider/DataBindTerminologyImportAndDefaultExporterServiceSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/test/provider/DataBindTerminologyImportAndDefaultExporterServiceSpec.groovy index 0f53dfded1..7019d1bfde 100644 --- a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/test/provider/DataBindTerminologyImportAndDefaultExporterServiceSpec.groovy +++ b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/test/provider/DataBindTerminologyImportAndDefaultExporterServiceSpec.groovy @@ -29,7 +29,6 @@ import grails.gorm.transactions.Rollback import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired import spock.lang.Shared -import spock.lang.Stepwise import spock.lang.Unroll import java.nio.charset.Charset @@ -39,7 +38,6 @@ import java.nio.charset.Charset */ @Rollback @Slf4j -@Stepwise abstract class DataBindTerminologyImportAndDefaultExporterServiceSpec extends BaseImportExportTerminologySpec { diff --git a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/test/provider/DataBindTerminologyImporterProviderServiceSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/test/provider/DataBindTerminologyImporterProviderServiceSpec.groovy index eb22b6497b..3702126ceb 100644 --- a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/test/provider/DataBindTerminologyImporterProviderServiceSpec.groovy +++ b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/test/provider/DataBindTerminologyImporterProviderServiceSpec.groovy @@ -35,7 +35,6 @@ import spock.lang.Shared @Rollback @Slf4j @SuppressWarnings("DuplicatedCode") -//@Stepwise abstract class DataBindTerminologyImporterProviderServiceSpec extends BaseImportExportTerminologySpec { abstract K getImporterService() diff --git a/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/unit/core/MultiFacetItemAwareServiceSpec.groovy b/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/unit/core/MultiFacetItemAwareServiceSpec.groovy index fccb24822e..98c59cb704 100644 --- a/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/unit/core/MultiFacetItemAwareServiceSpec.groovy +++ b/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/unit/core/MultiFacetItemAwareServiceSpec.groovy @@ -22,12 +22,9 @@ import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.MultiFacetItemAware import uk.ac.ox.softeng.maurodatamapper.core.traits.service.MultiFacetItemAwareService import uk.ac.ox.softeng.maurodatamapper.test.unit.BaseUnitSpec -import spock.lang.Stepwise - /** * @since 03/02/2020 */ -@Stepwise abstract class MultiFacetItemAwareServiceSpec extends BaseUnitSpec { abstract MultiFacetAware getMultiFacetAwareItem() diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy index f52fbda78f..90bc3003ef 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy @@ -35,7 +35,6 @@ import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus import org.springframework.beans.factory.annotation.Autowired import spock.lang.Ignore -import spock.lang.Stepwise import spock.lang.Unroll import java.util.regex.Pattern @@ -63,7 +62,6 @@ import static io.micronaut.http.HttpStatus.OK */ @Integration @Slf4j -@Stepwise class VersionedFolderFunctionalSpec extends UserAccessAndPermissionChangingFunctionalSpec { 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/federation/SubscribedCatalogueFunctionalSpec.groovy index 1da467b82a..3436da5731 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/federation/SubscribedCatalogueFunctionalSpec.groovy @@ -25,7 +25,6 @@ import grails.gorm.transactions.Transactional import grails.testing.mixin.integration.Integration import groovy.util.logging.Slf4j import io.micronaut.http.HttpResponse -import spock.lang.Stepwise import java.time.LocalDate import java.time.format.DateTimeFormatter @@ -41,7 +40,6 @@ import static io.micronaut.http.HttpStatus.UNPROCESSABLE_ENTITY @Integration @Slf4j -@Stepwise class SubscribedCatalogueFunctionalSpec extends FunctionalSpec { @Override diff --git a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelImportFunctionalSpec.groovy b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelImportFunctionalSpec.groovy index 1329fb2ddb..bd68883476 100644 --- a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelImportFunctionalSpec.groovy +++ b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelImportFunctionalSpec.groovy @@ -20,7 +20,6 @@ package uk.ac.ox.softeng.maurodatamapper.testing.functional import groovy.util.logging.Slf4j import io.micronaut.http.HttpStatus import spock.lang.Ignore -import spock.lang.Stepwise import static io.micronaut.http.HttpStatus.CREATED import static io.micronaut.http.HttpStatus.OK @@ -34,7 +33,6 @@ import static io.micronaut.http.HttpStatus.OK * * */ -@Stepwise @Slf4j @Ignore('No longer relevant') abstract class ModelImportFunctionalSpec extends FunctionalSpec { diff --git a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ReadOnlyUserAccessFunctionalSpec.groovy b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ReadOnlyUserAccessFunctionalSpec.groovy index ed6a9e7a9c..6c867517a6 100644 --- a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ReadOnlyUserAccessFunctionalSpec.groovy +++ b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ReadOnlyUserAccessFunctionalSpec.groovy @@ -19,7 +19,6 @@ package uk.ac.ox.softeng.maurodatamapper.testing.functional import groovy.util.logging.Slf4j import io.micronaut.http.HttpResponse -import spock.lang.Stepwise import static io.micronaut.http.HttpStatus.NOT_FOUND import static io.micronaut.http.HttpStatus.OK @@ -32,7 +31,6 @@ import static io.micronaut.http.HttpStatus.OK * | GET | /api/${resourcePath}/${id} | Action: show | * */ -@Stepwise @Slf4j abstract class ReadOnlyUserAccessFunctionalSpec extends FunctionalSpec { diff --git a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessAndCopyingInDataModelsFunctionalSpec.groovy b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessAndCopyingInDataModelsFunctionalSpec.groovy index ee16ea3953..a03e0caf24 100644 --- a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessAndCopyingInDataModelsFunctionalSpec.groovy +++ b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessAndCopyingInDataModelsFunctionalSpec.groovy @@ -29,7 +29,6 @@ import grails.testing.spock.OnceBefore import groovy.util.logging.Slf4j import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus -import spock.lang.Stepwise import static io.micronaut.http.HttpStatus.CREATED import static io.micronaut.http.HttpStatus.NOT_FOUND @@ -47,7 +46,6 @@ import static io.micronaut.http.HttpStatus.OK * * @see uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkController* @since 18/05/2018 */ -@Stepwise @Slf4j abstract class UserAccessAndCopyingInDataModelsFunctionalSpec extends UserAccessFunctionalSpec { diff --git a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessAndPermissionChangingFunctionalSpec.groovy b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessAndPermissionChangingFunctionalSpec.groovy index 5f65c07a01..1ddd7129c7 100644 --- a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessAndPermissionChangingFunctionalSpec.groovy +++ b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessAndPermissionChangingFunctionalSpec.groovy @@ -25,7 +25,6 @@ import uk.ac.ox.softeng.maurodatamapper.security.utils.SecurityUtils import grails.gorm.transactions.Transactional import grails.testing.spock.OnceBefore import groovy.util.logging.Slf4j -import spock.lang.Stepwise import static uk.ac.ox.softeng.maurodatamapper.util.GormUtils.checkAndSave @@ -50,7 +49,6 @@ import static io.micronaut.http.HttpStatus.OK * | POST | /api/${securableResourceDomainType}/${securableResourceId}/groupRoles/${groupRoleId}/userGroups/${userGroupId} | Action: save * */ -@Stepwise @Slf4j abstract class UserAccessAndPermissionChangingFunctionalSpec extends UserAccessFunctionalSpec { diff --git a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessFunctionalSpec.groovy b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessFunctionalSpec.groovy index 0c0e261891..b2d9854d66 100644 --- a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessFunctionalSpec.groovy +++ b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessFunctionalSpec.groovy @@ -22,12 +22,11 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.Edit import grails.gorm.transactions.Transactional import groovy.util.logging.Slf4j import io.micronaut.http.HttpResponse -import spock.lang.Stepwise import java.util.regex.Pattern -import static io.micronaut.http.HttpStatus.OK import static io.micronaut.http.HttpStatus.CREATED +import static io.micronaut.http.HttpStatus.OK import static io.micronaut.http.HttpStatus.UNPROCESSABLE_ENTITY /** @@ -44,7 +43,6 @@ import static io.micronaut.http.HttpStatus.UNPROCESSABLE_ENTITY * * @see uk.ac.ox.softeng.maurodatamapper.core.facet.EditController */ -@Stepwise @Slf4j abstract class UserAccessFunctionalSpec extends UserAccessWithoutUpdatingFunctionalSpec { diff --git a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessWithoutUpdatingFunctionalSpec.groovy b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessWithoutUpdatingFunctionalSpec.groovy index 613e38eb3b..ed99dfa5b2 100644 --- a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessWithoutUpdatingFunctionalSpec.groovy +++ b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/UserAccessWithoutUpdatingFunctionalSpec.groovy @@ -33,7 +33,6 @@ import io.micronaut.http.HttpStatus import org.apache.commons.lang3.NotImplementedException import org.junit.Assert import org.springframework.beans.factory.annotation.Autowired -import spock.lang.Stepwise import static uk.ac.ox.softeng.maurodatamapper.util.GormUtils.checkAndSave @@ -52,7 +51,6 @@ import static io.micronaut.http.HttpStatus.UNPROCESSABLE_ENTITY * | GET | /api/${resourcePath}/${id} | Action: show | [inherited test] * */ -@Stepwise @Slf4j abstract class UserAccessWithoutUpdatingFunctionalSpec extends ReadOnlyUserAccessFunctionalSpec { From 4aa5e7c29ddc1ab3836d2f10c0cf221a9c545495 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Thu, 15 Jul 2021 17:04:00 +0100 Subject: [PATCH 42/82] Update the diffable to use the pathIdentifier as the core of the diffIdentifier --- .../maurodatamapper/core/facet/Annotation.groovy | 5 ----- .../maurodatamapper/core/facet/Metadata.groovy | 5 ----- .../softeng/maurodatamapper/core/facet/Rule.groovy | 5 ----- .../core/facet/rule/RuleRepresentation.groovy | 5 ----- .../maurodatamapper/core/diff/Diffable.groovy | 13 +++++++++---- .../maurodatamapper/core/model/CatalogueItem.groovy | 5 ----- .../maurodatamapper/dataflow/DataFlow.groovy | 5 ----- .../dataflow/component/DataClassComponent.groovy | 5 ----- .../dataflow/component/DataElementComponent.groovy | 5 ----- .../maurodatamapper/datamodel/item/DataClass.groovy | 4 ++-- .../datamodel/item/DataElement.groovy | 2 +- .../datamodel/item/datatype/DataType.groovy | 5 ----- .../datatype/enumeration/EnumerationValue.groovy | 2 +- .../referencedata/item/ReferenceDataElement.groovy | 2 +- .../referencedata/item/ReferenceDataValue.groovy | 5 ----- .../item/datatype/ReferenceDataType.groovy | 5 ----- .../enumeration/ReferenceEnumerationValue.groovy | 2 +- .../terminology/item/term/TermRelationship.groovy | 9 ++------- 18 files changed, 17 insertions(+), 72 deletions(-) diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy index 185226837d..30ec8d09eb 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Annotation.groovy @@ -130,11 +130,6 @@ class Annotation implements MultiFacetItemAware, PathAware, InformationAware, Di } - @Override - String getDiffIdentifier() { - this.label - } - static DetachedCriteria by() { new DetachedCriteria(Annotation) } diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy index 871bf2b814..77fb1df9c5 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Metadata.groovy @@ -102,11 +102,6 @@ class Metadata implements MultiFacetItemAware, Diffable { .appendString('value', this.value, obj.value) } - @Override - String getDiffIdentifier() { - "${this.namespace}.${this.key}" - } - @Override String getPathIdentifier() { "${this.namespace}.${this.key}" diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy index 30eaab27e4..6b4929bdff 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/Rule.groovy @@ -105,11 +105,6 @@ class Rule implements MultiFacetItemAware, Diffable { .appendList(RuleRepresentation, 'ruleRepresentations', this.ruleRepresentations, obj.ruleRepresentations) } - @Override - String getDiffIdentifier() { - this.name - } - static DetachedCriteria by() { new DetachedCriteria(Rule) } diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy index c52c6c5c2a..a91a8c00c8 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentation.groovy @@ -100,11 +100,6 @@ class RuleRepresentation implements Diffable, EditHistoryAwa .appendString('language', this.language, obj.language) } - @Override - String getDiffIdentifier() { - "${this.language}" - } - static DetachedCriteria by() { new DetachedCriteria(RuleRepresentation) } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diffable.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diffable.groovy index 95e3e781b3..de9e9af152 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diffable.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diffable.groovy @@ -18,15 +18,20 @@ package uk.ac.ox.softeng.maurodatamapper.core.diff import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff +import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware import grails.compiler.GrailsCompileStatic +import groovy.transform.SelfType +@SelfType(CreatorAware) @GrailsCompileStatic -interface Diffable { +trait Diffable { - ObjectDiff diff(T obj) + abstract ObjectDiff diff(T obj) - String getDiffIdentifier() + String getDiffIdentifier() { + getPathIdentifier() + } - String getPathPrefix() + abstract String getPathPrefix() } \ No newline at end of file diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy index 7cf3cc8964..2f43696e9f 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy @@ -83,11 +83,6 @@ trait CatalogueItem implements InformationAware, EditHistory } } - @Override - String getDiffIdentifier() { - label - } - @Override String getPathIdentifier() { label diff --git a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlow.groovy b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlow.groovy index d5b236aa5d..b310b75be8 100644 --- a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlow.groovy +++ b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlow.groovy @@ -126,11 +126,6 @@ class DataFlow implements ModelItem { target } - @Override - String getDiffIdentifier() { - this.label - } - @Override Boolean hasChildren() { dataClassComponents diff --git a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponent.groovy b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponent.groovy index 8ba06f2672..037156403c 100644 --- a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponent.groovy +++ b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponent.groovy @@ -124,11 +124,6 @@ class DataClassComponent implements ModelItem { dataFlow.model } - @Override - String getDiffIdentifier() { - this.label - } - @Override Boolean hasChildren() { dataElementComponents diff --git a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponent.groovy b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponent.groovy index 1e4b6136bb..5a74b5ec40 100644 --- a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponent.groovy +++ b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponent.groovy @@ -122,11 +122,6 @@ class DataElementComponent implements ModelItem dataClassComponent.model } - @Override - String getDiffIdentifier() { - this.label - } - @Override Boolean hasChildren() { false diff --git a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClass.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClass.groovy index ef9b52254e..7258991adc 100644 --- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClass.groovy +++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClass.groovy @@ -233,8 +233,8 @@ class DataClass implements ModelItem, MultiplicityAware, S @Override String getDiffIdentifier() { - if (!parentDataClass) return this.label - parentDataClass.getDiffIdentifier() + "/" + this.label + if (!parentDataClass) return this.pathIdentifier + "${parentDataClass.getDiffIdentifier()}/${this.pathIdentifier}" } CatalogueItem getParent() { diff --git a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElement.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElement.groovy index b668ee70c2..f77f4cea86 100644 --- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElement.groovy +++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElement.groovy @@ -181,7 +181,7 @@ class DataElement implements ModelItem, MultiplicityAwar @Override String getDiffIdentifier() { - "${dataClass.getDiffIdentifier()}/${label}" + "${dataClass.getDiffIdentifier()}/${pathIdentifier}" } @Override diff --git a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataType.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataType.groovy index f819ea2213..4f7714d12b 100644 --- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataType.groovy +++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataType.groovy @@ -154,11 +154,6 @@ abstract class DataType implements ModelItem, SummaryMetadataAw dataModel } - @Override - String getDiffIdentifier() { - this.label - } - @Override Boolean hasChildren() { false diff --git a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValue.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValue.groovy index 29bda21345..960cbfd608 100644 --- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValue.groovy +++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValue.groovy @@ -129,7 +129,7 @@ class EnumerationValue implements ModelItem { } @Override - String getDiffIdentifier() { + String getPathIdentifier() { this.key } diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataElement.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataElement.groovy index b43fdbb46a..1900a8a032 100644 --- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataElement.groovy +++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/ReferenceDataElement.groovy @@ -161,7 +161,7 @@ class ReferenceDataElement implements ModelItem { rowNumber } - @Override - String getDiffIdentifier() { - this.id - } - ObjectDiff diff(ReferenceDataValue otherValue) { String lhsId = this.id ?: "Left:Unsaved_${this.domainType}" String rhsId = otherValue.id ?: "Right:Unsaved_${otherValue.domainType}" diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferenceDataType.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferenceDataType.groovy index 3699f0a9d5..c667fae963 100644 --- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferenceDataType.groovy +++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/ReferenceDataType.groovy @@ -137,11 +137,6 @@ abstract class ReferenceDataType implements ModelItem, referenceDataModel } - @Override - String getDiffIdentifier() { - this.label - } - @Override Boolean hasChildren() { false diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/enumeration/ReferenceEnumerationValue.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/enumeration/ReferenceEnumerationValue.groovy index e1df710e86..d59f6efcf7 100644 --- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/enumeration/ReferenceEnumerationValue.groovy +++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/item/datatype/enumeration/ReferenceEnumerationValue.groovy @@ -128,7 +128,7 @@ class ReferenceEnumerationValue implements ModelItem { 'tr' } - @Override - String getPathIdentifier() { - "${sourceTerm.code}-${relationshipType.label}-${targetTerm.code}" - } - def beforeValidate() { label = relationshipType?.label beforeValidateModelItem() @@ -125,9 +120,9 @@ class TermRelationship implements ModelItem { } @Override - String getDiffIdentifier() { + String getPathIdentifier() { if (!label) label = relationshipType?.label - "$sourceTerm.label-$label-$targetTerm.label" + "$sourceTerm.label:$label:$targetTerm.label" } ObjectDiff diff(TermRelationship obj) { From e85e7fc2e5aeb9f4c951627649ac8cb44ca29ae9 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Thu, 15 Jul 2021 17:08:56 +0100 Subject: [PATCH 43/82] Improved use of Path inside merge diffs * Update Path and PathNode with additional helper methods * make the MergeDiff classes all use Path for the qualified path methods rather than custom building strings --- .../softeng/maurodatamapper/util/Path.groovy | 52 ++++++++++++++++--- .../maurodatamapper/util/PathNode.groovy | 16 ++++-- .../softeng/maurodatamapper/util/Utils.groovy | 3 +- .../core/path/PathInterceptor.groovy | 2 +- .../core/path/PathService.groovy | 10 ++-- .../views/mergeDiff/_mergeDiff.gson | 2 +- .../diff/tridirectional/ArrayMergeDiff.groovy | 3 +- .../tridirectional/CreationMergeDiff.groovy | 7 +-- .../tridirectional/DeletionMergeDiff.groovy | 9 ++-- .../diff/tridirectional/FieldMergeDiff.groovy | 9 ++-- .../core/diff/tridirectional/MergeDiff.groovy | 38 +++++++------- .../tridirectional/TriDirectionalDiff.groovy | 11 +++- 12 files changed, 110 insertions(+), 52 deletions(-) diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Path.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Path.groovy index d450f95844..6b01440d13 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Path.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Path.groovy @@ -35,8 +35,8 @@ class Path { List pathNodes - Path() { - + private Path() { + pathNodes = [] } /* @@ -46,8 +46,8 @@ class Path { * @param path The path */ - Path(String path) { - pathNodes = [] + private Path(String path) { + this() if (path) { String[] splits = path.split(PATH_DELIMITER, MAX_NODES) @@ -58,7 +58,7 @@ class Path { } } - int getSize() { + int size() { pathNodes.size() } @@ -74,6 +74,21 @@ class Path { pathNodes.first() } + Path addToPathNodes(PathNode pathNode) { + pathNodes.add(pathNode) + this + } + + Path addToPathNodes(String prefix, String pathIdentifier) { + addToPathNodes(new PathNode(prefix, pathIdentifier)) + } + + Path getParent() { + clone().tap { + pathNodes.removeLast() + } + } + boolean isEmpty() { pathNodes.isEmpty() } @@ -83,23 +98,46 @@ class Path { } Path getChildPath() { - new Path(pathNodes: pathNodes[1..size - 1]) + new Path(pathNodes: pathNodes[1..size() - 1]) } String toString() { pathNodes.join('|') } + Path clone() { + Path local = this + new Path().tap { + pathNodes = local.pathNodes.collect {it.clone()} + } + } + + boolean matches(Path otherPath) { + if (size() != otherPath.size()) return false + for (i in 0.. parentClass, Class childClass) { - childClass.classLoader.loadClass(parentClass.name).isAssignableFrom(childClass) + // Try the standard way of checking and if that fails just check the classLoader which may be different due to reloading + parentClass.isAssignableFrom(childClass) ?: childClass.classLoader.loadClass(parentClass.name).isAssignableFrom(childClass) } static void toUuid(Map map, String key) { diff --git a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy index e0914f9c21..e6cf55f4a9 100644 --- a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy +++ b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy @@ -26,7 +26,7 @@ class PathInterceptor implements MdmInterceptor { if (actionName == 'listAllPrefixMappings') return true mapDomainTypeToClass('securableResource', true) - params.path = new Path(params.path) + params.path = Path.from(params.path) if (params.containsKey('securableResourceId')) { return currentUserSecurityPolicyManager.userCanReadSecuredResourceId(params.securableResourceClass, params.securableResourceId) ?: diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy index c79bbfd156..5f67f57853 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy @@ -31,7 +31,6 @@ import grails.core.GrailsApplication import grails.gorm.transactions.Transactional import groovy.util.logging.Slf4j import org.grails.core.artefact.DomainClassArtefactHandler -import org.grails.orm.hibernate.proxy.HibernateProxyHandler import org.springframework.beans.factory.annotation.Autowired @Transactional @@ -49,8 +48,6 @@ class PathService { GrailsApplication grailsApplication - private static HibernateProxyHandler proxyHandler = new HibernateProxyHandler() - SecurableResource findSecurableResourceByDomainClassAndId(Class resourceClass, UUID resourceId) { SecurableResourceService securableResourceService = securableResourceServices.find {it.handles(resourceClass)} if (!securableResourceService) throw new ApiBadRequestException('PS03', "No service available to handle [${resourceClass.simpleName}]") @@ -72,6 +69,7 @@ class PathService { } CreatorAware findResourceByPathFromRootResource(CreatorAware rootResourceOfPath, Path path) { + log.debug('Searching for path {} inside {}:{}', path, rootResourceOfPath.pathPrefix, rootResourceOfPath.pathIdentifier) if (path.isEmpty()) { throw new ApiBadRequestException('PS06', 'Must have a path to search') } @@ -83,11 +81,11 @@ class PathService { // Confirmed the path is inside the model // If only one node then return the model - if (path.size == 1) return rootResourceOfPath + if (path.size() == 1) return rootResourceOfPath // Only 2 nodes in path, first is model // Last part of path is a field access as has no type prefix so return the model - if (path.size == 2 && !path.last().hasTypePrefix()) return rootResourceOfPath + if (path.size() == 2 && !path.last().hasTypePrefix()) return rootResourceOfPath // Find the first child in the path Path childPath = path.childPath @@ -102,7 +100,7 @@ class PathService { return null } - log.debug('Found service [{}] to handle [{}]', domainService.class.simpleName, childNode.typePrefix) + log.trace('Found service [{}] to handle prefix [{}]', domainService.class.simpleName, childNode.typePrefix) def child = domainService.findByParentIdAndPathIdentifier(rootResourceOfPath.id, childNode.label) if (!child) { diff --git a/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson b/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson index 8bbbceac82..c32ac28c1e 100644 --- a/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson +++ b/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson @@ -11,7 +11,7 @@ json { sourceId mergeDiff.getSourceId() targetId mergeDiff.getTargetId() - path mergeDiff.fullyQualifiedPath + path mergeDiff.fullyQualifiedPath.toString() if (mergeDiff.getTarget() instanceof InformationAware) { label(((InformationAware) mergeDiff.getTarget()).getLabel()) diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy index eb3994de00..00c53dc8fe 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy @@ -18,6 +18,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable +import uk.ac.ox.softeng.maurodatamapper.util.Path import groovy.transform.CompileStatic @@ -59,7 +60,7 @@ class ArrayMergeDiff extends FieldMergeDiff> { super.forFieldName(fieldName) as ArrayMergeDiff } - ArrayMergeDiff insideFullyQualifiedObjectPath(String fullyQualifiedObjectPath) { + ArrayMergeDiff insideFullyQualifiedObjectPath(Path fullyQualifiedObjectPath) { super.insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) as ArrayMergeDiff } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy index 9af9ccd8f0..b6d7b01a3e 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy @@ -18,6 +18,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable +import uk.ac.ox.softeng.maurodatamapper.util.Path import groovy.transform.CompileStatic @@ -41,9 +42,9 @@ class CreationMergeDiff extends TriDirectionalDiff implem created.diffIdentifier } - String getFullyQualifiedPath() { + Path getFullyQualifiedPath() { String cleanedIdentifier = createdIdentifier.split('/').last() - "${fullyQualifiedObjectPath}|${created.pathPrefix}:${cleanedIdentifier}" + Path.from(fullyQualifiedObjectPath, created.pathPrefix, cleanedIdentifier) } boolean isSourceModificationAndTargetDeletion() { @@ -55,7 +56,7 @@ class CreationMergeDiff extends TriDirectionalDiff implem withSource(object) as CreationMergeDiff } - CreationMergeDiff insideFullyQualifiedObjectPath(String fullyQualifiedObjectPath) { + CreationMergeDiff insideFullyQualifiedObjectPath(Path fullyQualifiedObjectPath) { super.insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) as CreationMergeDiff } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy index a1b2c743c4..cbf1d11517 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy @@ -19,6 +19,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff +import uk.ac.ox.softeng.maurodatamapper.util.Path import groovy.transform.CompileStatic @@ -48,9 +49,9 @@ class DeletionMergeDiff extends TriDirectionalDiff implem mergeModificationDiff != null } - String getFullyQualifiedPath() { + Path getFullyQualifiedPath() { String cleanedIdentifier = deletedIdentifier.split('/').last() - "${fullyQualifiedObjectPath}|${deleted.pathPrefix}:${cleanedIdentifier}" + Path.from(fullyQualifiedObjectPath, deleted.pathPrefix, cleanedIdentifier) } DeletionMergeDiff whichDeleted(D object) { @@ -59,7 +60,7 @@ class DeletionMergeDiff extends TriDirectionalDiff implem } @Override - DeletionMergeDiff insideFullyQualifiedObjectPath(String fullyQualifiedObjectPath) { + DeletionMergeDiff insideFullyQualifiedObjectPath(Path fullyQualifiedObjectPath) { super.insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) as DeletionMergeDiff } @@ -93,7 +94,7 @@ class DeletionMergeDiff extends TriDirectionalDiff implem @Override String toString() { - String str = "Deleted :: ${deletedIdentifier}" + String str = "Deleted :: ${getFullyQualifiedPath()}" mergeModificationDiff ? "${str}\n >> Modified :: ${mergeModificationDiff}" : str } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy index 2b62f6dcb1..9d081af203 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy @@ -17,6 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional +import uk.ac.ox.softeng.maurodatamapper.util.Path import groovy.transform.CompileStatic @@ -34,7 +35,7 @@ class FieldMergeDiff extends TriDirectionalDiff implements Comparable insideFullyQualifiedObjectPath(String fullyQualifiedObjectPath) { + FieldMergeDiff insideFullyQualifiedObjectPath(Path fullyQualifiedObjectPath) { super.insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) as FieldMergeDiff } @@ -79,8 +80,8 @@ class FieldMergeDiff extends TriDirectionalDiff implements Comparable extends TriDirectionalDiff implements Comparable ${target} :: ${commonAncestor}" + "${getFullyQualifiedPath()} :: ${source} <> ${target} :: ${commonAncestor}" } @Override diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy index a99d416e1a..899a7bf1af 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy @@ -25,6 +25,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.CreationDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.DeletionDiff import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware +import uk.ac.ox.softeng.maurodatamapper.util.Path import groovy.transform.CompileStatic import groovy.transform.stc.ClosureParams @@ -96,10 +97,9 @@ class MergeDiff extends TriDirectionalDiff implements Com (target as CreatorAware).id } - String getFullyQualifiedPath() { + Path getFullyQualifiedPath() { String cleanedIdentifier = sourceIdentifier.split('/').last() - String localPath = "${source.pathPrefix}:${cleanedIdentifier}" - fullyQualifiedObjectPath ? "${fullyQualifiedObjectPath}|${localPath}" : localPath + Path.from(fullyQualifiedObjectPath, source.pathPrefix, cleanedIdentifier) } FieldMergeDiff first() { @@ -140,7 +140,7 @@ class MergeDiff extends TriDirectionalDiff implements Com } @Override - MergeDiff insideFullyQualifiedObjectPath(String fullyQualifiedObjectPath) { + MergeDiff insideFullyQualifiedObjectPath(Path fullyQualifiedObjectPath) { super.insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) as MergeDiff } @@ -287,7 +287,7 @@ class MergeDiff extends TriDirectionalDiff implements Com this } - static ArrayMergeDiff createArrayMergeDiffFromSourceTargetArrayDiff(String fullyQualifiedObjectPath, ArrayDiff sourceTargetArrayDiff) { + static ArrayMergeDiff createArrayMergeDiffFromSourceTargetArrayDiff(Path fullyQualifiedObjectPath, ArrayDiff sourceTargetArrayDiff) { log.debug('[{}] Processing array differences against target from source', sourceTargetArrayDiff.fieldName) // Created and Deleted diffs in this array are left as-is as they are guaranteed to be unique and no issue arrayMergeDiff(sourceTargetArrayDiff.targetClass) @@ -317,7 +317,7 @@ class MergeDiff extends TriDirectionalDiff implements Com } - static ArrayMergeDiff createBaseArrayMergeDiffPresentOnOneSide(String fullyQualifiedObjectPath, + static ArrayMergeDiff createBaseArrayMergeDiffPresentOnOneSide(Path fullyQualifiedObjectPath, ArrayDiff caSourceArrayDiff) { // Created and Deleted diffs in this array are left as-is as they are guaranteed to be unique and no issue @@ -330,7 +330,7 @@ class MergeDiff extends TriDirectionalDiff implements Com .withCommonAncestor(caSourceArrayDiff.left) // both diffs have the same LHS } - static ArrayMergeDiff createArrayMergeDiffPresentOnBothSides(String fullyQualifiedObjectPath, + static ArrayMergeDiff createArrayMergeDiffPresentOnBothSides(Path fullyQualifiedObjectPath, ArrayDiff caSourceArrayDiff, ArrayDiff caTargetArrayDiff) { log.debug('[{}] Processing array differences against common ancestor on both sides', caSourceArrayDiff.fieldName) @@ -343,7 +343,7 @@ class MergeDiff extends TriDirectionalDiff implements Com } - static ArrayMergeDiff createArrayMergeDiffPresentOnOneSide(String fullyQualifiedObjectPath, + static ArrayMergeDiff createArrayMergeDiffPresentOnOneSide(Path fullyQualifiedObjectPath, ArrayDiff caSourceArrayDiff) { log.debug('[{}] Processing array differences against common ancestor on one side', caSourceArrayDiff.fieldName) // Created and Deleted diffs in this array are left as-is as they are guaranteed to be unique and no issue @@ -354,7 +354,7 @@ class MergeDiff extends TriDirectionalDiff implements Com .getValidOnly() } - static FieldMergeDiff createFieldMergeDiffFromSourceTargetFieldDiff(String fullyQualifiedObjectPath, FieldDiff sourceTargetFieldDiff) { + static FieldMergeDiff createFieldMergeDiffFromSourceTargetFieldDiff(Path fullyQualifiedObjectPath, FieldDiff sourceTargetFieldDiff) { log.debug('[{}] Processing field difference against target from source', sourceTargetFieldDiff.fieldName) fieldMergeDiff(sourceTargetFieldDiff.targetClass) .forFieldName(sourceTargetFieldDiff.fieldName) @@ -366,7 +366,7 @@ class MergeDiff extends TriDirectionalDiff implements Com .getValidOnly() } - static FieldMergeDiff createFieldMergeDiffPresentOnBothSides(String fullyQualifiedObjectPath, FieldDiff caSourceFieldDiff, FieldDiff caTargetFieldDiff) { + static FieldMergeDiff createFieldMergeDiffPresentOnBothSides(Path fullyQualifiedObjectPath, FieldDiff caSourceFieldDiff, FieldDiff caTargetFieldDiff) { log.debug('[{}] Processing field difference against common ancestor on both sides', caSourceFieldDiff.fieldName) createBaseFieldMergeDiffPresentOnOneSide(fullyQualifiedObjectPath, caSourceFieldDiff) .withTarget(caTargetFieldDiff.right) @@ -374,7 +374,7 @@ class MergeDiff extends TriDirectionalDiff implements Com .getValidOnly() // This is a safety check to handle when 2 diffs are used with modifications but no actual difference } - static FieldMergeDiff createFieldMergeDiffPresentOnOneSide(String fullyQualifiedObjectPath, FieldDiff caSourceFieldDiff) { + static FieldMergeDiff createFieldMergeDiffPresentOnOneSide(Path fullyQualifiedObjectPath, FieldDiff caSourceFieldDiff) { log.debug('[{}] Processing field difference against common ancestor on one side', caSourceFieldDiff.fieldName) createBaseFieldMergeDiffPresentOnOneSide(fullyQualifiedObjectPath, caSourceFieldDiff) // just use whats in the commonAncestor as no caTarget data denotes no difference between the CA and the target @@ -382,7 +382,7 @@ class MergeDiff extends TriDirectionalDiff implements Com .getValidOnly() } - static FieldMergeDiff createBaseFieldMergeDiffPresentOnOneSide(String fullyQualifiedObjectPath, FieldDiff caSourceFieldDiff) { + static FieldMergeDiff createBaseFieldMergeDiffPresentOnOneSide(Path fullyQualifiedObjectPath, FieldDiff caSourceFieldDiff) { fieldMergeDiff(caSourceFieldDiff.targetClass) .forFieldName(caSourceFieldDiff.fieldName) .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) @@ -390,7 +390,7 @@ class MergeDiff extends TriDirectionalDiff implements Com .withCommonAncestor(caSourceFieldDiff.left) // both diffs have the same LHS } - static Collection> createCreationMergeDiffsPresentOnOneSide(String fullyQualifiedObjectPath, + static Collection> createCreationMergeDiffsPresentOnOneSide(Path fullyQualifiedObjectPath, Collection> caSourceCreatedDiffs) { caSourceCreatedDiffs.collect {created -> creationMergeDiff(created.targetClass) @@ -405,13 +405,13 @@ class MergeDiff extends TriDirectionalDiff implements Com * Important to note that due to the way diff works the same object cannot exist in more than one of created, deleted, modified in an * ArrayDIff */ - static Collection createCreationMergeDiffsPresentOnBothSides(String fullyQualifiedObjectPath, + static Collection createCreationMergeDiffsPresentOnBothSides(Path fullyQualifiedObjectPath, ArrayDiff caSourceDiff, ArrayDiff caTargetDiff) { Collection modificationCreationMergeDiffs = caSourceDiff.modified.collect {caSourceModificationDiff -> DeletionDiff caTargetDeletionDiff = caTargetDiff.deleted.find {it.deletedIdentifier == caSourceModificationDiff.leftIdentifier} if (caTargetDeletionDiff) { - log.debug('[{}] ca/source modified exists in ca/target deleted.') + log.debug('[{}] ca/source modified exists in ca/target deleted.', caSourceModificationDiff.leftIdentifier) return creationMergeDiff(caSourceModificationDiff.targetClass) .whichCreated(caSourceModificationDiff.right) .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) @@ -446,7 +446,7 @@ class MergeDiff extends TriDirectionalDiff implements Com } - static Collection> createDeletionMergeDiffsPresentOnOneSide(String fullyQualifiedObjectPath, + static Collection> createDeletionMergeDiffsPresentOnOneSide(Path fullyQualifiedObjectPath, Collection> caSourceDeletionDiffs) { caSourceDeletionDiffs.collect {deleted -> deletionMergeDiff(deleted.targetClass) @@ -462,7 +462,7 @@ class MergeDiff extends TriDirectionalDiff implements Com * ArrayDIff * */ - static Collection createDeletionMergeDiffsPresentOnBothSides(String fullyQualifiedObjectPath, + static Collection createDeletionMergeDiffsPresentOnBothSides(Path fullyQualifiedObjectPath, Collection> caSourceDeletedDiffs, ArrayDiff caTargetDiff) { caSourceDeletedDiffs.collect {diff -> @@ -495,7 +495,7 @@ class MergeDiff extends TriDirectionalDiff implements Com }.findAll() } - static Collection> createModifiedMergeDiffsPresentOnOneSide(String fullyQualifiedObjectPath, + static Collection> createModifiedMergeDiffsPresentOnOneSide(Path fullyQualifiedObjectPath, Collection> caSourceModifiedDiffs) { // Modified diffs represent diffs which have modifications down the chain but no changes on the target side // Therefore we can use an empty object diff @@ -511,7 +511,7 @@ class MergeDiff extends TriDirectionalDiff implements Com } } - static Collection> createModifiedMergeDiffsPresentOnBothSides(String fullyQualifiedObjectPath, + static Collection> createModifiedMergeDiffsPresentOnBothSides(Path fullyQualifiedObjectPath, ArrayDiff caSourceDiff, ArrayDiff caTargetDiff) { diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy index a4e2260a0b..848ff6c016 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy @@ -19,13 +19,14 @@ package uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.BiDirectionalDiff +import uk.ac.ox.softeng.maurodatamapper.util.Path import groovy.transform.CompileStatic @CompileStatic abstract class TriDirectionalDiff extends BiDirectionalDiff { - protected String fullyQualifiedObjectPath + protected Path fullyQualifiedObjectPath private Boolean mergeConflict private T commonAncestor @@ -34,6 +35,8 @@ abstract class TriDirectionalDiff extends BiDirectionalDiff { mergeConflict = false } + abstract Path getFullyQualifiedPath() + @Override boolean equals(o) { if (this.is(o)) return true @@ -47,7 +50,7 @@ abstract class TriDirectionalDiff extends BiDirectionalDiff { return true } - TriDirectionalDiff insideFullyQualifiedObjectPath(String fullyQualifiedObjectPath) { + TriDirectionalDiff insideFullyQualifiedObjectPath(Path fullyQualifiedObjectPath) { this.fullyQualifiedObjectPath = fullyQualifiedObjectPath this } @@ -90,6 +93,10 @@ abstract class TriDirectionalDiff extends BiDirectionalDiff { super.getLeft() } + Path getFullyQualifiedObjectPath() { + fullyQualifiedObjectPath + } + @Deprecated @Override T getRight() { From 40a9558180d43b8c55c292bb2b7844c000ec0ac3 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Thu, 15 Jul 2021 17:12:29 +0100 Subject: [PATCH 44/82] Add/Update copy methods to the CatalogueItemService and the MultiFacetItemAwareService To do the merge we need to be able to copy anything via the service so add abstract methods to allow this To make it faster to implement the CatalogueItemService class defines an Not yet implemented exception for the services which havent implemented the copy method --- .../core/facet/AnnotationService.groovy | 15 +++ .../core/facet/MetadataService.groovy | 8 +- .../core/facet/ReferenceFileService.groovy | 8 ++ .../core/facet/RuleService.groovy | 10 ++ .../core/facet/SemanticLinkService.groovy | 10 ++ .../core/facet/VersionLinkService.groovy | 11 ++ .../core/model/CatalogueItemService.groovy | 31 +++-- .../core/model/ModelItemService.groovy | 10 +- .../service/MultiFacetItemAwareService.groovy | 2 + .../facet/SummaryMetadataService.groovy | 11 ++ .../datamodel/item/DataClassService.groovy | 117 ++++++++++-------- .../datamodel/item/DataElementService.groovy | 10 +- .../item/datatype/DataTypeService.groovy | 6 +- .../datatype/EnumerationTypeService.groovy | 8 ++ .../item/datatype/ModelDataTypeService.groovy | 8 ++ .../item/datatype/PrimitiveTypeService.groovy | 8 ++ .../item/datatype/ReferenceTypeService.groovy | 10 +- .../EnumerationValueService.groovy | 25 ++-- .../ReferenceSummaryMetadataService.groovy | 10 ++ .../item/TermRelationshipTypeService.groovy | 3 +- .../terminology/item/TermService.groovy | 3 +- .../item/term/TermRelationshipService.groovy | 4 +- 22 files changed, 225 insertions(+), 103 deletions(-) diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/AnnotationService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/AnnotationService.groovy index 152f3e3365..ab72fb34e8 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/AnnotationService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/AnnotationService.groovy @@ -98,6 +98,21 @@ class AnnotationService implements MultiFacetItemAwareService { log.debug('stop') } + @Override + Annotation copy(Annotation facetToCopy, MultiFacetAware multiFacetAwareItemToCopyInto) { + Annotation copy = new Annotation(label: facetToCopy.label, description: facetToCopy.description, createdBy: facetToCopy.createdBy) + if (facetToCopy.childAnnotations) facetToCopy.childAnnotations.each {ca -> copy(ca, copy)} + multiFacetAwareItemToCopyInto.addToAnnotations(copy) + copy + } + + Annotation copy(Annotation facetToCopy, Annotation annotationToCopyInto) { + Annotation copy = new Annotation(label: facetToCopy.label, description: facetToCopy.description, createdBy: facetToCopy.createdBy) + if (facetToCopy.childAnnotations) facetToCopy.childAnnotations.each {ca -> copy(ca, copy)} + annotationToCopyInto.addToChildAnnotations(copy) + copy + } + List findAllWhereRootAnnotationOfMultiFacetAwareItemId(UUID multiFacetAwareItemId, Map paginate = [:]) { Annotation.whereRootAnnotationOfMultiFacetAwareItemId(multiFacetAwareItemId).list(paginate) } diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy index 121b783134..b425ee1a25 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy @@ -27,7 +27,6 @@ import uk.ac.ox.softeng.maurodatamapper.core.traits.service.MultiFacetAwareServi import uk.ac.ox.softeng.maurodatamapper.core.traits.service.MultiFacetItemAwareService import uk.ac.ox.softeng.maurodatamapper.gorm.PaginatedResultList import uk.ac.ox.softeng.maurodatamapper.provider.MauroDataMapperService -import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.util.Utils import grails.gorm.DetachedCriteria @@ -66,8 +65,11 @@ class MetadataService implements MultiFacetItemAwareService { metadata.delete(flush: flush) } - void copy(MultiFacetAware target, Metadata item, UserSecurityPolicyManager userSecurityPolicyManager) { - target.addToMetadata(item.namespace, item.key, item.value, userSecurityPolicyManager.user.emailAddress) + @Override + Metadata copy(Metadata facetToCopy, MultiFacetAware multiFacetAwareItemToCopyInto) { + Metadata copy = new Metadata(namespace: facetToCopy.namespace, key: facetToCopy.key, value: facetToCopy.value, createdBy: facetToCopy.createdBy) + multiFacetAwareItemToCopyInto.addToMetadata(copy) + copy } @Override diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/ReferenceFileService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/ReferenceFileService.groovy index 8b1078b1b7..c88611f469 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/ReferenceFileService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/ReferenceFileService.groovy @@ -65,6 +65,14 @@ class ReferenceFileService implements CatalogueFileService, Multi domain.addToReferenceFiles(facet) } + @Override + ReferenceFile copy(ReferenceFile facetToCopy, MultiFacetAware multiFacetAwareItemToCopyInto) { + ReferenceFile copy = new ReferenceFile(fileContents: facetToCopy.fileContents, fileName: facetToCopy.fileName, fileSize: facetToCopy.fileSize, + fileType: facetToCopy.fileSize, createdBy: facetToCopy.createdBy) + multiFacetAwareItemToCopyInto.addToReferenceFiles(copy) + copy + } + @Override ReferenceFile createNewFile(String name, byte[] contents, String type, User user) { createNewFileBase(name, contents, type, user.emailAddress) diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/RuleService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/RuleService.groovy index bb039089ad..ae3c3c9517 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/RuleService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/RuleService.groovy @@ -59,6 +59,16 @@ class RuleService implements MultiFacetItemAwareService { rule.delete(flush: flush) } + @Override + Rule copy(Rule facetToCopy, MultiFacetAware multiFacetAwareItemToCopyInto) { + Rule copy = new Rule(name: facetToCopy.name, description: facetToCopy.description, createdBy: facetToCopy.createdBy) + facetToCopy.ruleRepresentations.each {rr -> + copy.addToRuleRepresentations(language: rr.language, representation: rr.representation, createdBy: rr.createdBy) + } + multiFacetAwareItemToCopyInto.addToRules(copy) + copy + } + @Override void saveMultiFacetAwareItem(Rule facet) { if (!facet) return diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/SemanticLinkService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/SemanticLinkService.groovy index 7368242af1..2ab7319053 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/SemanticLinkService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/SemanticLinkService.groovy @@ -63,6 +63,16 @@ class SemanticLinkService implements MultiFacetItemAwareService { .get() } + @Override + SemanticLink copy(SemanticLink facetToCopy, MultiFacetAware multiFacetAwareItemToCopyInto) { + SemanticLink copy = new SemanticLink(linkType: facetToCopy.linkType, + targetMultiFacetAwareItemDomainType: facetToCopy.targetMultiFacetAwareItemDomainType, + targetMultiFacetAwareItemId: facetToCopy.targetMultiFacetAwareItemId, + createdBy: facetToCopy.createdBy) + multiFacetAwareItemToCopyInto.addToSemanticLinks(copy) + copy + } + void delete(SemanticLink semanticLink, boolean flush = false) { if (!semanticLink) return MultiFacetAwareService service = findServiceForMultiFacetAwareDomainType(semanticLink.multiFacetAwareItemDomainType) diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/VersionLinkService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/VersionLinkService.groovy index 7234f3550a..039a7bd5f1 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/VersionLinkService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/VersionLinkService.groovy @@ -19,6 +19,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.facet import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException import uk.ac.ox.softeng.maurodatamapper.core.model.Model +import uk.ac.ox.softeng.maurodatamapper.core.model.facet.MultiFacetAware import uk.ac.ox.softeng.maurodatamapper.core.model.facet.VersionLinkAware import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.VersionAware import uk.ac.ox.softeng.maurodatamapper.core.traits.service.MultiFacetItemAwareService @@ -70,6 +71,16 @@ class VersionLinkService implements MultiFacetItemAwareService { versionLink.delete(flush: flush) } + @Override + VersionLink copy(VersionLink facetToCopy, MultiFacetAware multiFacetAwareItemToCopyInto) { + VersionLink copy = new VersionLink(linkType: facetToCopy.linkType, + targetModelDomainType: facetToCopy.targetModelDomainType, + targetModelId: facetToCopy.targetModelId, + createdBy: facetToCopy.createdBy) + (multiFacetAwareItemToCopyInto as Model).addToVersionLinks(copy) + copy + } + void deleteBySourceModelAndTargetModelAndLinkType(Model sourceModel, Model targetModel, VersionLinkType linkType) { VersionLink sl = findBySourceModelAndTargetModelAndLinkType(sourceModel, targetModel, linkType) diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy index b0d1954d5b..c39daed3db 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy @@ -127,11 +127,20 @@ abstract class CatalogueItemService implements DomainSe } K copyCatalogueItemInformation(K original, K copy, User copier, UserSecurityPolicyManager userSecurityPolicyManager, - CopyInformation copyInformation = new CopyInformation()) { - copy = populateCopyData(original, copy, copier, copyInformation) - classifierService.findAllByCatalogueItemId(userSecurityPolicyManager, original.id).each { copy.addToClassifiers(it) } - metadataService.findAllByMultiFacetAwareItemId(original.id).each { copy.addToMetadata(it.namespace, it.key, it.value, copier.emailAddress) } - ruleService.findAllByMultiFacetAwareItemId(original.id).each { rule -> + CopyInformation copyInformation = null) { + copy.createdBy = copier.emailAddress + copy.description = original.description + + // Allow copying with a new label + if (copyInformation && copyInformation.validate()) { + copy.label = copyInformation.copyLabel + } else { + copy.label = original.label + } + + classifierService.findAllByCatalogueItemId(userSecurityPolicyManager, original.id).each {copy.addToClassifiers(it)} + metadataService.findAllByMultiFacetAwareItemId(original.id).each {copy.addToMetadata(it.namespace, it.key, it.value, copier.emailAddress)} + ruleService.findAllByMultiFacetAwareItemId(original.id).each {rule -> Rule copiedRule = new Rule(name: rule.name, description: rule.description, createdBy: copier.emailAddress) rule.ruleRepresentations.each { ruleRepresentation -> copiedRule.addToRuleRepresentations(language: ruleRepresentation.language, @@ -206,16 +215,4 @@ abstract class CatalogueItemService implements DomainSe metadataService.mergeMetadataIntoCatalogueItem(targetCatalogueItem, modifiedObjectPatchData) } } - - K populateCopyData(K original, K copy, User copier, CopyInformation copyInformation) { - copy.createdBy = copier.emailAddress - copy.description = original.description - if (copyInformation.validate()) { - copy.label = copyInformation.copyLabel - } else { - copy.label = original.label - log.debug('Creating Copy with original label as provided label is empty or Invalid') - } - return copy - } } \ No newline at end of file diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy index 397a7f27e3..5df6d9d7d5 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy @@ -47,16 +47,18 @@ abstract class ModelItemService extends CatalogueItemServic throw new ApiNotYetImplementedException('MIS01', "deleteAllByModelId for ${getModelItemClass().simpleName}") } + @Deprecated K copy(Model copiedModelInto, K original, UserSecurityPolicyManager userSecurityPolicyManager) { - throw new ApiNotYetImplementedException('MIS02', "copy [for ModelItem ${getModelItemClass().simpleName}]") + copy(copiedModelInto, original, null, userSecurityPolicyManager) } - K copy(Model copiedModelInto, K original, UserSecurityPolicyManager userSecurityPolicyManager, UUID parentId) { + @Deprecated + K copy(Model copiedModelInto, K original, UUID nonModelParentId, UserSecurityPolicyManager userSecurityPolicyManager) { throw new ApiNotYetImplementedException('MIS03', "copy [for ModelItem ${getModelItemClass().simpleName}] (with parent id)") } - K copy(Model copiedModelInto, K original, UserSecurityPolicyManager userSecurityPolicyManager, UUID parentId, CopyInformation copyInformation) { - throw new ApiNotYetImplementedException('MIS03', "copy [for ModelItem ${getModelItemClass().simpleName}] (with parent id), and relabel") + K copy(Model copiedModelInto, K original, CatalogueItem nonModelParent, UserSecurityPolicyManager userSecurityPolicyManager) { + throw new ApiNotYetImplementedException('MIS03', "copy [for ModelItem ${getModelItemClass().simpleName}]") } Model mergeObjectPatchDataIntoModelItem(ObjectPatchData objectPatchData, K targetModelItem, Model targetModel, diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/MultiFacetItemAwareService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/MultiFacetItemAwareService.groovy index 4bcfc20037..e03b529f54 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/MultiFacetItemAwareService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/service/MultiFacetItemAwareService.groovy @@ -56,6 +56,8 @@ trait MultiFacetItemAwareService extends DomainSe abstract void addFacetToDomain(M facet, String domainType, UUID domainId) + abstract M copy(M facetToCopy, MultiFacetAware multiFacetAwareItemToCopyInto) + M addCreatedEditToMultiFacetAwareItem(User creator, M domain, String multiFacetAwareItemDomainType, UUID multiFacetAwareItemId) { EditHistoryAware multiFacetAwareItem = findMultiFacetAwareItemByDomainTypeAndId(multiFacetAwareItemDomainType, multiFacetAwareItemId) as EditHistoryAware diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/SummaryMetadataService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/SummaryMetadataService.groovy index 7a3a952136..aeca98ce55 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/SummaryMetadataService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/facet/SummaryMetadataService.groovy @@ -17,6 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.datamodel.facet + import uk.ac.ox.softeng.maurodatamapper.core.model.facet.MultiFacetAware import uk.ac.ox.softeng.maurodatamapper.core.traits.service.MultiFacetAwareService import uk.ac.ox.softeng.maurodatamapper.core.traits.service.MultiFacetItemAwareService @@ -74,6 +75,16 @@ class SummaryMetadataService implements MultiFacetItemAwareService + copy.addToSummaryMetadataReports(reportDate: smr.reportDate, reportValue: smr.reportValue) + } + (multiFacetAwareItemToCopyInto as SummaryMetadataAware).addToSummaryMetadata(copy) + copy + } + @Override SummaryMetadata findByMultiFacetAwareItemIdAndId(UUID multiFacetAwareItemId, Serializable id) { SummaryMetadata.byMultiFacetAwareItemIdAndId(multiFacetAwareItemId, id).get() diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassService.groovy index e6d697326d..b7cf6d1f75 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassService.groovy @@ -102,13 +102,13 @@ class DataClassService extends ModelItemService implements SummaryMet Set dataTypes = extractAllUsedNewOrDirtyDataTypes(dataClass) log.debug('{} new or dirty used datatypes inside dataclass', dataTypes.size()) // Validation should have already been done - dataTypes.each { it.skipValidation(true) } + dataTypes.each {it.skipValidation(true)} dataTypeService.saveAll(dataTypes, false) } Set extractAllUsedNewOrDirtyDataTypes(DataClass dataClass) { - Set dataTypes = dataClass.dataElements.collect { it.dataType }.findAll { it.isDirty() || !it.ident() }.toSet() - dataTypes.addAll(dataClass.dataClasses.collect { extractAllUsedNewOrDirtyDataTypes(it) }.flatten().toSet() as Collection) + Set dataTypes = dataClass.dataElements.collect {it.dataType}.findAll {it.isDirty() || !it.ident()}.toSet() + dataTypes.addAll(dataClass.dataClasses.collect {extractAllUsedNewOrDirtyDataTypes(it)}.flatten().toSet() as Collection) dataTypes } @@ -133,7 +133,7 @@ class DataClassService extends ModelItemService implements SummaryMet dataClass.save(flush: false, validate: false) // Recurse through the hierarchy - dataClasses?.each { dc -> + dataClasses?.each {dc -> saveDataClassHierarchy(dc) } @@ -221,10 +221,10 @@ class DataClassService extends ModelItemService implements SummaryMet DataClass checkFacetsAfterImportingCatalogueItem(DataClass catalogueItem) { super.checkFacetsAfterImportingCatalogueItem(catalogueItem) if (catalogueItem.summaryMetadata) { - catalogueItem.summaryMetadata.each { sm -> + catalogueItem.summaryMetadata.each {sm -> sm.multiFacetAwareItemId = catalogueItem.id sm.createdBy = sm.createdBy ?: catalogueItem.createdBy - sm.summaryMetadataReports.each { smr -> + sm.summaryMetadataReports.each {smr -> smr.createdBy = catalogueItem.createdBy } } @@ -242,14 +242,14 @@ class DataClassService extends ModelItemService implements SummaryMet Collection saveAllAndGetDataElements(Collection dataClasses) { - List classifiers = dataClasses.collectMany { it.classifiers ?: [] } as List + List classifiers = dataClasses.collectMany {it.classifiers ?: []} as List if (classifiers) { log.trace('Saving {} classifiers') classifierService.saveAll(classifiers) } - Collection alreadySaved = dataClasses.findAll { it.ident() && it.isDirty() } - Collection notSaved = dataClasses.findAll { !it.ident() } + Collection alreadySaved = dataClasses.findAll {it.ident() && it.isDirty()} + Collection notSaved = dataClasses.findAll {!it.ident()} Collection dataElements = [] @@ -264,10 +264,10 @@ class DataClassService extends ModelItemService implements SummaryMet int count = 0 // Find all DCs which are either top level or have their parent DC already saved - Collection parentIsSaved = notSaved.findAll { !it.parentDataClass || it.parentDataClass.id } + Collection parentIsSaved = notSaved.findAll {!it.parentDataClass || it.parentDataClass.id} log.trace('Ready to save on first run {}', parentIsSaved.size()) while (parentIsSaved) { - parentIsSaved.each { dc -> + parentIsSaved.each {dc -> dataElements.addAll dc.dataElements ?: [] dc.dataClasses?.clear() @@ -285,7 +285,7 @@ class DataClassService extends ModelItemService implements SummaryMet batch.clear() // Find all DCs which have a saved parent DC notSaved.removeAll(parentIsSaved) - parentIsSaved = notSaved.findAll { it.parentDataClass && it.parentDataClass.id } + parentIsSaved = notSaved.findAll {it.parentDataClass && it.parentDataClass.id} log.trace('Ready to save on subsequent run {}', parentIsSaved.size()) } } @@ -298,29 +298,29 @@ class DataClassService extends ModelItemService implements SummaryMet dataClass.breadcrumbTree.removeFromParent() dataClass.dataModel.removeFromDataClasses(dataClass) dataClass.extendedDataClasses - dataClass.dataClasses?.each { removeAssociations(it) } + dataClass.dataClasses?.each {removeAssociations(it)} } private void removeSemanticLinks(DataClass dataClass) { List semanticLinks = semanticLinkService.findAllByMultiFacetAwareItemId(dataClass.id) - semanticLinks.each { semanticLinkService.delete(it) } + semanticLinks.each {semanticLinkService.delete(it)} } private void removeReferenceTypes(DataClass dataClass) { List referenceTypes = new ArrayList<>(dataClass.referenceTypes.findAll()) - referenceTypes.each { dataTypeService.delete(it) } + referenceTypes.each {dataTypeService.delete(it)} } private void removeAllDataElementsWithNoLabel(DataClass dataClass) { - List dataElements = new ArrayList<>(dataClass.dataElements.findAll { !it.label }) - dataElements.each { dataElementService.delete(it) } + List dataElements = new ArrayList<>(dataClass.dataElements.findAll {!it.label}) + dataElements.each {dataElementService.delete(it)} } private void removeAllDataElementsWithSameLabel(DataClass dataClass) { if (dataClass.dataElements) { - Map> identicalDataElements = dataClass.dataElements.groupBy { it.label }.findAll { it.value.size() > 1 } - identicalDataElements.each { label, dataElements -> + Map> identicalDataElements = dataClass.dataElements.groupBy {it.label}.findAll {it.value.size() > 1} + identicalDataElements.each {label, dataElements -> for (int i = 1; i < dataElements.size(); i++) { dataElementService.delete(dataElements[i]) } @@ -330,8 +330,8 @@ class DataClassService extends ModelItemService implements SummaryMet private void ensureChildDataClassesHaveUniqueNames(DataClass dataClass) { if (dataClass.dataClasses) { - dataClass.dataClasses.groupBy { it.label }.findAll { it.value.size() > 1 }.each { label, dataClasses -> - dataClasses.eachWithIndex { DataClass child, int i -> + dataClass.dataClasses.groupBy {it.label}.findAll {it.value.size() > 1}.each {label, dataClasses -> + dataClasses.eachWithIndex {DataClass child, int i -> child.label = "${child.label}-$i" } } @@ -341,10 +341,10 @@ class DataClassService extends ModelItemService implements SummaryMet private void collapseReferenceTypes(DataClass dataClass) { if (!dataClass.referenceTypes || dataClass.referenceTypes.size() == 1) return DataModel dataModel = dataClass.dataModel - Map> labelGroupedReferenceTypes = dataClass.referenceTypes.groupBy { it.label } + Map> labelGroupedReferenceTypes = dataClass.referenceTypes.groupBy {it.label} - labelGroupedReferenceTypes.findAll { it.value.size() > 1 }.each { label, labelReferenceTypes -> - Map> dmGrouped = labelReferenceTypes.groupBy { it.dataModel ? 'dataModel' : 'noDataModel' } + labelGroupedReferenceTypes.findAll {it.value.size() > 1}.each {label, labelReferenceTypes -> + Map> dmGrouped = labelReferenceTypes.groupBy {it.dataModel ? 'dataModel' : 'noDataModel'} // There will be only 1 datamodel owned type as we've already merged datamodel owned datatypes if (dmGrouped.dataModel) { @@ -362,11 +362,11 @@ class DataClassService extends ModelItemService implements SummaryMet private void setCreatedBy(User creator, DataClass dataClass) { dataClass.createdBy = creator.emailAddress - dataClass.dataClasses?.each { dc -> + dataClass.dataClasses?.each {dc -> setCreatedBy(creator, dc) } - dataClass.dataElements?.each { de -> + dataClass.dataElements?.each {de -> de.createdBy = creator.emailAddress } } @@ -378,13 +378,13 @@ class DataClassService extends ModelItemService implements SummaryMet checkFacetsAfterImportingCatalogueItem(dataClass) if (dataClass.dataClasses) { dataClass.fullSortOfChildren(dataClass.dataClasses) - dataClass.dataClasses.each { dc -> + dataClass.dataClasses.each {dc -> checkImportedDataClassAssociations(importingUser, dataModel, dc, matchDataTypes) } } if (dataClass.dataElements) { dataClass.fullSortOfChildren(dataClass.dataElements) - dataClass.dataElements.each { de -> + dataClass.dataElements.each {de -> de.createdBy = importingUser.emailAddress de.buildPath() dataElementService.checkFacetsAfterImportingCatalogueItem(de) @@ -394,7 +394,7 @@ class DataClassService extends ModelItemService implements SummaryMet } DataClass findSameLabelTree(DataModel dataModel, DataClass searchFor) { - dataModel.dataClasses.find { hasSameLabelTree(it, searchFor) } + dataModel.dataClasses.find {hasSameLabelTree(it, searchFor)} } private boolean hasSameLabelTree(DataClass left, DataClass right) { @@ -548,11 +548,11 @@ class DataClassService extends ModelItemService implements SummaryMet } DataClass findDataClass(DataModel dataModel, String label) { - dataModel.dataClasses.find { !it.parentDataClass && it.label == label.trim() } + dataModel.dataClasses.find {!it.parentDataClass && it.label == label.trim()} } DataClass findDataClass(DataClass parentDataClass, String label) { - parentDataClass.dataClasses.find { it.label == label.trim() } + parentDataClass.dataClasses.find {it.label == label.trim()} } DataClass findDataClassByPath(DataModel dataModel, List pathLabels) { @@ -581,9 +581,12 @@ class DataClassService extends ModelItemService implements SummaryMet } DataClass copyDataClassMatchingAllReferenceTypes(DataModel copiedDataModel, DataClass original, User copier, - UserSecurityPolicyManager userSecurityPolicyManager, Serializable parentDataClassId, - CopyInformation copyInformation = new CopyInformation()) { - DataClass copiedDataClass = copyDataClass(copiedDataModel, original, copier, userSecurityPolicyManager, parentDataClassId, false, + UserSecurityPolicyManager userSecurityPolicyManager, UUID parentDataClassId, + CopyInformation copyInformation = null) { + DataClass copiedDataClass = copyDataClass(copiedDataModel, original, copier, + userSecurityPolicyManager, + parentDataClassId ? get(parentDataClassId) : null, + false, copyInformation) log.debug('Copied required DataClass, now checking for reference classes which haven\'t been matched or added') matchUpAndAddMissingReferenceTypeClasses(copiedDataModel, original.dataModel, copier, userSecurityPolicyManager) @@ -591,18 +594,28 @@ class DataClassService extends ModelItemService implements SummaryMet } + @Override + DataClass copy(Model copiedDataModel, DataClass original, UUID parentDataClassId, UserSecurityPolicyManager userSecurityPolicyManager) { + copy(copiedDataModel as DataModel, original, parentDataClassId ? get(parentDataClassId) : null, userSecurityPolicyManager) + } @Override - DataClass copy(Model copiedDataModel, DataClass original, UserSecurityPolicyManager userSecurityPolicyManager, - UUID parentDataClassId = null, CopyInformation copyInformation = new CopyInformation()) { + DataClass copy(Model copiedDataModel, DataClass original, CatalogueItem parentDataClass, UserSecurityPolicyManager userSecurityPolicyManager) { copyDataClass(copiedDataModel as DataModel, original, userSecurityPolicyManager.user, userSecurityPolicyManager, - parentDataClassId, false, copyInformation) + parentDataClass as DataClass, + false, null) + } + + DataClass copyDataClass(DataModel copiedDataModel, DataClass original, User copier, UserSecurityPolicyManager userSecurityPolicyManager) { + copyDataClass(copiedDataModel, original, copier, userSecurityPolicyManager, null, false, null) } DataClass copyDataClass(DataModel copiedDataModel, DataClass original, User copier, UserSecurityPolicyManager userSecurityPolicyManager, - Serializable parentDataClassId = null, boolean copySummaryMetadata = false, - CopyInformation copyInformation = new CopyInformation()) { + DataClass parentDataClass, + boolean copySummaryMetadata, + CopyInformation copyInformation) { + if (!original) throw new ApiInternalException('DCSXX', 'Cannot copy non-existent DataClass') DataClass copy = new DataClass( @@ -615,15 +628,15 @@ class DataClassService extends ModelItemService implements SummaryMet copiedDataModel.addToDataClasses(copy) - if (parentDataClassId) { - get(parentDataClassId).addToDataClasses(copy) + if (parentDataClass) { + parentDataClass.addToDataClasses(copy) } if (!copy.validate()) //save(validate: false, copy) else throw new ApiInvalidModelException('DCS01', 'Copied DataClass is invalid', copy.errors, messageSource) - original.referenceTypes.each { refType -> - ReferenceType referenceType = copiedDataModel.referenceTypes.find { it.label == refType.label } + original.referenceTypes.each {refType -> + ReferenceType referenceType = copiedDataModel.referenceTypes.find {it.label == refType.label} if (!referenceType) { referenceType = new ReferenceType(createdBy: copier.emailAddress, label: refType.label) copiedDataModel.addToDataTypes(referenceType) @@ -632,11 +645,11 @@ class DataClassService extends ModelItemService implements SummaryMet } copy.dataClasses = [] - original.dataClasses.each { child -> + original.dataClasses.each {child -> copy.addToDataClasses(copyDataClass(copiedDataModel, child, copier, userSecurityPolicyManager)) } copy.dataElements = [] - original.dataElements.each { element -> + original.dataElements.each {element -> copy.addToDataElements(dataElementService.copyDataElement(copiedDataModel, element, copier, userSecurityPolicyManager)) } @@ -674,7 +687,7 @@ class DataClassService extends ModelItemService implements SummaryMet if (!emptyReferenceTypes) return log.debug('Found {} empty reference types', emptyReferenceTypes.size()) // Copy all the missing reference classes - emptyReferenceTypes.each { rt -> + emptyReferenceTypes.each {rt -> ReferenceType ort = originalDataModel.findDataTypeByLabel(rt.label) as ReferenceType String originalDataClassPath = buildPath(ort.referenceClass) DataClass copiedDataClass = findDataClassByPath(copiedDataModel, originalDataClassPath.split(/\|/).toList()) @@ -693,7 +706,7 @@ class DataClassService extends ModelItemService implements SummaryMet private Set getAllNestedReferenceTypes(DataClass dataClass) { Set referenceTypes = [] referenceTypes.addAll(dataClass.referenceTypes ?: []) - referenceTypes.addAll(dataClass.dataElements.dataType.findAll { it.instanceOf(ReferenceType) }) + referenceTypes.addAll(dataClass.dataElements.dataType.findAll {it.instanceOf(ReferenceType)}) dataClass.dataClasses.each { referenceTypes.addAll(getAllNestedReferenceTypes(it)) } @@ -702,7 +715,7 @@ class DataClassService extends ModelItemService implements SummaryMet private Set findAllEmptyReferenceTypes(DataModel dataModel) { - dataModel.referenceTypes.findAll { !(it as ReferenceType).referenceClass } as Set + dataModel.referenceTypes.findAll {!(it as ReferenceType).referenceClass} as Set } @@ -741,7 +754,7 @@ class DataClassService extends ModelItemService implements SummaryMet @Override List findAllReadableByClassifier(UserSecurityPolicyManager userSecurityPolicyManager, Classifier classifier) { - DataClass.byClassifierId(classifier.id).list().findAll { userSecurityPolicyManager.userCanReadSecuredResourceId(DataModel, it.model.id) } + DataClass.byClassifierId(classifier.id).list().findAll {userSecurityPolicyManager.userCanReadSecuredResourceId(DataModel, it.model.id)} } @Override @@ -779,10 +792,10 @@ class DataClassService extends ModelItemService implements SummaryMet List alreadyExistingLinks = semanticLinkService.findAllBySourceMultiFacetAwareItemIdInListAndTargetMultiFacetAwareItemIdInListAndLinkType( dataClasses*.id, fromDataClasses*.id, SemanticLinkType.IS_FROM) - dataClasses.each { de -> - fromDataClasses.each { fde -> + dataClasses.each {de -> + fromDataClasses.each {fde -> // If no link already exists then add a new one - if (!alreadyExistingLinks.any { it.multiFacetAwareItemId == de.id && it.targetMultiFacetAwareItemId == fde.id }) { + if (!alreadyExistingLinks.any {it.multiFacetAwareItemId == de.id && it.targetMultiFacetAwareItemId == fde.id}) { setDataClassIsFromDataClass(de, fde, user) } } diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementService.groovy index aba5e8e119..507454383d 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementService.groovy @@ -21,6 +21,7 @@ import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLink import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkType +import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItemService import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.CopyInformation @@ -322,13 +323,12 @@ class DataElementService extends ModelItemService implements Summar } //Put the dataClass lookup in this method for use when merging - DataElement copy(Model copiedDataModel, DataElement original, UserSecurityPolicyManager userSecurityPolicyManager) { + @Override + DataElement copy(Model copiedDataModel, DataElement original, CatalogueItem parentDataClass, UserSecurityPolicyManager userSecurityPolicyManager) { DataElement copy = copyDataElement(copiedDataModel as DataModel, original, userSecurityPolicyManager.user, userSecurityPolicyManager) - DataClass dataClass = copiedDataModel.getDataClasses()?.find { it.label == original.dataClass.label } - if (dataClass) { - dataClass.addToDataElements(copy) + if (parentDataClass) { + (parentDataClass as DataClass).addToDataElements(copy) } - copy } diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataTypeService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataTypeService.groovy index 1d4eaa519d..ff8314ffee 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataTypeService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataTypeService.groovy @@ -20,6 +20,7 @@ package uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata +import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItemService @@ -342,12 +343,13 @@ class DataTypeService extends ModelItemService implements DefaultDataT } } else { log.trace('Making best guess for matching reference class as no path nor bound class') - DataClass dataClass = dataModel.dataClasses.find { it.label == bindingMap.referenceClass.label } + DataClass dataClass = dataModel.dataClasses.find {it.label == bindingMap.referenceClass.label} if (dataClass) dataClass.addToReferenceTypes(referenceType) } } - DataType copy(Model copiedDataModel, DataType original, UserSecurityPolicyManager userSecurityPolicyManager) { + @Override + DataType copy(Model copiedDataModel, DataType original, CatalogueItem nonModelParent, UserSecurityPolicyManager userSecurityPolicyManager) { copyDataType(copiedDataModel as DataModel, original, userSecurityPolicyManager.user, userSecurityPolicyManager) } diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/EnumerationTypeService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/EnumerationTypeService.groovy index 8818a8bf87..dcca274953 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/EnumerationTypeService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/EnumerationTypeService.groovy @@ -18,6 +18,8 @@ package uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier +import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem +import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItemService import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel @@ -38,6 +40,7 @@ class EnumerationTypeService extends ModelItemService implement EnumerationValueService enumerationValueService SummaryMetadataService summaryMetadataService + DataTypeService dataTypeService @Override EnumerationType get(Serializable id) { @@ -72,6 +75,11 @@ class EnumerationTypeService extends ModelItemService implement enumerationType.delete(flush: flush) } + @Override + EnumerationType copy(Model copiedDataModel, EnumerationType original, CatalogueItem nonModelParent, UserSecurityPolicyManager userSecurityPolicyManager) { + dataTypeService.copy(copiedDataModel, original, nonModelParent, userSecurityPolicyManager) as EnumerationType + } + @Override boolean hasTreeTypeModelItems(EnumerationType catalogueItem, boolean fullTreeRender) { fullTreeRender && catalogueItem.enumerationValues diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ModelDataTypeService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ModelDataTypeService.groovy index 6f99b6bae7..6cbb3a21cd 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ModelDataTypeService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ModelDataTypeService.groovy @@ -18,6 +18,8 @@ package uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier +import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem +import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItemService import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.facet.SummaryMetadataService @@ -35,6 +37,7 @@ import org.grails.datastore.mapping.model.PersistentEntity class ModelDataTypeService extends ModelItemService implements SummaryMetadataAwareService { SummaryMetadataService summaryMetadataService + DataTypeService dataTypeService @Override ModelDataType get(Serializable id) { @@ -98,6 +101,11 @@ class ModelDataTypeService extends ModelItemService implements Su domainType == ModelDataType.simpleName } + @Override + ModelDataType copy(Model copiedDataModel, ModelDataType original, CatalogueItem nonModelParent, UserSecurityPolicyManager userSecurityPolicyManager) { + dataTypeService.copy(copiedDataModel, original, nonModelParent, userSecurityPolicyManager) as ModelDataType + } + @Override List findAllReadableTreeTypeCatalogueItemsBySearchTermAndDomain(UserSecurityPolicyManager userSecurityPolicyManager, String searchTerm, String domainType) { diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/PrimitiveTypeService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/PrimitiveTypeService.groovy index 7b29fff392..bb89188586 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/PrimitiveTypeService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/PrimitiveTypeService.groovy @@ -18,6 +18,8 @@ package uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier +import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem +import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItemService import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.facet.SummaryMetadataService @@ -38,6 +40,7 @@ class PrimitiveTypeService extends ModelItemService implements Su public static final String DEFAULT_TEXT_TYPE_DESCRIPTION = 'Text Data Type' SummaryMetadataService summaryMetadataService + DataTypeService dataTypeService @Override PrimitiveType get(Serializable id) { @@ -72,6 +75,11 @@ class PrimitiveTypeService extends ModelItemService implements Su primitiveType.delete(flush: flush) } + @Override + PrimitiveType copy(Model copiedDataModel, PrimitiveType original, CatalogueItem nonModelParent, UserSecurityPolicyManager userSecurityPolicyManager) { + dataTypeService.copy(copiedDataModel, original, nonModelParent, userSecurityPolicyManager) as PrimitiveType + } + @Override PrimitiveType findByIdJoinClassifiers(UUID id) { PrimitiveType.findById(id, [fetch: [classifiers: 'join']]) diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ReferenceTypeService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ReferenceTypeService.groovy index d14b79a261..231d975405 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ReferenceTypeService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ReferenceTypeService.groovy @@ -18,6 +18,8 @@ package uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier +import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem +import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItemService import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.facet.SummaryMetadata @@ -37,6 +39,7 @@ import org.grails.datastore.mapping.model.PersistentEntity class ReferenceTypeService extends ModelItemService implements SummaryMetadataAwareService { SummaryMetadataService summaryMetadataService + DataTypeService dataTypeService @Override ReferenceType get(Serializable id) { @@ -87,7 +90,7 @@ class ReferenceTypeService extends ModelItemService implements Su log.trace('Removing {} ReferenceTypes', referenceTypeIds.size()) sessionFactory.currentSession - .createSQLQuery('delete from datamodel.data_type where id in :ids') + .createSQLQuery('DELETE FROM datamodel.data_type WHERE id IN :ids') .setParameter('ids', referenceTypeIds) .executeUpdate() @@ -95,6 +98,11 @@ class ReferenceTypeService extends ModelItemService implements Su } } + @Override + ReferenceType copy(Model copiedDataModel, ReferenceType original, CatalogueItem nonModelParent, UserSecurityPolicyManager userSecurityPolicyManager) { + dataTypeService.copy(copiedDataModel, original, nonModelParent, userSecurityPolicyManager) as ReferenceType + } + @Override void deleteAllFacetDataByMultiFacetAwareIds(List catalogueItemIds) { super.deleteAllFacetDataByMultiFacetAwareIds(catalogueItemIds) diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValueService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValueService.groovy index ab1d1edf6e..84116c028b 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValueService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/enumeration/EnumerationValueService.groovy @@ -17,7 +17,9 @@ */ package uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.enumeration + import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier +import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItemService import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel @@ -29,7 +31,6 @@ import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.util.Utils import groovy.util.logging.Slf4j -import org.grails.web.databinding.bindingsource.InvalidRequestBodyException @Slf4j class EnumerationValueService extends ModelItemService implements SummaryMetadataAwareService { @@ -88,7 +89,7 @@ class EnumerationValueService extends ModelItemService impleme log.trace('Removing {} EnumerationValues', enumerationValueIds.size()) sessionFactory.currentSession - .createSQLQuery('delete from datamodel.enumeration_value where id in :ids') + .createSQLQuery('DELETE FROM datamodel.enumeration_value WHERE id IN :ids') .setParameter('ids', enumerationValueIds) .executeUpdate() @@ -168,28 +169,20 @@ class EnumerationValueService extends ModelItemService impleme } @Override - EnumerationValue copy(Model copiedDataModel, EnumerationValue original, UserSecurityPolicyManager userSecurityPolicyManager) { - copyDataClass(copiedDataModel as DataModel, original, userSecurityPolicyManager.user, userSecurityPolicyManager) + EnumerationValue copy(Model copiedDataModel, EnumerationValue original, CatalogueItem enumerationTypeToCopyInto, UserSecurityPolicyManager userSecurityPolicyManager) { + copyEnumerationValue(copiedDataModel as DataModel, original, enumerationTypeToCopyInto as EnumerationType, userSecurityPolicyManager.user, userSecurityPolicyManager) } - EnumerationValue copyDataClass(DataModel copiedDataModel, EnumerationValue original, User copier, - UserSecurityPolicyManager userSecurityPolicyManager, - Serializable parentDataClassId = null, boolean copySummaryMetadata = false) { + EnumerationValue copyEnumerationValue(DataModel copiedDataModel, EnumerationValue original, EnumerationType enumerationTypeToCopyInto, User copier, + UserSecurityPolicyManager userSecurityPolicyManager) { EnumerationValue copy = new EnumerationValue(key: original.key, - value: original.value) + value: original.value) copy = copyCatalogueItemInformation(original, copy, copier, userSecurityPolicyManager) setCatalogueItemRefinesCatalogueItem(copy, original, copier) - EnumerationType enumerationType = copiedDataModel.findEnumerationTypeByLabel(original.enumerationType.label) - - // We should not have a situation where there is not an EnumerationType - if (!enumerationType) { - throw new InvalidRequestBodyException('EVS01','EnumerationType not found for the given EnumerationValue') - } - + EnumerationType enumerationType = enumerationTypeToCopyInto ?: copiedDataModel.findEnumerationTypeByLabel(original.enumerationType.label) enumerationType.addToEnumerationValues(copy) - copy } diff --git a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadataService.groovy b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadataService.groovy index 79abbe04eb..b1b5aea35d 100644 --- a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadataService.groovy +++ b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadataService.groovy @@ -94,4 +94,14 @@ class ReferenceSummaryMetadataService implements MultiFacetItemAwareService + copy.addToSummaryMetadataReports(reportDate: smr.reportDate, reportValue: smr.reportValue) + } + (multiFacetAwareItemToCopyInto as ReferenceSummaryMetadataAware).addToReferenceSummaryMetadata(copy) + copy + } } \ No newline at end of file diff --git a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermRelationshipTypeService.groovy b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermRelationshipTypeService.groovy index 91fd69bbb7..166a881bee 100644 --- a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermRelationshipTypeService.groovy +++ b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermRelationshipTypeService.groovy @@ -19,6 +19,7 @@ package uk.ac.ox.softeng.maurodatamapper.terminology.item import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier import uk.ac.ox.softeng.maurodatamapper.core.facet.EditTitle +import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItemService import uk.ac.ox.softeng.maurodatamapper.security.User @@ -93,7 +94,7 @@ class TermRelationshipTypeService extends ModelItemService } @Override - TermRelationshipType copy(Model copiedTerminology, TermRelationshipType original, UserSecurityPolicyManager userSecurityPolicyManager) { + TermRelationshipType copy(Model copiedTerminology, TermRelationshipType original, CatalogueItem nonModelParent, UserSecurityPolicyManager userSecurityPolicyManager) { copyTermRelationshipType(copiedTerminology as Terminology, original, userSecurityPolicyManager.user) } diff --git a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermService.groovy b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermService.groovy index 4fdd096257..cfa7cb4da1 100644 --- a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermService.groovy +++ b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/TermService.groovy @@ -19,6 +19,7 @@ package uk.ac.ox.softeng.maurodatamapper.terminology.item import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier +import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItemService @@ -210,7 +211,7 @@ class TermService extends ModelItemService { Term.byTerminologyIdAndNotChild(terminologyId).list(pagination) } - Term copy(Model copiedModel, Term original, UserSecurityPolicyManager userSecurityPolicyManager) { + Term copy(Model copiedModel, Term original, CatalogueItem nonModelParent, UserSecurityPolicyManager userSecurityPolicyManager) { Term copy = copyTerm(original, userSecurityPolicyManager.user, userSecurityPolicyManager) if (copiedModel.instanceOf(Terminology)) { (copiedModel as Terminology).addToTerms(copy) diff --git a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/term/TermRelationshipService.groovy b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/term/TermRelationshipService.groovy index 6b9a6d1666..ff10fece18 100644 --- a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/term/TermRelationshipService.groovy +++ b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/item/term/TermRelationshipService.groovy @@ -19,6 +19,7 @@ package uk.ac.ox.softeng.maurodatamapper.terminology.item.term import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier +import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItemService import uk.ac.ox.softeng.maurodatamapper.security.User @@ -127,7 +128,8 @@ class TermRelationshipService extends ModelItemService { TermRelationship.byTermIdHasHierarchy(termId).count() } - TermRelationship copy(Model terminology, TermRelationship original, UserSecurityPolicyManager userSecurityPolicyManager, UUID parentId = null) { + @Override + TermRelationship copy(Model terminology, TermRelationship original, CatalogueItem nonModelParent, UserSecurityPolicyManager userSecurityPolicyManager) { copyTermRelationship(terminology as Terminology, original, userSecurityPolicyManager.user) } From 776aa56cfb1568d0dca4cbc39288f7c72ebe27dc Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Thu, 15 Jul 2021 17:15:54 +0100 Subject: [PATCH 45/82] Implement the mergeInto for the new style format * Move existing code into legacy methods * Implement each part inside ModelService for the new style using the path to find the object and then the services to act on that object where required * Implement full integration testing inside DataModel plugin, still need to add relevant testing inside terminology plugin Important to note that the only calls now outside of ModelService for merging are for overriding, this should make it easier for us to override/implement at the folder level --- .../transport/merge/FieldPatchData.groovy | 102 +++-- .../transport/merge/ObjectPatchData.groovy | 17 +- .../merge/{ => legacy}/ItemPatchData.groovy | 2 +- .../merge/legacy/LegacyFieldPatchData.groovy | 71 ++++ .../core/controller/ModelController.groovy | 3 +- .../core/model/CatalogueItemService.groovy | 13 +- .../core/model/ModelItemService.groovy | 28 +- .../core/model/ModelService.groovy | 165 +++++++- .../datamodel/DataModelService.groovy | 90 +++-- .../bootstrap/BootstrapModels.groovy | 56 +-- .../DataModelServiceIntegrationSpec.groovy | 365 +++++++++++++++++- .../ReferenceDataModelService.groovy | 15 + .../terminology/CodeSetService.groovy | 15 +- 13 files changed, 787 insertions(+), 155 deletions(-) rename mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/{ => legacy}/ItemPatchData.groovy (99%) create mode 100644 mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/legacy/LegacyFieldPatchData.groovy diff --git a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy index abfff48450..479b62cf1f 100644 --- a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy +++ b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy @@ -17,54 +17,104 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge +import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.CreationMergeDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.DeletionMergeDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.FieldMergeDiff +import uk.ac.ox.softeng.maurodatamapper.util.Path import grails.validation.Validateable /** * @since 07/02/2018 */ -class FieldPatchData implements Validateable { +class FieldPatchData implements Validateable { String fieldName - T value - Collection created - Collection deleted - Collection modified + Path path + T sourceValue + T targetValue + T commonAncestorValue + boolean isMergeConflict + String type static constraints = { fieldName nullable: false, blank: false - value validator: {val, obj -> - if (val && (created || deleted || modified)) return ['invalid.patch.value.and.array.changes'] - if (!val && !created && !deleted && !modified) return ['invalid.patch.no.changes'] - true - } - } + path nullable: false, blank: false + sourceValue nullable: true + targetValue nullable: true + commonAncestorValue nullable: true + type nullable: false, blank: false, inList: ['creation', 'deletion', 'modification'] - FieldPatchData() { - created = [] - deleted = [] - modified = [] } - boolean hasPatches() { - value || !created.isEmpty() || !deleted.isEmpty() || modified.any {it.hasPatches()} + boolean isMetadataChange() { + false } - boolean isFieldChange() { - value + boolean isCreation() { + type == 'creation' } - boolean isMetadataChange() { - fieldName == 'metadata' + boolean isDeletion() { + type == 'deletion' } - String getSummary() { - String prefix = "Merge patch summary on field [${fieldName}]" - if (isFieldChange()) return "${prefix}: Changing value" - "${prefix}: Creating ${created.size()} Deleting ${deleted.size()} Modifying ${modified.size()}" + boolean isModification() { + type == 'modification' } String toString() { - "Merge patch on field [${fieldName}]" + "Merge ${type} patch on ${path} :: Changing ${targetValue} to ${sourceValue}" + } + + void setPath(String path) { + this.path = Path.from(path) + } + + void setPath(Path path) { + this.path = path + } + + Path getRootIndependentPath() { + this.path.clone().tap { + first().label = null + } + } + + static

FieldPatchData

from(FieldMergeDiff

fieldMergeDiff) { + new FieldPatchData().tap { + fieldName = fieldMergeDiff.fieldName + sourceValue = fieldMergeDiff.source + targetValue = fieldMergeDiff.target + commonAncestorValue = fieldMergeDiff.commonAncestor + path = fieldMergeDiff.fullyQualifiedPath + isMergeConflict = fieldMergeDiff.isMergeConflict() + type = 'modification' + } + } + + static

FieldPatchData

from(CreationMergeDiff

creationMergeDiff) { + new FieldPatchData().tap { + // fieldName = creationMergeDiff.fieldName + sourceValue = creationMergeDiff.source + targetValue = creationMergeDiff.target + commonAncestorValue = creationMergeDiff.commonAncestor + path = creationMergeDiff.fullyQualifiedPath + isMergeConflict = creationMergeDiff.isMergeConflict() + type = 'creation' + } + } + + static

FieldPatchData

from(DeletionMergeDiff

deletionMergeDiff) { + new FieldPatchData().tap { + // fieldName = deletionMergeDiff.fieldName + sourceValue = deletionMergeDiff.source + targetValue = deletionMergeDiff.target + commonAncestorValue = deletionMergeDiff.commonAncestor + path = deletionMergeDiff.fullyQualifiedPath + isMergeConflict = deletionMergeDiff.isMergeConflict() + type = 'deletion' + } } } diff --git a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/ObjectPatchData.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/ObjectPatchData.groovy index 6d34da9cb9..99b15c6a8a 100644 --- a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/ObjectPatchData.groovy +++ b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/ObjectPatchData.groovy @@ -17,6 +17,8 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.legacy.LegacyFieldPatchData + import grails.validation.Validateable /** @@ -30,13 +32,15 @@ class ObjectPatchData implements Validateable { private List patches @Deprecated - List diffs + List diffs static constraints = { sourceId nullable: false targetId nullable: false label nullable: true, blank: false - patches minSize: 1 + patches validator: {val, obj -> + if (!val && !obj.diffs) ['default.invalid.min.message', 1] + } } ObjectPatchData() { @@ -45,11 +49,16 @@ class ObjectPatchData implements Validateable { } boolean hasPatches() { - getPatches().any {it.hasPatches()} + patches || getDiffsWithContent() } List getPatches() { - patches.findAll {it.hasPatches()} + diffs.findAll {it.hasPatches()} + return patches + } + + @Deprecated + List getDiffsWithContent() { + diffs.findAll {it.hasPatches()} } @Deprecated diff --git a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/ItemPatchData.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/legacy/ItemPatchData.groovy similarity index 99% rename from mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/ItemPatchData.groovy rename to mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/legacy/ItemPatchData.groovy index 3bcfa510ff..9e925512b8 100644 --- a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/ItemPatchData.groovy +++ b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/legacy/ItemPatchData.groovy @@ -15,7 +15,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge +package uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.legacy import grails.validation.Validateable diff --git a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/legacy/LegacyFieldPatchData.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/legacy/LegacyFieldPatchData.groovy new file mode 100644 index 0000000000..154253c7c6 --- /dev/null +++ b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/legacy/LegacyFieldPatchData.groovy @@ -0,0 +1,71 @@ +/* + * 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.rest.transport.merge.legacy + +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.ObjectPatchData + +import grails.validation.Validateable + +/** + * @since 07/02/2018 + */ +class LegacyFieldPatchData implements Validateable { + + String fieldName + T value + Collection created + Collection deleted + Collection modified + + static constraints = { + fieldName nullable: false, blank: false + value validator: {val, obj -> + if (val && (created || deleted || modified)) return ['invalid.patch.value.and.array.changes'] + if (!val && !created && !deleted && !modified) return ['invalid.patch.no.changes'] + true + } + } + + LegacyFieldPatchData() { + created = [] + deleted = [] + modified = [] + } + + boolean hasPatches() { + value || !created.isEmpty() || !deleted.isEmpty() || modified.any {it.hasPatches()} + } + + boolean isFieldChange() { + value + } + + boolean isMetadataChange() { + fieldName == 'metadata' + } + + String getSummary() { + String prefix = "Merge patch summary on field [${fieldName}]" + if (isFieldChange()) return "${prefix}: Changing value" + "${prefix}: Creating ${created.size()} Deleting ${deleted.size()} Modifying ${modified.size()}" + } + + String toString() { + "Merge patch on field [${fieldName}]" + } +} diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy index 8ebf586503..1fdde3e572 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/controller/ModelController.groovy @@ -295,7 +295,8 @@ abstract class ModelController extends CatalogueItemController< T targetModel = queryForResource params.otherModelId if (!targetModel) return notFound(params.otherModelId) - T instance = modelService.mergeObjectPatchDataIntoModel(mergeIntoData.patch, targetModel, currentUserSecurityPolicyManager) as T + T instance = modelService.mergeObjectPatchDataIntoModel(mergeIntoData.patch, targetModel, sourceModel, + params.boolean('isLegacy', true), currentUserSecurityPolicyManager) as T if (!validateResource(instance, 'merge')) return diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy index c39daed3db..4a52c5eff0 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy @@ -17,7 +17,6 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.model - import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier import uk.ac.ox.softeng.maurodatamapper.core.container.ClassifierService import uk.ac.ox.softeng.maurodatamapper.core.facet.AnnotationService @@ -28,7 +27,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.Rule import uk.ac.ox.softeng.maurodatamapper.core.facet.RuleService import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkService import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkType -import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.FieldPatchData +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.legacy.LegacyFieldPatchData import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.CopyInformation import uk.ac.ox.softeng.maurodatamapper.core.traits.service.DomainService import uk.ac.ox.softeng.maurodatamapper.core.traits.service.MultiFacetAwareService @@ -142,7 +141,7 @@ abstract class CatalogueItemService implements DomainSe metadataService.findAllByMultiFacetAwareItemId(original.id).each {copy.addToMetadata(it.namespace, it.key, it.value, copier.emailAddress)} ruleService.findAllByMultiFacetAwareItemId(original.id).each {rule -> Rule copiedRule = new Rule(name: rule.name, description: rule.description, createdBy: copier.emailAddress) - rule.ruleRepresentations.each { ruleRepresentation -> + rule.ruleRepresentations.each {ruleRepresentation -> copiedRule.addToRuleRepresentations(language: ruleRepresentation.language, representation: ruleRepresentation.representation, createdBy: copier.emailAddress) @@ -196,17 +195,17 @@ abstract class CatalogueItemService implements DomainSe findByParentIdAndLabel(parentId, pathIdentifier) } - void mergeMetadataIntoCatalogueItem(FieldPatchData fieldPatchData, K targetCatalogueItem, - UserSecurityPolicyManager userSecurityPolicyManager) { + void mergeLegacyMetadataIntoCatalogueItem(LegacyFieldPatchData fieldPatchData, K targetCatalogueItem, + UserSecurityPolicyManager userSecurityPolicyManager) { log.debug('Merging Metadata into Catalogue Item') // call metadataService version of below - fieldPatchData.deleted.each { deletedItemPatchData -> + fieldPatchData.deleted.each {deletedItemPatchData -> Metadata metadata = metadataService.get(deletedItemPatchData.id) metadataService.delete(metadata) } // copy additions from source to target object - fieldPatchData.created.each { createdItemPatchData -> + fieldPatchData.created.each {createdItemPatchData -> Metadata metadata = metadataService.get(createdItemPatchData.id) metadataService.copy(targetCatalogueItem, metadata, userSecurityPolicyManager) } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy index 5df6d9d7d5..3b5935261c 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy @@ -19,9 +19,8 @@ package uk.ac.ox.softeng.maurodatamapper.core.model import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiNotYetImplementedException import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier -import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.FieldPatchData import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.ObjectPatchData -import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.CopyInformation +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.legacy.LegacyFieldPatchData import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.util.Utils @@ -61,18 +60,17 @@ abstract class ModelItemService extends CatalogueItemServic throw new ApiNotYetImplementedException('MIS03', "copy [for ModelItem ${getModelItemClass().simpleName}]") } - Model mergeObjectPatchDataIntoModelItem(ObjectPatchData objectPatchData, K targetModelItem, Model targetModel, - UserSecurityPolicyManager userSecurityPolicyManager) { - //TODO validation on saving merges + Model mergeLegacyObjectPatchDataIntoModelItem(ObjectPatchData objectPatchData, K targetModelItem, Model targetModel, + UserSecurityPolicyManager userSecurityPolicyManager) { if (!objectPatchData.hasPatches()) return targetModel - log.debug('Merging {} diffs into modelItem [{}]', objectPatchData.getPatches().size(), targetModelItem.label) - objectPatchData.getPatches().each { mergeFieldDiff -> + log.debug('Merging {} diffs into modelItem [{}]', objectPatchData.getDiffsWithContent().size(), targetModelItem.label) + objectPatchData.getDiffsWithContent().each {mergeFieldDiff -> log.debug('{}', mergeFieldDiff.summary) if (mergeFieldDiff.isFieldChange()) { targetModelItem.setProperty(mergeFieldDiff.fieldName, mergeFieldDiff.value) } else if (mergeFieldDiff.isMetadataChange()) { - mergeMetadataIntoCatalogueItem(mergeFieldDiff, targetModelItem, userSecurityPolicyManager) + mergeLegacyMetadataIntoCatalogueItem(mergeFieldDiff, targetModelItem, userSecurityPolicyManager) } else { ModelItemService modelItemService UUID parentId @@ -85,7 +83,7 @@ abstract class ModelItemService extends CatalogueItemServic } if (modelItemService) { - modelItemService.processFieldPatchData(mergeFieldDiff, targetModel, userSecurityPolicyManager, parentId) + modelItemService.processLegacyFieldPatchData(mergeFieldDiff, targetModel, userSecurityPolicyManager, parentId) } else { log.error('Unknown ModelItem field to merge [{}]', mergeFieldDiff.fieldName) @@ -97,20 +95,20 @@ abstract class ModelItemService extends CatalogueItemServic targetModel } - void processFieldPatchData(FieldPatchData fieldPatchData, Model targetModel, UserSecurityPolicyManager userSecurityPolicyManager, - UUID parentId = null) { + void processLegacyFieldPatchData(LegacyFieldPatchData fieldPatchData, Model targetModel, UserSecurityPolicyManager userSecurityPolicyManager, + UUID parentId = null) { // apply deletions of children to target object - fieldPatchData.deleted.each { deletedItemPatchData -> + fieldPatchData.deleted.each {deletedItemPatchData -> ModelItem modelItem = get(deletedItemPatchData.id) as ModelItem delete(modelItem) } // copy additions from source to target object - fieldPatchData.created.each { createdItemPatchData -> + fieldPatchData.created.each {createdItemPatchData -> ModelItem modelItem = get(createdItemPatchData.id) as ModelItem ModelItem copyModelItem if (parentId) { - copyModelItem = copy(targetModel, modelItem, userSecurityPolicyManager, parentId) + copyModelItem = copy(targetModel, modelItem, parentId, userSecurityPolicyManager) } else { copyModelItem = copy(targetModel, modelItem, userSecurityPolicyManager) } @@ -120,7 +118,7 @@ abstract class ModelItemService extends CatalogueItemServic // for modifications, recursively call this method fieldPatchData.modified.each {modifiedObjectPatchData -> ModelItem modelItem = get(modifiedObjectPatchData.targetId) as ModelItem - mergeObjectPatchDataIntoModelItem(modifiedObjectPatchData, modelItem, targetModel, userSecurityPolicyManager) + mergeLegacyObjectPatchDataIntoModelItem(modifiedObjectPatchData, modelItem, targetModel, userSecurityPolicyManager) } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy index 769ec80573..59655f47be 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy @@ -18,6 +18,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.model import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException +import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInvalidModelException import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiNotYetImplementedException import uk.ac.ox.softeng.maurodatamapper.core.authority.Authority @@ -26,26 +27,37 @@ import uk.ac.ox.softeng.maurodatamapper.core.container.Folder import uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff +import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTreeService import uk.ac.ox.softeng.maurodatamapper.core.facet.EditService import uk.ac.ox.softeng.maurodatamapper.core.facet.EditTitle +import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata +import uk.ac.ox.softeng.maurodatamapper.core.facet.ReferenceFile +import uk.ac.ox.softeng.maurodatamapper.core.facet.Rule +import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLink import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkType import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLink import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkService import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints +import uk.ac.ox.softeng.maurodatamapper.core.path.PathService import uk.ac.ox.softeng.maurodatamapper.core.provider.dataloader.DataLoaderProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.ModelImporterProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.ModelImporterProviderServiceParameters import uk.ac.ox.softeng.maurodatamapper.core.rest.converter.json.OffsetDateTimeConverter +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.FieldPatchData import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.ObjectPatchData import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.VersionTreeModel +import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.MultiFacetItemAware import uk.ac.ox.softeng.maurodatamapper.core.traits.service.DomainService +import uk.ac.ox.softeng.maurodatamapper.core.traits.service.MultiFacetItemAwareService import uk.ac.ox.softeng.maurodatamapper.core.traits.service.VersionLinkAwareService 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.traits.domain.CreatorAware +import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.Utils import uk.ac.ox.softeng.maurodatamapper.util.Version import uk.ac.ox.softeng.maurodatamapper.util.VersionChangeType @@ -74,6 +86,9 @@ abstract class ModelService extends CatalogueItemService imp @Autowired(required = false) Set domainServices + @Autowired(required = false) + Set multiFacetItemAwareServices + @Autowired VersionLinkService versionLinkService @@ -83,6 +98,9 @@ abstract class ModelService extends CatalogueItemService imp @Autowired EditService editService + @Autowired + PathService pathService + @Autowired MessageSource messageSource @@ -220,6 +238,7 @@ abstract class ModelService extends CatalogueItemService imp } else { criteria.eq('branchName', identity) } + criteria.get() as K } @@ -414,23 +433,147 @@ abstract class ModelService extends CatalogueItemService imp * @param domainService Service which handles catalogueItems of the leftModel and rightModel type. * @return The model resulting from the merging of changes. */ - K mergeObjectPatchDataIntoModel(ObjectPatchData objectPatchData, K targetModel, + K mergeObjectPatchDataIntoModel(ObjectPatchData objectPatchData, K targetModel, K sourceModel, boolean isLegacy, UserSecurityPolicyManager userSecurityPolicyManager) { - //TODO validation on saving merges - if (!objectPatchData.hasPatches()) return targetModel - log.debug('Merging {} diffs into model {}', objectPatchData.getPatches().size(), targetModel.label) - objectPatchData.getPatches().each { mergeFieldDiff -> + + if (!objectPatchData.hasPatches()) { + log.debug('No patch data to merge into {}', targetModel.id) + return targetModel + } + log.debug('Merging patch data into {}', targetModel.id) + if (isLegacy) return mergeLegacyObjectPatchDataIntoModel(objectPatchData, targetModel, userSecurityPolicyManager) + + objectPatchData.patches.each {fieldPatch -> + switch (fieldPatch.type) { + case 'creation': + return processCreationPatchIntoModel(fieldPatch, targetModel, sourceModel, userSecurityPolicyManager) + case 'deletion': + return processDeletionPatchIntoModel(fieldPatch, targetModel) + case 'modification': + return processModificationPatchIntoModel(fieldPatch, targetModel) + default: + log.warn('Unknown field patch type [{}]', fieldPatch.type) + } + } + targetModel + } + + + void processCreationPatchIntoModel(FieldPatchData creationPatch, K targetModel, K sourceModel, UserSecurityPolicyManager userSecurityPolicyManager) { + CreatorAware domainToCopy = pathService.findResourceByPathFromRootResource(sourceModel, creationPatch.path) + log.debug('Creating {} into {}', creationPatch.path, creationPatch.rootIndependentPath.parent) + // Potential deletions are modelitems or facets from model or modelitem + if (Utils.parentClassIsAssignableFromChild(ModelItem, domainToCopy.class)) { + processCreationPatchOfModelItem(domainToCopy as ModelItem, targetModel, creationPatch.rootIndependentPath.parent, userSecurityPolicyManager) + } + if (Utils.parentClassIsAssignableFromChild(MultiFacetItemAware, domainToCopy.class)) { + processCreationPatchOfFacet(domainToCopy as MultiFacetItemAware, targetModel, creationPatch.rootIndependentPath.parent, userSecurityPolicyManager) + } + } + + void processDeletionPatchIntoModel(FieldPatchData deletionPatch, K targetModel) { + CreatorAware domain = pathService.findResourceByPathFromRootResource(targetModel, deletionPatch.rootIndependentPath) + log.debug('Deleting [{}]', deletionPatch.rootIndependentPath) + + // Potential deletions are modelitems or facets from model or modelitem + if (Utils.parentClassIsAssignableFromChild(ModelItem, domain.class)) { + processDeletionPatchOfModelItem(domain as ModelItem) + } + if (Utils.parentClassIsAssignableFromChild(MultiFacetItemAware, domain.class)) { + processDeletionPatchOfFacet(domain as MultiFacetItemAware, targetModel, deletionPatch.rootIndependentPath) + } + } + + void processModificationPatchIntoModel(FieldPatchData modificationPatch, K targetModel) { + CreatorAware domain = pathService.findResourceByPathFromRootResource(targetModel, modificationPatch.rootIndependentPath) + String fieldName = modificationPatch.fieldName + log.debug('Modifying [{}] in [{}]', fieldName, modificationPatch.rootIndependentPath) + domain."${fieldName}" = modificationPatch.sourceValue + } + + void processDeletionPatchOfModelItem(ModelItem modelItem) { + ModelItemService modelItemService = modelItemServices.find {it.handles(modelItem.class)} + if (!modelItemService) throw new ApiInternalException('MSXX', "No domain service to handle deletion of [${modelItem.domainType}]") + log.debug('Deleting ModelItem from Model') + modelItemService.delete(modelItem) + } + + CatalogueItem processDeletionPatchOfFacet(MultiFacetItemAware multiFacetItemAware, Model targetModel, Path path) { + MultiFacetItemAwareService multiFacetItemAwareService = multiFacetItemAwareServices.find {it.handles(multiFacetItemAware.class)} + if (!multiFacetItemAwareService) throw new ApiInternalException('MSXX', "No domain service to handle deletion of [${multiFacetItemAware.domainType}]") + log.debug('Deleting Facet from path [{}]', path) + multiFacetItemAwareService.delete(multiFacetItemAware) + + CatalogueItem catalogueItem = pathService.findResourceByPathFromRootResource(targetModel, path.getParent()) as CatalogueItem + switch (multiFacetItemAware.domainType) { + case Metadata.simpleName: + catalogueItem.metadata.remove(multiFacetItemAware) + break + case Annotation.simpleName: + catalogueItem.annotations.remove(multiFacetItemAware) + break + case Rule.simpleName: + catalogueItem.rules.remove(multiFacetItemAware) + break + case SemanticLink.simpleName: + catalogueItem.semanticLinks.remove(multiFacetItemAware) + break + case ReferenceFile.simpleName: + catalogueItem.referenceFiles.remove(multiFacetItemAware) + break + case VersionLink.simpleName: + (catalogueItem as Model).versionLinks.remove(multiFacetItemAware) + break + } + catalogueItem + } + + void processCreationPatchOfModelItem(ModelItem modelItemToCopy, Model targetModel, Path parentPathToCopyTo, UserSecurityPolicyManager userSecurityPolicyManager) { + ModelItemService modelItemService = modelItemServices.find {it.handles(modelItemToCopy.class)} + if (!modelItemService) throw new ApiInternalException('MSXX', "No domain service to handle creation of [${modelItemToCopy.domainType}]") + log.debug('Creating ModelItem into Model at [{}]', parentPathToCopyTo) + CatalogueItem parentToCopyInto = pathService.findResourceByPathFromRootResource(targetModel, parentPathToCopyTo) as CatalogueItem + if (Utils.parentClassIsAssignableFromChild(Model, parentToCopyInto.class)) parentToCopyInto = null + ModelItem copy = modelItemService.copy(targetModel, modelItemToCopy, parentToCopyInto, userSecurityPolicyManager) + + if (!copy.validate()) + throw new ApiInvalidModelException('MS01', 'Copied ModelItem is invalid', copy.errors, messageSource) + + modelItemService.save(copy, flush: false, validate: false) + } + + void processCreationPatchOfFacet(MultiFacetItemAware multiFacetItemAwareToCopy, Model targetModel, Path parentPathToCopyTo, + UserSecurityPolicyManager userSecurityPolicyManager) { + MultiFacetItemAwareService multiFacetItemAwareService = multiFacetItemAwareServices.find {it.handles(multiFacetItemAwareToCopy.class)} + if (!multiFacetItemAwareService) throw new ApiInternalException('MSXX', "No domain service to handle deletion of [${multiFacetItemAwareToCopy.domainType}]") + log.debug('Creating Facet into Model at [{}]', parentPathToCopyTo) + + CatalogueItem parentToCopyInto = pathService.findResourceByPathFromRootResource(targetModel, parentPathToCopyTo) as CatalogueItem + MultiFacetItemAware copy = multiFacetItemAwareService.copy(multiFacetItemAwareToCopy, parentToCopyInto) + + if (!copy.validate()) + throw new ApiInvalidModelException('MS01', 'Copied Facet is invalid', copy.errors, messageSource) + + multiFacetItemAwareService.save(copy, flush: false, validate: false) + } + + @SuppressWarnings('GrDeprecatedAPIUsage') + @Deprecated + K mergeLegacyObjectPatchDataIntoModel(ObjectPatchData objectPatchData, K targetModel, UserSecurityPolicyManager userSecurityPolicyManager) { + + log.debug('Merging legacy {} diffs into model {}', objectPatchData.getDiffsWithContent().size(), targetModel.label) + objectPatchData.getDiffsWithContent().each {mergeFieldDiff -> log.debug('{}', mergeFieldDiff.summary) if (mergeFieldDiff.isFieldChange()) { targetModel.setProperty(mergeFieldDiff.fieldName, mergeFieldDiff.value) } else if (mergeFieldDiff.isMetadataChange()) { - mergeMetadataIntoCatalogueItem(mergeFieldDiff, targetModel, userSecurityPolicyManager) + mergeLegacyMetadataIntoCatalogueItem(mergeFieldDiff, targetModel, userSecurityPolicyManager) } else { - ModelItemService modelItemService = modelItemServices.find { it.handles(mergeFieldDiff.fieldName) } + ModelItemService modelItemService = modelItemServices.find {it.handles(mergeFieldDiff.fieldName)} if (modelItemService) { - modelItemService.processFieldPatchData(mergeFieldDiff, targetModel, userSecurityPolicyManager) + modelItemService.processLegacyFieldPatchData(mergeFieldDiff, targetModel, userSecurityPolicyManager) } else { log.error('Unknown ModelItem field to merge [{}]', mergeFieldDiff.fieldName) } @@ -453,7 +596,7 @@ abstract class ModelService extends CatalogueItemService imp if (versionLinkType == VersionLinkType.NEW_FORK_OF) return includeForks ? versionTreeModelList : [] List versionLinks = versionLinkService.findAllByTargetModelId(instance.id) - versionLinks.each { link -> + versionLinks.each {link -> K linkedModel = get(link.multiFacetAwareItemId) versionTreeModelList. addAll(buildModelVersionTree(linkedModel, link.linkType, rootVersionTreeModel, includeForks, branchesOnly, userSecurityPolicyManager)) @@ -666,12 +809,12 @@ abstract class ModelService extends CatalogueItemService imp if (countByAuthorityAndLabel(model.authority, model.label)) { List existingModels = findAllByAuthorityAndLabel(model.authority, model.label) - existingModels.each { existing -> + existingModels.each {existing -> log.debug('Setting Model as new documentation version of [{}:{}]', existing.label, existing.documentationVersion) if (!existing.finalised) finaliseModel(existing, catalogueUser, null, null, null) setModelIsNewDocumentationVersionOfModel(model, existing, catalogueUser) } - Version latestVersion = existingModels.max { it.documentationVersion }.documentationVersion + Version latestVersion = existingModels.max {it.documentationVersion}.documentationVersion model.documentationVersion = Version.nextMajorVersion(latestVersion) } else log.info('Marked as importAsNewDocumentationVersion but no existing Models with label [{}]', model.label) diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelService.groovy index 10795a4f7b..ce84623493 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelService.groovy @@ -24,13 +24,17 @@ import uk.ac.ox.softeng.maurodatamapper.core.authority.Authority import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier import uk.ac.ox.softeng.maurodatamapper.core.container.Folder import uk.ac.ox.softeng.maurodatamapper.core.facet.EditTitle +import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.model.Container +import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem import uk.ac.ox.softeng.maurodatamapper.core.model.ModelService import uk.ac.ox.softeng.maurodatamapper.core.provider.dataloader.DataLoaderProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.ModelImporterProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.ModelImporterProviderServiceParameters +import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.MultiFacetItemAware import uk.ac.ox.softeng.maurodatamapper.datamodel.facet.SummaryMetadata +import uk.ac.ox.softeng.maurodatamapper.datamodel.facet.SummaryMetadataAware import uk.ac.ox.softeng.maurodatamapper.datamodel.facet.SummaryMetadataService import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClass import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClassService @@ -50,6 +54,7 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.traits.service.SummaryMetadata import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.util.GormUtils +import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.Utils import uk.ac.ox.softeng.maurodatamapper.util.Version @@ -109,7 +114,7 @@ class DataModelService extends ModelService implements SummaryMetadat * DataModel allows the import of DataType and DataClass * @Override - List domainImportableModelItemClasses() {[DataType, DataClass, PrimitiveType, EnumerationType, ReferenceType]} + List domainImportableModelItemClasses() {[DataType, DataClass, PrimitiveType, EnumerationType, ReferenceType]} */ Long count() { DataModel.count() @@ -199,10 +204,10 @@ class DataModelService extends ModelService implements SummaryMetadat DataModel checkFacetsAfterImportingCatalogueItem(DataModel catalogueItem) { super.checkFacetsAfterImportingCatalogueItem(catalogueItem) if (catalogueItem.summaryMetadata) { - catalogueItem.summaryMetadata.each { sm -> + catalogueItem.summaryMetadata.each {sm -> sm.multiFacetAwareItemId = catalogueItem.id sm.createdBy = sm.createdBy ?: catalogueItem.createdBy - sm.summaryMetadataReports.each { smr -> + sm.summaryMetadataReports.each {smr -> smr.createdBy = catalogueItem.createdBy } } @@ -213,7 +218,7 @@ class DataModelService extends ModelService implements SummaryMetadat @Override DataModel saveModelWithContent(DataModel dataModel) { - if (dataModel.dataTypes.any { it.id } || dataModel.dataClasses.any { it.id }) { + if (dataModel.dataTypes.any {it.id} || dataModel.dataClasses.any {it.id}) { throw new ApiInternalException('DMSXX', 'Cannot use saveModelWithContent method to save DataModel', new IllegalStateException('DataModel has previously saved content')) } @@ -249,7 +254,7 @@ class DataModelService extends ModelService implements SummaryMetadat } if (dataModel.breadcrumbTree.children) { - dataModel.breadcrumbTree.children.each { it.skipValidation(true) } + dataModel.breadcrumbTree.children.each {it.skipValidation(true)} } save(dataModel) @@ -279,15 +284,15 @@ class DataModelService extends ModelService implements SummaryMetadat } if (dataModel.dataTypes) { - enumerationTypes.addAll dataModel.enumerationTypes.findAll { !it.id } - primitiveTypes.addAll dataModel.primitiveTypes.findAll { !it.id } - modelDataTypes.addAll dataModel.modelDataTypes.findAll { !it.id } - referenceTypes.addAll dataModel.referenceTypes.findAll { !it.id } + enumerationTypes.addAll dataModel.enumerationTypes.findAll {!it.id} + primitiveTypes.addAll dataModel.primitiveTypes.findAll {!it.id} + modelDataTypes.addAll dataModel.modelDataTypes.findAll {!it.id} + referenceTypes.addAll dataModel.referenceTypes.findAll {!it.id} } if (dataModel.dataClasses) { - dataClasses.addAll dataModel.dataClasses.findAll { !it.id } - dataElements.addAll dataModel.dataClasses.collectMany { it.dataElements.findAll { !it.id } } + dataClasses.addAll dataModel.dataClasses.findAll {!it.id} + dataElements.addAll dataModel.dataClasses.collectMany {it.dataElements.findAll {!it.id}} } saveContent(enumerationTypes, primitiveTypes, referenceTypes, modelDataTypes, dataClasses, dataElements) @@ -312,12 +317,12 @@ class DataModelService extends ModelService implements SummaryMetadat } primitiveTypes.each {it.skipValidation(true)} referenceTypes.each {it.skipValidation(true)} - modelDataTypes.each { it.skipValidation(true) } - dataClasses.each { dc -> + modelDataTypes.each {it.skipValidation(true)} + dataClasses.each {dc -> dc.skipValidation(true) - dc.dataElements.each { de -> de.skipValidation(true) } + dc.dataElements.each {de -> de.skipValidation(true)} } - referenceTypes.each { it.skipValidation(true) } + referenceTypes.each {it.skipValidation(true)} long subStart = System.currentTimeMillis() dataTypeService.saveAll(enumerationTypes) @@ -350,7 +355,7 @@ class DataModelService extends ModelService implements SummaryMetadat modelItemServices.findAll { !(it.modelItemClass in [DataClass, DataElement, DataType, EnumerationType, ModelDataType, PrimitiveType, ReferenceType, EnumerationValue]) - }.each { modelItemService -> + }.each {modelItemService -> try { modelItemService.deleteAllByModelId(dataModel.id) } catch (ApiNotYetImplementedException ignored) { @@ -478,7 +483,7 @@ class DataModelService extends ModelService implements SummaryMetadat DataModel checkForAndAddDefaultDataTypes(DataModel resource, String defaultDataTypeProvider) { if (defaultDataTypeProvider) { - DefaultDataTypeProvider provider = defaultDataTypeProviders.find { it.name == defaultDataTypeProvider } + DefaultDataTypeProvider provider = defaultDataTypeProviders.find {it.name == defaultDataTypeProvider} if (provider) { log.debug("Adding ${provider.displayName} default DataTypes") return dataTypeService.addDefaultListOfDataTypesToDataModel(resource, provider.defaultListOfDataTypes) @@ -489,14 +494,14 @@ class DataModelService extends ModelService implements SummaryMetadat void deleteAllUnusedDataTypes(DataModel dataModel) { log.debug('Cleaning DataModel {} of DataTypes', dataModel.label) - dataModel.dataTypes.findAll { !it.dataElements }.each { + dataModel.dataTypes.findAll {!it.dataElements}.each { dataTypeService.delete(it) } } void deleteAllUnusedDataClasses(DataModel dataModel) { log.debug('Cleaning DataModel {} of DataClasses', dataModel.label) - dataModel.dataClasses.findAll { dataClassService.isUnusedDataClass(it) }.each { + dataModel.dataClasses.findAll {dataClassService.isUnusedDataClass(it)}.each { dataClassService.delete(it) } } @@ -508,24 +513,24 @@ class DataModelService extends ModelService implements SummaryMetadat if (dataModel.dataClasses) { dataModel.fullSortOfChildren(dataModel.childDataClasses) Collection dataClasses = dataModel.childDataClasses - dataClasses.each { dc -> + dataClasses.each {dc -> dataClassService.checkImportedDataClassAssociations(importingUser, dataModel, dc, !bindingMap.isEmpty()) } } if (bindingMap && dataModel.dataTypes) { - Set referenceTypes = dataModel.dataTypes.findAll { it.instanceOf(ReferenceType) } as Set + Set referenceTypes = dataModel.dataTypes.findAll {it.instanceOf(ReferenceType)} as Set if (referenceTypes) { log.debug('Matching {} ReferenceType referenceClasses', referenceTypes.size()) dataTypeService.matchReferenceClasses(dataModel, referenceTypes, - bindingMap.dataTypes.findAll { it.domainType == DataType.REFERENCE_DOMAIN_TYPE }) + bindingMap.dataTypes.findAll {it.domainType == DataType.REFERENCE_DOMAIN_TYPE}) } } // Make sure we have all the DTs inside the DM first as some will have been imported from the DEs if (dataModel.dataTypes) { dataModel.fullSortOfChildren(dataModel.dataTypes) - dataModel.dataTypes.each { dt -> + dataModel.dataTypes.each {dt -> dataTypeService.checkImportedDataTypeAssociations(importingUser, dataModel, dt) } } @@ -534,7 +539,7 @@ class DataModelService extends ModelService implements SummaryMetadat } DataModel ensureAllEnumerationTypesHaveValues(DataModel dataModel) { - dataModel.dataTypes.findAll { it.instanceOf(EnumerationType) && !(it as EnumerationType).getEnumerationValues() }.each { EnumerationType et -> + dataModel.dataTypes.findAll {it.instanceOf(EnumerationType) && !(it as EnumerationType).getEnumerationValues()}.each {EnumerationType et -> et.addToEnumerationValues(key: '-', value: '-') } dataModel @@ -542,7 +547,7 @@ class DataModelService extends ModelService implements SummaryMetadat List getAllDataElementsOfDataModel(DataModel dataModel) { List allElements = [] - dataModel.dataClasses.each { allElements += it.dataElements ?: [] } + dataModel.dataClasses.each {allElements += it.dataElements ?: []} allElements } @@ -550,16 +555,16 @@ class DataModelService extends ModelService implements SummaryMetadat if (!dataElementNames) return [] getAllDataElementsOfDataModel(dataModel).findAll { caseInsensitive ? - it.label.toLowerCase() in dataElementNames.collect { it.toLowerCase() } : + it.label.toLowerCase() in dataElementNames.collect {it.toLowerCase()} : it.label in dataElementNames } } Set findAllEnumerationTypeByNames(DataModel dataModel, Set enumerationTypeNames, boolean caseInsensitive) { if (!enumerationTypeNames) return [] - dataModel.dataTypes.findAll { it.instanceOf(EnumerationType) }.findAll { + dataModel.dataTypes.findAll {it.instanceOf(EnumerationType)}.findAll { caseInsensitive ? - it.label.toLowerCase() in enumerationTypeNames.collect { it.toLowerCase() } : + it.label.toLowerCase() in enumerationTypeNames.collect {it.toLowerCase()} : it.label in enumerationTypeNames } as Set } @@ -611,14 +616,14 @@ class DataModelService extends ModelService implements SummaryMetadat if (original.dataTypes) { // Copy all the datatypes - original.dataTypes.each { dt -> + original.dataTypes.each {dt -> dataTypeService.copyDataType(copy, dt, copier, userSecurityPolicyManager) } } if (original.childDataClasses) { // Copy all the dataclasses (this will also match up the reference types) - original.childDataClasses.each { dc -> + original.childDataClasses.each {dc -> dataClassService.copyDataClass(copy, dc, copier, userSecurityPolicyManager) } } @@ -645,17 +650,11 @@ class DataModelService extends ModelService implements SummaryMetadat copy.addToSummaryMetadata(label: it.label, summaryMetadataType: it.summaryMetadataType, createdBy: copier.emailAddress) } } - - // modelImportService.findAllByCatalogueItemId(original.id).each { - // copy.addToModelImports(it.importedCatalogueItemDomainType, - // it.importedCatalogueItemId, - // copier) - // } copy } List suggestLinksBetweenModels(DataModel dataModel, DataModel otherDataModel, int maxResults) { - dataModel.getAllDataElements().collect { de -> + dataModel.getAllDataElements().collect {de -> dataElementService.findAllSimilarDataElementsInDataModel(otherDataModel, de, maxResults) } } @@ -670,7 +669,7 @@ class DataModelService extends ModelService implements SummaryMetadat groupProperty('dataModel.id') count() }.order('dataModel') - criteria.list().collectEntries { [it[0], it[1]] } + criteria.list().collectEntries {[it[0], it[1]]} } @Override @@ -720,7 +719,7 @@ class DataModelService extends ModelService implements SummaryMetadat List findAllReadableByClassifier(UserSecurityPolicyManager userSecurityPolicyManager, Classifier classifier) { DataModel.byClassifierId(classifier.id) .list() - .findAll { userSecurityPolicyManager.userCanReadSecuredResourceId(DataModel, it.id) } as List + .findAll {userSecurityPolicyManager.userCanReadSecuredResourceId(DataModel, it.id)} as List } @Override @@ -760,7 +759,7 @@ class DataModelService extends ModelService implements SummaryMetadat @Override List findAllModelIdsWithTreeChildren(List models) { - models.collect { it.id }.findAll { dataClassService.countByDataModelId(it) } + models.collect {it.id}.findAll {dataClassService.countByDataModelId(it)} } @Override @@ -823,4 +822,15 @@ class DataModelService extends ModelService implements SummaryMetadat ModelImporterProviderService getJsonModelImporterProviderService() { dataModelJsonImporterService } + + @Override + CatalogueItem processDeletionPatchOfFacet(MultiFacetItemAware multiFacetItemAware, Model targetModel, Path path) { + CatalogueItem catalogueItem = super.processDeletionPatchOfFacet(multiFacetItemAware, targetModel, path) + + if (multiFacetItemAware.domainType == SummaryMetadata.simpleName) { + (catalogueItem as SummaryMetadataAware).summaryMetadata.remove(multiFacetItemAware) + } + + catalogueItem + } } diff --git a/mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/bootstrap/BootstrapModels.groovy b/mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/bootstrap/BootstrapModels.groovy index c1cbacff08..046823e862 100644 --- a/mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/bootstrap/BootstrapModels.groovy +++ b/mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/bootstrap/BootstrapModels.groovy @@ -657,31 +657,31 @@ v1 --------------------------- v2 -- v3 -- v4 --------------- v5 --- main // Generate main/target branch - UUID rightMainId = createAndSaveNewBranchModel(VersionAwareConstraints.DEFAULT_BRANCH_NAME, commonAncestor, creator, dataModelService, - messageSource, policyManager) + UUID targetModelId = createAndSaveNewBranchModel(VersionAwareConstraints.DEFAULT_BRANCH_NAME, commonAncestor, creator, dataModelService, + messageSource, policyManager) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(rightMainId, 'deleteTargetOnlyFromExistingClass')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(rightMainId, 'deleteTargetOnly')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(rightMainId, 'deleteBoth')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(rightMainId, 'modifySourceAndDeleteTarget')) + dataClassService.delete(dataClassService.findByDataModelIdAndLabel(targetModelId, 'deleteTargetOnlyFromExistingClass')) + dataClassService.delete(dataClassService.findByDataModelIdAndLabel(targetModelId, 'deleteTargetOnly')) + dataClassService.delete(dataClassService.findByDataModelIdAndLabel(targetModelId, 'deleteBoth')) + dataClassService.delete(dataClassService.findByDataModelIdAndLabel(targetModelId, 'modifySourceAndDeleteTarget')) - checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(rightMainId, 'modifyTargetOnly').tap { + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(targetModelId, 'modifyTargetOnly').tap { description = 'Description' } - checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(rightMainId, 'deleteSourceAndModifyTarget').tap { + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(targetModelId, 'deleteSourceAndModifyTarget').tap { description = 'Description' } - checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(rightMainId, 'modifyBothReturningNoDifference').tap { + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(targetModelId, 'modifyBothReturningNoDifference').tap { description = 'Description' } - checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(rightMainId, 'modifyBothReturningDifference').tap { + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(targetModelId, 'modifyBothReturningDifference').tap { description = 'DescriptionTarget' } - checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(rightMainId, 'existingClass') + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(targetModelId, 'existingClass') .addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'addTargetToExistingClass')) - DataModel draftModel = dataModelService.get(rightMainId) + DataModel draftModel = dataModelService.get(targetModelId) draftModel.author = 'dick' checkAndSave messageSource, new DataClass(createdBy: creator.emailAddress, label: 'addTargetWithNestedChild', dataModel: draftModel) .addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'addTargetNestedChild', dataModel: draftModel)) @@ -689,7 +689,7 @@ v1 --------------------------- v2 -- v3 -- v4 --------------- v5 --- main checkAndSave messageSource, new DataClass(createdBy: creator.emailAddress, label: 'addBothReturningNoDifference', dataModel: draftModel) checkAndSave messageSource, new DataClass(createdBy: creator.emailAddress, label: 'addBothReturningDifference', description: 'target', dataModel: draftModel) - checkAndSave messageSource, dataModelService.get(rightMainId).tap { + checkAndSave messageSource, dataModelService.get(targetModelId).tap { description = 'DescriptionTarget' } @@ -698,31 +698,31 @@ v1 --------------------------- v2 -- v3 -- v4 --------------- v5 --- main // Generate test/source branch - UUID leftTestId = createAndSaveNewBranchModel('test', commonAncestor, creator, dataModelService, messageSource, policyManager) + UUID sourceModelId = createAndSaveNewBranchModel('test', commonAncestor, creator, dataModelService, messageSource, policyManager) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteSourceOnlyFromExistingClass')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteSourceOnly')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteBoth')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(leftTestId, 'deleteSourceAndModifyTarget')) - metadataService.delete(metadataService.findAllByMultiFacetAwareItemId(leftTestId).find {it.key == 'deleteSourceOnly'}) + dataClassService.delete(dataClassService.findByDataModelIdAndLabel(sourceModelId, 'deleteSourceOnlyFromExistingClass')) + dataClassService.delete(dataClassService.findByDataModelIdAndLabel(sourceModelId, 'deleteSourceOnly')) + dataClassService.delete(dataClassService.findByDataModelIdAndLabel(sourceModelId, 'deleteBoth')) + dataClassService.delete(dataClassService.findByDataModelIdAndLabel(sourceModelId, 'deleteSourceAndModifyTarget')) + metadataService.delete(metadataService.findAllByMultiFacetAwareItemId(sourceModelId).find {it.key == 'deleteSourceOnly'}) - checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(leftTestId, 'modifySourceOnly').tap { + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(sourceModelId, 'modifySourceOnly').tap { description = 'Description' } - checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(leftTestId, 'modifySourceAndDeleteTarget').tap { + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(sourceModelId, 'modifySourceAndDeleteTarget').tap { description = 'Description' } - checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(leftTestId, 'modifyBothReturningNoDifference').tap { + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(sourceModelId, 'modifyBothReturningNoDifference').tap { description = 'Description' } - checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(leftTestId, 'modifyBothReturningDifference').tap { + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(sourceModelId, 'modifyBothReturningDifference').tap { description = 'DescriptionSource' } - checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(leftTestId, 'existingClass') + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(sourceModelId, 'existingClass') .addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'addSourceToExistingClass')) - DataModel testModel = dataModelService.get(leftTestId) + DataModel testModel = dataModelService.get(sourceModelId) testModel.organisation = 'under test' testModel.author = 'harry' checkAndSave messageSource, new DataClass(createdBy: creator.emailAddress, label: 'addSourceWithNestedChild', dataModel: testModel) @@ -734,7 +734,7 @@ v1 --------------------------- v2 -- v3 -- v4 --------------- v5 --- main checkAndSave messageSource, new PrimitiveType(createdBy: StandardEmailAddress.ADMIN, label: 'addSourceOnlyOnlyChangeInArray', dataModel: testModel) - checkAndSave messageSource, metadataService.findAllByMultiFacetAwareItemId(leftTestId).find {it.key == 'modifySourceOnly'}.tap { + checkAndSave messageSource, metadataService.findAllByMultiFacetAwareItemId(sourceModelId).find {it.key == 'modifySourceOnly'}.tap { value = 'altered' } @@ -742,8 +742,8 @@ v1 --------------------------- v2 -- v3 -- v4 --------------- v5 --- main sessionFactory.currentSession.clear() [commonAncestorId: commonAncestor.id, - sourceId : leftTestId, - targetId : rightMainId] + sourceId : sourceModelId, + targetId : targetModelId] } static UUID createAndSaveNewBranchModel(String branchName, DataModel base, User creator, DataModelService dataModelService, MessageSource messageSource, diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy index 8ad4957eb2..f3443c4531 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy @@ -17,6 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.datamodel +import uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.ArrayMergeDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.CreationMergeDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.DeletionMergeDiff @@ -27,8 +28,9 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.MetadataService import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.FieldPatchData -import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.ItemPatchData import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.ObjectPatchData +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.legacy.ItemPatchData +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.legacy.LegacyFieldPatchData import uk.ac.ox.softeng.maurodatamapper.datamodel.bootstrap.BootstrapModels import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClass import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClassService @@ -39,6 +41,7 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.ReferenceType import uk.ac.ox.softeng.maurodatamapper.datamodel.similarity.DataElementSimilarityResult import uk.ac.ox.softeng.maurodatamapper.datamodel.test.BaseDataModelIntegrationSpec import uk.ac.ox.softeng.maurodatamapper.util.GormUtils +import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.Version import grails.gorm.transactions.Rollback @@ -47,6 +50,8 @@ import groovy.util.logging.Slf4j import org.spockframework.util.Assert import spock.lang.PendingFeature +import java.util.function.Predicate + @Slf4j @Integration @Rollback @@ -1143,7 +1148,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { existingClassDiff.deleted.first().commonAncestor } - void 'DMSM02 : test merging diff into draft model'() { + void 'DMSM02 : test merging legacy diff into draft model'() { given: setupData() @@ -1165,12 +1170,12 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { def patch = new ObjectPatchData( targetId: rightMain.id, sourceId: leftTest.id, - patches: [ - new FieldPatchData( + diffs: [ + new LegacyFieldPatchData( fieldName: 'description', value: 'DescriptionLeft' ), - new FieldPatchData( + new LegacyFieldPatchData( fieldName: 'dataClasses', deleted: [ new ItemPatchData( @@ -1196,8 +1201,8 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { new ObjectPatchData( targetId: dataClassService.findByParentAndLabel(rightMain, 'addBothReturningDifference').id, label: 'addBothReturningDifference', - patches: [ - new FieldPatchData( + diffs: [ + new LegacyFieldPatchData( fieldName: 'description', value: 'addedDescriptionSource' ) @@ -1206,8 +1211,8 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { new ObjectPatchData( targetId: targetExistingClass.id, label: 'existingClass', - patches: [ - new FieldPatchData( + diffs: [ + new LegacyFieldPatchData( fieldName: "dataClasses", deleted: [ new ItemPatchData( @@ -1228,8 +1233,8 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { new ObjectPatchData( targetId: dataClassService.findByParentAndLabel(rightMain, 'modifyBothReturningDifference').id, label: 'modifyBothReturningDifference', - patches: [ - new FieldPatchData( + diffs: [ + new LegacyFieldPatchData( fieldName: 'description', value: 'DescriptionSource' ), @@ -1238,8 +1243,8 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { new ObjectPatchData( targetId: dataClassService.findByParentAndLabel(rightMain, 'modifySourceOnly').id, label: 'modifySourceOnly', - patches: [ - new FieldPatchData( + diffs: [ + new LegacyFieldPatchData( fieldName: 'description', value: 'DescriptionSource' ) @@ -1255,7 +1260,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { check(patch) when: - def mergedModel = dataModelService.mergeObjectPatchDataIntoModel(patch, rightMain, adminSecurityPolicyManager) + def mergedModel = dataModelService.mergeLegacyObjectPatchDataIntoModel(patch, rightMain, adminSecurityPolicyManager) List dataClassLabels = mergedModel.dataClasses*.label then: @@ -1281,6 +1286,338 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { } + void 'DMSM03 : test merging new style single modification diff into draft model'() { + given: + setupData() + + when: 'generate models' + Map mergeData = BootstrapModels.buildMergeModelsForTestingOnly(id, admin, dataModelService, dataClassService, metadataService, sessionFactory, + messageSource) + DataModel targetModel = dataModelService.get(mergeData.targetId) + DataModel sourceModel = dataModelService.get(mergeData.sourceId) + MergeDiff mergeDiff = dataModelService.getMergeDiffForModels(sourceModel, targetModel) + + then: + !mergeDiff.isEmpty() + + when: + def diff = mergeDiff.diffs.find {it.fieldName == 'author'} + + then: + diff + + when: 'using a patch pulled from the actual diff' + def patch = new ObjectPatchData( + targetId: targetModel.id, + sourceId: sourceModel.id, + patches: [FieldPatchData.from(diff)]) + + then: + check(patch) + + when: + DataModel mergedModel = mergeObjectPatchDataIntoModel(patch, targetModel, sourceModel) + + then: + mergedModel.author == 'harry' + } + + void 'DMSM04 : test merging new style single dataclass modification diff into draft model'() { + given: + setupData() + + when: 'generate models' + Map mergeData = BootstrapModels.buildMergeModelsForTestingOnly(id, admin, dataModelService, dataClassService, metadataService, sessionFactory, + messageSource) + DataModel targetModel = dataModelService.get(mergeData.targetId) + DataModel sourceModel = dataModelService.get(mergeData.sourceId) + MergeDiff mergeDiff = dataModelService.getMergeDiffForModels(sourceModel, targetModel) + + then: + !mergeDiff.isEmpty() + + when: + FieldMergeDiff diff = mergeDiff.flattenedDiffs.findAll {it instanceof FieldMergeDiff} + .find {FieldMergeDiff fmd -> + fmd.fieldName == 'description' && Path.from('dm:test database:test|dc:modifyBothReturningDifference').matches(fmd.getFullyQualifiedObjectPath()) + } + + then: + diff + + when: 'using a patch pulled from the actual diff' + def patch = new ObjectPatchData( + targetId: targetModel.id, + sourceId: sourceModel.id, + patches: [FieldPatchData.from(diff)]) + + then: + check(patch) + + when: + DataModel mergedModel = mergeObjectPatchDataIntoModel(patch, targetModel, sourceModel) + + then: + mergedModel.dataClasses.find {it.label == 'modifyBothReturningDifference'}.description == 'DescriptionSource' + } + + void 'DMSM05 : test merging new style modification diffs into draft model'() { + given: + setupData() + + when: 'generate models' + Map mergeData = BootstrapModels.buildMergeModelsForTestingOnly(id, admin, dataModelService, dataClassService, metadataService, sessionFactory, + messageSource) + DataModel targetModel = dataModelService.get(mergeData.targetId) + DataModel sourceModel = dataModelService.get(mergeData.sourceId) + MergeDiff mergeDiff = dataModelService.getMergeDiffForModels(sourceModel, targetModel) + + then: + !mergeDiff.isEmpty() + + when: + List diffs = mergeDiff.flattenedDiffs.findAll {it instanceof FieldMergeDiff} + diffs.removeIf([test: {FieldMergeDiff fieldMergeDiff -> + fieldMergeDiff.fieldName == 'branchName' + }] as Predicate) + + then: + diffs + diffs.size() == 6 + + when: 'using a patch pulled from the actual diff' + def patch = new ObjectPatchData( + targetId: targetModel.id, + sourceId: sourceModel.id, + patches: diffs.collect {FieldPatchData.from(it)} + ) + + then: + check(patch) + + when: + DataModel mergedModel = mergeObjectPatchDataIntoModel(patch, targetModel, sourceModel) + + then: + mergedModel.author == 'harry' + mergedModel.organisation == 'under test' + mergedModel.dataClasses.find {it.label == 'modifyBothReturningDifference'}.description == 'DescriptionSource' + mergedModel.dataClasses.find {it.label == 'addBothReturningDifference'}.description == 'source' + mergedModel.dataClasses.find {it.label == 'modifySourceOnly'}.description == 'Description' + mergedModel.metadata.find {it.namespace == 'test' && it.key == 'modifySourceOnly'}.value == 'altered' + } + + void 'DMSM06 : test merging new style single deletion diff into draft model'() { + given: + setupData() + + when: 'generate models' + Map mergeData = BootstrapModels.buildMergeModelsForTestingOnly(id, admin, dataModelService, dataClassService, metadataService, sessionFactory, + messageSource) + DataModel targetModel = dataModelService.get(mergeData.targetId) + DataModel sourceModel = dataModelService.get(mergeData.sourceId) + MergeDiff mergeDiff = dataModelService.getMergeDiffForModels(sourceModel, targetModel) + + then: + !mergeDiff.isEmpty() + + when: + DeletionMergeDiff diff = mergeDiff.flattenedDiffs.findAll {it instanceof DeletionMergeDiff} + .find {DeletionMergeDiff dmd -> dmd.value.label == 'deleteSourceOnly'} + + then: + diff + + when: 'using a patch pulled from the actual diff' + def patch = new ObjectPatchData( + targetId: targetModel.id, + sourceId: sourceModel.id, + patches: [FieldPatchData.from(diff)]) + + then: + check(patch) + + when: + DataModel mergedModel = mergeObjectPatchDataIntoModel(patch, targetModel, sourceModel) + + then: + !mergedModel.dataClasses.find {it.label == 'deleteSourceOnly'} + } + + void 'DMSM07 : test merging new style all deletion diff into draft model'() { + given: + setupData() + + when: 'generate models' + Map mergeData = BootstrapModels.buildMergeModelsForTestingOnly(id, admin, dataModelService, dataClassService, metadataService, sessionFactory, + messageSource) + DataModel targetModel = dataModelService.get(mergeData.targetId) + DataModel sourceModel = dataModelService.get(mergeData.sourceId) + MergeDiff mergeDiff = dataModelService.getMergeDiffForModels(sourceModel, targetModel) + + then: + !mergeDiff.isEmpty() + + when: + List diffs = mergeDiff.flattenedDiffs.findAll {it instanceof DeletionMergeDiff} + + then: + diffs + diffs.size() == 4 + + when: 'using a patch pulled from the actual diff' + def patch = new ObjectPatchData( + targetId: targetModel.id, + sourceId: sourceModel.id, + patches: diffs.collect {FieldPatchData.from(it)} + ) + + then: + check(patch) + + when: + DataModel mergedModel = mergeObjectPatchDataIntoModel(patch, targetModel, sourceModel) + + then: + !mergedModel.dataClasses.find {it.label == 'deleteSourceOnly'} + !mergedModel.dataClasses.find {it.label == 'deleteSourceAndModifyTarget'} + !mergedModel.dataClasses.find {it.label == 'deleteSourceOnlyFromExistingClass'} + !mergedModel.metadata.find {it.namespace == 'test' && it.key == 'deleteSourceOnly'} + } + + DataModel mergeObjectPatchDataIntoModel(ObjectPatchData patch, DataModel targetModel, DataModel sourceModel) { + DataModel mergedModel = dataModelService.mergeObjectPatchDataIntoModel(patch, targetModel, sourceModel, false, adminSecurityPolicyManager) + sessionFactory.currentSession.flush() + dataModelService.get(mergedModel.id) + } + + void 'DMSM08 : test merging new style single creation diff into draft model'() { + given: + setupData() + + when: 'generate models' + Map mergeData = BootstrapModels.buildMergeModelsForTestingOnly(id, admin, dataModelService, dataClassService, metadataService, sessionFactory, + messageSource) + DataModel targetModel = dataModelService.get(mergeData.targetId) + DataModel sourceModel = dataModelService.get(mergeData.sourceId) + MergeDiff mergeDiff = dataModelService.getMergeDiffForModels(sourceModel, targetModel) + + then: + !mergeDiff.isEmpty() + + when: + CreationMergeDiff diff = mergeDiff.flattenedDiffs.findAll {it instanceof CreationMergeDiff} + .find {CreationMergeDiff cmd -> cmd.value.label == 'addSourceOnly'} + + then: + diff + + when: 'using a patch pulled from the actual diff' + def patch = new ObjectPatchData( + targetId: targetModel.id, + sourceId: sourceModel.id, + patches: [FieldPatchData.from(diff)]) + + then: + check(patch) + + when: + DataModel mergedModel = mergeObjectPatchDataIntoModel(patch, targetModel, sourceModel) + + then: + mergedModel.dataClasses.find {it.label == 'addSourceOnly'} + } + + + void 'DMSM09 : test merging new style creation diffs into draft model'() { + given: + setupData() + + when: 'generate models' + Map mergeData = BootstrapModels.buildMergeModelsForTestingOnly(id, admin, dataModelService, dataClassService, metadataService, sessionFactory, + messageSource) + DataModel targetModel = dataModelService.get(mergeData.targetId) + DataModel sourceModel = dataModelService.get(mergeData.sourceId) + MergeDiff mergeDiff = dataModelService.getMergeDiffForModels(sourceModel, targetModel) + + then: + !mergeDiff.isEmpty() + + when: + List diffs = mergeDiff.flattenedDiffs.findAll {it instanceof CreationMergeDiff} + + then: + diffs + diffs.size() == 5 + + when: 'using a patch pulled from the actual diff' + def patch = new ObjectPatchData( + targetId: targetModel.id, + sourceId: sourceModel.id, + patches: diffs.collect {FieldPatchData.from(it)} + ) + + then: + check(patch) + + when: + DataModel mergedModel = mergeObjectPatchDataIntoModel(patch, targetModel, sourceModel) + + then: + mergedModel.dataClasses.find {it.label == 'addSourceOnly'} + mergedModel.dataClasses.find {it.label == 'addSourceWithNestedChild'} + mergedModel.dataClasses.find {it.label == 'modifySourceAndDeleteTarget'} + mergedModel.dataClasses.find {it.label == 'addSourceToExistingClass'} + mergedModel.dataClasses.find {it.label == 'existingClass'}.dataClasses.find {it.label == 'addSourceToExistingClass'} + mergedModel.dataTypes.find {it.label == 'addSourceOnlyOnlyChangeInArray'} + } + + void 'DMSM10 : test merging new style facet creation diff into draft model'() { + given: + setupData() + + when: 'generate models' + Map mergeData = BootstrapModels.buildMergeModelsForTestingOnly(id, admin, dataModelService, dataClassService, metadataService, sessionFactory, + messageSource) + DataModel targetModel = dataModelService.get(mergeData.targetId) + DataModel sourceModel = dataModelService.get(mergeData.sourceId) + sourceModel.addToMetadata('test', 'addSourceOnly', 'addSourceOnly', StandardEmailAddress.INTEGRATION_TEST) + sourceModel.dataClasses.find {it.label == 'existingClass'}.addToMetadata('test', 'addDCSourceOnly', 'addDCSourceOnly', + StandardEmailAddress.INTEGRATION_TEST) + checkAndSave(sourceModel) + + MergeDiff mergeDiff = dataModelService.getMergeDiffForModels(sourceModel, targetModel) + + then: + !mergeDiff.isEmpty() + + when: + List diffs = mergeDiff.flattenedDiffs + .findAll {it instanceof CreationMergeDiff} + .findAll {CreationMergeDiff cmd -> cmd.created instanceof Metadata} + + then: + diffs + diffs.size() == 2 + + when: 'using a patch pulled from the actual diff' + def patch = new ObjectPatchData( + targetId: targetModel.id, + sourceId: sourceModel.id, + patches: diffs.collect {FieldPatchData.from(it)} + ) + + then: + check(patch) + + when: + DataModel mergedModel = mergeObjectPatchDataIntoModel(patch, targetModel, sourceModel) + + then: + mergedModel.metadata.find {it.key == 'addSourceOnly'} + sourceModel.dataClasses.find {it.label == 'existingClass'}.metadata.find {it.key == 'addDCSourceOnly'} + } + void 'DMSV01 : test validation on valid model'() { given: setupData() diff --git a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelService.groovy b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelService.groovy index aef89d4baa..b75e6dc531 100644 --- a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelService.groovy +++ b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelService.groovy @@ -25,12 +25,16 @@ import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier import uk.ac.ox.softeng.maurodatamapper.core.container.Folder import uk.ac.ox.softeng.maurodatamapper.core.facet.EditTitle import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkType +import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.model.Container +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.dataloader.DataLoaderProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.ModelImporterProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.ModelImporterProviderServiceParameters +import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.MultiFacetItemAware import uk.ac.ox.softeng.maurodatamapper.referencedata.facet.ReferenceSummaryMetadata +import uk.ac.ox.softeng.maurodatamapper.referencedata.facet.ReferenceSummaryMetadataAware import uk.ac.ox.softeng.maurodatamapper.referencedata.facet.ReferenceSummaryMetadataService import uk.ac.ox.softeng.maurodatamapper.referencedata.item.ReferenceDataElement import uk.ac.ox.softeng.maurodatamapper.referencedata.item.ReferenceDataElementService @@ -45,6 +49,7 @@ import uk.ac.ox.softeng.maurodatamapper.referencedata.similarity.ReferenceDataEl import uk.ac.ox.softeng.maurodatamapper.referencedata.traits.service.ReferenceSummaryMetadataAwareService import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager +import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.Utils import uk.ac.ox.softeng.maurodatamapper.util.Version @@ -692,4 +697,14 @@ class ReferenceDataModelService extends ModelService impleme ReferenceDataModel.byMetadataNamespace(namespace).list(pagination) } + @Override + CatalogueItem processDeletionPatchOfFacet(MultiFacetItemAware multiFacetItemAware, Model targetModel, Path path) { + CatalogueItem catalogueItem = processDeletionPatchOfFacet(multiFacetItemAware, targetModel, path) + + if (multiFacetItemAware.domainType == ReferenceSummaryMetadata.simpleName) { + (catalogueItem as ReferenceSummaryMetadataAware).referenceSummaryMetadata.remove(multiFacetItemAware) + } + + catalogueItem + } } \ No newline at end of file diff --git a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy index e71b866e5a..e400c1b787 100644 --- a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy +++ b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy @@ -28,7 +28,6 @@ import uk.ac.ox.softeng.maurodatamapper.core.model.Container import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItemService import uk.ac.ox.softeng.maurodatamapper.core.model.ModelService -import uk.ac.ox.softeng.maurodatamapper.core.path.PathService import uk.ac.ox.softeng.maurodatamapper.core.provider.dataloader.DataLoaderProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.ModelImporterProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.ModelImporterProviderServiceParameters @@ -55,7 +54,6 @@ class CodeSetService extends ModelService { TermService termService TermRelationshipService termRelationshipService CodeSetJsonImporterService codeSetJsonImporterService - PathService pathService @Override CodeSet get(Serializable id) { @@ -203,18 +201,18 @@ class CodeSetService extends ModelService { } @Override - CodeSet mergeObjectPatchDataIntoModel(ObjectPatchData objectPatchData, CodeSet targetModel, - UserSecurityPolicyManager userSecurityPolicyManager) { + CodeSet mergeLegacyObjectPatchDataIntoModel(ObjectPatchData objectPatchData, CodeSet targetModel, + UserSecurityPolicyManager userSecurityPolicyManager) { if (!objectPatchData.hasPatches()) return targetModel - objectPatchData.getPatches().each {mergeFieldDiff -> + objectPatchData.getDiffsWithContent().each {mergeFieldDiff -> if (mergeFieldDiff.isFieldChange()) { targetModel.setProperty(mergeFieldDiff.fieldName, mergeFieldDiff.value) } else if (mergeFieldDiff.isMetadataChange()) { - mergeMetadataIntoCatalogueItem(mergeFieldDiff, targetModel, userSecurityPolicyManager) + mergeLegacyMetadataIntoCatalogueItem(mergeFieldDiff, targetModel, userSecurityPolicyManager) } else { ModelItemService modelItemService = modelItemServices.find {it.handles(mergeFieldDiff.fieldName)} @@ -253,9 +251,10 @@ class CodeSetService extends ModelService { modelItemService.copy(targetModel, modelItem, userSecurityPolicyManager) } // for modifications, recursively call this method - mergeFieldDiff.modified.each { mergeObjectDiffData -> + mergeFieldDiff.modified.each {mergeObjectDiffData -> ModelItem modelItem = modelItemService.get(mergeObjectDiffData.leftId) as ModelItem - modelItemService.mergeObjectPatchDataIntoModelItem(mergeObjectDiffData, modelItem, targetModel, userSecurityPolicyManager) + modelItemService. + mergeLegacyObjectPatchDataIntoModelItem(mergeObjectDiffData, modelItem, targetModel, userSecurityPolicyManager) } } } else { From ec631761342b8a78659cf720e6835feab727a53f Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Thu, 15 Jul 2021 17:45:33 +0100 Subject: [PATCH 46/82] Implement a JSON converter for Path and move the path files into their own package --- mdm-common/dependencies.gradle | 1 + .../maurodatamapper/{util => path}/Path.groovy | 2 +- .../path/PathJsonConverter.groovy | 18 ++++++++++++++++++ .../{util => path}/PathNode.groovy | 2 +- ...plugin.json.builder.JsonGenerator$Converter | 1 + .../core/path/PathInterceptor.groovy | 2 +- .../core/path/PathService.groovy | 4 ++-- .../rest/transport/merge/FieldPatchData.groovy | 2 +- .../grails-app/views/mergeDiff/_mergeDiff.gson | 2 +- .../diff/tridirectional/ArrayMergeDiff.groovy | 2 +- .../tridirectional/CreationMergeDiff.groovy | 2 +- .../tridirectional/DeletionMergeDiff.groovy | 2 +- .../diff/tridirectional/FieldMergeDiff.groovy | 2 +- .../core/diff/tridirectional/MergeDiff.groovy | 3 +-- .../tridirectional/TriDirectionalDiff.groovy | 2 +- .../core/model/ModelService.groovy | 2 +- .../dataflow/DataFlowService.groovy | 2 +- .../component/DataClassComponentService.groovy | 2 +- .../DataElementComponentService.groovy | 2 +- .../datamodel/DataModelService.groovy | 2 +- .../DataModelServiceIntegrationSpec.groovy | 2 +- .../path/DataModelPathServiceSpec.groovy | 2 +- .../ReferenceDataModelService.groovy | 2 +- .../ReferenceDataModelFunctionalSpec.groovy | 1 - .../ReferenceDataModelServiceSpec.groovy | 1 - .../terminology/CodeSetService.groovy | 2 +- .../path/TerminologyPathServiceSpec.groovy | 2 +- ...nChangingAndVersioningFunctionalSpec.groovy | 1 - 28 files changed, 43 insertions(+), 27 deletions(-) rename mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/{util => path}/Path.groovy (98%) create mode 100644 mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathJsonConverter.groovy rename mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/{util => path}/PathNode.groovy (98%) create mode 100644 mdm-common/src/main/resources/META-INF/services/grails.plugin.json.builder.JsonGenerator$Converter diff --git a/mdm-common/dependencies.gradle b/mdm-common/dependencies.gradle index a2a4594e14..1ac5a0d4e5 100644 --- a/mdm-common/dependencies.gradle +++ b/mdm-common/dependencies.gradle @@ -12,6 +12,7 @@ dependencies { implementation group: 'org.grails', name: 'grails-core', version: grailsVersion implementation group: 'org.grails', name: 'grails-datastore-gorm' implementation group: 'org.grails', name: 'grails-plugin-validation' + implementation "org.grails.plugins:views-json:$grailsViewsVersion" api group: 'com.bertramlabs.plugins', name: 'asset-pipeline-core', version: assetPipelineVersion diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Path.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy similarity index 98% rename from mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Path.groovy rename to mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy index 6b01440d13..d47f349f00 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Path.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy @@ -15,7 +15,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package uk.ac.ox.softeng.maurodatamapper.util +package uk.ac.ox.softeng.maurodatamapper.path import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathJsonConverter.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathJsonConverter.groovy new file mode 100644 index 0000000000..4d5e79d994 --- /dev/null +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathJsonConverter.groovy @@ -0,0 +1,18 @@ +package uk.ac.ox.softeng.maurodatamapper.path + +import grails.plugin.json.builder.JsonGenerator + +/** + * @since 15/07/2021 + */ +class PathJsonConverter implements JsonGenerator.Converter { + @Override + boolean handles(Class type) { + Path.isAssignableFrom(type) + } + + @Override + Object convert(Object value, String key) { + ((Path) value).toString() + } +} diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/PathNode.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy similarity index 98% rename from mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/PathNode.groovy rename to mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy index 10ac237931..1bd8d7630a 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/PathNode.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy @@ -15,7 +15,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package uk.ac.ox.softeng.maurodatamapper.util +package uk.ac.ox.softeng.maurodatamapper.path import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware diff --git a/mdm-common/src/main/resources/META-INF/services/grails.plugin.json.builder.JsonGenerator$Converter b/mdm-common/src/main/resources/META-INF/services/grails.plugin.json.builder.JsonGenerator$Converter new file mode 100644 index 0000000000..9c8f00ac21 --- /dev/null +++ b/mdm-common/src/main/resources/META-INF/services/grails.plugin.json.builder.JsonGenerator$Converter @@ -0,0 +1 @@ +uk.ac.ox.softeng.maurodatamapper.path.PathJsonConverter \ No newline at end of file diff --git a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy index e6cf55f4a9..70367356d0 100644 --- a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy +++ b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/path/PathInterceptor.groovy @@ -18,7 +18,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.path import uk.ac.ox.softeng.maurodatamapper.core.traits.controller.MdmInterceptor -import uk.ac.ox.softeng.maurodatamapper.util.Path +import uk.ac.ox.softeng.maurodatamapper.path.Path class PathInterceptor implements MdmInterceptor { diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy index 5f67f57853..8335561963 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy @@ -20,12 +20,12 @@ package uk.ac.ox.softeng.maurodatamapper.core.path import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItemService import uk.ac.ox.softeng.maurodatamapper.core.traits.service.DomainService +import uk.ac.ox.softeng.maurodatamapper.path.Path +import uk.ac.ox.softeng.maurodatamapper.path.PathNode import uk.ac.ox.softeng.maurodatamapper.security.SecurableResource import uk.ac.ox.softeng.maurodatamapper.security.SecurableResourceService import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware -import uk.ac.ox.softeng.maurodatamapper.util.Path -import uk.ac.ox.softeng.maurodatamapper.util.PathNode import grails.core.GrailsApplication import grails.gorm.transactions.Transactional diff --git a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy index 479b62cf1f..033be72bf1 100644 --- a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy +++ b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy @@ -21,7 +21,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.CreationMergeDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.DeletionMergeDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.FieldMergeDiff -import uk.ac.ox.softeng.maurodatamapper.util.Path +import uk.ac.ox.softeng.maurodatamapper.path.Path import grails.validation.Validateable diff --git a/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson b/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson index c32ac28c1e..8bbbceac82 100644 --- a/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson +++ b/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson @@ -11,7 +11,7 @@ json { sourceId mergeDiff.getSourceId() targetId mergeDiff.getTargetId() - path mergeDiff.fullyQualifiedPath.toString() + path mergeDiff.fullyQualifiedPath if (mergeDiff.getTarget() instanceof InformationAware) { label(((InformationAware) mergeDiff.getTarget()).getLabel()) diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy index 00c53dc8fe..1d87a08d59 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy @@ -18,7 +18,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable -import uk.ac.ox.softeng.maurodatamapper.util.Path +import uk.ac.ox.softeng.maurodatamapper.path.Path import groovy.transform.CompileStatic diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy index b6d7b01a3e..899cad7c42 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy @@ -18,7 +18,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable -import uk.ac.ox.softeng.maurodatamapper.util.Path +import uk.ac.ox.softeng.maurodatamapper.path.Path import groovy.transform.CompileStatic diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy index cbf1d11517..b70860bf02 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy @@ -19,7 +19,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff -import uk.ac.ox.softeng.maurodatamapper.util.Path +import uk.ac.ox.softeng.maurodatamapper.path.Path import groovy.transform.CompileStatic diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy index 9d081af203..71326ce0fd 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy @@ -17,7 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional -import uk.ac.ox.softeng.maurodatamapper.util.Path +import uk.ac.ox.softeng.maurodatamapper.path.Path import groovy.transform.CompileStatic diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy index 899a7bf1af..b7e6e65e6a 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy @@ -17,15 +17,14 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional - import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ArrayDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.FieldDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.CreationDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional.DeletionDiff +import uk.ac.ox.softeng.maurodatamapper.path.Path import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware -import uk.ac.ox.softeng.maurodatamapper.util.Path import groovy.transform.CompileStatic import groovy.transform.stc.ClosureParams diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy index 848ff6c016..988580c567 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy @@ -19,7 +19,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.BiDirectionalDiff -import uk.ac.ox.softeng.maurodatamapper.util.Path +import uk.ac.ox.softeng.maurodatamapper.path.Path import groovy.transform.CompileStatic diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy index 59655f47be..e30c2cc8fb 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy @@ -52,12 +52,12 @@ import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.MultiFacetItemAware import uk.ac.ox.softeng.maurodatamapper.core.traits.service.DomainService import uk.ac.ox.softeng.maurodatamapper.core.traits.service.MultiFacetItemAwareService import uk.ac.ox.softeng.maurodatamapper.core.traits.service.VersionLinkAwareService +import uk.ac.ox.softeng.maurodatamapper.path.Path 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.traits.domain.CreatorAware -import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.Utils import uk.ac.ox.softeng.maurodatamapper.util.Version import uk.ac.ox.softeng.maurodatamapper.util.VersionChangeType diff --git a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowService.groovy b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowService.groovy index fd370ce9db..86e730ba3a 100644 --- a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowService.groovy +++ b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/DataFlowService.groovy @@ -26,9 +26,9 @@ import uk.ac.ox.softeng.maurodatamapper.core.path.PathService import uk.ac.ox.softeng.maurodatamapper.dataflow.component.DataClassComponent import uk.ac.ox.softeng.maurodatamapper.dataflow.component.DataClassComponentService import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel +import uk.ac.ox.softeng.maurodatamapper.path.Path import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.Utils import grails.gorm.transactions.Transactional diff --git a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponentService.groovy b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponentService.groovy index ac1e667837..c9895247cc 100644 --- a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponentService.groovy +++ b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataClassComponentService.groovy @@ -25,9 +25,9 @@ import uk.ac.ox.softeng.maurodatamapper.core.path.PathService import uk.ac.ox.softeng.maurodatamapper.dataflow.DataFlow import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClass +import uk.ac.ox.softeng.maurodatamapper.path.Path import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.Utils import grails.gorm.transactions.Transactional diff --git a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponentService.groovy b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponentService.groovy index 16c7bad105..82b6fc10c1 100644 --- a/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponentService.groovy +++ b/mdm-plugin-dataflow/grails-app/services/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponentService.groovy @@ -25,9 +25,9 @@ import uk.ac.ox.softeng.maurodatamapper.core.path.PathService import uk.ac.ox.softeng.maurodatamapper.dataflow.DataFlow import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataElement +import uk.ac.ox.softeng.maurodatamapper.path.Path import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.Utils import grails.gorm.transactions.Transactional diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelService.groovy index ce84623493..807d78384c 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelService.groovy @@ -51,10 +51,10 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.provider.DefaultDataTypeProvid import uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer.DataModelJsonImporterService import uk.ac.ox.softeng.maurodatamapper.datamodel.similarity.DataElementSimilarityResult import uk.ac.ox.softeng.maurodatamapper.datamodel.traits.service.SummaryMetadataAwareService +import uk.ac.ox.softeng.maurodatamapper.path.Path import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.util.GormUtils -import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.Utils import uk.ac.ox.softeng.maurodatamapper.util.Version diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy index f3443c4531..0d7fe99a8d 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy @@ -40,8 +40,8 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.PrimitiveType import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.ReferenceType import uk.ac.ox.softeng.maurodatamapper.datamodel.similarity.DataElementSimilarityResult import uk.ac.ox.softeng.maurodatamapper.datamodel.test.BaseDataModelIntegrationSpec +import uk.ac.ox.softeng.maurodatamapper.path.Path import uk.ac.ox.softeng.maurodatamapper.util.GormUtils -import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.Version import grails.gorm.transactions.Rollback diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/path/DataModelPathServiceSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/path/DataModelPathServiceSpec.groovy index 1c8f61c975..2ccbac3908 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/path/DataModelPathServiceSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/path/DataModelPathServiceSpec.groovy @@ -26,7 +26,7 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataElement import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.DataType import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.PrimitiveType import uk.ac.ox.softeng.maurodatamapper.datamodel.test.BaseDataModelIntegrationSpec -import uk.ac.ox.softeng.maurodatamapper.util.Path +import uk.ac.ox.softeng.maurodatamapper.path.Path import grails.gorm.transactions.Rollback import grails.testing.mixin.integration.Integration diff --git a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelService.groovy b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelService.groovy index b75e6dc531..c18538aa41 100644 --- a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelService.groovy +++ b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelService.groovy @@ -33,6 +33,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.provider.dataloader.DataLoaderProvi import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.ModelImporterProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.ModelImporterProviderServiceParameters import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.MultiFacetItemAware +import uk.ac.ox.softeng.maurodatamapper.path.Path import uk.ac.ox.softeng.maurodatamapper.referencedata.facet.ReferenceSummaryMetadata import uk.ac.ox.softeng.maurodatamapper.referencedata.facet.ReferenceSummaryMetadataAware import uk.ac.ox.softeng.maurodatamapper.referencedata.facet.ReferenceSummaryMetadataService @@ -49,7 +50,6 @@ import uk.ac.ox.softeng.maurodatamapper.referencedata.similarity.ReferenceDataEl import uk.ac.ox.softeng.maurodatamapper.referencedata.traits.service.ReferenceSummaryMetadataAwareService import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.Utils import uk.ac.ox.softeng.maurodatamapper.util.Version 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 de84c7dfd9..f52467e108 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 @@ -21,7 +21,6 @@ import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier import uk.ac.ox.softeng.maurodatamapper.core.container.Folder import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkType import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType -import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModel import uk.ac.ox.softeng.maurodatamapper.referencedata.item.ReferenceDataElement import uk.ac.ox.softeng.maurodatamapper.referencedata.item.ReferenceDataValue import uk.ac.ox.softeng.maurodatamapper.referencedata.item.datatype.ReferenceDataType diff --git a/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelServiceSpec.groovy b/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelServiceSpec.groovy index 00e77ded8c..1f39dd6786 100644 --- a/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelServiceSpec.groovy +++ b/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelServiceSpec.groovy @@ -21,7 +21,6 @@ import uk.ac.ox.softeng.maurodatamapper.core.authority.Authority import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTree import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTreeService import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType -import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModelService import uk.ac.ox.softeng.maurodatamapper.referencedata.bootstrap.BootstrapModels import uk.ac.ox.softeng.maurodatamapper.referencedata.facet.ReferenceSummaryMetadataService import uk.ac.ox.softeng.maurodatamapper.referencedata.item.ReferenceDataElement diff --git a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy index e400c1b787..cec531f0c0 100644 --- a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy +++ b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy @@ -32,6 +32,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.provider.dataloader.DataLoaderProvi import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.ModelImporterProviderService import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.ModelImporterProviderServiceParameters import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.ObjectPatchData +import uk.ac.ox.softeng.maurodatamapper.path.Path import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.terminology.item.Term @@ -39,7 +40,6 @@ import uk.ac.ox.softeng.maurodatamapper.terminology.item.TermRelationshipTypeSer import uk.ac.ox.softeng.maurodatamapper.terminology.item.TermService import uk.ac.ox.softeng.maurodatamapper.terminology.item.term.TermRelationshipService import uk.ac.ox.softeng.maurodatamapper.terminology.provider.importer.CodeSetJsonImporterService -import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.Utils import uk.ac.ox.softeng.maurodatamapper.util.Version diff --git a/mdm-plugin-terminology/src/integration-test/groovy/path/TerminologyPathServiceSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/path/TerminologyPathServiceSpec.groovy index f7ae4bf945..2e95c476fb 100644 --- a/mdm-plugin-terminology/src/integration-test/groovy/path/TerminologyPathServiceSpec.groovy +++ b/mdm-plugin-terminology/src/integration-test/groovy/path/TerminologyPathServiceSpec.groovy @@ -19,11 +19,11 @@ package path import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.path.PathService +import uk.ac.ox.softeng.maurodatamapper.path.Path import uk.ac.ox.softeng.maurodatamapper.terminology.CodeSet import uk.ac.ox.softeng.maurodatamapper.terminology.Terminology import uk.ac.ox.softeng.maurodatamapper.terminology.item.Term import uk.ac.ox.softeng.maurodatamapper.terminology.test.BaseTerminologyIntegrationSpec -import uk.ac.ox.softeng.maurodatamapper.util.Path import uk.ac.ox.softeng.maurodatamapper.util.Version import grails.gorm.transactions.Rollback diff --git a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelUserAccessPermissionChangingAndVersioningFunctionalSpec.groovy b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelUserAccessPermissionChangingAndVersioningFunctionalSpec.groovy index fb81531f79..87f5cc0fd0 100644 --- a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelUserAccessPermissionChangingAndVersioningFunctionalSpec.groovy +++ b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelUserAccessPermissionChangingAndVersioningFunctionalSpec.groovy @@ -22,7 +22,6 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints import uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions import uk.ac.ox.softeng.maurodatamapper.security.role.GroupRole -import uk.ac.ox.softeng.maurodatamapper.testing.functional.UserAccessAndPermissionChangingFunctionalSpec import uk.ac.ox.softeng.maurodatamapper.util.VersionChangeType import grails.gorm.transactions.Transactional From b2dca75632cebc9716762b39c6aa7a0c0b888045 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Thu, 15 Jul 2021 17:51:13 +0100 Subject: [PATCH 47/82] Implement a JSON converter for Version and move the path files into their own package --- .../hibernate/VersionUserType.groovy | 2 +- .../VersionUserTypeDescriptor.groovy | 2 +- .../provider/MauroDataMapperService.groovy | 2 +- .../{util => version}/Version.groovy | 2 +- .../VersionChangeType.groovy | 2 +- .../version/VersionJsonConverter.groovy | 19 +++++++++++++++++++ ...lugin.json.builder.JsonGenerator$Converter | 3 ++- .../{util => version}/VersionSpec.groovy | 3 ++- .../container/VersionedFolderService.groovy | 4 ++-- .../callable/VersionAwareConstraints.groovy | 2 +- .../rest/transport/model/FinaliseData.groovy | 4 ++-- .../transport/tree/ContainerTreeItem.groovy | 2 +- .../rest/transport/tree/ModelTreeItem.groovy | 2 +- .../grails-app/views/treeItem/_treeItem.gson | 8 ++++---- .../_simpleVersionTreeModel.gson | 4 ++-- .../versionTreeModel/_versionTreeModel.gson | 4 ++-- .../versionedFolder/_versionedFolder.gson | 4 ++-- .../_versionedFolder_full.gson | 4 ++-- .../versionedFolder/latestModelVersion.gson | 4 ++-- .../DocumentationVersionValidator.groovy | 2 +- .../validator/ModelVersionValidator.groovy | 2 +- .../core/model/ModelService.groovy | 4 ++-- .../DataLoaderProviderService.groovy | 2 +- .../core/traits/domain/DataLoadable.groovy | 2 +- .../core/traits/domain/VersionAware.groovy | 2 +- .../core/container/VersionedFolderSpec.groovy | 2 +- .../core/model/BasicModelSpec.groovy | 2 +- .../model/VersionTreeModelSpec.groovy | 2 +- .../core/tree/TreeItemServiceSpec.groovy | 2 +- .../datamodel/DataModelService.groovy | 2 +- .../bootstrap/BootstrapModels.groovy | 2 +- .../views/dataModel/_dataModel.gson | 4 ++-- .../views/dataModel/_dataModel_full.gson | 4 ++-- .../grails-app/views/dataModel/_export.gson | 4 ++-- .../views/dataModel/latestModelVersion.gson | 4 ++-- .../datamodel/DataModelFunctionalSpec.groovy | 2 +- .../DataModelServiceIntegrationSpec.groovy | 2 +- .../item/DataClassFunctionalSpec.groovy | 2 +- .../item/DataElementFunctionalSpec.groovy | 2 +- .../datatype/DataTypeFunctionalSpec.groovy | 2 +- .../DataModelJsonImporterServiceSpec.groovy | 2 +- .../datamodel/tree/TreeItemServiceSpec.groovy | 2 +- .../datamodel/DataModelServiceSpec.groovy | 2 +- .../federation/PublishedModel.groovy | 2 +- .../referencedata/ReferenceDataModel.groovy | 2 +- .../ReferenceDataModelService.groovy | 2 +- .../views/referenceDataModel/_export.gson | 4 ++-- .../_referenceDataModel.gson | 4 ++-- .../_referenceDataModel_full.gson | 4 ++-- .../latestModelVersion.gson | 4 ++-- .../ReferenceDataModelFunctionalSpec.groovy | 2 +- ...enceDataModelServiceIntegrationSpec.groovy | 2 +- .../tree/TreeItemServiceSpec.groovy | 2 +- .../ReferenceDataModelServiceSpec.groovy | 2 +- .../terminology/CodeSetService.groovy | 2 +- .../terminology/TerminologyService.groovy | 2 +- .../bootstrap/BootstrapModels.groovy | 2 +- .../grails-app/views/codeSet/_codeSet.gson | 4 ++-- .../views/codeSet/_codeSet_full.gson | 4 ++-- .../grails-app/views/codeSet/_export.gson | 4 ++-- .../views/codeSet/latestModelVersion.gson | 4 ++-- .../grails-app/views/terminology/_export.gson | 4 ++-- .../views/terminology/_terminology.gson | 4 ++-- .../views/terminology/_terminology_full.gson | 4 ++-- .../views/terminology/latestModelVersion.gson | 4 ++-- .../path/TerminologyPathServiceSpec.groovy | 2 +- .../terminology/CodeSetFunctionalSpec.groovy | 2 +- .../TerminologyFunctionalSpec.groovy | 2 +- .../TerminologyServiceIntegrationSpec.groovy | 2 +- .../terminology/TerminologyServiceSpec.groovy | 2 +- .../test/json/matcher/VersionMatcher.groovy | 2 +- .../test/unit/core/ModelSpec.groovy | 2 +- .../VersionedFolderFunctionalSpec.groovy | 2 +- ...ChangingAndVersioningFunctionalSpec.groovy | 2 +- 74 files changed, 121 insertions(+), 100 deletions(-) rename mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/{util => version}/Version.groovy (98%) rename mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/{util => version}/VersionChangeType.groovy (96%) create mode 100644 mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/version/VersionJsonConverter.groovy rename mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/{util => version}/VersionSpec.groovy (99%) diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/hibernate/VersionUserType.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/hibernate/VersionUserType.groovy index 25005dbaeb..56b67e7e9b 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/hibernate/VersionUserType.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/hibernate/VersionUserType.groovy @@ -17,7 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.hibernate -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import org.hibernate.dialect.Dialect import org.hibernate.type.AbstractSingleColumnStandardBasicType diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/hibernate/VersionUserTypeDescriptor.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/hibernate/VersionUserTypeDescriptor.groovy index 2bdb0cdd49..3c4fc55b94 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/hibernate/VersionUserTypeDescriptor.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/hibernate/VersionUserTypeDescriptor.groovy @@ -17,7 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.hibernate -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import org.hibernate.type.descriptor.WrapperOptions import org.hibernate.type.descriptor.java.AbstractTypeDescriptor diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/provider/MauroDataMapperService.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/provider/MauroDataMapperService.groovy index bb0eaa7405..d4987b8e87 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/provider/MauroDataMapperService.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/provider/MauroDataMapperService.groovy @@ -18,7 +18,7 @@ package uk.ac.ox.softeng.maurodatamapper.provider -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import groovy.transform.CompileStatic import groovy.util.logging.Slf4j diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Version.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/version/Version.groovy similarity index 98% rename from mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Version.groovy rename to mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/version/Version.groovy index 7ebff41fd0..a8413031cd 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Version.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/version/Version.groovy @@ -15,7 +15,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package uk.ac.ox.softeng.maurodatamapper.util +package uk.ac.ox.softeng.maurodatamapper.version import asset.pipeline.AssetFile diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/VersionChangeType.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/version/VersionChangeType.groovy similarity index 96% rename from mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/VersionChangeType.groovy rename to mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/version/VersionChangeType.groovy index fd8e59674e..aa012ee558 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/VersionChangeType.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/version/VersionChangeType.groovy @@ -15,7 +15,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package uk.ac.ox.softeng.maurodatamapper.util +package uk.ac.ox.softeng.maurodatamapper.version import grails.databinding.DataBindingSource; diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/version/VersionJsonConverter.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/version/VersionJsonConverter.groovy new file mode 100644 index 0000000000..39aed91468 --- /dev/null +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/version/VersionJsonConverter.groovy @@ -0,0 +1,19 @@ +package uk.ac.ox.softeng.maurodatamapper.version + + +import grails.plugin.json.builder.JsonGenerator + +/** + * @since 15/07/2021 + */ +class VersionJsonConverter implements JsonGenerator.Converter { + @Override + boolean handles(Class type) { + Version.isAssignableFrom(type) + } + + @Override + Object convert(Object value, String key) { + ((Version) value).toString() + } +} diff --git a/mdm-common/src/main/resources/META-INF/services/grails.plugin.json.builder.JsonGenerator$Converter b/mdm-common/src/main/resources/META-INF/services/grails.plugin.json.builder.JsonGenerator$Converter index 9c8f00ac21..6411e0c5a0 100644 --- a/mdm-common/src/main/resources/META-INF/services/grails.plugin.json.builder.JsonGenerator$Converter +++ b/mdm-common/src/main/resources/META-INF/services/grails.plugin.json.builder.JsonGenerator$Converter @@ -1 +1,2 @@ -uk.ac.ox.softeng.maurodatamapper.path.PathJsonConverter \ No newline at end of file +uk.ac.ox.softeng.maurodatamapper.path.PathJsonConverter +uk.ac.ox.softeng.maurodatamapper.version.VersionJsonConverter \ No newline at end of file diff --git a/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/util/VersionSpec.groovy b/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/version/VersionSpec.groovy similarity index 99% rename from mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/util/VersionSpec.groovy rename to mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/version/VersionSpec.groovy index 39d8246e5a..16ffca83dc 100644 --- a/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/util/VersionSpec.groovy +++ b/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/version/VersionSpec.groovy @@ -15,7 +15,8 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package uk.ac.ox.softeng.maurodatamapper.util +package uk.ac.ox.softeng.maurodatamapper.version + import spock.lang.Specification diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy index 7ec5e5b0b2..d0cf826df2 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy @@ -39,8 +39,8 @@ 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.util.Utils -import uk.ac.ox.softeng.maurodatamapper.util.Version -import uk.ac.ox.softeng.maurodatamapper.util.VersionChangeType +import uk.ac.ox.softeng.maurodatamapper.version.Version +import uk.ac.ox.softeng.maurodatamapper.version.VersionChangeType import grails.gorm.DetachedCriteria import grails.gorm.transactions.Transactional diff --git a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/gorm/constraint/callable/VersionAwareConstraints.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/gorm/constraint/callable/VersionAwareConstraints.groovy index 36a3081da6..cfc1594a18 100644 --- a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/gorm/constraint/callable/VersionAwareConstraints.groovy +++ b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/gorm/constraint/callable/VersionAwareConstraints.groovy @@ -20,7 +20,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.validator.DocumentationVersionValidator import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.validator.ModelVersionValidator import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.VersionAware -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version class VersionAwareConstraints { diff --git a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/FinaliseData.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/FinaliseData.groovy index bcacd3a946..3fbbe202ae 100644 --- a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/FinaliseData.groovy +++ b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/FinaliseData.groovy @@ -17,8 +17,8 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model -import uk.ac.ox.softeng.maurodatamapper.util.Version -import uk.ac.ox.softeng.maurodatamapper.util.VersionChangeType +import uk.ac.ox.softeng.maurodatamapper.version.Version +import uk.ac.ox.softeng.maurodatamapper.version.VersionChangeType import grails.databinding.BindUsing import grails.validation.Validateable diff --git a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/tree/ContainerTreeItem.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/tree/ContainerTreeItem.groovy index 726e90997a..68e1e0e2fc 100644 --- a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/tree/ContainerTreeItem.groovy +++ b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/tree/ContainerTreeItem.groovy @@ -20,7 +20,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.rest.transport.tree import uk.ac.ox.softeng.maurodatamapper.core.container.VersionedFolder import uk.ac.ox.softeng.maurodatamapper.core.model.Container import uk.ac.ox.softeng.maurodatamapper.util.Utils -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import org.grails.datastore.gorm.GormEntity diff --git a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/tree/ModelTreeItem.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/tree/ModelTreeItem.groovy index 22bd6e5764..2675561be0 100644 --- a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/tree/ModelTreeItem.groovy +++ b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/tree/ModelTreeItem.groovy @@ -18,7 +18,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.rest.transport.tree import uk.ac.ox.softeng.maurodatamapper.core.model.Model -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import org.grails.datastore.gorm.GormEntity diff --git a/mdm-core/grails-app/views/treeItem/_treeItem.gson b/mdm-core/grails-app/views/treeItem/_treeItem.gson index d2cae74a4e..860e8e2791 100644 --- a/mdm-core/grails-app/views/treeItem/_treeItem.gson +++ b/mdm-core/grails-app/views/treeItem/_treeItem.gson @@ -20,8 +20,8 @@ json { if (cti.containerId) parentFolder cti.containerId if (cti.versionAware) { finalised cti.finalised - documentationVersion cti.documentationVersion.toString() - if (cti.modelVersion) modelVersion cti.modelVersion.toString() + documentationVersion cti.documentationVersion + if (cti.modelVersion) modelVersion cti.modelVersion if (cti.modelVersionTag) modelVersionTag cti.modelVersionTag if (cti.branchName && !cti.modelVersion) branchName cti.branchName } @@ -30,10 +30,10 @@ json { deleted mti.deleted finalised mti.finalised superseded mti.superseded - documentationVersion mti.documentationVersion.toString() + documentationVersion mti.documentationVersion folder mti.containerId type mti.modelType - if (mti.modelVersion) modelVersion mti.modelVersion.toString() + if (mti.modelVersion) modelVersion mti.modelVersion if (mti.modelVersionTag) modelVersionTag mti.modelVersionTag if (mti.branchName && !mti.modelVersion) branchName mti.branchName } else if (treeItem instanceof ModelItemTreeItem) { diff --git a/mdm-core/grails-app/views/versionTreeModel/_simpleVersionTreeModel.gson b/mdm-core/grails-app/views/versionTreeModel/_simpleVersionTreeModel.gson index 5a5a297025..766191b59e 100644 --- a/mdm-core/grails-app/views/versionTreeModel/_simpleVersionTreeModel.gson +++ b/mdm-core/grails-app/views/versionTreeModel/_simpleVersionTreeModel.gson @@ -7,7 +7,7 @@ model { json { id versionTreeModel.id branch versionTreeModel.branchName - modelVersion versionTreeModel.modelVersion?.toString() - documentationVersion versionTreeModel.documentationVersion?.toString() + modelVersion versionTreeModel.modelVersion + documentationVersion versionTreeModel.documentationVersion displayName versionTreeModel.simpleDisplayName } diff --git a/mdm-core/grails-app/views/versionTreeModel/_versionTreeModel.gson b/mdm-core/grails-app/views/versionTreeModel/_versionTreeModel.gson index 8c6a373989..995768dc4d 100644 --- a/mdm-core/grails-app/views/versionTreeModel/_versionTreeModel.gson +++ b/mdm-core/grails-app/views/versionTreeModel/_versionTreeModel.gson @@ -8,8 +8,8 @@ json { id versionTreeModel.id label versionTreeModel.label branch versionTreeModel.branchName - modelVersion versionTreeModel.modelVersion?.toString() - documentationVersion versionTreeModel.documentationVersion?.toString() + modelVersion versionTreeModel.modelVersion + documentationVersion versionTreeModel.documentationVersion isNewBranchModelVersion versionTreeModel.newBranchModelVersion isNewDocumentationVersion versionTreeModel.newDocumentationVersion isNewFork versionTreeModel.newFork diff --git a/mdm-core/grails-app/views/versionedFolder/_versionedFolder.gson b/mdm-core/grails-app/views/versionedFolder/_versionedFolder.gson index cb4c8c8557..9258a8da33 100644 --- a/mdm-core/grails-app/views/versionedFolder/_versionedFolder.gson +++ b/mdm-core/grails-app/views/versionedFolder/_versionedFolder.gson @@ -8,7 +8,7 @@ model { json { branchName versionedFolder.branchName - documentationVersion versionedFolder.documentationVersion.toString() - if (versionedFolder.modelVersion) modelVersion versionedFolder.modelVersion.toString() + documentationVersion versionedFolder.documentationVersion + if (versionedFolder.modelVersion) modelVersion versionedFolder.modelVersion if (versionedFolder.modelVersionTag) modelVersionTag versionedFolder.modelVersionTag } \ No newline at end of file diff --git a/mdm-core/grails-app/views/versionedFolder/_versionedFolder_full.gson b/mdm-core/grails-app/views/versionedFolder/_versionedFolder_full.gson index 12ffa90393..013c040d8d 100644 --- a/mdm-core/grails-app/views/versionedFolder/_versionedFolder_full.gson +++ b/mdm-core/grails-app/views/versionedFolder/_versionedFolder_full.gson @@ -10,10 +10,10 @@ model { json { branchName versionedFolder.branchName - documentationVersion versionedFolder.documentationVersion.toString() + documentationVersion versionedFolder.documentationVersion finalised versionedFolder.finalised if (versionedFolder.finalised) dateFinalised versionedFolder.dateFinalised - if (versionedFolder.modelVersion) modelVersion versionedFolder.modelVersion.toString() + if (versionedFolder.modelVersion) modelVersion versionedFolder.modelVersion if (versionedFolder.modelVersionTag) modelVersionTag versionedFolder.modelVersionTag authority tmpl.'/authority/authority'(versionedFolder.authority) diff --git a/mdm-core/grails-app/views/versionedFolder/latestModelVersion.gson b/mdm-core/grails-app/views/versionedFolder/latestModelVersion.gson index 4b9baff3eb..d21d960e75 100644 --- a/mdm-core/grails-app/views/versionedFolder/latestModelVersion.gson +++ b/mdm-core/grails-app/views/versionedFolder/latestModelVersion.gson @@ -1,8 +1,8 @@ -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version model { Version version } json { - modelVersion version.toString() + modelVersion version } \ No newline at end of file diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/gorm/constraint/validator/DocumentationVersionValidator.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/gorm/constraint/validator/DocumentationVersionValidator.groovy index 71229cb4c5..6901ddc44d 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/gorm/constraint/validator/DocumentationVersionValidator.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/gorm/constraint/validator/DocumentationVersionValidator.groovy @@ -20,7 +20,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.validator import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.VersionAware import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.validator.Validator -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version class DocumentationVersionValidator implements Validator { diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/gorm/constraint/validator/ModelVersionValidator.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/gorm/constraint/validator/ModelVersionValidator.groovy index d896e7989c..fffec468d5 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/gorm/constraint/validator/ModelVersionValidator.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/gorm/constraint/validator/ModelVersionValidator.groovy @@ -20,7 +20,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.validator import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.VersionAware import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.validator.Validator -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version class ModelVersionValidator implements Validator { diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy index e30c2cc8fb..3eeea6feba 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy @@ -59,8 +59,8 @@ import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware import uk.ac.ox.softeng.maurodatamapper.util.Utils -import uk.ac.ox.softeng.maurodatamapper.util.Version -import uk.ac.ox.softeng.maurodatamapper.util.VersionChangeType +import uk.ac.ox.softeng.maurodatamapper.version.Version +import uk.ac.ox.softeng.maurodatamapper.version.VersionChangeType import grails.gorm.DetachedCriteria import groovy.util.logging.Slf4j diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/provider/dataloader/DataLoaderProviderService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/provider/dataloader/DataLoaderProviderService.groovy index f02d686dc8..e66e2fe09d 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/provider/dataloader/DataLoaderProviderService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/provider/dataloader/DataLoaderProviderService.groovy @@ -25,7 +25,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.DataLoadable import uk.ac.ox.softeng.maurodatamapper.provider.MauroDataMapperService import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.util.Utils -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import groovy.util.logging.Slf4j import org.hibernate.SessionFactory diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/domain/DataLoadable.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/domain/DataLoadable.groovy index a52f62cb1a..928712c25e 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/domain/DataLoadable.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/domain/DataLoadable.groovy @@ -20,7 +20,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.traits.domain import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier import uk.ac.ox.softeng.maurodatamapper.core.model.container.ClassifierAware -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import org.grails.datastore.gorm.GormEntityApi import org.grails.datastore.gorm.GormValidateable diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/domain/VersionAware.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/domain/VersionAware.groovy index ce9d91bf97..36f28fcd9c 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/domain/VersionAware.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/domain/VersionAware.groovy @@ -20,7 +20,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.traits.domain import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.databinding.BindUsing import groovy.transform.CompileStatic diff --git a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderSpec.groovy b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderSpec.groovy index 0c716cec3e..ec77f58a6a 100644 --- a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderSpec.groovy +++ b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderSpec.groovy @@ -18,7 +18,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.container import uk.ac.ox.softeng.maurodatamapper.core.authority.Authority -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.testing.gorm.DomainUnitTest import org.spockframework.util.InternalSpockError diff --git a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/BasicModelSpec.groovy b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/BasicModelSpec.groovy index 9066d552d3..fc01eb7981 100644 --- a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/BasicModelSpec.groovy +++ b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/BasicModelSpec.groovy @@ -20,7 +20,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.model import uk.ac.ox.softeng.maurodatamapper.core.container.Folder import uk.ac.ox.softeng.maurodatamapper.core.util.test.BasicModel import uk.ac.ox.softeng.maurodatamapper.test.unit.core.ModelSpec -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.testing.gorm.DomainUnitTest import groovy.util.logging.Slf4j diff --git a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/VersionTreeModelSpec.groovy b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/VersionTreeModelSpec.groovy index 709d242353..1488455d2f 100644 --- a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/VersionTreeModelSpec.groovy +++ b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/VersionTreeModelSpec.groovy @@ -20,7 +20,7 @@ package uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType import uk.ac.ox.softeng.maurodatamapper.core.util.test.BasicModel import uk.ac.ox.softeng.maurodatamapper.test.unit.BaseUnitSpec -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import groovy.util.logging.Slf4j diff --git a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/tree/TreeItemServiceSpec.groovy b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/tree/TreeItemServiceSpec.groovy index 96ccc80208..b25d620996 100644 --- a/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/tree/TreeItemServiceSpec.groovy +++ b/mdm-core/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/core/tree/TreeItemServiceSpec.groovy @@ -33,7 +33,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.util.test.BasicModel import uk.ac.ox.softeng.maurodatamapper.core.util.test.BasicModelItem import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.test.unit.BaseUnitSpec -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.testing.services.ServiceUnitTest diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelService.groovy index 807d78384c..8c05fb4fd5 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelService.groovy @@ -56,7 +56,7 @@ import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.util.GormUtils import uk.ac.ox.softeng.maurodatamapper.util.Utils -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.DetachedCriteria import grails.gorm.transactions.Transactional diff --git a/mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/bootstrap/BootstrapModels.groovy b/mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/bootstrap/BootstrapModels.groovy index 046823e862..a54de897c8 100644 --- a/mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/bootstrap/BootstrapModels.groovy +++ b/mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/bootstrap/BootstrapModels.groovy @@ -40,7 +40,7 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.ReferenceType import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.security.basic.PublicAccessSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import asset.pipeline.grails.AssetResourceLocator import groovy.util.logging.Slf4j diff --git a/mdm-plugin-datamodel/grails-app/views/dataModel/_dataModel.gson b/mdm-plugin-datamodel/grails-app/views/dataModel/_dataModel.gson index de6d49d191..fc9ca3e83a 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataModel/_dataModel.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataModel/_dataModel.gson @@ -9,8 +9,8 @@ model { json { type dataModel.modelType branchName dataModel.branchName - documentationVersion dataModel.documentationVersion.toString() - if (dataModel.modelVersion) modelVersion dataModel.modelVersion.toString() + documentationVersion dataModel.documentationVersion + if (dataModel.modelVersion) modelVersion dataModel.modelVersion if (dataModel.modelVersionTag) modelVersionTag dataModel.modelVersionTag if (dataModel.classifiers) classifiers g.render(dataModel.classifiers) diff --git a/mdm-plugin-datamodel/grails-app/views/dataModel/_dataModel_full.gson b/mdm-plugin-datamodel/grails-app/views/dataModel/_dataModel_full.gson index 0eb0528613..895984a297 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataModel/_dataModel_full.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataModel/_dataModel_full.gson @@ -12,7 +12,7 @@ model { json { type dataModel.modelType branchName dataModel.branchName - documentationVersion dataModel.documentationVersion.toString() + documentationVersion dataModel.documentationVersion finalised dataModel.finalised readableByEveryone dataModel.readableByEveryone readableByAuthenticatedUsers dataModel.readableByAuthenticatedUsers @@ -22,7 +22,7 @@ json { if (dataModel.deleted) deleted dataModel.deleted if (dataModel.author) author dataModel.author if (dataModel.organisation) organisation dataModel.organisation - if (dataModel.modelVersion) modelVersion dataModel.modelVersion.toString() + if (dataModel.modelVersion) modelVersion dataModel.modelVersion if (dataModel.modelVersionTag) modelVersionTag dataModel.modelVersionTag authority tmpl.'/authority/authority'(dataModel.authority) diff --git a/mdm-plugin-datamodel/grails-app/views/dataModel/_export.gson b/mdm-plugin-datamodel/grails-app/views/dataModel/_export.gson index 6edf1f698b..e210ddf45b 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataModel/_export.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataModel/_export.gson @@ -12,11 +12,11 @@ json { if (export.author) author export.author if (export.organisation) organisation export.organisation - documentationVersion export.documentationVersion.toString() + documentationVersion export.documentationVersion finalised export.finalised if (export.finalised) dateFinalised OffsetDateTimeConverter.toString(export.dateFinalised) - if (export.modelVersion) modelVersion export.modelVersion.toString() + if (export.modelVersion) modelVersion export.modelVersion authority tmpl.'/authority/authority'(export.authority) if (export.dataTypes) dataTypes tmpl.'/dataType/export'(export.dataTypes.sort()) if (export.childDataClasses) childDataClasses tmpl.'/dataClass/export'(export.childDataClasses) diff --git a/mdm-plugin-datamodel/grails-app/views/dataModel/latestModelVersion.gson b/mdm-plugin-datamodel/grails-app/views/dataModel/latestModelVersion.gson index 4b9baff3eb..d21d960e75 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataModel/latestModelVersion.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataModel/latestModelVersion.gson @@ -1,8 +1,8 @@ -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version model { Version version } json { - modelVersion version.toString() + modelVersion version } \ No newline at end of file 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 9f087ab567..501fa922d9 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 @@ -26,7 +26,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwa import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClass import uk.ac.ox.softeng.maurodatamapper.test.functional.ResourceFunctionalSpec import uk.ac.ox.softeng.maurodatamapper.util.Utils -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Transactional import grails.testing.mixin.integration.Integration diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy index 0d7fe99a8d..0714905089 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy @@ -42,7 +42,7 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.similarity.DataElementSimilari import uk.ac.ox.softeng.maurodatamapper.datamodel.test.BaseDataModelIntegrationSpec import uk.ac.ox.softeng.maurodatamapper.path.Path import uk.ac.ox.softeng.maurodatamapper.util.GormUtils -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Rollback import grails.testing.mixin.integration.Integration diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassFunctionalSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassFunctionalSpec.groovy index 829c81b346..1b023d5211 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassFunctionalSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassFunctionalSpec.groovy @@ -23,7 +23,7 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.DataType import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.PrimitiveType import uk.ac.ox.softeng.maurodatamapper.test.functional.OrderedResourceFunctionalSpec -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Rollback import grails.gorm.transactions.Transactional diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementFunctionalSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementFunctionalSpec.groovy index 790f18decf..e423046850 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementFunctionalSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementFunctionalSpec.groovy @@ -23,7 +23,7 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.DataType import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.PrimitiveType import uk.ac.ox.softeng.maurodatamapper.test.functional.OrderedResourceFunctionalSpec -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Rollback import grails.gorm.transactions.Transactional diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataTypeFunctionalSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataTypeFunctionalSpec.groovy index 35c402f137..f1087994a5 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataTypeFunctionalSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/DataTypeFunctionalSpec.groovy @@ -22,7 +22,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.container.Folder import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClass import uk.ac.ox.softeng.maurodatamapper.test.functional.OrderedResourceFunctionalSpec -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Transactional import grails.testing.mixin.integration.Integration diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/importer/DataModelJsonImporterServiceSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/importer/DataModelJsonImporterServiceSpec.groovy index 1f61bf4691..60ea9916bc 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/importer/DataModelJsonImporterServiceSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/provider/importer/DataModelJsonImporterServiceSpec.groovy @@ -23,7 +23,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.FilePar import uk.ac.ox.softeng.maurodatamapper.core.provider.importer.parameter.ModelImporterProviderServiceParameters import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.test.provider.DataBindDataModelImporterProviderServiceSpec -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Rollback import grails.testing.mixin.integration.Integration diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/tree/TreeItemServiceSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/tree/TreeItemServiceSpec.groovy index 1bc07b3ad3..09dc3fddf3 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/tree/TreeItemServiceSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/tree/TreeItemServiceSpec.groovy @@ -33,7 +33,7 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.DataType import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.PrimitiveType import uk.ac.ox.softeng.maurodatamapper.datamodel.test.BaseDataModelIntegrationSpec import uk.ac.ox.softeng.maurodatamapper.security.basic.PublicAccessSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Rollback import grails.testing.mixin.integration.Integration diff --git a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceSpec.groovy b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceSpec.groovy index 4119605b1b..c8db688f30 100644 --- a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceSpec.groovy +++ b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceSpec.groovy @@ -35,7 +35,7 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.ReferenceType import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.enumeration.EnumerationValue import uk.ac.ox.softeng.maurodatamapper.test.unit.service.CatalogueItemServiceSpec import uk.ac.ox.softeng.maurodatamapper.util.GormUtils -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.testing.services.ServiceUnitTest import groovy.util.logging.Slf4j 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 c96159f68d..c08c34cb40 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 @@ -18,7 +18,7 @@ package uk.ac.ox.softeng.maurodatamapper.federation import uk.ac.ox.softeng.maurodatamapper.core.model.Model -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import java.time.OffsetDateTime diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModel.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModel.groovy index 72bb9711e7..4a89326f13 100644 --- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModel.groovy +++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModel.groovy @@ -40,7 +40,7 @@ import uk.ac.ox.softeng.maurodatamapper.referencedata.facet.ReferenceSummaryMeta import uk.ac.ox.softeng.maurodatamapper.referencedata.item.ReferenceDataElement import uk.ac.ox.softeng.maurodatamapper.referencedata.item.ReferenceDataValue import uk.ac.ox.softeng.maurodatamapper.referencedata.item.datatype.ReferenceDataType -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.DetachedCriteria import grails.rest.Resource diff --git a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelService.groovy b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelService.groovy index c18538aa41..e5cf0f030d 100644 --- a/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelService.groovy +++ b/mdm-plugin-referencedata/grails-app/services/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelService.groovy @@ -51,7 +51,7 @@ import uk.ac.ox.softeng.maurodatamapper.referencedata.traits.service.ReferenceSu import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.util.Utils -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Transactional import groovy.util.logging.Slf4j diff --git a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/_export.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/_export.gson index 83c47f00a5..327ef2933c 100644 --- a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/_export.gson +++ b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/_export.gson @@ -12,11 +12,11 @@ json { if (export.author) author export.author if (export.organisation) organisation export.organisation - documentationVersion export.documentationVersion.toString() + documentationVersion export.documentationVersion finalised export.finalised if (export.finalised) dateFinalised OffsetDateTimeConverter.toString(export.dateFinalised) - if (export.modelVersion) modelVersion export.modelVersion.toString() + if (export.modelVersion) modelVersion export.modelVersion authority tmpl.'/authority/authority'(export.authority) if (export.referenceDataTypes) referenceDataTypes tmpl.'/referenceDataType/export'(export.referenceDataTypes.sort()) if (export.referenceDataElements) referenceDataElements tmpl.'/referenceDataElement/export'(export.referenceDataElements.sort()) diff --git a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/_referenceDataModel.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/_referenceDataModel.gson index af251b9f24..15dd48e1c9 100644 --- a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/_referenceDataModel.gson +++ b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/_referenceDataModel.gson @@ -9,8 +9,8 @@ model { json { type referenceDataModel.modelType branchName referenceDataModel.branchName - documentationVersion referenceDataModel.documentationVersion.toString() - if (referenceDataModel.modelVersion) modelVersion referenceDataModel.modelVersion.toString() + documentationVersion referenceDataModel.documentationVersion + if (referenceDataModel.modelVersion) modelVersion referenceDataModel.modelVersion if (referenceDataModel.modelVersionTag) modelVersionTag referenceDataModel.modelVersionTag if (referenceDataModel.classifiers) classifiers g.render(referenceDataModel.classifiers) diff --git a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/_referenceDataModel_full.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/_referenceDataModel_full.gson index 08f424e9ed..8fe39ee985 100644 --- a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/_referenceDataModel_full.gson +++ b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/_referenceDataModel_full.gson @@ -12,7 +12,7 @@ model { json { type referenceDataModel.modelType branchName referenceDataModel.branchName - documentationVersion referenceDataModel.documentationVersion.toString() + documentationVersion referenceDataModel.documentationVersion finalised referenceDataModel.finalised readableByEveryone referenceDataModel.readableByEveryone readableByAuthenticatedUsers referenceDataModel.readableByAuthenticatedUsers @@ -22,7 +22,7 @@ json { if (referenceDataModel.deleted) deleted referenceDataModel.deleted if (referenceDataModel.author) author referenceDataModel.author if (referenceDataModel.organisation) organisation referenceDataModel.organisation - if (referenceDataModel.modelVersion) modelVersion referenceDataModel.modelVersion.toString() + if (referenceDataModel.modelVersion) modelVersion referenceDataModel.modelVersion if (referenceDataModel.modelVersionTag) modelVersionTag referenceDataModel.modelVersionTag authority tmpl.'/authority/authority'(referenceDataModel.authority) diff --git a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/latestModelVersion.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/latestModelVersion.gson index 4b9baff3eb..d21d960e75 100644 --- a/mdm-plugin-referencedata/grails-app/views/referenceDataModel/latestModelVersion.gson +++ b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/latestModelVersion.gson @@ -1,8 +1,8 @@ -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version model { Version version } json { - modelVersion version.toString() + modelVersion version } \ No newline at end of file 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 f52467e108..2fa055d031 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 @@ -26,7 +26,7 @@ import uk.ac.ox.softeng.maurodatamapper.referencedata.item.ReferenceDataValue import uk.ac.ox.softeng.maurodatamapper.referencedata.item.datatype.ReferenceDataType import uk.ac.ox.softeng.maurodatamapper.referencedata.item.datatype.ReferencePrimitiveType import uk.ac.ox.softeng.maurodatamapper.test.functional.ResourceFunctionalSpec -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Transactional import grails.testing.mixin.integration.Integration diff --git a/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelServiceIntegrationSpec.groovy b/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelServiceIntegrationSpec.groovy index cf9b2942ef..520104d46e 100644 --- a/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelServiceIntegrationSpec.groovy +++ b/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelServiceIntegrationSpec.groovy @@ -26,7 +26,7 @@ import uk.ac.ox.softeng.maurodatamapper.referencedata.similarity.ReferenceDataEl import uk.ac.ox.softeng.maurodatamapper.referencedata.test.BaseReferenceDataModelIntegrationSpec import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import uk.ac.ox.softeng.maurodatamapper.util.GormUtils -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Rollback import grails.testing.mixin.integration.Integration diff --git a/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/tree/TreeItemServiceSpec.groovy b/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/tree/TreeItemServiceSpec.groovy index 5551ff2dee..e03f0d9cd6 100644 --- a/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/tree/TreeItemServiceSpec.groovy +++ b/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/tree/TreeItemServiceSpec.groovy @@ -29,7 +29,7 @@ import uk.ac.ox.softeng.maurodatamapper.referencedata.item.datatype.ReferenceDat import uk.ac.ox.softeng.maurodatamapper.referencedata.item.datatype.ReferencePrimitiveType import uk.ac.ox.softeng.maurodatamapper.referencedata.test.BaseReferenceDataModelIntegrationSpec import uk.ac.ox.softeng.maurodatamapper.security.basic.PublicAccessSecurityPolicyManager -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Rollback import grails.testing.mixin.integration.Integration diff --git a/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelServiceSpec.groovy b/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelServiceSpec.groovy index 1f39dd6786..dd606d609b 100644 --- a/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelServiceSpec.groovy +++ b/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelServiceSpec.groovy @@ -32,7 +32,7 @@ import uk.ac.ox.softeng.maurodatamapper.referencedata.item.datatype.ReferencePri import uk.ac.ox.softeng.maurodatamapper.referencedata.item.datatype.enumeration.ReferenceEnumerationValue import uk.ac.ox.softeng.maurodatamapper.test.unit.service.CatalogueItemServiceSpec import uk.ac.ox.softeng.maurodatamapper.util.GormUtils -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.testing.services.ServiceUnitTest import groovy.util.logging.Slf4j diff --git a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy index cec531f0c0..845cfd9abc 100644 --- a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy +++ b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetService.groovy @@ -41,7 +41,7 @@ import uk.ac.ox.softeng.maurodatamapper.terminology.item.TermService import uk.ac.ox.softeng.maurodatamapper.terminology.item.term.TermRelationshipService import uk.ac.ox.softeng.maurodatamapper.terminology.provider.importer.CodeSetJsonImporterService import uk.ac.ox.softeng.maurodatamapper.util.Utils -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Transactional import groovy.util.logging.Slf4j diff --git a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyService.groovy b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyService.groovy index 18ee9180dd..0c9db1e50f 100644 --- a/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyService.groovy +++ b/mdm-plugin-terminology/grails-app/services/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyService.groovy @@ -41,7 +41,7 @@ import uk.ac.ox.softeng.maurodatamapper.terminology.item.term.TermRelationshipSe import uk.ac.ox.softeng.maurodatamapper.terminology.provider.importer.TerminologyJsonImporterService import uk.ac.ox.softeng.maurodatamapper.util.GormUtils import uk.ac.ox.softeng.maurodatamapper.util.Utils -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Transactional import grails.util.Environment diff --git a/mdm-plugin-terminology/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/terminology/bootstrap/BootstrapModels.groovy b/mdm-plugin-terminology/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/terminology/bootstrap/BootstrapModels.groovy index 8ce40fddc8..98fad4ef9b 100644 --- a/mdm-plugin-terminology/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/terminology/bootstrap/BootstrapModels.groovy +++ b/mdm-plugin-terminology/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/terminology/bootstrap/BootstrapModels.groovy @@ -26,7 +26,7 @@ import uk.ac.ox.softeng.maurodatamapper.terminology.TerminologyService import uk.ac.ox.softeng.maurodatamapper.terminology.item.Term import uk.ac.ox.softeng.maurodatamapper.terminology.item.TermRelationshipType import uk.ac.ox.softeng.maurodatamapper.terminology.item.term.TermRelationship -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import org.springframework.context.MessageSource diff --git a/mdm-plugin-terminology/grails-app/views/codeSet/_codeSet.gson b/mdm-plugin-terminology/grails-app/views/codeSet/_codeSet.gson index 8f23472c11..0bff624931 100644 --- a/mdm-plugin-terminology/grails-app/views/codeSet/_codeSet.gson +++ b/mdm-plugin-terminology/grails-app/views/codeSet/_codeSet.gson @@ -8,8 +8,8 @@ model { json { branchName codeSet.branchName - documentationVersion codeSet.documentationVersion.toString() - if (codeSet.modelVersion) modelVersion codeSet.modelVersion.toString() + documentationVersion codeSet.documentationVersion + if (codeSet.modelVersion) modelVersion codeSet.modelVersion if (codeSet.modelVersionTag) modelVersionTag codeSet.modelVersionTag if (codeSet.classifiers) classifiers g.render(codeSet.classifiers) diff --git a/mdm-plugin-terminology/grails-app/views/codeSet/_codeSet_full.gson b/mdm-plugin-terminology/grails-app/views/codeSet/_codeSet_full.gson index c642b67af9..f877a103f8 100644 --- a/mdm-plugin-terminology/grails-app/views/codeSet/_codeSet_full.gson +++ b/mdm-plugin-terminology/grails-app/views/codeSet/_codeSet_full.gson @@ -11,7 +11,7 @@ model { json { type codeSet.modelType branchName codeSet.branchName - documentationVersion codeSet.documentationVersion.toString() + documentationVersion codeSet.documentationVersion finalised codeSet.finalised readableByEveryone codeSet.readableByEveryone readableByAuthenticatedUsers codeSet.readableByAuthenticatedUsers @@ -21,7 +21,7 @@ json { if (codeSet.deleted) deleted codeSet.deleted if (codeSet.author) author codeSet.author if (codeSet.organisation) organisation codeSet.organisation - if (codeSet.modelVersion) modelVersion codeSet.modelVersion.toString() + if (codeSet.modelVersion) modelVersion codeSet.modelVersion if (codeSet.modelVersionTag) modelVersionTag codeSet.modelVersionTag authority tmpl.'/authority/authority'(codeSet.authority) diff --git a/mdm-plugin-terminology/grails-app/views/codeSet/_export.gson b/mdm-plugin-terminology/grails-app/views/codeSet/_export.gson index 89aa43836f..c9cef3bea1 100644 --- a/mdm-plugin-terminology/grails-app/views/codeSet/_export.gson +++ b/mdm-plugin-terminology/grails-app/views/codeSet/_export.gson @@ -10,11 +10,11 @@ json { if (export.author) author export.author if (export.organisation) organisation export.organisation - documentationVersion export.documentationVersion.toString() + documentationVersion export.documentationVersion finalised export.finalised if (export.finalised) dateFinalised OffsetDateTimeConverter.toString(export.dateFinalised) - if (export.modelVersion) modelVersion export.modelVersion.toString() + if (export.modelVersion) modelVersion export.modelVersion if (export.modelVersionTag) modelVersionTag export.modelVersionTag if (export.authority) authority tmpl.'/authority/export'(export.authority) diff --git a/mdm-plugin-terminology/grails-app/views/codeSet/latestModelVersion.gson b/mdm-plugin-terminology/grails-app/views/codeSet/latestModelVersion.gson index 4b9baff3eb..d21d960e75 100644 --- a/mdm-plugin-terminology/grails-app/views/codeSet/latestModelVersion.gson +++ b/mdm-plugin-terminology/grails-app/views/codeSet/latestModelVersion.gson @@ -1,8 +1,8 @@ -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version model { Version version } json { - modelVersion version.toString() + modelVersion version } \ No newline at end of file diff --git a/mdm-plugin-terminology/grails-app/views/terminology/_export.gson b/mdm-plugin-terminology/grails-app/views/terminology/_export.gson index e5289faf16..515aa07a1a 100644 --- a/mdm-plugin-terminology/grails-app/views/terminology/_export.gson +++ b/mdm-plugin-terminology/grails-app/views/terminology/_export.gson @@ -11,11 +11,11 @@ json { if (export.author) author export.author if (export.organisation) organisation export.organisation - documentationVersion export.documentationVersion.toString() + documentationVersion export.documentationVersion finalised export.finalised if (export.finalised) dateFinalised OffsetDateTimeConverter.toString(export.dateFinalised) - if (export.modelVersion) modelVersion export.modelVersion.toString() + if (export.modelVersion) modelVersion export.modelVersion if (export.authority) authority tmpl.'/authority/export'(export.authority) if (export.termRelationshipTypes) termRelationshipTypes tmpl.'/termRelationshipType/export'(export.termRelationshipTypes) diff --git a/mdm-plugin-terminology/grails-app/views/terminology/_terminology.gson b/mdm-plugin-terminology/grails-app/views/terminology/_terminology.gson index 9719bbfbb9..24e64bdb7b 100644 --- a/mdm-plugin-terminology/grails-app/views/terminology/_terminology.gson +++ b/mdm-plugin-terminology/grails-app/views/terminology/_terminology.gson @@ -8,8 +8,8 @@ model { json { branchName terminology.branchName - documentationVersion terminology.documentationVersion.toString() - if (terminology.modelVersion) modelVersion terminology.modelVersion.toString() + documentationVersion terminology.documentationVersion + if (terminology.modelVersion) modelVersion terminology.modelVersion if (terminology.modelVersionTag) modelVersionTag terminology.modelVersionTag if (terminology.classifiers) classifiers g.render(terminology.classifiers) diff --git a/mdm-plugin-terminology/grails-app/views/terminology/_terminology_full.gson b/mdm-plugin-terminology/grails-app/views/terminology/_terminology_full.gson index 6a0c6319da..44a91395be 100644 --- a/mdm-plugin-terminology/grails-app/views/terminology/_terminology_full.gson +++ b/mdm-plugin-terminology/grails-app/views/terminology/_terminology_full.gson @@ -12,7 +12,7 @@ model { json { type terminology.modelType branchName terminology.branchName - documentationVersion terminology.documentationVersion.toString() + documentationVersion terminology.documentationVersion finalised terminology.finalised readableByEveryone terminology.readableByEveryone readableByAuthenticatedUsers terminology.readableByAuthenticatedUsers @@ -22,7 +22,7 @@ json { if (terminology.deleted) deleted terminology.deleted if (terminology.author) author terminology.author if (terminology.organisation) organisation terminology.organisation - if (terminology.modelVersion) modelVersion terminology.modelVersion.toString() + if (terminology.modelVersion) modelVersion terminology.modelVersion if (terminology.modelVersionTag) modelVersionTag terminology.modelVersionTag authority tmpl.'/authority/authority'(terminology.authority) diff --git a/mdm-plugin-terminology/grails-app/views/terminology/latestModelVersion.gson b/mdm-plugin-terminology/grails-app/views/terminology/latestModelVersion.gson index 4b9baff3eb..d21d960e75 100644 --- a/mdm-plugin-terminology/grails-app/views/terminology/latestModelVersion.gson +++ b/mdm-plugin-terminology/grails-app/views/terminology/latestModelVersion.gson @@ -1,8 +1,8 @@ -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version model { Version version } json { - modelVersion version.toString() + modelVersion version } \ No newline at end of file diff --git a/mdm-plugin-terminology/src/integration-test/groovy/path/TerminologyPathServiceSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/path/TerminologyPathServiceSpec.groovy index 2e95c476fb..7dec5534c0 100644 --- a/mdm-plugin-terminology/src/integration-test/groovy/path/TerminologyPathServiceSpec.groovy +++ b/mdm-plugin-terminology/src/integration-test/groovy/path/TerminologyPathServiceSpec.groovy @@ -24,7 +24,7 @@ import uk.ac.ox.softeng.maurodatamapper.terminology.CodeSet import uk.ac.ox.softeng.maurodatamapper.terminology.Terminology import uk.ac.ox.softeng.maurodatamapper.terminology.item.Term import uk.ac.ox.softeng.maurodatamapper.terminology.test.BaseTerminologyIntegrationSpec -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Rollback import grails.testing.mixin.integration.Integration diff --git a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetFunctionalSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetFunctionalSpec.groovy index 7bbd7d9071..4b4bbf7a0d 100644 --- a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetFunctionalSpec.groovy +++ b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetFunctionalSpec.groovy @@ -24,7 +24,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkType import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints import uk.ac.ox.softeng.maurodatamapper.test.functional.ResourceFunctionalSpec -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Transactional import grails.testing.mixin.integration.Integration 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 3aa584f29e..0c4f59a501 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 @@ -24,7 +24,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkType import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints import uk.ac.ox.softeng.maurodatamapper.test.functional.ResourceFunctionalSpec -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Transactional import grails.testing.mixin.integration.Integration diff --git a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy index 73e881a4b8..53fd82c43a 100644 --- a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy +++ b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy @@ -21,7 +21,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints import uk.ac.ox.softeng.maurodatamapper.terminology.test.BaseTerminologyIntegrationSpec import uk.ac.ox.softeng.maurodatamapper.util.GormUtils -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Rollback import grails.testing.mixin.integration.Integration diff --git a/mdm-plugin-terminology/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceSpec.groovy b/mdm-plugin-terminology/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceSpec.groovy index 80ea0e33bd..4671b35a5f 100644 --- a/mdm-plugin-terminology/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceSpec.groovy +++ b/mdm-plugin-terminology/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceSpec.groovy @@ -29,7 +29,7 @@ import uk.ac.ox.softeng.maurodatamapper.terminology.item.term.TermRelationship import uk.ac.ox.softeng.maurodatamapper.terminology.item.term.TermRelationshipService import uk.ac.ox.softeng.maurodatamapper.test.unit.service.CatalogueItemServiceSpec import uk.ac.ox.softeng.maurodatamapper.util.GormUtils -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.testing.services.ServiceUnitTest import groovy.util.logging.Slf4j diff --git a/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/json/matcher/VersionMatcher.groovy b/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/json/matcher/VersionMatcher.groovy index 136d2d673f..1f4368c531 100644 --- a/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/json/matcher/VersionMatcher.groovy +++ b/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/json/matcher/VersionMatcher.groovy @@ -17,7 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.test.json.matcher -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import org.hamcrest.BaseMatcher import org.hamcrest.Description diff --git a/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/unit/core/ModelSpec.groovy b/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/unit/core/ModelSpec.groovy index 8d5bc6dc40..3081543357 100644 --- a/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/unit/core/ModelSpec.groovy +++ b/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/unit/core/ModelSpec.groovy @@ -21,7 +21,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.container.Folder import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.FieldDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.model.Model -import uk.ac.ox.softeng.maurodatamapper.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import groovy.util.logging.Slf4j import org.spockframework.util.InternalSpockError diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy index 90bc3003ef..45eae96d5c 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy @@ -25,7 +25,7 @@ import uk.ac.ox.softeng.maurodatamapper.security.UserGroup import uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions import uk.ac.ox.softeng.maurodatamapper.security.role.GroupRole import uk.ac.ox.softeng.maurodatamapper.testing.functional.UserAccessAndPermissionChangingFunctionalSpec -import uk.ac.ox.softeng.maurodatamapper.util.VersionChangeType +import uk.ac.ox.softeng.maurodatamapper.version.VersionChangeType import grails.gorm.transactions.Transactional import grails.testing.mixin.integration.Integration diff --git a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelUserAccessPermissionChangingAndVersioningFunctionalSpec.groovy b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelUserAccessPermissionChangingAndVersioningFunctionalSpec.groovy index 87f5cc0fd0..3bf293e91e 100644 --- a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelUserAccessPermissionChangingAndVersioningFunctionalSpec.groovy +++ b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelUserAccessPermissionChangingAndVersioningFunctionalSpec.groovy @@ -22,7 +22,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints import uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions import uk.ac.ox.softeng.maurodatamapper.security.role.GroupRole -import uk.ac.ox.softeng.maurodatamapper.util.VersionChangeType +import uk.ac.ox.softeng.maurodatamapper.version.VersionChangeType import grails.gorm.transactions.Transactional import grails.testing.mixin.integration.Integration From fc20e7612bbbf4aa17c1f9df3544137d94bb40df Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Thu, 15 Jul 2021 17:52:57 +0100 Subject: [PATCH 48/82] Implement a JSON converter for UUID --- .../util/UuidJsonConverter.groovy | 19 +++++++++++++++++++ ...lugin.json.builder.JsonGenerator$Converter | 3 ++- ...FileImporterProviderServiceParameters.gson | 2 +- ...FileImporterProviderServiceParameters.gson | 2 +- 4 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/UuidJsonConverter.groovy diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/UuidJsonConverter.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/UuidJsonConverter.groovy new file mode 100644 index 0000000000..b43c8b32ab --- /dev/null +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/UuidJsonConverter.groovy @@ -0,0 +1,19 @@ +package uk.ac.ox.softeng.maurodatamapper.util + + +import grails.plugin.json.builder.JsonGenerator + +/** + * @since 15/07/2021 + */ +class UuidJsonConverter implements JsonGenerator.Converter { + @Override + boolean handles(Class type) { + UUID.isAssignableFrom(type) + } + + @Override + Object convert(Object value, String key) { + ((UUID) value).toString() + } +} diff --git a/mdm-common/src/main/resources/META-INF/services/grails.plugin.json.builder.JsonGenerator$Converter b/mdm-common/src/main/resources/META-INF/services/grails.plugin.json.builder.JsonGenerator$Converter index 6411e0c5a0..6fe95fcff8 100644 --- a/mdm-common/src/main/resources/META-INF/services/grails.plugin.json.builder.JsonGenerator$Converter +++ b/mdm-common/src/main/resources/META-INF/services/grails.plugin.json.builder.JsonGenerator$Converter @@ -1,2 +1,3 @@ uk.ac.ox.softeng.maurodatamapper.path.PathJsonConverter -uk.ac.ox.softeng.maurodatamapper.version.VersionJsonConverter \ No newline at end of file +uk.ac.ox.softeng.maurodatamapper.version.VersionJsonConverter +uk.ac.ox.softeng.maurodatamapper.util.UuidJsonConverter \ No newline at end of file diff --git a/mdm-plugin-datamodel/grails-app/views/dataModelFileImporterProviderServiceParameters/_dataModelFileImporterProviderServiceParameters.gson b/mdm-plugin-datamodel/grails-app/views/dataModelFileImporterProviderServiceParameters/_dataModelFileImporterProviderServiceParameters.gson index bd7797ab72..4c87885755 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataModelFileImporterProviderServiceParameters/_dataModelFileImporterProviderServiceParameters.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataModelFileImporterProviderServiceParameters/_dataModelFileImporterProviderServiceParameters.gson @@ -12,7 +12,7 @@ json { } finalised dataModelFileImporterProviderServiceParameters.finalised modelName dataModelFileImporterProviderServiceParameters.modelName - folderId dataModelFileImporterProviderServiceParameters.folderId.toString() + folderId dataModelFileImporterProviderServiceParameters.folderId importAsNewDocumentationVersion dataModelFileImporterProviderServiceParameters.importAsNewDocumentationVersion importAsNewBanchModelVersion dataModelFileImporterProviderServiceParameters.importAsNewBranchModelVersion } \ No newline at end of file diff --git a/mdm-plugin-terminology/grails-app/views/terminologyFileImporterProviderServiceParameters/_terminologyFileImporterProviderServiceParameters.gson b/mdm-plugin-terminology/grails-app/views/terminologyFileImporterProviderServiceParameters/_terminologyFileImporterProviderServiceParameters.gson index 2df0829528..e3f5dd7da3 100644 --- a/mdm-plugin-terminology/grails-app/views/terminologyFileImporterProviderServiceParameters/_terminologyFileImporterProviderServiceParameters.gson +++ b/mdm-plugin-terminology/grails-app/views/terminologyFileImporterProviderServiceParameters/_terminologyFileImporterProviderServiceParameters.gson @@ -12,6 +12,6 @@ json { } finalised terminologyFileImporterProviderServiceParameters.finalised modelName terminologyFileImporterProviderServiceParameters.modelName - folderId terminologyFileImporterProviderServiceParameters.folderId.toString() + folderId terminologyFileImporterProviderServiceParameters.folderId importAsNewDocumentationVersion terminologyFileImporterProviderServiceParameters.importAsNewDocumentationVersion } \ No newline at end of file From f8ce08e068f119125b15c13c6d4faa6585719b57 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Thu, 15 Jul 2021 18:00:55 +0100 Subject: [PATCH 49/82] Update test following changes to the pathing with pipes for field name separattion and branch name in the path --- .../datamodel/DataModelFunctionalSpec.groovy | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) 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 501fa922d9..052f3502ea 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 @@ -441,13 +441,13 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { '''{ "sourceId": "${json-unit.matches:id}", "targetId": "${json-unit.matches:id}", - "path": "dm:Functional Test Model", + "path": "dm:Functional Test Model:source", "label": "Functional Test Model", "count": 11, "diffs": [ { "fieldName": "branchName", - "path": "dm:Functional Test Model:branchName", + "path": "dm:Functional Test Model:source|branchName", "sourceValue": "source", "targetValue": "main", "commonAncestorValue": "main", @@ -456,7 +456,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { }, { "fieldName": "description", - "path": "dm:Functional Test Model:description", + "path": "dm:Functional Test Model:source|description", "sourceValue": "DescriptionLeft", "targetValue": "DescriptionRight", "commonAncestorValue": null, @@ -464,32 +464,32 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "type": "modification" }, { - "path": "dm:Functional Test Model|dc:addLeftOnly", + "path": "dm:Functional Test Model:source|dc:addLeftOnly", "isMergeConflict": false, "isSourceModificationAndTargetDeletion": false, "type": "creation" }, { - "path": "dm:Functional Test Model|dc:modifyAndDelete", + "path": "dm:Functional Test Model:source|dc:modifyAndDelete", "isMergeConflict": true, "isSourceModificationAndTargetDeletion": true, "type": "creation" }, { - "path": "dm:Functional Test Model|dc:deleteAndModify", + "path": "dm:Functional Test Model:source|dc:deleteAndModify", "isMergeConflict": true, "isSourceDeletionAndTargetModification": true, "type": "deletion" }, { - "path": "dm:Functional Test Model|dc:deleteLeftOnly", + "path": "dm:Functional Test Model:source|dc:deleteLeftOnly", "isMergeConflict": false, "isSourceDeletionAndTargetModification": false, "type": "deletion" }, { "fieldName": "description", - "path": "dm:Functional Test Model|dc:addAndAddReturningDifference:description", + "path": "dm:Functional Test Model:source|dc:addAndAddReturningDifference|description", "sourceValue": "DescriptionLeft", "targetValue": "DescriptionRight", "commonAncestorValue": null, @@ -497,20 +497,20 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "type": "modification" }, { - "path": "dm:Functional Test Model|dc:existingClass|dc:addLeftToExistingClass", + "path": "dm:Functional Test Model:source|dc:existingClass|dc:addLeftToExistingClass", "isMergeConflict": false, "isSourceModificationAndTargetDeletion": false, "type": "creation" }, { - "path": "dm:Functional Test Model|dc:existingClass|dc:deleteLeftOnlyFromExistingClass", + "path": "dm:Functional Test Model:source|dc:existingClass|dc:deleteLeftOnlyFromExistingClass", "isMergeConflict": false, "isSourceDeletionAndTargetModification": false, "type": "deletion" }, { "fieldName": "description", - "path": "dm:Functional Test Model|dc:modifyAndModifyReturningDifference:description", + "path": "dm:Functional Test Model:source|dc:modifyAndModifyReturningDifference|description", "sourceValue": "DescriptionLeft", "targetValue": "DescriptionRight", "commonAncestorValue": null, @@ -519,7 +519,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { }, { "fieldName": "description", - "path": "dm:Functional Test Model|dc:modifyLeftOnly:description", + "path": "dm:Functional Test Model:source|dc:modifyLeftOnly|description", "sourceValue": "Description", "targetValue": null, "commonAncestorValue": null, From 20ce860fae13b15f7816e3931635b289fa221636 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Fri, 16 Jul 2021 11:32:52 +0100 Subject: [PATCH 50/82] Get the DataModelFunctionalSpec tests working for merging * Fix legacy issues which became apparent from the tests * Add tests to test the merging using the new style --- .../softeng/maurodatamapper/path/Path.groovy | 12 ++ .../maurodatamapper/path/PathNode.groovy | 4 +- .../core/facet/MetadataService.groovy | 4 +- .../transport/merge/FieldPatchData.groovy | 7 +- .../core/model/CatalogueItemService.groovy | 4 +- .../core/model/ModelService.groovy | 11 ++ .../datamodel/item/DataElementService.groovy | 52 +++++-- .../datamodel/DataModelFunctionalSpec.groovy | 147 ++++++++++++++++++ 8 files changed, 219 insertions(+), 22 deletions(-) diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy index d47f349f00..d548adaf36 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy @@ -89,6 +89,12 @@ class Path { } } + Path getRootIndependentPath() { + clone().tap { + first().label = null + } + } + boolean isEmpty() { pathNodes.isEmpty() } @@ -143,4 +149,10 @@ class Path { pathNodes = domains.collect {new PathNode(it.pathPrefix, it.pathIdentifier)} } } + + static Path from(List domains) { + new Path().tap { + pathNodes = domains.collect {new PathNode(it.pathPrefix, it.pathIdentifier)} + } + } } diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy index 1bd8d7630a..2f77788a5f 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy @@ -66,8 +66,8 @@ class PathNode { String toString() { if (typePrefix && label) return "${typePrefix}:${label}" - if (typePrefix) return typePrefix - label + if (typePrefix) return "${typePrefix}:" + ":${label}" } boolean matches(PathNode pathNode) { diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy index b425ee1a25..d1541a8168 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy @@ -236,7 +236,7 @@ class MetadataService implements MultiFacetItemAwareService { } - void mergeMetadataIntoCatalogueItem(CatalogueItem targetCatalogueItem, ObjectPatchData objectPatchData) { + void mergeLegacyMetadataIntoCatalogueItem(CatalogueItem targetCatalogueItem, ObjectPatchData objectPatchData) { if (!objectPatchData.hasPatches()) return @@ -246,7 +246,7 @@ class MetadataService implements MultiFacetItemAwareService { targetCatalogueItem.id) } - objectPatchData.getPatches().each {fieldPatchData -> + objectPatchData.getDiffsWithContent().each {fieldPatchData -> if (fieldPatchData.value) { targetMetadata.setProperty(fieldPatchData.fieldName, fieldPatchData.value) } else { diff --git a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy index 033be72bf1..d5677a850d 100644 --- a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy +++ b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy @@ -65,7 +65,8 @@ class FieldPatchData implements Validateable { } String toString() { - "Merge ${type} patch on ${path} :: Changing ${targetValue} to ${sourceValue}" + String base = "Merge ${type} patch on ${path}" + type == 'modification' ? "${base} :: Changing ${targetValue} to ${sourceValue}" : base } void setPath(String path) { @@ -77,9 +78,7 @@ class FieldPatchData implements Validateable { } Path getRootIndependentPath() { - this.path.clone().tap { - first().label = null - } + this.path.rootIndependentPath } static

FieldPatchData

from(FieldMergeDiff

fieldMergeDiff) { diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy index 4a52c5eff0..c2cb5ebb56 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy @@ -207,11 +207,11 @@ abstract class CatalogueItemService implements DomainSe // copy additions from source to target object fieldPatchData.created.each {createdItemPatchData -> Metadata metadata = metadataService.get(createdItemPatchData.id) - metadataService.copy(targetCatalogueItem, metadata, userSecurityPolicyManager) + metadataService.copy(metadata, targetCatalogueItem) } // for modifications, recursively call this method fieldPatchData.modified.each { modifiedObjectPatchData -> - metadataService.mergeMetadataIntoCatalogueItem(targetCatalogueItem, modifiedObjectPatchData) + metadataService.mergeLegacyMetadataIntoCatalogueItem(targetCatalogueItem, modifiedObjectPatchData) } } } \ No newline at end of file diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy index 3eeea6feba..0133c0a339 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy @@ -177,6 +177,11 @@ abstract class ModelService extends CatalogueItemService imp throw new ApiNotYetImplementedException('MSXX', 'deleteModelAndContent') } + Set getDomainServices() { + domainServices.add(this) + domainServices + } + K shallowValidate(K model) { log.debug('Shallow validating model') long st = System.currentTimeMillis() @@ -490,6 +495,12 @@ abstract class ModelService extends CatalogueItemService imp String fieldName = modificationPatch.fieldName log.debug('Modifying [{}] in [{}]', fieldName, modificationPatch.rootIndependentPath) domain."${fieldName}" = modificationPatch.sourceValue + DomainService domainService = getDomainServices().find {it.handles(domain.class)} + if (!domainService) throw new ApiInternalException('MSXX', "No domain service to handle modification of [${domain.domainType}]") + + if (!domain.validate()) + throw new ApiInvalidModelException('MS01', 'Modified domain is invalid', domain.errors, messageSource) + domainService.save(domain, flush: false, validate: false) } void processDeletionPatchOfModelItem(ModelItem modelItem) { diff --git a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementService.groovy b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementService.groovy index 507454383d..7034bc9499 100644 --- a/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementService.groovy +++ b/mdm-plugin-datamodel/grails-app/services/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementService.groovy @@ -24,6 +24,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkType import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItemService +import uk.ac.ox.softeng.maurodatamapper.core.path.PathService import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.CopyInformation import uk.ac.ox.softeng.maurodatamapper.core.similarity.SimilarityResult import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel @@ -33,8 +34,10 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.DataType import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.DataTypeService import uk.ac.ox.softeng.maurodatamapper.datamodel.similarity.DataElementSimilarityResult import uk.ac.ox.softeng.maurodatamapper.datamodel.traits.service.SummaryMetadataAwareService +import uk.ac.ox.softeng.maurodatamapper.path.Path import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager +import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware import uk.ac.ox.softeng.maurodatamapper.util.Utils import grails.gorm.transactions.Transactional @@ -55,6 +58,7 @@ class DataElementService extends ModelItemService implements Summar DataClassService dataClassService DataTypeService dataTypeService SummaryMetadataService summaryMetadataService + PathService pathService @Override DataElement get(Serializable id) { @@ -82,7 +86,7 @@ class DataElementService extends ModelItemService implements Summar @Override void deleteAll(Collection dataElements) { - dataElements.each { delete(it) } + dataElements.each {delete(it)} } void delete(UUID id) { @@ -154,10 +158,10 @@ class DataElementService extends ModelItemService implements Summar DataElement checkFacetsAfterImportingCatalogueItem(DataElement catalogueItem) { super.checkFacetsAfterImportingCatalogueItem(catalogueItem) if (catalogueItem.summaryMetadata) { - catalogueItem.summaryMetadata.each { sm -> + catalogueItem.summaryMetadata.each {sm -> sm.multiFacetAwareItemId = catalogueItem.id sm.createdBy = sm.createdBy ?: catalogueItem.createdBy - sm.summaryMetadataReports.each { smr -> + sm.summaryMetadataReports.each {smr -> smr.createdBy = catalogueItem.createdBy } } @@ -179,7 +183,7 @@ class DataElementService extends ModelItemService implements Summar @Override List findAllReadableByClassifier(UserSecurityPolicyManager userSecurityPolicyManager, Classifier classifier) { - DataElement.byClassifierId(classifier.id).list().findAll { userSecurityPolicyManager.userCanReadSecuredResourceId(DataModel, it.model.id) } + DataElement.byClassifierId(classifier.id).list().findAll {userSecurityPolicyManager.userCanReadSecuredResourceId(DataModel, it.model.id)} } @Override @@ -213,13 +217,13 @@ class DataElementService extends ModelItemService implements Summar void matchUpDataTypes(DataModel dataModel, Collection dataElements) { if (dataElements) { log.debug("Matching up {} DataElements to a possible {} DataTypes", dataElements.size(), dataModel.dataTypes.size()) - def grouped = dataElements.groupBy { it.dataType.label }.sort { a, b -> + def grouped = dataElements.groupBy {it.dataType.label}.sort {a, b -> def res = a.value.size() <=> b.value.size() if (res == 0) res = a.key <=> b.key res } log.debug('Grouped {} DataElements by DataType label', grouped.size()) - grouped.each { label, elements -> + grouped.each {label, elements -> log.trace('Matching {} elements to DataType label {}', elements.size(), label) DataType dataType = dataModel.findDataTypeByLabel(label) @@ -230,7 +234,7 @@ class DataElementService extends ModelItemService implements Summar dataType.createdBy = dataElement.createdBy dataModel.addToDataTypes(dataType) } - elements.each { dataType.addToDataElements(it) } + elements.each {dataType.addToDataElements(it)} } } } @@ -322,7 +326,31 @@ class DataElementService extends ModelItemService implements Summar dataElement } - //Put the dataClass lookup in this method for use when merging + Path buildDataElementPath(DataElement dataElement) { + DataClass parent = dataElement.dataClass + List parents = [] + + while (parent) { + parents << parent + parent = parent.parentDataClass + } + + List pathObjects = [] + pathObjects << dataElement.model + pathObjects.addAll(parents.reverse()) + pathObjects << dataElement + Path.from(pathObjects) + } + + @Deprecated + @Override + DataElement copy(Model copiedModelInto, DataElement original, UserSecurityPolicyManager userSecurityPolicyManager) { + // The old code just searched for a label that matched which could result in the wrong DC being used, the path is better and more reliable + Path originalPath = buildDataElementPath(original) + DataClass parentToCopyInto = pathService.findResourceByPathFromRootResource(copiedModelInto, originalPath.rootIndependentPath.parent) + copy(copiedModelInto, original, parentToCopyInto, userSecurityPolicyManager) + } + @Override DataElement copy(Model copiedDataModel, DataElement original, CatalogueItem parentDataClass, UserSecurityPolicyManager userSecurityPolicyManager) { DataElement copy = copyDataElement(copiedDataModel as DataModel, original, userSecurityPolicyManager.user, userSecurityPolicyManager) @@ -434,10 +462,10 @@ class DataElementService extends ModelItemService implements Summar List alreadyExistingLinks = semanticLinkService.findAllBySourceMultiFacetAwareItemIdInListAndTargetMultiFacetAwareItemIdInListAndLinkType( dataElements*.id, fromDataElements*.id, SemanticLinkType.IS_FROM) - dataElements.each { de -> - fromDataElements.each { fde -> + dataElements.each {de -> + fromDataElements.each {fde -> // If no link already exists then add a new one - if (!alreadyExistingLinks.any { it.multiFacetAwareItemId == de.id && it.targetMultiFacetAwareItemId == fde.id }) { + if (!alreadyExistingLinks.any {it.multiFacetAwareItemId == de.id && it.targetMultiFacetAwareItemId == fde.id}) { setDataElementIsFromDataElement(de, fde, user) } } @@ -469,7 +497,7 @@ class DataElementService extends ModelItemService implements Summar * @param label The label of the DataElement being sought */ DataElement findDataElement(DataClass dataClass, String label) { - dataClass.dataElements.find { it.label == label.trim() } + dataClass.dataElements.find {it.label == label.trim()} } /** 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 052f3502ea..3b248df252 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 @@ -35,6 +35,8 @@ import grails.web.mime.MimeType import groovy.util.logging.Slf4j import spock.lang.Shared +import java.util.function.Predicate + import static uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress.FUNCTIONAL_TEST import static io.micronaut.http.HttpStatus.CREATED @@ -2224,6 +2226,151 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { cleanUpData(id) } + void 'MP06 : test merging diff with no patch data with new style'() { + given: + String id = createNewItem(validJson) + + PUT("$id/finalise", [versionChangeType: 'Major']) + verifyResponse OK, response + PUT("$id/newBranchModelVersion", [branchName: VersionAwareConstraints.DEFAULT_BRANCH_NAME]) + verifyResponse CREATED, response + String target = responseBody().id + PUT("$id/newBranchModelVersion", [branchName: 'source']) + verifyResponse CREATED, response + String source = responseBody().id + + when: + PUT("$source/mergeInto/$target?isLegacy=false", [:]) + + then: + verifyResponse(UNPROCESSABLE_ENTITY, response) + responseBody().total == 1 + responseBody().errors[0].message.contains('cannot be null') + + cleanup: + cleanUpData(source) + cleanUpData(target) + cleanUpData(id) + } + + void 'MP07 : test merging diff with URI id not matching body id with new style'() { + given: + String id = createNewItem(validJson) + + PUT("$id/finalise", [versionChangeType: 'Major']) + verifyResponse OK, response + PUT("$id/newBranchModelVersion", [branchName: VersionAwareConstraints.DEFAULT_BRANCH_NAME]) + verifyResponse CREATED, response + String target = responseBody().id + PUT("$id/newBranchModelVersion", [branchName: 'source']) + verifyResponse CREATED, response + String source = responseBody().id + + when: + PUT("$source/mergeInto/$target?isLegacy=false", [patch: + [ + targetId: target, + sourceId: UUID.randomUUID().toString(), + label : "Functional Test Model", + count : 0, + patches : [] + ] + ]) + + then: + verifyResponse(UNPROCESSABLE_ENTITY, response) + responseBody().message == 'Source model id passed in request body does not match source model id in URI.' + + when: + PUT("$source/mergeInto/$target", [patch: + [ + targetId: UUID.randomUUID().toString(), + sourceId: source, + label : "Functional Test Model", + count : 0, + patches : [] + ] + ]) + + then: + verifyResponse(UNPROCESSABLE_ENTITY, response) + responseBody().message == 'Target model id passed in request body does not match target model id in URI.' + + when: + PUT("$source/mergeInto/$target", [patch: + [ + targetId: target, + sourceId: source, + label : "Functional Test Model", + count : 0, + patches : [] + ] + ]) + + then: + verifyResponse(OK, response) + responseBody().id == target + + cleanup: + cleanUpData(source) + cleanUpData(target) + cleanUpData(id) + } + + void 'MP08 : test merging diff into draft model using new style'() { + given: + Map mergeData = buildComplexDataModelsForMerging() + + when: + GET("$mergeData.source/mergeDiff/$mergeData.target?isLegacy=false") + + then: + verifyResponse OK, response + responseBody().diffs.size() == 11 + + when: + List patches = responseBody().diffs + patches.removeIf([test: {Map map -> map.fieldName == 'branchName'}] as Predicate) + PUT("$mergeData.source/mergeInto/$mergeData.target?isLegacy=false", [ + patch: [ + targetId: responseBody().targetId, + sourceId: responseBody().sourceId, + label : responseBody().label, + count : patches.size(), + patches : patches] + ]) + + then: + verifyResponse OK, response + responseBody().id == mergeData.target + responseBody().description == 'DescriptionLeft' + + when: + GET("$mergeData.target/dataClasses") + + then: + responseBody().items.label as Set == ['existingClass', 'modifyAndModifyReturningDifference', 'modifyLeftOnly', + 'addAndAddReturningDifference', 'modifyAndDelete', 'addLeftOnly', + 'modifyRightOnly', 'addRightOnly', 'modifyAndModifyReturningNoDifference', + 'addAndAddReturningNoDifference'] as Set + responseBody().items.find {dataClass -> dataClass.label == 'modifyAndDelete'}.description == 'Description' + responseBody().items.find {dataClass -> dataClass.label == 'addAndAddReturningDifference'}.description == 'DescriptionLeft' + responseBody().items.find {dataClass -> dataClass.label == 'modifyAndModifyReturningDifference'}.description == 'DescriptionLeft' + responseBody().items.find {dataClass -> dataClass.label == 'modifyLeftOnly'}.description == 'Description' + + when: + GET("$mergeData.target/dataClasses/$mergeData.existingClass/dataClasses") + + then: + responseBody().items.label as Set == ['addRightToExistingClass', 'addLeftToExistingClass'] as Set + + cleanup: + cleanUpData(mergeData.source) + cleanUpData(mergeData.target) + cleanUpData(mergeData.id) + } + + Map buildComplexDataModelsForMerging() { String id = createNewItem(validJson) From c18b0c217f96aebeef383371dffd4d37c9ea1cc7 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Tue, 20 Jul 2021 15:12:35 +0100 Subject: [PATCH 51/82] Fix failing tests due to wiring --- .../maurodatamapper/datamodel/DataModelServiceSpec.groovy | 2 ++ .../referencedata/ReferenceDataModelServiceSpec.groovy | 2 ++ .../facet/ReferenceSummaryMetadataServiceSpec.groovy | 2 ++ .../terminology/TerminologyServiceSpec.groovy | 2 ++ .../core/tree/FolderTreeItemFunctionalSpec.groovy | 7 ++++++- 5 files changed, 14 insertions(+), 1 deletion(-) diff --git a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceSpec.groovy b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceSpec.groovy index c8db688f30..3809759b5d 100644 --- a/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceSpec.groovy +++ b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceSpec.groovy @@ -21,6 +21,7 @@ package uk.ac.ox.softeng.maurodatamapper.datamodel import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTree import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTreeService import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType +import uk.ac.ox.softeng.maurodatamapper.core.path.PathService import uk.ac.ox.softeng.maurodatamapper.datamodel.bootstrap.BootstrapModels import uk.ac.ox.softeng.maurodatamapper.datamodel.facet.SummaryMetadataService import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClass @@ -55,6 +56,7 @@ class DataModelServiceSpec extends CatalogueItemServiceSpec implements ServiceUn mockArtefact(DataElementService) mockArtefact(DataTypeService) mockArtefact(SummaryMetadataService) + mockArtefact(PathService) mockDomains(DataModel, DataClass, DataType, PrimitiveType, ReferenceType, EnumerationType, EnumerationValue, DataElement) diff --git a/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelServiceSpec.groovy b/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelServiceSpec.groovy index dd606d609b..65eac6e214 100644 --- a/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelServiceSpec.groovy +++ b/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModelServiceSpec.groovy @@ -21,6 +21,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.authority.Authority import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTree import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTreeService import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType +import uk.ac.ox.softeng.maurodatamapper.core.path.PathService import uk.ac.ox.softeng.maurodatamapper.referencedata.bootstrap.BootstrapModels import uk.ac.ox.softeng.maurodatamapper.referencedata.facet.ReferenceSummaryMetadataService import uk.ac.ox.softeng.maurodatamapper.referencedata.item.ReferenceDataElement @@ -50,6 +51,7 @@ class ReferenceDataModelServiceSpec extends CatalogueItemServiceSpec implements mockArtefact(ReferenceDataElementService) mockArtefact(ReferenceDataTypeService) mockArtefact(ReferenceSummaryMetadataService) + mockArtefact(PathService) mockDomains(ReferenceDataModel, ReferenceDataType, ReferencePrimitiveType, ReferenceEnumerationType, ReferenceEnumerationValue, ReferenceDataElement) diff --git a/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadataServiceSpec.groovy b/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadataServiceSpec.groovy index 36c08e1876..1d8b49143e 100644 --- a/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadataServiceSpec.groovy +++ b/mdm-plugin-referencedata/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadataServiceSpec.groovy @@ -32,6 +32,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkService import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLink import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkService import uk.ac.ox.softeng.maurodatamapper.core.model.facet.MultiFacetAware +import uk.ac.ox.softeng.maurodatamapper.core.path.PathService import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModel import uk.ac.ox.softeng.maurodatamapper.referencedata.ReferenceDataModelService import uk.ac.ox.softeng.maurodatamapper.referencedata.facet.summarymetadata.ReferenceSummaryMetadataReport @@ -55,6 +56,7 @@ class ReferenceSummaryMetadataServiceSpec extends MultiFacetItemAwareServiceSpec mockArtefact(SemanticLinkService) mockArtefact(EditService) mockArtefact(MetadataService) + mockArtefact(PathService) mockArtefact(ReferenceDataTypeService) mockDomains(Folder, ReferenceDataModel, Edit, ReferenceSummaryMetadata, ReferenceSummaryMetadataReport, Authority, Metadata, VersionLink, SemanticLink, Classifier) mockArtefact(ReferenceDataModelService) diff --git a/mdm-plugin-terminology/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceSpec.groovy b/mdm-plugin-terminology/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceSpec.groovy index 4671b35a5f..348d0aef61 100644 --- a/mdm-plugin-terminology/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceSpec.groovy +++ b/mdm-plugin-terminology/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceSpec.groovy @@ -20,6 +20,7 @@ package uk.ac.ox.softeng.maurodatamapper.terminology import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTree import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTreeService import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType +import uk.ac.ox.softeng.maurodatamapper.core.path.PathService import uk.ac.ox.softeng.maurodatamapper.terminology.bootstrap.BootstrapModels import uk.ac.ox.softeng.maurodatamapper.terminology.item.Term import uk.ac.ox.softeng.maurodatamapper.terminology.item.TermRelationshipType @@ -50,6 +51,7 @@ class TerminologyServiceSpec extends CatalogueItemServiceSpec implements Service mockArtefact(BreadcrumbTreeService) mockArtefact(TermRelationshipService) mockArtefact(TermRelationshipTypeService) + mockArtefact(PathService) mockDomains(Terminology, Term, TermRelationship, TermRelationshipType) service.breadcrumbTreeService = Stub(BreadcrumbTreeService) { diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/tree/FolderTreeItemFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/tree/FolderTreeItemFunctionalSpec.groovy index f12fe3f61d..0d5c511436 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/tree/FolderTreeItemFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/tree/FolderTreeItemFunctionalSpec.groovy @@ -27,6 +27,7 @@ import io.micronaut.core.type.Argument import io.micronaut.http.HttpResponse import static io.micronaut.http.HttpStatus.CREATED +import static io.micronaut.http.HttpStatus.NO_CONTENT import static io.micronaut.http.HttpStatus.OK /** @@ -288,7 +289,7 @@ class FolderTreeItemFunctionalSpec extends TreeItemFunctionalSpec { loginEditor() POST('versionedFolders', [label: 'Tree Testing Versioned Folder'], MAP_ARG, true) verifyResponse(CREATED, response) - def vfId = responseBody().id + String vfId = responseBody().id POST("versionedFolders/$vfId/folders", [label: 'Tree Testing Folder'], MAP_ARG, true) verifyResponse(CREATED, response) @@ -319,6 +320,10 @@ class FolderTreeItemFunctionalSpec extends TreeItemFunctionalSpec { 'moveToVersionedFolder', 'softDelete' ] + + cleanup: + DELETE("versionedFolders/$vfId?permanent=true", MAP_ARG, true) + verifyResponse(NO_CONTENT, response) } String getReaderTree() { From 5c267ab2578787f96926b55cf012fa1cdce08626 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Tue, 20 Jul 2021 20:56:05 +0100 Subject: [PATCH 52/82] Change the way we path attributes/fields and model identifiers * Use the $ to delimit the branch name or model version * Use the @ to delimit fieldnames * Update all the code in place to handle these changes * Rename a few methods to be more appropriate * Remove the concept of an empty root using prefix only, instead use the more traditional relative path where the root is not supplied and we have the root object to search into * Update the PathService to handle this change to the relative paths * Improve the searching of a model to handle defaulting onto the latest branch or finalised version * Update all tests accordingly * Add tests to prove node and path work as expected --- .../softeng/maurodatamapper/path/Path.groovy | 48 +++-- .../maurodatamapper/path/PathNode.groovy | 172 +++++++++++++----- .../maurodatamapper/path/PathNodeSpec.groovy | 152 ++++++++++++++++ .../maurodatamapper/path/PathSpec.groovy | 110 +++++++++++ .../core/path/PathService.groovy | 41 ++--- .../transport/merge/FieldPatchData.groovy | 4 +- .../diff/tridirectional/FieldMergeDiff.groovy | 2 +- .../maurodatamapper/core/model/Model.groovy | 3 +- .../core/model/ModelService.groovy | 45 +++-- ...DataFlowImporterProviderServiceSpec.groovy | 12 +- .../datamodel/item/DataElementService.groovy | 2 +- .../datamodel/DataModelFunctionalSpec.groovy | 70 +++---- .../DataModelServiceIntegrationSpec.groovy | 2 +- .../path/DataModelPathServiceSpec.groovy | 38 ++-- .../path/TerminologyPathServiceSpec.groovy | 81 ++++++--- .../terminology/CodeSetFunctionalSpec.groovy | 14 +- .../TerminologyFunctionalSpec.groovy | 24 +-- .../core/path/PathFunctionalSpec.groovy | 9 +- 18 files changed, 610 insertions(+), 219 deletions(-) create mode 100644 mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNodeSpec.groovy create mode 100644 mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathSpec.groovy diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy index d548adaf36..a2d40cb976 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy @@ -40,7 +40,7 @@ class Path { } /* - * Make a list of PathNode from the provided path string. The path string is like dm:|dc:class-label|de:element-label + * Make a list of PathNode from the provided path string. The path string is like dc:class-label|de:element-label * which means 'The DataElement labelled element-label which belongs to the DataClass labelled class-label which * belongs to the current DataModel' * @param path The path @@ -51,9 +51,10 @@ class Path { if (path) { String[] splits = path.split(PATH_DELIMITER, MAX_NODES) + int lastIndex = splits.size() - 1 - for (String s : splits) { - pathNodes << new PathNode(s) + splits.eachWithIndex {String node, int i -> + pathNodes << new PathNode(node, i == 0, i == lastIndex) } } } @@ -79,8 +80,8 @@ class Path { this } - Path addToPathNodes(String prefix, String pathIdentifier) { - addToPathNodes(new PathNode(prefix, pathIdentifier)) + Path addToPathNodes(String prefix, String pathIdentifier, boolean isLast) { + addToPathNodes(new PathNode(prefix, pathIdentifier, pathNodes.isEmpty(), isLast)) } Path getParent() { @@ -89,10 +90,17 @@ class Path { } } - Path getRootIndependentPath() { - clone().tap { - first().label = null - } + boolean isAbsoluteTo(CreatorAware creatorAware) { + // If the first node in this path matches the supplied object then this path is absolute against the supplied object, + // otherwise it may be relative or may not be inside this object + Path rootPath = from(creatorAware) + isAbsoluteTo(rootPath) + } + + boolean isAbsoluteTo(Path rootPath) { + // If the first node in this path matches the supplied object then this path is absolute against the supplied object, + // otherwise it may be relative or may not be inside this object + rootPath.first().matches(this.first()) } boolean isEmpty() { @@ -104,7 +112,9 @@ class Path { } Path getChildPath() { - new Path(pathNodes: pathNodes[1..size() - 1]) + clone().tap { + pathNodes.removeAt(0) + } } String toString() { @@ -132,12 +142,12 @@ class Path { static Path from(String prefix, String pathIdentifier) { new Path().tap { - addToPathNodes(prefix, pathIdentifier) + pathNodes << new PathNode(prefix, pathIdentifier, true, true) } } static Path from(Path parentPath, String prefix, String pathIdentifier) { - parentPath ? parentPath.clone().addToPathNodes(prefix, pathIdentifier) : from(prefix, pathIdentifier) + parentPath ? parentPath.clone().addToPathNodes(prefix, pathIdentifier, false) : from(prefix, pathIdentifier) } static Path from(String parentPath, String prefix, String pathIdentifier) { @@ -146,13 +156,23 @@ class Path { static Path from(CreatorAware... domains) { new Path().tap { - pathNodes = domains.collect {new PathNode(it.pathPrefix, it.pathIdentifier)} + domains.eachWithIndex {CreatorAware domain, int i -> + pathNodes << new PathNode(domain.pathPrefix, domain.pathIdentifier, i == 0, false) + } } } static Path from(List domains) { new Path().tap { - pathNodes = domains.collect {new PathNode(it.pathPrefix, it.pathIdentifier)} + domains.eachWithIndex {CreatorAware domain, int i -> + pathNodes << new PathNode(domain.pathPrefix, domain.pathIdentifier, i == 0, false) + } } } + + static Path forAttributeOnPath(Path path, String attribute) { + Path attributePath = path.clone() + attributePath.last().attribute = attribute + attributePath + } } diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy index 2f77788a5f..57dec17542 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy @@ -18,6 +18,7 @@ package uk.ac.ox.softeng.maurodatamapper.path import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware +import uk.ac.ox.softeng.maurodatamapper.version.Version import groovy.util.logging.Slf4j @@ -27,89 +28,164 @@ import groovy.util.logging.Slf4j @Slf4j class PathNode { - static String NODE_DELIMITER = ":" + static final String MODEL_PATH_IDENTIFIER_SEPARATOR = '$' + static final String ESCAPED_MODEL_PATH_IDENTIFIER_SEPARATOR = "\\${MODEL_PATH_IDENTIFIER_SEPARATOR}" + static final String ATTRIBUTE_PATH_IDENTIFIER_SEPARATOR = '@' - String typePrefix - String label + String prefix + String identifier + String attribute + String modelIdentifier - PathNode(String prefix, String label) { - this.typePrefix = prefix - this.label = label + PathNode(String prefix, String identifier, boolean isRoot, boolean isLast) { + this.prefix = prefix + parseIdentifier(identifier, isRoot, isLast) + } + + PathNode(String prefix, String identifier, String modelIdentifier, String attribute) { + this.prefix = prefix + this.identifier = identifier + this.attribute = attribute + this.modelIdentifier = modelIdentifier } /* - Parse a string into a type prefix and label. - The string is imagined to be of the format tp:label - Label can contain a : character, so some real examples are - dm:my-data-model (type prefix = dm, label = my-data-model) - te:my-code:my-definition (type prefix = te, label = my-code:my-definition) + Parse a string into a type prefix and identifier. + The string is imagined to be of the format tp:identifier + identifier can contain a : character, so some real examples are + dm:my-data-model (type prefix = dm, identifier = my-data-model) + te:my-code:my-definition (type prefix = te, identifier = my-code:my-definition) */ - PathNode(String node) { - //Look for the first : - int index = node.indexOf(NODE_DELIMITER) - - //If there is a : not in the zero index then extract the type prefix - if (index > 0) { - typePrefix = node.substring(0, index) + PathNode(String node, boolean isRoot, boolean isLast) { + node.find(/^(\w+):(.+)$/) {full, foundPrefix, fullIdentifier -> + prefix = foundPrefix + parseIdentifier(fullIdentifier, isRoot, isLast) } + } - //If there are characters after the : then extract these as the label - if (index < node.length() - 1) { - label = node.substring(index + 1) + void parseIdentifier(String fullIdentifier, boolean isRoot, boolean isLast) { + String parsed = fullIdentifier + if (isLast) { + parsed.find(/^(.+?)${ATTRIBUTE_PATH_IDENTIFIER_SEPARATOR}(.+?)$/) {full, subIdentifier, attr -> + attribute = attr + parsed = subIdentifier + } } + if (isRoot) { + parsed.find(/^(.+?)${ESCAPED_MODEL_PATH_IDENTIFIER_SEPARATOR}(.+?)$/) {full, subIdentifier, foundIdentifier -> + parsed = subIdentifier + modelIdentifier = foundIdentifier + } + } + identifier = parsed } - boolean hasTypePrefix() { - typePrefix + boolean isPropertyNode() { + attribute } String toString() { - if (typePrefix && label) return "${typePrefix}:${label}" - if (typePrefix) return "${typePrefix}:" - ":${label}" + String base = "${prefix}:${getFullIdentifier()}" + if (attribute) base += "${ATTRIBUTE_PATH_IDENTIFIER_SEPARATOR}${attribute}" + base + } + + String getFullIdentifier() { + String base = identifier + if (modelIdentifier) base += "${MODEL_PATH_IDENTIFIER_SEPARATOR}${modelIdentifier}" + base + } + + boolean equals(o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + + PathNode pathNode = (PathNode) o + if (prefix != pathNode.prefix) return false + if (identifier != pathNode.identifier) return false + + if (attribute != pathNode.attribute) return false + + if (modelIdentifier || pathNode.modelIdentifier) { + if (Version.isVersionable(modelIdentifier) && + Version.isVersionable(pathNode.modelIdentifier) && + Version.from(modelIdentifier) == Version.from(pathNode.modelIdentifier)) { + return true + } + return modelIdentifier == pathNode.modelIdentifier + } + + return true + } + + int hashCode() { + int result + result = prefix.hashCode() + result = 31 * result + identifier.hashCode() + String adjusted = Version.isVersionable(modelIdentifier) ? Version.from(modelIdentifier).toString() : modelIdentifier + result = 31 * result + (adjusted != null ? adjusted.hashCode() : 0) + result = 31 * result + (attribute != null ? attribute.hashCode() : 0) + return result } boolean matches(PathNode pathNode) { - matches(pathNode.typePrefix, pathNode.label) + matchesPrefix(pathNode.prefix) && matchesIdentifier(pathNode) } boolean matches(CreatorAware creatorAware) { - matches(creatorAware.pathPrefix, creatorAware.pathIdentifier) + matches(Path.from(creatorAware).last()) } - boolean matches(String pathPrefix, String pathIdentifier) { - matchesLabel(pathIdentifier) && matchesPrefix(pathPrefix) + boolean matchesPrefix(String otherPrefix) { + if (prefix != otherPrefix) { + log.warn("Resource prefix [{}] does not match the path node [{}]", otherPrefix, this) + return false + } + true } - boolean matchesLabel(String pathIdentifier) { + boolean matchesIdentifier(PathNode otherPathNode) { - // No label suggests we're at the top of the path and we've come in via an id tracking path - // confirmation will come via the prefix check - if (!label) return true + if (identifier == otherPathNode.identifier && modelIdentifier == otherPathNode.modelIdentifier) return true - if (label == pathIdentifier) return true - - // Allow for the possibility of a label which has been defaulted (models can be path'd without the branch or version) - String[] pathIdentifierSplit = pathIdentifier.split(/:/) - if (label == pathIdentifierSplit[0]) return true + // If model identifier present on either side then we need to do some verification + if ((modelIdentifier || otherPathNode.modelIdentifier)) { + return matchesModelPathIdentifierFormat(otherPathNode.identifier, otherPathNode.modelIdentifier) + } // Some of the legacy paths included : so we handle submissions of this format - String[] labelSplit = label.split(/:/) - if (labelSplit[0] == pathIdentifier) return true - log.warn("Resource identifier [{}] does not match the path node [{}]", pathIdentifier, this) + String[] identifierSplit = identifier.split(/:/) + if (identifierSplit[0] == otherPathNode.identifier) return true + + identifierSplit = otherPathNode.identifier.split(/:/) + if (identifier == identifierSplit[0]) return true + log.warn("Resource identifier [{}] does not match the path node [{}]", otherPathNode, this) false } - boolean matchesPrefix(String pathPrefix) { - if (pathPrefix != typePrefix) { - log.warn("Resource prefix [{}] does not match the path node [{}]", typePrefix, this) - return false + boolean matchesModelPathIdentifierFormat(String otherIdentifier, String otherModelIdentifier) { + + // if the main identifiers dont match then theres no point continuing + if (identifier != otherIdentifier) return false + + // If the either node has no model identifier then its defaulting and if the main identifiers match then we're happy + if ((!otherModelIdentifier || !modelIdentifier) && identifier == otherIdentifier) return true + + // identifier part and identity part match + // Covers exact string match on both version and branch names + if (modelIdentifier == otherModelIdentifier) return true + + // If path identity is versionable then its possible the identifierIdentity is a short hand version of the same version e.g. 1 vs 1.0.0 + if (Version.isVersionable(modelIdentifier) && Version.isVersionable(otherModelIdentifier)) { + return Version.from(modelIdentifier) == Version.from(otherModelIdentifier) } - true + + // no other possible match style + false } PathNode clone() { - new PathNode(this.typePrefix, this.label) + new PathNode(this.prefix, this.identifier, this.modelIdentifier, this.attribute) } } diff --git a/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNodeSpec.groovy b/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNodeSpec.groovy new file mode 100644 index 0000000000..5c01dd13a5 --- /dev/null +++ b/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNodeSpec.groovy @@ -0,0 +1,152 @@ +package uk.ac.ox.softeng.maurodatamapper.path + +import spock.lang.Specification + +/** + * @since 20/07/2021 + */ +class PathNodeSpec extends Specification { + + void 'test path node creation'() { + given: + PathNode pathNode = new PathNode('dm', 'test model', true, true) + + expect: + pathNode.prefix == 'dm' + pathNode.identifier == 'test model' + !pathNode.modelIdentifier + !pathNode.attribute + pathNode.toString() == 'dm:test model' + } + + void 'test path node creation with parsing'() { + given: + PathNode pathNode = new PathNode('dm:test model', true, true) + + expect: + pathNode.prefix == 'dm' + pathNode.identifier == 'test model' + !pathNode.modelIdentifier + !pathNode.attribute + } + + void 'test path node creation with parsing with model'() { + given: + PathNode pathNode = new PathNode('dm:test model$main', true, true) + + expect: + pathNode.prefix == 'dm' + pathNode.identifier == 'test model' + pathNode.modelIdentifier == 'main' + !pathNode.attribute + pathNode.toString() == 'dm:test model$main' + } + + void 'test path node creation with parsing with attribute'() { + given: + PathNode pathNode = new PathNode('dm:test model@description', true, true) + + expect: + pathNode.prefix == 'dm' + pathNode.identifier == 'test model' + !pathNode.modelIdentifier + pathNode.attribute == 'description' + pathNode.isPropertyNode() + pathNode.toString() == 'dm:test model@description' + } + + void 'test path node creation with parsing with model and attribute'() { + given: + PathNode pathNode = new PathNode('dm:test model$main@description', true, true) + + expect: + pathNode.prefix == 'dm' + pathNode.identifier == 'test model' + pathNode.modelIdentifier == 'main' + pathNode.attribute == 'description' + pathNode.toString() == 'dm:test model$main@description' + } + + void 'test path node matching on identifier and model identifier'() { + when: 'same branch names' + PathNode pathNode1 = new PathNode('dm:test model$main', true, true) + PathNode pathNode2 = new PathNode('dm:test model$main', true, true) + + then: 'matches' + pathNode1.matches(pathNode2) + pathNode1 == pathNode2 + + when: 'defaulting branch name means we dont care about it' + pathNode2 = new PathNode('dm:test model', true, true) + + then: 'matches but does not equal' + pathNode1.matches(pathNode2) + pathNode1 != pathNode2 + + when: 'different branch names' + pathNode2 = new PathNode('dm:test model$test', true, true) + + then: 'no match' + !pathNode1.matches(pathNode2) + pathNode1 != pathNode2 + + when: 'same branch name different identifier' + pathNode2 = new PathNode('dm:test model 2$main', true, true) + + then: 'no match' + !pathNode1.matches(pathNode2) + pathNode1 != pathNode2 + + when: 'versionable model identifier vs branch name' + pathNode2 = new PathNode('dm:test model$1.0.0', true, true) + + then: 'no match' + !pathNode1.matches(pathNode2) + pathNode1 != pathNode2 + + when: 'same versionable model identifier' + pathNode1 = new PathNode('dm:test model$1.0.0', true, true) + + then: 'match and equality' + pathNode1.matches(pathNode2) + pathNode1 == pathNode2 + + when: 'same versionable model identifier' + pathNode1 = new PathNode('dm:test model$1.0', true, true) + + then: 'match and equality' + pathNode1.matches(pathNode2) + pathNode1 == pathNode2 + + when: 'same versionable model identifier' + pathNode1 = new PathNode('dm:test model$1', true, true) + + then: 'match and equality' + pathNode1.matches(pathNode2) + pathNode1 == pathNode2 + + when: 'different identifiers and different prefix' + pathNode2 = new PathNode('dc:test class', true, true) + + then: 'no match' + !pathNode1.matches(pathNode2) + pathNode1 != pathNode2 + } + + void 'test path node matching with attributes'() { + when: 'same branch names' + PathNode pathNode1 = new PathNode('dm:test model$main', true, true) + PathNode pathNode2 = new PathNode('dm:test model$main', true, true) + + then: 'matches' + pathNode1.matches(pathNode2) + pathNode1 == pathNode2 + + when: 'attribute included' + pathNode2 = new PathNode('dm:test model$main@description', true, true) + + then: 'matches but does not equal' + pathNode1.matches(pathNode2) + pathNode1 != pathNode2 + } +} diff --git a/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathSpec.groovy b/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathSpec.groovy new file mode 100644 index 0000000000..81da384b4f --- /dev/null +++ b/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathSpec.groovy @@ -0,0 +1,110 @@ +package uk.ac.ox.softeng.maurodatamapper.path + +import spock.lang.Specification + +/** + * @since 20/07/2021 + */ +class PathSpec extends Specification { + + void 'test single path creation'() { + when: + Path path = Path.from('dm:test') + + then: + path.first().prefix == 'dm' + path.first().identifier == 'test' + + when: + path = Path.from('dm:test$main') + + then: + path.first().prefix == 'dm' + path.first().identifier == 'test' + path.first().modelIdentifier == 'main' + + when: + path = Path.from('dm:test@description') + + then: + path.first().prefix == 'dm' + path.first().identifier == 'test' + !path.first().modelIdentifier + path.first().attribute == 'description' + + when: + path = Path.from('dm:test$main@description') + + then: + path.first().prefix == 'dm' + path.first().identifier == 'test' + path.first().modelIdentifier == 'main' + path.first().attribute == 'description' + } + + void 'test depth path creation'() { + when: + Path path = Path.from('dm:test$main|dc:test2|de:test3') + + then: + path.size() == 3 + path[0].prefix == 'dm' + path[0].identifier == 'test' + path[1].prefix == 'dc' + path[1].identifier == 'test2' + path[2].prefix == 'de' + path[2].identifier == 'test3' + + and: + path.matches(Path.from('dm:test$main|dc:test2|de:test3')) + path.isAbsoluteTo(Path.from('dm:test$main')) + !path.isAbsoluteTo(Path.from('dm:test$test')) + + when: + path = Path.from('dm:test$main|dc:test2|de:test3@description') + + then: + path.size() == 3 + path[0].prefix == 'dm' + path[0].identifier == 'test' + path[0].modelIdentifier == 'main' + path[1].prefix == 'dc' + path[1].identifier == 'test2' + path[2].prefix == 'de' + path[2].identifier == 'test3' + path[2].attribute == 'description' + + and: + path.matches(Path.from('dm:test$main|dc:test2|de:test3')) + + when: + path = Path.from('dm:test|dc:test2|de:test3') + + then: + path.size() == 3 + path[0].prefix == 'dm' + path[0].identifier == 'test' + path[1].prefix == 'dc' + path[1].identifier == 'test2' + path[2].prefix == 'de' + path[2].identifier == 'test3' + + and: + path.matches(Path.from('dm:test$main|dc:test2|de:test3')) + path.isAbsoluteTo(Path.from('dm:test')) + + } + + void 'child path testing'() { + given: + Path path = Path.from('dm:test$main|dc:test2|de:test3') + + when: + Path childPath = path.childPath + + then: + childPath.size() == 2 + childPath.first().prefix == 'dc' + childPath.first().identifier == 'test2' + } +} diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy index 8335561963..1b8709482c 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy @@ -71,37 +71,30 @@ class PathService { CreatorAware findResourceByPathFromRootResource(CreatorAware rootResourceOfPath, Path path) { log.debug('Searching for path {} inside {}:{}', path, rootResourceOfPath.pathPrefix, rootResourceOfPath.pathIdentifier) if (path.isEmpty()) { - throw new ApiBadRequestException('PS06', 'Must have a path to search') + // assume we're in an empty/relative root which means we want the root resource + return rootResourceOfPath } - if (!(path.first().matches(rootResourceOfPath))) { - log.warn('Path cannot exist inside resource as first path node is not the resource node') - return null - } - - // Confirmed the path is inside the model - // If only one node then return the model - if (path.size() == 1) return rootResourceOfPath + // If the current path is absolute to the root then get the relative path so we can search into the root + Path pathToFind = path.isAbsoluteTo(rootResourceOfPath) ? path.childPath : path - // Only 2 nodes in path, first is model - // Last part of path is a field access as has no type prefix so return the model - if (path.size() == 2 && !path.last().hasTypePrefix()) return rootResourceOfPath + // If no nodes in the pathToFind then return the model + if (pathToFind.isEmpty()) return rootResourceOfPath // Find the first child in the path - Path childPath = path.childPath - PathNode childNode = childPath.first() + PathNode childNode = pathToFind.first() DomainService domainService = domainServices.find {service -> - service.handlesPathPrefix(childNode.typePrefix) + service.handlesPathPrefix(childNode.prefix) } if (!domainService) { - log.warn("Unknown path prefix [${childNode.typePrefix}] in path") + log.warn("Unknown path prefix [${childNode.prefix}] in path") return null } - log.trace('Found service [{}] to handle prefix [{}]', domainService.class.simpleName, childNode.typePrefix) - def child = domainService.findByParentIdAndPathIdentifier(rootResourceOfPath.id, childNode.label) + log.trace('Found service [{}] to handle prefix [{}]', domainService.class.simpleName, childNode.prefix) + def child = domainService.findByParentIdAndPathIdentifier(rootResourceOfPath.id, childNode.getFullIdentifier()) if (!child) { log.warn("Child [${childNode}] does not exist in path [${path}]") @@ -109,11 +102,7 @@ class PathService { } // Recurse down the path for that child - findResourceByPathFromRootResource(child, childPath) - } - - CreatorAware findResourceByPathFromRootClass(Class rootClass, String path) { - findResourceByPathFromRootClass(rootClass, Path.from(path)) + findResourceByPathFromRootResource(child, pathToFind) } CreatorAware findResourceByPathFromRootClass(Class rootClass, Path path) { @@ -135,14 +124,14 @@ class PathService { throw new ApiBadRequestException('PS04', "[${rootClass.simpleName}] is not a pathable resource") } - CreatorAware rootResource = securableResourceService.findByParentIdAndPathIdentifier(null, rootNode.label) + CreatorAware rootResource = securableResourceService.findByParentIdAndPathIdentifier(null, rootNode.getFullIdentifier()) if (!rootResource) return null // Confirm root resource exists and its prefix matches the pathed prefix // We dont need to check the prefix in the findResourceByPathFromRootResource method as we "have" a resource at this point // And all subsequent calls in that method use the prefix to find the domain service - if (rootResource.pathPrefix != rootNode.typePrefix) { - log.warn("Root resource prefix [${rootNode.typePrefix}] does not match the root class to search") + if (rootResource.pathPrefix != rootNode.prefix) { + log.warn("Root resource prefix [${rootNode.prefix}] does not match the root class to search") return null } diff --git a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy index d5677a850d..4cec669f38 100644 --- a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy +++ b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy @@ -77,8 +77,8 @@ class FieldPatchData implements Validateable { this.path = path } - Path getRootIndependentPath() { - this.path.rootIndependentPath + Path getRelativePathToRoot() { + this.path.childPath } static

FieldPatchData

from(FieldMergeDiff

fieldMergeDiff) { diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy index 71326ce0fd..05df6bab01 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy @@ -81,7 +81,7 @@ class FieldMergeDiff extends TriDirectionalDiff implements Comparable extends CatalogueItem implements SecurableRes @Override String getPathIdentifier() { - "${label}:${modelVersion ?: branchName}" + "${label}${PathNode.MODEL_PATH_IDENTIFIER_SEPARATOR}${modelVersion ?: branchName}" } @Override diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy index 0133c0a339..4b10d1befa 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy @@ -53,6 +53,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.traits.service.DomainService import uk.ac.ox.softeng.maurodatamapper.core.traits.service.MultiFacetItemAwareService import uk.ac.ox.softeng.maurodatamapper.core.traits.service.VersionLinkAwareService import uk.ac.ox.softeng.maurodatamapper.path.Path +import uk.ac.ox.softeng.maurodatamapper.path.PathNode import uk.ac.ox.softeng.maurodatamapper.security.SecurableResourceService import uk.ac.ox.softeng.maurodatamapper.security.SecurityPolicyManagerService import uk.ac.ox.softeng.maurodatamapper.security.User @@ -229,22 +230,30 @@ abstract class ModelService extends CatalogueItemService imp @Override K findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { - String[] split = pathIdentifier.split(/:/) + String[] split = pathIdentifier.split(PathNode.ESCAPED_MODEL_PATH_IDENTIFIER_SEPARATOR) String label = split[0] - // Default to main branch - String identity = split.size() == 1 ? 'main' : split[1] - DetachedCriteria criteria = parentId ? modelClass.byFolderId(parentId) : modelClass.by() + // A specific identity of the model has been requested so make sure we limit to that + if (split.size() == 2) { + String identity = split[1] + DetachedCriteria criteria = parentId ? modelClass.byFolderId(parentId) : modelClass.by() - criteria.eq('label', label) + criteria.eq('label', label) - if (Version.isVersionable(identity)) { - criteria.eq('modelVersion', Version.from(identity)) - } else { - criteria.eq('branchName', identity) + // Try the search by modelVersion or branchName and no modelVersion + // This will return the requested model or the latest non-finalised main branch + if (Version.isVersionable(identity)) { + criteria.eq('modelVersion', Version.from(identity)) + } else { + // Need to make sure that if the main branch is requested we return the one without a modelVersion + criteria.eq('branchName', identity) + .isNull('modelVersion') + } + return criteria.get() as K } - criteria.get() as K + // If no identity part then we can just get the latest model by the label + findLatestModelByLabel(label) } K finaliseModel(K model, User user, Version requestedModelVersion, VersionChangeType versionChangeType, @@ -467,33 +476,33 @@ abstract class ModelService extends CatalogueItemService imp void processCreationPatchIntoModel(FieldPatchData creationPatch, K targetModel, K sourceModel, UserSecurityPolicyManager userSecurityPolicyManager) { CreatorAware domainToCopy = pathService.findResourceByPathFromRootResource(sourceModel, creationPatch.path) - log.debug('Creating {} into {}', creationPatch.path, creationPatch.rootIndependentPath.parent) + log.debug('Creating {} into {}', creationPatch.path, creationPatch.relativePathToRoot.parent) // Potential deletions are modelitems or facets from model or modelitem if (Utils.parentClassIsAssignableFromChild(ModelItem, domainToCopy.class)) { - processCreationPatchOfModelItem(domainToCopy as ModelItem, targetModel, creationPatch.rootIndependentPath.parent, userSecurityPolicyManager) + processCreationPatchOfModelItem(domainToCopy as ModelItem, targetModel, creationPatch.relativePathToRoot.parent, userSecurityPolicyManager) } if (Utils.parentClassIsAssignableFromChild(MultiFacetItemAware, domainToCopy.class)) { - processCreationPatchOfFacet(domainToCopy as MultiFacetItemAware, targetModel, creationPatch.rootIndependentPath.parent, userSecurityPolicyManager) + processCreationPatchOfFacet(domainToCopy as MultiFacetItemAware, targetModel, creationPatch.relativePathToRoot.parent, userSecurityPolicyManager) } } void processDeletionPatchIntoModel(FieldPatchData deletionPatch, K targetModel) { - CreatorAware domain = pathService.findResourceByPathFromRootResource(targetModel, deletionPatch.rootIndependentPath) - log.debug('Deleting [{}]', deletionPatch.rootIndependentPath) + CreatorAware domain = pathService.findResourceByPathFromRootResource(targetModel, deletionPatch.relativePathToRoot) + log.debug('Deleting [{}]', deletionPatch.relativePathToRoot) // Potential deletions are modelitems or facets from model or modelitem if (Utils.parentClassIsAssignableFromChild(ModelItem, domain.class)) { processDeletionPatchOfModelItem(domain as ModelItem) } if (Utils.parentClassIsAssignableFromChild(MultiFacetItemAware, domain.class)) { - processDeletionPatchOfFacet(domain as MultiFacetItemAware, targetModel, deletionPatch.rootIndependentPath) + processDeletionPatchOfFacet(domain as MultiFacetItemAware, targetModel, deletionPatch.relativePathToRoot) } } void processModificationPatchIntoModel(FieldPatchData modificationPatch, K targetModel) { - CreatorAware domain = pathService.findResourceByPathFromRootResource(targetModel, modificationPatch.rootIndependentPath) + CreatorAware domain = pathService.findResourceByPathFromRootResource(targetModel, modificationPatch.relativePathToRoot) String fieldName = modificationPatch.fieldName - log.debug('Modifying [{}] in [{}]', fieldName, modificationPatch.rootIndependentPath) + log.debug('Modifying [{}] in [{}]', fieldName, modificationPatch.relativePathToRoot) domain."${fieldName}" = modificationPatch.sourceValue DomainService domainService = getDomainServices().find {it.handles(domain.class)} if (!domainService) throw new ApiInternalException('MSXX', "No domain service to handle modification of [${domain.domainType}]") diff --git a/mdm-plugin-dataflow/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/test/provider/DataBindDataFlowImporterProviderServiceSpec.groovy b/mdm-plugin-dataflow/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/test/provider/DataBindDataFlowImporterProviderServiceSpec.groovy index d8b6990d0b..9dfb3d52a8 100644 --- a/mdm-plugin-dataflow/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/test/provider/DataBindDataFlowImporterProviderServiceSpec.groovy +++ b/mdm-plugin-dataflow/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/dataflow/test/provider/DataBindDataFlowImporterProviderServiceSpec.groovy @@ -267,7 +267,7 @@ abstract class DataBindDataFlowImporterProviderServiceSpec implements Summar DataElement copy(Model copiedModelInto, DataElement original, UserSecurityPolicyManager userSecurityPolicyManager) { // The old code just searched for a label that matched which could result in the wrong DC being used, the path is better and more reliable Path originalPath = buildDataElementPath(original) - DataClass parentToCopyInto = pathService.findResourceByPathFromRootResource(copiedModelInto, originalPath.rootIndependentPath.parent) + DataClass parentToCopyInto = pathService.findResourceByPathFromRootResource(copiedModelInto, originalPath.childPath.parent) copy(copiedModelInto, original, parentToCopyInto, userSecurityPolicyManager) } 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 3b248df252..9aff8bb24d 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 @@ -443,13 +443,13 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { '''{ "sourceId": "${json-unit.matches:id}", "targetId": "${json-unit.matches:id}", - "path": "dm:Functional Test Model:source", + "path": "dm:Functional Test Model$source", "label": "Functional Test Model", "count": 11, "diffs": [ { "fieldName": "branchName", - "path": "dm:Functional Test Model:source|branchName", + "path": "dm:Functional Test Model$source@branchName", "sourceValue": "source", "targetValue": "main", "commonAncestorValue": "main", @@ -458,7 +458,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { }, { "fieldName": "description", - "path": "dm:Functional Test Model:source|description", + "path": "dm:Functional Test Model$source@description", "sourceValue": "DescriptionLeft", "targetValue": "DescriptionRight", "commonAncestorValue": null, @@ -466,32 +466,32 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "type": "modification" }, { - "path": "dm:Functional Test Model:source|dc:addLeftOnly", + "path": "dm:Functional Test Model$source|dc:addLeftOnly", "isMergeConflict": false, "isSourceModificationAndTargetDeletion": false, "type": "creation" }, { - "path": "dm:Functional Test Model:source|dc:modifyAndDelete", + "path": "dm:Functional Test Model$source|dc:modifyAndDelete", "isMergeConflict": true, "isSourceModificationAndTargetDeletion": true, "type": "creation" }, { - "path": "dm:Functional Test Model:source|dc:deleteAndModify", + "path": "dm:Functional Test Model$source|dc:deleteAndModify", "isMergeConflict": true, "isSourceDeletionAndTargetModification": true, "type": "deletion" }, { - "path": "dm:Functional Test Model:source|dc:deleteLeftOnly", + "path": "dm:Functional Test Model$source|dc:deleteLeftOnly", "isMergeConflict": false, "isSourceDeletionAndTargetModification": false, "type": "deletion" }, { "fieldName": "description", - "path": "dm:Functional Test Model:source|dc:addAndAddReturningDifference|description", + "path": "dm:Functional Test Model$source|dc:addAndAddReturningDifference@description", "sourceValue": "DescriptionLeft", "targetValue": "DescriptionRight", "commonAncestorValue": null, @@ -499,20 +499,20 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "type": "modification" }, { - "path": "dm:Functional Test Model:source|dc:existingClass|dc:addLeftToExistingClass", + "path": "dm:Functional Test Model$source|dc:existingClass|dc:addLeftToExistingClass", "isMergeConflict": false, "isSourceModificationAndTargetDeletion": false, "type": "creation" }, { - "path": "dm:Functional Test Model:source|dc:existingClass|dc:deleteLeftOnlyFromExistingClass", + "path": "dm:Functional Test Model$source|dc:existingClass|dc:deleteLeftOnlyFromExistingClass", "isMergeConflict": false, "isSourceDeletionAndTargetModification": false, "type": "deletion" }, { "fieldName": "description", - "path": "dm:Functional Test Model:source|dc:modifyAndModifyReturningDifference|description", + "path": "dm:Functional Test Model$source|dc:modifyAndModifyReturningDifference@description", "sourceValue": "DescriptionLeft", "targetValue": "DescriptionRight", "commonAncestorValue": null, @@ -521,7 +521,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { }, { "fieldName": "description", - "path": "dm:Functional Test Model:source|dc:modifyLeftOnly|description", + "path": "dm:Functional Test Model$source|dc:modifyLeftOnly@description", "sourceValue": "Description", "targetValue": null, "commonAncestorValue": null, @@ -1806,7 +1806,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { when: //to modify - GET("$source/path/dm%3A%7Cdc%3AmodifyLeftOnly") + GET("$source/path/dc%3AmodifyLeftOnly") verifyResponse OK, response String modifyLeftOnly = responseBody().id @@ -1849,7 +1849,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { when: // for mergeInto json - GET("$target/path/dm%3A%7Cdc%3AmodifyLeftOnly") + GET("$target/path/dc%3AmodifyLeftOnly") verifyResponse OK, response modifyLeftOnly = responseBody().id @@ -2410,32 +2410,32 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { verifyResponse CREATED, response String source = responseBody().id - GET("$source/path/dm%3A%7Cdc%3AexistingClass") + GET("$source/path/dc%3AexistingClass") verifyResponse OK, response String sourceExistingClass = responseBody().id - GET("$source/path/dm%3A%7Cdc%3AexistingClass%7Cdc%3AdeleteLeftOnlyFromExistingClass") + GET("$source/path/dc%3AexistingClass%7Cdc%3AdeleteLeftOnlyFromExistingClass") verifyResponse OK, response String deleteLeftOnlyFromExistingClass = responseBody().id - GET("$source/path/dm%3A%7Cdc%3AdeleteLeftOnly") + GET("$source/path/dc%3AdeleteLeftOnly") verifyResponse OK, response String deleteLeftOnly = responseBody().id - GET("$source/path/dm%3A%7Cdc%3AdeleteAndDelete") + GET("$source/path/dc%3AdeleteAndDelete") verifyResponse OK, response String deleteAndDelete = responseBody().id - GET("$source/path/dm%3A%7Cdc%3AdeleteAndModify") + GET("$source/path/dc%3AdeleteAndModify") verifyResponse OK, response String deleteAndModify = responseBody().id - GET("$source/path/dm%3A%7Cdc%3AmodifyLeftOnly") + GET("$source/path/dc%3AmodifyLeftOnly") verifyResponse OK, response String modifyLeftOnly = responseBody().id - GET("$source/path/dm%3A%7Cdc%3AmodifyAndDelete") + GET("$source/path/dc%3AmodifyAndDelete") verifyResponse OK, response String modifyAndDelete = responseBody().id - GET("$source/path/dm%3A%7Cdc%3AmodifyAndModifyReturningNoDifference") + GET("$source/path/dc%3AmodifyAndModifyReturningNoDifference") verifyResponse OK, response String modifyAndModifyReturningNoDifference = responseBody().id - GET("$source/path/dm%3A%7Cdc%3AmodifyAndModifyReturningDifference") + GET("$source/path/dc%3AmodifyAndModifyReturningDifference") verifyResponse OK, response String modifyAndModifyReturningDifference = responseBody().id @@ -2472,32 +2472,32 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { PUT("$source", [description: 'DescriptionLeft']) verifyResponse OK, response - GET("$target/path/dm%3A%7Cdc%3AexistingClass") + GET("$target/path/dc%3AexistingClass") verifyResponse OK, response String targetExistingClass = responseBody().id - GET("$target/path/dm%3A%7Cdc%3AexistingClass%7Cdc%3AdeleteRightOnlyFromExistingClass") + GET("$target/path/dc%3AexistingClass%7Cdc%3AdeleteRightOnlyFromExistingClass") verifyResponse OK, response String deleteRightOnlyFromExistingClass = responseBody().id - GET("$target/path/dm%3A%7Cdc%3AdeleteRightOnly") + GET("$target/path/dc%3AdeleteRightOnly") verifyResponse OK, response String deleteRightOnly = responseBody().id - GET("$target/path/dm%3A%7Cdc%3AdeleteAndDelete") + GET("$target/path/dc%3AdeleteAndDelete") verifyResponse OK, response deleteAndDelete = responseBody().id - GET("$target/path/dm%3A%7Cdc%3AmodifyAndDelete") + GET("$target/path/dc%3AmodifyAndDelete") verifyResponse OK, response String targetModifyAndDelete = responseBody().id - GET("$target/path/dm%3A%7Cdc%3AmodifyRightOnly") + GET("$target/path/dc%3AmodifyRightOnly") verifyResponse OK, response String modifyRightOnly = responseBody().id - GET("$target/path/dm%3A%7Cdc%3AdeleteAndModify") + GET("$target/path/dc%3AdeleteAndModify") verifyResponse OK, response deleteAndModify = responseBody().id - GET("$target/path/dm%3A%7Cdc%3AmodifyAndModifyReturningNoDifference") + GET("$target/path/dc%3AmodifyAndModifyReturningNoDifference") verifyResponse OK, response modifyAndModifyReturningNoDifference = responseBody().id - GET("$target/path/dm%3A%7Cdc%3AmodifyAndModifyReturningDifference") + GET("$target/path/dc%3AmodifyAndModifyReturningDifference") verifyResponse OK, response modifyAndModifyReturningDifference = responseBody().id @@ -2533,13 +2533,13 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { verifyResponse OK, response // for mergeInto json - GET("$target/path/dm%3A%7Cdc%3AdeleteLeftOnly") + GET("$target/path/dc%3AdeleteLeftOnly") verifyResponse OK, response deleteLeftOnly = responseBody().id - GET("$target/path/dm%3A%7Cdc%3AmodifyLeftOnly") + GET("$target/path/dc%3AmodifyLeftOnly") verifyResponse OK, response modifyLeftOnly = responseBody().id - GET("$target/path/dm%3A%7Cdc%3AexistingClass%7Cdc%3AdeleteLeftOnlyFromExistingClass") + GET("$target/path/dc%3AexistingClass%7Cdc%3AdeleteLeftOnlyFromExistingClass") verifyResponse OK, response deleteLeftOnlyFromExistingClass = responseBody().id diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy index 0714905089..578a502961 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy @@ -1339,7 +1339,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { when: FieldMergeDiff diff = mergeDiff.flattenedDiffs.findAll {it instanceof FieldMergeDiff} .find {FieldMergeDiff fmd -> - fmd.fieldName == 'description' && Path.from('dm:test database:test|dc:modifyBothReturningDifference').matches(fmd.getFullyQualifiedObjectPath()) + fmd.fieldName == 'description' && Path.from('dm:test database$test|dc:modifyBothReturningDifference').matches(fmd.getFullyQualifiedObjectPath()) } then: diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/path/DataModelPathServiceSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/path/DataModelPathServiceSpec.groovy index 2ccbac3908..df83030dff 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/path/DataModelPathServiceSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/path/DataModelPathServiceSpec.groovy @@ -222,6 +222,13 @@ class DataModelPathServiceSpec extends BaseDataModelIntegrationSpec { then: catalogueItem.label == "data model 1" catalogueItem.domainType == "DataModel" + + when: 'providing the ID and using absolute path from the id' + catalogueItem = pathService.findResourceByPathFromRootResource(dataModel1, path) as CatalogueItem + + then: + catalogueItem.label == "data model 1" + catalogueItem.domainType == "DataModel" } /* @@ -252,7 +259,7 @@ class DataModelPathServiceSpec extends BaseDataModelIntegrationSpec { CatalogueItem catalogueItem when: - Path path = Path.from('dm:|dc:data class 1') + Path path = Path.from('dc:data class 1') catalogueItem = pathService.findResourceByPathFromRootResource(dataModel1, path) as CatalogueItem then: @@ -262,7 +269,7 @@ class DataModelPathServiceSpec extends BaseDataModelIntegrationSpec { //This is the nested data class when: - path = Path.from('dm:|dc:data class 1|dc:data class 1_1') + path = Path.from('dc:data class 1|dc:data class 1_1') catalogueItem = pathService.findResourceByPathFromRootResource(dataModel1, path) as CatalogueItem then: @@ -271,7 +278,7 @@ class DataModelPathServiceSpec extends BaseDataModelIntegrationSpec { catalogueItem.model.id == dataModel1.id when: - path = Path.from('dm:|dc:data class 2') + path = Path.from('dc:data class 2') catalogueItem = pathService.findResourceByPathFromRootResource(dataModel1, path) as CatalogueItem then: @@ -280,7 +287,7 @@ class DataModelPathServiceSpec extends BaseDataModelIntegrationSpec { catalogueItem.model.id == dataModel1.id when: - path = Path.from('dm:|dc:data class 3') + path = Path.from('dc:data class 3') catalogueItem = pathService.findResourceByPathFromRootResource(dataModel1, path) as CatalogueItem then: @@ -289,11 +296,20 @@ class DataModelPathServiceSpec extends BaseDataModelIntegrationSpec { catalogueItem.model.id == dataModel1.id when: - path = Path.from('dm:|dc:data class 4') + path = Path.from('dc:data class 4') catalogueItem = pathService.findResourceByPathFromRootResource(dataModel1, path) as CatalogueItem then: catalogueItem == null + + when: 'absolute path of DC with the correct DM' + path = Path.from(dataModel1, dataClass1_1) + catalogueItem = pathService.findResourceByPathFromRootResource(dataModel1, path) as CatalogueItem + + then: + catalogueItem.label == "data class 1" + catalogueItem.domainType == "DataClass" + catalogueItem.model.id == dataModel1.id } /* @@ -307,7 +323,7 @@ class DataModelPathServiceSpec extends BaseDataModelIntegrationSpec { CatalogueItem catalogueItem when: - Path path = Path.from('dm:|dc:data class 1') + Path path = Path.from('dc:data class 1') catalogueItem = pathService.findResourceByPathFromRootResource(dataModel2, path) as CatalogueItem then: @@ -316,7 +332,7 @@ class DataModelPathServiceSpec extends BaseDataModelIntegrationSpec { catalogueItem.model.id == dataModel2.id when: - path = Path.from('dm:|dc:data class 2') + path = Path.from('dc:data class 2') catalogueItem = pathService.findResourceByPathFromRootResource(dataModel2, path) as CatalogueItem then: @@ -325,7 +341,7 @@ class DataModelPathServiceSpec extends BaseDataModelIntegrationSpec { catalogueItem.model.id == dataModel2.id when: - path = Path.from('dm:|dc:data class 3') + path = Path.from('dc:data class 3') catalogueItem = pathService.findResourceByPathFromRootResource(dataModel2, path) as CatalogueItem then: @@ -334,7 +350,7 @@ class DataModelPathServiceSpec extends BaseDataModelIntegrationSpec { catalogueItem.model.id == dataModel2.id when: - path = Path.from('dm:|dc:data class 4') + path = Path.from('dc:data class 4') catalogueItem = pathService.findResourceByPathFromRootResource(dataModel2, path) as CatalogueItem then: @@ -364,7 +380,7 @@ class DataModelPathServiceSpec extends BaseDataModelIntegrationSpec { //When the data model is root when: - path = Path.from('dm:|dc:data class 1|de:data element 1') + path = Path.from('dc:data class 1|de:data element 1') catalogueItem = pathService.findResourceByPathFromRootResource(dataModel2, path) as CatalogueItem then: @@ -384,7 +400,7 @@ class DataModelPathServiceSpec extends BaseDataModelIntegrationSpec { //When the data model ID is provided when: - Path path = Path.from('dm:|dt:path service test data type') + Path path = Path.from('dt:path service test data type') catalogueItem = pathService.findResourceByPathFromRootResource(dataModel2, path) as CatalogueItem then: diff --git a/mdm-plugin-terminology/src/integration-test/groovy/path/TerminologyPathServiceSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/path/TerminologyPathServiceSpec.groovy index 7dec5534c0..62cffa1ecd 100644 --- a/mdm-plugin-terminology/src/integration-test/groovy/path/TerminologyPathServiceSpec.groovy +++ b/mdm-plugin-terminology/src/integration-test/groovy/path/TerminologyPathServiceSpec.groovy @@ -29,7 +29,6 @@ import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Rollback import grails.testing.mixin.integration.Integration import groovy.util.logging.Slf4j -import spock.lang.PendingFeature import java.time.OffsetDateTime import java.time.ZoneOffset @@ -173,7 +172,7 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { Terminology 1 by ID */ when: - Path path = Path.from('te:') + Path path = Path.from('') catalogueItem = pathService.findResourceByPathFromRootResource(terminology1, path) as CatalogueItem then: @@ -185,7 +184,7 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { Terminology 2 by ID */ when: - path = Path.from('te:') + path = Path.from('') catalogueItem = pathService.findResourceByPathFromRootResource(terminology2, path) as CatalogueItem then: @@ -271,7 +270,16 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { Terminology 1 Term 1 */ when: - Path path = Path.from("te:|tm:${terminology1_term1.pathIdentifier}") + Path path = Path.from("tm:${terminology1_term1.pathIdentifier}") + catalogueItem = pathService.findResourceByPathFromRootResource(terminology1, path) as CatalogueItem + + then: + catalogueItem.label == terminology1_term1.label + catalogueItem.domainType == "Term" + catalogueItem.id == terminology1_term1.id + + when: + path = Path.from("te:${terminology1.pathIdentifier}|tm:${terminology1_term1.pathIdentifier}") catalogueItem = pathService.findResourceByPathFromRootResource(terminology1, path) as CatalogueItem then: @@ -283,7 +291,7 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { Terminology 1 Term 1 */ when: 'using the label' - path = Path.from("te:|tm:${terminology1_term1.label}") + path = Path.from("tm:${terminology1_term1.label}") catalogueItem = pathService.findResourceByPathFromRootResource(terminology1, path) as CatalogueItem then: @@ -295,7 +303,7 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { Terminology 1 Term 2 */ when: - path = Path.from("te:|tm:${terminology1_term2.pathIdentifier}") + path = Path.from("tm:${terminology1_term2.pathIdentifier}") catalogueItem = pathService.findResourceByPathFromRootResource(terminology1, path) as CatalogueItem then: @@ -307,7 +315,7 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { Terminology 2 Term 1 */ when: - path = Path.from("te:|tm:${terminology2_term1.pathIdentifier}") + path = Path.from("tm:${terminology2_term1.pathIdentifier}") catalogueItem = pathService.findResourceByPathFromRootResource(terminology2, path) as CatalogueItem then: @@ -319,7 +327,7 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { Terminology 2 Term 2 */ when: - path = Path.from("te:|tm:${terminology2_term2.pathIdentifier}") + path = Path.from("tm:${terminology2_term2.pathIdentifier}") catalogueItem = pathService.findResourceByPathFromRootResource(terminology2, path) as CatalogueItem then: @@ -343,7 +351,14 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { Try to get a term which doesn't exist on Terminology 1 */ when: - path = Path.from("te:|tm:${terminology2_term1.pathIdentifier}") + path = Path.from("tm:${terminology2_term1.pathIdentifier}") + catalogueItem = pathService.findResourceByPathFromRootResource(terminology1, path) as CatalogueItem + + then: + catalogueItem == null + + when: + path = Path.from("te:${terminology1.pathIdentifier}|tm:${terminology2_term1.pathIdentifier}") catalogueItem = pathService.findResourceByPathFromRootResource(terminology1, path) as CatalogueItem then: @@ -353,7 +368,7 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { Try to get a term which doesn't exist on Terminology 2 */ when: - path = Path.from("te:|tm:${terminology1_term1.pathIdentifier}") + path = Path.from("tm:${terminology1_term1.pathIdentifier}") catalogueItem = pathService.findResourceByPathFromRootResource(terminology2, path) as CatalogueItem then: @@ -369,7 +384,7 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { CodeSet 1 by ID */ when: - Path path = Path.from('cs:') + Path path = Path.from('') catalogueItem = pathService.findResourceByPathFromRootResource(codeSet1, path) as CatalogueItem then: @@ -381,7 +396,7 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { CodeSet 2 by ID */ when: - path = Path.from('cs:') + path = Path.from('') catalogueItem = pathService.findResourceByPathFromRootResource(codeSet2, path) as CatalogueItem then: @@ -467,7 +482,7 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { CodeSet 1 Term 1 */ when: - Path path = Path.from("cs:|tm:${terminology1_term1.pathIdentifier}") + Path path = Path.from("tm:${terminology1_term1.pathIdentifier}") catalogueItem = pathService.findResourceByPathFromRootResource(codeSet1, path) as CatalogueItem then: @@ -479,7 +494,7 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { CodeSet 1 Term 2 */ when: - path = Path.from("cs:|tm:${terminology1_term2.pathIdentifier}") + path = Path.from("tm:${terminology1_term2.pathIdentifier}") catalogueItem = pathService.findResourceByPathFromRootResource(codeSet1, path) as CatalogueItem then: @@ -491,7 +506,16 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { CodeSet 2 Term 1 */ when: - path = Path.from("cs:|tm:${terminology2_term1.pathIdentifier}") + path = Path.from("tm:${terminology2_term1.pathIdentifier}") + catalogueItem = pathService.findResourceByPathFromRootResource(codeSet2, path) as CatalogueItem + + then: + catalogueItem.label == terminology2_term1.label + catalogueItem.domainType == "Term" + catalogueItem.id == terminology2_term1.id + + when: + path = Path.from("cs:${codeSet2.pathIdentifier}|tm:${terminology2_term1.pathIdentifier}") catalogueItem = pathService.findResourceByPathFromRootResource(codeSet2, path) as CatalogueItem then: @@ -503,7 +527,7 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { CodeSet 2 Term 2 */ when: - path = Path.from("cs:|tm:${terminology2_term2.pathIdentifier}") + path = Path.from("tm:${terminology2_term2.pathIdentifier}") catalogueItem = pathService.findResourceByPathFromRootResource(codeSet2, path) as CatalogueItem then: @@ -515,7 +539,7 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { Try to get a term which doesn't exist on CodeSet 1 */ when: - path = Path.from("cs:|tm:${terminology2_term1.pathIdentifier}") + path = Path.from("tm:${terminology2_term1.pathIdentifier}") catalogueItem = pathService.findResourceByPathFromRootResource(codeSet1, path) as CatalogueItem then: @@ -525,7 +549,7 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { Try to get a term which doesn't exist on CodeSet 2 */ when: - path = Path.from("cs:|tm:${terminology1_term1.pathIdentifier}") + path = Path.from("tm:${terminology1_term1.pathIdentifier}") catalogueItem = pathService.findResourceByPathFromRootResource(codeSet2, path) as CatalogueItem then: @@ -552,7 +576,7 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { catalogueItem.description == terminology3main.description when: 'using the branch name main' - path = Path.from('te:terminology 3:main') + path = Path.from('te:terminology 3$main') catalogueItem = pathService.findResourceByPathFromRootClass(Terminology, path) as CatalogueItem then: @@ -561,17 +585,15 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { catalogueItem.id == terminology3main.id when: 'using the branch name draft' - path = Path.from('te:terminology 3:draft') + path = Path.from('te:terminology 3$draft') catalogueItem = pathService.findResourceByPathFromRootClass(Terminology, path) as CatalogueItem then: catalogueItem.label == 'terminology 3' catalogueItem.domainType == "Terminology" catalogueItem.id == terminology3draft.id - } - @PendingFeature void "test get Terminology by path when there are finalised and non-finalised versions"() { given: setupData() @@ -592,7 +614,7 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { catalogueItem.description == terminology4notFinalised.description when: 'using the version' - path = Path.from('te:terminology 4:1.0.0') + path = Path.from('te:terminology 4$1.0.0') catalogueItem = pathService.findResourceByPathFromRootClass(Terminology, path) as CatalogueItem then: @@ -602,8 +624,7 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { } - @PendingFeature - void "test get Terminology by path when there are two finalised versions"() { + void "FV : test get Terminology by path when there are two finalised versions"() { given: setupData() CatalogueItem catalogueItem @@ -623,7 +644,7 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { catalogueItem.modelVersion.toString() == '2.0.0' when: 'using version 2.0.0' - path = Path.from('te:terminology 5:2.0.0') + path = Path.from('te:terminology 5$2.0.0') catalogueItem = pathService.findResourceByPathFromRootClass(Terminology, path) as CatalogueItem then: @@ -633,7 +654,7 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { catalogueItem.modelVersion.toString() == '2.0.0' when: 'using version 2' - path = Path.from('te:terminology 5:2') + path = Path.from('te:terminology 5$2') catalogueItem = pathService.findResourceByPathFromRootClass(Terminology, path) as CatalogueItem then: @@ -643,14 +664,14 @@ class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { catalogueItem.modelVersion.toString() == '2.0.0' when: 'using version 1' - path = Path.from('te:terminology 5:1') + path = Path.from('te:terminology 5$1') catalogueItem = pathService.findResourceByPathFromRootClass(Terminology, path) as CatalogueItem then: catalogueItem.label == 'terminology 5' catalogueItem.domainType == "Terminology" - catalogueItem.id == terminology5second.id - catalogueItem.modelVersion.toString() == '2.0.0' + catalogueItem.id == terminology5first.id + catalogueItem.modelVersion.toString() == '1.0.0' } } diff --git a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetFunctionalSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetFunctionalSpec.groovy index 4b4bbf7a0d..e7492dcdb0 100644 --- a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetFunctionalSpec.groovy +++ b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetFunctionalSpec.groovy @@ -1218,13 +1218,13 @@ class CodeSetFunctionalSpec extends ResourceFunctionalSpec { verifyResponse OK, response //to modify - GET("terminologies/$sourceTerminology/path/te%3A%7Ctm%3AMLO:%20modifyLeftOnly", MAP_ARG, true) + GET("terminologies/$sourceTerminology/path/tm%3AMLO:%20modifyLeftOnly", MAP_ARG, true) verifyResponse OK, response String sourceModifyLeftOnly = responseBody().id - GET("terminologies/$sourceTerminology/path/te%3A%7Ctm%3AMAD:%20modifyAndDelete", MAP_ARG, true) + GET("terminologies/$sourceTerminology/path/tm%3AMAD:%20modifyAndDelete", MAP_ARG, true) verifyResponse OK, response String sourceModifyAndDelete = responseBody().id - GET("terminologies/$sourceTerminology/path/te%3A%7Ctm%3AMAMRD:%20modifyAndModifyReturningDifference", MAP_ARG, true) + GET("terminologies/$sourceTerminology/path/tm%3AMAMRD:%20modifyAndModifyReturningDifference", MAP_ARG, true) verifyResponse OK, response String sourceModifyAndModifyReturningDifference = responseBody().id @@ -1259,17 +1259,17 @@ class CodeSetFunctionalSpec extends ResourceFunctionalSpec { verifyResponse OK, response - GET("terminologies/$targetTerminology/path/te%3A%7Ctm%3ADAM:%20deleteAndModify", MAP_ARG, true) + GET("terminologies/$targetTerminology/path/tm%3ADAM:%20deleteAndModify", MAP_ARG, true) verifyResponse OK, response deleteAndModify = responseBody().id - GET("terminologies/$targetTerminology/path/te%3A%7Ctm%3AMAMRD:%20modifyAndModifyReturningDifference", MAP_ARG, true) + GET("terminologies/$targetTerminology/path/tm%3AMAMRD:%20modifyAndModifyReturningDifference", MAP_ARG, true) verifyResponse OK, response modifyAndModifyReturningDifference = responseBody().id - GET("terminologies/$targetTerminology//path/te%3A%7Ctm%3ADLO:%20deleteLeftOnly", MAP_ARG, true) + GET("terminologies/$targetTerminology//path/tm%3ADLO:%20deleteLeftOnly", MAP_ARG, true) verifyResponse OK, response deleteLeftOnly = responseBody().id - GET("terminologies/$targetTerminology/path/te%3A%7Ctm%3AMLO:%20modifyLeftOnly", MAP_ARG, true) + GET("terminologies/$targetTerminology/path/tm%3AMLO:%20modifyLeftOnly", MAP_ARG, true) verifyResponse OK, response modifyLeftOnly = responseBody().id 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 0c4f59a501..026cb939c9 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 @@ -1506,23 +1506,23 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { when: //to delete - GET("$source/path/te%3A%7Ctm%3ADLO:%20deleteLeftOnly") + GET("$source/path/tm%3ADLO:%20deleteLeftOnly") verifyResponse OK, response String deleteLeftOnly = responseBody().id - GET("$source/path/te%3A%7Ctm%3ADAM:%20deleteAndModify") + GET("$source/path/tm%3ADAM:%20deleteAndModify") verifyResponse OK, response deleteAndModify = responseBody().id //to modify - GET("$source/path/te%3A%7Ctm%3AMLO:%20modifyLeftOnly") + GET("$source/path/tm%3AMLO:%20modifyLeftOnly") verifyResponse OK, response modifyLeftOnly = responseBody().id - GET("$source/path/te%3A%7Ctm%3ASMLO:%20secondModifyLeftOnly") + GET("$source/path/tm%3ASMLO:%20secondModifyLeftOnly") verifyResponse OK, response secondModifyLeftOnly = responseBody().id - GET("$source/path/te%3A%7Ctm%3AMAD:%20modifyAndDelete") + GET("$source/path/tm%3AMAD:%20modifyAndDelete") verifyResponse OK, response String sourceModifyAndDelete = responseBody().id - GET("$source/path/te%3A%7Ctm%3AMAMRD:%20modifyAndModifyReturningDifference") + GET("$source/path/tm%3AMAMRD:%20modifyAndModifyReturningDifference") verifyResponse OK, response modifyAndModifyReturningDifference = responseBody().id @@ -1616,14 +1616,14 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { when: // for mergeInto json - GET("$target/path/te%3A%7Ctm%3AMAD:%20modifyAndDelete") + GET("$target/path/tm%3AMAD:%20modifyAndDelete") verifyResponse OK, response String targetModifyAndDelete = responseBody().id - GET("$target/path/te%3A%7Ctm%3ADAM:%20deleteAndModify") + GET("$target/path/tm%3ADAM:%20deleteAndModify") verifyResponse OK, response deleteAndModify = responseBody().id - GET("$target/path/te%3A%7Ctm%3AMAMRD:%20modifyAndModifyReturningDifference") + GET("$target/path/tm%3AMAMRD:%20modifyAndModifyReturningDifference") verifyResponse OK, response modifyAndModifyReturningDifference = responseBody().id @@ -1647,13 +1647,13 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { when: // for mergeInto json - GET("$target/path/te%3A%7Ctm%3ADLO:%20deleteLeftOnly") + GET("$target/path/tm%3ADLO:%20deleteLeftOnly") verifyResponse OK, response deleteLeftOnly = responseBody().id - GET("$target/path/te%3A%7Ctm%3AMLO:%20modifyLeftOnly") + GET("$target/path/tm%3AMLO:%20modifyLeftOnly") verifyResponse OK, response modifyLeftOnly = responseBody().id - GET("$target/path/te%3A%7Ctm%3ASMLO:%20secondModifyLeftOnly") + GET("$target/path/tm%3ASMLO:%20secondModifyLeftOnly") verifyResponse OK, response secondModifyLeftOnly = responseBody().id diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy index cd85920ec6..7365e0b132 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy @@ -353,8 +353,7 @@ class PathFunctionalSpec extends FunctionalSpec { //With Terminology ID and no label when: - node = makePathNodes(makePathNode('te', ''), - makePathNode('tm', 'STT01: Simple Test Term 01')) + node = makePathNodes(makePathNode('tm', 'STT01: Simple Test Term 01')) GET("/api/terminologies/${getSimpleTerminologyId()}/path/${makePath(node)}", STRING_ARG) then: "The response is OK" @@ -382,8 +381,7 @@ class PathFunctionalSpec extends FunctionalSpec { //With CodeSet ID and no label when: - node = makePathNodes(makePathNode('cs', ''), - makePathNode('tm', 'STT01: Simple Test Term 01')) + node = makePathNodes(makePathNode('tm', 'STT01: Simple Test Term 01')) GET("/api/codeSets/${getSimpleCodeSetId()}/path/${makePath(node)}") then: "The response is Not Found" @@ -414,8 +412,7 @@ class PathFunctionalSpec extends FunctionalSpec { //With CodeSet ID and no label when: - node = makePathNodes(makePathNode('cs', ''), - makePathNode('tm', 'STT01: Simple Test Term 01')) + node = makePathNodes(makePathNode('tm', 'STT01: Simple Test Term 01')) GET("/api/codeSets/${getSimpleCodeSetId()}/path/${makePath(node)}", STRING_ARG) then: "The response is OK" From e78a23d686826d0ce8f256a0bf652ff589377f90 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Tue, 20 Jul 2021 20:56:33 +0100 Subject: [PATCH 53/82] Improve the run-all-tests.sh script to make it easier to exclude specific tests --- run-all-tests.sh | 77 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/run-all-tests.sh b/run-all-tests.sh index ba3933a619..bfc7969aa0 100755 --- a/run-all-tests.sh +++ b/run-all-tests.sh @@ -1,20 +1,12 @@ #!/bin/zsh -# This executes all the commands Jenkinsfile executes in the same order and format that Jenkins does -# The hope is that we can repeat the tests locally and get the same instability -echo ">> Info <<" -./gradlew -v -./gradlew jvmArgs sysProps jenkinsClean -pushd mdm-core -./gradlew -v -popd -echo ">> Compile <<" -./gradlew --build-cache compile -echo ">> License Check <<" -./gradlew --build-cache license -echo ">> Unit Tests <<" +function unitTest(){ + echo ">> Unit Tests <<" ./gradlew --build-cache test -echo ">> Integration Tests <<" +} + +function integrationTest(){ + echo ">> Integration Tests <<" ./gradlew --build-cache -Dgradle.integrationTest=true \ :mdm-core:integrationTest \ :mdm-plugin-email-proxy:integrationTest \ @@ -24,7 +16,10 @@ echo ">> Integration Tests <<" :mdm-plugin-dataflow:integrationTest \ :mdm-plugin-referencedata:integrationTest \ :mdm-plugin-federation:integrationTest -echo ">> Functional Tests <<" +} + +function functionalTest(){ + echo ">> Functional Tests <<" ./gradlew --build-cache -Dgradle.functionalTest=true \ :mdm-core:integrationTest \ :mdm-plugin-authentication-apikey:integrationTest \ @@ -36,15 +31,49 @@ echo ">> Functional Tests <<" :mdm-plugin-profile:integrationTest \ :mdm-security:integrationTest \ :mdm-plugin-federation:integrationTest -echo ">> E2E Tests <<" -./gradlew --build-cache -Dgradle.test.package=core :mdm-testing-functional:integrationTest -./gradlew --build-cache -Dgradle.test.package=security :mdm-testing-functional:integrationTest -./gradlew --build-cache -Dgradle.test.package=authentication :mdm-testing-functional:integrationTest -./gradlew --build-cache -Dgradle.test.package=datamodel :mdm-testing-functional:integrationTest -./gradlew --build-cache -Dgradle.test.package=terminology :mdm-testing-functional:integrationTest -./gradlew --build-cache -Dgradle.test.package=dataflow :mdm-testing-functional:integrationTest -./gradlew --build-cache -Dgradle.test.package=referencedata :mdm-testing-functional:integrationTest -./gradlew --build-cache -Dgradle.test.package=federation :mdm-testing-functional:integrationTest + +} + +function e2eTest(){ + echo ">> E2E Tests <<" + ./gradlew --build-cache -Dgradle.test.package=core :mdm-testing-functional:integrationTest + ./gradlew --build-cache -Dgradle.test.package=security :mdm-testing-functional:integrationTest + ./gradlew --build-cache -Dgradle.test.package=authentication :mdm-testing-functional:integrationTest + ./gradlew --build-cache -Dgradle.test.package=datamodel :mdm-testing-functional:integrationTest + ./gradlew --build-cache -Dgradle.test.package=terminology :mdm-testing-functional:integrationTest + ./gradlew --build-cache -Dgradle.test.package=dataflow :mdm-testing-functional:integrationTest + ./gradlew --build-cache -Dgradle.test.package=referencedata :mdm-testing-functional:integrationTest + ./gradlew --build-cache -Dgradle.test.package=federation :mdm-testing-functional:integrationTest +} + +function initialReport(){ + echo ">> Info <<" + ./gradlew -v + pushd mdm-core || exit + ./gradlew -v + popd || exit + echo ">> Compile <<" + ./gradlew --build-cache compile + echo ">> License Check <<" + ./gradlew --build-cache license +} + +######################################################################################################## +# This executes all the commands Jenkinsfile executes in the same order and format that Jenkins does +# The hope is that we can repeat the tests locally and get the same instability + +./gradlew jenkinsClean + +initialReport + +unitTest + +integrationTest + +functionalTest + +e2eTest + echo ">> Root Test Report <<" ./gradlew --build-cache rootTestReport #./gradlew --build-cache jacocoTestReport From 48e06e62110808d1dbb768924ed1d1820e93d732 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Tue, 20 Jul 2021 21:58:48 +0100 Subject: [PATCH 54/82] Fix tests following rebase --- .../DataBindImportAndDefaultExporterServiceSpec.groovy | 6 +++--- ...indTerminologyImportAndDefaultExporterServiceSpec.groovy | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy index 1fa11c84d8..76b7451451 100644 --- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy +++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/test/provider/DataBindImportAndDefaultExporterServiceSpec.groovy @@ -243,11 +243,11 @@ abstract class DataBindImportAndDefaultExporterServiceSpec Date: Wed, 21 Jul 2021 09:46:21 +0100 Subject: [PATCH 55/82] Add missing headers --- .../path/PathJsonConverter.groovy | 17 +++++++++++++++++ .../util/UuidJsonConverter.groovy | 17 +++++++++++++++++ .../version/VersionJsonConverter.groovy | 17 +++++++++++++++++ .../maurodatamapper/path/PathNodeSpec.groovy | 17 +++++++++++++++++ .../maurodatamapper/path/PathSpec.groovy | 17 +++++++++++++++++ 5 files changed, 85 insertions(+) diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathJsonConverter.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathJsonConverter.groovy index 4d5e79d994..399e09ca56 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathJsonConverter.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathJsonConverter.groovy @@ -1,3 +1,20 @@ +/* + * 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.path import grails.plugin.json.builder.JsonGenerator diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/UuidJsonConverter.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/UuidJsonConverter.groovy index b43c8b32ab..48b6a36db1 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/UuidJsonConverter.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/UuidJsonConverter.groovy @@ -1,3 +1,20 @@ +/* + * 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.util diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/version/VersionJsonConverter.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/version/VersionJsonConverter.groovy index 39aed91468..fc1a7f3c5a 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/version/VersionJsonConverter.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/version/VersionJsonConverter.groovy @@ -1,3 +1,20 @@ +/* + * 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.version diff --git a/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNodeSpec.groovy b/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNodeSpec.groovy index 5c01dd13a5..ee251fb853 100644 --- a/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNodeSpec.groovy +++ b/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNodeSpec.groovy @@ -1,3 +1,20 @@ +/* + * 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.path import spock.lang.Specification diff --git a/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathSpec.groovy b/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathSpec.groovy index 81da384b4f..969d155d6f 100644 --- a/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathSpec.groovy +++ b/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathSpec.groovy @@ -1,3 +1,20 @@ +/* + * 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.path import spock.lang.Specification From 813a4e75392b4f71ed465eca82d82c7da881eb5d Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Tue, 20 Jul 2021 10:15:16 +0100 Subject: [PATCH 56/82] mc-9584 Reduce time taken to update any catalogue item * Updating any CI was causing checks on facets which only needed to be done on an insert as they update the IDs * Updating any CI was causing checks on the the full BreadcrumbTree, this then recurses into all the children This recurse is unnecessary unless the parent tree has actually changed. --- .../core/facet/BreadcrumbTree.groovy | 8 ++++--- .../core/model/CatalogueItemService.groovy | 21 +++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/BreadcrumbTree.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/BreadcrumbTree.groovy index 3da4390cd8..6a60980283 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/BreadcrumbTree.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/BreadcrumbTree.groovy @@ -37,7 +37,6 @@ class BreadcrumbTree { String domainType String label Boolean finalised - Breadcrumb breadcrumb Boolean topBreadcrumbTree CatalogueItem domainEntity @@ -115,8 +114,11 @@ class BreadcrumbTree { markDirty('label', domainEntity.label, getOriginalValue('label')) } checkTree() - children?.each { - it.beforeValidate() + // After checking the tree, if its changed (or we havent been saved before) then we will need to update all the children + if (!id || isDirty('treeString')) { + children?.each { + it.beforeValidate() + } } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy index c2cb5ebb56..31320e2719 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy @@ -67,15 +67,22 @@ abstract class CatalogueItemService implements DomainSe abstract void deleteAll(Collection catalogueItems) K save(Map args, K catalogueItem) { + // If inserting then we will need to update all the facets with the CIs "id" after insert + // If updating then we dont need to do this as the ID has already been done + boolean inserting = !(catalogueItem as GormEntity).ident() Map saveArgs = new HashMap(args) if (args.flush) { saveArgs.remove('flush') (catalogueItem as GormEntity).save(saveArgs) - updateFacetsAfterInsertingCatalogueItem(catalogueItem) + if (inserting) updateFacetsAfterInsertingCatalogueItem(catalogueItem) + // We do need to ensure the BT hasnt changed (e.g. a move of a MI inside an M) + checkBreadcrumbTreeAfterSavingCatalogueItem(catalogueItem) sessionFactory.currentSession.flush() } else { (catalogueItem as GormEntity).save(args) - updateFacetsAfterInsertingCatalogueItem(catalogueItem) + if (inserting) updateFacetsAfterInsertingCatalogueItem(catalogueItem) + // We do need to ensure the BT hasnt changed (e.g. a move of a MI inside an M) + checkBreadcrumbTreeAfterSavingCatalogueItem(catalogueItem) } catalogueItem } @@ -149,7 +156,7 @@ abstract class CatalogueItemService implements DomainSe copy.addToRules(copiedRule) } - semanticLinkService.findAllBySourceMultiFacetAwareItemId(original.id).each { link -> + semanticLinkService.findAllBySourceMultiFacetAwareItemId(original.id).each {link -> copy.addToSemanticLinks(createdBy: copier.emailAddress, linkType: link.linkType, targetMultiFacetAwareItemId: link.targetMultiFacetAwareItemId, targetMultiFacetAwareItemDomainType: link.targetMultiFacetAwareItemDomainType, @@ -163,12 +170,14 @@ abstract class CatalogueItemService implements DomainSe source.addToSemanticLinks(linkType: SemanticLinkType.REFINES, createdBy: catalogueUser.emailAddress, targetMultiFacetAwareItem: target) } - K updateFacetsAfterInsertingCatalogueItem(K catalogueItem) { - updateFacetsAfterInsertingMultiFacetAware(catalogueItem) + void checkBreadcrumbTreeAfterSavingCatalogueItem(K catalogueItem) { catalogueItem.breadcrumbTree?.trackChanges() catalogueItem.breadcrumbTree?.beforeValidate() catalogueItem.breadcrumbTree?.save(validate: false) - catalogueItem + } + + K updateFacetsAfterInsertingCatalogueItem(K catalogueItem) { + updateFacetsAfterInsertingMultiFacetAware(catalogueItem) } K checkFacetsAfterImportingCatalogueItem(K catalogueItem) { From 8c431ce269ccfce1f4b553e39d5fe90f7e55c417 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Wed, 21 Jul 2021 10:24:04 +0100 Subject: [PATCH 57/82] Make sure the correct breadcrumb tree checks happen when they need to --- .../core/facet/BreadcrumbTree.groovy | 3 ++- .../core/model/ModelItemService.groovy | 24 ++++++++++--------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/BreadcrumbTree.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/BreadcrumbTree.groovy index 6a60980283..022018f9a5 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/BreadcrumbTree.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/BreadcrumbTree.groovy @@ -115,8 +115,9 @@ class BreadcrumbTree { } checkTree() // After checking the tree, if its changed (or we havent been saved before) then we will need to update all the children - if (!id || isDirty('treeString')) { + if (isDirty('treeString')) { children?.each { + it.buildTree() it.beforeValidate() } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy index 3b5935261c..9aa3519188 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelItemService.groovy @@ -78,7 +78,7 @@ abstract class ModelItemService extends CatalogueItemServic modelItemService = this parentId = targetModelItem.id } else { - modelItemService = modelItemServices.find {it.handles(mergeFieldDiff.fieldName)} + modelItemService = modelItemServices.find { it.handles(mergeFieldDiff.fieldName) } parentId = null } @@ -128,14 +128,14 @@ abstract class ModelItemService extends CatalogueItemServic def saveAll(Collection modelItems, boolean batching = true) { - List classifiers = modelItems.collectMany {it.classifiers ?: []} as List + List classifiers = modelItems.collectMany { it.classifiers ?: [] } as List if (classifiers) { log.trace('Saving {} classifiers') classifierService.saveAll(classifiers) } - Collection alreadySaved = modelItems.findAll {it.ident() && it.isDirty()} - Collection notSaved = modelItems.findAll {!it.ident()} + Collection alreadySaved = modelItems.findAll { it.ident() && it.isDirty() } + Collection notSaved = modelItems.findAll { !it.ident() } if (alreadySaved) { log.debug('Straight saving {} already saved {}', alreadySaved.size(), getModelItemClass().simpleName) @@ -148,7 +148,7 @@ abstract class ModelItemService extends CatalogueItemServic List batch = [] int count = 0 - notSaved.each {mi -> + notSaved.each { mi -> batch << mi count++ @@ -161,9 +161,10 @@ abstract class ModelItemService extends CatalogueItemServic batch.clear() } else { log.debug('Straight saving {} new {}', notSaved.size(), getModelItemClass().simpleName) - notSaved.each {dt -> - save(flush: false, validate: false, dt) - updateFacetsAfterInsertingCatalogueItem(dt) + notSaved.each { mi -> + save(flush: false, validate: false, mi) + updateFacetsAfterInsertingCatalogueItem(mi) + checkBreadcrumbTreeAfterSavingCatalogueItem(mi) } } } @@ -172,10 +173,11 @@ abstract class ModelItemService extends CatalogueItemServic void batchSave(List modelItems) { long start = System.currentTimeMillis() log.debug('Performing batch save of {} {}', modelItems.size(), getModelItemClass().simpleName) - + List inserts = modelItems.collect { !it.id } getModelItemClass().saveAll(modelItems) - modelItems.each {dt -> - updateFacetsAfterInsertingCatalogueItem(dt) + modelItems.eachWithIndex { mi, i -> + if (inserts[i]) updateFacetsAfterInsertingCatalogueItem(mi) + checkBreadcrumbTreeAfterSavingCatalogueItem(mi) } sessionFactory.currentSession.flush() From 2cd7cd884580b720a66e37d5ae62310e1267b15b Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Wed, 21 Jul 2021 15:25:44 +0100 Subject: [PATCH 58/82] Handle copying dataclasses and facets with the changes --- .../maurodatamapper/core/model/CatalogueItemService.groovy | 2 +- .../datamodel/item/DataClassController.groovy | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy index 31320e2719..b4488051e5 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItemService.groovy @@ -69,7 +69,7 @@ abstract class CatalogueItemService implements DomainSe K save(Map args, K catalogueItem) { // If inserting then we will need to update all the facets with the CIs "id" after insert // If updating then we dont need to do this as the ID has already been done - boolean inserting = !(catalogueItem as GormEntity).ident() + boolean inserting = !(catalogueItem as GormEntity).ident() ?: args.insert Map saveArgs = new HashMap(args) if (args.flush) { saveArgs.remove('flush') diff --git a/mdm-plugin-datamodel/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassController.groovy b/mdm-plugin-datamodel/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassController.groovy index 2abaab3c33..bfe3e1e8ce 100644 --- a/mdm-plugin-datamodel/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassController.groovy +++ b/mdm-plugin-datamodel/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataClassController.groovy @@ -20,11 +20,11 @@ package uk.ac.ox.softeng.maurodatamapper.datamodel.item import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInvalidModelException import uk.ac.ox.softeng.maurodatamapper.core.controller.CatalogueItemController import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.CopyInformation import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.search.SearchParams import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModelService import uk.ac.ox.softeng.maurodatamapper.datamodel.SearchService -import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.CopyInformation import uk.ac.ox.softeng.maurodatamapper.search.PaginatedLuceneResult import grails.gorm.transactions.Transactional @@ -270,7 +270,9 @@ class DataClassController extends CatalogueItemController { @Override protected void serviceInsertResource(DataClass resource) { - dataClassService.save([flush: true, validate: false, deepSave: actionName == 'copyDataClass', saveDataTypes: actionName == 'copyDataClass'], + dataClassService.save([flush : true, validate: false, insert: actionName == 'copyDataClass', + deepSave : actionName == 'copyDataClass', + saveDataTypes: actionName == 'copyDataClass'], resource) } From fa1a65a8e75c687a94051857323b711e36d12b9f Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Wed, 21 Jul 2021 20:57:22 +0100 Subject: [PATCH 59/82] mc-9586 Only conceivable fix The only reason i can think the exception is being thrown is the use of list() on the domain class which plays weirdly with pagedresultlist class. Change to the detached criteria which we use everywhere else. --- .../federation/SubscribedCatalogueService.groovy | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 9e231d5585..db703a5630 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 @@ -51,8 +51,8 @@ class SubscribedCatalogueService implements XmlImportMapping { SubscribedCatalogue.get(id) } - List list(Map pagination) { - pagination ? SubscribedCatalogue.list(pagination) : SubscribedCatalogue.list() + List list(Map pagination = [:]) { + SubscribedCatalogue.by().list(pagination) } Long count() { @@ -106,7 +106,7 @@ class SubscribedCatalogueService implements XmlImportMapping { * 4. Return the list of AvailableModel, in order that this can be rendered as json * * @param subscribedCatalogue The catalogue we want to query - * @return List The list of available models returned by the catalogue + * @return List The list of available models returned by the catalogue * */ List listPublishedModels(SubscribedCatalogue subscribedCatalogue) { @@ -117,7 +117,7 @@ class SubscribedCatalogueService implements XmlImportMapping { if (subscribedCatalogueModels.publishedModels.isEmpty()) return [] - (subscribedCatalogueModels.publishedModels as List>).collect { pm -> + (subscribedCatalogueModels.publishedModels as List>).collect {pm -> new PublishedModel().tap { modelId = Utils.toUuid(pm.id) title = pm.title From 43010b1c0d6cbdca2c72523d83dc6ce8ff673a30 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Thu, 22 Jul 2021 21:21:46 +0100 Subject: [PATCH 60/82] gh-106 Fix the deletion of a profile * Define a profile service method to properly delete and use the metadata service to do it * Update the way validation and store profile work by binding to a new profile the submitted data to make it easier to retrieve and match * Update the saving and storing of a profile to make better use of the session and to handle removal of profile fields without flush/save issues * Add a series of functional tests to prove all this works against a real profile --- .../profile/ProfileController.groovy | 75 +++-- .../profile/UrlMappings.groovy | 2 +- .../profile/ProfileService.groovy | 65 ++-- .../profile/ProfileFunctionalSpec.groovy | 301 +++++++++++++++++- .../profile/domain/ProfileField.groovy | 3 - .../profile/domain/ProfileSection.groovy | 5 + .../profile/object/Profile.groovy | 7 + .../JsonProfileProviderService.groovy | 22 +- .../provider/ProfileProviderService.groovy | 5 + 9 files changed, 390 insertions(+), 95 deletions(-) diff --git a/mdm-plugin-profile/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/profile/ProfileController.groovy b/mdm-plugin-profile/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/profile/ProfileController.groovy index 32982b1ae8..2a5f16e0cf 100644 --- a/mdm-plugin-profile/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/profile/ProfileController.groovy +++ b/mdm-plugin-profile/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/profile/ProfileController.groovy @@ -35,6 +35,8 @@ import groovy.util.logging.Slf4j import org.hibernate.SessionFactory import org.springframework.beans.factory.annotation.Autowired +import static org.springframework.http.HttpStatus.NO_CONTENT + @Slf4j class ProfileController implements ResourcelessMdmController { static responseFormats = ['json', 'xml'] @@ -82,13 +84,9 @@ class ProfileController implements ResourcelessMdmController { return notFound(params.multiFacetAwareItemClass, params.multiFacetAwareItemId) } Set usedProfiles = profileService.getUsedProfileServices(multiFacetAware) - Set profileNamespaces = usedProfiles.collect{it.metadataNamespace} - respond metadataService.findAllByMultiFacetAwareItemIdAndNotNamespaces(multiFacetAware.id, profileNamespaces.asList(),params), + Set profileNamespaces = usedProfiles.collect {it.metadataNamespace} + respond metadataService.findAllByMultiFacetAwareItemIdAndNotNamespaces(multiFacetAware.id, profileNamespaces.asList(), params), view: "/metadata/index" - -// respond(view: "/metadata/index", -// model: [metadataList: metadataService.findAllByMultiFacetAwareItemIdAndNotNamespaces(multiFacetAware.id, profileNamespaces.asList(), -// params)]) } @Transactional @@ -105,14 +103,11 @@ class ProfileController implements ResourcelessMdmController { return notFound(ProfileProviderService, getProfileProviderServiceId(params)) } - Set mds = - multiFacetAware.metadata - .findAll{ it.namespace == profileProviderService.metadataNamespace } + profileService.deleteProfile(profileProviderService, multiFacetAware, currentUser) - mds.each {md -> - //multiFacetAware.metadata.remove(md) - metadataService.delete(md, true) - metadataService.addDeletedEditToMultiFacetAwareItem(currentUser, md, params.multiFacetAwareItemDomainType, params.multiFacetAwareItemId)} + request.withFormat { + '*' {render status: NO_CONTENT} // NO CONTENT STATUS CODE + } } def show() { @@ -149,31 +144,32 @@ class ProfileController implements ResourcelessMdmController { return notFound(ProfileProviderService, getProfileProviderServiceId(params)) } - profileService.storeProfile(profileProviderService, multiFacetAware, request, currentUser) + Profile instance = profileProviderService.getNewProfile() + bindData(instance, request) - // Flush the profile before we create as the create method retrieves whatever is stored in the database - sessionFactory.currentSession.flush() + MultiFacetAware profiled = profileService.storeProfile(profileProviderService, multiFacetAware, instance, currentUser) // Create the profile as the stored profile may only be segments of the profile and we now want to get everything - respond profileService.createProfile(profileProviderService, multiFacetAware) + respond profileService.createProfile(profileProviderService, profiled) } def validate() { - log.debug("validating profile...") - MultiFacetAware multiFacetAware = - profileService.findMultiFacetAwareItemByDomainTypeAndId(params.multiFacetAwareItemDomainType, params.multiFacetAwareItemId) + log.debug("Validating profile") + MultiFacetAware multiFacetAware = profileService.findMultiFacetAwareItemByDomainTypeAndId(params.multiFacetAwareItemDomainType, params.multiFacetAwareItemId) if (!multiFacetAware) { return notFound(params.multiFacetAwareItemClass, params.multiFacetAwareItemId) } - ProfileProviderService profileProviderService = profileService.findProfileProviderService(params.profileNamespace, params.profileName, - params.profileVersion) + ProfileProviderService profileProviderService = profileService.findProfileProviderService(params.profileNamespace, params.profileName, params.profileVersion) if (!profileProviderService) { return notFound(ProfileProviderService, getProfileProviderServiceId(params)) } - respond profileService.validateProfile(profileProviderService, request) + Profile submittedInstance = profileProviderService.getNewProfile() + bindData(submittedInstance, request) + + respond profileService.validateProfile(profileProviderService, submittedInstance) } @@ -184,13 +180,13 @@ class ProfileController implements ResourcelessMdmController { return notFound(ProfileProviderService, getProfileProviderServiceId(params)) } PaginatedResultList profiles = - profileService.getModelsWithProfile(profileProviderService, currentUserSecurityPolicyManager, params.multiFacetAwareItemDomainType, params) + profileService.getModelsWithProfile(profileProviderService, currentUserSecurityPolicyManager, params.multiFacetAwareItemDomainType, params) respond profileList: profiles } def listValuesInProfile() { ProfileProviderService profileProviderService = - profileService.findProfileProviderService(params.profileNamespace, params.profileName, params.profileVersion) + profileService.findProfileProviderService(params.profileNamespace, params.profileName, params.profileVersion) if (!profileProviderService) { return notFound(ProfileProviderService, getProfileProviderServiceId(params)) } @@ -198,26 +194,27 @@ class ProfileController implements ResourcelessMdmController { List profiledItems = profileProviderService.findAllProfiledItems(params.multiFacetAwareItemDomainType) List filteredProfiledItems = [] profiledItems.each {profiledItem -> - if(profiledItem instanceof Model - && currentUserSecurityPolicyManager.userCanReadSecuredResourceId(profiledItem.getClass(), profiledItem.id)) { - filteredProfiledItems.add(profiledItem) + if (profiledItem instanceof Model + && currentUserSecurityPolicyManager.userCanReadSecuredResourceId(profiledItem.getClass(), profiledItem.id)) { + filteredProfiledItems.add(profiledItem) } else if (profiledItem instanceof ModelItem) { Model model = proxyHandler.unwrapIfProxy(profiledItem.getModel()) - if(currentUserSecurityPolicyManager.userCanReadResourceId(profiledItem.getClass(), profiledItem.id, model.getClass(), model.id)) { - filteredProfiledItems.add(profiledItem) - } + if (currentUserSecurityPolicyManager.userCanReadResourceId(profiledItem.getClass(), profiledItem.id, model.getClass(), model.id)) { + filteredProfiledItems.add(profiledItem) + } } } Map> allValuesMap = [:] - profileProviderService.getKnownMetadataKeys().findAll{key -> (!params.filter || params.filter.contains(key))}.each { key -> + profileProviderService.getKnownMetadataKeys().findAll {key -> (!params.filter || params.filter.contains(key))}.each {key -> Set allValues = new HashSet(); - filteredProfiledItems.each { profiledItem -> + filteredProfiledItems.each {profiledItem -> Metadata md = profiledItem.metadata.find { it.namespace == profileProviderService.metadataNamespace && - it.key == key } - if(md) { + it.key == key + } + if (md) { allValues.add(md.value) } } @@ -244,10 +241,10 @@ class ProfileController implements ResourcelessMdmController { return notFound(ProfileProviderService, getProfileProviderServiceId(params)) } -/* if (!(profileProviderService instanceof DataModelProfileProviderService)) { - throw new ApiNotYetImplementedException('PCXX', 'Non-DataModel Based searching in profiles') - } -*/ + /* if (!(profileProviderService instanceof DataModelProfileProviderService)) { + throw new ApiNotYetImplementedException('PCXX', 'Non-DataModel Based searching in profiles') + } + */ searchParams.searchTerm = searchParams.searchTerm ?: params.search searchParams.offset = 0 searchParams.max = null diff --git a/mdm-plugin-profile/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/profile/UrlMappings.groovy b/mdm-plugin-profile/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/profile/UrlMappings.groovy index 7038ee2c54..7b7de0c48b 100644 --- a/mdm-plugin-profile/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/profile/UrlMappings.groovy +++ b/mdm-plugin-profile/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/profile/UrlMappings.groovy @@ -38,9 +38,9 @@ class UrlMappings { // Provide multiple ways to obtain profile of a multiFacetAware get "/${multiFacetAwareItemDomainType}/${multiFacetAwareItemId}"(controller: 'profile', action: 'show') - post "/$multiFacetAwareItemDomainType/$multiFacetAwareItemId"(controller: 'profile', action: 'save') post "/$multiFacetAwareItemDomainType/$multiFacetAwareItemId/validate"(controller: 'profile', action: 'validate') + delete "/$multiFacetAwareItemDomainType/$multiFacetAwareItemId"(controller: 'profile', action: 'delete') } } diff --git a/mdm-plugin-profile/grails-app/services/uk/ac/ox/softeng/maurodatamapper/profile/ProfileService.groovy b/mdm-plugin-profile/grails-app/services/uk/ac/ox/softeng/maurodatamapper/profile/ProfileService.groovy index 672a9b34df..364906f336 100644 --- a/mdm-plugin-profile/grails-app/services/uk/ac/ox/softeng/maurodatamapper/profile/ProfileService.groovy +++ b/mdm-plugin-profile/grails-app/services/uk/ac/ox/softeng/maurodatamapper/profile/ProfileService.groovy @@ -29,7 +29,6 @@ import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModelService import uk.ac.ox.softeng.maurodatamapper.gorm.PaginatedResultList import uk.ac.ox.softeng.maurodatamapper.profile.domain.ProfileField import uk.ac.ox.softeng.maurodatamapper.profile.domain.ProfileSection -import uk.ac.ox.softeng.maurodatamapper.profile.object.JsonProfile import uk.ac.ox.softeng.maurodatamapper.profile.object.Profile import uk.ac.ox.softeng.maurodatamapper.profile.provider.DynamicJsonProfileProviderService import uk.ac.ox.softeng.maurodatamapper.profile.provider.ProfileProviderService @@ -37,10 +36,9 @@ import uk.ac.ox.softeng.maurodatamapper.security.User import uk.ac.ox.softeng.maurodatamapper.security.UserSecurityPolicyManager import grails.gorm.transactions.Transactional +import org.hibernate.SessionFactory import org.springframework.beans.factory.annotation.Autowired -import javax.servlet.http.HttpServletRequest - @Transactional class ProfileService { @@ -56,6 +54,7 @@ class ProfileService { DataModelService dataModelService MetadataService metadataService ProfileSpecificationProfileService profileSpecificationProfileService + SessionFactory sessionFactory Profile createProfile(ProfileProviderService profileProviderService, MultiFacetAware multiFacetAwareItem) { profileProviderService.createProfileFromEntity(multiFacetAwareItem) @@ -76,49 +75,37 @@ class ProfileService { }.max() } - void storeProfile(ProfileProviderService profileProviderService, MultiFacetAware multiFacetAwareItem, HttpServletRequest request, User user) { - - Profile profile = profileProviderService.getNewProfile() - if (profileProviderService.isJsonProfileService()) { - profile.sections.each {section -> - ProfileSection submittedSection = request.getJSON().sections.find {it.sectionName == section.sectionName} - if (submittedSection) { - section.fields.each {field -> - ProfileField submittedField = submittedSection.fields.find {it.fieldName == field.fieldName} - field.currentValue = submittedField.currentValue ?: "" - } - } - } - } - else if (!profileProviderService.isJsonProfileService()) { - profile.fromSections(request.getJSON().sections) - } - - /*final DataBindingSource bindingSource = DataBindingUtils.createDataBindingSource(grailsApplication, profile.getClass(), request) - bindingSource.propertyNames.each { propertyName -> - profile.setField(propertyName, bindingSource[propertyName]) - } - */ - profileProviderService.storeProfileInEntity(multiFacetAwareItem, profile, user) + MultiFacetAware storeProfile(ProfileProviderService profileProviderService, MultiFacetAware multiFacetAwareItem, Profile profileToStore, User user) { + profileProviderService.storeProfileInEntity(multiFacetAwareItem, profileToStore, user) + MultiFacetAwareService service = multiFacetAwareServices.find {it.handles(multiFacetAwareItem.domainType)} + if (!service) throw new ApiBadRequestException('CIAS02', "Facet retrieval for catalogue item [${multiFacetAwareItem.domainType}] with no supporting service") + service.save(flush: true, validate: false, multiFacetAwareItem) } - def validateProfile(ProfileProviderService profileProviderService, HttpServletRequest request) { - Profile profile = profileProviderService.getNewProfile() - profile.sections.each {section -> - ProfileSection submittedSection = request.getJSON().sections.find {it.sectionName == section.sectionName} + Profile validateProfile(ProfileProviderService profileProviderService, Profile submittedProfile) { + Profile cleanProfile = profileProviderService.newProfile + + cleanProfile.sections.each {section -> + ProfileSection submittedSection = submittedProfile.sections.find {it.sectionName == section.sectionName} if (submittedSection) { section.fields.each {field -> ProfileField submittedField = submittedSection.fields.find {it.fieldName == field.fieldName} - field.currentValue = submittedField.currentValue ?: "" + field.currentValue = submittedField.currentValue ?: '' } } } - profile.sections.each {section -> - section.fields.each {field -> - field.validate() - } + cleanProfile.validate() + } + + void deleteProfile(ProfileProviderService profileProviderService, MultiFacetAware multiFacetAwareItem, User currentUser) { + + Set mds = profileProviderService.getAllProfileMetadataByMultiFacetAwareItemId(multiFacetAwareItem.id) + + mds.each {md -> + metadataService.delete(md) + metadataService.addDeletedEditToMultiFacetAwareItem(currentUser, md, multiFacetAwareItem.domainType, multiFacetAwareItem.id) } - profile + sessionFactory.currentSession.flush() } @@ -150,10 +137,6 @@ class ProfileService { new PaginatedResultList<>(profiles, pagination) } - MultiFacetAware findMultiFacetAwareByDomainTypeAndId(String domainType, String multiFacetAwareItemIdString) { - findMultiFacetAwareItemByDomainTypeAndId(domainType, UUID.fromString(multiFacetAwareItemIdString)) - } - MultiFacetAware findMultiFacetAwareItemByDomainTypeAndId(String domainType, UUID multiFacetAwareItemId) { MultiFacetAwareService service = multiFacetAwareServices.find {it.handles(domainType)} if (!service) throw new ApiBadRequestException('CIAS02', "Facet retrieval for catalogue item [${domainType}] with no supporting service") diff --git a/mdm-plugin-profile/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/profile/ProfileFunctionalSpec.groovy b/mdm-plugin-profile/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/profile/ProfileFunctionalSpec.groovy index 45f6242cbd..19be9a3b8a 100644 --- a/mdm-plugin-profile/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/profile/ProfileFunctionalSpec.groovy +++ b/mdm-plugin-profile/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/profile/ProfileFunctionalSpec.groovy @@ -34,9 +34,8 @@ import io.micronaut.http.HttpStatus import spock.lang.Shared import static uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress.FUNCTIONAL_TEST -import static uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress.getDEVELOPMENT -import static uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress.getFUNCTIONAL_TEST +import static io.micronaut.http.HttpStatus.NO_CONTENT import static io.micronaut.http.HttpStatus.OK @Slf4j @@ -52,6 +51,8 @@ class ProfileFunctionalSpec extends BaseFunctionalSpec { @Shared UUID simpleDataModelId + ProfileSpecificationProfileService profileSpecificationProfileService + @OnceBefore @Transactional def checkAndSetupData() { @@ -335,4 +336,300 @@ class ProfileFunctionalSpec extends BaseFunctionalSpec { ]} ''' } + + void 'N01 : test validating profile on DataModel'() { + given: + Map namespaceFieldMap = [ + currentValue : '', + metadataPropertyName: 'metadataNamespace', + dataType : 'string', + fieldName : 'Metadata namespace', + validationErrors : [], + regularExpression : null, + allowedValues : null, + description : 'The namespace under which properties of this profile will be stored', + minMultiplicity : 1, + maxMultiplicity : 1 + ] + Map domainsFieldMap = [ + currentValue : '', + metadataPropertyName: 'domainsApplicable', + dataType : 'string', + fieldName : 'Applicable for domains', + validationErrors : [], + regularExpression : null, + allowedValues : null, + description : "Determines which types of catalogue item can be profiled using this profile. For example, 'DataModel'. " + + "Separate multiple domains with a semi-colon (';'). Leave blank to allow this profile to be applicable to any catalogue item.", + minMultiplicity : 0, + maxMultiplicity : 1 + ] + Map profileMap = [ + sections : [ + [ + sectionDescription: 'The details necessary for this Data Model to be used as the specification for a dynamic profile.', + fields : [ + namespaceFieldMap, + domainsFieldMap + ], + sectionName : 'Profile Specification' + ] + ], + id : simpleDataModelId.toString(), + label : 'Simple Test DataModel', + domainType: 'DataModel', + namespace : profileSpecificationProfileService.namespace, + name : profileSpecificationProfileService.name + + ] + + when: + POST("profiles/${profileSpecificationProfileService.namespace}/${profileSpecificationProfileService.name}/dataModels/${simpleDataModelId}/validate", profileMap) + + then: + verifyResponse(OK, response) + responseBody().sections.first().fields.find {it.fieldName == namespaceFieldMap.fieldName}.validationErrors == ['This field is mandatory'] + responseBody().sections.first().fields.find {it.fieldName == domainsFieldMap.fieldName}.validationErrors.isEmpty() + + when: + namespaceFieldMap.currentValue = 'functional.test.profile' + domainsFieldMap.currentValue = 'DataModel' + + POST("profiles/${profileSpecificationProfileService.namespace}/${profileSpecificationProfileService.name}/dataModels/${simpleDataModelId}/validate", profileMap) + + then: + verifyResponse(OK, response) + responseBody().sections.first().fields.find {it.fieldName == namespaceFieldMap.fieldName}.validationErrors.isEmpty() + responseBody().sections.first().fields.find {it.fieldName == domainsFieldMap.fieldName}.validationErrors.isEmpty() + } + + void 'N02 : test saving profile'() { + given: + Map namespaceFieldMap = [ + currentValue : 'functional.test.profile', + metadataPropertyName: 'metadataNamespace', + dataType : 'string', + fieldName : 'Metadata namespace', + validationErrors : [], + regularExpression : null, + allowedValues : null, + description : 'The namespace under which properties of this profile will be stored', + minMultiplicity : 1, + maxMultiplicity : 1 + ] + Map domainsFieldMap = [ + currentValue : 'DataModel', + metadataPropertyName: 'domainsApplicable', + dataType : 'string', + fieldName : 'Applicable for domains', + validationErrors : [], + regularExpression : null, + allowedValues : null, + description : "Determines which types of catalogue item can be profiled using this profile. For example, 'DataModel'. " + + "Separate multiple domains with a semi-colon (';'). Leave blank to allow this profile to be applicable to any catalogue item.", + minMultiplicity : 0, + maxMultiplicity : 1 + ] + Map profileMap = [ + sections : [ + [ + sectionDescription: 'The details necessary for this Data Model to be used as the specification for a dynamic profile.', + fields : [ + namespaceFieldMap, + domainsFieldMap + ], + sectionName : 'Profile Specification' + ] + ], + id : simpleDataModelId.toString(), + label : 'Simple Test DataModel', + domainType: 'DataModel', + namespace : profileSpecificationProfileService.namespace, + name : profileSpecificationProfileService.name + + ] + + when: + POST("profiles/${profileSpecificationProfileService.namespace}/${profileSpecificationProfileService.name}/dataModels/${simpleDataModelId}", profileMap) + + then: + verifyResponse(OK, response) + responseBody().sections.first().fields.find {it.fieldName == namespaceFieldMap.fieldName}.currentValue == namespaceFieldMap.currentValue + responseBody().sections.first().fields.find {it.fieldName == domainsFieldMap.fieldName}.currentValue == domainsFieldMap.currentValue + + when: + HttpResponse> localResponse = GET("dataModels/${simpleDataModelId}/profiles/used", Argument.listOf(Map)) + + then: + verifyResponse(OK, localResponse) + localResponse.body().size() == 1 + localResponse.body().first().name == profileSpecificationProfileService.name + localResponse.body().first().namespace == profileSpecificationProfileService.namespace + } + + void 'N03 : test editing profile'() { + given: + Map namespaceFieldMap = [ + currentValue : 'functional.test.profile', + metadataPropertyName: 'metadataNamespace', + dataType : 'string', + fieldName : 'Metadata namespace', + validationErrors : [], + regularExpression : null, + allowedValues : null, + description : 'The namespace under which properties of this profile will be stored', + minMultiplicity : 1, + maxMultiplicity : 1 + ] + Map domainsFieldMap = [ + currentValue : 'DataModel', + metadataPropertyName: 'domainsApplicable', + dataType : 'string', + fieldName : 'Applicable for domains', + validationErrors : [], + regularExpression : null, + allowedValues : null, + description : "Determines which types of catalogue item can be profiled using this profile. For example, 'DataModel'. " + + "Separate multiple domains with a semi-colon (';'). Leave blank to allow this profile to be applicable to any catalogue item.", + minMultiplicity : 0, + maxMultiplicity : 1 + ] + Map profileMap = [ + sections : [ + [ + sectionDescription: 'The details necessary for this Data Model to be used as the specification for a dynamic profile.', + fields : [ + namespaceFieldMap, + domainsFieldMap + ], + sectionName : 'Profile Specification' + ] + ], + id : simpleDataModelId.toString(), + label : 'Simple Test DataModel', + domainType: 'DataModel', + namespace : profileSpecificationProfileService.namespace, + name : profileSpecificationProfileService.name + + ] + POST("profiles/${profileSpecificationProfileService.namespace}/${profileSpecificationProfileService.name}/dataModels/${simpleDataModelId}", profileMap) + verifyResponse(OK, response) + + + when: + namespaceFieldMap.currentValue = 'functional.test.profile.adjusted' + profileMap = [ + sections : [ + [ + sectionDescription: 'The details necessary for this Data Model to be used as the specification for a dynamic profile.', + fields : [ + namespaceFieldMap, + ], + sectionName : 'Profile Specification' + ] + ], + id : simpleDataModelId.toString(), + label : 'Simple Test DataModel', + domainType: 'DataModel', + namespace : profileSpecificationProfileService.namespace, + name : profileSpecificationProfileService.name + + ] + POST("profiles/${profileSpecificationProfileService.namespace}/${profileSpecificationProfileService.name}/dataModels/${simpleDataModelId}", profileMap) + verifyResponse(OK, response) + + then: + responseBody().sections.first().fields.find {it.fieldName == namespaceFieldMap.fieldName}.currentValue == 'functional.test.profile.adjusted' + responseBody().sections.first().fields.find {it.fieldName == domainsFieldMap.fieldName}.currentValue == domainsFieldMap.currentValue + + when: + domainsFieldMap.currentValue = '' + profileMap = [ + sections : [ + [ + sectionDescription: 'The details necessary for this Data Model to be used as the specification for a dynamic profile.', + fields : [ + domainsFieldMap, + ], + sectionName : 'Profile Specification' + ] + ], + id : simpleDataModelId.toString(), + label : 'Simple Test DataModel', + domainType: 'DataModel', + namespace : profileSpecificationProfileService.namespace, + name : profileSpecificationProfileService.name + + ] + POST("profiles/${profileSpecificationProfileService.namespace}/${profileSpecificationProfileService.name}/dataModels/${simpleDataModelId}", profileMap) + verifyResponse(OK, response) + + then: + responseBody().sections.first().fields.find {it.fieldName == namespaceFieldMap.fieldName}.currentValue == 'functional.test.profile.adjusted' + responseBody().sections.first().fields.find {it.fieldName == domainsFieldMap.fieldName}.currentValue == '' + + } + + void 'N04 : test deleting profile'() { + given: + Map namespaceFieldMap = [ + currentValue : 'functional.test.profile', + metadataPropertyName: 'metadataNamespace', + dataType : 'string', + fieldName : 'Metadata namespace', + validationErrors : [], + regularExpression : null, + allowedValues : null, + description : 'The namespace under which properties of this profile will be stored', + minMultiplicity : 1, + maxMultiplicity : 1 + ] + Map domainsFieldMap = [ + currentValue : 'DataModel', + metadataPropertyName: 'domainsApplicable', + dataType : 'string', + fieldName : 'Applicable for domains', + validationErrors : [], + regularExpression : null, + allowedValues : null, + description : "Determines which types of catalogue item can be profiled using this profile. For example, 'DataModel'. " + + "Separate multiple domains with a semi-colon (';'). Leave blank to allow this profile to be applicable to any catalogue item.", + minMultiplicity : 0, + maxMultiplicity : 1 + ] + Map profileMap = [ + sections : [ + [ + sectionDescription: 'The details necessary for this Data Model to be used as the specification for a dynamic profile.', + fields : [ + namespaceFieldMap, + domainsFieldMap + ], + sectionName : 'Profile Specification' + ] + ], + id : simpleDataModelId.toString(), + label : 'Simple Test DataModel', + domainType: 'DataModel', + namespace : profileSpecificationProfileService.namespace, + name : profileSpecificationProfileService.name + + ] + POST("profiles/${profileSpecificationProfileService.namespace}/${profileSpecificationProfileService.name}/dataModels/${simpleDataModelId}", profileMap) + verifyResponse(OK, response) + + + when: + DELETE("profiles/${profileSpecificationProfileService.namespace}/${profileSpecificationProfileService.name}/dataModels/${simpleDataModelId}") + + then: + verifyResponse(NO_CONTENT, response) + + when: + HttpResponse> localResponse = GET("dataModels/${simpleDataModelId}/profiles/used", Argument.listOf(Map)) + + then: + verifyResponse(OK, localResponse) + localResponse.body().isEmpty() + } } diff --git a/mdm-plugin-profile/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/profile/domain/ProfileField.groovy b/mdm-plugin-profile/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/profile/domain/ProfileField.groovy index 26a54319a3..cffb1acd6b 100644 --- a/mdm-plugin-profile/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/profile/domain/ProfileField.groovy +++ b/mdm-plugin-profile/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/profile/domain/ProfileField.groovy @@ -17,9 +17,6 @@ */ package uk.ac.ox.softeng.maurodatamapper.profile.domain -import grails.rest.Resource - -@Resource(readOnly = false, formats = ['json', 'xml']) class ProfileField { String fieldName diff --git a/mdm-plugin-profile/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/profile/domain/ProfileSection.groovy b/mdm-plugin-profile/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/profile/domain/ProfileSection.groovy index 4d89ae77ce..45290db97b 100644 --- a/mdm-plugin-profile/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/profile/domain/ProfileSection.groovy +++ b/mdm-plugin-profile/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/profile/domain/ProfileSection.groovy @@ -26,4 +26,9 @@ class ProfileSection implements Cloneable { String sectionDescription List fields = [] + void validate() { + fields.each {field -> + field.validate() + } + } } diff --git a/mdm-plugin-profile/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/profile/object/Profile.groovy b/mdm-plugin-profile/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/profile/object/Profile.groovy index 6822122335..7d05c7c598 100644 --- a/mdm-plugin-profile/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/profile/object/Profile.groovy +++ b/mdm-plugin-profile/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/profile/object/Profile.groovy @@ -60,4 +60,11 @@ abstract class Profile implements Comparable { abstract void fromSections(List profileSections) + Profile validate() { + sections.each {section -> + section.validate() + } + this + } + } \ No newline at end of file diff --git a/mdm-plugin-profile/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/profile/provider/JsonProfileProviderService.groovy b/mdm-plugin-profile/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/profile/provider/JsonProfileProviderService.groovy index 115c96ca29..3663832e68 100644 --- a/mdm-plugin-profile/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/profile/provider/JsonProfileProviderService.groovy +++ b/mdm-plugin-profile/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/profile/provider/JsonProfileProviderService.groovy @@ -34,7 +34,7 @@ abstract class JsonProfileProviderService extends ProfileProviderService metadataList = metadataService.findAllByMultiFacetAwareItemIdAndNamespace(entity.id, this.getMetadataNamespace()) + List metadataList = getAllProfileMetadataByMultiFacetAwareItemId(entity.id) jsonProfile.sections.each {section -> section.fields.each {field -> @@ -63,25 +63,32 @@ abstract class JsonProfileProviderService extends ProfileProviderService ProfileField submittedField = submittedSection.fields.find {it.fieldName == field.fieldName} - String newValue = submittedField.currentValue ?: "" - String key = field.getMetadataKeyForSaving(submittedSection.sectionName) - storeFieldInEntity(entity, newValue, key, userEmailAddress) + if (submittedField) { + String newValue = submittedField.currentValue ?: '' + String key = field.getMetadataKeyForSaving(submittedSection.sectionName) + storeFieldInEntity(entity, newValue, key, userEmailAddress) + } } } } entity.addToMetadata(metadataNamespace, '_profiled', 'Yes', userEmailAddress) - Metadata.saveAll(entity.metadata) + + entity.findMetadataByNamespace(metadataNamespace).each {md -> + metadataService.save(md) + } } void storeFieldInEntity(MultiFacetAware entity, String value, String key, String userEmailAddress) { + if (!key) return - if (value && value != "" && key ) { + if (value) { entity.addToMetadata(metadataNamespace, key, value, userEmailAddress) } else { Metadata md = entity.metadata.find { it.namespace == metadataNamespace && it.key == key } if (md) { + entity.metadata.remove(md) metadataService.delete(md) } } @@ -102,7 +109,4 @@ abstract class JsonProfileProviderService extends ProfileProviderService getAllProfileMetadataByMultiFacetAwareItemId(UUID multiFacetAwareItemId) { + metadataService.findAllByMultiFacetAwareItemIdAndNamespace(multiFacetAwareItemId, this.getMetadataNamespace()) + } + UUID getDefiningDataModel() { return null } From 6439d01d43c00bd0bafbe7697ec1b3b8d676f970 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Fri, 23 Jul 2021 17:59:30 +0100 Subject: [PATCH 61/82] Remove the branch name from the mergeDiff for models Under merge we will never want to actually merge the branchname so we should exclude it --- .../maurodatamapper/core/model/ModelService.groovy | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy index 4b10d1befa..ee5ee2a84d 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy @@ -72,6 +72,7 @@ import org.springframework.context.MessageSource import java.time.OffsetDateTime import java.time.ZoneOffset +import java.util.function.Predicate @Slf4j abstract class ModelService extends CatalogueItemService implements SecurableResourceService, VersionLinkAwareService { @@ -707,7 +708,14 @@ abstract class ModelService extends CatalogueItemService imp ObjectDiff caDiffSource = commonAncestor.diff(sourceModel) ObjectDiff caDiffTarget = commonAncestor.diff(targetModel) - // ObjectDiff sourceDiffTarget = sourceModel.diff(targetModel) + + // Remove the branchname as diff as we know its a diff and for merging we dont want it + Predicate branchNamePredicate = [test: {FieldDiff fieldDiff -> + fieldDiff.fieldName == 'branchName' + },] as Predicate + + caDiffSource.diffs.removeIf(branchNamePredicate) + caDiffTarget.diffs.removeIf(branchNamePredicate) DiffBuilder .mergeDiff(sourceModel.class as Class) From 453d6fb55aa8aa226dc494cf2cb0f4ba0a5f761a Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Fri, 23 Jul 2021 18:01:28 +0100 Subject: [PATCH 62/82] Tidy up the ModelService with logging and unnecessary parameters --- .../core/model/ModelService.groovy | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy index ee5ee2a84d..d94dbd9e1e 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy @@ -25,6 +25,7 @@ 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.container.Folder import uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder +import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.FieldDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation @@ -483,7 +484,7 @@ abstract class ModelService extends CatalogueItemService imp processCreationPatchOfModelItem(domainToCopy as ModelItem, targetModel, creationPatch.relativePathToRoot.parent, userSecurityPolicyManager) } if (Utils.parentClassIsAssignableFromChild(MultiFacetItemAware, domainToCopy.class)) { - processCreationPatchOfFacet(domainToCopy as MultiFacetItemAware, targetModel, creationPatch.relativePathToRoot.parent, userSecurityPolicyManager) + processCreationPatchOfFacet(domainToCopy as MultiFacetItemAware, targetModel, creationPatch.relativePathToRoot.parent) } } @@ -564,10 +565,9 @@ abstract class ModelService extends CatalogueItemService imp modelItemService.save(copy, flush: false, validate: false) } - void processCreationPatchOfFacet(MultiFacetItemAware multiFacetItemAwareToCopy, Model targetModel, Path parentPathToCopyTo, - UserSecurityPolicyManager userSecurityPolicyManager) { + void processCreationPatchOfFacet(MultiFacetItemAware multiFacetItemAwareToCopy, Model targetModel, Path parentPathToCopyTo) { MultiFacetItemAwareService multiFacetItemAwareService = multiFacetItemAwareServices.find {it.handles(multiFacetItemAwareToCopy.class)} - if (!multiFacetItemAwareService) throw new ApiInternalException('MSXX', "No domain service to handle deletion of [${multiFacetItemAwareToCopy.domainType}]") + if (!multiFacetItemAwareService) throw new ApiInternalException('MSXX', "No domain service to handle creation of [${multiFacetItemAwareToCopy.domainType}]") log.debug('Creating Facet into Model at [{}]', parentPathToCopyTo) CatalogueItem parentToCopyInto = pathService.findResourceByPathFromRootResource(targetModel, parentPathToCopyTo) as CatalogueItem @@ -904,4 +904,27 @@ abstract class ModelService extends CatalogueItemService imp void setModelIsFromModel(K source, K target, User user) { source.addToSemanticLinks(linkType: SemanticLinkType.IS_FROM, createdBy: user.getEmailAddress(), targetMultiFacetAwareItem: target) } + + Model copyModelAndValidateAndSave(K original, + Folder folderToCopyInto, + User copier, + boolean copyPermissions, + String label, + Version copyDocVersion, + String branchName, + boolean throwErrors, + UserSecurityPolicyManager userSecurityPolicyManager) { + Model copiedModel = copyModel(original, folderToCopyInto, copier, true, original.label, original.documentationVersion, + branchName, false, userSecurityPolicyManager) + + if ((copiedModel as GormValidateable).validate()) { + saveModelWithContent(copiedModel) + if (securityPolicyManagerService) { + userSecurityPolicyManager = securityPolicyManagerService.addSecurityForSecurableResource(copiedModel, userSecurityPolicyManager.user, + copiedModel.label) + } + } else throw new ApiInvalidModelException('DMSXX', 'Copied Model is invalid', + (copiedModel as GormValidateable).errors, messageSource) + copiedModel + } } \ No newline at end of file From e092b992e891a9d01ae75c742ab6e0fa0d25c917 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Fri, 23 Jul 2021 18:02:07 +0100 Subject: [PATCH 63/82] Add a few accessor methods in path --- .../ac/ox/softeng/maurodatamapper/path/Path.groovy | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy index a2d40cb976..cd09519740 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy @@ -107,10 +107,14 @@ class Path { pathNodes.isEmpty() } - void each(@DelegatesTo(List) @ClosureParams(value = SimpleType, options = 'uk.ac.ox.softeng.maurodatamapper.util.PathNode') Closure closure) { + void each(@DelegatesTo(List) @ClosureParams(value = SimpleType, options = 'uk.ac.ox.softeng.maurodatamapper.path.PathNode') Closure closure) { pathNodes.each closure } + PathNode find(@DelegatesTo(List) @ClosureParams(value = SimpleType, options = 'uk.ac.ox.softeng.maurodatamapper.path.PathNode') Closure closure) { + pathNodes.find closure + } + Path getChildPath() { clone().tap { pathNodes.removeAt(0) @@ -140,6 +144,12 @@ class Path { new Path(path) } + static Path from(PathNode pathNode) { + new Path().tap { + pathNodes << pathNode + } + } + static Path from(String prefix, String pathIdentifier) { new Path().tap { pathNodes << new PathNode(prefix, pathIdentifier, true, true) From a33537cb8deb220dd6d62d56d8bf3caa644fa18a Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Fri, 23 Jul 2021 18:04:36 +0100 Subject: [PATCH 64/82] Add diffable to folder and versionedfolder * We do howwever need to use the service to add the models into the diffs * Add method to copy a folder recursively including the models inside the folder --- .../core/container/Folder.groovy | 25 +++- .../core/container/VersionedFolder.groovy | 14 ++- .../core/container/FolderService.groovy | 113 ++++++++++++++++-- 3 files changed, 140 insertions(+), 12 deletions(-) diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Folder.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Folder.groovy index aab56c81aa..d3acecd621 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Folder.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/Folder.groovy @@ -17,6 +17,9 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.container +import uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder +import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable +import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata import uk.ac.ox.softeng.maurodatamapper.core.facet.ReferenceFile @@ -36,7 +39,7 @@ import grails.plugins.hibernate.search.HibernateSearchApi import grails.rest.Resource @Resource(readOnly = false, formats = ['json', 'xml']) -class Folder implements Container { +class Folder implements Container, Diffable { public static final String MISCELLANEOUS_FOLDER_LABEL = 'Miscellaneous' public static final String DEFAULT_FOLDER_LABEL = 'New Folder' @@ -99,6 +102,26 @@ class Folder implements Container { Folder.simpleName } + @Override + ObjectDiff diff(Folder that) { + folderDiffBuilder(Folder, this, that) + } + + static ObjectDiff folderDiffBuilder(Class diffClass, T lhs, T rhs) { + String lhsId = lhs.id ?: "Left:Unsaved_${lhs.domainType}" + String rhsId = rhs.id ?: "Right:Unsaved_${rhs.domainType}" + DiffBuilder.objectDiff(diffClass) + .leftHandSide(lhsId, lhs) + .rightHandSide(rhsId, rhs) + .appendString('label', lhs.label, rhs.label) + .appendString('description', lhs.description, rhs.description) + .appendList(Metadata, 'metadata', lhs.metadata, rhs.metadata) + .appendList(Annotation, 'annotations', lhs.annotations, rhs.annotations) + .appendList(Rule, 'rule', lhs.rules, rhs.rules) + .appendBoolean('deleted', lhs.deleted, rhs.deleted) + .appendList(Folder, 'folders', lhs.childFolders, rhs.childFolders) + } + @Override String getPathPrefix() { 'fo' diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolder.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolder.groovy index a3c2b7a31e..f7b10e0a97 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolder.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolder.groovy @@ -18,6 +18,8 @@ package uk.ac.ox.softeng.maurodatamapper.core.container import uk.ac.ox.softeng.maurodatamapper.core.authority.Authority +import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable +import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLink import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.InformationAwareConstraints import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints @@ -31,7 +33,7 @@ import uk.ac.ox.softeng.maurodatamapper.hibernate.VersionUserType import grails.gorm.DetachedCriteria import grails.plugins.hibernate.search.HibernateSearchApi -class VersionedFolder extends Folder implements VersionAware, VersionLinkAware { +class VersionedFolder extends Folder implements VersionAware, VersionLinkAware, Diffable { Authority authority @@ -69,6 +71,16 @@ class VersionedFolder extends Folder implements VersionAware, VersionLinkAware { VersionedFolder.simpleName } + @Override + ObjectDiff diff(VersionedFolder that) { + folderDiffBuilder(VersionedFolder, this, that) + .appendBoolean('finalised', this.finalised, that.finalised) + .appendString('documentationVersion', this.documentationVersion.toString(), that.documentationVersion.toString()) + .appendString('modelVersion', this.modelVersion.toString(), that.modelVersion.toString()) + .appendString('branchName', this.branchName, that.branchName) + .appendOffsetDateTime('dateFinalised', this.dateFinalised, that.dateFinalised) + } + @Override String getPathPrefix() { 'vf' 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 1a683791d7..502afe8733 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 @@ -17,6 +17,13 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.container +import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException +import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInvalidModelException +import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiNotYetImplementedException +import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ArrayDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff +import uk.ac.ox.softeng.maurodatamapper.core.facet.EditService +import uk.ac.ox.softeng.maurodatamapper.core.facet.EditTitle import uk.ac.ox.softeng.maurodatamapper.core.facet.Rule import uk.ac.ox.softeng.maurodatamapper.core.model.ContainerService import uk.ac.ox.softeng.maurodatamapper.core.model.Model @@ -30,6 +37,7 @@ import grails.gorm.DetachedCriteria import grails.gorm.transactions.Transactional import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.MessageSource @Transactional @Slf4j @@ -40,6 +48,8 @@ class FolderService extends ContainerService { @Autowired(required = false) SecurityPolicyManagerService securityPolicyManagerService + EditService editService + MessageSource messageSource @Override boolean handles(Class clazz) { @@ -68,14 +78,14 @@ class FolderService extends ContainerService { @Override List getAll(Collection containerIds) { - Folder.getAll(containerIds).findAll().collect { unwrapIfProxy(it) } + Folder.getAll(containerIds).findAll().collect {unwrapIfProxy(it)} } @Override List findAllReadableContainersBySearchTerm(UserSecurityPolicyManager userSecurityPolicyManager, String searchTerm) { log.debug('Searching readable folders for search term in label') List readableIds = userSecurityPolicyManager.listReadableSecuredResourceIds(Folder) - Folder.luceneTreeLabelSearch(readableIds.collect { it.toString() }, searchTerm) + Folder.luceneTreeLabelSearch(readableIds.collect {it.toString()}, searchTerm) } @Override @@ -127,7 +137,7 @@ class FolderService extends ContainerService { @Override List list() { - Folder.list().collect { unwrapIfProxy(it) } + Folder.list().collect {unwrapIfProxy(it)} } Long count() { @@ -148,15 +158,15 @@ class FolderService extends ContainerService { return } if (permanent) { - folder.childFolders.each { delete(it, permanent, false) } - modelServices.each { it.deleteAllInContainer(folder) } + folder.childFolders.each {delete(it, permanent, false)} + modelServices.each {it.deleteAllInContainer(folder)} if (securityPolicyManagerService) { securityPolicyManagerService.removeSecurityForSecurableResource(folder, null) } folder.trackChanges() folder.delete(flush: flush) } else { - folder.childFolders.each { delete(it) } + folder.childFolders.each {delete(it)} delete(folder) } } @@ -252,9 +262,9 @@ class FolderService extends ContainerService { copy.description = original.description metadataService.findAllByMultiFacetAwareItemId(original.id).each {copy.addToMetadata(it.namespace, it.key, it.value, copier.emailAddress)} - ruleService.findAllByMultiFacetAwareItemId(original.id).each { rule -> + ruleService.findAllByMultiFacetAwareItemId(original.id).each {rule -> Rule copiedRule = new Rule(name: rule.name, description: rule.description, createdBy: copier.emailAddress) - rule.ruleRepresentations.each { ruleRepresentation -> + rule.ruleRepresentations.each {ruleRepresentation -> copiedRule.addToRuleRepresentations(language: ruleRepresentation.language, representation: ruleRepresentation.representation, createdBy: copier.emailAddress) @@ -262,7 +272,7 @@ class FolderService extends ContainerService { copy.addToRules(copiedRule) } - semanticLinkService.findAllBySourceMultiFacetAwareItemId(original.id).each { link -> + semanticLinkService.findAllBySourceMultiFacetAwareItemId(original.id).each {link -> copy.addToSemanticLinks(createdBy: copier.emailAddress, linkType: link.linkType, targetMultiFacetAwareItemId: link.targetMultiFacetAwareItemId, targetMultiFacetAwareItemDomainType: link.targetMultiFacetAwareItemDomainType, @@ -274,8 +284,91 @@ class FolderService extends ContainerService { List findAllModelsInFolder(Folder folder) { if (!modelServices) return [] - modelServices.collectMany { service -> + modelServices.collectMany {service -> service.findAllByFolderId(folder.id) } as List } + + def void loadModelsIntoFolderObjectDiff(ObjectDiff diff, Folder leftHandSide, Folder rightHandSide) { + List thisModels = findAllModelsInFolder(leftHandSide) + List thatModels = findAllModelsInFolder(rightHandSide) + diff.appendList(Model, 'models', thisModels, thatModels) + + // Recurse into child folder diffs + ArrayDiff childFolderDiff = diff.diffs.find {it.fieldName == 'folders'} + + if (childFolderDiff) { + // Created folders wont have any need for a model diff as all models will be new + // Deleted folders wont have any need for a model diff as all models will not exist + childFolderDiff.modified.each {childDiff -> + loadModelsIntoFolderObjectDiff(childDiff, childDiff.left, childDiff.right) + } + } + } + + ModelService findModelServiceForModel(Model model) { + ModelService modelService = modelServices.find {it.handles(model.class)} + if (!modelService) throw new ApiInternalException('MSXX', "No model service to handle model [${model.domainType}]") + modelService + } + + Folder copyFolder(Folder original, Folder folderToCopyInto, User copier, boolean copyPermissions, String modelBranchName, boolean throwErrors, + UserSecurityPolicyManager userSecurityPolicyManager) { + Folder copiedFolder = new Folder(deleted: false, parentFolder: folderToCopyInto, createdBy: copier, description: original.description, + label: original.label) + + metadataService.findAllByMultiFacetAwareItemId(original.id).each {copiedFolder.addToMetadata(it.namespace, it.key, it.value, copier.emailAddress)} + ruleService.findAllByMultiFacetAwareItemId(original.id).each {rule -> + Rule copiedRule = new Rule(name: rule.name, description: rule.description, createdBy: copier.emailAddress) + rule.ruleRepresentations.each {ruleRepresentation -> + copiedRule.addToRuleRepresentations(language: ruleRepresentation.language, + representation: ruleRepresentation.representation, + createdBy: copier.emailAddress) + } + copiedFolder.addToRules(copiedRule) + } + + semanticLinkService.findAllBySourceMultiFacetAwareItemId(original.id).each {link -> + copiedFolder.addToSemanticLinks(createdBy: copier.emailAddress, linkType: link.linkType, + targetMultiFacetAwareItemId: link.targetMultiFacetAwareItemId, + targetMultiFacetAwareItemDomainType: link.targetMultiFacetAwareItemDomainType, + unconfirmed: true) + } + + if (copyPermissions) { + if (throwErrors) { + throw new ApiNotYetImplementedException('MSXX', 'Folder permission copying') + } + log.warn('Permission copying is not yet implemented') + + } + + if (copiedFolder.validate()) { + save(copiedFolder, validate: false) + editService.createAndSaveEdit(EditTitle.COPY, copiedFolder.id, copiedFolder.domainType, + "Folder ${original.label} created as a copy of ${original.id}", + copier + ) + if (securityPolicyManagerService) { + userSecurityPolicyManager = securityPolicyManagerService.addSecurityForSecurableResource(copiedFolder, userSecurityPolicyManager.user, + copiedFolder.label) + } + } else throw new ApiInvalidModelException('FS01', 'Copied Folder is invalid', copiedFolder.errors, messageSource) + + + List models = findAllModelsInFolder(original) + models.each {modelToCopy -> + ModelService modelService = findModelServiceForModel(modelToCopy) + modelService.copyModelAndValidateAndSave(modelToCopy, copiedFolder, userSecurityPolicyManager.user, true, modelToCopy.label, modelToCopy.documentationVersion, + modelBranchName, false, userSecurityPolicyManager) + } + + List childFolders = findAllByParentId(original.id, [:]) + childFolders.each {childFolderToCopy -> + copiedFolder(childFolderToCopy, copiedFolder, copier, copyPermissions, childFolderToCopy.label, modelBranchName, throwErrors, userSecurityPolicyManager) + + } + + copiedFolder + } } From 668b9c5f00359b6a206a39690dc0c708baa14dc2 Mon Sep 17 00:00:00 2001 From: OButler Date: Thu, 15 Jul 2021 17:10:11 +0100 Subject: [PATCH 65/82] Added tests for the new endpoints testing logged in/not logged in and not found data --- .../gorm/PaginatedResultList.groovy | 4 +- mdm-testing-functional/README.md | 4 - .../container/ClassifierFunctionalSpec.groovy | 156 +++++++++++++++++- 3 files changed, 151 insertions(+), 13 deletions(-) diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/gorm/PaginatedResultList.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/gorm/PaginatedResultList.groovy index 7532038167..3d33c98d95 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/gorm/PaginatedResultList.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/gorm/PaginatedResultList.groovy @@ -28,8 +28,8 @@ class PaginatedResultList extends PagedResultList { totalCount = results.size() this.pagination = pagination - Integer max = pagination.max?.toInteger ?: 10 - Integer offset = pagination.offset?.toInteger ?: 0 + Integer max = pagination.max?: 10 + Integer offset = pagination.offset?: 0 resultList = results.subList(Math.min(totalCount, offset), Math.min(totalCount, offset + max)) } diff --git a/mdm-testing-functional/README.md b/mdm-testing-functional/README.md index aa7859d7c4..c38bd2f3ca 100644 --- a/mdm-testing-functional/README.md +++ b/mdm-testing-functional/README.md @@ -92,10 +92,6 @@ Controller: classifier | DELETE | /api/classifiers/${id} | Action: delete | PUT | /api/classifiers/${id} | Action: update | GET | /api/classifiers/${id} | Action: show - | POST | /api/${catalogueItemDomainType}/${catalogueItemId}/classifiers | Action: save - | GET | /api/${catalogueItemDomainType}/${catalogueItemId}/classifiers | Action: index - | DELETE | /api/${catalogueItemDomainType}/${catalogueItemId}/classifiers/${id} | Action: delete - | GET | /api/${catalogueItemDomainType}/${catalogueItemId}/classifiers/${id} | Action: show Controller: codeSet | GET | /api/codeSets/providers/importers | Action: importerProviders diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/ClassifierFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/ClassifierFunctionalSpec.groovy index 043ffaaa42..15e6b390b8 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/ClassifierFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/ClassifierFunctionalSpec.groovy @@ -17,20 +17,30 @@ */ package uk.ac.ox.softeng.maurodatamapper.testing.functional.core.container +import uk.ac.ox.softeng.maurodatamapper.core.authority.Authority +import uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress import uk.ac.ox.softeng.maurodatamapper.core.container.Classifier +import uk.ac.ox.softeng.maurodatamapper.core.container.Folder +import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.security.role.GroupRole import uk.ac.ox.softeng.maurodatamapper.terminology.Terminology import uk.ac.ox.softeng.maurodatamapper.terminology.bootstrap.BootstrapModels import uk.ac.ox.softeng.maurodatamapper.testing.functional.UserAccessAndPermissionChangingFunctionalSpec +import grails.gorm.transactions.Rollback import grails.gorm.transactions.Transactional +import grails.plugin.json.builder.JsonOutput import grails.testing.mixin.integration.Integration +import groovy.json.JsonBuilder import groovy.util.logging.Slf4j import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus import java.util.regex.Pattern +import static io.micronaut.http.HttpStatus.CREATED +import static io.micronaut.http.HttpStatus.FORBIDDEN +import static io.micronaut.http.HttpStatus.NOT_FOUND import static io.micronaut.http.HttpStatus.OK /** @@ -44,7 +54,16 @@ import static io.micronaut.http.HttpStatus.OK * | DELETE | /api/classifiers/${classifierId}/readByAuthenticated | Action: readByAuthenticated * | PUT | /api/classifiers/${classifierId}/readByAuthenticated | Action: readByAuthenticated * | DELETE | /api/classifiers/${classifierId}/readByEveryone | Action: readByEveryone - * | PUT | /api/classifiers/${classifierId}/readByEveryone | Action: readByEveryone + * | PUT | /api/classifiers/${classifierId}/readByEveryone | Action: readByEveryone# + * + * + * | POST | /api/${catalogueItemDomainType}/${catalogueItemId}/classifiers | Action: save + * | GET | /api/${catalogueItemDomainType}/${catalogueItemId}/classifiers | Action: index + * | DELETE | /api/${catalogueItemDomainType}/${catalogueItemId}/classifiers/${id}* | Action: delete + * | GET | /api/${catalogueItemDomainType}/${catalogueItemId}/classifiers/${id}* | Action: show + * + * | POST | /api/${catalogueItemDomainType}/${catalogueItemId}/classifiers | Action: save + * | GET | /api/${catalogueItemDomainType}/${catalogueItemId}/classifiers | Action: index * * @see uk.ac.ox.softeng.maurodatamapper.core.container.ClassifierController */ @@ -68,20 +87,20 @@ class ClassifierFunctionalSpec extends UserAccessAndPermissionChangingFunctional Map getValidJson() { [ - label: 'Functional Test Classifier 2', + label: 'Functional Test Classifier 2', ] } Map getInvalidJson() { [ - label: 'Functional Test Classifier' + label: 'Functional Test Classifier' ] } @Override Map getValidUpdateJson() { [ - description: 'Just something for testing' + description: 'Just something for testing' ] } @@ -177,7 +196,7 @@ class ClassifierFunctionalSpec extends UserAccessAndPermissionChangingFunctional "availableActions": ["comment","delete","editDescription","save","show","softDelete","update"] }''' } - + void "Test the catalogueItems action for classifier"() { when: "The catalogueItems action on a known classifier ID is requested unlogged in" GET("${getTestClassifierId()}/catalogueItems") @@ -208,7 +227,7 @@ class ClassifierFunctionalSpec extends UserAccessAndPermissionChangingFunctional when: "A classifier is added to a terminology" loginAdmin() POST("terminologies/${getSimpleTerminologyId()}/classifiers", [ - label: 'A test classifier for a terminology' + label: 'A test classifier for a terminology' ], MAP_ARG, true) then: "Resource is created" @@ -231,6 +250,14 @@ class ClassifierFunctionalSpec extends UserAccessAndPermissionChangingFunctional ] }''' + when: "The classifier is requested from the terminology" + loginAdmin() + GET("terminologies/${getSimpleTerminologyId()}/classifiers/${newId}", MAP_ARG, true) + + then: "Resource is shown" + verifyResponse(OK, response) + assert responseBody().id == newId + when: "The classifier is deleted from the terminology" loginAdmin() DELETE("terminologies/${getSimpleTerminologyId()}/classifiers/${newId}", MAP_ARG, true) @@ -251,4 +278,119 @@ class ClassifierFunctionalSpec extends UserAccessAndPermissionChangingFunctional cleanup: removeValidIdObject(newId) } -} \ No newline at end of file + + void "CA01 test the creation of a classifier as part of a terminology"() { + given: 'putting a catalog item id' + String terminologyId = getSimpleTerminologyId() + + when: 'not authenticated' + POST("terminologies/$terminologyId/classifiers", [ + label: 'A test classifier for a terminology' + ], MAP_ARG, true) + + then: + verifyResponse(NOT_FOUND, response) + + when: 'authenticated' + loginAuthenticated() + POST("terminologies/$terminologyId/classifiers", [ + label: 'A test classifier for a terminology' + ], MAP_ARG, true) + + then: + verifyResponse(NOT_FOUND, response) + + when: 'reader' + loginReader() + POST("terminologies/$terminologyId/classifiers", [ + label: 'A test classifier for a terminology' + ], MAP_ARG, true) + + then: + verifyResponse(FORBIDDEN, response) + } + + void "CA01A Test as an editor"() { + given: 'putting a catalog item id' + String terminologyId = getSimpleTerminologyId() + + when: 'Editor' + loginEditor() + POST("terminologies/$terminologyId/classifiers", [ + label: 'A test classifier for a terminology' + ], MAP_ARG, true) + + then: + verifyResponse(CREATED, response) + String createdId = response.body().id + + cleanup: + removeValidIdObject(createdId) + + } + + + void "CA01B Test as an Admin"() { + given: 'putting a catalog item id' + String terminologyId = getSimpleTerminologyId() + + when: "Admin" + loginAdmin() + POST("terminologies/$terminologyId/classifiers", [ + label: 'A test classifier for a terminology' + ], MAP_ARG, true) + + then: + verifyResponse(CREATED, response) + String createdId = response.body().id + + cleanup: + removeValidIdObject(createdId) + + } + + + void "CA02 Test the catalogueItems delete action for classifier"() { + + given: 'putting a catalog item id' + String terminologyId = getSimpleTerminologyId() + + when: 'making the call not logged in' + GET("terminologies/$terminologyId/classifiers", MAP_ARG, true) + + then: + verifyResponse(FORBIDDEN, response) + + when: 'making the call as authenticated' + loginAuthenticated() + GET("terminologies/$terminologyId/classifiers", MAP_ARG, true) + + then: + verifyResponse(FORBIDDEN, response) + + when: 'making the call as a reader' + loginReader() + GET("terminologies/$terminologyId/classifiers", MAP_ARG, true) + + then: + verifyResponse(OK, response) + + when: 'making the call as an editor' + loginEditor() + GET("terminologies/$terminologyId/classifiers", MAP_ARG, true) + + then: + verifyResponse(OK, response) + + when: 'making the call as as admin' + loginAdmin() + GET("terminologies/$terminologyId/classifiers", MAP_ARG, true) + + then: 'response should be OK and include the classifier inside the terminology' + verifyResponse(OK, response) + assert responseBody().count == 1 + assert responseBody().items.any { it.label == 'test classifier simple' } + + } +} + From 23c43b98370882430510b186c94c37f9c3e5f45a Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Wed, 28 Jul 2021 09:44:00 +0100 Subject: [PATCH 66/82] gh-116 use the hibernate proxy handler to unwrap in path aware Under some circumstances the entity is just a plain entity proxy but under others its a hibernate proxy, this makes sure we handle both situations as the hibernate proxy handler defers to its superclass if it cant unwrap --- .../softeng/maurodatamapper/core/traits/domain/PathAware.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/domain/PathAware.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/domain/PathAware.groovy index 680acc0cea..c02ba069bc 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/domain/PathAware.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/traits/domain/PathAware.groovy @@ -53,7 +53,7 @@ trait PathAware { if (ge) { if (ge.instanceOf(PathAware)) { // Ensure proxies are unwrapped - PathAware parent = (ge instanceof EntityProxy ? ((EntityProxy) ge).getTarget() : ge) as PathAware + PathAware parent =proxyHandler.unwrapIfProxy(ge) as PathAware depth = parent.depth + 1 path = "${parent.getPath()}/${parent.getId() ?: UNSET}" } else { From e82e2a2d959a533884245fdaeb415ccfd6c44779 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Fri, 23 Jul 2021 18:07:23 +0100 Subject: [PATCH 67/82] mc-9525 Add endpoints for diff, mergediff and mergeinto for VersionedFolder * Add methods into service to handle all the endpoint work * Include interceptor checks --- .../maurodatamapper/core/UrlMappings.groovy | 18 +- .../VersionedFolderController.groovy | 67 ++++ .../VersionedFolderInterceptor.groovy | 14 +- .../container/VersionedFolderService.groovy | 293 +++++++++++++++++- .../views/versionedFolder/diff.gson | 9 + .../views/versionedFolder/mergeDiff.gson | 8 + 6 files changed, 388 insertions(+), 21 deletions(-) create mode 100644 mdm-core/grails-app/views/versionedFolder/diff.gson create mode 100644 mdm-core/grails-app/views/versionedFolder/mergeDiff.gson 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 e74b375bf8..9f4066a3a9 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 @@ -104,13 +104,17 @@ class UrlMappings { post '/search'(controller: 'versionedFolder', action: 'search') get '/search'(controller: 'versionedFolder', action: 'search') - get "/commonAncestor/$otherVersionedFolderId"(controller: 'VersionedFolder', action: 'commonAncestor') - get '/latestFinalisedModel'(controller: 'VersionedFolder', action: 'latestFinalisedModel') - get '/latestModelVersion'(controller: 'VersionedFolder', action: 'latestModelVersion') - get '/modelVersionTree'(controller: 'VersionedFolder', action: 'modelVersionTree') - get '/currentMainBranch'(controller: 'VersionedFolder', action: 'currentMainBranch') - get '/availableBranches'(controller: 'VersionedFolder', action: 'availableBranches') - get '/simpleModelVersionTree'(controller: 'VersionedFolder', action: 'simpleModelVersionTree') + get "/commonAncestor/$otherVersionedFolderId"(controller: 'versionedFolder', action: 'commonAncestor') + get '/latestFinalisedModel'(controller: 'versionedFolder', action: 'latestFinalisedModel') + get '/latestModelVersion'(controller: 'versionedFolder', action: 'latestModelVersion') + get '/modelVersionTree'(controller: 'versionedFolder', action: 'modelVersionTree') + get '/currentMainBranch'(controller: 'versionedFolder', action: 'currentMainBranch') + get '/availableBranches'(controller: 'versionedFolder', action: 'availableBranches') + get '/simpleModelVersionTree'(controller: 'versionedFolder', action: 'simpleModelVersionTree') + + get "/mergeDiff/$otherVersionedFolderId"(controller: 'versionedFolder', action: 'mergeDiff') + put "/mergeInto/$otherVersionedFolderId"(controller: 'versionedFolder', action: 'mergeInto') + get "/diff/$otherVersionedFolderId"(controller: 'versionedFolder', action: 'diff') } '/classifiers'(resources: 'classifier', excludes: DEFAULT_EXCLUDES) { diff --git a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderController.groovy b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderController.groovy index 3c0de6835f..0acf69bd4b 100644 --- a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderController.groovy +++ b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderController.groovy @@ -19,10 +19,12 @@ package uk.ac.ox.softeng.maurodatamapper.core.container import uk.ac.ox.softeng.maurodatamapper.core.authority.AuthorityService import uk.ac.ox.softeng.maurodatamapper.core.controller.EditLoggingController +import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem 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.rest.transport.merge.MergeIntoData import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.CreateNewVersionData import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.FinaliseData import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.VersionTreeModel @@ -36,6 +38,7 @@ import grails.gorm.transactions.Transactional import org.springframework.beans.factory.annotation.Autowired import static org.springframework.http.HttpStatus.NO_CONTENT +import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY class VersionedFolderController extends EditLoggingController { static responseFormats = ['json', 'xml'] @@ -313,6 +316,70 @@ class VersionedFolderController extends EditLoggingController { respond versionTreeModelList.findAll {!it.newFork} } + def diff() { + VersionedFolder thisVersionedFolder = queryForResource params.versionedFolderId + VersionedFolder otherVersionedFolder = queryForResource params.otherVersionedFolderId + + if (!thisVersionedFolder) return notFound(params.versionedFolderId) + if (!otherVersionedFolder) return notFound(params.otherVersionedFolderId) + + ObjectDiff diff = versionedFolderService.getDiffForVersionedFolders(thisVersionedFolder, otherVersionedFolder) + respond diff + } + + def mergeDiff() { + + VersionedFolder source = queryForResource params.versionedFolderId + if (!source) return notFound(params.versionedFolderId) + + VersionedFolder target = queryForResource params.otherVersionedFolderId + if (!target) return notFound(params.otherVersionedFolderId) + + respond versionedFolderService.getMergeDiffForVersionedFolders(source, target) + } + + @Transactional + def mergeInto(MergeIntoData mergeIntoData) { + if (!mergeIntoData.validate()) { + respond mergeIntoData.errors + return + } + + if (mergeIntoData.patch.sourceId != params.versionedFolderId) { + return errorResponse(UNPROCESSABLE_ENTITY, 'Source versioned folder id passed in request body does not match source versioned folder id in URI.') + } + if (mergeIntoData.patch.targetId != params.otherVersionedFolderId) { + return errorResponse(UNPROCESSABLE_ENTITY, 'Target versioned folder id passed in request body does not match target versioned folder id in URI.') + } + + VersionedFolder source = queryForResource params.versionedFolderId + if (!source) return notFound(params.versionedFolderId) + + VersionedFolder target = queryForResource params.otherVersionedFolderId + if (!target) return notFound(params.otherVersionedFolderId) + + VersionedFolder instance = versionedFolderService.mergeObjectPatchDataIntoVersionedFolder(mergeIntoData.patch, target, source, currentUserSecurityPolicyManager) + + if (!validateResource(instance, 'merge')) return + + if (mergeIntoData.deleteBranch) { + if (!currentUserSecurityPolicyManager.userCanEditSecuredResourceId(source.class, source.id)) { + return forbiddenDueToPermissions(currentUserSecurityPolicyManager.userAvailableActions(source.class, source.id)) + } + versionedFolderService.delete(source, true) + if (securityPolicyManagerService) { + currentUserSecurityPolicyManager = securityPolicyManagerService.retrieveUserSecurityPolicyManager(currentUser.emailAddress) + } + } + + if (mergeIntoData.changeNotice) { + instance.addChangeNoticeEdit(currentUser, mergeIntoData.changeNotice) + } + + updateResource(instance) + + updateResponse(instance) + } @Override protected VersionedFolder queryForResource(Serializable id) { diff --git a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderInterceptor.groovy b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderInterceptor.groovy index 2ca229ac77..1157df6bbf 100644 --- a/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderInterceptor.groovy +++ b/mdm-core/grails-app/controllers/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderInterceptor.groovy @@ -55,7 +55,7 @@ class VersionedFolderInterceptor extends TieredAccessSecurableResourceIntercepto boolean before() { securableResourceChecks() - if (actionName == 'commonAncestor') { + if (actionName in ['commonAncestor', 'diff', 'mergeDiff']) { if (!currentUserSecurityPolicyManager.userCanReadSecuredResourceId(VersionedFolder, getId())) { return notFound(VersionedFolder, getId()) } @@ -64,6 +64,18 @@ class VersionedFolderInterceptor extends TieredAccessSecurableResourceIntercepto } return true } + if (actionName == 'mergeInto') { + //TODO confirm all correct + if (!currentUserSecurityPolicyManager.userCanReadSecuredResourceId(getSecuredClass(), getId())) { + return notFound(getSecuredClass(), getId()) + } + if (!currentUserSecurityPolicyManager.userCanReadSecuredResourceId(getSecuredClass(), params.otherVersionedFolderId)) { + return notFound(getSecuredClass(), params.otherVersionedFolderId) + } + + return currentUserSecurityPolicyManager.userCanWriteSecuredResourceId(getSecuredClass(), params.otherVersionedFolderId, actionName) ?: + forbiddenDueToPermissions(currentUserSecurityPolicyManager.userAvailableActions(getSecuredClass(), params.otherVersionedFolderId)) + } if (params.id || params.versionedFolderId) { return checkTieredAccessActionAuthorisationOnSecuredResource(VersionedFolder, getId(), true) diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy index d0cf826df2..a42322e8a3 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy @@ -18,11 +18,22 @@ package uk.ac.ox.softeng.maurodatamapper.core.container import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException +import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInvalidModelException import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiNotYetImplementedException import uk.ac.ox.softeng.maurodatamapper.core.authority.AuthorityService +import uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder +import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ArrayDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.FieldDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff +import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation import uk.ac.ox.softeng.maurodatamapper.core.facet.EditService import uk.ac.ox.softeng.maurodatamapper.core.facet.EditTitle +import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata +import uk.ac.ox.softeng.maurodatamapper.core.facet.ReferenceFile +import uk.ac.ox.softeng.maurodatamapper.core.facet.Rule +import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLink import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkType import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLink import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkService @@ -31,13 +42,24 @@ import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwa 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.model.Model +import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem +import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItemService import uk.ac.ox.softeng.maurodatamapper.core.model.ModelService +import uk.ac.ox.softeng.maurodatamapper.core.model.facet.MultiFacetAware +import uk.ac.ox.softeng.maurodatamapper.core.path.PathService import uk.ac.ox.softeng.maurodatamapper.core.rest.converter.json.OffsetDateTimeConverter +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.FieldPatchData +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.ObjectPatchData import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model.VersionTreeModel +import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.MultiFacetItemAware +import uk.ac.ox.softeng.maurodatamapper.core.traits.service.DomainService +import uk.ac.ox.softeng.maurodatamapper.core.traits.service.MultiFacetItemAwareService import uk.ac.ox.softeng.maurodatamapper.core.traits.service.VersionLinkAwareService +import uk.ac.ox.softeng.maurodatamapper.path.Path 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.traits.domain.CreatorAware import uk.ac.ox.softeng.maurodatamapper.util.Utils import uk.ac.ox.softeng.maurodatamapper.version.Version import uk.ac.ox.softeng.maurodatamapper.version.VersionChangeType @@ -52,6 +74,7 @@ import org.springframework.context.MessageSource import java.time.OffsetDateTime import java.time.ZoneOffset +import java.util.function.Predicate @Transactional @Slf4j @@ -64,10 +87,20 @@ class VersionedFolderService extends ContainerService implement VersionLinkService versionLinkService MessageSource messageSource AuthorityService authorityService + PathService pathService + + @Autowired(required = false) + Set domainServices + + @Autowired(required = false) + Set multiFacetItemAwareServices @Autowired(required = false) List modelServices + @Autowired(required = false) + Set modelItemServices + @Autowired(required = false) SecurityPolicyManagerService securityPolicyManagerService @@ -100,16 +133,21 @@ class VersionedFolderService extends ContainerService implement folderService.getContainerPropertyNameInModel() } + Set getDomainServices() { + domainServices.add(this) + domainServices + } + @Override List getAll(Collection containerIds) { - VersionedFolder.getAll(containerIds).findAll().collect { unwrapIfProxy(it) } + VersionedFolder.getAll(containerIds).findAll().collect {unwrapIfProxy(it)} } @Override List findAllReadableContainersBySearchTerm(UserSecurityPolicyManager userSecurityPolicyManager, String searchTerm) { log.debug('Searching readable folders for search term in label') List readableIds = userSecurityPolicyManager.listReadableSecuredResourceIds(Folder) - VersionedFolder.luceneTreeLabelSearch(readableIds.collect { it.toString() }, searchTerm) + VersionedFolder.luceneTreeLabelSearch(readableIds.collect {it.toString()}, searchTerm) } @Override @@ -194,16 +232,16 @@ class VersionedFolderService extends ContainerService implement long start = System.currentTimeMillis() log.debug('Finalising models inside folder') - modelServices.each { service -> + modelServices.each {service -> Collection modelsInFolder = service.findAllByFolderId(folder.id) - modelsInFolder.each { model -> + modelsInFolder.each {model -> service.finaliseModel(model as Model, user, folderVersion, null, folderVersionTag) } } List folders = findAllByParentId(folder.id) log.debug('Finalising {} sub folders inside folder', folders.size()) - folders.each { childFolder -> + folders.each {childFolder -> finaliseFolderContents(childFolder, user, folderVersion, folderVersionTag) } @@ -295,7 +333,7 @@ class VersionedFolderService extends ContainerService implement @Override List list() { - VersionedFolder.list().collect { unwrapIfProxy(it) } + VersionedFolder.list().collect {unwrapIfProxy(it)} } Long count() { @@ -515,9 +553,9 @@ class VersionedFolderService extends ContainerService implement String labelSuffix = folderCopy.label == original.label ? '' : " (${folderCopy.label})" log.debug('Copying models from original folder into copied folder') - modelServices.each { service -> + modelServices.each {service -> List originalModels = service.findAllByContainerId(original.id) as List - List copiedModels = originalModels.collect { Model model -> + List copiedModels = originalModels.collect {Model model -> service.copyModel(model, folderCopy, copier, copyPermissions, "${model.label}${labelSuffix}", @@ -525,7 +563,7 @@ class VersionedFolderService extends ContainerService implement userSecurityPolicyManager) } // We can't save until after all copied as the save clears the sessions - copiedModels.each { copy -> + copiedModels.each {copy -> log.debug('Validating and saving model copy') service.validate(copy) if (copy.hasErrors()) { @@ -537,7 +575,7 @@ class VersionedFolderService extends ContainerService implement List folders = findAllByParentId(original.id) log.debug('Copying {} sub folders inside folder', folders.size()) - folders.each { childFolder -> + folders.each {childFolder -> Folder childCopy = new Folder(parentFolder: folderCopy, deleted: false) childCopy = folderService.copyBasicFolderInformation(childFolder, childCopy, copier) folderService.validate(childCopy) @@ -616,7 +654,7 @@ class VersionedFolderService extends ContainerService implement List versionLinks = versionLinkService.findAllByTargetModelId(instance.id) - versionLinks.each { link -> + versionLinks.each {link -> VersionedFolder linkedModel = get(link.multiFacetAwareItemId) versionTreeModelList. addAll(buildModelVersionTree(linkedModel, link.linkType, rootVersionTreeModel, includeForks, userSecurityPolicyManager)) @@ -681,7 +719,7 @@ class VersionedFolderService extends ContainerService implement } boolean doesDepthTreeContainVersionedFolder(Folder folder) { - folder.instanceOf(VersionedFolder) || folderService.findAllByParentId(folder.id).any { doesDepthTreeContainVersionedFolder(it) } + folder.instanceOf(VersionedFolder) || folderService.findAllByParentId(folder.id).any {doesDepthTreeContainVersionedFolder(it)} } boolean isVersionedFolderFamily(Folder folder) { @@ -690,6 +728,235 @@ class VersionedFolderService extends ContainerService implement boolean doesDepthTreeContainFinalisedModel(Folder folder) { List models = folderService.findAllModelsInFolder(folder) - models.any { it.finalised } || findAllByParentId(folder.id).any { doesDepthTreeContainFinalisedModel(it) } + models.any {it.finalised} || findAllByParentId(folder.id).any {doesDepthTreeContainFinalisedModel(it)} + } + + ObjectDiff getDiffForVersionedFolders(VersionedFolder thisVersionedFolder, VersionedFolder otherVersionedFolder) { + ObjectDiff coreDiff = thisVersionedFolder.diff(otherVersionedFolder) + folderService.loadModelsIntoFolderObjectDiff(coreDiff, thisVersionedFolder, otherVersionedFolder) + coreDiff + } + + MergeDiff getMergeDiffForVersionedFolders(VersionedFolder sourceVersionedFolder, VersionedFolder targetVersionedFolder) { + VersionedFolder commonAncestor = findCommonAncestorBetweenModels(sourceVersionedFolder, targetVersionedFolder) + + ObjectDiff caDiffSource = commonAncestor.diff(sourceVersionedFolder) + ObjectDiff caDiffTarget = commonAncestor.diff(targetVersionedFolder) + + removeBranchNameDiff(caDiffSource) + removeBranchNameDiff(caDiffTarget) + + DiffBuilder + .mergeDiff(VersionedFolder) + .forMergingDiffable(sourceVersionedFolder) + .intoDiffable(targetVersionedFolder) + .havingCommonAncestor(commonAncestor) + .withCommonAncestorDiffedAgainstSource(caDiffSource) + .withCommonAncestorDiffedAgainstTarget(caDiffTarget) + .generate() + } + + void removeBranchNameDiff(ObjectDiff diff) { + + Predicate branchNamePredicate = [test: {FieldDiff fieldDiff -> + fieldDiff.fieldName == 'branchName' + },] as Predicate + + diff.diffs.removeIf(branchNamePredicate) + + ArrayDiff modelsDiff = diff.diffs.find {it.fieldName == 'models'} + if (modelsDiff) { + modelsDiff.modified.each {md -> + md.diffs.removeIf(branchNamePredicate) + } + } + + ArrayDiff folderDiff = diff.diffs.find {it.fieldName == 'folders'} + if (folderDiff) { + folderDiff.modified.each {fd -> + removeBranchNameDiff(fd) + } + } + } + + VersionedFolder mergeObjectPatchDataIntoVersionedFolder(ObjectPatchData objectPatchData, VersionedFolder targetVersionedFolder, VersionedFolder sourceVersionedFolder, + UserSecurityPolicyManager userSecurityPolicyManager) { + + + if (!objectPatchData.hasPatches()) { + log.debug('No patch data to merge into {}', targetVersionedFolder.id) + return targetVersionedFolder + } + log.debug('Merging patch data into {}', targetVersionedFolder.id) + + objectPatchData.patches.each {fieldPatch -> + switch (fieldPatch.type) { + case 'creation': + return processCreationPatchIntoVersionedFolder(fieldPatch, targetVersionedFolder, sourceVersionedFolder, userSecurityPolicyManager) + case 'deletion': + return processDeletionPatchIntoVersionedFolder(fieldPatch, targetVersionedFolder) + case 'modification': + return processModificationPatchIntoVersionedFolder(fieldPatch, targetVersionedFolder) + default: + log.warn('Unknown field patch type [{}]', fieldPatch.type) + } + } + targetVersionedFolder + } + + + void processCreationPatchIntoVersionedFolder(FieldPatchData creationPatch, VersionedFolder targetVersionedFolder, VersionedFolder sourceVersionedFolder, + UserSecurityPolicyManager userSecurityPolicyManager) { + CreatorAware domainToCopy = pathService.findResourceByPathFromRootResource(sourceVersionedFolder, creationPatch.path) + log.debug('Creating {} into {}', creationPatch.path, creationPatch.relativePathToRoot.parent) + // Potential creations are folders, models, modelItems or facets + if (Utils.parentClassIsAssignableFromChild(Folder, domainToCopy.class)) { + processCreationPatchOfFolder(domainToCopy as Folder, targetVersionedFolder, creationPatch.relativePathToRoot.parent, userSecurityPolicyManager) + } + if (Utils.parentClassIsAssignableFromChild(Model, domainToCopy.class)) { + processCreationPatchOfModel(domainToCopy as Model, targetVersionedFolder, creationPatch.relativePathToRoot.parent, userSecurityPolicyManager) + } + if (Utils.parentClassIsAssignableFromChild(ModelItem, domainToCopy.class)) { + processCreationPatchOfModelItem(domainToCopy as ModelItem, targetVersionedFolder, creationPatch.relativePathToRoot.parent, userSecurityPolicyManager) + } + if (Utils.parentClassIsAssignableFromChild(MultiFacetItemAware, domainToCopy.class)) { + processCreationPatchOfFacet(domainToCopy as MultiFacetItemAware, targetVersionedFolder, creationPatch.relativePathToRoot.parent) + } + } + + void processDeletionPatchIntoVersionedFolder(FieldPatchData deletionPatch, VersionedFolder targetVersionedFolder) { + CreatorAware domain = pathService.findResourceByPathFromRootResource(targetVersionedFolder, deletionPatch.relativePathToRoot) + log.debug('Deleting [{}]', deletionPatch.relativePathToRoot) + + // Potential deletions are folders, models, modelItems or facets + if (Utils.parentClassIsAssignableFromChild(Folder, domain.class)) { + processDeletionPatchOfFolder(domain as Folder) + } + if (Utils.parentClassIsAssignableFromChild(Model, domain.class)) { + processDeletionPatchOfModel(domain as Model) + } + if (Utils.parentClassIsAssignableFromChild(ModelItem, domain.class)) { + processDeletionPatchOfModelItem(domain as ModelItem) + } + if (Utils.parentClassIsAssignableFromChild(MultiFacetItemAware, domain.class)) { + processDeletionPatchOfFacet(domain as MultiFacetItemAware, targetVersionedFolder, deletionPatch.relativePathToRoot) + } + } + + void processModificationPatchIntoVersionedFolder(FieldPatchData modificationPatch, VersionedFolder targetVersionedFolder) { + CreatorAware domain = pathService.findResourceByPathFromRootResource(targetVersionedFolder, modificationPatch.relativePathToRoot) + String fieldName = modificationPatch.fieldName + log.debug('Modifying [{}] in [{}]', fieldName, modificationPatch.relativePathToRoot) + domain."${fieldName}" = modificationPatch.sourceValue + DomainService domainService = getDomainServices().find {it.handles(domain.class)} + if (!domainService) throw new ApiInternalException('MSXX', "No domain service to handle modification of [${domain.domainType}]") + + if (!domain.validate()) + throw new ApiInvalidModelException('MS01', 'Modified domain is invalid', domain.errors, messageSource) + domainService.save(domain, flush: false, validate: false) + } + + void processDeletionPatchOfFolder(Folder folder) { + log.debug('Deleting Folder from VersionedFolder') + folderService.delete(folder, true) + } + + void processDeletionPatchOfModel(Model model) { + ModelService modelService = folderService.findModelServiceForModel(model) + log.debug('Deleting Model from VersionedFolder') + modelService.delete(model, true) + } + + void processDeletionPatchOfModelItem(ModelItem modelItem) { + ModelItemService modelItemService = modelItemServices.find {it.handles(modelItem.class)} + if (!modelItemService) throw new ApiInternalException('MSXX', "No domain service to handle deletion of [${modelItem.domainType}]") + log.debug('Deleting ModelItem from VersionedFolder') + modelItemService.delete(modelItem) + } + + MultiFacetAware processDeletionPatchOfFacet(MultiFacetItemAware multiFacetItemAware, VersionedFolder targetVersionedFolder, Path path) { + MultiFacetItemAwareService multiFacetItemAwareService = multiFacetItemAwareServices.find {it.handles(multiFacetItemAware.class)} + if (!multiFacetItemAwareService) throw new ApiInternalException('MSXX', "No domain service to handle deletion of [${multiFacetItemAware.domainType}]") + log.debug('Deleting Facet from path [{}]', path) + multiFacetItemAwareService.delete(multiFacetItemAware) + + MultiFacetAware multiFacetAwareItem = pathService.findResourceByPathFromRootResource(targetVersionedFolder, path.getParent()) as MultiFacetAware + switch (multiFacetItemAware.domainType) { + case Metadata.simpleName: + multiFacetAwareItem.metadata.remove(multiFacetItemAware) + break + case Annotation.simpleName: + multiFacetAwareItem.annotations.remove(multiFacetItemAware) + break + case Rule.simpleName: + multiFacetAwareItem.rules.remove(multiFacetItemAware) + break + case SemanticLink.simpleName: + multiFacetAwareItem.semanticLinks.remove(multiFacetItemAware) + break + case ReferenceFile.simpleName: + multiFacetAwareItem.referenceFiles.remove(multiFacetItemAware) + break + case VersionLink.simpleName: + (multiFacetAwareItem as Model).versionLinks.remove(multiFacetItemAware) + break + } + multiFacetAwareItem + } + + void processCreationPatchOfFolder(Folder folderToCopy, VersionedFolder targetVersionedFolder, Path relativeParentPathToCopyTo, + UserSecurityPolicyManager userSecurityPolicyManager) { + log.debug('Creating Folder into VersionedFolder at [{}]', relativeParentPathToCopyTo) + Folder parentFolder = pathService.findResourceByPathFromRootResource(targetVersionedFolder, relativeParentPathToCopyTo) as Folder + folderService.copyFolder(folderToCopy, parentFolder, userSecurityPolicyManager.user, true, targetVersionedFolder.branchName, false, userSecurityPolicyManager) + } + + void processCreationPatchOfModel(Model modelToCopy, VersionedFolder targetVersionedFolder, Path relativeParentPathToCopyTo, + UserSecurityPolicyManager userSecurityPolicyManager) { + ModelService modelService = folderService.findModelServiceForModel(modelToCopy) + log.debug('Creating Model into VersionedFolder at [{}]', relativeParentPathToCopyTo) + Folder parentFolder = pathService.findResourceByPathFromRootResource(targetVersionedFolder, relativeParentPathToCopyTo) as Folder + modelService.copyModelAndValidateAndSave(modelToCopy, parentFolder, userSecurityPolicyManager.user, true, modelToCopy.label, modelToCopy.documentationVersion, + targetVersionedFolder.branchName, false, userSecurityPolicyManager) + } + + void processCreationPatchOfModelItem(ModelItem modelItemToCopy, VersionedFolder targetVersionedFolder, Path relativeParentPathToCopyTo, + UserSecurityPolicyManager userSecurityPolicyManager) { + ModelService modelService + Path modelItemToModelAbsolutePath + Path modelRelativeToTargetPath + + relativeParentPathToCopyTo.each {node -> + if (!modelService) { + // Build up the path to the model + if (!modelRelativeToTargetPath) modelRelativeToTargetPath = Path.from(node) + else modelRelativeToTargetPath.addToPathNodes(node) + + modelService = modelServices.find {s -> s.handlesPathPrefix(node.prefix)} + } else { + // Build up the path from the model to the modelitem + if (!modelItemToModelAbsolutePath) modelItemToModelAbsolutePath = Path.from(node) + else modelItemToModelAbsolutePath.addToPathNodes(node) + } + } + if (!modelService) throw new ApiInternalException('MSXX', "No model service to handle creation of model item [${modelItemToCopy.domainType}]") + + Model targetModel = pathService.findResourceByPathFromRootResource(targetVersionedFolder, modelRelativeToTargetPath) as Model + + modelService.processCreationPatchOfModelItem(modelItemToCopy, targetModel, modelItemToModelAbsolutePath.childPath, userSecurityPolicyManager) + } + + void processCreationPatchOfFacet(MultiFacetItemAware multiFacetItemAwareToCopy, VersionedFolder targetVersionedFolder, Path parentPathToCopyTo) { + MultiFacetItemAwareService multiFacetItemAwareService = multiFacetItemAwareServices.find {it.handles(multiFacetItemAwareToCopy.class)} + if (!multiFacetItemAwareService) throw new ApiInternalException('MSXX', "No domain service to handle creation of [${multiFacetItemAwareToCopy.domainType}]") + log.debug('Creating Facet into VersionedFolder at [{}]', parentPathToCopyTo) + + MultiFacetAware parentToCopyInto = pathService.findResourceByPathFromRootResource(targetVersionedFolder, parentPathToCopyTo) as MultiFacetAware + MultiFacetItemAware copy = multiFacetItemAwareService.copy(multiFacetItemAwareToCopy, parentToCopyInto) + + if (!copy.validate()) + throw new ApiInvalidModelException('MS01', 'Copied Facet is invalid', copy.errors, messageSource) + + multiFacetItemAwareService.save(copy, flush: false, validate: false) } } diff --git a/mdm-core/grails-app/views/versionedFolder/diff.gson b/mdm-core/grails-app/views/versionedFolder/diff.gson new file mode 100644 index 0000000000..4a0765e107 --- /dev/null +++ b/mdm-core/grails-app/views/versionedFolder/diff.gson @@ -0,0 +1,9 @@ +import uk.ac.ox.softeng.maurodatamapper.core.container.VersionedFolder +import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff + +model { + ObjectDiff objectDiff +} + +json tmpl.'/objectDiff/objectDiff'(objectDiff) + diff --git a/mdm-core/grails-app/views/versionedFolder/mergeDiff.gson b/mdm-core/grails-app/views/versionedFolder/mergeDiff.gson new file mode 100644 index 0000000000..5f2cad087d --- /dev/null +++ b/mdm-core/grails-app/views/versionedFolder/mergeDiff.gson @@ -0,0 +1,8 @@ +import uk.ac.ox.softeng.maurodatamapper.core.container.VersionedFolder +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.MergeDiff + +model { + MergeDiff mergeDiff +} + +json tmpl.'/mergeDiff/mergeDiff'(mergeDiff) \ No newline at end of file From cb0e18b11d4b9ddf23cfeddab668b22586514a98 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Mon, 26 Jul 2021 08:12:49 +0100 Subject: [PATCH 68/82] Chnage the ModelInterceptor check for mergeInto The write permissions need to be on the otherModelId as we;re merging the modelId into the otherModelId Therefore writing the otherModelId --- .../maurodatamapper/core/interceptor/ModelInterceptor.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/interceptor/ModelInterceptor.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/interceptor/ModelInterceptor.groovy index 4108aa9204..e0d1a9c9ea 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/interceptor/ModelInterceptor.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/interceptor/ModelInterceptor.groovy @@ -106,8 +106,8 @@ abstract class ModelInterceptor extends TieredAccessSecurableResourceInterceptor return notFound(getSecuredClass(), params.otherModelId) } - return currentUserSecurityPolicyManager.userCanWriteSecuredResourceId(getSecuredClass(), getId(), actionName) ?: - forbiddenDueToPermissions(currentUserSecurityPolicyManager.userAvailableActions(getSecuredClass(), getId())) + return currentUserSecurityPolicyManager.userCanWriteSecuredResourceId(getSecuredClass(), params.otherModelId, actionName) ?: + forbiddenDueToPermissions(currentUserSecurityPolicyManager.userAvailableActions(getSecuredClass(), params.otherModelId)) } if (actionName == 'exportModels') { From ac7eaad41d968eb7f8487ee23845ceee1db4c5ea Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Mon, 26 Jul 2021 12:01:11 +0100 Subject: [PATCH 69/82] gh-111 Fix the issue on metadata and rule representation merging * Metadata wasnt being found due to the way we were splitting the path identifier Fixed by splitting on the last `.` rather than on all of them * RuleRepresentationService wasnt a domain service so rule representations could not be found * Add warning logs when we cant find the object to create, delete or modify so we gracefully handle rather than throwing an exception * Get the path node to url decode the full identifier, this solves issues where the path has been properly encoded replacing spaces with `+` * Add test to prove gh-111 is fixed * Update the DMFS to make use of a cleaner method for finding the ids from paths This method asserts the id exists which means we know it was found * Update the DMFS to use a map of all source and target ids to make it easier to see which id we're expecting or testing --- .../maurodatamapper/path/PathNode.groovy | 8 +- .../core/facet/MetadataService.groovy | 11 +- .../rule/RuleRepresentationService.groovy | 8 +- .../core/model/ModelService.groovy | 12 + .../datamodel/DataModelFunctionalSpec.groovy | 497 +++++++++++------- .../test/functional/TestMergeData.groovy | 14 + 6 files changed, 358 insertions(+), 192 deletions(-) create mode 100644 mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/functional/TestMergeData.groovy diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy index 57dec17542..8361cd027e 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy @@ -22,6 +22,8 @@ import uk.ac.ox.softeng.maurodatamapper.version.Version import groovy.util.logging.Slf4j +import java.nio.charset.Charset + /** * @since 28/08/2020 */ @@ -65,7 +67,7 @@ class PathNode { } void parseIdentifier(String fullIdentifier, boolean isRoot, boolean isLast) { - String parsed = fullIdentifier + String parsed = URLDecoder.decode(fullIdentifier, Charset.defaultCharset()) if (isLast) { parsed.find(/^(.+?)${ATTRIBUTE_PATH_IDENTIFIER_SEPARATOR}(.+?)$/) {full, subIdentifier, attr -> attribute = attr @@ -139,7 +141,7 @@ class PathNode { boolean matchesPrefix(String otherPrefix) { if (prefix != otherPrefix) { - log.warn("Resource prefix [{}] does not match the path node [{}]", otherPrefix, this) + log.trace("Resource prefix [{}] does not match the path node [{}]", otherPrefix, this) return false } true @@ -160,7 +162,7 @@ class PathNode { identifierSplit = otherPathNode.identifier.split(/:/) if (identifier == identifierSplit[0]) return true - log.warn("Resource identifier [{}] does not match the path node [{}]", otherPathNode, this) + log.trace("Resource identifier [{}] does not match the path node [{}]", otherPathNode, this) false } diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy index d1541a8168..c8837f732a 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/MetadataService.groovy @@ -270,9 +270,16 @@ class MetadataService implements MultiFacetItemAwareService { @Override Metadata findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + String ns + String key + pathIdentifier.find(/^(.+)\.(.+)$/) {all, foundNs, foundKey -> + ns = foundNs + key = foundKey + } + Metadata.byMultiFacetAwareItemId(parentId) - .eq('namespace', pathIdentifier.split(/\./)[0]) - .eq('key', pathIdentifier.split(/\./)[1]) + .eq('namespace', ns) + .eq('key', key) .get() } } \ No newline at end of file diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentationService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentationService.groovy index d856396159..7920d1b9c1 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentationService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/rule/RuleRepresentationService.groovy @@ -18,13 +18,14 @@ package uk.ac.ox.softeng.maurodatamapper.core.facet.rule import uk.ac.ox.softeng.maurodatamapper.core.facet.Rule +import uk.ac.ox.softeng.maurodatamapper.core.traits.service.DomainService import grails.gorm.transactions.Transactional import groovy.util.logging.Slf4j @Slf4j @Transactional -class RuleRepresentationService { +class RuleRepresentationService implements DomainService { RuleRepresentation get(Serializable id) { RuleRepresentation.get(id) @@ -62,4 +63,9 @@ class RuleRepresentationService { RuleRepresentation save(RuleRepresentation ruleRepresentation) { save(flush: true, validate: true, ruleRepresentation) } + + @Override + RuleRepresentation findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + RuleRepresentation.byRuleId(parentId).eq('language', pathIdentifier).get() + } } \ No newline at end of file diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy index d94dbd9e1e..98bdddd79c 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy @@ -478,6 +478,10 @@ abstract class ModelService extends CatalogueItemService imp void processCreationPatchIntoModel(FieldPatchData creationPatch, K targetModel, K sourceModel, UserSecurityPolicyManager userSecurityPolicyManager) { CreatorAware domainToCopy = pathService.findResourceByPathFromRootResource(sourceModel, creationPatch.path) + if (!domainToCopy) { + log.warn('Could not process creation patch into model at path [{}] as no such path exists in the source', creationPatch.path) + return + } log.debug('Creating {} into {}', creationPatch.path, creationPatch.relativePathToRoot.parent) // Potential deletions are modelitems or facets from model or modelitem if (Utils.parentClassIsAssignableFromChild(ModelItem, domainToCopy.class)) { @@ -490,6 +494,10 @@ abstract class ModelService extends CatalogueItemService imp void processDeletionPatchIntoModel(FieldPatchData deletionPatch, K targetModel) { CreatorAware domain = pathService.findResourceByPathFromRootResource(targetModel, deletionPatch.relativePathToRoot) + if (!domain) { + log.warn('Could not process deletion patch into model at path [{}] as no such path exists in the target', deletionPatch.relativePathToRoot) + return + } log.debug('Deleting [{}]', deletionPatch.relativePathToRoot) // Potential deletions are modelitems or facets from model or modelitem @@ -503,6 +511,10 @@ abstract class ModelService extends CatalogueItemService imp void processModificationPatchIntoModel(FieldPatchData modificationPatch, K targetModel) { CreatorAware domain = pathService.findResourceByPathFromRootResource(targetModel, modificationPatch.relativePathToRoot) + if (!domain) { + log.warn('Could not process modifiation patch into model at path [{}] as no such path exists in the target', modificationPatch.relativePathToRoot) + return + } String fieldName = modificationPatch.fieldName log.debug('Modifying [{}] in [{}]', fieldName, modificationPatch.relativePathToRoot) domain."${fieldName}" = modificationPatch.sourceValue 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 9aff8bb24d..cce703cf5f 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 @@ -25,6 +25,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClass import uk.ac.ox.softeng.maurodatamapper.test.functional.ResourceFunctionalSpec +import uk.ac.ox.softeng.maurodatamapper.test.functional.TestMergeData import uk.ac.ox.softeng.maurodatamapper.util.Utils import uk.ac.ox.softeng.maurodatamapper.version.Version @@ -35,7 +36,7 @@ import grails.web.mime.MimeType import groovy.util.logging.Slf4j import spock.lang.Shared -import java.util.function.Predicate +import java.nio.charset.Charset import static uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress.FUNCTIONAL_TEST @@ -157,7 +158,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "leftId": "${json-unit.matches:id}", "rightId": "${json-unit.matches:id}", "label": "Functional Test Model", - "count": 11, + "count": 14, "diffs": [ { "description": { @@ -167,20 +168,13 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "commonAncestorValue": null } }, - { - "branchName": { - "left": "main", - "right": "source", - "isMergeConflict": false - } - }, { "dataClasses": { "deleted": [ { "value": { "id": "${json-unit.matches:id}", - "label": "deleteAndModify", + "label": "deleteLeftOnly", "breadcrumbs": [ { "id": "${json-unit.matches:id}", @@ -190,8 +184,10 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { } ] }, - "isMergeConflict": true, - "commonAncestorValue": { + "isMergeConflict": false + }, + { + "value": { "id": "${json-unit.matches:id}", "label": "deleteAndModify", "breadcrumbs": [ @@ -202,12 +198,11 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "finalised": true } ] - } - }, - { - "value": { + }, + "isMergeConflict": true, + "commonAncestorValue": { "id": "${json-unit.matches:id}", - "label": "deleteLeftOnly", + "label": "deleteAndModify", "breadcrumbs": [ { "id": "${json-unit.matches:id}", @@ -216,8 +211,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "finalised": true } ] - }, - "isMergeConflict": false + } } ], "created": [ @@ -268,7 +262,38 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { { "leftId": "${json-unit.matches:id}", "rightId": "${json-unit.matches:id}", - "label": "addAndAddReturningDifference", + "label": "modifyLeftOnly", + "leftBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": true + } + ], + "rightBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": true + } + ], + "count": 1, + "diffs": [ + { + "description": { + "left": null, + "right": "Description", + "isMergeConflict": false + } + } + ] + }, + { + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "modifyAndModifyReturningDifference", "leftBreadcrumbs": [ { "id": "${json-unit.matches:id}", @@ -372,7 +397,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { { "leftId": "${json-unit.matches:id}", "rightId": "${json-unit.matches:id}", - "label": "modifyAndModifyReturningDifference", + "label": "addAndAddReturningDifference", "leftBreadcrumbs": [ { "id": "${json-unit.matches:id}", @@ -400,33 +425,61 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { } } ] + } + ] + } + }, + { + "metadata": { + "deleted": [ + { + "value": { + "id": "${json-unit.matches:id}", + "namespace": "functional.test", + "key": "deleteFromSource", + "value": "some other original value" + }, + "isMergeConflict": false + } + ], + "created": [ + { + "value": { + "id": "${json-unit.matches:id}", + "namespace": "functional.test", + "key": "addToSourceOnly", + "value": "adding to source only" + }, + "isMergeConflict": false }, + { + "value": { + "id": "${json-unit.matches:id}", + "namespace": "functional.test", + "key": "modifyAndDelete", + "value": "source has modified this also" + }, + "isMergeConflict": true, + "commonAncestorValue": { + "id": "${json-unit.matches:id}", + "namespace": "functional.test", + "key": "modifyAndDelete", + "value": "some other original value 2" + } + } + ], + "modified": [ { "leftId": "${json-unit.matches:id}", "rightId": "${json-unit.matches:id}", - "label": "modifyLeftOnly", - "leftBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": true - } - ], - "rightBreadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": true - } - ], + "namespace": "functional.test", + "key": "modifyOnSource", "count": 1, "diffs": [ { - "description": { - "left": null, - "right": "Description", + "value": { + "left": "some original value", + "right": "source has modified this", "isMergeConflict": false } } @@ -445,17 +498,8 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "targetId": "${json-unit.matches:id}", "path": "dm:Functional Test Model$source", "label": "Functional Test Model", - "count": 11, + "count": 14, "diffs": [ - { - "fieldName": "branchName", - "path": "dm:Functional Test Model$source@branchName", - "sourceValue": "source", - "targetValue": "main", - "commonAncestorValue": "main", - "isMergeConflict": false, - "type": "modification" - }, { "fieldName": "description", "path": "dm:Functional Test Model$source@description", @@ -527,6 +571,33 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "commonAncestorValue": null, "isMergeConflict": false, "type": "modification" + }, + { + "path": "dm:Functional Test Model$source|md:functional.test.addToSourceOnly", + "isMergeConflict": false, + "isSourceModificationAndTargetDeletion": false, + "type": "creation" + }, + { + "path": "dm:Functional Test Model$source|md:functional.test.modifyAndDelete", + "isMergeConflict": true, + "isSourceModificationAndTargetDeletion": true, + "type": "creation" + }, + { + "path": "dm:Functional Test Model$source|md:functional.test.deleteFromSource", + "isMergeConflict": false, + "isSourceDeletionAndTargetModification": false, + "type": "deletion" + }, + { + "fieldName": "value", + "path": "dm:Functional Test Model$source|md:functional.test.modifyOnSource@value", + "sourceValue": "source has modified this", + "targetValue": "some original value", + "commonAncestorValue": "some original value", + "isMergeConflict": false, + "type": "modification" } ] }''' @@ -1484,7 +1555,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { void 'MD02 : test finding merge difference of two complex datamodels'() { given: - Map mergeData = buildComplexDataModelsForMerging() + TestMergeData mergeData = buildComplexDataModelsForMerging() when: GET("$mergeData.source/mergeDiff/$mergeData.target", STRING_ARG) @@ -1496,7 +1567,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { cleanup: cleanUpData(mergeData.source) cleanUpData(mergeData.target) - cleanUpData(mergeData.id) + cleanUpData(mergeData.commonAncestor) } void 'MD03 : test finding merge difference of two datamodels with the new style'() { @@ -1543,7 +1614,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { void 'MD04 : test finding merge difference of two complex datamodels with the new style'() { given: - Map mergeData = buildComplexDataModelsForMerging() + TestMergeData mergeData = buildComplexDataModelsForMerging() when: GET("$mergeData.source/mergeDiff/$mergeData.target?isLegacy=false", STRING_ARG) @@ -1554,7 +1625,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { cleanup: cleanUpData(mergeData.source) cleanUpData(mergeData.target) - cleanUpData(mergeData.id) + cleanUpData(mergeData.commonAncestor) } void 'MP01 : test merging diff with no patch data'() { @@ -1635,7 +1706,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { void 'MP03 : test merging diff into draft model'() { given: - Map mergeData = buildComplexDataModelsForMerging() + TestMergeData mergeData = buildComplexDataModelsForMerging() when: GET("$mergeData.source/mergeDiff/$mergeData.target", STRING_ARG) @@ -1662,27 +1733,27 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { deleted : [ [ - id : mergeData.deleteAndModify, + id : mergeData.targetMap.deleteAndModify, label: "deleteAndModify" ], [ - id : mergeData.deleteLeftOnly, + id : mergeData.targetMap.deleteLeftOnly, label: "deleteLeftOnly" ] ], created : [ [ - id : mergeData.addLeftOnly, + id : mergeData.sourceMap.addLeftOnly, label: "addLeftOnly" ], [ - id : mergeData.modifyAndDelete, + id : mergeData.sourceMap.modifyAndDelete, label: "modifyAndDelete" ] ], modified : [ [ - leftId: mergeData.addAndAddReturningDifference, + leftId: mergeData.targetMap.addAndAddReturningDifference, label : "addAndAddReturningDifference", diffs : [ [ @@ -1692,7 +1763,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { ] ], [ - leftId: mergeData.existingClass, + leftId: mergeData.targetMap.existingClass, label : "existingClass", diffs : [ [ @@ -1700,13 +1771,13 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { deleted : [ [ - id : mergeData.deleteLeftOnlyFromExistingClass, + id : mergeData.targetMap.deleteLeftOnlyFromExistingClass, label: "deleteLeftOnlyFromExistingClass" ] ], created : [ [ - id : mergeData.addLeftToExistingClass, + id : mergeData.sourceMap.addLeftToExistingClass, label: "addLeftToExistingClass" ] ] @@ -1715,7 +1786,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { ] ], [ - leftId: mergeData.modifyAndModifyReturningDifference, + leftId: mergeData.targetMap.modifyAndModifyReturningDifference, label : "modifyAndModifyReturningDifference", diffs : [ [ @@ -1725,7 +1796,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { ] ], [ - leftId: mergeData.modifyLeftOnly, + leftId: mergeData.targetMap.modifyLeftOnly, label : "modifyLeftOnly", diffs : [ [ @@ -1761,7 +1832,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { responseBody().items.find {dataClass -> dataClass.label == 'modifyLeftOnly'}.description == 'modifiedDescriptionSourceOnly' when: - GET("$mergeData.target/dataClasses/$mergeData.existingClass/dataClasses") + GET("$mergeData.target/dataClasses/$mergeData.targetMap.existingClass/dataClasses") then: responseBody().items.label as Set == ['addRightToExistingClass', 'addLeftToExistingClass'] as Set @@ -1780,7 +1851,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { cleanup: cleanUpData(mergeData.source) cleanUpData(mergeData.target) - cleanUpData(mergeData.id) + cleanUpData(mergeData.commonAncestor) } void 'MP04 : test merging metadata diff into draft model'() { @@ -1866,15 +1937,8 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { "leftId": "${json-unit.matches:id}", "rightId": "${json-unit.matches:id}", "label": "Functional Test Model", - "count": 7, + "count": 6, "diffs": [ - { - "branchName": { - "left": "main", - "right": "source", - "isMergeConflict": false - } - }, { "description": { "left": "DescriptionRight", @@ -2319,18 +2383,17 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { void 'MP08 : test merging diff into draft model using new style'() { given: - Map mergeData = buildComplexDataModelsForMerging() + TestMergeData mergeData = buildComplexDataModelsForMerging() when: GET("$mergeData.source/mergeDiff/$mergeData.target?isLegacy=false") then: verifyResponse OK, response - responseBody().diffs.size() == 11 + responseBody().diffs.size() == 14 when: List patches = responseBody().diffs - patches.removeIf([test: {Map map -> map.fieldName == 'branchName'}] as Predicate) PUT("$mergeData.source/mergeInto/$mergeData.target?isLegacy=false", [ patch: [ targetId: responseBody().targetId, @@ -2359,19 +2422,98 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { responseBody().items.find {dataClass -> dataClass.label == 'modifyLeftOnly'}.description == 'Description' when: - GET("$mergeData.target/dataClasses/$mergeData.existingClass/dataClasses") + GET("$mergeData.target/dataClasses/$mergeData.targetMap.existingClass/dataClasses") then: responseBody().items.label as Set == ['addRightToExistingClass', 'addLeftToExistingClass'] as Set + when: + GET("${mergeData.target}/metadata") + + then: + responseBody().items.find {it.namespace == 'functional.test' && it.key == 'modifyOnSource'}.value == 'source has modified this' + responseBody().items.find {it.namespace == 'functional.test' && it.key == 'modifyAndDelete'}.value == 'source has modified this also' + !responseBody().items.find {it.namespace == 'functional.test' && it.key == 'metadataDeleteFromSource'} + responseBody().items.find {it.namespace == 'functional.test' && it.key == 'addToSourceOnly'} + cleanup: cleanUpData(mergeData.source) cleanUpData(mergeData.target) - cleanUpData(mergeData.id) + cleanUpData(mergeData.commonAncestor) } + void 'MP09 : test merging new style diff with metadata creation gh-111'() { + given: + String id = createNewItem(validJson) + POST("$id/rules", [name: 'Bootstrapped versioning V2Model Rule']) + verifyResponse(CREATED, response) + + PUT("$id/finalise", [versionChangeType: 'Major']) + verifyResponse OK, response + PUT("$id/newBranchModelVersion", [branchName: VersionAwareConstraints.DEFAULT_BRANCH_NAME]) + verifyResponse CREATED, response + String target = responseBody().id + PUT("$id/newBranchModelVersion", [branchName: 'interestingBranch']) + verifyResponse CREATED, response + String source = responseBody().id - Map buildComplexDataModelsForMerging() { + POST("$source/metadata", [namespace: 'test.com', key: 'testProperty', value: 'testValue']) + verifyResponse(CREATED, response) + + String ruleId = getIdFromPath(source, 'dm:Functional Test Model$interestingBranch|ru:Bootstrapped versioning V2Model Rule') + POST("$source/rules/${ruleId}/representations", [ + language : 'sql', + representation: 'testing' + ]) + verifyResponse(CREATED, response) + + when: + PUT("$source/mergeInto/$target?isLegacy=false", [ + changeNotice: "Metadata test", + deleteBranch: false, + patch : [ + sourceId: source, + targetId: target, + count : 2, + patches : [ + [ + path : 'dm:Functional Test Model$interestingBranch|md:test.com.testProperty', + isMergeConflict : false, + isSourceModificationAndTargetDeletion: false, + type : 'creation', + branchSelected : 'source', + branchNameSelected : 'interestingBranch' + ], + [ + + path : 'dm:Functional Test Model$interestingBranch|ru:Bootstrapped versioning V2Model Rule|rr:sql', + isMergeConflict : false, + isSourceModificationAndTargetDeletion: false, + type : 'creation', + branchSelected : 'source', + branchNameSelected : 'interestingBranch' + ] + ] + ] + ]) + + then: + verifyResponse(OK, response) + responseBody().id == target + + when: + GET("${target}/metadata") + + then: + responseBody().items.find {it.namespace == 'test.com' && it.key == 'testProperty'} + + cleanup: + cleanUpData(source) + cleanUpData(target) + cleanUpData(id) + } + + TestMergeData buildComplexDataModelsForMerging() { String id = createNewItem(validJson) @@ -2401,6 +2543,15 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { POST("$id/dataClasses/$caExistingClass/dataClasses", [label: 'deleteRightOnlyFromExistingClass']) verifyResponse CREATED, response + POST("$id/metadata", [namespace: 'functional.test', key: 'nothingDifferent', value: 'this shouldnt change']) + verifyResponse CREATED, response + POST("$id/metadata", [namespace: 'functional.test', key: 'modifyOnSource', value: 'some original value']) + verifyResponse CREATED, response + POST("$id/metadata", [namespace: 'functional.test', key: 'deleteFromSource', value: 'some other original value']) + verifyResponse CREATED, response + POST("$id/metadata", [namespace: 'functional.test', key: 'modifyAndDelete', value: 'some other original value 2']) + verifyResponse CREATED, response + PUT("$id/finalise", [versionChangeType: 'Major']) verifyResponse OK, response PUT("$id/newBranchModelVersion", [branchName: VersionAwareConstraints.DEFAULT_BRANCH_NAME]) @@ -2410,116 +2561,105 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { verifyResponse CREATED, response String source = responseBody().id - GET("$source/path/dc%3AexistingClass") - verifyResponse OK, response - String sourceExistingClass = responseBody().id - GET("$source/path/dc%3AexistingClass%7Cdc%3AdeleteLeftOnlyFromExistingClass") - verifyResponse OK, response - String deleteLeftOnlyFromExistingClass = responseBody().id - GET("$source/path/dc%3AdeleteLeftOnly") - verifyResponse OK, response - String deleteLeftOnly = responseBody().id - GET("$source/path/dc%3AdeleteAndDelete") - verifyResponse OK, response - String deleteAndDelete = responseBody().id - GET("$source/path/dc%3AdeleteAndModify") - verifyResponse OK, response - String deleteAndModify = responseBody().id - - GET("$source/path/dc%3AmodifyLeftOnly") - verifyResponse OK, response - String modifyLeftOnly = responseBody().id - GET("$source/path/dc%3AmodifyAndDelete") - verifyResponse OK, response - String modifyAndDelete = responseBody().id - GET("$source/path/dc%3AmodifyAndModifyReturningNoDifference") - verifyResponse OK, response - String modifyAndModifyReturningNoDifference = responseBody().id - GET("$source/path/dc%3AmodifyAndModifyReturningDifference") - verifyResponse OK, response - String modifyAndModifyReturningDifference = responseBody().id + // Get source ids + Map sourceMap = [ + existingClass : getIdFromPath(source, 'dc:existingClass'), + deleteLeftOnlyFromExistingClass : getIdFromPath(source, 'dc:deleteLeftOnlyFromExistingClass'), + deleteLeftOnly : getIdFromPath(source, 'dc:deleteLeftOnly'), + deleteAndDelete : getIdFromPath(source, 'dc:deleteAndDelete'), + deleteAndModify : getIdFromPath(source, 'dc:deleteAndModify'), + modifyLeftOnly : getIdFromPath(source, 'dc:modifyLeftOnly'), + modifyAndDelete : getIdFromPath(source, 'dc:modifyAndDelete'), + modifyAndModifyReturningNoDifference: getIdFromPath(source, 'dc:modifyAndModifyReturningNoDifference'), + modifyAndModifyReturningDifference : getIdFromPath(source, 'dc:modifyAndModifyReturningDifference'), + metadataModifyOnSource : getIdFromPath(source, 'md:functional.test.modifyOnSource'), + metadataDeleteFromSource : getIdFromPath(source, 'md:functional.test.deleteFromSource'), + metadataModifyAndDelete : getIdFromPath(source, 'md:functional.test.modifyAndDelete'), + ] - DELETE("$source/dataClasses/$deleteAndDelete") + // Modify source + DELETE("$source/dataClasses/${sourceMap.deleteAndDelete}") verifyResponse NO_CONTENT, response - DELETE("$source/dataClasses/$sourceExistingClass/dataClasses/$deleteLeftOnlyFromExistingClass") + DELETE("$source/dataClasses/${sourceMap.existingClass}/dataClasses/${sourceMap.deleteLeftOnlyFromExistingClass}") verifyResponse NO_CONTENT, response - DELETE("$source/dataClasses/$deleteLeftOnly") + DELETE("$source/dataClasses/${sourceMap.deleteLeftOnly}") verifyResponse NO_CONTENT, response - DELETE("$source/dataClasses/$deleteAndModify") + DELETE("$source/dataClasses/${sourceMap.deleteAndModify}") verifyResponse NO_CONTENT, response - PUT("$source/dataClasses/$modifyLeftOnly", [description: 'Description']) + PUT("$source/dataClasses/${sourceMap.modifyLeftOnly}", [description: 'Description']) verifyResponse OK, response - PUT("$source/dataClasses/$modifyAndDelete", [description: 'Description']) + PUT("$source/dataClasses/${sourceMap.modifyAndDelete}", [description: 'Description']) verifyResponse OK, response - PUT("$source/dataClasses/$modifyAndModifyReturningNoDifference", [description: 'Description']) + PUT("$source/dataClasses/${sourceMap.modifyAndModifyReturningNoDifference}", [description: 'Description']) verifyResponse OK, response - PUT("$source/dataClasses/$modifyAndModifyReturningDifference", [description: 'DescriptionLeft']) + PUT("$source/dataClasses/${sourceMap.modifyAndModifyReturningDifference}", [description: 'DescriptionLeft']) verifyResponse OK, response - POST("$source/dataClasses/$sourceExistingClass/dataClasses", [label: 'addLeftToExistingClass']) + POST("$source/dataClasses/${sourceMap.existingClass}/dataClasses", [label: 'addLeftToExistingClass']) verifyResponse CREATED, response - String addLeftToExistingClass = responseBody().id + sourceMap.addLeftToExistingClass = responseBody().id POST("$source/dataClasses", [label: 'addLeftOnly']) verifyResponse CREATED, response - String addLeftOnly = responseBody().id + sourceMap.addLeftOnly = responseBody().id POST("$source/dataClasses", [label: 'addAndAddReturningNoDifference']) verifyResponse CREATED, response POST("$source/dataClasses", [label: 'addAndAddReturningDifference', description: 'DescriptionLeft']) verifyResponse CREATED, response - String addAndAddReturningDifference = responseBody().id + sourceMap.addAndAddReturningDifference = responseBody().id PUT("$source", [description: 'DescriptionLeft']) verifyResponse OK, response - GET("$target/path/dc%3AexistingClass") - verifyResponse OK, response - String targetExistingClass = responseBody().id - GET("$target/path/dc%3AexistingClass%7Cdc%3AdeleteRightOnlyFromExistingClass") - verifyResponse OK, response - String deleteRightOnlyFromExistingClass = responseBody().id - GET("$target/path/dc%3AdeleteRightOnly") - verifyResponse OK, response - String deleteRightOnly = responseBody().id - GET("$target/path/dc%3AdeleteAndDelete") + POST("$source/metadata", [namespace: 'functional.test', key: 'addToSourceOnly', value: 'adding to source only']) + verifyResponse CREATED, response + PUT("$source/metadata/${sourceMap.metadataModifyOnSource}", [value: 'source has modified this']) verifyResponse OK, response - deleteAndDelete = responseBody().id - GET("$target/path/dc%3AmodifyAndDelete") + PUT("$source/metadata/${sourceMap.metadataModifyAndDelete}", [value: 'source has modified this also']) verifyResponse OK, response - String targetModifyAndDelete = responseBody().id + DELETE("$source/metadata/${sourceMap.metadataDeleteFromSource}") + verifyResponse NO_CONTENT, response - GET("$target/path/dc%3AmodifyRightOnly") - verifyResponse OK, response - String modifyRightOnly = responseBody().id - GET("$target/path/dc%3AdeleteAndModify") - verifyResponse OK, response - deleteAndModify = responseBody().id - GET("$target/path/dc%3AmodifyAndModifyReturningNoDifference") - verifyResponse OK, response - modifyAndModifyReturningNoDifference = responseBody().id - GET("$target/path/dc%3AmodifyAndModifyReturningDifference") - verifyResponse OK, response - modifyAndModifyReturningDifference = responseBody().id + // Get target ids + // Get source ids + Map targetMap = [ + existingClass : getIdFromPath(target, 'dc:existingClass'), + deleteLeftOnlyFromExistingClass : getIdFromPath(target, 'dc:deleteLeftOnlyFromExistingClass'), + deleteRightOnlyFromExistingClass : getIdFromPath(target, 'dc:deleteRightOnlyFromExistingClass'), + deleteLeftOnly : getIdFromPath(target, 'dc:deleteLeftOnly'), + deleteRightOnly : getIdFromPath(target, 'dc:deleteRightOnly'), + deleteAndDelete : getIdFromPath(target, 'dc:deleteAndDelete'), + deleteAndModify : getIdFromPath(target, 'dc:deleteAndModify'), + modifyLeftOnly : getIdFromPath(target, 'dc:modifyLeftOnly'), + modifyRightOnly : getIdFromPath(target, 'dc:modifyRightOnly'), + modifyAndDelete : getIdFromPath(target, 'dc:modifyAndDelete'), + modifyAndModifyReturningNoDifference: getIdFromPath(target, 'dc:modifyAndModifyReturningNoDifference'), + modifyAndModifyReturningDifference : getIdFromPath(target, 'dc:modifyAndModifyReturningDifference'), + metadataModifyOnSource : getIdFromPath(target, 'md:functional.test.modifyOnSource'), + metadataDeleteFromSource : getIdFromPath(target, 'md:functional.test.deleteFromSource'), + metadataModifyAndDelete : getIdFromPath(target, 'md:functional.test.modifyAndDelete'), + ] - DELETE("$target/dataClasses/$targetExistingClass/dataClasses/$deleteRightOnlyFromExistingClass") + // Modify target + DELETE("$target/dataClasses/${targetMap.existingClass}/dataClasses/${targetMap.deleteRightOnlyFromExistingClass}") verifyResponse NO_CONTENT, response - DELETE("$target/dataClasses/$deleteRightOnly") + DELETE("$target/dataClasses/${targetMap.deleteRightOnly}") verifyResponse NO_CONTENT, response - DELETE("$target/dataClasses/$deleteAndDelete") + DELETE("$target/dataClasses/${targetMap.deleteAndDelete}") verifyResponse NO_CONTENT, response - DELETE("$target/dataClasses/$targetModifyAndDelete") + DELETE("$target/dataClasses/${targetMap.modifyAndDelete}") verifyResponse NO_CONTENT, response - PUT("$target/dataClasses/$modifyRightOnly", [description: 'Description']) + PUT("$target/dataClasses/${targetMap.modifyRightOnly}", [description: 'Description']) verifyResponse OK, response - PUT("$target/dataClasses/$deleteAndModify", [description: 'Description']) + PUT("$target/dataClasses/${targetMap.deleteAndModify}", [description: 'Description']) verifyResponse OK, response - PUT("$target/dataClasses/$modifyAndModifyReturningNoDifference", [description: 'Description']) + PUT("$target/dataClasses/${targetMap.modifyAndModifyReturningNoDifference}", [description: 'Description']) verifyResponse OK, response - PUT("$target/dataClasses/$modifyAndModifyReturningDifference", [description: 'DescriptionRight']) + PUT("$target/dataClasses/${targetMap.modifyAndModifyReturningDifference}", [description: 'DescriptionRight']) verifyResponse OK, response - POST("$target/dataClasses/$targetExistingClass/dataClasses", [label: 'addRightToExistingClass']) + POST("$target/dataClasses/${targetMap.existingClass}/dataClasses", [label: 'addRightToExistingClass']) verifyResponse CREATED, response POST("$target/dataClasses", [label: 'addRightOnly']) verifyResponse CREATED, response @@ -2527,41 +2667,26 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { verifyResponse CREATED, response POST("$target/dataClasses", [label: 'addAndAddReturningDifference', description: 'DescriptionRight']) verifyResponse CREATED, response - addAndAddReturningDifference = responseBody().id + targetMap.addAndAddReturningDifference = responseBody().id PUT("$target", [description: 'DescriptionRight']) verifyResponse OK, response + DELETE("$target/metadata/${targetMap.metadataModifyAndDelete}") + verifyResponse NO_CONTENT, response - // for mergeInto json - GET("$target/path/dc%3AdeleteLeftOnly") - verifyResponse OK, response - deleteLeftOnly = responseBody().id - GET("$target/path/dc%3AmodifyLeftOnly") - verifyResponse OK, response - modifyLeftOnly = responseBody().id - GET("$target/path/dc%3AexistingClass%7Cdc%3AdeleteLeftOnlyFromExistingClass") + + new TestMergeData(commonAncestor: id, + source: source, + target: target, + sourceMap: sourceMap, + targetMap: targetMap) + } + + String getIdFromPath(String rootResourceId, String path) { + GET("$rootResourceId/path/${URLEncoder.encode(path, Charset.defaultCharset())}") verifyResponse OK, response - deleteLeftOnlyFromExistingClass = responseBody().id - - [id : id, - source : source, - target : target, - // For legacy testing - existingClass : targetExistingClass, - deleteLeftOnlyFromExistingClass : deleteLeftOnlyFromExistingClass, - deleteLeftOnly : deleteLeftOnly, - deleteAndDelete : deleteAndDelete, - deleteAndModify : deleteAndModify, - modifyLeftOnly : modifyLeftOnly, - modifyAndDelete : modifyAndDelete, - modifyAndModifyReturningNoDifference: modifyAndModifyReturningNoDifference, - modifyAndModifyReturningDifference : modifyAndModifyReturningDifference, - deleteRightOnlyFromExistingClass : deleteRightOnlyFromExistingClass, - deleteRightOnly : deleteRightOnly, - modifyRightOnly : modifyRightOnly, - addLeftOnly : addLeftOnly, - addAndAddReturningDifference : addAndAddReturningDifference, - addLeftToExistingClass : addLeftToExistingClass] + assert responseBody().id + responseBody().id } void 'test changing folder from DataModel context'() { diff --git a/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/functional/TestMergeData.groovy b/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/functional/TestMergeData.groovy new file mode 100644 index 0000000000..506727cea8 --- /dev/null +++ b/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/functional/TestMergeData.groovy @@ -0,0 +1,14 @@ +package uk.ac.ox.softeng.maurodatamapper.test.functional + +/** + * @since 26/07/2021 + */ +class TestMergeData { + + String source + String target + String commonAncestor + + Map sourceMap + Map targetMap +} From 71e24dd3ec9694d825ca58f5a9fd562829ad5402 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Tue, 27 Jul 2021 15:21:59 +0100 Subject: [PATCH 70/82] Diff and mergeDiff endpoints tested * Add merge models to test using the DM functionalspec as the base for changes * Update ObjectDiff to handle diffing an array of versioned objects where the versioning would indicate different models which are actually the same as they;'re inside a VersionedFolder * Tidy up the copyFolder and copyVersionedFolder methods to make them both the same to avoid duplication and issues found in VF tests * Update the path identifier in VF to use the Model identifier separator --- .../core/container/VersionedFolder.groovy | 3 +- .../core/container/FolderService.groovy | 157 ++- .../container/VersionedFolderService.groovy | 114 +- .../core/diff/bidirectional/ObjectDiff.groovy | 24 +- .../VersionedFolderFunctionalSpec.groovy | 983 ++++++++++++++++++ 5 files changed, 1130 insertions(+), 151 deletions(-) diff --git a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolder.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolder.groovy index f7b10e0a97..eacd162953 100644 --- a/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolder.groovy +++ b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolder.groovy @@ -29,6 +29,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.VersionAware import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CallableConstraints import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CreatorAwareConstraints import uk.ac.ox.softeng.maurodatamapper.hibernate.VersionUserType +import uk.ac.ox.softeng.maurodatamapper.path.PathNode import grails.gorm.DetachedCriteria import grails.plugins.hibernate.search.HibernateSearchApi @@ -88,7 +89,7 @@ class VersionedFolder extends Folder implements VersionAware, VersionLinkAware, @Override String getPathIdentifier() { - "${label}.${modelVersion ?: branchName}" + "${label}${PathNode.MODEL_PATH_IDENTIFIER_SEPARATOR}${modelVersion ?: branchName}" } static DetachedCriteria by() { 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 502afe8733..36f1401ec7 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 @@ -25,6 +25,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff import uk.ac.ox.softeng.maurodatamapper.core.facet.EditService import uk.ac.ox.softeng.maurodatamapper.core.facet.EditTitle import uk.ac.ox.softeng.maurodatamapper.core.facet.Rule +import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkType import uk.ac.ox.softeng.maurodatamapper.core.model.ContainerService import uk.ac.ox.softeng.maurodatamapper.core.model.Model import uk.ac.ox.softeng.maurodatamapper.core.model.ModelService @@ -32,10 +33,12 @@ 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.util.Utils +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.DetachedCriteria import grails.gorm.transactions.Transactional import groovy.util.logging.Slf4j +import org.grails.datastore.gorm.GormEntity import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.MessageSource @@ -176,6 +179,24 @@ class FolderService extends ContainerService { folder } + @Override + Folder save(Map args, Folder folder) { + // If inserting then we will need to update all the facets with the CIs "id" after insert + // If updating then we dont need to do this as the ID has already been done + boolean inserting = !(folder as GormEntity).ident() ?: args.insert + Map saveArgs = new HashMap(args) + if (args.flush) { + saveArgs.remove('flush') + (folder as GormEntity).save(saveArgs) + if (inserting) updateFacetsAfterInsertingMultiFacetAware(folder) + sessionFactory.currentSession.flush() + } else { + (folder as GormEntity).save(args) + if (inserting) updateFacetsAfterInsertingMultiFacetAware(folder) + } + folder + } + /** * Find all resources by the defined user security policy manager. If none provided then assume no security policy in place in which case * everything is public. @@ -256,32 +277,6 @@ class FolderService extends ContainerService { getFullPathDomains(folder) } - Folder copyBasicFolderInformation(Folder original, Folder copy, User copier) { - copy.createdBy = copier.emailAddress - copy.label = original.label - copy.description = original.description - - metadataService.findAllByMultiFacetAwareItemId(original.id).each {copy.addToMetadata(it.namespace, it.key, it.value, copier.emailAddress)} - ruleService.findAllByMultiFacetAwareItemId(original.id).each {rule -> - Rule copiedRule = new Rule(name: rule.name, description: rule.description, createdBy: copier.emailAddress) - rule.ruleRepresentations.each {ruleRepresentation -> - copiedRule.addToRuleRepresentations(language: ruleRepresentation.language, - representation: ruleRepresentation.representation, - createdBy: copier.emailAddress) - } - copy.addToRules(copiedRule) - } - - semanticLinkService.findAllBySourceMultiFacetAwareItemId(original.id).each {link -> - copy.addToSemanticLinks(createdBy: copier.emailAddress, linkType: link.linkType, - targetMultiFacetAwareItemId: link.targetMultiFacetAwareItemId, - targetMultiFacetAwareItemDomainType: link.targetMultiFacetAwareItemDomainType, - unconfirmed: true) - } - - copy - } - List findAllModelsInFolder(Folder folder) { if (!modelServices) return [] modelServices.collectMany {service -> @@ -312,28 +307,18 @@ class FolderService extends ContainerService { modelService } - Folder copyFolder(Folder original, Folder folderToCopyInto, User copier, boolean copyPermissions, String modelBranchName, boolean throwErrors, - UserSecurityPolicyManager userSecurityPolicyManager) { + Folder copyFolder(Folder original, Folder folderToCopyInto, User copier, boolean copyPermissions, String modelBranchName, + Version modelCopyDocVersion, boolean throwErrors, UserSecurityPolicyManager userSecurityPolicyManager) { + log.debug('Copying folder {}', original.id) Folder copiedFolder = new Folder(deleted: false, parentFolder: folderToCopyInto, createdBy: copier, description: original.description, label: original.label) + copyFolder(original, copiedFolder, original.label, copier, copyPermissions, modelBranchName, modelCopyDocVersion, throwErrors, userSecurityPolicyManager) + } - metadataService.findAllByMultiFacetAwareItemId(original.id).each {copiedFolder.addToMetadata(it.namespace, it.key, it.value, copier.emailAddress)} - ruleService.findAllByMultiFacetAwareItemId(original.id).each {rule -> - Rule copiedRule = new Rule(name: rule.name, description: rule.description, createdBy: copier.emailAddress) - rule.ruleRepresentations.each {ruleRepresentation -> - copiedRule.addToRuleRepresentations(language: ruleRepresentation.language, - representation: ruleRepresentation.representation, - createdBy: copier.emailAddress) - } - copiedFolder.addToRules(copiedRule) - } - - semanticLinkService.findAllBySourceMultiFacetAwareItemId(original.id).each {link -> - copiedFolder.addToSemanticLinks(createdBy: copier.emailAddress, linkType: link.linkType, - targetMultiFacetAwareItemId: link.targetMultiFacetAwareItemId, - targetMultiFacetAwareItemDomainType: link.targetMultiFacetAwareItemDomainType, - unconfirmed: true) - } + Folder copyFolder(Folder original, Folder copiedFolder, String label, User copier, boolean copyPermissions, String modelBranchName, + Version modelCopyDocVersion, boolean throwErrors, + UserSecurityPolicyManager userSecurityPolicyManager) { + copiedFolder = copyBasicFolderInformation(original, copiedFolder, label, copier) if (copyPermissions) { if (throwErrors) { @@ -342,9 +327,11 @@ class FolderService extends ContainerService { log.warn('Permission copying is not yet implemented') } + log.debug('Validating and saving copy') + setFolderRefinesFolder(copiedFolder, original, copier) if (copiedFolder.validate()) { - save(copiedFolder, validate: false) + save(copiedFolder, flush: true, validate: false) editService.createAndSaveEdit(EditTitle.COPY, copiedFolder.id, copiedFolder.domainType, "Folder ${original.label} created as a copy of ${original.id}", copier @@ -356,19 +343,79 @@ class FolderService extends ContainerService { } else throw new ApiInvalidModelException('FS01', 'Copied Folder is invalid', copiedFolder.errors, messageSource) - List models = findAllModelsInFolder(original) - models.each {modelToCopy -> - ModelService modelService = findModelServiceForModel(modelToCopy) - modelService.copyModelAndValidateAndSave(modelToCopy, copiedFolder, userSecurityPolicyManager.user, true, modelToCopy.label, modelToCopy.documentationVersion, - modelBranchName, false, userSecurityPolicyManager) + // folderCopy.trackChanges() + + copyFolderContents(original, copiedFolder, copier, copyPermissions, modelCopyDocVersion, modelBranchName, throwErrors, userSecurityPolicyManager) + + log.debug('Folder copy complete') + copiedFolder + } + + Folder copyBasicFolderInformation(Folder original, Folder copy, String label, User copier) { + copy.createdBy = copier.emailAddress + copy.label = label + copy.description = original.description + + metadataService.findAllByMultiFacetAwareItemId(original.id).each {copy.addToMetadata(it.namespace, it.key, it.value, copier.emailAddress)} + ruleService.findAllByMultiFacetAwareItemId(original.id).each {rule -> + Rule copiedRule = new Rule(name: rule.name, description: rule.description, createdBy: copier.emailAddress) + rule.ruleRepresentations.each {ruleRepresentation -> + copiedRule.addToRuleRepresentations(language: ruleRepresentation.language, + representation: ruleRepresentation.representation, + createdBy: copier.emailAddress) + } + copy.addToRules(copiedRule) + } + + semanticLinkService.findAllBySourceMultiFacetAwareItemId(original.id).each {link -> + copy.addToSemanticLinks(createdBy: copier.emailAddress, linkType: link.linkType, + targetMultiFacetAwareItemId: link.targetMultiFacetAwareItemId, + targetMultiFacetAwareItemDomainType: link.targetMultiFacetAwareItemDomainType, + unconfirmed: true) } - List childFolders = findAllByParentId(original.id, [:]) - childFolders.each {childFolderToCopy -> - copiedFolder(childFolderToCopy, copiedFolder, copier, copyPermissions, childFolderToCopy.label, modelBranchName, throwErrors, userSecurityPolicyManager) + copy + } + void copyFolderContents(Folder original, Folder folderCopy, User copier, + boolean copyPermissions, + Version copyDocVersion, + String branchName, + boolean throwErrors, UserSecurityPolicyManager userSecurityPolicyManager) { + + // If changing label then we need to prefix all the new models so the names dont introduce label conflicts as this situation arises in forking + String labelSuffix = folderCopy.label == original.label ? '' : " (${folderCopy.label})" + + log.debug('Copying models from original folder into copied folder') + modelServices.each {service -> + List originalModels = service.findAllByContainerId(original.id) as List + List copiedModels = originalModels.collect {Model model -> + + service.copyModel(model, folderCopy, copier, copyPermissions, + "${model.label}${labelSuffix}", + copyDocVersion, branchName, throwErrors, + userSecurityPolicyManager) + } + // We can't save until after all copied as the save clears the sessions + copiedModels.each {copy -> + log.debug('Validating and saving model copy') + service.validate(copy) + if (copy.hasErrors()) { + throw new ApiInvalidModelException('VFS02', 'Copied Model is invalid', copy.errors, messageSource) + } + service.saveModelWithContent(copy) + } } - copiedFolder + List folders = findAllByParentId(original.id) + log.debug('Copying {} sub folders inside folder', folders.size()) + folders.each {childFolder -> + Folder childCopy = new Folder(parentFolder: folderCopy, deleted: false) + copyFolder(childFolder, childCopy, childFolder.label, copier, copyPermissions, branchName, copyDocVersion, throwErrors, userSecurityPolicyManager) + } + } + + void setFolderRefinesFolder(Folder source, Folder target, User catalogueUser) { + source.addToSemanticLinks(linkType: SemanticLinkType.REFINES, createdBy: catalogueUser.emailAddress, targetMultiFacetAwareItem: target) } } diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy index a42322e8a3..a99ff29f0f 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy @@ -20,7 +20,6 @@ package uk.ac.ox.softeng.maurodatamapper.core.container import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiBadRequestException import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInternalException import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiInvalidModelException -import uk.ac.ox.softeng.maurodatamapper.api.exception.ApiNotYetImplementedException import uk.ac.ox.softeng.maurodatamapper.core.authority.AuthorityService import uk.ac.ox.softeng.maurodatamapper.core.diff.DiffBuilder import uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ArrayDiff @@ -34,7 +33,6 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata import uk.ac.ox.softeng.maurodatamapper.core.facet.ReferenceFile import uk.ac.ox.softeng.maurodatamapper.core.facet.Rule import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLink -import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLinkType import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLink import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkService import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType @@ -68,6 +66,7 @@ import grails.gorm.DetachedCriteria import grails.gorm.transactions.Transactional import groovy.util.logging.Slf4j import org.grails.datastore.gorm.GormValidateable +import org.grails.datastore.mapping.model.PersistentEntity import org.grails.orm.hibernate.proxy.HibernateProxyHandler import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.MessageSource @@ -352,6 +351,11 @@ class VersionedFolderService extends ContainerService implement folderService.delete(folder, permanent, flush) } + @Override + VersionedFolder save(Map args, VersionedFolder folder) { + folderService.save(args, folder) as VersionedFolder + } + /** * Find all resources by the defined user security policy manager. If none provided then assume no security policy in place in which case * everything is public. @@ -484,107 +488,31 @@ class VersionedFolderService extends ContainerService implement VersionedFolder copyFolderAsNewBranchFolder(VersionedFolder original, User copier, boolean copyPermissions, String label, String branchName, boolean throwErrors, UserSecurityPolicyManager userSecurityPolicyManager) { - copyFolder(original, copier, copyPermissions, label, Version.from('1'), branchName, throwErrors, userSecurityPolicyManager) + copyVersionedFolder(original, copier, copyPermissions, label, Version.from('1'), branchName, throwErrors, userSecurityPolicyManager) } VersionedFolder copyModelAsNewForkModel(VersionedFolder original, User copier, boolean copyPermissions, String label, boolean throwErrors, UserSecurityPolicyManager userSecurityPolicyManager) { - copyFolder(original, copier, copyPermissions, label, Version.from('1'), original.branchName, throwErrors, userSecurityPolicyManager) + copyVersionedFolder(original, copier, copyPermissions, label, Version.from('1'), original.branchName, throwErrors, userSecurityPolicyManager) } VersionedFolder copyFolderAsNewDocumentationModel(VersionedFolder original, User copier, boolean copyPermissions, String label, Version copyDocVersion, String branchName, boolean throwErrors, UserSecurityPolicyManager userSecurityPolicyManager) { - copyFolder(original, copier, copyPermissions, label, copyDocVersion, branchName, throwErrors, userSecurityPolicyManager) + copyVersionedFolder(original, copier, copyPermissions, label, copyDocVersion, branchName, throwErrors, userSecurityPolicyManager) } - VersionedFolder copyFolder(VersionedFolder original, User copier, boolean copyPermissions, String label, Version copyDocVersion, - String branchName, - boolean throwErrors, UserSecurityPolicyManager userSecurityPolicyManager) { + VersionedFolder copyVersionedFolder(VersionedFolder original, User copier, boolean copyPermissions, String label, Version copyDocVersion, + String branchName, + boolean throwErrors, UserSecurityPolicyManager userSecurityPolicyManager) { log.debug('Copying folder {}', original.id) Folder parentFolder = original.parentFolder ? proxyHandler.unwrapIfProxy(original.parentFolder) as Folder : null VersionedFolder folderCopy = new VersionedFolder(finalised: false, deleted: false, documentationVersion: copyDocVersion, parentFolder: parentFolder, - branchName: branchName) - folderCopy = copyBasicFolderInformation(original, folderCopy, copier) - folderCopy.label = label - - if (copyPermissions) { - if (throwErrors) { - throw new ApiNotYetImplementedException('VFSXX', 'VersionedFolder permission copying') - } - log.warn('Permission copying is not yet implemented') - - } - - setFolderRefinesFolder(folderCopy, original, copier) - - log.debug('Validating and saving copy') - if (folderCopy.validate()) { - save(folderCopy, validate: false) - editService.createAndSaveEdit(EditTitle.COPY, folderCopy.id, folderCopy.domainType, - "VersionedFolder ${original.label} created as a copy of ${original.id}", - copier - ) - } else throw new ApiInvalidModelException('VFS01', 'Copied VersionedFolder is invalid', folderCopy.errors, messageSource) - - folderCopy.trackChanges() - - copyFolderContents(original, copier, folderCopy, copyPermissions, copyDocVersion, branchName, throwErrors, userSecurityPolicyManager) - - log.debug('Folder copy complete') - folderCopy - } - - VersionedFolder copyBasicFolderInformation(VersionedFolder original, VersionedFolder copy, User copier) { - copy = folderService.copyBasicFolderInformation(original, copy, copier) as VersionedFolder - copy.authority = authorityService.defaultAuthority - copy - } - - void copyFolderContents(Folder original, User copier, Folder folderCopy, - boolean copyPermissions, - Version copyDocVersion, - String branchName, - boolean throwErrors, UserSecurityPolicyManager userSecurityPolicyManager) { - - // If changing label then we need to prefix all the new models so the names dont introduce label conflicts as this situation arises in forking - String labelSuffix = folderCopy.label == original.label ? '' : " (${folderCopy.label})" - - log.debug('Copying models from original folder into copied folder') - modelServices.each {service -> - List originalModels = service.findAllByContainerId(original.id) as List - List copiedModels = originalModels.collect {Model model -> - - service.copyModel(model, folderCopy, copier, copyPermissions, - "${model.label}${labelSuffix}", - copyDocVersion, branchName, throwErrors, - userSecurityPolicyManager) - } - // We can't save until after all copied as the save clears the sessions - copiedModels.each {copy -> - log.debug('Validating and saving model copy') - service.validate(copy) - if (copy.hasErrors()) { - throw new ApiInvalidModelException('VFS02', 'Copied Model is invalid', copy.errors, messageSource) - } - service.saveModelWithContent(copy) - } - } - - List folders = findAllByParentId(original.id) - log.debug('Copying {} sub folders inside folder', folders.size()) - folders.each {childFolder -> - Folder childCopy = new Folder(parentFolder: folderCopy, deleted: false) - childCopy = folderService.copyBasicFolderInformation(childFolder, childCopy, copier) - folderService.validate(childCopy) - if (childCopy.hasErrors()) { - throw new ApiInvalidModelException('VFS02', 'Copied Folder is invalid', childCopy.errors, messageSource) - } - folderService.save(flush: false, validate: false, childCopy) - copyFolderContents(childFolder, copier, childCopy, copyPermissions, copyDocVersion, branchName, throwErrors, userSecurityPolicyManager) - } + branchName: branchName, + authority: authorityService.defaultAuthority) + folderService.copyFolder(original, folderCopy, label, copier, copyPermissions, branchName, copyDocVersion, throwErrors, userSecurityPolicyManager) as VersionedFolder } void setFolderIsNewBranchModelVersionOfFolder(VersionedFolder newVersionedFolder, VersionedFolder oldVersionedFolder, User catalogueUser) { @@ -611,9 +539,6 @@ class VersionedFolderService extends ContainerService implement ) } - void setFolderRefinesFolder(VersionedFolder source, VersionedFolder target, User catalogueUser) { - source.addToSemanticLinks(linkType: SemanticLinkType.REFINES, createdBy: catalogueUser.emailAddress, targetMultiFacetAwareItem: target) - } VersionedFolder findLatestFinalisedModelByLabel(String label) { VersionedFolder.byLabelAndBranchNameAndFinalisedAndLatestModelVersion(label, VersionAwareConstraints.DEFAULT_BRANCH_NAME).get() @@ -740,8 +665,8 @@ class VersionedFolderService extends ContainerService implement MergeDiff getMergeDiffForVersionedFolders(VersionedFolder sourceVersionedFolder, VersionedFolder targetVersionedFolder) { VersionedFolder commonAncestor = findCommonAncestorBetweenModels(sourceVersionedFolder, targetVersionedFolder) - ObjectDiff caDiffSource = commonAncestor.diff(sourceVersionedFolder) - ObjectDiff caDiffTarget = commonAncestor.diff(targetVersionedFolder) + ObjectDiff caDiffSource = getDiffForVersionedFolders(commonAncestor, sourceVersionedFolder) + ObjectDiff caDiffTarget = getDiffForVersionedFolders(commonAncestor, targetVersionedFolder) removeBranchNameDiff(caDiffSource) removeBranchNameDiff(caDiffTarget) @@ -959,4 +884,9 @@ class VersionedFolderService extends ContainerService implement multiFacetItemAwareService.save(copy, flush: false, validate: false) } + + @Override + PersistentEntity getPersistentEntity() { + grailsApplication.mappingContext.getPersistentEntity(Folder.name) + } } diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy index a20815f688..8b2c44950e 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy @@ -19,6 +19,8 @@ package uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional import uk.ac.ox.softeng.maurodatamapper.core.api.exception.ApiDiffException import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable +import uk.ac.ox.softeng.maurodatamapper.path.Path +import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware import groovy.transform.stc.ClosureParams import groovy.transform.stc.SimpleType @@ -79,6 +81,10 @@ class ObjectDiff extends BiDirectionalDiff { right.diffIdentifier } + boolean isVersionedDiff() { + Path.from(left.pathPrefix, left.pathIdentifier).first().modelIdentifier + } + ObjectDiff leftHandSide(String leftId, O lhs) { super.leftHandSide(lhs) this.leftId = leftId @@ -139,11 +145,23 @@ class ObjectDiff extends BiDirectionalDiff { // Assume all rhs have been created new List created = new ArrayList<>(rhs) - Map lhsMap = lhs.collectEntries { [it.getDiffIdentifier(), it] } - Map rhsMap = rhs.collectEntries { [it.getDiffIdentifier(), it] } + Map lhsMap = lhs.collectEntries {[it.getDiffIdentifier(), it]} + Map rhsMap = rhs.collectEntries {[it.getDiffIdentifier(), it]} + // This object diff is being performed on an object which has the concept of modelIdentifier, e.g branch name or version + // If this is the case we want to make sure we ignore any versioning on sub contents as child versioning is controlled by the parent + // This should only happen to models inside versioned folders, but we want to try and be more dynamic + if (isVersionedDiff()) { + Path childPath = Path.from((CreatorAware) lhs.first()) + if (childPath.size() == 1 && childPath.first().modelIdentifier) { + // child collection has versioning + // recollect entries using the clean identifier rather than the full thing + lhsMap = lhs.collectEntries {[Path.from(it.pathPrefix, it.getDiffIdentifier()).first().identifier, it]} + rhsMap = rhs.collectEntries {[Path.from(it.pathPrefix, it.getDiffIdentifier()).first().identifier, it]} + } + } // Work through each lhs object and compare to rhs object - lhsMap.each { di, lObj -> + lhsMap.each {di, lObj -> K rObj = rhsMap[di] if (rObj) { // If robj then it exists and has not been created diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy index 45eae96d5c..c09658cc7c 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy @@ -24,6 +24,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.model.ModelService import uk.ac.ox.softeng.maurodatamapper.security.UserGroup import uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions import uk.ac.ox.softeng.maurodatamapper.security.role.GroupRole +import uk.ac.ox.softeng.maurodatamapper.test.functional.TestMergeData import uk.ac.ox.softeng.maurodatamapper.testing.functional.UserAccessAndPermissionChangingFunctionalSpec import uk.ac.ox.softeng.maurodatamapper.version.VersionChangeType @@ -37,6 +38,7 @@ import org.springframework.beans.factory.annotation.Autowired import spock.lang.Ignore import spock.lang.Unroll +import java.nio.charset.Charset import java.util.regex.Pattern import static io.micronaut.http.HttpStatus.BAD_REQUEST @@ -1709,6 +1711,214 @@ class VersionedFolderFunctionalSpec extends UserAccessAndPermissionChangingFunct cleanupModelVersionTree(data) } + + void 'D01 : test diffing 2 versioned folders (as not logged in)'() { + + given: + TestMergeData mergeData = buildSimpleVersionedFoldersForMerging() + + when: 'not logged in' + GET("$mergeData.source/diff/$mergeData.target") + + then: + verifyNotFound response, mergeData.source + + cleanup: + removeValidIdObject(mergeData.source) + removeValidIdObject(mergeData.target) + removeValidIdObject(mergeData.commonAncestor) + } + + void 'D02 : test diffing 2 versioned folders (as authenticated/no access)'() { + given: + TestMergeData mergeData = buildSimpleVersionedFoldersForMerging() + + when: + loginAuthenticated() + GET("$mergeData.source/diff/$mergeData.target") + + then: + verifyNotFound response, mergeData.source + + cleanup: + removeValidIdObject(mergeData.source) + removeValidIdObject(mergeData.target) + removeValidIdObject(mergeData.commonAncestor) + } + + void 'D03 : test diffing 2 versioned folders (as reader of LH model)'() { + given: + TestMergeData mergeData = buildSimpleVersionedFoldersForMerging(true, false) + + when: 'able to read right model only' + loginReader() + GET("$mergeData.source/diff/$mergeData.target") + + then: + verifyNotFound response, mergeData.target + + cleanup: + removeValidIdObject(mergeData.source) + removeValidIdObject(mergeData.target) + removeValidIdObject(mergeData.commonAncestor) + } + + void 'D04 : test diffing 2 versioned folders (as reader of RH model)'() { + given: + TestMergeData mergeData = buildSimpleVersionedFoldersForMerging(false, true) + + when: + loginReader() + GET("$mergeData.source/diff/$mergeData.target") + + then: + verifyNotFound response, mergeData.source + + cleanup: + removeValidIdObject(mergeData.source) + removeValidIdObject(mergeData.target) + removeValidIdObject(mergeData.commonAncestor) + } + + void 'D05 : test diffing 2 simple versioned folders (as reader of both models)'() { + + given: + TestMergeData mergeData = buildSimpleVersionedFoldersForMerging() + + when: + loginReader() + GET("$mergeData.source/diff/$mergeData.target", STRING_ARG) + + then: + verifyJsonResponse OK, '''{ + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "Functional Test VersionedFolder 3", + "count": 1, + "diffs": [ + { + "branchName": { + "left": "left", + "right": "main" + } + } + ] +} +''' + + cleanup: + removeValidIdObject(mergeData.source) + removeValidIdObject(mergeData.target) + removeValidIdObject(mergeData.commonAncestor) + } + + void 'D06 : test diffing 2 complex versioned folders (as reader of both models)'() { + + given: + TestMergeData mergeData = buildComplexVersionedFoldersForMerging() + + when: + loginReader() + GET("$mergeData.source/diff/$mergeData.target", STRING_ARG) + + then: + verifyJsonResponse OK, getExpectedComplexDiffJson() + + cleanup: + removeValidIdObject(mergeData.source) + removeValidIdObject(mergeData.target) + removeValidIdObject(mergeData.commonAncestor) + } + + + void 'MD01 : test finding merge difference of two versioned folders (as not logged in)'() { + given: + TestMergeData mergeData = buildSimpleVersionedFoldersForMerging() + + when: + GET("$mergeData.source/mergeDiff/$mergeData.target") + + then: + verifyResponse NOT_FOUND, response + + cleanup: + removeValidIdObject(mergeData.source) + removeValidIdObject(mergeData.target) + removeValidIdObject(mergeData.commonAncestor) + } + + void 'MD02 : test finding merge difference of two versioned folders (as authenticated/logged in)'() { + given: + TestMergeData mergeData = buildSimpleVersionedFoldersForMerging() + + when: + loginAuthenticated() + GET("$mergeData.source/mergeDiff/$mergeData.target") + + then: + verifyResponse NOT_FOUND, response + + cleanup: + removeValidIdObject(mergeData.source) + removeValidIdObject(mergeData.target) + removeValidIdObject(mergeData.commonAncestor) + } + + void 'MD03 : test finding merge difference of two versioned folders (as reader)'() { + given: + TestMergeData mergeData = buildSimpleVersionedFoldersForMerging() + + when: + loginReader() + GET("$mergeData.source/mergeDiff/$mergeData.target") + + then: + verifyResponse OK, response + responseBody().targetId == mergeData.target + responseBody().sourceId == mergeData.source + + cleanup: + removeValidIdObject(mergeData.source) + removeValidIdObject(mergeData.target) + removeValidIdObject(mergeData.commonAncestor) + } + + void 'MD04 : test finding merge difference of two versioned folders (as editor)'() { + given: + TestMergeData mergeData = buildSimpleVersionedFoldersForMerging() + + when: + loginEditor() + GET("$mergeData.source/mergeDiff/$mergeData.target") + + then: + verifyResponse OK, response + responseBody().targetId == mergeData.target + responseBody().sourceId == mergeData.source + + cleanup: + removeValidIdObject(mergeData.source) + removeValidIdObject(mergeData.target) + removeValidIdObject(mergeData.commonAncestor) + } + + void 'MD05 : test finding merge difference of two complex datamodels with the new style (as reader)'() { + given: + TestMergeData mergeData = buildComplexVersionedFoldersForMerging() + + when: + loginReader() + GET("$mergeData.source/mergeDiff/$mergeData.target", STRING_ARG) + + then: + verifyJsonResponse OK, expectedMergeDiffJson + + cleanup: + removeValidIdObject(mergeData.source) + removeValidIdObject(mergeData.target) + removeValidIdObject(mergeData.commonAncestor) + } + Map buildModelVersionTree() { /* /- anotherFork @@ -2129,4 +2339,777 @@ class VersionedFolderFunctionalSpec extends UserAccessAndPermissionChangingFunct } ]""" } + + String getIdFromPath(String rootResourceId, String path) { + GET("$rootResourceId/path/${URLEncoder.encode(path, Charset.defaultCharset())}") + verifyResponse OK, response + assert responseBody().id + responseBody().id + } + + TestMergeData buildSimpleVersionedFoldersForMerging(boolean readLhs = true, boolean readRhs = true) { + String id = getValidId() + loginEditor() + PUT("$id/finalise", [versionChangeType: 'Major']) + verifyResponse OK, response + PUT("$id/newBranchModelVersion", [:]) + verifyResponse CREATED, response + String mainId = responseBody().id + if (readRhs) addReaderShare(mainId) + PUT("$id/newBranchModelVersion", [branchName: 'left']) + verifyResponse CREATED, response + String leftId = responseBody().id + if (readLhs) addReaderShare(leftId) + logout() + new TestMergeData(commonAncestor: id, source: leftId, target: mainId) + } + + TestMergeData buildComplexVersionedFoldersForMerging() { + + // Somethings up with the MD, when running properly the diff happily returns the changed MD, but under test it doesnt. + // The MD exists in the daabase and is returned if using the MD endpoint but when calling folder.metadata the collection is empty. + // When run-app all the tables are correctly populated and the collection is not empty + + String commonAncestorId = getValidId() + loginEditor() + POST("folders/$commonAncestorId/dataModels", [ + label: 'Functional Test DataModel 1' + ], MAP_ARG, true) + verifyResponse(CREATED, response) + String dataModel1Id = responseBody().id + + POST("dataModels/$dataModel1Id/dataClasses", [label: 'deleteLeftOnly'], MAP_ARG, true) + verifyResponse CREATED, response + POST("dataModels/$dataModel1Id/dataClasses", [label: 'deleteRightOnly'], MAP_ARG, true) + verifyResponse CREATED, response + POST("dataModels/$dataModel1Id/dataClasses", [label: 'modifyLeftOnly'], MAP_ARG, true) + verifyResponse CREATED, response + POST("dataModels/$dataModel1Id/dataClasses", [label: 'modifyRightOnly'], MAP_ARG, true) + verifyResponse CREATED, response + POST("dataModels/$dataModel1Id/dataClasses", [label: 'deleteAndDelete'], MAP_ARG, true) + verifyResponse CREATED, response + POST("dataModels/$dataModel1Id/dataClasses", [label: 'deleteAndModify'], MAP_ARG, true) + verifyResponse CREATED, response + POST("dataModels/$dataModel1Id/dataClasses", [label: 'modifyAndDelete'], MAP_ARG, true) + verifyResponse CREATED, response + POST("dataModels/$dataModel1Id/dataClasses", [label: 'modifyAndModifyReturningNoDifference'], MAP_ARG, true) + verifyResponse CREATED, response + POST("dataModels/$dataModel1Id/dataClasses", [label: 'modifyAndModifyReturningDifference'], MAP_ARG, true) + verifyResponse CREATED, response + POST("dataModels/$dataModel1Id/dataClasses", [label: 'existingClass'], MAP_ARG, true) + verifyResponse CREATED, response + String caExistingClass = responseBody().id + POST("dataModels/$dataModel1Id/dataClasses/$caExistingClass/dataClasses", [label: 'deleteLeftOnlyFromExistingClass'], MAP_ARG, true) + verifyResponse CREATED, response + POST("dataModels/$dataModel1Id/dataClasses/$caExistingClass/dataClasses", [label: 'deleteRightOnlyFromExistingClass'], MAP_ARG, true) + verifyResponse CREATED, response + POST("dataModels/$dataModel1Id/metadata", [namespace: 'functional.test', key: 'nothingDifferent', value: 'this shouldnt change'], MAP_ARG, true) + verifyResponse CREATED, response + POST("dataModels/$dataModel1Id/metadata", [namespace: 'functional.test', key: 'modifyOnSource', value: 'some original value'], MAP_ARG, true) + verifyResponse CREATED, response + POST("dataModels/$dataModel1Id/metadata", [namespace: 'functional.test', key: 'deleteFromSource', value: 'some other original value'], MAP_ARG, true) + verifyResponse CREATED, response + POST("dataModels/$dataModel1Id/metadata", [namespace: 'functional.test', key: 'modifyAndDelete', value: 'some other original value 2'], MAP_ARG, true) + + + // POST("$commonAncestorId/metadata", [namespace: 'functional.test', key: 'nothingDifferent', value: 'this shouldnt change']) + // verifyResponse CREATED, response + // POST("$commonAncestorId/metadata", [namespace: 'functional.test', key: 'modifyOnSource', value: 'some original value']) + // verifyResponse CREATED, response + // POST("$commonAncestorId/metadata", [namespace: 'functional.test', key: 'deleteFromSource', value: 'some other original value']) + // verifyResponse CREATED, response + // POST("$commonAncestorId/metadata", [namespace: 'functional.test', key: 'modifyAndDelete', value: 'some other original value 2']) + verifyResponse CREATED, response + + PUT("$commonAncestorId/finalise", [versionChangeType: 'Major']) + verifyResponse OK, response + PUT("$commonAncestorId/newBranchModelVersion", [branchName: VersionAwareConstraints.DEFAULT_BRANCH_NAME]) + verifyResponse CREATED, response + String target = responseBody().id + addReaderShare(target) + PUT("$commonAncestorId/newBranchModelVersion", [branchName: 'source']) + verifyResponse CREATED, response + String source = responseBody().id + addReaderShare(source) + + // Modify Source + Map sourceMap = [ + dataModelId : getIdFromPath(source, 'dm:Functional Test DataModel 1$source'), + existingClass : getIdFromPath(source, 'dm:Functional Test DataModel 1$source|dc:existingClass'), + deleteLeftOnlyFromExistingClass : getIdFromPath(source, 'dm:Functional Test DataModel 1$source|dc:existingClass|dc:deleteLeftOnlyFromExistingClass'), + deleteLeftOnly : getIdFromPath(source, 'dm:Functional Test DataModel 1$source|dc:deleteLeftOnly'), + modifyLeftOnly : getIdFromPath(source, 'dm:Functional Test DataModel 1$source|dc:modifyLeftOnly'), + deleteAndDelete : getIdFromPath(source, 'dm:Functional Test DataModel 1$source|dc:deleteAndDelete'), + deleteAndModify : getIdFromPath(source, 'dm:Functional Test DataModel 1$source|dc:deleteAndModify'), + modifyAndDelete : getIdFromPath(source, 'dm:Functional Test DataModel 1$source|dc:modifyAndDelete'), + modifyAndModifyReturningNoDifference: getIdFromPath(source, 'dm:Functional Test DataModel 1$source|dc:modifyAndModifyReturningNoDifference'), + modifyAndModifyReturningDifference : getIdFromPath(source, 'dm:Functional Test DataModel 1$source|dc:modifyAndModifyReturningDifference'), + dataModelMetadataModifyOnSource : getIdFromPath(source, 'dm:Functional Test DataModel 1$source|md:functional.test.modifyOnSource'), + dataModelMetadataDeleteFromSource : getIdFromPath(source, 'dm:Functional Test DataModel 1$source|md:functional.test.deleteFromSource'), + dataModelMetadataModifyAndDelete : getIdFromPath(source, 'dm:Functional Test DataModel 1$source|md:functional.test.modifyAndDelete'), + // metadataModifyOnSource : getIdFromPath(source, 'md:functional.test.modifyOnSource'), + // metadataDeleteFromSource : getIdFromPath(source, 'md:functional.test.deleteFromSource'), + // metadataModifyAndDelete : getIdFromPath(source, 'md:functional.test.modifyAndDelete'), + ] + + DELETE("dataModels/$sourceMap.dataModelId/dataClasses/$sourceMap.deleteAndDelete", MAP_ARG, true) + verifyResponse NO_CONTENT, response + DELETE("dataModels/$sourceMap.dataModelId/dataClasses/$sourceMap.existingClass/dataClasses/$sourceMap.deleteLeftOnlyFromExistingClass", MAP_ARG, true) + verifyResponse NO_CONTENT, response + DELETE("dataModels/$sourceMap.dataModelId/dataClasses/$sourceMap.deleteLeftOnly", MAP_ARG, true) + verifyResponse NO_CONTENT, response + DELETE("dataModels/$sourceMap.dataModelId/dataClasses/$sourceMap.deleteAndModify", MAP_ARG, true) + verifyResponse NO_CONTENT, response + + PUT("dataModels/$sourceMap.dataModelId/dataClasses/$sourceMap.modifyLeftOnly", [description: 'Description'], MAP_ARG, true) + verifyResponse OK, response + PUT("dataModels/$sourceMap.dataModelId/dataClasses/$sourceMap.modifyAndDelete", [description: 'Description'], MAP_ARG, true) + verifyResponse OK, response + PUT("dataModels/$sourceMap.dataModelId/dataClasses/$sourceMap.modifyAndModifyReturningNoDifference", [description: 'Description'], MAP_ARG, true) + verifyResponse OK, response + PUT("dataModels/$sourceMap.dataModelId/dataClasses/$sourceMap.modifyAndModifyReturningDifference", [description: 'DescriptionLeft'], MAP_ARG, true) + verifyResponse OK, response + + POST("dataModels/$sourceMap.dataModelId/dataClasses/$sourceMap.existingClass/dataClasses", [label: 'addLeftToExistingClass'], MAP_ARG, true) + verifyResponse CREATED, response + sourceMap.addLeftToExistingClass = responseBody().id + POST("dataModels/$sourceMap.dataModelId/dataClasses", [label: 'addLeftOnly'], MAP_ARG, true) + verifyResponse CREATED, response + sourceMap.addLeftOnly = responseBody().id + POST("dataModels/$sourceMap.dataModelId/dataClasses", [label: 'addAndAddReturningNoDifference'], MAP_ARG, true) + verifyResponse CREATED, response + POST("dataModels/$sourceMap.dataModelId/dataClasses", [label: 'addAndAddReturningDifference', description: 'DescriptionLeft'], MAP_ARG, true) + verifyResponse CREATED, response + sourceMap.addAndAddReturningDifference = responseBody().id + + PUT("dataModels/$sourceMap.dataModelId", [description: 'DescriptionLeft'], MAP_ARG, true) + verifyResponse OK, response + + PUT("$source", [description: 'source description on the versioned folder']) + verifyResponse OK, response + + POST("dataModels/$sourceMap.dataModelId/metadata", [namespace: 'functional.test', key: 'addToSourceOnly', value: 'adding to source only'], MAP_ARG, true) + verifyResponse CREATED, response + PUT("dataModels/$sourceMap.dataModelId/metadata/$sourceMap.dataModelMetadataModifyOnSource", [value: 'source has modified this'], MAP_ARG, true) + verifyResponse OK, response + PUT("dataModels/$sourceMap.dataModelId/metadata/$sourceMap.dataModelMetadataModifyAndDelete", [value: 'source has modified this also'], MAP_ARG, true) + verifyResponse OK, response + DELETE("dataModels/$sourceMap.dataModelId/metadata/$sourceMap.dataModelMetadataDeleteFromSource", MAP_ARG, true) + verifyResponse NO_CONTENT, response + + // POST("$source/metadata", [namespace: 'functional.test', key: 'addToSourceOnly', value: 'adding to source only']) + // verifyResponse CREATED, response + // PUT("$source/metadata/$sourceMap.metadataModifyOnSource", [value: 'source has modified this']) + // verifyResponse OK, response + // PUT("$source/metadata/$sourceMap.metadataModifyAndDelete", [value: 'source has modified this also']) + // verifyResponse OK, response + // DELETE("$source/metadata/$sourceMap.metadataDeleteFromSource") + // verifyResponse NO_CONTENT, response + + + // Modify Target + Map targetMap = [ + dataModelId : getIdFromPath(target, 'dm:Functional Test DataModel 1$main'), + existingClass : getIdFromPath(target, 'dm:Functional Test DataModel 1$main|dc:existingClass'), + deleteRightOnly : getIdFromPath(target, 'dm:Functional Test DataModel 1$main|dc:deleteRightOnly'), + modifyRightOnly : getIdFromPath(target, 'dm:Functional Test DataModel 1$main|dc:modifyRightOnly'), + deleteRightOnlyFromExistingClass : getIdFromPath(target, 'dm:Functional Test DataModel 1$main|dc:existingClass|dc:deleteRightOnlyFromExistingClass'), + deleteAndDelete : getIdFromPath(target, 'dm:Functional Test DataModel 1$main|dc:deleteAndDelete'), + deleteAndModify : getIdFromPath(target, 'dm:Functional Test DataModel 1$main|dc:deleteAndModify'), + modifyAndDelete : getIdFromPath(target, 'dm:Functional Test DataModel 1$main|dc:modifyAndDelete'), + modifyAndModifyReturningNoDifference: getIdFromPath(target, 'dm:Functional Test DataModel 1$main|dc:modifyAndModifyReturningNoDifference'), + modifyAndModifyReturningDifference : getIdFromPath(target, 'dm:Functional Test DataModel 1$main|dc:modifyAndModifyReturningDifference'), + deleteLeftOnly : getIdFromPath(target, 'dm:Functional Test DataModel 1$main|dc:deleteLeftOnly'), + modifyLeftOnly : getIdFromPath(target, 'dm:Functional Test DataModel 1$main|dc:modifyLeftOnly'), + deleteLeftOnlyFromExistingClass : getIdFromPath(target, 'dm:Functional Test DataModel 1$main|dc:deleteLeftOnlyFromExistingClass'), + dataModelMetadataModifyAndDelete : getIdFromPath(target, 'dm:Functional Test DataModel 1$main|md:functional.test.modifyAndDelete'), + // metadataModifyAndDelete : getIdFromPath(target, 'md:functional.test.modifyAndDelete'), + ] + + DELETE("dataModels/$targetMap.dataModelId/dataClasses/$targetMap.existingClass/dataClasses/$targetMap.deleteRightOnlyFromExistingClass", MAP_ARG, true) + verifyResponse NO_CONTENT, response + DELETE("dataModels/$targetMap.dataModelId/dataClasses/$targetMap.deleteRightOnly", MAP_ARG, true) + verifyResponse NO_CONTENT, response + DELETE("dataModels/$targetMap.dataModelId/dataClasses/$targetMap.deleteAndDelete", MAP_ARG, true) + verifyResponse NO_CONTENT, response + DELETE("dataModels/$targetMap.dataModelId/dataClasses/$targetMap.modifyAndDelete", MAP_ARG, true) + verifyResponse NO_CONTENT, response + + PUT("dataModels/$targetMap.dataModelId/dataClasses/$targetMap.modifyRightOnly", [description: 'Description'], MAP_ARG, true) + verifyResponse OK, response + PUT("dataModels/$targetMap.dataModelId/dataClasses/$targetMap.deleteAndModify", [description: 'Description'], MAP_ARG, true) + verifyResponse OK, response + PUT("dataModels/$targetMap.dataModelId/dataClasses/$targetMap.modifyAndModifyReturningNoDifference", [description: 'Description'], MAP_ARG, true) + verifyResponse OK, response + PUT("dataModels/$targetMap.dataModelId/dataClasses/$targetMap.modifyAndModifyReturningDifference", [description: 'DescriptionRight'], MAP_ARG, true) + verifyResponse OK, response + + POST("dataModels/$targetMap.dataModelId/dataClasses/$targetMap.existingClass/dataClasses", [label: 'addRightToExistingClass'], MAP_ARG, true) + verifyResponse CREATED, response + POST("dataModels/$targetMap.dataModelId/dataClasses", [label: 'addRightOnly'], MAP_ARG, true) + verifyResponse CREATED, response + POST("dataModels/$targetMap.dataModelId/dataClasses", [label: 'addAndAddReturningNoDifference'], MAP_ARG, true) + verifyResponse CREATED, response + POST("dataModels/$targetMap.dataModelId/dataClasses", [label: 'addAndAddReturningDifference', description: 'DescriptionRight'], MAP_ARG, true) + verifyResponse CREATED, response + targetMap.addAndAddReturningDifference = responseBody().id + + PUT("dataModels/$targetMap.dataModelId", [description: 'DescriptionRight'], MAP_ARG, true) + verifyResponse OK, response + PUT("$target", [description: 'Target modified description']) + verifyResponse OK, response + DELETE("dataModels/$targetMap.dataModelId/metadata/$targetMap.dataModelMetadataModifyAndDelete", MAP_ARG, true) + verifyResponse NO_CONTENT, response + // DELETE("$target/metadata/$targetMap.metadataModifyAndDelete") + // verifyResponse NO_CONTENT, response + logout() + + + new TestMergeData(commonAncestor: commonAncestorId, + source: source, + target: target, + sourceMap: sourceMap, + targetMap: targetMap + ) + } + + String getExpectedComplexDiffJson() { + '''{ + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "Functional Test VersionedFolder 3", + "count": 22, + "diffs": [ + { + "description": { + "left": "source description on the versioned folder", + "right": "Target modified description" + } + }, + { + "branchName": { + "left": "source", + "right": "main" + } + }, + { + "models": { + "modified": [ + { + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "count": 20, + "diffs": [ + { + "description": { + "left": "DescriptionLeft", + "right": "DescriptionRight" + } + }, + { + "metadata": { + "deleted": [ + { + "value": { + "id": "${json-unit.matches:id}", + "namespace": "functional.test", + "key": "addToSourceOnly", + "value": "adding to source only" + } + }, + { + "value": { + "id": "${json-unit.matches:id}", + "namespace": "functional.test", + "key": "modifyAndDelete", + "value": "source has modified this also" + } + } + ], + "created": [ + { + "value": { + "id": "${json-unit.matches:id}", + "namespace": "functional.test", + "key": "deleteFromSource", + "value": "some other original value" + } + } + ], + "modified": [ + { + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "namespace": "functional.test", + "key": "modifyOnSource", + "count": 1, + "diffs": [ + { + "value": { + "left": "source has modified this", + "right": "some original value" + } + } + ] + } + ] + } + }, + { + "branchName": { + "left": "source", + "right": "main" + } + }, + { + "dataClasses": { + "deleted": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "deleteRightOnly", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + } + ] + } + }, + { + "value": { + "id": "${json-unit.matches:id}", + "label": "modifyAndDelete", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + } + ] + } + }, + { + "value": { + "id": "${json-unit.matches:id}", + "label": "addLeftOnly", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + } + ] + } + } + ], + "created": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "deleteLeftOnly", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + } + ] + } + }, + { + "value": { + "id": "${json-unit.matches:id}", + "label": "deleteAndModify", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + } + ] + } + }, + { + "value": { + "id": "${json-unit.matches:id}", + "label": "addRightOnly", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + } + ] + } + } + ], + "modified": [ + { + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "modifyLeftOnly", + "leftBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + } + ], + "rightBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + } + ], + "count": 1, + "diffs": [ + { + "description": { + "left": "Description", + "right": null + } + } + ] + }, + { + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "modifyRightOnly", + "leftBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + } + ], + "rightBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + } + ], + "count": 1, + "diffs": [ + { + "description": { + "left": null, + "right": "Description" + } + } + ] + }, + { + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "modifyAndModifyReturningDifference", + "leftBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + } + ], + "rightBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + } + ], + "count": 1, + "diffs": [ + { + "description": { + "left": "DescriptionLeft", + "right": "DescriptionRight" + } + } + ] + }, + { + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "addAndAddReturningDifference", + "leftBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + } + ], + "rightBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + } + ], + "count": 1, + "diffs": [ + { + "description": { + "left": "DescriptionLeft", + "right": "DescriptionRight" + } + } + ] + }, + { + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "label": "existingClass", + "leftBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + } + ], + "rightBreadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + } + ], + "count": 4, + "diffs": [ + { + "dataClasses": { + "deleted": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "addLeftToExistingClass", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + }, + { + "id": "${json-unit.matches:id}", + "label": "existingClass", + "domainType": "DataClass" + } + ] + } + }, + { + "value": { + "id": "${json-unit.matches:id}", + "label": "deleteRightOnlyFromExistingClass", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + }, + { + "id": "${json-unit.matches:id}", + "label": "existingClass", + "domainType": "DataClass" + } + ] + } + } + ], + "created": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "deleteLeftOnlyFromExistingClass", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + }, + { + "id": "${json-unit.matches:id}", + "label": "existingClass", + "domainType": "DataClass" + } + ] + } + }, + { + "value": { + "id": "${json-unit.matches:id}", + "label": "addRightToExistingClass", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test DataModel 1", + "domainType": "DataModel", + "finalised": false + }, + { + "id": "${json-unit.matches:id}", + "label": "existingClass", + "domainType": "DataClass" + } + ] + } + } + ] + } + } + ] + } + ] + } + } + ] + } + ] + } + } + ] +}''' + } + + String getExpectedMergeDiffJson() { + '''{ + "sourceId": "${json-unit.matches:id}", + "targetId": "${json-unit.matches:id}", + "path": "vf:Functional Test VersionedFolder 3$source", + "label": "Functional Test VersionedFolder 3", + "count": 15, + "diffs": [ + { + "fieldName": "description", + "path": "vf:Functional Test VersionedFolder 3$source@description", + "sourceValue": "source description on the versioned folder", + "targetValue": "Target modified description", + "commonAncestorValue": null, + "isMergeConflict": true, + "type": "modification" + }, + { + "fieldName": "description", + "path": "vf:Functional Test VersionedFolder 3$source|dm:Functional Test DataModel 1$source@description", + "sourceValue": "DescriptionLeft", + "targetValue": "DescriptionRight", + "commonAncestorValue": null, + "isMergeConflict": true, + "type": "modification" + }, + { + "path": "vf:Functional Test VersionedFolder 3$source|dm:Functional Test DataModel 1$source|dc:addLeftOnly", + "isMergeConflict": false, + "isSourceModificationAndTargetDeletion": false, + "type": "creation" + }, + { + "path": "vf:Functional Test VersionedFolder 3$source|dm:Functional Test DataModel 1$source|dc:modifyAndDelete", + "isMergeConflict": true, + "isSourceModificationAndTargetDeletion": true, + "type": "creation" + }, + { + "path": "vf:Functional Test VersionedFolder 3$source|dm:Functional Test DataModel 1$source|dc:deleteAndModify", + "isMergeConflict": true, + "isSourceDeletionAndTargetModification": true, + "type": "deletion" + }, + { + "path": "vf:Functional Test VersionedFolder 3$source|dm:Functional Test DataModel 1$source|dc:deleteLeftOnly", + "isMergeConflict": false, + "isSourceDeletionAndTargetModification": false, + "type": "deletion" + }, + { + "fieldName": "description", + "path": "vf:Functional Test VersionedFolder 3$source|dm:Functional Test DataModel 1$source|dc:addAndAddReturningDifference@description", + "sourceValue": "DescriptionLeft", + "targetValue": "DescriptionRight", + "commonAncestorValue": null, + "isMergeConflict": true, + "type": "modification" + }, + { + "path": "vf:Functional Test VersionedFolder 3$source|dm:Functional Test DataModel 1$source|dc:existingClass|dc:addLeftToExistingClass", + "isMergeConflict": false, + "isSourceModificationAndTargetDeletion": false, + "type": "creation" + }, + { + "path": "vf:Functional Test VersionedFolder 3$source|dm:Functional Test DataModel 1$source|dc:existingClass|dc:deleteLeftOnlyFromExistingClass", + "isMergeConflict": false, + "isSourceDeletionAndTargetModification": false, + "type": "deletion" + }, + { + "fieldName": "description", + "path": "vf:Functional Test VersionedFolder 3$source|dm:Functional Test DataModel 1$source|dc:modifyAndModifyReturningDifference@description", + "sourceValue": "DescriptionLeft", + "targetValue": "DescriptionRight", + "commonAncestorValue": null, + "isMergeConflict": true, + "type": "modification" + }, + { + "fieldName": "description", + "path": "vf:Functional Test VersionedFolder 3$source|dm:Functional Test DataModel 1$source|dc:modifyLeftOnly@description", + "sourceValue": "Description", + "targetValue": null, + "commonAncestorValue": null, + "isMergeConflict": false, + "type": "modification" + }, + { + "path": "vf:Functional Test VersionedFolder 3$source|dm:Functional Test DataModel 1$source|md:functional.test.addToSourceOnly", + "isMergeConflict": false, + "isSourceModificationAndTargetDeletion": false, + "type": "creation" + }, + { + "path": "vf:Functional Test VersionedFolder 3$source|dm:Functional Test DataModel 1$source|md:functional.test.modifyAndDelete", + "isMergeConflict": true, + "isSourceModificationAndTargetDeletion": true, + "type": "creation" + }, + { + "path": "vf:Functional Test VersionedFolder 3$source|dm:Functional Test DataModel 1$source|md:functional.test.deleteFromSource", + "isMergeConflict": false, + "isSourceDeletionAndTargetModification": false, + "type": "deletion" + }, + { + "fieldName": "value", + "path": "vf:Functional Test VersionedFolder 3$source|dm:Functional Test DataModel 1$source|md:functional.test.modifyOnSource@value", + "sourceValue": "source has modified this", + "targetValue": "some original value", + "commonAncestorValue": "some original value", + "isMergeConflict": false, + "type": "modification" + } + ] +}''' + } } \ No newline at end of file From d6a20107346af62f6d280e82336780ca47962169 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Wed, 28 Jul 2021 10:14:04 +0100 Subject: [PATCH 71/82] Updates to pathing * remove the classification of isFirst which was used to strip modelIdentifier, we need to strip it at any point in the path * Allow the model identifier to be overridden This allows us to use a path from inside one versioned path to search a path in another versioned resource --- .../softeng/maurodatamapper/path/Path.groovy | 18 +++++----- .../maurodatamapper/path/PathNode.groovy | 35 +++++++++--------- .../maurodatamapper/path/PathNodeSpec.groovy | 36 +++++++++---------- .../core/path/PathService.groovy | 10 +++--- 4 files changed, 51 insertions(+), 48 deletions(-) diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy index cd09519740..0afee4a13d 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy @@ -54,7 +54,7 @@ class Path { int lastIndex = splits.size() - 1 splits.eachWithIndex {String node, int i -> - pathNodes << new PathNode(node, i == 0, i == lastIndex) + pathNodes << new PathNode(node, i == lastIndex) } } } @@ -81,7 +81,7 @@ class Path { } Path addToPathNodes(String prefix, String pathIdentifier, boolean isLast) { - addToPathNodes(new PathNode(prefix, pathIdentifier, pathNodes.isEmpty(), isLast)) + addToPathNodes(new PathNode(prefix, pathIdentifier, isLast)) } Path getParent() { @@ -90,17 +90,17 @@ class Path { } } - boolean isAbsoluteTo(CreatorAware creatorAware) { + boolean isAbsoluteTo(CreatorAware creatorAware, String modelIdentifierOverride = null) { // If the first node in this path matches the supplied object then this path is absolute against the supplied object, // otherwise it may be relative or may not be inside this object Path rootPath = from(creatorAware) - isAbsoluteTo(rootPath) + isAbsoluteTo(rootPath, modelIdentifierOverride) } - boolean isAbsoluteTo(Path rootPath) { + boolean isAbsoluteTo(Path rootPath, String modelIdentifierOverride = null) { // If the first node in this path matches the supplied object then this path is absolute against the supplied object, // otherwise it may be relative or may not be inside this object - rootPath.first().matches(this.first()) + rootPath.first().matches(this.first(), modelIdentifierOverride) } boolean isEmpty() { @@ -152,7 +152,7 @@ class Path { static Path from(String prefix, String pathIdentifier) { new Path().tap { - pathNodes << new PathNode(prefix, pathIdentifier, true, true) + pathNodes << new PathNode(prefix, pathIdentifier, true) } } @@ -167,7 +167,7 @@ class Path { static Path from(CreatorAware... domains) { new Path().tap { domains.eachWithIndex {CreatorAware domain, int i -> - pathNodes << new PathNode(domain.pathPrefix, domain.pathIdentifier, i == 0, false) + pathNodes << new PathNode(domain.pathPrefix, domain.pathIdentifier, false) } } } @@ -175,7 +175,7 @@ class Path { static Path from(List domains) { new Path().tap { domains.eachWithIndex {CreatorAware domain, int i -> - pathNodes << new PathNode(domain.pathPrefix, domain.pathIdentifier, i == 0, false) + pathNodes << new PathNode(domain.pathPrefix, domain.pathIdentifier, false) } } } diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy index 8361cd027e..52cf91fb35 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy @@ -39,9 +39,9 @@ class PathNode { String attribute String modelIdentifier - PathNode(String prefix, String identifier, boolean isRoot, boolean isLast) { + PathNode(String prefix, String identifier, boolean isLast) { this.prefix = prefix - parseIdentifier(identifier, isRoot, isLast) + parseIdentifier(identifier, isLast) } PathNode(String prefix, String identifier, String modelIdentifier, String attribute) { @@ -59,14 +59,14 @@ class PathNode { te:my-code:my-definition (type prefix = te, identifier = my-code:my-definition) */ - PathNode(String node, boolean isRoot, boolean isLast) { + PathNode(String node, boolean isLast) { node.find(/^(\w+):(.+)$/) {full, foundPrefix, fullIdentifier -> prefix = foundPrefix - parseIdentifier(fullIdentifier, isRoot, isLast) + parseIdentifier(fullIdentifier, isLast) } } - void parseIdentifier(String fullIdentifier, boolean isRoot, boolean isLast) { + void parseIdentifier(String fullIdentifier, boolean isLast) { String parsed = URLDecoder.decode(fullIdentifier, Charset.defaultCharset()) if (isLast) { parsed.find(/^(.+?)${ATTRIBUTE_PATH_IDENTIFIER_SEPARATOR}(.+?)$/) {full, subIdentifier, attr -> @@ -74,12 +74,11 @@ class PathNode { parsed = subIdentifier } } - if (isRoot) { - parsed.find(/^(.+?)${ESCAPED_MODEL_PATH_IDENTIFIER_SEPARATOR}(.+?)$/) {full, subIdentifier, foundIdentifier -> - parsed = subIdentifier - modelIdentifier = foundIdentifier - } + parsed.find(/^(.+?)${ESCAPED_MODEL_PATH_IDENTIFIER_SEPARATOR}(.+?)$/) {full, subIdentifier, foundIdentifier -> + parsed = subIdentifier + modelIdentifier = foundIdentifier } + identifier = parsed } @@ -93,9 +92,9 @@ class PathNode { base } - String getFullIdentifier() { + String getFullIdentifier(String modelIdentifierOverride = null) { String base = identifier - if (modelIdentifier) base += "${MODEL_PATH_IDENTIFIER_SEPARATOR}${modelIdentifier}" + if (modelIdentifier) base += "${MODEL_PATH_IDENTIFIER_SEPARATOR}${modelIdentifierOverride ?: modelIdentifier}" base } @@ -131,8 +130,8 @@ class PathNode { return result } - boolean matches(PathNode pathNode) { - matchesPrefix(pathNode.prefix) && matchesIdentifier(pathNode) + boolean matches(PathNode pathNode, String modelIdentifierOverride = null) { + matchesPrefix(pathNode.prefix) && matchesIdentifier(pathNode, modelIdentifierOverride) } boolean matches(CreatorAware creatorAware) { @@ -147,9 +146,13 @@ class PathNode { true } - boolean matchesIdentifier(PathNode otherPathNode) { + boolean matchesIdentifier(PathNode otherPathNode, String modelIdentifierOverride = null) { - if (identifier == otherPathNode.identifier && modelIdentifier == otherPathNode.modelIdentifier) return true + if (modelIdentifierOverride) { + if (identifier == otherPathNode.identifier && modelIdentifier == modelIdentifierOverride) return true + } else { + if (identifier == otherPathNode.identifier && modelIdentifier == otherPathNode.modelIdentifier) return true + } // If model identifier present on either side then we need to do some verification if ((modelIdentifier || otherPathNode.modelIdentifier)) { diff --git a/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNodeSpec.groovy b/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNodeSpec.groovy index ee251fb853..e6bb26aaad 100644 --- a/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNodeSpec.groovy +++ b/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNodeSpec.groovy @@ -26,7 +26,7 @@ class PathNodeSpec extends Specification { void 'test path node creation'() { given: - PathNode pathNode = new PathNode('dm', 'test model', true, true) + PathNode pathNode = new PathNode('dm', 'test model', true) expect: pathNode.prefix == 'dm' @@ -38,7 +38,7 @@ class PathNodeSpec extends Specification { void 'test path node creation with parsing'() { given: - PathNode pathNode = new PathNode('dm:test model', true, true) + PathNode pathNode = new PathNode('dm:test model', true) expect: pathNode.prefix == 'dm' @@ -49,7 +49,7 @@ class PathNodeSpec extends Specification { void 'test path node creation with parsing with model'() { given: - PathNode pathNode = new PathNode('dm:test model$main', true, true) + PathNode pathNode = new PathNode('dm:test model$main', true) expect: pathNode.prefix == 'dm' @@ -61,7 +61,7 @@ class PathNodeSpec extends Specification { void 'test path node creation with parsing with attribute'() { given: - PathNode pathNode = new PathNode('dm:test model@description', true, true) + PathNode pathNode = new PathNode('dm:test model@description', true) expect: pathNode.prefix == 'dm' @@ -74,7 +74,7 @@ class PathNodeSpec extends Specification { void 'test path node creation with parsing with model and attribute'() { given: - PathNode pathNode = new PathNode('dm:test model$main@description', true, true) + PathNode pathNode = new PathNode('dm:test model$main@description', true) expect: pathNode.prefix == 'dm' @@ -86,64 +86,64 @@ class PathNodeSpec extends Specification { void 'test path node matching on identifier and model identifier'() { when: 'same branch names' - PathNode pathNode1 = new PathNode('dm:test model$main', true, true) - PathNode pathNode2 = new PathNode('dm:test model$main', true, true) + PathNode pathNode1 = new PathNode('dm:test model$main', true) + PathNode pathNode2 = new PathNode('dm:test model$main', true) then: 'matches' pathNode1.matches(pathNode2) pathNode1 == pathNode2 when: 'defaulting branch name means we dont care about it' - pathNode2 = new PathNode('dm:test model', true, true) + pathNode2 = new PathNode('dm:test model', true) then: 'matches but does not equal' pathNode1.matches(pathNode2) pathNode1 != pathNode2 when: 'different branch names' - pathNode2 = new PathNode('dm:test model$test', true, true) + pathNode2 = new PathNode('dm:test model$test', true) then: 'no match' !pathNode1.matches(pathNode2) pathNode1 != pathNode2 when: 'same branch name different identifier' - pathNode2 = new PathNode('dm:test model 2$main', true, true) + pathNode2 = new PathNode('dm:test model 2$main', true) then: 'no match' !pathNode1.matches(pathNode2) pathNode1 != pathNode2 when: 'versionable model identifier vs branch name' - pathNode2 = new PathNode('dm:test model$1.0.0', true, true) + pathNode2 = new PathNode('dm:test model$1.0.0', true) then: 'no match' !pathNode1.matches(pathNode2) pathNode1 != pathNode2 when: 'same versionable model identifier' - pathNode1 = new PathNode('dm:test model$1.0.0', true, true) + pathNode1 = new PathNode('dm:test model$1.0.0', true) then: 'match and equality' pathNode1.matches(pathNode2) pathNode1 == pathNode2 when: 'same versionable model identifier' - pathNode1 = new PathNode('dm:test model$1.0', true, true) + pathNode1 = new PathNode('dm:test model$1.0', true) then: 'match and equality' pathNode1.matches(pathNode2) pathNode1 == pathNode2 when: 'same versionable model identifier' - pathNode1 = new PathNode('dm:test model$1', true, true) + pathNode1 = new PathNode('dm:test model$1', true) then: 'match and equality' pathNode1.matches(pathNode2) pathNode1 == pathNode2 when: 'different identifiers and different prefix' - pathNode2 = new PathNode('dc:test class', true, true) + pathNode2 = new PathNode('dc:test class', true) then: 'no match' !pathNode1.matches(pathNode2) @@ -152,15 +152,15 @@ class PathNodeSpec extends Specification { void 'test path node matching with attributes'() { when: 'same branch names' - PathNode pathNode1 = new PathNode('dm:test model$main', true, true) - PathNode pathNode2 = new PathNode('dm:test model$main', true, true) + PathNode pathNode1 = new PathNode('dm:test model$main', true) + PathNode pathNode2 = new PathNode('dm:test model$main', true) then: 'matches' pathNode1.matches(pathNode2) pathNode1 == pathNode2 when: 'attribute included' - pathNode2 = new PathNode('dm:test model$main@description', true, true) + pathNode2 = new PathNode('dm:test model$main@description', true) then: 'matches but does not equal' pathNode1.matches(pathNode2) diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy index 1b8709482c..67b16b9e0f 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/path/PathService.groovy @@ -68,7 +68,7 @@ class PathService { }.sort() as Map } - CreatorAware findResourceByPathFromRootResource(CreatorAware rootResourceOfPath, Path path) { + CreatorAware findResourceByPathFromRootResource(CreatorAware rootResourceOfPath, Path path, String modelIdentifierOverride = null) { log.debug('Searching for path {} inside {}:{}', path, rootResourceOfPath.pathPrefix, rootResourceOfPath.pathIdentifier) if (path.isEmpty()) { // assume we're in an empty/relative root which means we want the root resource @@ -76,7 +76,7 @@ class PathService { } // If the current path is absolute to the root then get the relative path so we can search into the root - Path pathToFind = path.isAbsoluteTo(rootResourceOfPath) ? path.childPath : path + Path pathToFind = path.isAbsoluteTo(rootResourceOfPath, modelIdentifierOverride) ? path.childPath : path // If no nodes in the pathToFind then return the model if (pathToFind.isEmpty()) return rootResourceOfPath @@ -94,15 +94,15 @@ class PathService { } log.trace('Found service [{}] to handle prefix [{}]', domainService.class.simpleName, childNode.prefix) - def child = domainService.findByParentIdAndPathIdentifier(rootResourceOfPath.id, childNode.getFullIdentifier()) + def child = domainService.findByParentIdAndPathIdentifier(rootResourceOfPath.id, childNode.getFullIdentifier(modelIdentifierOverride)) if (!child) { - log.warn("Child [${childNode}] does not exist in path [${path}]") + log.warn("Child [{}] does not exist in root resource [{}]", childNode, Path.from(rootResourceOfPath)) return null } // Recurse down the path for that child - findResourceByPathFromRootResource(child, pathToFind) + findResourceByPathFromRootResource(child, pathToFind, modelIdentifierOverride) } CreatorAware findResourceByPathFromRootClass(Class rootClass, Path path) { From 6df819ed627dfa41efbd354c6cbb9d2007d7c3f9 Mon Sep 17 00:00:00 2001 From: Oliver Freeman Date: Wed, 28 Jul 2021 10:15:10 +0100 Subject: [PATCH 72/82] mc-9525 Merge Into tested and working * Make a few changes to the VersionedFolderService to solve issues found during testing --- .../container/VersionedFolderService.groovy | 60 +++-- .../VersionedFolderFunctionalSpec.groovy | 226 +++++++++++++++++- 2 files changed, 270 insertions(+), 16 deletions(-) diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy index a99ff29f0f..a2a5182cd6 100644 --- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy +++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy @@ -733,6 +733,10 @@ class VersionedFolderService extends ContainerService implement void processCreationPatchIntoVersionedFolder(FieldPatchData creationPatch, VersionedFolder targetVersionedFolder, VersionedFolder sourceVersionedFolder, UserSecurityPolicyManager userSecurityPolicyManager) { CreatorAware domainToCopy = pathService.findResourceByPathFromRootResource(sourceVersionedFolder, creationPatch.path) + if (!domainToCopy) { + log.warn('Could not process creation patch into versioned folder at path [{}] as no such path exists in the source', creationPatch.path) + return + } log.debug('Creating {} into {}', creationPatch.path, creationPatch.relativePathToRoot.parent) // Potential creations are folders, models, modelItems or facets if (Utils.parentClassIsAssignableFromChild(Folder, domainToCopy.class)) { @@ -742,7 +746,7 @@ class VersionedFolderService extends ContainerService implement processCreationPatchOfModel(domainToCopy as Model, targetVersionedFolder, creationPatch.relativePathToRoot.parent, userSecurityPolicyManager) } if (Utils.parentClassIsAssignableFromChild(ModelItem, domainToCopy.class)) { - processCreationPatchOfModelItem(domainToCopy as ModelItem, targetVersionedFolder, creationPatch.relativePathToRoot.parent, userSecurityPolicyManager) + processCreationPatchOfModelItem(domainToCopy as ModelItem, targetVersionedFolder, creationPatch.relativePathToRoot, userSecurityPolicyManager) } if (Utils.parentClassIsAssignableFromChild(MultiFacetItemAware, domainToCopy.class)) { processCreationPatchOfFacet(domainToCopy as MultiFacetItemAware, targetVersionedFolder, creationPatch.relativePathToRoot.parent) @@ -750,7 +754,12 @@ class VersionedFolderService extends ContainerService implement } void processDeletionPatchIntoVersionedFolder(FieldPatchData deletionPatch, VersionedFolder targetVersionedFolder) { - CreatorAware domain = pathService.findResourceByPathFromRootResource(targetVersionedFolder, deletionPatch.relativePathToRoot) + CreatorAware domain = + pathService.findResourceByPathFromRootResource(targetVersionedFolder, deletionPatch.relativePathToRoot, getModelIdentifier(targetVersionedFolder)) + if (!domain) { + log.warn('Could not process deletion patch from versioned folder at path [{}] as no such path exists in the target', deletionPatch.relativePathToRoot) + return + } log.debug('Deleting [{}]', deletionPatch.relativePathToRoot) // Potential deletions are folders, models, modelItems or facets @@ -769,9 +778,14 @@ class VersionedFolderService extends ContainerService implement } void processModificationPatchIntoVersionedFolder(FieldPatchData modificationPatch, VersionedFolder targetVersionedFolder) { - CreatorAware domain = pathService.findResourceByPathFromRootResource(targetVersionedFolder, modificationPatch.relativePathToRoot) + CreatorAware domain = + pathService.findResourceByPathFromRootResource(targetVersionedFolder, modificationPatch.relativePathToRoot, getModelIdentifier(targetVersionedFolder)) + if (!domain) { + log.warn('Could not process modification patch into model at path [{}] as no such path exists in the target', modificationPatch.relativePathToRoot) + return + } String fieldName = modificationPatch.fieldName - log.debug('Modifying [{}] in [{}]', fieldName, modificationPatch.relativePathToRoot) + log.debug('Modifying [{}] in [{}]', fieldName, modificationPatch.path) domain."${fieldName}" = modificationPatch.sourceValue DomainService domainService = getDomainServices().find {it.handles(domain.class)} if (!domainService) throw new ApiInternalException('MSXX', "No domain service to handle modification of [${domain.domainType}]") @@ -805,7 +819,8 @@ class VersionedFolderService extends ContainerService implement log.debug('Deleting Facet from path [{}]', path) multiFacetItemAwareService.delete(multiFacetItemAware) - MultiFacetAware multiFacetAwareItem = pathService.findResourceByPathFromRootResource(targetVersionedFolder, path.getParent()) as MultiFacetAware + MultiFacetAware multiFacetAwareItem = + pathService.findResourceByPathFromRootResource(targetVersionedFolder, path.getParent(), getModelIdentifier(targetVersionedFolder)) as MultiFacetAware switch (multiFacetItemAware.domainType) { case Metadata.simpleName: multiFacetAwareItem.metadata.remove(multiFacetItemAware) @@ -832,43 +847,53 @@ class VersionedFolderService extends ContainerService implement void processCreationPatchOfFolder(Folder folderToCopy, VersionedFolder targetVersionedFolder, Path relativeParentPathToCopyTo, UserSecurityPolicyManager userSecurityPolicyManager) { log.debug('Creating Folder into VersionedFolder at [{}]', relativeParentPathToCopyTo) - Folder parentFolder = pathService.findResourceByPathFromRootResource(targetVersionedFolder, relativeParentPathToCopyTo) as Folder - folderService.copyFolder(folderToCopy, parentFolder, userSecurityPolicyManager.user, true, targetVersionedFolder.branchName, false, userSecurityPolicyManager) + Folder parentFolder = + pathService.findResourceByPathFromRootResource(targetVersionedFolder, relativeParentPathToCopyTo, getModelIdentifier(targetVersionedFolder)) as Folder + folderService. + copyFolder(folderToCopy, parentFolder, userSecurityPolicyManager.user, true, targetVersionedFolder.branchName, + targetVersionedFolder.documentationVersion, false, userSecurityPolicyManager) } void processCreationPatchOfModel(Model modelToCopy, VersionedFolder targetVersionedFolder, Path relativeParentPathToCopyTo, UserSecurityPolicyManager userSecurityPolicyManager) { ModelService modelService = folderService.findModelServiceForModel(modelToCopy) log.debug('Creating Model into VersionedFolder at [{}]', relativeParentPathToCopyTo) - Folder parentFolder = pathService.findResourceByPathFromRootResource(targetVersionedFolder, relativeParentPathToCopyTo) as Folder + Folder parentFolder = + pathService.findResourceByPathFromRootResource(targetVersionedFolder, relativeParentPathToCopyTo, getModelIdentifier(targetVersionedFolder)) as Folder modelService.copyModelAndValidateAndSave(modelToCopy, parentFolder, userSecurityPolicyManager.user, true, modelToCopy.label, modelToCopy.documentationVersion, targetVersionedFolder.branchName, false, userSecurityPolicyManager) } - void processCreationPatchOfModelItem(ModelItem modelItemToCopy, VersionedFolder targetVersionedFolder, Path relativeParentPathToCopyTo, + void processCreationPatchOfModelItem(ModelItem modelItemToCopy, VersionedFolder targetVersionedFolder, Path relativePathToCopyTo, UserSecurityPolicyManager userSecurityPolicyManager) { ModelService modelService Path modelItemToModelAbsolutePath Path modelRelativeToTargetPath - relativeParentPathToCopyTo.each {node -> + relativePathToCopyTo.each {node -> if (!modelService) { // Build up the path to the model if (!modelRelativeToTargetPath) modelRelativeToTargetPath = Path.from(node) else modelRelativeToTargetPath.addToPathNodes(node) modelService = modelServices.find {s -> s.handlesPathPrefix(node.prefix)} - } else { + } + // Dont use else as we want to make sure the model node is added to the absolute path therefore as soon as the modelservice is found we should add the node + if (modelService) { // Build up the path from the model to the modelitem - if (!modelItemToModelAbsolutePath) modelItemToModelAbsolutePath = Path.from(node) + // Make sure we repoint the path to the target model so we can use the model service code to do the copy + if (!modelItemToModelAbsolutePath) modelItemToModelAbsolutePath = Path.from(node).tap { + it.first().modelIdentifier = getModelIdentifier(targetVersionedFolder) + } else modelItemToModelAbsolutePath.addToPathNodes(node) } } if (!modelService) throw new ApiInternalException('MSXX', "No model service to handle creation of model item [${modelItemToCopy.domainType}]") - Model targetModel = pathService.findResourceByPathFromRootResource(targetVersionedFolder, modelRelativeToTargetPath) as Model + Model targetModel = + pathService.findResourceByPathFromRootResource(targetVersionedFolder, modelRelativeToTargetPath, getModelIdentifier(targetVersionedFolder)) as Model - modelService.processCreationPatchOfModelItem(modelItemToCopy, targetModel, modelItemToModelAbsolutePath.childPath, userSecurityPolicyManager) + modelService.processCreationPatchOfModelItem(modelItemToCopy, targetModel, modelItemToModelAbsolutePath.parent, userSecurityPolicyManager) } void processCreationPatchOfFacet(MultiFacetItemAware multiFacetItemAwareToCopy, VersionedFolder targetVersionedFolder, Path parentPathToCopyTo) { @@ -876,7 +901,8 @@ class VersionedFolderService extends ContainerService implement if (!multiFacetItemAwareService) throw new ApiInternalException('MSXX', "No domain service to handle creation of [${multiFacetItemAwareToCopy.domainType}]") log.debug('Creating Facet into VersionedFolder at [{}]', parentPathToCopyTo) - MultiFacetAware parentToCopyInto = pathService.findResourceByPathFromRootResource(targetVersionedFolder, parentPathToCopyTo) as MultiFacetAware + MultiFacetAware parentToCopyInto = + pathService.findResourceByPathFromRootResource(targetVersionedFolder, parentPathToCopyTo, getModelIdentifier(targetVersionedFolder)) as MultiFacetAware MultiFacetItemAware copy = multiFacetItemAwareService.copy(multiFacetItemAwareToCopy, parentToCopyInto) if (!copy.validate()) @@ -889,4 +915,8 @@ class VersionedFolderService extends ContainerService implement PersistentEntity getPersistentEntity() { grailsApplication.mappingContext.getPersistentEntity(Folder.name) } + + static String getModelIdentifier(VersionedFolder versionedFolder) { + Path.from(versionedFolder).first().getModelIdentifier() + } } diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy index c09658cc7c..326c88e566 100644 --- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy +++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy @@ -46,6 +46,7 @@ import static io.micronaut.http.HttpStatus.CREATED import static io.micronaut.http.HttpStatus.NOT_FOUND import static io.micronaut.http.HttpStatus.NO_CONTENT import static io.micronaut.http.HttpStatus.OK +import static io.micronaut.http.HttpStatus.UNPROCESSABLE_ENTITY /** *

@@ -1902,7 +1903,7 @@ class VersionedFolderFunctionalSpec extends UserAccessAndPermissionChangingFunct
         removeValidIdObject(mergeData.commonAncestor)
     }
 
-    void 'MD05 : test finding merge difference of two complex datamodels with the new style (as reader)'() {
+    void 'MD05 : test finding merge difference of two complex versioned folders (as reader)'() {
         given:
         TestMergeData mergeData = buildComplexVersionedFoldersForMerging()
 
@@ -1919,6 +1920,229 @@ class VersionedFolderFunctionalSpec extends UserAccessAndPermissionChangingFunct
         removeValidIdObject(mergeData.commonAncestor)
     }
 
+    void 'MI01 : test merge into of two versioned folders (as not logged in)'() {
+        given:
+        TestMergeData mergeData = buildSimpleVersionedFoldersForMerging()
+
+        when:
+        GET("$mergeData.source/mergeInto/$mergeData.target")
+
+        then:
+        verifyResponse NOT_FOUND, response
+
+        cleanup:
+        removeValidIdObject(mergeData.source)
+        removeValidIdObject(mergeData.target)
+        removeValidIdObject(mergeData.commonAncestor)
+    }
+
+    void 'MI02 : test merge into of two versioned folders (as authenticated/logged in)'() {
+        given:
+        TestMergeData mergeData = buildSimpleVersionedFoldersForMerging()
+
+        when:
+        loginAuthenticated()
+        GET("$mergeData.source/mergeInto/$mergeData.target")
+
+        then:
+        verifyResponse NOT_FOUND, response
+
+        cleanup:
+        removeValidIdObject(mergeData.source)
+        removeValidIdObject(mergeData.target)
+        removeValidIdObject(mergeData.commonAncestor)
+    }
+
+    void 'MI03 : test merge into of two versioned folders (as reader)'() {
+        given:
+        TestMergeData mergeData = buildSimpleVersionedFoldersForMerging()
+
+        when:
+        loginReader()
+        PUT("$mergeData.source/mergeInto/$mergeData.target", [:])
+
+        then:
+        verifyForbidden(response)
+
+        cleanup:
+        removeValidIdObject(mergeData.source)
+        removeValidIdObject(mergeData.target)
+        removeValidIdObject(mergeData.commonAncestor)
+    }
+
+
+    void 'MI04 : test merging diff with no patch data'() {
+        given:
+        TestMergeData mergeData = buildSimpleVersionedFoldersForMerging()
+
+        when:
+        loginEditor()
+        PUT("$mergeData.source/mergeInto/$mergeData.target", [:])
+
+        then:
+        verifyResponse(UNPROCESSABLE_ENTITY, response)
+        responseBody().total == 1
+        responseBody().errors[0].message.contains('cannot be null')
+
+        cleanup:
+        removeValidIdObject(mergeData.source)
+        removeValidIdObject(mergeData.target)
+        removeValidIdObject(mergeData.commonAncestor)
+    }
+
+    void 'MI05 : test merging diff with URI id not matching body id'() {
+        given:
+        TestMergeData mergeData = buildSimpleVersionedFoldersForMerging()
+
+        when:
+        loginEditor()
+        PUT("$mergeData.source/mergeInto/$mergeData.target", [patch:
+                                                                  [
+                                                                      targetId: mergeData.target,
+                                                                      sourceId: UUID.randomUUID().toString(),
+                                                                      label   : "Functional Test Model",
+                                                                      count   : 0,
+                                                                      patches : []
+                                                                  ]
+        ])
+
+        then:
+        verifyResponse(UNPROCESSABLE_ENTITY, response)
+        responseBody().message == 'Source versioned folder id passed in request body does not match source versioned folder id in URI.'
+
+        when:
+        PUT("$mergeData.source/mergeInto/$mergeData.target", [patch:
+                                                                  [
+                                                                      targetId: UUID.randomUUID().toString(),
+                                                                      sourceId: mergeData.source,
+                                                                      label   : "Functional Test Model",
+                                                                      count   : 0,
+                                                                      patches : []
+                                                                  ]
+        ])
+
+        then:
+        verifyResponse(UNPROCESSABLE_ENTITY, response)
+        responseBody().message == 'Target versioned folder id passed in request body does not match target versioned folder id in URI.'
+
+        when:
+        PUT("$mergeData.source/mergeInto/$mergeData.target", [patch:
+                                                                  [
+                                                                      targetId: mergeData.target,
+                                                                      sourceId: mergeData.source,
+                                                                      label   : "Functional Test Model",
+                                                                      count   : 0,
+                                                                      patches : []
+                                                                  ]
+        ])
+
+        then:
+        verifyResponse(OK, response)
+        responseBody().id == mergeData.target
+
+        cleanup:
+        removeValidIdObject(mergeData.source)
+        removeValidIdObject(mergeData.target)
+        removeValidIdObject(mergeData.commonAncestor)
+    }
+
+    void 'MI06 : test merge into of two versioned folders'() {
+        given:
+        TestMergeData mergeData = buildSimpleVersionedFoldersForMerging()
+
+        when:
+        loginEditor()
+        PUT("$mergeData.source/mergeInto/$mergeData.target", [
+            patch:
+                [
+                    targetId: mergeData.target,
+                    sourceId: mergeData.source,
+                    label   : "Functional Test Model",
+                    count   : 0,
+                    patches : []
+                ]
+        ])
+
+        then:
+        verifyResponse OK, response
+        responseBody().id == mergeData.target
+
+        cleanup:
+        removeValidIdObject(mergeData.source)
+        removeValidIdObject(mergeData.target)
+        removeValidIdObject(mergeData.commonAncestor)
+    }
+
+    void 'MI07 : test merge into of two complex versioned folders'() {
+        given:
+        TestMergeData mergeData = buildComplexVersionedFoldersForMerging()
+
+        when:
+        loginReader()
+        GET("$mergeData.source/mergeDiff/$mergeData.target")
+
+        then:
+        verifyResponse(OK, response)
+
+        when:
+        def diffs = responseBody().diffs
+        loginEditor()
+        PUT("$mergeData.source/mergeInto/$mergeData.target", [
+            patch:
+                [
+                    targetId: mergeData.target,
+                    sourceId: mergeData.source,
+                    label   : "Functional Test Model",
+                    count   : 0,
+                    patches : diffs
+                ]
+        ])
+
+        then:
+        verifyResponse OK, response
+        responseBody().id == mergeData.target
+        responseBody().description == 'source description on the versioned folder'
+
+        when:
+        GET("dataModels/$mergeData.targetMap.dataModelId", MAP_ARG, true)
+
+        then:
+        responseBody().description == 'DescriptionLeft'
+
+        when:
+        GET("dataModels/$mergeData.targetMap.dataModelId/dataClasses", MAP_ARG, true)
+
+        then:
+        responseBody().items.label as Set == ['existingClass', 'modifyAndModifyReturningDifference', 'modifyLeftOnly',
+                                              'addAndAddReturningDifference', 'modifyAndDelete', 'addLeftOnly',
+                                              'modifyRightOnly', 'addRightOnly', 'modifyAndModifyReturningNoDifference',
+                                              'addAndAddReturningNoDifference'] as Set
+        responseBody().items.find {dataClass -> dataClass.label == 'modifyAndDelete'}.description == 'Description'
+        responseBody().items.find {dataClass -> dataClass.label == 'addAndAddReturningDifference'}.description == 'DescriptionLeft'
+        responseBody().items.find {dataClass -> dataClass.label == 'modifyAndModifyReturningDifference'}.description == 'DescriptionLeft'
+        responseBody().items.find {dataClass -> dataClass.label == 'modifyLeftOnly'}.description == 'Description'
+
+        when:
+        GET("dataModels/$mergeData.targetMap.dataModelId/dataClasses/$mergeData.targetMap.existingClass/dataClasses", MAP_ARG, true)
+
+        then:
+        responseBody().items.label as Set == ['addRightToExistingClass', 'addLeftToExistingClass'] as Set
+
+        when:
+        GET("dataModels/$mergeData.targetMap.dataModelId/metadata", MAP_ARG, true)
+
+        then:
+        responseBody().items.find {it.namespace == 'functional.test' && it.key == 'modifyOnSource'}.value == 'source has modified this'
+        responseBody().items.find {it.namespace == 'functional.test' && it.key == 'modifyAndDelete'}.value == 'source has modified this also'
+        !responseBody().items.find {it.namespace == 'functional.test' && it.key == 'metadataDeleteFromSource'}
+        responseBody().items.find {it.namespace == 'functional.test' && it.key == 'addToSourceOnly'}
+
+        cleanup:
+        removeValidIdObject(mergeData.source)
+        removeValidIdObject(mergeData.target)
+        removeValidIdObject(mergeData.commonAncestor)
+    }
+
     Map buildModelVersionTree() {
         /*
                                                    /- anotherFork

From 089b34e187eaa1ca5ab70c6641b718b5aeb44165 Mon Sep 17 00:00:00 2001
From: Oliver Freeman 
Date: Wed, 28 Jul 2021 11:01:07 +0100
Subject: [PATCH 73/82] gh-112 Add aliasesString to the diff for CIs

* We cant add aliases as a proper list as its a list of strings which arent "diffable"
* Add tests to prove it mergeDiffs and mergeInto
---
 .../core/model/CatalogueItem.groovy           |  2 +-
 .../datamodel/DataModelFunctionalSpec.groovy  | 80 +++++++++++++++++++
 2 files changed, 81 insertions(+), 1 deletion(-)

diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy
index 2f43696e9f..5dfa92b4d8 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/CatalogueItem.groovy
@@ -24,7 +24,6 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.Annotation
 import uk.ac.ox.softeng.maurodatamapper.core.facet.BreadcrumbTree
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Metadata
 import uk.ac.ox.softeng.maurodatamapper.core.facet.Rule
-import uk.ac.ox.softeng.maurodatamapper.core.facet.rule.RuleRepresentation
 import uk.ac.ox.softeng.maurodatamapper.core.model.container.CatalogueItemClassifierAware
 import uk.ac.ox.softeng.maurodatamapper.core.model.facet.MultiFacetAware
 import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.EditHistoryAware
@@ -100,6 +99,7 @@ trait CatalogueItem implements InformationAware, EditHistory
             .rightHandSide(rhsId, rhs)
             .appendString('label', lhs.label, rhs.label)
             .appendString('description', lhs.description, rhs.description)
+            .appendString('aliasesString', lhs.aliasesString, rhs.aliasesString)
             .appendList(Metadata, 'metadata', lhs.metadata, rhs.metadata)
             .appendList(Annotation, 'annotations', lhs.annotations, rhs.annotations)
             .appendList(Rule, 'rule', lhs.rules, rhs.rules)
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 cce703cf5f..17847ab0ff 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
@@ -1628,6 +1628,41 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec {
         cleanUpData(mergeData.commonAncestor)
     }
 
+    void 'MD05 : test finding merge diff with new style diff with aliases gh-112'() {
+        given:
+        String id = createNewItem(validJson)
+
+        PUT("$id/finalise", [versionChangeType: 'Major'])
+        verifyResponse OK, response
+        PUT("$id/newBranchModelVersion", [branchName: VersionAwareConstraints.DEFAULT_BRANCH_NAME])
+        verifyResponse CREATED, response
+        String target = responseBody().id
+        PUT("$id/newBranchModelVersion", [branchName: 'interestingBranch'])
+        verifyResponse CREATED, response
+        String source = responseBody().id
+        PUT(source, [aliases: ['not main branch', 'mergeInto']])
+        verifyResponse OK, response
+
+
+        when:
+        GET("$source/mergeDiff/$target?isLegacy=false", STRING_ARG)
+        log.warn('{}', jsonResponseBody())
+        GET("$source/mergeDiff/$target?isLegacy=false")
+
+        then:
+        verifyResponse OK, response
+        responseBody().targetId == target
+        responseBody().sourceId == source
+        responseBody().diffs.first().path == 'dm:Functional Test Model$interestingBranch@aliasesString'
+        responseBody().diffs.first().sourceValue == 'mergeInto|not main branch'
+        responseBody().diffs.first().type == 'modification'
+
+        cleanup:
+        cleanUpData(source)
+        cleanUpData(target)
+        cleanUpData(id)
+    }
+
     void 'MP01 : test merging diff with no patch data'() {
         given:
         String id = createNewItem(validJson)
@@ -2513,6 +2548,51 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec {
         cleanUpData(id)
     }
 
+    void 'MP10 : test merge into with new style diff with aliases gh-112'() {
+        given:
+        String id = createNewItem(validJson)
+
+        PUT("$id/finalise", [versionChangeType: 'Major'])
+        verifyResponse OK, response
+        PUT("$id/newBranchModelVersion", [branchName: VersionAwareConstraints.DEFAULT_BRANCH_NAME])
+        verifyResponse CREATED, response
+        String target = responseBody().id
+        PUT("$id/newBranchModelVersion", [branchName: 'interestingBranch'])
+        verifyResponse CREATED, response
+        String source = responseBody().id
+        PUT(source, [aliases: ['not main branch', 'mergeInto']])
+
+
+        when:
+        GET("$source/mergeDiff/$target?isLegacy=false")
+
+        then:
+        verifyResponse OK, response
+
+        when:
+        List patches = responseBody().diffs
+        PUT("$source/mergeInto/$target?isLegacy=false", [
+            patch: [
+                targetId: responseBody().targetId,
+                sourceId: responseBody().sourceId,
+                label   : responseBody().label,
+                count   : patches.size(),
+                patches : patches]
+        ])
+
+        then:
+        verifyResponse OK, response
+        responseBody().id == target
+        responseBody().aliases.size() == 2
+        responseBody().aliases.any {it == 'mergeInto'}
+        responseBody().aliases.any {it == 'not main branch'}
+
+        cleanup:
+        cleanUpData(source)
+        cleanUpData(target)
+        cleanUpData(id)
+    }
+
     TestMergeData buildComplexDataModelsForMerging() {
 
         String id = createNewItem(validJson)

From 69ba50cf1fee75f32ee8ba9f3b524247eb8c0903 Mon Sep 17 00:00:00 2001
From: Oliver Freeman 
Date: Wed, 28 Jul 2021 12:20:43 +0100
Subject: [PATCH 74/82] gh-113 Fix datamodel type search filtering

* Need to make sure all models have the modeltype searched
* Need to use phrase searching on the modeltype rather than keyword
---
 .../softeng/maurodatamapper/core/search/ModelSearch.groovy   | 4 ++--
 .../ac/ox/softeng/maurodatamapper/datamodel/DataModel.groovy | 4 ++--
 .../search/searchparamfilter/DataModelTypeFilter.groovy      | 5 +++--
 .../maurodatamapper/referencedata/ReferenceDataModel.groovy  | 4 ++--
 .../ac/ox/softeng/maurodatamapper/terminology/CodeSet.groovy | 4 ++--
 .../softeng/maurodatamapper/terminology/Terminology.groovy   | 4 ++--
 6 files changed, 13 insertions(+), 12 deletions(-)
 rename mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/hibernate/search/DataModelSearch.groovy => mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/search/ModelSearch.groovy (91%)

diff --git a/mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/hibernate/search/DataModelSearch.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/search/ModelSearch.groovy
similarity index 91%
rename from mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/hibernate/search/DataModelSearch.groovy
rename to mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/search/ModelSearch.groovy
index a4acda9e58..c7907d5e1f 100644
--- a/mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/hibernate/search/DataModelSearch.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/search/ModelSearch.groovy
@@ -15,7 +15,7 @@
  *
  * SPDX-License-Identifier: Apache-2.0
  */
-package uk.ac.ox.softeng.maurodatamapper.datamodel.hibernate.search
+package uk.ac.ox.softeng.maurodatamapper.core.search
 
 import uk.ac.ox.softeng.maurodatamapper.core.search.StandardSearch
 import uk.ac.ox.softeng.maurodatamapper.hibernate.search.CallableSearch
@@ -23,7 +23,7 @@ import uk.ac.ox.softeng.maurodatamapper.hibernate.search.CallableSearch
 /**
  * @since 05/03/2020
  */
-class DataModelSearch {
+class ModelSearch {
 
     static search = {
         CallableSearch.call(StandardSearch, delegate)
diff --git a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModel.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModel.groovy
index 39eaa0fad9..fc845c576c 100644
--- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModel.groovy
+++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModel.groovy
@@ -30,13 +30,13 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLink
 import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.ModelConstraints
 import uk.ac.ox.softeng.maurodatamapper.core.model.Model
 import uk.ac.ox.softeng.maurodatamapper.core.model.ModelItem
+import uk.ac.ox.softeng.maurodatamapper.core.search.ModelSearch
 import uk.ac.ox.softeng.maurodatamapper.core.traits.domain.IndexedSiblingAware
 import uk.ac.ox.softeng.maurodatamapper.datamodel.databinding.DataTypeCollectionBindingHelper
 import uk.ac.ox.softeng.maurodatamapper.datamodel.facet.SummaryMetadata
 import uk.ac.ox.softeng.maurodatamapper.datamodel.facet.SummaryMetadataAware
 import uk.ac.ox.softeng.maurodatamapper.datamodel.gorm.constraint.validator.DataModelDataClassCollectionValidator
 import uk.ac.ox.softeng.maurodatamapper.datamodel.gorm.constraint.validator.ImportLabelValidator
-import uk.ac.ox.softeng.maurodatamapper.datamodel.hibernate.search.DataModelSearch
 import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataClass
 import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataElement
 import uk.ac.ox.softeng.maurodatamapper.datamodel.item.datatype.DataType
@@ -139,7 +139,7 @@ class DataModel implements Model, SummaryMetadataAware, IndexedSiblin
     ]
 
     static search = {
-        CallableSearch.call(DataModelSearch, delegate)
+        CallableSearch.call(ModelSearch, delegate)
     }
 
     /**
diff --git a/mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/rest/transport/search/searchparamfilter/DataModelTypeFilter.groovy b/mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/rest/transport/search/searchparamfilter/DataModelTypeFilter.groovy
index 3df54fa959..418328d100 100644
--- a/mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/rest/transport/search/searchparamfilter/DataModelTypeFilter.groovy
+++ b/mdm-plugin-datamodel/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/datamodel/rest/transport/search/searchparamfilter/DataModelTypeFilter.groovy
@@ -30,10 +30,11 @@ class DataModelTypeFilter implements SearchParamFilter {
     }
 
     Closure getClosure(SearchParams searchParams) {
+        List validModelTypes = searchParams.dataModelTypes.collect {dt -> DataModelType.findForLabel(dt).toString()}.findAll()
         Lucene.defineAdditionalLuceneQuery {
             should {
-                searchParams.dataModelTypes.each {dt ->
-                    keyword 'dataModelType', DataModelType.findForLabel(dt).toString()
+                validModelTypes.each {dt ->
+                    phrase 'modelType', dt
                 }
             }
         }
diff --git a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModel.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModel.groovy
index 4a89326f13..ccd24805d0 100644
--- a/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModel.groovy
+++ b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/ReferenceDataModel.groovy
@@ -30,7 +30,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLink
 import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.ModelConstraints
 import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.VersionAwareConstraints
 import uk.ac.ox.softeng.maurodatamapper.core.model.Model
-import uk.ac.ox.softeng.maurodatamapper.core.search.StandardSearch
+import uk.ac.ox.softeng.maurodatamapper.core.search.ModelSearch
 import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CallableConstraints
 import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.validator.ParentOwnedLabelCollectionValidator
 import uk.ac.ox.softeng.maurodatamapper.hibernate.VersionUserType
@@ -95,7 +95,7 @@ class ReferenceDataModel implements Model, ReferenceSummaryM
     ]
 
     static search = {
-        CallableSearch.call(StandardSearch, delegate)
+        CallableSearch.call(ModelSearch, delegate)
     }
 
     ReferenceDataModel() {
diff --git a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSet.groovy b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSet.groovy
index e57e6b4ec9..397feecbcf 100644
--- a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSet.groovy
+++ b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSet.groovy
@@ -28,7 +28,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLink
 import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLink
 import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.ModelConstraints
 import uk.ac.ox.softeng.maurodatamapper.core.model.Model
-import uk.ac.ox.softeng.maurodatamapper.core.search.StandardSearch
+import uk.ac.ox.softeng.maurodatamapper.core.search.ModelSearch
 import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CallableConstraints
 import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.validator.ParentOwnedLabelCollectionValidator
 import uk.ac.ox.softeng.maurodatamapper.hibernate.VersionUserType
@@ -84,7 +84,7 @@ class CodeSet implements Model {
     ]
 
     static search = {
-        CallableSearch.call(StandardSearch, delegate)
+        CallableSearch.call(ModelSearch, delegate)
     }
 
     CodeSet() {
diff --git a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/Terminology.groovy b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/Terminology.groovy
index 076511f0ba..0339c64cb1 100644
--- a/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/Terminology.groovy
+++ b/mdm-plugin-terminology/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/terminology/Terminology.groovy
@@ -29,7 +29,7 @@ import uk.ac.ox.softeng.maurodatamapper.core.facet.SemanticLink
 import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLink
 import uk.ac.ox.softeng.maurodatamapper.core.gorm.constraint.callable.ModelConstraints
 import uk.ac.ox.softeng.maurodatamapper.core.model.Model
-import uk.ac.ox.softeng.maurodatamapper.core.search.StandardSearch
+import uk.ac.ox.softeng.maurodatamapper.core.search.ModelSearch
 import uk.ac.ox.softeng.maurodatamapper.gorm.constraint.callable.CallableConstraints
 import uk.ac.ox.softeng.maurodatamapper.hibernate.VersionUserType
 import uk.ac.ox.softeng.maurodatamapper.hibernate.search.CallableSearch
@@ -87,7 +87,7 @@ class Terminology implements Model {
     ]
 
     static search = {
-        CallableSearch.call(StandardSearch, delegate)
+        CallableSearch.call(ModelSearch, delegate)
     }
 
     Terminology() {

From e4a315dbf5678358476a05aa5ea4665a391ca1de Mon Sep 17 00:00:00 2001
From: Oliver Freeman 
Date: Wed, 28 Jul 2021 13:29:34 +0100
Subject: [PATCH 75/82] Fix issues found during testing

---
 .../container/VersionedFolderService.groovy     | 15 +++++++++++++++
 .../DataModelServiceIntegrationSpec.groovy      | 16 +++++-----------
 .../terminology/CodeSetFunctionalSpec.groovy    |  9 +--------
 .../TerminologyFunctionalSpec.groovy            |  9 +--------
 .../TerminologyServiceIntegrationSpec.groovy    |  7 +------
 .../test/functional/TestMergeData.groovy        | 17 +++++++++++++++++
 6 files changed, 40 insertions(+), 33 deletions(-)

diff --git a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy
index a2a5182cd6..428f845d86 100644
--- a/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy
+++ b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/container/VersionedFolderService.groovy
@@ -67,6 +67,8 @@ import grails.gorm.transactions.Transactional
 import groovy.util.logging.Slf4j
 import org.grails.datastore.gorm.GormValidateable
 import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.orm.hibernate.cfg.JoinTable
+import org.grails.orm.hibernate.cfg.PropertyConfig
 import org.grails.orm.hibernate.proxy.HibernateProxyHandler
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.context.MessageSource
@@ -916,6 +918,19 @@ class VersionedFolderService extends ContainerService implement
         grailsApplication.mappingContext.getPersistentEntity(Folder.name)
     }
 
+    @Override
+    JoinTable getJoinTable(PersistentEntity persistentEntity, String facetProperty) {
+        if (facetProperty == 'versionLinks') {
+            PropertyConfig propertyConfig = grailsApplication
+                .mappingContext
+                .getPersistentEntity(VersionedFolder.name)
+                .getPropertyByName(facetProperty)
+                .mapping
+                .mappedForm as PropertyConfig
+            return propertyConfig.joinTable
+        } else super.getJoinTable(persistentEntity, facetProperty)
+    }
+
     static String getModelIdentifier(VersionedFolder versionedFolder) {
         Path.from(versionedFolder).first().getModelIdentifier()
     }
diff --git a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy
index 578a502961..5f822ba3ad 100644
--- a/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy
+++ b/mdm-plugin-datamodel/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/DataModelServiceIntegrationSpec.groovy
@@ -957,19 +957,13 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec {
 
         then:
         !mergeDiff.isEmpty()
-        mergeDiff.numberOfDiffs == 16
+        mergeDiff.numberOfDiffs == 15
 
-        when: 'branch name is a non-conflicting diff'
-        FieldMergeDiff stringFieldDiff = mergeDiff.find {it.fieldName == 'branchName'}
-
-        then:
-        stringFieldDiff.source == 'test'
-        stringFieldDiff.target == VersionAwareConstraints.DEFAULT_BRANCH_NAME
-        stringFieldDiff.commonAncestor == VersionAwareConstraints.DEFAULT_BRANCH_NAME
-        !stringFieldDiff.isMergeConflict()
+        then: 'branch name is not a diff'
+        !mergeDiff.find {it.fieldName == 'branchName'}
 
         when: 'organisation is a non-conflicting change'
-        stringFieldDiff = mergeDiff.find {it.fieldName == 'organisation'}
+        FieldMergeDiff stringFieldDiff = mergeDiff.find {it.fieldName == 'organisation'}
 
         then:
         stringFieldDiff.source == 'under test'
@@ -1161,7 +1155,7 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec {
 
         then:
         !mergeDiff.isEmpty()
-        mergeDiff.numberOfDiffs == 16
+        mergeDiff.numberOfDiffs == 15
 
         when:
         DataClass targetExistingClass = dataClassService.findByParentAndLabel(rightMain, 'existingClass')
diff --git a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetFunctionalSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetFunctionalSpec.groovy
index e7492dcdb0..b07664cd47 100644
--- a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetFunctionalSpec.groovy
+++ b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/CodeSetFunctionalSpec.groovy
@@ -170,15 +170,8 @@ class CodeSetFunctionalSpec extends ResourceFunctionalSpec {
   "leftId": "${json-unit.matches:id}",
   "rightId": "${json-unit.matches:id}",
   "label": "Functional Test Model",
-  "count": 9,
+  "count": 8,
   "diffs": [
-    {
-      "branchName": {
-        "left": "main",
-        "right": "source",
-        "isMergeConflict": false
-      }
-    },
     {
       "description": {
         "left": "DescriptionRight",
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 026cb939c9..c4bca6bc75 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
@@ -165,15 +165,8 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec {
   "leftId": "${json-unit.matches:id}",
   "rightId": "${json-unit.matches:id}",
   "label": "Functional Test Model",
-  "count": 18,
+  "count": 17,
   "diffs": [
-    {
-      "branchName": {
-        "left": "main",
-        "right": "source",
-        "isMergeConflict": false
-      }
-    },
     {
       "description": {
         "left": "DescriptionRight",
diff --git a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy
index 53fd82c43a..7c85eaed29 100644
--- a/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy
+++ b/mdm-plugin-terminology/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/terminology/TerminologyServiceIntegrationSpec.groovy
@@ -360,12 +360,7 @@ class TerminologyServiceIntegrationSpec extends BaseTerminologyIntegrationSpec {
         MergeDiff mergeDiff = terminologyService.getMergeDiffForModels(terminologyService.get(left.id), terminologyService.get(right.id))
 
         then:
-        mergeDiff.size() == 1
-        mergeDiff.first().fieldName == 'branchName'
-        mergeDiff.first().target == 'right'
-        mergeDiff.first().source == 'left'
-        mergeDiff.first().isMergeConflict()
-        mergeDiff.first().commonAncestor == VersionAwareConstraints.DEFAULT_BRANCH_NAME
+        mergeDiff.size() == 0
     }
 }
 
diff --git a/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/functional/TestMergeData.groovy b/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/functional/TestMergeData.groovy
index 506727cea8..2c67de9806 100644
--- a/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/functional/TestMergeData.groovy
+++ b/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/functional/TestMergeData.groovy
@@ -1,3 +1,20 @@
+/*
+ * 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.test.functional
 
 /**

From c4fa4c50f964fd3330d7204fe3a77c009fe0868c Mon Sep 17 00:00:00 2001
From: OButler 
Date: Wed, 28 Jul 2021 13:25:09 +0100
Subject: [PATCH 76/82] Added new actions postFinalisedReadable and
 postFinalisedEditable

Fixed tests that broke due to new actions
Renamed actions to postFinalisedReadable and postFinalisedEditable
---
 .../policy/GroupBasedUserSecurityPolicyManager.groovy      | 6 ++++++
 .../maurodatamapper/security/policy/ResourceActions.groovy | 4 ++++
 .../core/container/VersionedFolderFunctionalSpec.groovy    | 1 +
 .../testing/functional/core/path/PathFunctionalSpec.groovy | 1 +
 ...essPermissionChangingAndVersioningFunctionalSpec.groovy | 7 +++++--
 5 files changed, 17 insertions(+), 2 deletions(-)

diff --git a/mdm-security/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/policy/GroupBasedUserSecurityPolicyManager.groovy b/mdm-security/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/policy/GroupBasedUserSecurityPolicyManager.groovy
index b171ed49c3..8c18dbbd34 100644
--- a/mdm-security/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/policy/GroupBasedUserSecurityPolicyManager.groovy
+++ b/mdm-security/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/policy/GroupBasedUserSecurityPolicyManager.groovy
@@ -72,6 +72,8 @@ import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.S
 import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.STANDARD_EDIT_ACTIONS
 import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.UPDATE_ACTION
 import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.USER_ADMIN_ACTIONS
+import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.POST_FINALISED_EDITABLE
+import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.POST_FINALISED_READABLE
 import static uk.ac.ox.softeng.maurodatamapper.security.policy.TreeActions.CONTAINER_ADMIN_CONTAINER_TREE_ACTIONS
 import static uk.ac.ox.softeng.maurodatamapper.security.policy.TreeActions.CONTAINER_ADMIN_FOLDER_TREE_ACTIONS
 import static uk.ac.ox.softeng.maurodatamapper.security.policy.TreeActions.CONTAINER_ADMIN_MODEL_TREE_ACTIONS
@@ -94,6 +96,8 @@ import static uk.ac.ox.softeng.maurodatamapper.security.role.GroupRole.READER_RO
 import static uk.ac.ox.softeng.maurodatamapper.security.role.GroupRole.REVIEWER_ROLE_NAME
 import static uk.ac.ox.softeng.maurodatamapper.security.role.GroupRole.USER_ADMIN_ROLE_NAME
 
+
+
 /**
  * This class should be built using the GroupBasedSecurityPolicyManagerService which will have transactionality available.
  * All operations on this class and inside this class should assume no session and no transaction are available.
@@ -633,6 +637,7 @@ class GroupBasedUserSecurityPolicyManager implements UserSecurityPolicyManager {
         if (role.canFinalise()) updatedActions << FINALISE_ACTION
         if (role.isFinalised()) {
             updatedActions.removeAll(DISALLOWED_ONCE_FINALISED_ACTIONS)
+            updatedActions.addAll(POST_FINALISED_EDITABLE)
         }
         if (role.canVersion()) {
             updatedActions.addAll(EDITOR_VERSIONING_ACTIONS)
@@ -644,6 +649,7 @@ class GroupBasedUserSecurityPolicyManager implements UserSecurityPolicyManager {
         List updatedActions = new ArrayList<>(baseActions)
         if (role.isFinalised()) {
             updatedActions.removeAll(DISALLOWED_ONCE_FINALISED_ACTIONS)
+            updatedActions.addAll(POST_FINALISED_READABLE)
         }
         if (role.canVersion() && isAuthenticated()) {
             updatedActions.addAll(READER_VERSIONING_ACTIONS)
diff --git a/mdm-security/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/policy/ResourceActions.groovy b/mdm-security/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/policy/ResourceActions.groovy
index 60346972bd..6ccbd8ab17 100644
--- a/mdm-security/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/policy/ResourceActions.groovy
+++ b/mdm-security/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/policy/ResourceActions.groovy
@@ -39,6 +39,8 @@ class ResourceActions {
     public static final String MERGE_INTO_ACTION = 'mergeInto'
     public static final String READ_BY_EVERYONE_ACTION = 'readByEveryone'
     public static final String READ_BY_AUTHENTICATED_ACTION = 'readByAuthenticated'
+    public static final String POST_FINALISED_READABLE = 'postFinalisedReadable'
+    public static final String POST_FINALISED_EDITABLE = 'postFinalisedEditable'
 
     public static final List READER_VERSIONING_ACTIONS = [CREATE_NEW_VERSIONS_ACTION,
                                                                   NEW_FORK_MODEL_ACTION]
@@ -90,4 +92,6 @@ class ResourceActions {
     public static final List USER_ADMIN_ACTIONS = [SHOW_ACTION,
                                                            UPDATE_ACTION,
                                                            DISABLE_ACTION]
+
+
 }
diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy
index 45eae96d5c..882fffc60f 100644
--- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy
+++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy
@@ -1803,6 +1803,7 @@ class VersionedFolderFunctionalSpec extends UserAccessAndPermissionChangingFunct
             'newForkModel',
             'comment',
             'newModelVersion',
+            'postFinalisedEditable',
             'newDocumentationVersion',
             'newBranchModelVersion',
             'softDelete',
diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy
index 7365e0b132..5b3fbf0809 100644
--- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy
+++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy
@@ -824,6 +824,7 @@ class PathFunctionalSpec extends FunctionalSpec {
             "show",
             "createNewVersions",
             "newForkModel",
+            "postFinalisedReadable",
             "comment"
           ],
           "lastUpdated": "${json-unit.matches:offsetDateTime}",
diff --git a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelUserAccessPermissionChangingAndVersioningFunctionalSpec.groovy b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelUserAccessPermissionChangingAndVersioningFunctionalSpec.groovy
index 3bf293e91e..9b5e07e859 100644
--- a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelUserAccessPermissionChangingAndVersioningFunctionalSpec.groovy
+++ b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelUserAccessPermissionChangingAndVersioningFunctionalSpec.groovy
@@ -117,6 +117,7 @@ abstract class ModelUserAccessPermissionChangingAndVersioningFunctionalSpec exte
             'show',
             'createNewVersions',
             'newForkModel',
+            'postFinalisedReadable'
         ].sort()
     }
 
@@ -130,7 +131,9 @@ abstract class ModelUserAccessPermissionChangingAndVersioningFunctionalSpec exte
             'newDocumentationVersion',
             'newBranchModelVersion',
             'softDelete',
-            'delete'
+            'delete',
+            'postFinalisedReadable'
+
         ].sort()
     }
 
@@ -1579,7 +1582,7 @@ abstract class ModelUserAccessPermissionChangingAndVersioningFunctionalSpec exte
         then:
         verifyResponse(OK, response)
         response.body().readableByEveryone == true
-        response.body().availableActions == ['show']
+        response.body().availableActions == ['postFinalisedReadable', 'show']
 
         when: 'removing readable by everyone'
         loginEditor()

From 5fcd682abb48c8bc075ba350086c8012e4c00905 Mon Sep 17 00:00:00 2001
From: Oliver Freeman 
Date: Wed, 28 Jul 2021 16:54:28 +0100
Subject: [PATCH 77/82] gh-115 Update how we hanlde import as new doc and
 branch version

---
 .../maurodatamapper/core/model/Model.groovy   |  2 +
 .../core/model/ModelService.groovy            | 52 ++++++++++++-------
 2 files changed, 36 insertions(+), 18 deletions(-)

diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Model.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Model.groovy
index 51b621c6de..311356900c 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Model.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/Model.groovy
@@ -71,6 +71,8 @@ trait Model extends CatalogueItem implements SecurableRes
         }
         if (that instanceof Model) {
             res == 0 ? this.documentationVersion <=> that.documentationVersion : res
+            res == 0 ? this.modelVersion <=> that.modelVersion : res
+            res == 0 ? this.branchName <=> that.branchName : res
         }
         res
     }
diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy
index 4b10d1befa..03062e313b 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy
@@ -815,6 +815,10 @@ abstract class ModelService extends CatalogueItemService imp
         if (model.finalised) {
             model.dateFinalised = model.dateFinalised ?: OffsetDateTime.now()
             model.modelVersion = model.modelVersion ?: getNextModelVersion(model, null, VersionChangeType.MAJOR)
+        } else {
+            // Make sure that, if after all the checking, the model is not finalised we dont have any modelVersion or date set
+            model.dateFinalised = null
+            model.modelVersion = null
         }
     }
 
@@ -828,15 +832,23 @@ abstract class ModelService extends CatalogueItemService imp
         if (importAsNewDocumentationVersion) {
 
             if (countByAuthorityAndLabel(model.authority, model.label)) {
-                List existingModels = findAllByAuthorityAndLabel(model.authority, model.label)
-                existingModels.each {existing ->
-                    log.debug('Setting Model as new documentation version of [{}:{}]', existing.label, existing.documentationVersion)
-                    if (!existing.finalised) finaliseModel(existing, catalogueUser, null, null, null)
-                    setModelIsNewDocumentationVersionOfModel(model, existing, catalogueUser)
+                // Doc versions must be built off finalised versions, they cannot be built of a finalised version where a branch already exists
+                // So we just get the latest model and finalise if its not finalised
+                K latest = findLatestModelByLabel(model.label)
+
+                if (!latest || latest.id == model.id) {
+                    log.info('Marked as importAsNewDocumentationVersion but no existing Models with label [{}]', model.label)
+                    return
+                }
+
+                if (!latest.finalised) {
+                    finaliseModel(latest, catalogueUser, Version.from('1'), null, null)
+                    save(latest, flush: false, validate: false)
                 }
-                Version latestVersion = existingModels.max {it.documentationVersion}.documentationVersion
-                model.documentationVersion = Version.nextMajorVersion(latestVersion)
 
+                // Now we have a finalised model to work from
+                setModelIsNewDocumentationVersionOfModel(model, latest, catalogueUser)
+                model.documentationVersion = Version.nextMajorVersion(latest.documentationVersion)
             } else log.info('Marked as importAsNewDocumentationVersion but no existing Models with label [{}]', model.label)
         }
     }
@@ -845,26 +857,30 @@ abstract class ModelService extends CatalogueItemService imp
         if (importAsNewBranchModelVersion) {
 
             if (countByAuthorityAndLabel(model.authority, model.label)) {
+                // Branches need to be created from a finalised version
+                // But we can create a new branch even if existing branches
+                // So try for a finalised model, if none then get the latest main branch and finalise that so we have
+                // a finalised version
                 K latest = findLatestFinalisedModelByLabel(model.label)
 
                 if (!latest) {
                     log.info('No finalised model to create branch from so finalising existing main branch')
                     latest = findCurrentMainBranchByLabel(model.label)
+                    if (!latest || latest.id == model.id) {
+                        log.info('Marked as importAsNewBranchModelVersion but no existing Models with label [{}]', model.label)
+                        return
+                    }
                     finaliseModel(latest, catalogueUser, Version.from('1'), null, null)
-                    save(latest, flush: true, validate: false)
+                    save(latest, flush: false, validate: false)
                 }
 
                 // Now we have a finalised model to work from
-                if (latest) {
-                    setModelIsNewBranchModelVersionOfModel(model, latest, catalogueUser)
-                    model.dateFinalised = null
-                    model.finalised = false
-                    model.modelVersion = null
-                    model.branchName = branchName ?: VersionAwareConstraints.DEFAULT_BRANCH_NAME
-                    model.documentationVersion = Version.from('1')
-                } else {
-                    throw new ApiBadRequestException('MSXX', 'Request to importAsNewBranchModelVersion but no finalised model or main branch available')
-                }
+                setModelIsNewBranchModelVersionOfModel(model, latest, catalogueUser)
+                model.dateFinalised = null
+                model.finalised = false
+                model.modelVersion = null
+                model.branchName = branchName ?: VersionAwareConstraints.DEFAULT_BRANCH_NAME
+                model.documentationVersion = Version.from('1')
             } else log.info('Marked as importAsNewBranchModelVersion but no existing Models with label [{}]', model.label)
         }
     }

From 7794acfa933f1394e55e275f735d42f166e83029 Mon Sep 17 00:00:00 2001
From: Oliver Freeman 
Date: Wed, 28 Jul 2021 17:06:55 +0100
Subject: [PATCH 78/82] mc-9601 Hopefully fix this issue by using the closure
 access through to dataFlow rather than dot-separated property access

---
 .../dataflow/component/DataElementComponent.groovy        | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponent.groovy b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponent.groovy
index 5a74b5ec40..5d2c419729 100644
--- a/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponent.groovy
+++ b/mdm-plugin-dataflow/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/dataflow/component/DataElementComponent.groovy
@@ -137,7 +137,13 @@ class DataElementComponent implements ModelItem
 
     @Deprecated(forRemoval = true)
     static DetachedCriteria byDataFlowId(UUID dataFlowId) {
-        by().eq('dataClassComponent.dataFlow.id', dataFlowId)
+        by().where {
+            dataClassComponent {
+                dataFlow {
+                    eq(id, dataFlowId)
+                }
+            }
+        }
     }
 
     @Deprecated(forRemoval = true)

From 1fb2b2bb724da147aeb8b3d72d3f0cbd8becf96a Mon Sep 17 00:00:00 2001
From: Oliver Freeman 
Date: Thu, 29 Jul 2021 11:49:43 +0100
Subject: [PATCH 79/82] Improve the checking of branch model version when
 importing

We need to be very specific about what we branch off.
---
 .../core/model/ModelService.groovy            | 45 ++++++++++++++-----
 .../datamodel/DataModelFunctionalSpec.groovy  |  5 +--
 2 files changed, 35 insertions(+), 15 deletions(-)

diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy
index 03062e313b..f6129c0c85 100644
--- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy
+++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/model/ModelService.groovy
@@ -843,7 +843,7 @@ abstract class ModelService extends CatalogueItemService imp
 
                 if (!latest.finalised) {
                     finaliseModel(latest, catalogueUser, Version.from('1'), null, null)
-                    save(latest, flush: false, validate: false)
+                    save(latest, flush: true, validate: false)
                 }
 
                 // Now we have a finalised model to work from
@@ -859,19 +859,42 @@ abstract class ModelService extends CatalogueItemService imp
             if (countByAuthorityAndLabel(model.authority, model.label)) {
                 // Branches need to be created from a finalised version
                 // But we can create a new branch even if existing branches
-                // So try for a finalised model, if none then get the latest main branch and finalise that so we have
-                // a finalised version
-                K latest = findLatestFinalisedModelByLabel(model.label)
 
-                if (!latest) {
-                    log.info('No finalised model to create branch from so finalising existing main branch')
+
+                K latest
+
+                // If the branch name is not the default the default branch name then we need a finalised model to branch from
+                if (branchName && branchName != VersionAwareConstraints.DEFAULT_BRANCH_NAME) {
+                    latest = findLatestFinalisedModelByLabel(model.label)
+                    // If no finalised model exists then we finalise the existing default branch so we can branch from it
+                    if (!latest) {
+                        log.info('No finalised model to create branch from so finalising existing main branch')
+                        latest = findCurrentMainBranchByLabel(model.label)
+                        // If there is no default branch or finalised branch then the countBy found the current imported model so we dont need to do anything
+                        if (!latest) {
+                            log.info('Marked as importAsNewBranchModelVersion but no existing Models with label [{}]', model.label)
+                            return
+                        }
+                        finaliseModel(latest, catalogueUser, Version.from('1'), null, null)
+                        save(latest, flush: true, validate: false)
+                    }
+                } else {
+                    // If the branch name is not provided, or is the default then we would be using the default,
+                    // which would cause a unique label failure if theres already an unfinalised model with that branch
+                    // therefore we should make sure we have a clean finalised model to work from
                     latest = findCurrentMainBranchByLabel(model.label)
-                    if (!latest || latest.id == model.id) {
-                        log.info('Marked as importAsNewBranchModelVersion but no existing Models with label [{}]', model.label)
-                        return
+                    if (latest && latest.id != model.id) {
+                        log.info('Main branch exists already so finalising to ensure no conflicts')
+                        finaliseModel(latest, catalogueUser, getNextModelVersion(latest, null, VersionChangeType.MAJOR), null, null)
+                        save(latest, flush: true, validate: false)
+                    } else {
+                        // No main branch exists so get the latest finalised model
+                        latest = findLatestFinalisedModelByLabel(model.label)
+                        if (!latest) {
+                            log.info('Marked as importAsNewBranchModelVersion but no existing Models with label [{}]', model.label)
+                            return
+                        }
                     }
-                    finaliseModel(latest, catalogueUser, Version.from('1'), null, null)
-                    save(latest, flush: false, validate: false)
                 }
 
                 // Now we have a finalised model to work from
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 9aff8bb24d..4cd76d844c 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
@@ -3048,10 +3048,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec {
         ])
 
         then:
-        verifyResponse UNPROCESSABLE_ENTITY, response
-        responseBody().total == 1
-        responseBody().errors[0].message == 'Property [label] of class [class uk.ac.ox.softeng.maurodatamapper.datamodel.DataModel] with value ' +
-        '[Functional Test Model] must be unique by branch name'
+        verifyResponse CREATED, response
 
         when:
         POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [

From 23dc2552a1d7d51472c8cd512462656db39c5cfb Mon Sep 17 00:00:00 2001
From: Oliver Freeman 
Date: Thu, 29 Jul 2021 11:02:00 +0100
Subject: [PATCH 80/82] Updated action names to be a little more readable and
 appropriate

---
 .../policy/GroupBasedUserSecurityPolicyManager.groovy     | 8 ++++----
 .../security/policy/ResourceActions.groovy                | 4 ++--
 .../core/container/VersionedFolderFunctionalSpec.groovy   | 2 +-
 .../functional/core/path/PathFunctionalSpec.groovy        | 2 +-
 ...ssPermissionChangingAndVersioningFunctionalSpec.groovy | 6 +++---
 5 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/mdm-security/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/policy/GroupBasedUserSecurityPolicyManager.groovy b/mdm-security/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/policy/GroupBasedUserSecurityPolicyManager.groovy
index 8c18dbbd34..9c0096eead 100644
--- a/mdm-security/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/policy/GroupBasedUserSecurityPolicyManager.groovy
+++ b/mdm-security/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/policy/GroupBasedUserSecurityPolicyManager.groovy
@@ -53,6 +53,8 @@ import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.D
 import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.DISALLOWED_ONCE_FINALISED_ACTIONS
 import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.EDITOR_ACTIONS
 import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.EDITOR_VERSIONING_ACTIONS
+import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.FINALISED_EDIT_ACTIONS
+import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.FINALISED_READ_ACTIONS
 import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.FINALISE_ACTION
 import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.FULL_DELETE_ACTIONS
 import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.IMPORT_ACTION
@@ -72,8 +74,6 @@ import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.S
 import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.STANDARD_EDIT_ACTIONS
 import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.UPDATE_ACTION
 import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.USER_ADMIN_ACTIONS
-import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.POST_FINALISED_EDITABLE
-import static uk.ac.ox.softeng.maurodatamapper.security.policy.ResourceActions.POST_FINALISED_READABLE
 import static uk.ac.ox.softeng.maurodatamapper.security.policy.TreeActions.CONTAINER_ADMIN_CONTAINER_TREE_ACTIONS
 import static uk.ac.ox.softeng.maurodatamapper.security.policy.TreeActions.CONTAINER_ADMIN_FOLDER_TREE_ACTIONS
 import static uk.ac.ox.softeng.maurodatamapper.security.policy.TreeActions.CONTAINER_ADMIN_MODEL_TREE_ACTIONS
@@ -637,7 +637,7 @@ class GroupBasedUserSecurityPolicyManager implements UserSecurityPolicyManager {
         if (role.canFinalise()) updatedActions << FINALISE_ACTION
         if (role.isFinalised()) {
             updatedActions.removeAll(DISALLOWED_ONCE_FINALISED_ACTIONS)
-            updatedActions.addAll(POST_FINALISED_EDITABLE)
+            updatedActions << FINALISED_EDIT_ACTIONS
         }
         if (role.canVersion()) {
             updatedActions.addAll(EDITOR_VERSIONING_ACTIONS)
@@ -649,7 +649,7 @@ class GroupBasedUserSecurityPolicyManager implements UserSecurityPolicyManager {
         List updatedActions = new ArrayList<>(baseActions)
         if (role.isFinalised()) {
             updatedActions.removeAll(DISALLOWED_ONCE_FINALISED_ACTIONS)
-            updatedActions.addAll(POST_FINALISED_READABLE)
+            updatedActions << FINALISED_READ_ACTIONS
         }
         if (role.canVersion() && isAuthenticated()) {
             updatedActions.addAll(READER_VERSIONING_ACTIONS)
diff --git a/mdm-security/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/policy/ResourceActions.groovy b/mdm-security/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/policy/ResourceActions.groovy
index 6ccbd8ab17..00e0a3daec 100644
--- a/mdm-security/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/policy/ResourceActions.groovy
+++ b/mdm-security/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/security/policy/ResourceActions.groovy
@@ -39,8 +39,8 @@ class ResourceActions {
     public static final String MERGE_INTO_ACTION = 'mergeInto'
     public static final String READ_BY_EVERYONE_ACTION = 'readByEveryone'
     public static final String READ_BY_AUTHENTICATED_ACTION = 'readByAuthenticated'
-    public static final String POST_FINALISED_READABLE = 'postFinalisedReadable'
-    public static final String POST_FINALISED_EDITABLE = 'postFinalisedEditable'
+    public static final String FINALISED_EDIT_ACTIONS = 'finalisedEditActions'
+    public static final String FINALISED_READ_ACTIONS = 'finalisedReadActions'
 
     public static final List READER_VERSIONING_ACTIONS = [CREATE_NEW_VERSIONS_ACTION,
                                                                   NEW_FORK_MODEL_ACTION]
diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy
index 882fffc60f..0da2483887 100644
--- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy
+++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/container/VersionedFolderFunctionalSpec.groovy
@@ -1803,7 +1803,7 @@ class VersionedFolderFunctionalSpec extends UserAccessAndPermissionChangingFunct
             'newForkModel',
             'comment',
             'newModelVersion',
-            'postFinalisedEditable',
+            'finalisedEditActions',
             'newDocumentationVersion',
             'newBranchModelVersion',
             'softDelete',
diff --git a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy
index 5b3fbf0809..7108f1cb52 100644
--- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy
+++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/core/path/PathFunctionalSpec.groovy
@@ -824,7 +824,7 @@ class PathFunctionalSpec extends FunctionalSpec {
             "show",
             "createNewVersions",
             "newForkModel",
-            "postFinalisedReadable",
+            "finalisedReadActions",
             "comment"
           ],
           "lastUpdated": "${json-unit.matches:offsetDateTime}",
diff --git a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelUserAccessPermissionChangingAndVersioningFunctionalSpec.groovy b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelUserAccessPermissionChangingAndVersioningFunctionalSpec.groovy
index 9b5e07e859..1b732d7e9a 100644
--- a/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelUserAccessPermissionChangingAndVersioningFunctionalSpec.groovy
+++ b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelUserAccessPermissionChangingAndVersioningFunctionalSpec.groovy
@@ -117,7 +117,7 @@ abstract class ModelUserAccessPermissionChangingAndVersioningFunctionalSpec exte
             'show',
             'createNewVersions',
             'newForkModel',
-            'postFinalisedReadable'
+            'finalisedReadActions'
         ].sort()
     }
 
@@ -132,7 +132,7 @@ abstract class ModelUserAccessPermissionChangingAndVersioningFunctionalSpec exte
             'newBranchModelVersion',
             'softDelete',
             'delete',
-            'postFinalisedReadable'
+            'finalisedEditActions'
 
         ].sort()
     }
@@ -1582,7 +1582,7 @@ abstract class ModelUserAccessPermissionChangingAndVersioningFunctionalSpec exte
         then:
         verifyResponse(OK, response)
         response.body().readableByEveryone == true
-        response.body().availableActions == ['postFinalisedReadable', 'show']
+        response.body().availableActions == ['finalisedReadActions', 'show']
 
         when: 'removing readable by everyone'
         loginEditor()

From 6322be936a0b196543ce060d89bc2ddba813c7f7 Mon Sep 17 00:00:00 2001
From: Oliver Freeman 
Date: Fri, 30 Jul 2021 11:15:29 +0100
Subject: [PATCH 81/82] gh-124 Make sure hidden import parameters are not
 validated

---
 .../maurodatamapper/core/importer/ImporterService.groovy      | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

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 0ba470fa1d..6dd3868a37 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
@@ -117,7 +117,9 @@ class ImporterService implements DataBinder {
         Errors errors = new ValidationErrors(paramsObj, clazz.getName())
         for (Field field : fields) {
             ImportParameterConfig config = field.getAnnotation(ImportParameterConfig)
-            if (config && !config.optional()) {
+            if (config) {
+                // Dont validate optional or hidden parameters
+                if (config.optional() || config.hidden()) continue
                 Object o = PropertyUtils.getProperty(paramsObj, field.getName())
                 if (!o?.toString()) {
                     errors.rejectValue(field.name, 'default.null.message',

From cdbb2a1a4033f629df03598896e71fa171072fbc Mon Sep 17 00:00:00 2001
From: Oliver Freeman 
Date: Sat, 7 Aug 2021 21:22:40 +0100
Subject: [PATCH 82/82] Release 4.8.0

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index 37111d2433..9387d5ab86 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,5 @@
 # Core Info
-version=4.8.0-SNAPSHOT
+version=4.8.0
 group=uk.ac.ox.softeng.maurodatamapper
 # Gradle
 gradleVersion=6.7.1