diff --git a/gradle.properties b/gradle.properties index 542b80656f..a0eb0f583e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Core Info -version=4.7.0 +version=4.8.0 group=uk.ac.ox.softeng.maurodatamapper # Gradle gradleVersion=6.7.1 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/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-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/path/Path.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy new file mode 100644 index 0000000000..0afee4a13d --- /dev/null +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/Path.groovy @@ -0,0 +1,188 @@ +/* + * 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 uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware + +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType + +/** + * @since 28/08/2020 + */ +class Path { + + //Need to escape the vertical bar which we are using as the split delimiter + static String PATH_DELIMITER = '\\|' + + //Arbitrary maximum number of nodes, to avoid unexpectedly long iteration + static int MAX_NODES = 10 + + List pathNodes + + private Path() { + pathNodes = [] + } + + /* + * 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 + */ + + private Path(String path) { + this() + + if (path) { + String[] splits = path.split(PATH_DELIMITER, MAX_NODES) + int lastIndex = splits.size() - 1 + + splits.eachWithIndex {String node, int i -> + pathNodes << new PathNode(node, i == lastIndex) + } + } + } + + int size() { + pathNodes.size() + } + + PathNode getAt(int i) { + pathNodes[i] + } + + PathNode last() { + pathNodes.last() + } + + PathNode first() { + pathNodes.first() + } + + Path addToPathNodes(PathNode pathNode) { + pathNodes.add(pathNode) + this + } + + Path addToPathNodes(String prefix, String pathIdentifier, boolean isLast) { + addToPathNodes(new PathNode(prefix, pathIdentifier, isLast)) + } + + Path getParent() { + clone().tap { + pathNodes.removeLast() + } + } + + 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, modelIdentifierOverride) + } + + 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(), modelIdentifierOverride) + } + + boolean isEmpty() { + pathNodes.isEmpty() + } + + 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) + } + } + + 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.. + pathNodes << new PathNode(domain.pathPrefix, domain.pathIdentifier, false) + } + } + } + + static Path from(List domains) { + new Path().tap { + domains.eachWithIndex {CreatorAware domain, int i -> + pathNodes << new PathNode(domain.pathPrefix, domain.pathIdentifier, 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/PathJsonConverter.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathJsonConverter.groovy new file mode 100644 index 0000000000..399e09ca56 --- /dev/null +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathJsonConverter.groovy @@ -0,0 +1,35 @@ +/* + * 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 + +/** + * @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/path/PathNode.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy new file mode 100644 index 0000000000..52cf91fb35 --- /dev/null +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNode.groovy @@ -0,0 +1,196 @@ +/* + * 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 uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware +import uk.ac.ox.softeng.maurodatamapper.version.Version + +import groovy.util.logging.Slf4j + +import java.nio.charset.Charset + +/** + * @since 28/08/2020 + */ +@Slf4j +class PathNode { + + 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 prefix + String identifier + String attribute + String modelIdentifier + + PathNode(String prefix, String identifier, boolean isLast) { + this.prefix = prefix + parseIdentifier(identifier, 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 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, boolean isLast) { + node.find(/^(\w+):(.+)$/) {full, foundPrefix, fullIdentifier -> + prefix = foundPrefix + parseIdentifier(fullIdentifier, 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 -> + attribute = attr + parsed = subIdentifier + } + } + parsed.find(/^(.+?)${ESCAPED_MODEL_PATH_IDENTIFIER_SEPARATOR}(.+?)$/) {full, subIdentifier, foundIdentifier -> + parsed = subIdentifier + modelIdentifier = foundIdentifier + } + + identifier = parsed + } + + boolean isPropertyNode() { + attribute + } + + String toString() { + String base = "${prefix}:${getFullIdentifier()}" + if (attribute) base += "${ATTRIBUTE_PATH_IDENTIFIER_SEPARATOR}${attribute}" + base + } + + String getFullIdentifier(String modelIdentifierOverride = null) { + String base = identifier + if (modelIdentifier) base += "${MODEL_PATH_IDENTIFIER_SEPARATOR}${modelIdentifierOverride ?: 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, String modelIdentifierOverride = null) { + matchesPrefix(pathNode.prefix) && matchesIdentifier(pathNode, modelIdentifierOverride) + } + + boolean matches(CreatorAware creatorAware) { + matches(Path.from(creatorAware).last()) + } + + boolean matchesPrefix(String otherPrefix) { + if (prefix != otherPrefix) { + log.trace("Resource prefix [{}] does not match the path node [{}]", otherPrefix, this) + return false + } + true + } + + boolean matchesIdentifier(PathNode otherPathNode, String modelIdentifierOverride = null) { + + 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)) { + return matchesModelPathIdentifierFormat(otherPathNode.identifier, otherPathNode.modelIdentifier) + } + + // Some of the legacy paths included : so we handle submissions of this format + String[] identifierSplit = identifier.split(/:/) + if (identifierSplit[0] == otherPathNode.identifier) return true + + identifierSplit = otherPathNode.identifier.split(/:/) + if (identifier == identifierSplit[0]) return true + log.trace("Resource identifier [{}] does not match the path node [{}]", otherPathNode, this) + 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) + } + + // no other possible match style + false + } + + PathNode clone() { + new PathNode(this.prefix, this.identifier, this.modelIdentifier, this.attribute) + } +} 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/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/traits/domain/CreatorAware.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/traits/domain/CreatorAware.groovy index db6e51c999..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,6 +45,16 @@ trait CreatorAware { abstract String getDomainType() + // 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) { this.createdBy = user?.emailAddress 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 deleted file mode 100644 index a4c97489d1..0000000000 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Path.groovy +++ /dev/null @@ -1,51 +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.util - -/** - * @since 28/08/2020 - */ -class Path { - - //Need to escape the vertical bar which we are using as the split delimiter - static String PATH_DELIMITER = "\\|" - - //Arbitrary maximum number of nodes, to avoid unexpectedly long iteration - static int MAX_NODES = 10 - - List pathNodes - - /* - * 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) { - pathNodes = [] - - if (path) { - String[] splits = path.split(PATH_DELIMITER, MAX_NODES) - - for (String s : splits) { - pathNodes << new PathNode(s) - } - } - } - -} 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 deleted file mode 100644 index 4ecf504019..0000000000 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/PathNode.groovy +++ /dev/null @@ -1,53 +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.util - -/** - * @since 28/08/2020 - */ -class PathNode { - - static String NODE_DELIMITER = ":" - - String typePrefix - String label - - /* - 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) - */ - 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) - } - - //If there are characters after the : then extract these as the label - if (index < node.length() -1) { - label = node.substring(index + 1) - } - - } - -} diff --git a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Utils.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Utils.groovy index 1692141e4f..fde0fcb714 100644 --- a/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Utils.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/Utils.groovy @@ -85,7 +85,8 @@ class Utils { } static boolean parentClassIsAssignableFromChild(Class 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/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/MergeWrapper.groovy b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/UuidJsonConverter.groovy similarity index 62% rename from mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/MergeWrapper.groovy rename to mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/UuidJsonConverter.groovy index fbe7bbd0bf..48b6a36db1 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/MergeWrapper.groovy +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/util/UuidJsonConverter.groovy @@ -15,29 +15,22 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package uk.ac.ox.softeng.maurodatamapper.core.diff +package uk.ac.ox.softeng.maurodatamapper.util -class MergeWrapper extends Mergeable { - T value - MergeWrapper(T value) { - this.value = value - } +import grails.plugin.json.builder.JsonGenerator +/** + * @since 15/07/2021 + */ +class UuidJsonConverter implements JsonGenerator.Converter { @Override - boolean equals(o) { - if (this.is(o)) return true - if (getClass() != o.class) return false - - MergeWrapper diff = (MergeWrapper) o - - if (value != diff.value) return false - - return true + boolean handles(Class type) { + UUID.isAssignableFrom(type) } @Override - String toString() { - value.toString() + Object convert(Object value, String key) { + ((UUID) value).toString() } } 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..fc1a7f3c5a --- /dev/null +++ b/mdm-common/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/version/VersionJsonConverter.groovy @@ -0,0 +1,36 @@ +/* + * 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 + + +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 new file mode 100644 index 0000000000..6fe95fcff8 --- /dev/null +++ b/mdm-common/src/main/resources/META-INF/services/grails.plugin.json.builder.JsonGenerator$Converter @@ -0,0 +1,3 @@ +uk.ac.ox.softeng.maurodatamapper.path.PathJsonConverter +uk.ac.ox.softeng.maurodatamapper.version.VersionJsonConverter +uk.ac.ox.softeng.maurodatamapper.util.UuidJsonConverter \ No newline at end of file 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..e6bb26aaad --- /dev/null +++ b/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathNodeSpec.groovy @@ -0,0 +1,169 @@ +/* + * 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 + +/** + * @since 20/07/2021 + */ +class PathNodeSpec extends Specification { + + void 'test path node creation'() { + given: + PathNode pathNode = new PathNode('dm', 'test model', 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) + + 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) + + 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) + + 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) + + 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) + 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) + + then: 'matches but does not equal' + pathNode1.matches(pathNode2) + pathNode1 != pathNode2 + + when: 'different branch names' + 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) + + 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) + + then: 'no match' + !pathNode1.matches(pathNode2) + pathNode1 != pathNode2 + + when: 'same versionable model identifier' + 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) + + then: 'match and equality' + pathNode1.matches(pathNode2) + pathNode1 == pathNode2 + + when: 'same versionable model identifier' + 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) + + 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) + 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) + + 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..969d155d6f --- /dev/null +++ b/mdm-common/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/path/PathSpec.groovy @@ -0,0 +1,127 @@ +/* + * 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 + +/** + * @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-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/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..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 @@ -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') @@ -102,14 +103,18 @@ 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) { @@ -165,11 +170,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 */ @@ -216,10 +216,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 @@ -227,11 +241,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/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/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..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 @@ -17,10 +17,14 @@ */ 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.artefact.DomainClass import grails.rest.RestfulController import org.springframework.beans.factory.annotation.Autowired @@ -38,12 +42,34 @@ 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, currentUserSecurityPolicyManager) + } + + if (!pathedResource) return notFound(DomainClass, params.path) + + respond(pathedResource, [model: [userSecurityPolicyManager: currentUserSecurityPolicyManager, + pathedResource : pathedResource], + 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..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,13 +18,21 @@ package uk.ac.ox.softeng.maurodatamapper.core.path import uk.ac.ox.softeng.maurodatamapper.core.traits.controller.MdmInterceptor +import uk.ac.ox.softeng.maurodatamapper.path.Path 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' - } + if (actionName == 'listAllPrefixMappings') return true + + mapDomainTypeToClass('securableResource', true) + params.path = Path.from(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/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..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 @@ -65,6 +65,16 @@ class ApiProperty implements CreatorAware { ApiProperty.simpleName } + @Override + String getPathPrefix() { + '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 683a998343..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 @@ -55,6 +55,15 @@ class Authority implements InformationAware, CreatorAware, SecurableResource { Authority.simpleName } + @Override + String getPathPrefix() { + '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 35d467a4b4..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 @@ -94,6 +94,15 @@ class Classifier implements Container { Classifier.simpleName } + @Override + String getPathPrefix() { + 'cl' + } + + @Override + String getPathIdentifier() { + label + } @Override Classifier getPathParent() { @@ -103,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 47fbcfe370..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,35 @@ 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' + } + + @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 8b82ccd312..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 @@ -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 @@ -27,11 +29,12 @@ 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 -class VersionedFolder extends Folder implements VersionAware, VersionLinkAware { +class VersionedFolder extends Folder implements VersionAware, VersionLinkAware, Diffable { Authority authority @@ -44,13 +47,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 } } @@ -69,6 +72,26 @@ 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' + } + + @Override + String getPathIdentifier() { + "${label}${PathNode.MODEL_PATH_IDENTIFIER_SEPARATOR}${modelVersion ?: branchName}" + } + static DetachedCriteria by() { new DetachedCriteria(VersionedFolder) } @@ -118,7 +141,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 0a79f9c032..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 @@ -17,22 +17,22 @@ */ 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.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 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 @@ -76,6 +76,16 @@ class Annotation implements MultiFacetItemAware, PathAware, InformationAware, Cr Annotation.simpleName } + @Override + String getPathPrefix() { + 'ann' + } + + @Override + String getPathIdentifier() { + label + } + @Override Annotation getPathParent() { parentAnnotation @@ -83,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) @@ -112,7 +122,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) @@ -120,11 +130,6 @@ class Annotation implements MultiFacetItemAware, PathAware, InformationAware, Cr } - @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/BreadcrumbTree.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/BreadcrumbTree.groovy index 3da4390cd8..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 @@ -37,7 +37,6 @@ class BreadcrumbTree { String domainType String label Boolean finalised - Breadcrumb breadcrumb Boolean topBreadcrumbTree CatalogueItem domainEntity @@ -115,8 +114,12 @@ 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 (isDirty('treeString')) { + children?.each { + it.buildTree() + it.beforeValidate() + } } } 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..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 @@ -49,6 +49,15 @@ class Edit implements CreatorAware { Edit.simpleName } + @Override + String getPathPrefix() { + 'ed' + } + + @Override + String getPathIdentifier() { + title + } @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 b30d6da55d..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 @@ -17,19 +17,19 @@ */ 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.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 -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 @@ -73,6 +73,10 @@ class Metadata implements MultiFacetItemAware, CreatorAware, Diffable Metadata.simpleName } + @Override + String getPathPrefix() { + 'md' + } @Override String toString() { @@ -90,7 +94,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) @@ -99,7 +103,7 @@ class Metadata implements MultiFacetItemAware, CreatorAware, Diffable } @Override - String getDiffIdentifier() { + String getPathIdentifier() { "${this.namespace}.${this.key}" } 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..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'] @@ -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 6fef14a9e4..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 @@ -17,25 +17,26 @@ */ 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.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 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 String name String description + Set ruleRepresentations static hasMany = [ ruleRepresentations: RuleRepresentation @@ -74,6 +75,15 @@ class Rule implements MultiFacetItemAware, CreatorAware, Diffable { Rule.simpleName } + @Override + String getPathPrefix() { + 'ru' + } + + @Override + String getPathIdentifier() { + name + } @Override String toString() { @@ -87,16 +97,12 @@ 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) .appendString('description', this.description, obj.description) - } - - @Override - String getDiffIdentifier() { - "${this.name}.${this.description}" + .appendList(RuleRepresentation, 'ruleRepresentations', this.ruleRepresentations, obj.ruleRepresentations) } static DetachedCriteria by() { @@ -128,5 +134,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/SemanticLink.groovy b/mdm-core/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/core/facet/SemanticLink.groovy index fee4aba51f..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 @@ -74,6 +72,15 @@ class SemanticLink implements MultiFacetItemAware, CreatorAware { SemanticLink.simpleName } + @Override + String getPathPrefix() { + 'sl' + } + + @Override + String getPathIdentifier() { + "${linkType}.${targetMultiFacetAwareItemId}" + } @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..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 @@ -74,6 +72,15 @@ class VersionLink implements MultiFacetItemAware, CreatorAware { VersionLink.simpleName } + @Override + String getPathPrefix() { + 'vl' + } + + @Override + String getPathIdentifier() { + "${linkType}.${targetModelId}" + } @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 fc33931db4..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 @@ -17,8 +17,9 @@ */ 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.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 +61,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() @@ -71,6 +72,16 @@ class RuleRepresentation implements Diffable, EditHistoryAwa RuleRepresentation.simpleName } + @Override + String getPathPrefix() { + 'rr' + } + + @Override + String getPathIdentifier() { + language + } + @Override String toString() { "${getClass().getName()} : ${language}/${representation} : ${id ?: '(unsaved)'}" @@ -83,16 +94,10 @@ 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) - .appendString('representation', this.representation, obj.representation) - } - - @Override - String getDiffIdentifier() { - "${this.language}.${this.representation}" } static DetachedCriteria by() { 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/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/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..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 @@ -17,7 +17,15 @@ */ 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.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 @@ -25,11 +33,14 @@ 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 @Transactional @Slf4j @@ -40,6 +51,8 @@ class FolderService extends ContainerService { @Autowired(required = false) SecurityPolicyManagerService securityPolicyManagerService + EditService editService + MessageSource messageSource @Override boolean handles(Class clazz) { @@ -89,7 +102,7 @@ class FolderService extends ContainerService { } @Override - Folder findDomainByParentIdAndLabel(UUID parentId, String label) { + Folder findByParentIdAndLabel(UUID parentId, String label) { Folder.byParentFolderIdAndLabel(parentId, label.trim()).get() } @@ -166,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. @@ -218,7 +249,7 @@ class FolderService extends ContainerService { @Deprecated Folder findFolder(Folder parentFolder, String label) { - findDomainByParentIdAndLabel(parentFolder.id, label) + findByParentIdAndLabel(parentFolder.id, label) } @Deprecated @@ -246,9 +277,83 @@ class FolderService extends ContainerService { getFullPathDomains(folder) } - Folder copyBasicFolderInformation(Folder original, Folder copy, User copier) { + List findAllModelsInFolder(Folder folder) { + if (!modelServices) return [] + 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, + 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) + } + + 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) { + throw new ApiNotYetImplementedException('MSXX', 'Folder permission copying') + } + log.warn('Permission copying is not yet implemented') + + } + log.debug('Validating and saving copy') + setFolderRefinesFolder(copiedFolder, original, copier) + + if (copiedFolder.validate()) { + 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 + ) + if (securityPolicyManagerService) { + userSecurityPolicyManager = securityPolicyManagerService.addSecurityForSecurableResource(copiedFolder, userSecurityPolicyManager.user, + copiedFolder.label) + } + } else throw new ApiInvalidModelException('FS01', 'Copied Folder is invalid', copiedFolder.errors, messageSource) + + + // 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 = original.label + copy.label = label copy.description = original.description metadataService.findAllByMultiFacetAwareItemId(original.id).each {copy.addToMetadata(it.namespace, it.key, it.value, copier.emailAddress)} @@ -272,10 +377,45 @@ class FolderService extends ContainerService { copy } - List findAllModelsInFolder(Folder folder) { - if (!modelServices) return [] - modelServices.collectMany {service -> - service.findAllByFolderId(folder.id) - } as List + 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) + } + } + + 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 a24c666434..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 @@ -18,12 +18,21 @@ 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.SemanticLinkType +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.VersionLink import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkService import uk.ac.ox.softeng.maurodatamapper.core.facet.VersionLinkType @@ -31,27 +40,42 @@ 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.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 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 import java.time.OffsetDateTime import java.time.ZoneOffset +import java.util.function.Predicate @Transactional @Slf4j @@ -64,10 +88,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,6 +134,11 @@ 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)} @@ -123,10 +162,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) @@ -301,6 +353,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. @@ -433,107 +490,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) { @@ -560,9 +541,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() @@ -579,7 +557,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) { @@ -613,8 +592,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) @@ -676,4 +657,281 @@ class VersionedFolderService extends ContainerService implement List models = folderService.findAllModelsInFolder(folder) 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 = getDiffForVersionedFolders(commonAncestor, sourceVersionedFolder) + ObjectDiff caDiffTarget = getDiffForVersionedFolders(commonAncestor, 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) + 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)) { + 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, 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, 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 + 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, 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.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}]") + + 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(), getModelIdentifier(targetVersionedFolder)) 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, 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, 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 relativePathToCopyTo, + UserSecurityPolicyManager userSecurityPolicyManager) { + ModelService modelService + Path modelItemToModelAbsolutePath + Path modelRelativeToTargetPath + + 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)} + } + // 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 + // 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, getModelIdentifier(targetVersionedFolder)) as Model + + modelService.processCreationPatchOfModelItem(modelItemToCopy, targetModel, modelItemToModelAbsolutePath.parent, 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, getModelIdentifier(targetVersionedFolder)) 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) + } + + @Override + PersistentEntity getPersistentEntity() { + 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-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..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) } @@ -114,4 +129,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 025379b734..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 @@ -22,12 +22,11 @@ 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 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 @@ -234,19 +236,19 @@ class MetadataService implements MultiFacetItemAwareService { } - void mergeMetadataIntoCatalogueItem(CatalogueItem targetCatalogueItem, MergeObjectDiffData mergeObjectDiffData) { + void mergeLegacyMetadataIntoCatalogueItem(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.getDiffsWithContent().each {fieldPatchData -> + if (fieldPatchData.value) { + targetMetadata.setProperty(fieldPatchData.fieldName, fieldPatchData.value) } else { log.error('Only field diff types can be handled inside MetadataService') } @@ -265,4 +267,19 @@ class MetadataService implements MultiFacetItemAwareService { log.trace('Batch save took {}', Utils.getTimeString(System.currentTimeMillis() - start)) } + + @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', 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/ReferenceFileService.groovy b/mdm-core/grails-app/services/uk/ac/ox/softeng/maurodatamapper/core/facet/ReferenceFileService.groovy index 3581f133f8..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) @@ -93,4 +101,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..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 @@ -142,7 +152,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..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 @@ -54,6 +54,25 @@ 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() + } + + @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 8781e36309..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) @@ -83,6 +94,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/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/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', 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..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 @@ -17,18 +17,20 @@ */ package uk.ac.ox.softeng.maurodatamapper.core.path -import uk.ac.ox.softeng.maurodatamapper.core.model.CatalogueItem +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.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.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.util.Path -import uk.ac.ox.softeng.maurodatamapper.util.PathNode +import uk.ac.ox.softeng.maurodatamapper.traits.domain.CreatorAware +import grails.core.GrailsApplication import grails.gorm.transactions.Transactional import groovy.util.logging.Slf4j -import org.grails.orm.hibernate.proxy.HibernateProxyHandler +import org.grails.core.artefact.DomainClassArtefactHandler import org.springframework.beans.factory.annotation.Autowired @Transactional @@ -38,71 +40,109 @@ class PathService { @Autowired(required = false) List catalogueItemServices - private static HibernateProxyHandler proxyHandler = new HibernateProxyHandler(); - - 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 - } - - } - } + @Autowired(required = false) + List domainServices + + @Autowired(required = false) + List securableResourceServices + + GrailsApplication grailsApplication + + 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() { + 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 + 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, 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 + 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, modelIdentifierOverride) ? path.childPath : path + + // If no nodes in the pathToFind then return the model + if (pathToFind.isEmpty()) return rootResourceOfPath + + // Find the first child in the path + PathNode childNode = pathToFind.first() + + DomainService domainService = domainServices.find {service -> + service.handlesPathPrefix(childNode.prefix) + } + + if (!domainService) { + log.warn("Unknown path prefix [${childNode.prefix}] in path") + return null } - catalogueItem + log.trace('Found service [{}] to handle prefix [{}]', domainService.class.simpleName, childNode.prefix) + def child = domainService.findByParentIdAndPathIdentifier(rootResourceOfPath.id, childNode.getFullIdentifier(modelIdentifierOverride)) + + if (!child) { + 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, modelIdentifierOverride) + } + + 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)} + 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.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.prefix) { + log.warn("Root resource prefix [${rootNode.prefix}] does not match the root class to search") + return null + } + + // 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 + } + + findResourceByPathFromRootResource(rootResource, path) + } } 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/merge/FieldPatchData.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy new file mode 100644 index 0000000000..4cec669f38 --- /dev/null +++ b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/FieldPatchData.groovy @@ -0,0 +1,119 @@ +/* + * 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 + +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.path.Path + +import grails.validation.Validateable + +/** + * @since 07/02/2018 + */ +class FieldPatchData implements Validateable { + + String fieldName + Path path + T sourceValue + T targetValue + T commonAncestorValue + boolean isMergeConflict + String type + + static constraints = { + fieldName nullable: false, blank: false + path nullable: false, blank: false + sourceValue nullable: true + targetValue nullable: true + commonAncestorValue nullable: true + type nullable: false, blank: false, inList: ['creation', 'deletion', 'modification'] + + } + + boolean isMetadataChange() { + false + } + + boolean isCreation() { + type == 'creation' + } + + boolean isDeletion() { + type == 'deletion' + } + + boolean isModification() { + type == 'modification' + } + + String toString() { + String base = "Merge ${type} patch on ${path}" + type == 'modification' ? "${base} :: Changing ${targetValue} to ${sourceValue}" : base + } + + void setPath(String path) { + this.path = Path.from(path) + } + + void setPath(Path path) { + this.path = path + } + + Path getRelativePathToRoot() { + this.path.childPath + } + + 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/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/merge/ObjectPatchData.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/ObjectPatchData.groovy new file mode 100644 index 0000000000..99b15c6a8a --- /dev/null +++ b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/ObjectPatchData.groovy @@ -0,0 +1,83 @@ +/* + * 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 + +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.legacy.LegacyFieldPatchData + +import grails.validation.Validateable + +/** + * @since 07/02/2018 + */ +class ObjectPatchData implements Validateable { + + UUID sourceId + UUID targetId + String label + private List patches + + @Deprecated + List diffs + + static constraints = { + sourceId nullable: false + targetId nullable: false + label nullable: true, blank: false + patches validator: {val, obj -> + if (!val && !obj.diffs) ['default.invalid.min.message', 1] + } + } + + ObjectPatchData() { + patches = [] + diffs = [] + } + + boolean hasPatches() { + patches || getDiffsWithContent() + } + + List getPatches() { + return patches + } + + @Deprecated + List getDiffsWithContent() { + diffs.findAll {it.hasPatches()} + } + + @Deprecated + void setLeftId(UUID leftId) { + this.targetId = leftId + } + + @Deprecated + void setRightId(UUID rightId) { + this.sourceId = rightId + } + + @Deprecated + UUID getLeftId() { + this.targetId + } + + @Deprecated + UUID getRightId() { + this.sourceId + } +} 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/legacy/ItemPatchData.groovy similarity index 78% 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/legacy/ItemPatchData.groovy index 589df8b46a..9e925512b8 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/legacy/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.legacy 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/MergeFieldDiffData.groovy b/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/merge/legacy/LegacyFieldPatchData.groovy similarity index 60% 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/legacy/LegacyFieldPatchData.groovy index 8e01740ca4..154253c7c6 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/legacy/LegacyFieldPatchData.groovy @@ -15,33 +15,42 @@ * * 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.legacy +import uk.ac.ox.softeng.maurodatamapper.core.rest.transport.merge.ObjectPatchData import grails.validation.Validateable /** * @since 07/02/2018 */ -class MergeFieldDiffData implements Validateable { +class LegacyFieldPatchData 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 + } + } + + LegacyFieldPatchData() { 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 +60,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/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/arrayDiff/_arrayDiff.gson b/mdm-core/grails-app/views/arrayDiff/_arrayDiff.gson index 8fe2006284..69f6974be6 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 @@ -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/arrayMergeDiff/_arrayMergeDiff.gson b/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson new file mode 100644 index 0000000000..f89727675d --- /dev/null +++ b/mdm-core/grails-app/views/arrayMergeDiff/_arrayMergeDiff.gson @@ -0,0 +1,13 @@ +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.ArrayMergeDiff + +model { + ArrayMergeDiff arrayMergeDiff +} + +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/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/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/creationMergeDiff/_creationMergeDiff.gson b/mdm-core/grails-app/views/creationMergeDiff/_creationMergeDiff.gson new file mode 100644 index 0000000000..3dbf599c6e --- /dev/null +++ b/mdm-core/grails-app/views/creationMergeDiff/_creationMergeDiff.gson @@ -0,0 +1,12 @@ +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.CreationMergeDiff + +model { + CreationMergeDiff creationMergeDiff +} + +json { + path creationMergeDiff.fullyQualifiedPath + isMergeConflict creationMergeDiff.isMergeConflict() + isSourceModificationAndTargetDeletion creationMergeDiff.isSourceModificationAndTargetDeletion() + type 'creation' +} \ No newline at end of file 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/deletionMergeDiff/_deletionMergeDiff.gson b/mdm-core/grails-app/views/deletionMergeDiff/_deletionMergeDiff.gson new file mode 100644 index 0000000000..c5a72090ea --- /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 { + path deletionMergeDiff.fullyQualifiedPath + isMergeConflict deletionMergeDiff.isMergeConflict() + isSourceDeletionAndTargetModification deletionMergeDiff.isSourceDeletionAndTargetModification() + type 'deletion' +} \ 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/fieldDiff/_fieldDiff.gson b/mdm-core/grails-app/views/fieldDiff/_fieldDiff.gson index 424e2af57e..2e6712598f 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 @@ -7,10 +7,4 @@ model { json("${fieldDiff.fieldName}") { left fieldDiff.getLeft() right fieldDiff.getRight() - - if (fieldDiff.isMergeConflict != null) { - isMergeConflict fieldDiff.isMergeConflict - if (fieldDiff.isMergeConflict) commonAncestorValue fieldDiff.commonAncestorValue - } - } 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..5ebe53cd06 --- /dev/null +++ b/mdm-core/grails-app/views/fieldMergeDiff/_fieldMergeDiff.gson @@ -0,0 +1,15 @@ +import uk.ac.ox.softeng.maurodatamapper.core.diff.tridirectional.FieldMergeDiff + +model { + FieldMergeDiff fieldMergeDiff +} + +json { + fieldName fieldMergeDiff.fieldName + path fieldMergeDiff.fullyQualifiedPath + 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/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/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/grails-app/views/mergeDiff/_mergeDiff.gson b/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson new file mode 100644 index 0000000000..8bbbceac82 --- /dev/null +++ b/mdm-core/grails-app/views/mergeDiff/_mergeDiff.gson @@ -0,0 +1,27 @@ +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.traits.domain.InformationAware + +model { + MergeDiff mergeDiff +} + +json { + + sourceId mergeDiff.getSourceId() + targetId mergeDiff.getTargetId() + + path mergeDiff.fullyQualifiedPath + + 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()) + } + + count mergeDiff.getNumberOfDiffs() + diffs g.render(mergeDiff.getFlattenedDiffs()) +} 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 27f347b4eb..0000000000 --- a/mdm-core/grails-app/views/mergeWrapper/_mergeWrapper.gson +++ /dev/null @@ -1,14 +0,0 @@ -import uk.ac.ox.softeng.maurodatamapper.core.diff.MergeWrapper - -model { - MergeWrapper mergeWrapper - -} - -json { - value tmpl.'/diffable/diffable'(mergeWrapper.value) - if (mergeWrapper.isMergeConflict != null) { - isMergeConflict mergeWrapper.isMergeConflict - if (mergeWrapper.isMergeConflict) commonAncestorValue tmpl.'/diffable/diffable'(mergeWrapper.commonAncestorValue) - } -} diff --git a/mdm-core/grails-app/views/objectDiff/_objectDiff.gson b/mdm-core/grails-app/views/objectDiff/_objectDiff.gson index 943a425fea..89e038a6dc 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 @@ -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()) } diff --git a/mdm-core/grails-app/views/path/show.gson b/mdm-core/grails-app/views/path/show.gson index a893536139..4a20c41a46 100644 --- a/mdm-core/grails-app/views/path/show.gson +++ b/mdm-core/grails-app/views/path/show.gson @@ -1,26 +1,26 @@ -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 grails.plugin.json.view.template.JsonViewTemplate 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'. +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 -String template = "/${Introspector.decapitalize(catalogueItem.domainType)}/showPath" -json g.render(template: template, model: [catalogueItem: catalogueItem, - userSecurityPolicyManager: userSecurityPolicyManager]) \ 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/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/_fullVersionedFolder.gson b/mdm-core/grails-app/views/versionedFolder/_versionedFolder_full.gson similarity index 73% rename from mdm-core/grails-app/views/versionedFolder/_fullVersionedFolder.gson rename to mdm-core/grails-app/views/versionedFolder/_versionedFolder_full.gson index 7c71b08afc..013c040d8d 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 @@ -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/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/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/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 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/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..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 @@ -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 @@ -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,13 +264,15 @@ abstract class ModelController extends CatalogueItemController< def mergeDiff() { - T left = queryForResource params[alternateParamsIdKey] - if (!left) return notFound(params[alternateParamsIdKey]) + T source = queryForResource params[alternateParamsIdKey] + if (!source) return notFound(params[alternateParamsIdKey]) - T right = queryForResource params.otherModelId - if (!right) return notFound(params.otherModelId) + T target = queryForResource params.otherModelId + if (!target) return notFound(params.otherModelId) - respond modelService.getMergeDiffForModels(left, right) + // default to legacy until UI is updated + String view = params.boolean('isLegacy', true) ? 'legacyMergeDiff' : 'mergeDiff' + respond modelService.getMergeDiffForModels(source, target), view: view } @Transactional @@ -280,10 +282,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.') } @@ -293,7 +295,8 @@ 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, 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/diff/ArrayDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/ArrayDiff.groovy deleted file mode 100644 index 1dc6ae289c..0000000000 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/ArrayDiff.groovy +++ /dev/null @@ -1,90 +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 - -class ArrayDiff extends FieldDiff> { - - Collection> created - Collection> deleted - Collection> modified - - private ArrayDiff() { - created = [] - deleted = [] - modified = [] - } - - ArrayDiff created(Collection created) { - this.created = created.collect { new MergeWrapper(it) } - this - } - - ArrayDiff deleted(Collection deleted) { - this.deleted = deleted.collect { new MergeWrapper(it) } - this - } - - ArrayDiff modified(Collection> modified) { - this.modified = modified - this - } - - @Override - ArrayDiff fieldName(String fieldName) { - super.fieldName(fieldName) as ArrayDiff - } - - @Override - ArrayDiff leftHandSide(Collection lhs) { - super.leftHandSide(lhs) as ArrayDiff - } - - @Override - ArrayDiff rightHandSide(Collection rhs) { - super.rightHandSide(rhs) as ArrayDiff - } - - @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() - } - - 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/Diff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diff.groovy index 5a19161711..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 @@ -17,10 +17,21 @@ */ 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 + + Class targetClass + + protected Diff(Class targetClass) { + this.targetClass = targetClass + + } + + abstract Integer getNumberOfDiffs() String getDiffType() { getClass().simpleName @@ -33,25 +44,12 @@ 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 - } - - Diff rightHandSide(T rhs) { - this.right = rhs - this - } - boolean objectsAreIdentical() { !getNumberOfDiffs() } - - 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..aea074c9db --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/DiffBuilder.groovy @@ -0,0 +1,75 @@ +/* + * Copyright 2020 University of Oxford and Health and Social Care Information Centre, also known as NHS Digital + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package uk.ac.ox.softeng.maurodatamapper.core.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 + +import groovy.transform.CompileStatic + +@CompileStatic +class DiffBuilder { + + static ArrayDiff arrayDiff(Class> arrayClass) { + new ArrayDiff(arrayClass) + } + + 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) + } + + 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/Diffable.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Diffable.groovy index 86c43e7a60..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 @@ -17,12 +17,21 @@ */ 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 { + + abstract ObjectDiff diff(T obj) - ObjectDiff diff(T obj) + String getDiffIdentifier() { + getPathIdentifier() + } - String getDiffIdentifier() + abstract String getPathPrefix() } \ 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/bidirectional/ArrayDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ArrayDiff.groovy new file mode 100644 index 0000000000..82d2b74ce0 --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ArrayDiff.groovy @@ -0,0 +1,108 @@ +/* + * 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.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 + +/** + * 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> targetArrayClass) { + super(targetArrayClass) + created = [] + deleted = [] + modified = [] + } + + ArrayDiff createdObjects(Collection created) { + this.created = created.collect { creationDiff(it.class as Class).created(it) } + this + } + + ArrayDiff deletedObjects(Collection deleted) { + this.deleted = deleted.collect { deletionDiff(it.class as Class).deleted(it) } + this + } + + ArrayDiff withModifiedDiffs(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 + } + + @Override + ArrayDiff leftHandSide(Collection lhs) { + super.leftHandSide(lhs) as ArrayDiff + } + + @Override + ArrayDiff rightHandSide(Collection rhs) { + super.rightHandSide(rhs) as ArrayDiff + } + + @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/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..dbcaff0f58 --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/BiDirectionalDiff.groovy @@ -0,0 +1,63 @@ +/* + * 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 + +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 69% 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..aca74ed68b 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,20 @@ * * 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 groovy.transform.CompileStatic + +@CompileStatic +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 +39,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,25 +52,17 @@ 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()}" } - - static FieldDiff builder(Class fieldClass) { - new FieldDiff() - } - - 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 new file mode 100644 index 0000000000..8b2c44950e --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/bidirectional/ObjectDiff.groovy @@ -0,0 +1,219 @@ +/* + * 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 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 + +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 leftId + String rightId + + 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 (leftId != objectDiff.leftId) return false + if (rightId != objectDiff.rightId) 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 + } + + String getLeftIdentifier() { + left.diffIdentifier + } + + String getRightIdentifier() { + right.diffIdentifier + } + + boolean isVersionedDiff() { + Path.from(left.pathPrefix, left.pathIdentifier).first().modelIdentifier + } + + ObjectDiff leftHandSide(String leftId, O lhs) { + super.leftHandSide(lhs) + this.leftId = leftId + this + } + + ObjectDiff rightHandSide(String rightId, O rhs) { + rightHandSide(rhs) + this.rightId = 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 + + List diffableList = [] + + ArrayDiff diff = arrayDiff(diffableList.class) + .fieldName(fieldName) + .leftHandSide(lhs) + .rightHandSide(rhs) as ArrayDiff + + + // If no lhs then all rhs have been created/added + if (!lhs) { + return append(diff.createdObjects(rhs)) + } + + // If no rhs then all lhs have been deleted/removed + if (!rhs) { + return append(diff.deletedObjects(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]} + // 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 -> + 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.createdObjects(created) + .deletedObjects(deleted) + .withModifiedDiffs(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 + } +} + 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..1d87a08d59 --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/ArrayMergeDiff.groovy @@ -0,0 +1,126 @@ +/* + * 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 uk.ac.ox.softeng.maurodatamapper.path.Path + +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 + } + + ArrayMergeDiff insideFullyQualifiedObjectPath(Path fullyQualifiedObjectPath) { + super.insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) 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 + ArrayMergeDiff getValidOnly() { + super.getValidOnly() as ArrayMergeDiff + } + + @Override + boolean hasDiff() { + getNumberOfDiffs() != 0 + } + + @Override + Integer getNumberOfDiffs() { + 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()}") + + if (created) { + stringBuilder.append('\n').append(created.join('\n')) + } + if (deleted) { + stringBuilder.append('\n').append(deleted.join('\n')) + } + if (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/CreationMergeDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy new file mode 100644 index 0000000000..899cad7c42 --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/CreationMergeDiff.groovy @@ -0,0 +1,91 @@ +/* + * 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 uk.ac.ox.softeng.maurodatamapper.path.Path + +import groovy.transform.CompileStatic + +@CompileStatic +class CreationMergeDiff extends TriDirectionalDiff implements Comparable { + + CreationMergeDiff(Class targetClass) { + super(targetClass) + } + + @Override + Integer getNumberOfDiffs() { + 1 + } + + C getCreated() { + super.getValue() as C + } + + String getCreatedIdentifier() { + created.diffIdentifier + } + + Path getFullyQualifiedPath() { + String cleanedIdentifier = createdIdentifier.split('/').last() + Path.from(fullyQualifiedObjectPath, created.pathPrefix, cleanedIdentifier) + } + + boolean isSourceModificationAndTargetDeletion() { + commonAncestor != null + } + + @SuppressWarnings('GrDeprecatedAPIUsage') + CreationMergeDiff whichCreated(C object) { + withSource(object) as CreationMergeDiff + } + + CreationMergeDiff insideFullyQualifiedObjectPath(Path fullyQualifiedObjectPath) { + super.insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) as CreationMergeDiff + } + + CreationMergeDiff withCommonAncestor(C ca) { + super.withCommonAncestor(ca) as CreationMergeDiff + } + + @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}" + } + + @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 new file mode 100644 index 0000000000..b70860bf02 --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/DeletionMergeDiff.groovy @@ -0,0 +1,105 @@ +/* + * 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 uk.ac.ox.softeng.maurodatamapper.core.diff.bidirectional.ObjectDiff +import uk.ac.ox.softeng.maurodatamapper.path.Path + +import groovy.transform.CompileStatic + +@CompileStatic +class DeletionMergeDiff extends TriDirectionalDiff implements Comparable { + + ObjectDiff mergeModificationDiff + + DeletionMergeDiff(Class targetClass) { + super(targetClass) + } + + @Override + Integer getNumberOfDiffs() { + 1 + } + + D getDeleted() { + super.getValue() as D + } + + String getDeletedIdentifier() { + value.diffIdentifier + } + + boolean isSourceDeletionAndTargetModification() { + mergeModificationDiff != null + } + + Path getFullyQualifiedPath() { + String cleanedIdentifier = deletedIdentifier.split('/').last() + Path.from(fullyQualifiedObjectPath, deleted.pathPrefix, cleanedIdentifier) + } + + DeletionMergeDiff whichDeleted(D object) { + this.value = object + withCommonAncestor object + } + + @Override + DeletionMergeDiff insideFullyQualifiedObjectPath(Path fullyQualifiedObjectPath) { + super.insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) as DeletionMergeDiff + } + + DeletionMergeDiff withMergeModification(ObjectDiff modifiedDiff) { + this.mergeModificationDiff = modifiedDiff + this + } + + 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() { + String str = "Deleted :: ${getFullyQualifiedPath()}" + 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 new file mode 100644 index 0000000000..05df6bab01 --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/FieldMergeDiff.groovy @@ -0,0 +1,106 @@ +/* + * 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.path.Path + +import groovy.transform.CompileStatic + +@CompileStatic +class FieldMergeDiff extends TriDirectionalDiff implements Comparable { + + private String fieldName + + FieldMergeDiff(Class targetClass) { + super(targetClass) + } + + FieldMergeDiff forFieldName(String fieldName) { + this.fieldName = fieldName + this + } + + FieldMergeDiff insideFullyQualifiedObjectPath(Path fullyQualifiedObjectPath) { + super.insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) as FieldMergeDiff + } + + @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 + } + + FieldMergeDiff getValidOnly() { + hasDiff() ? this : null + } + + @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 + } + + Path getFullyQualifiedPath() { + Path.forAttributeOnPath(fullyQualifiedObjectPath, fieldName) + } + + boolean hasDiff() { + source != target + } + + String getFieldName() { + fieldName + } + + @Override + String toString() { + "${getFullyQualifiedPath()} :: ${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 new file mode 100644 index 0000000000..b7e6e65e6a --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/MergeDiff.groovy @@ -0,0 +1,578 @@ +/* + * 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 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 groovy.transform.CompileStatic +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.creationMergeDiff +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 + +/** + * 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 +@CompileStatic +class MergeDiff extends TriDirectionalDiff implements Comparable { + + private List diffs + + 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 + } + + String getSourceIdentifier() { + source.diffIdentifier + } + + String getTargetIdentifier() { + target.diffIdentifier + } + + String getSourceId() { + (source as CreatorAware).id + } + + String getTargetId() { + (target as CreatorAware).id + } + + Path getFullyQualifiedPath() { + String cleanedIdentifier = sourceIdentifier.split('/').last() + Path.from(fullyQualifiedObjectPath, source.pathPrefix, cleanedIdentifier) + } + + FieldMergeDiff first() { + diffs.first() + } + + int size() { + diffs.size() + } + + boolean isEmpty() { + diffs.isEmpty() + } + + List getDiffs() { + 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}" + if (commonAncestor) str = "${str} [${commonAncestor.diffIdentifier}]" + str + } + + MergeDiff forMergingDiffable(M sourceSide) { + super.withSource(sourceSide) as MergeDiff + } + + @Override + MergeDiff insideFullyQualifiedObjectPath(Path fullyQualifiedObjectPath) { + super.insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) 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 withSourceDiffedAgainstTarget(ObjectDiff sourceDiffTarget) { + this.sourceDiffTarget = sourceDiffTarget + this + } + + MergeDiff append(FieldMergeDiff 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 + } + + @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. + * + */ + @SuppressWarnings('GroovyVariableNotAssigned') + MergeDiff generate() { + + // 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(fullyQualifiedPath, + caSourceFieldDiff as ArrayDiff, + caTargetFieldDiff as ArrayDiff) + break + case FieldDiff.simpleName: + append createFieldMergeDiffPresentOnBothSides(fullyQualifiedPath, 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 { + log.debug('[{}] No change present between common ancestor and target', caSourceFieldDiff.fieldName) + switch (caSourceFieldDiff.diffType) { + case ArrayDiff.simpleName: + append createArrayMergeDiffPresentOnOneSide(fullyQualifiedPath, caSourceFieldDiff as ArrayDiff) + break + case FieldDiff.simpleName: + append createFieldMergeDiffPresentOnOneSide(fullyQualifiedPath, caSourceFieldDiff) + break + default: + log.warn('Unhandled diff type {} on both sides', caSourceFieldDiff.diffType) + } + } + } + this + } + + 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(fullyQualifiedPath, caSourceFieldDiff as ArrayDiff) + break + case FieldDiff.simpleName: + append createFieldMergeDiffPresentOnOneSide(fullyQualifiedPath, 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(fullyQualifiedPath, sourceTargetFieldDiff as ArrayDiff) + break + case FieldDiff.simpleName: + append createFieldMergeDiffFromSourceTargetFieldDiff(fullyQualifiedPath, sourceTargetFieldDiff) + break + default: + log.warn('Unhandled diff type {} on both sides', sourceTargetFieldDiff.diffType) + } + } + this + } + + 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) + .forFieldName(sourceTargetArrayDiff.fieldName) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) + .withSource(sourceTargetArrayDiff.left) + .withTarget(sourceTargetArrayDiff.right) + .withCommonAncestor(null) + .withCreatedMergeDiffs(sourceTargetArrayDiff.created.collect {c -> + creationMergeDiff(c.targetClass) + .whichCreated(c.created) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) + }) + .withDeletedMergeDiffs(sourceTargetArrayDiff.deleted.collect {d -> + deletionMergeDiff(d.targetClass) + .whichDeleted(d.deleted) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) + }) + .withModifiedMergeDiffs(sourceTargetArrayDiff.modified.collect {m -> + mergeDiff(m.targetClass) + .forMergingDiffable(m.left) + .intoDiffable(m.right) + .havingCommonAncestor(null) + .withSourceDiffedAgainstTarget(m) + .generate() + }) + + } + + 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 + 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(Path fullyQualifiedObjectPath, + ArrayDiff caSourceArrayDiff, + ArrayDiff caTargetArrayDiff) { + log.debug('[{}] Processing array differences against common ancestor on both sides', caSourceArrayDiff.fieldName) + createBaseArrayMergeDiffPresentOnOneSide(fullyQualifiedObjectPath, caSourceArrayDiff) + .withTarget(caTargetArrayDiff.right) + .withCreatedMergeDiffs(createCreationMergeDiffsPresentOnBothSides(fullyQualifiedObjectPath, caSourceArrayDiff, caTargetArrayDiff)) + .withDeletedMergeDiffs(createDeletionMergeDiffsPresentOnBothSides(fullyQualifiedObjectPath, caSourceArrayDiff.deleted, caTargetArrayDiff)) + .withModifiedMergeDiffs(createModifiedMergeDiffsPresentOnBothSides(fullyQualifiedObjectPath, caSourceArrayDiff, caTargetArrayDiff)) + .getValidOnly() + } + + + 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 + createBaseArrayMergeDiffPresentOnOneSide(fullyQualifiedObjectPath, caSourceArrayDiff) + .withCreatedMergeDiffs(createCreationMergeDiffsPresentOnOneSide(fullyQualifiedObjectPath, caSourceArrayDiff.created)) + .withDeletedMergeDiffs(createDeletionMergeDiffsPresentOnOneSide(fullyQualifiedObjectPath, caSourceArrayDiff.deleted)) + .withModifiedMergeDiffs(createModifiedMergeDiffsPresentOnOneSide(fullyQualifiedObjectPath, caSourceArrayDiff.modified)) + .getValidOnly() + } + + static FieldMergeDiff createFieldMergeDiffFromSourceTargetFieldDiff(Path 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(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) + .asMergeConflict() + .getValidOnly() // This is a safety check to handle when 2 diffs are used with modifications but no actual difference + } + + 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 + .withTarget(caSourceFieldDiff.left) + .getValidOnly() + } + + static FieldMergeDiff createBaseFieldMergeDiffPresentOnOneSide(Path 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(Path fullyQualifiedObjectPath, + Collection> caSourceCreatedDiffs) { + caSourceCreatedDiffs.collect {created -> + creationMergeDiff(created.targetClass) + .whichCreated(created.created) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) + } + } + + /** + * 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 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.', caSourceModificationDiff.leftIdentifier) + return creationMergeDiff(caSourceModificationDiff.targetClass) + .whichCreated(caSourceModificationDiff.right) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) + .withCommonAncestor(caSourceModificationDiff.left) + .asMergeConflict() + } + null + }.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) + 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 creationMergeDiff(diff.targetClass) + .whichCreated(diff.value) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) + }.findAll() + + creationMergeDiffs.addAll(modificationCreationMergeDiffs) + creationMergeDiffs + } + + + static Collection> createDeletionMergeDiffsPresentOnOneSide(Path fullyQualifiedObjectPath, + Collection> caSourceDeletionDiffs) { + caSourceDeletionDiffs.collect {deleted -> + deletionMergeDiff(deleted.targetClass) + .whichDeleted(deleted.deleted) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) + } + } + + /** + * 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 createDeletionMergeDiffsPresentOnBothSides(Path fullyQualifiedObjectPath, + 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.trace('[{}] ca/source deleted exists in ca/target deleted', diff.deletedIdentifier) + return null + } + 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 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) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) + .withNoMergeModification() + }.findAll() + } + + 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 + 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) + .generate() + } + } + + static Collection> createModifiedMergeDiffsPresentOnBothSides(Path fullyQualifiedObjectPath, + ArrayDiff caSourceDiff, + ArrayDiff caTargetDiff) { + + 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) + .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. TREATED AS CREATION', caSourceModifiedDiff.leftIdentifier) + return null + } + + 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) + .generate() + }.findAll() + + + 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 + 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) + .insideFullyQualifiedObjectPath(fullyQualifiedObjectPath) + .intoDiffable(caTargetCreationDiff.created) + .havingCommonAncestor(null) + .withSourceDiffedAgainstTarget(sourceTargetDiff) + .generate() + } + null + }.findAll() + 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 new file mode 100644 index 0000000000..988580c567 --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/tridirectional/TriDirectionalDiff.groovy @@ -0,0 +1,140 @@ +/* + * 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.bidirectional.BiDirectionalDiff +import uk.ac.ox.softeng.maurodatamapper.path.Path + +import groovy.transform.CompileStatic + +@CompileStatic +abstract class TriDirectionalDiff extends BiDirectionalDiff { + + protected Path fullyQualifiedObjectPath + private Boolean mergeConflict + private T commonAncestor + + protected TriDirectionalDiff(Class targetClass) { + super(targetClass) + mergeConflict = false + } + + abstract Path getFullyQualifiedPath() + + @Override + boolean equals(o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + + TriDirectionalDiff diff = (TriDirectionalDiff) o + + if (source != diff.source) return false + if (target != diff.target) return false + + return true + } + + TriDirectionalDiff insideFullyQualifiedObjectPath(Path fullyQualifiedObjectPath) { + this.fullyQualifiedObjectPath = fullyQualifiedObjectPath + this + } + + @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 getCommonAncestor() { + commonAncestor + } + + T getTarget() { + super.getRight() + } + + T getSource() { + super.getLeft() + } + + Path getFullyQualifiedObjectPath() { + fullyQualifiedObjectPath + } + + @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/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/MergeObjectDiffData.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/CreationDiff.groovy similarity index 59% rename from mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/MergeObjectDiffData.groovy rename to mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/CreationDiff.groovy index 2533f55a60..afa167c299 100644 --- a/mdm-core/grails-app/utils/uk/ac/ox/softeng/maurodatamapper/core/rest/transport/model/MergeObjectDiffData.groovy +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/CreationDiff.groovy @@ -15,30 +15,29 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package uk.ac.ox.softeng.maurodatamapper.core.rest.transport.model +package uk.ac.ox.softeng.maurodatamapper.core.diff.unidirectional +import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable -import grails.validation.Validateable +import groovy.transform.CompileStatic -/** - * @since 07/02/2018 - */ -class MergeObjectDiffData implements Validateable { +@CompileStatic +class CreationDiff extends UniDirectionalDiff { - UUID leftId - UUID rightId - String label - List diffs + CreationDiff(Class targetClass) { + super(targetClass) + } - MergeObjectDiffData() { - diffs = [] + C getCreated() { + super.getValue() as C } - boolean hasDiffs() { - diffs.any {it.hasDiffs()} + String getCreatedIdentifier() { + value.diffIdentifier } - List getValidDiffs() { - diffs.findAll {it.hasDiffs()} + 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 new file mode 100644 index 0000000000..a948998695 --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/DeletionDiff.groovy @@ -0,0 +1,43 @@ +/* + * Copyright 2020 University of Oxford and Health and Social Care Information Centre, also known as NHS Digital + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package uk.ac.ox.softeng.maurodatamapper.core.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) + } + + D getDeleted() { + super.getValue() as D + } + + String getDeletedIdentifier() { + value.diffIdentifier + } + + DeletionDiff deleted(D object) { + this.value = object + this + } +} diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/UniDirectionalDiff.groovy b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/UniDirectionalDiff.groovy new file mode 100644 index 0000000000..38e8e4dfc1 --- /dev/null +++ b/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/unidirectional/UniDirectionalDiff.groovy @@ -0,0 +1,45 @@ +/* + * 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.Diff +import uk.ac.ox.softeng.maurodatamapper.core.diff.Diffable + +import groovy.transform.CompileStatic + +@CompileStatic +abstract class UniDirectionalDiff extends Diff { + + protected UniDirectionalDiff(Class targetClass) { + super(targetClass) + } + + 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/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/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') { 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..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 @@ -17,12 +17,13 @@ */ 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.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 +import uk.ac.ox.softeng.maurodatamapper.core.facet.Rule 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 @@ -82,7 +83,7 @@ trait CatalogueItem implements InformationAware, EditHistory } @Override - String getDiffIdentifier() { + String getPathIdentifier() { label } @@ -93,14 +94,15 @@ 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) .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) } static DetachedCriteria withCatalogueItemFilter(DetachedCriteria criteria, Map filters) { 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..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 @@ -17,7 +17,6 @@ */ 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 @@ -28,17 +27,14 @@ 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.legacy.LegacyFieldPatchData 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 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 @@ -64,18 +60,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 } @@ -83,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() ?: args.insert 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 } @@ -142,13 +133,22 @@ 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 -> + rule.ruleRepresentations.each {ruleRepresentation -> copiedRule.addToRuleRepresentations(language: ruleRepresentation.language, representation: ruleRepresentation.representation, createdBy: copier.emailAddress) @@ -156,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, @@ -170,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) { @@ -190,38 +192,35 @@ abstract class CatalogueItemService implements DomainSe */ K findByParentAndLabel(CatalogueItem parentCatalogueItem, String label) { + findByParentIdAndLabel(parentCatalogueItem.id, label) + } + + K findByParentIdAndLabel(UUID parentId, String label) { null } - void mergeMetadataIntoCatalogueItem(MergeFieldDiffData mergeFieldDiff, K targetCatalogueItem, - UserSecurityPolicyManager userSecurityPolicyManager) { + @Override + K findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + findByParentIdAndLabel(parentId, pathIdentifier) + } + + void mergeLegacyMetadataIntoCatalogueItem(LegacyFieldPatchData 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) - metadataService.copy(targetCatalogueItem, metadata, userSecurityPolicyManager) + fieldPatchData.created.each {createdItemPatchData -> + Metadata metadata = metadataService.get(createdItemPatchData.id) + metadataService.copy(metadata, targetCatalogueItem) } // for modifications, recursively call this method - mergeFieldDiff.modified.each { mergeObjectDiffData -> - metadataService.mergeMetadataIntoCatalogueItem(targetCatalogueItem, mergeObjectDiffData) - } - } - - 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') + fieldPatchData.modified.each { modifiedObjectPatchData -> + metadataService.mergeLegacyMetadataIntoCatalogueItem(targetCatalogueItem, modifiedObjectPatchData) } - return copy } } \ No newline at end of file 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 dc4f7ec28a..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 @@ -20,9 +20,10 @@ 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.path.PathNode import uk.ac.ox.softeng.maurodatamapper.security.SecurableResource import grails.gorm.DetachedCriteria @@ -56,6 +57,11 @@ trait Model extends CatalogueItem implements SecurableRes static mapping = { } + @Override + String getPathIdentifier() { + "${label}${PathNode.MODEL_PATH_IDENTIFIER_SEPARATOR}${modelVersion ?: branchName}" + } + @Override int compareTo(D that) { int res = 0 @@ -65,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 } @@ -136,7 +144,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 +159,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/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..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 @@ -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.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.ObjectPatchData +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 @@ -47,30 +46,31 @@ 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 mergeObjectDiffIntoModelItem(MergeObjectDiffData mergeObjectDiff, 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 -> + Model mergeLegacyObjectPatchDataIntoModelItem(ObjectPatchData objectPatchData, K targetModelItem, Model targetModel, + UserSecurityPolicyManager userSecurityPolicyManager) { + if (!objectPatchData.hasPatches()) return targetModel + 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 @@ -78,12 +78,12 @@ 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 } if (modelItemService) { - modelItemService.processMergeFieldDiff(mergeFieldDiff, targetModel, userSecurityPolicyManager, parentId) + modelItemService.processLegacyFieldPatchData(mergeFieldDiff, targetModel, userSecurityPolicyManager, parentId) } else { log.error('Unknown ModelItem field to merge [{}]', mergeFieldDiff.fieldName) @@ -95,20 +95,20 @@ abstract class ModelItemService extends CatalogueItemServic targetModel } - void processMergeFieldDiff(MergeFieldDiffData mergeFieldDiff, 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 - 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) + copyModelItem = copy(targetModel, modelItem, parentId, userSecurityPolicyManager) } else { copyModelItem = copy(targetModel, modelItem, userSecurityPolicyManager) } @@ -116,9 +116,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 + mergeLegacyObjectPatchDataIntoModelItem(modifiedObjectPatchData, modelItem, targetModel, userSecurityPolicyManager) } } @@ -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() 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..857d44e83a 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,35 +18,53 @@ 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 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.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.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.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.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.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 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 import org.grails.datastore.gorm.GormValidateable import org.grails.orm.hibernate.proxy.HibernateProxyHandler @@ -55,6 +73,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 { @@ -67,6 +86,12 @@ abstract class ModelService extends CatalogueItemService imp @Autowired(required = false) Set modelItemServices + @Autowired(required = false) + Set domainServices + + @Autowired(required = false) + Set multiFacetItemAwareServices + @Autowired VersionLinkService versionLinkService @@ -76,6 +101,9 @@ abstract class ModelService extends CatalogueItemService imp @Autowired EditService editService + @Autowired + PathService pathService + @Autowired MessageSource messageSource @@ -152,6 +180,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() @@ -197,6 +230,34 @@ abstract class ModelService extends CatalogueItemService imp findAllReadableModels(constrainedIds, includeDeleted) } + @Override + K findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + String[] split = pathIdentifier.split(PathNode.ESCAPED_MODEL_PATH_IDENTIFIER_SEPARATOR) + String label = split[0] + + // 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) + + // 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 + } + + // 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, String versionTag) { log.debug('Finalising model') @@ -383,27 +444,169 @@ 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) { - //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 -> + K mergeObjectPatchDataIntoModel(ObjectPatchData objectPatchData, K targetModel, K sourceModel, boolean isLegacy, + UserSecurityPolicyManager userSecurityPolicyManager) { + + + 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) + 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)) { + processCreationPatchOfModelItem(domainToCopy as ModelItem, targetModel, creationPatch.relativePathToRoot.parent, userSecurityPolicyManager) + } + if (Utils.parentClassIsAssignableFromChild(MultiFacetItemAware, domainToCopy.class)) { + processCreationPatchOfFacet(domainToCopy as MultiFacetItemAware, targetModel, creationPatch.relativePathToRoot.parent) + } + } + + 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 + if (Utils.parentClassIsAssignableFromChild(ModelItem, domain.class)) { + processDeletionPatchOfModelItem(domain as ModelItem) + } + if (Utils.parentClassIsAssignableFromChild(MultiFacetItemAware, domain.class)) { + processDeletionPatchOfFacet(domain as MultiFacetItemAware, targetModel, deletionPatch.relativePathToRoot) + } + } + + 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 + 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) { + 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) { + 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 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)} if (modelItemService) { - modelItemService.processMergeFieldDiff(mergeFieldDiff, targetModel, userSecurityPolicyManager) + modelItemService.processLegacyFieldPatchData(mergeFieldDiff, targetModel, userSecurityPolicyManager) } else { log.error('Unknown ModelItem field to merge [{}]', mergeFieldDiff.fieldName) } @@ -441,8 +644,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) @@ -511,14 +715,28 @@ 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 caDiffSource = commonAncestor.diff(sourceModel) + ObjectDiff caDiffTarget = commonAncestor.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 - ObjectDiff left = commonAncestor.diff(leftModel) - ObjectDiff right = commonAncestor.diff(rightModel) - ObjectDiff top = rightModel.diff(leftModel) + caDiffSource.diffs.removeIf(branchNamePredicate) + caDiffTarget.diffs.removeIf(branchNamePredicate) - top.mergeDiff(left, right) + DiffBuilder + .mergeDiff(sourceModel.class as Class) + .forMergingDiffable(sourceModel) + .intoDiffable(targetModel) + .havingCommonAncestor(commonAncestor) + .withCommonAncestorDiffedAgainstSource(caDiffSource) + .withCommonAncestorDiffedAgainstTarget(caDiffTarget) + .generate() } @Override @@ -617,6 +835,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 } } @@ -630,15 +852,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 } - Version latestVersion = existingModels.max {it.documentationVersion}.documentationVersion - model.documentationVersion = Version.nextMajorVersion(latestVersion) + if (!latest.finalised) { + finaliseModel(latest, catalogueUser, Version.from('1'), null, null) + save(latest, flush: true, validate: false) + } + + // 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) } } @@ -647,26 +877,53 @@ abstract class ModelService extends CatalogueItemService imp if (importAsNewBranchModelVersion) { if (countByAuthorityAndLabel(model.authority, model.label)) { - K latest = findLatestFinalisedModelByLabel(model.label) - - if (!latest) { - log.info('No finalised model to create branch from so finalising existing main branch') + // Branches need to be created from a finalised version + // But we can create a new branch even if existing branches + + + 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) - finaliseModel(latest, catalogueUser, Version.from('1'), null, null) - save(latest, flush: true, validate: false) + 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 + } + } } // 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) } } @@ -698,4 +955,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 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/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-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-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/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/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 { 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/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..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 @@ -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,31 @@ trait DomainService { K unwrapIfProxy(def ge) { HIBERNATE_PROXY_HANDLER.unwrapIfProxy(ge) as K } + + Class getDomainClass() { + ParameterizedType parameterizedType = this.getClass().getGenericInterfaces().find {it instanceof ParameterizedType} + if (!parameterizedType) { + parameterizedType = this.getClass().getGenericSuperclass() + } + (Class) parameterizedType.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..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 @@ -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,21 @@ 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) { + abstract M copy(M facetToCopy, MultiFacetAware multiFacetAwareItemToCopyInto) + + 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 +66,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-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/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-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/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/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-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..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 @@ -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 @@ -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 6c3f6dee00..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 @@ -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 @@ -76,6 +76,10 @@ class BasicModelItem implements ModelItem, GormEntit BasicModelItem.simpleName } + @Override + String getPathPrefix() { + 'bmi' + } @Override String getEditLabel() { 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/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..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 @@ -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 @@ -90,6 +90,11 @@ class DataFlow implements ModelItem { DataFlow.simpleName } + @Override + String getPathPrefix() { + 'df' + } + @Override GormEntity getPathParent() { target @@ -121,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 e6b8dc606a..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 @@ -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 @@ -88,6 +88,11 @@ class DataClassComponent implements ModelItem { DataClassComponent.simpleName } + @Override + String getPathPrefix() { + 'dcc' + } + @Override GormEntity getPathParent() { dataFlow @@ -119,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 d47028e755..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 @@ -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 @@ -87,6 +87,11 @@ class DataElementComponent implements ModelItem DataElementComponent.simpleName } + @Override + String getPathPrefix() { + 'dec' + } + @Override GormEntity getPathParent() { dataClassComponent @@ -117,11 +122,6 @@ class DataElementComponent implements ModelItem dataClassComponent.model } - @Override - String getDiffIdentifier() { - this.label - } - @Override Boolean hasChildren() { false @@ -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) 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..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 @@ -19,16 +19,16 @@ 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 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.security.basic.PublicAccessSecurityPolicyManager 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..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.security.basic.PublicAccessSecurityPolicyManager 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..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.security.basic.PublicAccessSecurityPolicyManager 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-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-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..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 @@ -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 extends BaseImportExportSpec { @@ -138,14 +133,14 @@ abstract class DataBindImportAndDefaultExporterServiceSpec { @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) } 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-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 61c3e513e8..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 @@ -19,7 +19,7 @@ package uk.ac.ox.softeng.maurodatamapper.datamodel 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 @@ -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) } /** @@ -163,6 +163,11 @@ class DataModel implements Model, SummaryMetadataAware, IndexedSiblin DataModel.simpleName } + @Override + String getPathPrefix() { + 'dm' + } + void setType(DataModelType type) { modelType = type.label } @@ -189,7 +194,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() { 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..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 @@ -64,6 +64,16 @@ class SummaryMetadata implements MultiFacetItemAware, InformationAware, CreatorA SummaryMetadata.simpleName } + @Override + String getPathPrefix() { + '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 979bbe9381..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 @@ -52,7 +56,17 @@ class SummaryMetadataReport implements CreatorAware { @Override String getDomainType() { - SummaryMetadata.simpleName + SummaryMetadataReport.simpleName + } + + @Override + String getPathPrefix() { + 'smr' + } + + @Override + String getPathIdentifier() { + reportDate.withOffsetSameInstant(ZoneOffset.UTC).format(PATH_FORMATTER) } 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 50e342860f..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 @@ -18,7 +18,7 @@ package uk.ac.ox.softeng.maurodatamapper.datamodel.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 @@ -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() { @@ -229,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() { @@ -294,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/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 22cf1fc788..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 @@ -18,7 +18,7 @@ package uk.ac.ox.softeng.maurodatamapper.datamodel.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 @@ -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() { @@ -177,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 bc440c3eeb..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 @@ -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 @@ -149,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/EnumerationType.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/EnumerationType.groovy index e55b8910f0..c0c2bb6fee 100644 --- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/EnumerationType.groovy +++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/EnumerationType.groovy @@ -17,7 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.datamodel.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.datamodel.item.datatype.enumeration.EnumerationValue diff --git a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ModelDataType.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ModelDataType.groovy index fc29fa3e1b..714d8299d9 100644 --- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ModelDataType.groovy +++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ModelDataType.groovy @@ -17,7 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.datamodel.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.datamodel.DataModel import grails.gorm.DetachedCriteria diff --git a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/PrimitiveType.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/PrimitiveType.groovy index 8fb7c21dce..9cf65554b0 100644 --- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/PrimitiveType.groovy +++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/PrimitiveType.groovy @@ -17,7 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.datamodel.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-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ReferenceType.groovy b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ReferenceType.groovy index 01ac6ac90a..04deabba5c 100644 --- a/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ReferenceType.groovy +++ b/mdm-plugin-datamodel/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/datamodel/item/datatype/ReferenceType.groovy @@ -17,7 +17,7 @@ */ package uk.ac.ox.softeng.maurodatamapper.datamodel.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.datamodel.item.DataClass import grails.gorm.DetachedCriteria 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 c7aa1d9167..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 @@ -18,7 +18,7 @@ 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.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 @@ -91,6 +91,10 @@ class EnumerationValue implements ModelItem { EnumerationValue.simpleName } + @Override + String getPathPrefix() { + 'ev' + } @Override GormEntity getPathParent() { @@ -125,7 +129,7 @@ class EnumerationValue implements ModelItem { } @Override - String getDiffIdentifier() { + String getPathIdentifier() { this.key } 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..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 @@ -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 @@ -47,11 +51,12 @@ 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.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 @@ -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/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..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() @@ -88,4 +99,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..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 @@ -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 @@ -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 } } @@ -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 } + ReferenceType referenceType = copiedDataModel.referenceTypes.find {it.label == refType.label} if (!referenceType) { referenceType = new ReferenceType(createdBy: copier.emailAddress, label: refType.label) copiedDataModel.addToDataTypes(referenceType) @@ -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 @@ -782,7 +795,7 @@ class DataClassService extends ModelItemService implements SummaryMet 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 +828,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 = findByDataModelIdAndLabel(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..732feb4b92 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) { @@ -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 } } @@ -322,14 +326,37 @@ class DataElementService extends ModelItemService implements Summar dataElement } - //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} - if (dataClass) { - dataClass.addToDataElements(copy) + 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.childPath.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) + if (parentDataClass) { + (parentDataClass as DataClass).addToDataElements(copy) + } copy } @@ -358,7 +385,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 { @@ -480,8 +507,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..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 @@ -92,7 +92,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 +204,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 +284,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 +309,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 +329,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) @@ -348,7 +348,8 @@ class DataTypeService extends ModelItemService implements DefaultDataT } } - 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) } @@ -364,7 +365,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 +393,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 +445,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 +468,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/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 010d67836d..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 { @@ -67,6 +68,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 { @@ -83,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() @@ -163,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-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..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 @@ -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 @@ -36,10 +40,11 @@ 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 +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 targetModelId = createAndSaveNewBranchModel(VersionAwareConstraints.DEFAULT_BRANCH_NAME, commonAncestor, creator, dataModelService, + messageSource, policyManager) + + 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(targetModelId, 'modifyTargetOnly').tap { + description = 'Description' + } + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(targetModelId, 'deleteSourceAndModifyTarget').tap { + description = 'Description' + } + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(targetModelId, 'modifyBothReturningNoDifference').tap { + description = 'Description' + } + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(targetModelId, 'modifyBothReturningDifference').tap { + description = 'DescriptionTarget' + } + + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(targetModelId, 'existingClass') + .addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'addTargetToExistingClass')) + + 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)) + 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(targetModelId).tap { + description = 'DescriptionTarget' + } + + sessionFactory.currentSession.flush() + sessionFactory.currentSession.clear() + + + // Generate test/source branch + UUID sourceModelId = createAndSaveNewBranchModel('test', commonAncestor, creator, dataModelService, messageSource, policyManager) + + 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(sourceModelId, 'modifySourceOnly').tap { + description = 'Description' + } + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(sourceModelId, 'modifySourceAndDeleteTarget').tap { + description = 'Description' + } + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(sourceModelId, 'modifyBothReturningNoDifference').tap { + description = 'Description' + } + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(sourceModelId, 'modifyBothReturningDifference').tap { + description = 'DescriptionSource' + } + + checkAndSave messageSource, dataClassService.findByDataModelIdAndLabel(sourceModelId, 'existingClass') + .addToDataClasses(new DataClass(createdBy: creator.emailAddress, label: 'addSourceToExistingClass')) + + DataModel testModel = dataModelService.get(sourceModelId) + 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(sourceModelId).find {it.key == 'modifySourceOnly'}.tap { + value = 'altered' + } + + sessionFactory.currentSession.flush() + sessionFactory.currentSession.clear() + + [commonAncestorId: commonAncestor.id, + sourceId : sourceModelId, + targetId : targetModelId] + } + + 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/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-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/_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/_fullDataModel.gson b/mdm-plugin-datamodel/grails-app/views/dataModel/_dataModel_full.gson similarity index 75% 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..895984a297 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 @@ -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/_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/diff.gson b/mdm-plugin-datamodel/grails-app/views/dataModel/diff.gson index 102ebf0a74..f6783a4d85 100644 --- a/mdm-plugin-datamodel/grails-app/views/dataModel/diff.gson +++ b/mdm-plugin-datamodel/grails-app/views/dataModel/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.datamodel.DataModel model { 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/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/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-datamodel/grails-app/views/dataModel/mergeDiff.gson b/mdm-plugin-datamodel/grails-app/views/dataModel/mergeDiff.gson index 3a738fd224..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.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/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/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-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-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..f949e2d6b8 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,8 +25,9 @@ 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.util.Version +import uk.ac.ox.softeng.maurodatamapper.version.Version import grails.gorm.transactions.Transactional import grails.testing.mixin.integration.Integration @@ -35,6 +36,8 @@ import grails.web.mime.MimeType import groovy.util.logging.Slf4j import spock.lang.Shared +import java.nio.charset.Charset + import static uk.ac.ox.softeng.maurodatamapper.core.bootstrap.StandardEmailAddress.FUNCTIONAL_TEST import static io.micronaut.http.HttpStatus.CREATED @@ -74,7 +77,6 @@ import static io.micronaut.http.HttpStatus.UNPROCESSABLE_ENTITY */ @Integration @Slf4j -//@Stepwise class DataModelFunctionalSpec extends ResourceFunctionalSpec { @Shared @@ -151,311 +153,474 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { }''' } - String getExpectedMergeDiffJson() { + String getExpectedLegacyMergeDiffJson() { '''{ - "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": 14, + "diffs": [ + { + "description": { + "left": "DescriptionRight", + "right": "DescriptionLeft", + "isMergeConflict": true, + "commonAncestorValue": null + } + }, + { + "dataClasses": { + "deleted": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "deleteLeftOnly", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "domainType": "DataModel", + "finalised": true + } + ] + }, + "isMergeConflict": false + }, + { + "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 + } + ], + "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": "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}", + "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": [ - { - "value": { - "id": "${json-unit.matches:id}", - "label": "addLeftOnly", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "domainType": "DataModel", - "finalised": false - } - ] - }, - "isMergeConflict": false - }, + ], + "created": [ { - "value": { + "value": { + "id": "${json-unit.matches:id}", + "label": "addLeftToExistingClass", + "breadcrumbs": [ + { "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 } - ] - } - } - ] -}''' - } - - void 'test getting DataModel types'() { - when: - GET('types', STRING_ARG) - - then: - verifyJsonResponse OK, '''[ - "Data Asset", - "Data Standard" - ]''' - } - - void 'test getting DataModel exporters'() { - when: - GET('providers/exporters', STRING_ARG) - - then: - verifyJsonResponse OK, '''[ - { + ] + } + } + ] + }, + { + "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 + } + } + ] + } + ] + } + }, + { + "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}", + "namespace": "functional.test", + "key": "modifyOnSource", + "count": 1, + "diffs": [ + { + "value": { + "left": "some original value", + "right": "source has modified this", + "isMergeConflict": false + } + } + ] + } + ] + } + } + ] +}''' + } + + String getExpectedMergeDiffJson() { + '''{ + "sourceId": "${json-unit.matches:id}", + "targetId": "${json-unit.matches:id}", + "path": "dm:Functional Test Model$source", + "label": "Functional Test Model", + "count": 14, + "diffs": [ + { + "fieldName": "description", + "path": "dm:Functional Test Model$source@description", + "sourceValue": "DescriptionLeft", + "targetValue": "DescriptionRight", + "commonAncestorValue": null, + "isMergeConflict": true, + "type": "modification" + }, + { + "path": "dm:Functional Test Model$source|dc:addLeftOnly", + "isMergeConflict": false, + "isSourceModificationAndTargetDeletion": false, + "type": "creation" + }, + { + "path": "dm:Functional Test Model$source|dc:modifyAndDelete", + "isMergeConflict": true, + "isSourceModificationAndTargetDeletion": true, + "type": "creation" + }, + { + "path": "dm:Functional Test Model$source|dc:deleteAndModify", + "isMergeConflict": true, + "isSourceDeletionAndTargetModification": true, + "type": "deletion" + }, + { + "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", + "sourceValue": "DescriptionLeft", + "targetValue": "DescriptionRight", + "commonAncestorValue": null, + "isMergeConflict": true, + "type": "modification" + }, + { + "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", + "isMergeConflict": false, + "isSourceDeletionAndTargetModification": false, + "type": "deletion" + }, + { + "fieldName": "description", + "path": "dm:Functional Test Model$source|dc:modifyAndModifyReturningDifference@description", + "sourceValue": "DescriptionLeft", + "targetValue": "DescriptionRight", + "commonAncestorValue": null, + "isMergeConflict": true, + "type": "modification" + }, + { + "fieldName": "description", + "path": "dm:Functional Test Model$source|dc:modifyLeftOnly@description", + "sourceValue": "Description", + "targetValue": null, + "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" + } + ] +}''' + } + + void 'test getting DataModel types'() { + when: + GET('types', STRING_ARG) + + then: + verifyJsonResponse OK, '''[ + "Data Asset", + "Data Standard" + ]''' + } + + void 'test getting DataModel exporters'() { + when: + GET('providers/exporters', STRING_ARG) + + then: + verifyJsonResponse OK, '''[ + { "providerType": "DataModelExporter", "knownMetadataKeys": [ @@ -1346,7 +1511,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 +1527,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 +1537,8 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { responseBody().rightId == leftId when: + GET("$rightId/mergeDiff/$mainId", STRING_ARG) + log.debug('{}', jsonResponseBody()) GET("$rightId/mergeDiff/$mainId") then: @@ -1392,173 +1553,109 @@ 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) + TestMergeData mergeData = buildComplexDataModelsForMerging() - 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 + 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.commonAncestor) + } + 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", [branchName: VersionAwareConstraints.DEFAULT_BRANCH_NAME]) + PUT("$id/newBranchModelVersion", [:]) verifyResponse CREATED, response - String target = responseBody().id - PUT("$id/newBranchModelVersion", [branchName: 'source']) + String mainId = responseBody().id + PUT("$id/newBranchModelVersion", [branchName: 'left']) verifyResponse CREATED, response - String source = responseBody().id + String leftId = responseBody().id + PUT("$id/newBranchModelVersion", [branchName: 'right']) + verifyResponse CREATED, response + String rightId = responseBody().id when: - 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("$leftId/mergeDiff/$mainId?isLegacy=false", STRING_ARG) + log.debug('{}', jsonResponseBody()) + GET("$leftId/mergeDiff/$mainId?isLegacy=false") - 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") + then: verifyResponse OK, response - String modifyAndModifyReturningDifference = responseBody().id + responseBody().targetId == mainId + responseBody().sourceId == leftId - then: - 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 + when: + GET("$rightId/mergeDiff/$mainId?isLegacy=false", STRING_ARG) + log.debug('{}', jsonResponseBody()) + GET("$rightId/mergeDiff/$mainId?isLegacy=false") - 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']) + then: verifyResponse OK, response + responseBody().targetId == mainId + responseBody().sourceId == rightId - 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 + cleanup: + cleanUpData(mainId) + cleanUpData(leftId) + cleanUpData(rightId) + cleanUpData(id) + } - PUT("$source", [description: 'DescriptionLeft']) - verifyResponse OK, response + void 'MD04 : test finding merge difference of two complex datamodels with the new style'() { + given: + TestMergeData mergeData = buildComplexDataModelsForMerging() when: - 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 + GET("$mergeData.source/mergeDiff/$mergeData.target?isLegacy=false", STRING_ARG) then: - 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 + verifyJsonResponse OK, expectedMergeDiffJson - 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 + cleanup: + cleanUpData(mergeData.source) + cleanUpData(mergeData.target) + cleanUpData(mergeData.commonAncestor) + } - POST("$target/dataClasses/$existingClass/dataClasses", [label: 'addRightToExistingClass']) - verifyResponse CREATED, response - POST("$target/dataClasses", [label: 'addRightOnly']) - verifyResponse CREATED, response - POST("$target/dataClasses", [label: 'addAndAddReturningNoDifference']) + 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 - POST("$target/dataClasses", [label: 'addAndAddReturningDifference', description: 'DescriptionRight']) + String target = responseBody().id + PUT("$id/newBranchModelVersion", [branchName: 'interestingBranch']) verifyResponse CREATED, response - - PUT("$target", [description: 'DescriptionRight']) + String source = responseBody().id + PUT(source, [aliases: ['not main branch', 'mergeInto']]) verifyResponse OK, response + when: - GET("$source/mergeDiff/$target", STRING_ARG) - // GET("$source/mergeDiff/$target") + GET("$source/mergeDiff/$target?isLegacy=false", STRING_ARG) + log.warn('{}', jsonResponseBody()) + GET("$source/mergeDiff/$target?isLegacy=false") then: - verifyJsonResponse OK, expectedMergeDiffJson + 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) @@ -1566,7 +1663,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 +1690,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,164 +1739,15 @@ 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) + TestMergeData mergeData = buildComplexDataModelsForMerging() - 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 + when: + GET("$mergeData.source/mergeDiff/$mergeData.target", STRING_ARG) - // 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 - - 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) - - then: - verifyJsonResponse OK, expectedMergeDiffJson + then: + verifyJsonResponse OK, expectedLegacyMergeDiffJson when: String modifiedDescriptionSource = 'modifiedDescriptionSource' @@ -1807,8 +1755,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 : [ [ @@ -1820,27 +1768,27 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { deleted : [ [ - id : deleteAndModify, + id : mergeData.targetMap.deleteAndModify, label: "deleteAndModify" ], [ - id : deleteLeftOnly, + id : mergeData.targetMap.deleteLeftOnly, label: "deleteLeftOnly" ] ], created : [ [ - id : addLeftOnly, + id : mergeData.sourceMap.addLeftOnly, label: "addLeftOnly" ], [ - id : sourceModifyAndDelete, + id : mergeData.sourceMap.modifyAndDelete, label: "modifyAndDelete" ] ], modified : [ [ - leftId: addAndAddReturningDifference, + leftId: mergeData.targetMap.addAndAddReturningDifference, label : "addAndAddReturningDifference", diffs : [ [ @@ -1850,7 +1798,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { ] ], [ - leftId: existingClass, + leftId: mergeData.targetMap.existingClass, label : "existingClass", diffs : [ [ @@ -1858,13 +1806,13 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { deleted : [ [ - id : deleteLeftOnlyFromExistingClass, + id : mergeData.targetMap.deleteLeftOnlyFromExistingClass, label: "deleteLeftOnlyFromExistingClass" ] ], created : [ [ - id : addLeftToExistingClass, + id : mergeData.sourceMap.addLeftToExistingClass, label: "addLeftToExistingClass" ] ] @@ -1873,7 +1821,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { ] ], [ - leftId: modifyAndModifyReturningDifference, + leftId: mergeData.targetMap.modifyAndModifyReturningDifference, label : "modifyAndModifyReturningDifference", diffs : [ [ @@ -1883,7 +1831,7 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { ] ], [ - leftId: modifyLeftOnly, + leftId: mergeData.targetMap.modifyLeftOnly, label : "modifyLeftOnly", diffs : [ [ @@ -1899,32 +1847,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.targetMap.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 @@ -1935,12 +1884,12 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { } cleanup: - cleanUpData(source) - cleanUpData(target) - cleanUpData(id) + cleanUpData(mergeData.source) + cleanUpData(mergeData.target) + cleanUpData(mergeData.commonAncestor) } - void 'VB09d : test merging metadata diff into draft model'() { + void 'MP04 : test merging metadata diff into draft model'() { given: String id = createNewItem(validJson) @@ -1963,7 +1912,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 @@ -2006,7 +1955,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 @@ -2020,123 +1969,116 @@ 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": 6, + "diffs": [ + { + "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: @@ -2251,7 +2193,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) @@ -2383,867 +2325,1318 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { cleanUpData(id) } - void 'test changing folder from DataModel context'() { - given: 'The save action is executed with valid data' + void 'MP06 : test merging diff with no patch data with new style'() { + given: String id = createNewItem(validJson) - when: - PUT("$id/folder/${movingFolderId}", [:]) - - then: + 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: - GET("folders/$movingFolderId/dataModels", MAP_ARG, true) - - then: - response.body().count == 1 - response.body().items[0].id == id - - when: - GET("folders/$folderId/dataModels", MAP_ARG, true) + PUT("$source/mergeInto/$target?isLegacy=false", [:]) then: - response.body().count == 0 + verifyResponse(UNPROCESSABLE_ENTITY, response) + responseBody().total == 1 + responseBody().errors[0].message.contains('cannot be null') cleanup: + cleanUpData(source) + cleanUpData(target) cleanUpData(id) } - void 'test changing folder from Folder context'() { - given: 'The save action is executed with valid data' + 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: - response = PUT("folders/${movingFolderId}/dataModels/$id", [:], MAP_ARG, true) + PUT("$source/mergeInto/$target?isLegacy=false", [patch: + [ + targetId: target, + sourceId: UUID.randomUUID().toString(), + label : "Functional Test Model", + count : 0, + patches : [] + ] + ]) then: - verifyResponse OK, response + verifyResponse(UNPROCESSABLE_ENTITY, response) + responseBody().message == 'Source model id passed in request body does not match source model id in URI.' when: - GET("folders/$movingFolderId/dataModels", MAP_ARG, true) + PUT("$source/mergeInto/$target", [patch: + [ + targetId: UUID.randomUUID().toString(), + sourceId: source, + label : "Functional Test Model", + count : 0, + patches : [] + ] + ]) then: - response.body().count == 1 - response.body().items[0].id == id + verifyResponse(UNPROCESSABLE_ENTITY, response) + responseBody().message == 'Target model id passed in request body does not match target model id in URI.' when: - GET("folders/$folderId/dataModels", MAP_ARG, true) + PUT("$source/mergeInto/$target", [patch: + [ + targetId: target, + sourceId: source, + label : "Functional Test Model", + count : 0, + patches : [] + ] + ]) then: - response.body().count == 0 + verifyResponse(OK, response) + responseBody().id == target cleanup: + cleanUpData(source) + cleanUpData(target) cleanUpData(id) } - void 'D01 : test diffing 2 DataModels'() { - given: 'The save action is executed with valid data' - String id = createNewItem(validJson) - String otherId = createNewItem([label: 'Functional Test Model 2']) + void 'MP08 : test merging diff into draft model using new style'() { + given: + TestMergeData mergeData = buildComplexDataModelsForMerging() when: - GET("${id}/diff/${otherId}", STRING_ARG) + GET("$mergeData.source/mergeDiff/$mergeData.target?isLegacy=false") then: - verifyJsonResponse OK, '''{ - "leftId": "${json-unit.matches:id}", - "rightId": "${json-unit.matches:id}", - "count": 1, - "label": "Functional Test Model", - "diffs": [ - { - "label": { - "left": "Functional Test Model", - "right": "Functional Test Model 2" - } - } - ] -}''' + verifyResponse OK, response + responseBody().diffs.size() == 14 - cleanup: - cleanUpData(id) - } + when: + List patches = responseBody().diffs + PUT("$mergeData.source/mergeInto/$mergeData.target?isLegacy=false", [ + patch: [ + targetId: responseBody().targetId, + sourceId: responseBody().sourceId, + label : responseBody().label, + count : patches.size(), + patches : patches] + ]) - void 'D02 : test diffing branches'() { - given: - // Create base model and finalise - String id = createNewItem(validJson) - PUT("$id/finalise", [versionChangeType: 'Major']) + then: verifyResponse OK, response + responseBody().id == mergeData.target + responseBody().description == 'DescriptionLeft' - // Create a new branch main - PUT("$id/newBranchModelVersion", [:]) - verifyResponse CREATED, response - String mainId = responseBody().id + when: + GET("$mergeData.target/dataClasses") - // Create a new branch test - PUT("$id/newBranchModelVersion", [branchName: 'test']) - verifyResponse CREATED, response - String testId = responseBody().id + 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' - // Add dataclass test2 to main branch - POST("$mainId/dataClasses", [label: 'test2']) - verifyResponse CREATED, response + when: + GET("$mergeData.target/dataClasses/$mergeData.targetMap.existingClass/dataClasses") - // Add dataclass test2 to test branch - POST("$testId/dataClasses", [label: 'test2']) - verifyResponse CREATED, response + then: + responseBody().items.label as Set == ['addRightToExistingClass', 'addLeftToExistingClass'] as Set - when: 'performing diff' - GET("$testId/diff/$mainId") + when: + GET("${mergeData.target}/metadata") then: - verifyResponse OK, response - responseBody() + 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(id) - cleanUpData(testId) - cleanUpData(mainId) + cleanUpData(mergeData.source) + cleanUpData(mergeData.target) + cleanUpData(mergeData.commonAncestor) } - void 'D03 : test diffing branches on modifications'() { + void 'MP09 : test merging new style diff with metadata creation gh-111'() { given: - // Create base model String id = createNewItem(validJson) - // Create content - POST("$id/dataClasses", [label: 'parent']) - verifyResponse(CREATED, response) - String parentId = responseBody().id - POST("$id/dataClasses/${parentId}/dataClasses", [label: 'child']) - verifyResponse(CREATED, response) - POST("$id/dataClasses", [label: 'content', description: 'some interesting content']) + POST("$id/rules", [name: 'Bootstrapped versioning V2Model Rule']) verifyResponse(CREATED, response) - //Finalise PUT("$id/finalise", [versionChangeType: 'Major']) verifyResponse OK, response - - // Create a new branch main - PUT("$id/newBranchModelVersion", [:]) + PUT("$id/newBranchModelVersion", [branchName: VersionAwareConstraints.DEFAULT_BRANCH_NAME]) verifyResponse CREATED, response - String mainId = responseBody().id - - // Create a new branch test - PUT("$id/newBranchModelVersion", [branchName: 'test']) + String target = responseBody().id + PUT("$id/newBranchModelVersion", [branchName: 'interestingBranch']) verifyResponse CREATED, response - String testId = responseBody().id - - //Change child DC label - GET("$testId/dataClasses") - verifyResponse(OK, response) - parentId = responseBody().items.find {it.label == 'parent'}.id - String contentId = responseBody().items.find {it.label == 'content'}.id - GET("$testId/dataClasses/${parentId}/dataClasses") - verifyResponse(OK, response) - assert responseBody().items.first().id - assert responseBody().items.first().label == 'child' - String childId = responseBody().items.first().id - PUT("$testId/dataClasses/${parentId}/dataClasses/$childId", [label: 'child edit']) - verifyResponse(OK, response) - // change description of the content - PUT("$testId/dataClasses/$contentId", [description: 'a change to the description']) - - when: 'performing diff' - GET("$testId/diff/$mainId") - - then: - verifyResponse OK, response - responseBody().count == 4 - responseBody().diffs.size() == 2 - responseBody().diffs.first().branchName.left == 'test' - responseBody().diffs.first().branchName.right == VersionAwareConstraints.DEFAULT_BRANCH_NAME + String source = responseBody().id - and: - Map dataClassesDiffs = responseBody().diffs[1].dataClasses - dataClassesDiffs.modified.size() == 2 + POST("$source/metadata", [namespace: 'test.com', key: 'testProperty', value: 'testValue']) + verifyResponse(CREATED, response) - and: - Map contentDiff = dataClassesDiffs.modified.find {it.label == 'content'} - contentDiff.diffs.size() == 1 - contentDiff.diffs.first().description.left == 'a change to the description' - contentDiff.diffs.first().description.right == 'some interesting content' + 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) - and: - Map parentDiff = dataClassesDiffs.modified.find {it.label == 'parent'} - parentDiff.diffs.size() == 1 - parentDiff.diffs.first().dataClasses.deleted.size() == 1 - parentDiff.diffs.first().dataClasses.created.size() == 1 - parentDiff.diffs.first().dataClasses.deleted.first().value.label == 'child edit' - parentDiff.diffs.first().dataClasses.created.first().value.label == 'child' + 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' + ], + [ - cleanup: - cleanUpData(id) - cleanUpData(testId) - cleanUpData(mainId) - } + path : 'dm:Functional Test Model$interestingBranch|ru:Bootstrapped versioning V2Model Rule|rr:sql', + isMergeConflict : false, + isSourceModificationAndTargetDeletion: false, + type : 'creation', + branchSelected : 'source', + branchNameSelected : 'interestingBranch' + ] + ] + ] + ]) - void 'E01 : test export a single DataModel'() { - given: - String id = createNewItem(validJson) + then: + verifyResponse(OK, response) + responseBody().id == target when: - GET("${id}/export/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/2.0", STRING_ARG) + GET("${target}/metadata") then: - verifyJsonResponse OK, '''{ - "dataModel": { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "lastUpdated": "${json-unit.matches:offsetDateTime}", - "type": "Data Standard", - "documentationVersion": "1.0.0", - "finalised": false, - "authority": { - "id": "${json-unit.matches:id}", - "url": "http://localhost", - "label": "Test Authority" - } - }, - "exportMetadata": { - "exportedBy": "Unlogged User", - "exportedOn": "${json-unit.matches:offsetDateTime}", - "exporter": { - "namespace": "uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter", - "name": "DataModelJsonExporterService", - "version": "2.0" - } - } -}''' + responseBody().items.find {it.namespace == 'test.com' && it.key == 'testProperty'} cleanup: + cleanUpData(source) + cleanUpData(target) cleanUpData(id) } - void 'E02 : test export multiple DataModels (json only exports first id)'() { + void 'MP10 : test merge into with new style diff with aliases gh-112'() { given: String id = createNewItem(validJson) - String id2 = createNewItem([label: 'Functional Test Model 2']) - - when: - POST('export/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/2.0', - [dataModelIds: [id, id2]], STRING_ARG - ) - - then: - verifyJsonResponse OK, '''{ - "dataModel": { - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "lastUpdated": "${json-unit.matches:offsetDateTime}", - "type": "Data Standard", - "documentationVersion": "1.0.0", - "finalised": false, - "authority": { - "id": "${json-unit.matches:id}", - "url": "http://localhost", - "label": "Test Authority" - } - }, - "exportMetadata": { - "exportedBy": "Unlogged User", - "exportedOn": "${json-unit.matches:offsetDateTime}", - "exporter": { - "namespace": "uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter", - "name": "DataModelJsonExporterService", - "version": "2.0" - } - } -}''' - cleanup: - cleanUpData(id) - cleanUpData(id2) - } + 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']]) - void 'I01 : test import basic DataModel'() { - given: - String id = createNewItem(validJson) - GET("${id}/export/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/2.0", STRING_ARG) - verifyResponse OK, jsonCapableResponse - String exportedJsonString = jsonCapableResponse.body() + when: + GET("$source/mergeDiff/$target?isLegacy=false") - expect: - exportedJsonString + then: + verifyResponse OK, response when: - POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ - finalised : false, - modelName : 'Functional Test Import', - folderId : folderId.toString(), - importAsNewDocumentationVersion: false, - importFile : [ - fileName : 'FT Import', - fileType : MimeType.JSON_API.name, - fileContents: exportedJsonString.bytes.toList() - ] - ], STRING_ARG) + 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: - verifyJsonResponse CREATED, '''{ - "count": 1, - "items": [ - { - "domainType": "DataModel", - "id": "${json-unit.matches:id}", - "label": "Functional Test Import", - "type": "Data Standard", - "branchName": "main", - "documentationVersion": "1.0.0" - } - ] - }''' + 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) } - void 'I02 : test import basic DataModel as new documentation version'() { - given: - String id = createNewItem([ - label : 'Functional Test Model', - finalised : true, - modelVersion: Version.from('1.0.0') - ]) + TestMergeData buildComplexDataModelsForMerging() { - GET("${id}/export/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/2.0", STRING_ARG) - verifyResponse OK, jsonCapableResponse - String exportedJsonString = jsonCapableResponse.body() + String id = createNewItem(validJson) - expect: - exportedJsonString + 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 caExistingClass = responseBody().id + POST("$id/dataClasses/$caExistingClass/dataClasses", [label: 'deleteLeftOnlyFromExistingClass']) + verifyResponse CREATED, response + POST("$id/dataClasses/$caExistingClass/dataClasses", [label: 'deleteRightOnlyFromExistingClass']) + verifyResponse CREATED, response - when: - POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ - finalised : true, - modelName : 'Functional Test Model', - folderId : folderId.toString(), - importAsNewDocumentationVersion: true, - importFile : [ - fileName : 'FT Import', - fileType : MimeType.JSON_API.name, - fileContents: exportedJsonString.bytes.toList() - ] - ], STRING_ARG) + 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 - then: - verifyJsonResponse CREATED, '''{ - "count": 1, - "items": [ - { - "domainType": "DataModel", - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "type": "Data Standard", - "branchName": "main", - "documentationVersion": "2.0.0", - "modelVersion": "1.0.0" - } - ] - }''' + 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 - cleanup: - cleanUpData(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'), + ] - void 'I03 : test import basic DataModel as new branch model version'() { - given: - String id = createNewItem([ - label : 'Functional Test Model', - finalised : true, - modelVersion: Version.from('1.0.0') - ]) + // Modify source + DELETE("$source/dataClasses/${sourceMap.deleteAndDelete}") + verifyResponse NO_CONTENT, response + DELETE("$source/dataClasses/${sourceMap.existingClass}/dataClasses/${sourceMap.deleteLeftOnlyFromExistingClass}") + verifyResponse NO_CONTENT, response + DELETE("$source/dataClasses/${sourceMap.deleteLeftOnly}") + verifyResponse NO_CONTENT, response + DELETE("$source/dataClasses/${sourceMap.deleteAndModify}") + verifyResponse NO_CONTENT, response - GET("${id}/export/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/2.0", STRING_ARG) - verifyResponse OK, jsonCapableResponse - String exportedJsonString = jsonCapableResponse.body() + PUT("$source/dataClasses/${sourceMap.modifyLeftOnly}", [description: 'Description']) + verifyResponse OK, response + PUT("$source/dataClasses/${sourceMap.modifyAndDelete}", [description: 'Description']) + verifyResponse OK, response + PUT("$source/dataClasses/${sourceMap.modifyAndModifyReturningNoDifference}", [description: 'Description']) + verifyResponse OK, response + PUT("$source/dataClasses/${sourceMap.modifyAndModifyReturningDifference}", [description: 'DescriptionLeft']) + verifyResponse OK, response - expect: - exportedJsonString + POST("$source/dataClasses/${sourceMap.existingClass}/dataClasses", [label: 'addLeftToExistingClass']) + verifyResponse CREATED, response + sourceMap.addLeftToExistingClass = responseBody().id + POST("$source/dataClasses", [label: 'addLeftOnly']) + verifyResponse CREATED, response + sourceMap.addLeftOnly = responseBody().id + POST("$source/dataClasses", [label: 'addAndAddReturningNoDifference']) + verifyResponse CREATED, response + POST("$source/dataClasses", [label: 'addAndAddReturningDifference', description: 'DescriptionLeft']) + verifyResponse CREATED, response + sourceMap.addAndAddReturningDifference = responseBody().id - when: - POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ - finalised : true, - modelName : 'Functional Test Model', - folderId : folderId.toString(), - importAsNewDocumentationVersion: false, - importAsNewBranchModelVersion : true, - importFile : [ - fileName : 'FT Import', - fileType : MimeType.JSON_API.name, - fileContents: exportedJsonString.bytes.toList() - ] - ], STRING_ARG) + PUT("$source", [description: 'DescriptionLeft']) + verifyResponse OK, response - then: - verifyJsonResponse CREATED, '''{ - "count": 1, - "items": [ - { - "domainType": "DataModel", - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "type": "Data Standard", - "branchName": "main", - "documentationVersion": "1.0.0", - "modelVersion": "2.0.0" - } - ] - }''' + 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 - cleanup: - cleanUpData(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'), + ] + // Modify target + DELETE("$target/dataClasses/${targetMap.existingClass}/dataClasses/${targetMap.deleteRightOnlyFromExistingClass}") + verifyResponse NO_CONTENT, response + DELETE("$target/dataClasses/${targetMap.deleteRightOnly}") + verifyResponse NO_CONTENT, response + DELETE("$target/dataClasses/${targetMap.deleteAndDelete}") + verifyResponse NO_CONTENT, response + DELETE("$target/dataClasses/${targetMap.modifyAndDelete}") + verifyResponse NO_CONTENT, response - void 'I04 : test import basic DataModel as new main branch model version with another branch version that exists'() { - given: - String id = createNewItem([ - label : 'Functional Test Model', - finalised : true, - modelVersion: Version.from('1.0.0') - ]) + PUT("$target/dataClasses/${targetMap.modifyRightOnly}", [description: 'Description']) + verifyResponse OK, response + PUT("$target/dataClasses/${targetMap.deleteAndModify}", [description: 'Description']) + verifyResponse OK, response + PUT("$target/dataClasses/${targetMap.modifyAndModifyReturningNoDifference}", [description: 'Description']) + verifyResponse OK, response + PUT("$target/dataClasses/${targetMap.modifyAndModifyReturningDifference}", [description: 'DescriptionRight']) + verifyResponse OK, response - GET("${id}/export/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/2.0", STRING_ARG) - verifyResponse OK, jsonCapableResponse - String exportedJsonString = jsonCapableResponse.body() + POST("$target/dataClasses/${targetMap.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 + targetMap.addAndAddReturningDifference = responseBody().id - expect: - exportedJsonString + PUT("$target", [description: 'DescriptionRight']) + verifyResponse OK, response + DELETE("$target/metadata/${targetMap.metadataModifyAndDelete}") + verifyResponse NO_CONTENT, response + + + 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 + assert responseBody().id + responseBody().id + } + + void 'test changing folder from DataModel context'() { + given: 'The save action is executed with valid data' + String id = createNewItem(validJson) when: - POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ - finalised : false, - modelName : 'Functional Test Model', - folderId : folderId.toString(), - importAsNewDocumentationVersion: false, - importAsNewBranchModelVersion : true, - importFile : [ - fileName : 'FT Import', - fileType : MimeType.JSON_API.name, - fileContents: exportedJsonString.bytes.toList() - ] - ], STRING_ARG) + PUT("$id/folder/${movingFolderId}", [:]) then: - verifyJsonResponse CREATED, '''{ - "count": 1, - "items": [ - { - "domainType": "DataModel", - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "type": "Data Standard", - "branchName": "main", - "documentationVersion": "1.0.0" - } - ] - }''' + verifyResponse OK, response when: - POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ - finalised : false, - modelName : 'Functional Test Model', - folderId : folderId.toString(), - importAsNewDocumentationVersion: false, - importAsNewBranchModelVersion : true, - importFile : [ - fileName : 'FT Import', - fileType : MimeType.JSON_API.name, - fileContents: exportedJsonString.bytes.toList() - ] - ]) + GET("folders/$movingFolderId/dataModels", MAP_ARG, true) 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' + response.body().count == 1 + response.body().items[0].id == id when: - POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ - finalised : false, - modelName : 'Functional Test Model', - folderId : folderId.toString(), - importAsNewDocumentationVersion: false, - importAsNewBranchModelVersion : true, - newBranchName : 'functionalTest', - importFile : [ - fileName : 'FT Import', - fileType : MimeType.JSON_API.name, - fileContents: exportedJsonString.bytes.toList() - ] - ], STRING_ARG) + GET("folders/$folderId/dataModels", MAP_ARG, true) then: - verifyJsonResponse CREATED, '''{ - "count": 1, - "items": [ - { - "domainType": "DataModel", - "id": "${json-unit.matches:id}", - "label": "Functional Test Model", - "type": "Data Standard", - "branchName": "functionalTest", - "documentationVersion": "1.0.0" - } - ] - }''' + response.body().count == 0 cleanup: cleanUpData(id) } - void 'test delete multiple models'() { - given: - def idstoDelete = [] - (1..4).each {n -> - idstoDelete << createNewItem([ - folder: folderId, - label : UUID.randomUUID().toString() - ]) - } - - when: - DELETE('', [ - ids : idstoDelete, - permanent: false - ], STRING_ARG) - - then: - verifyJsonResponse OK, '''{ - "count": 4, - "items": [ - { - "id": "${json-unit.matches:id}", - "domainType": "DataModel", - "label": "${json-unit.matches:id}", - "type": "Data Standard", - "branchName": "main", - "documentationVersion": "1.0.0", - "deleted": true - }, - { - "id": "${json-unit.matches:id}", - "domainType": "DataModel", - "label": "${json-unit.matches:id}", - "type": "Data Standard", - "branchName": "main", - "documentationVersion": "1.0.0", - "deleted": true - }, - { - "id": "${json-unit.matches:id}", - "domainType": "DataModel", - "label": "${json-unit.matches:id}", - "type": "Data Standard", - "branchName": "main", - "documentationVersion": "1.0.0", - "deleted": true - }, - { - "id": "${json-unit.matches:id}", - "domainType": "DataModel", - "label": "${json-unit.matches:id}", - "type": "Data Standard", - "branchName": "main", - "documentationVersion": "1.0.0", - "deleted": true - } - ] -}''' + void 'test changing folder from Folder context'() { + given: 'The save action is executed with valid data' + String id = createNewItem(validJson) when: - DELETE('', [ - ids : idstoDelete, - permanent: true - ]) + response = PUT("folders/${movingFolderId}/dataModels/$id", [:], MAP_ARG, true) then: - verifyResponse NO_CONTENT, response - } + verifyResponse OK, response - void 'I05 : test importing simple test DataModel'() { when: - POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ - finalised : true, - folderId : folderId.toString(), - importAsNewDocumentationVersion: false, - importFile : [ - fileName : 'FT Import', - fileType : MimeType.JSON_API.name, - fileContents: loadTestFile('simpleDataModel').toList() - ] - ]) - verifyResponse CREATED, response - def id = response.body().items[0].id + GET("folders/$movingFolderId/dataModels", MAP_ARG, true) then: - id - - cleanup: - cleanUpData(id) - } + response.body().count == 1 + response.body().items[0].id == id - void 'I06 : test importing complex test DataModel'() { when: - POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ - finalised : true, - folderId : folderId.toString(), - importAsNewDocumentationVersion: false, - importFile : [ - fileName : 'FT Import', - fileType : MimeType.JSON_API.name, - fileContents: loadTestFile('complexDataModel').toList() - ] - ]) - verifyResponse CREATED, response - def id = response.body().items[0].id + GET("folders/$folderId/dataModels", MAP_ARG, true) then: - id + response.body().count == 0 cleanup: cleanUpData(id) } - void 'I07 : test importing DataModel with classifiers'() { + void 'D01 : test diffing 2 DataModels'() { + given: 'The save action is executed with valid data' + String id = createNewItem(validJson) + String otherId = createNewItem([label: 'Functional Test Model 2']) + when: - POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ - finalised : true, - folderId : folderId.toString(), - importAsNewDocumentationVersion: false, - importFile : [ - fileName : 'FT Import', - fileType : MimeType.JSON_API.name, - fileContents: loadTestFile('fullModelWithClassifiers').toList() - ] - ]) - verifyResponse CREATED, response - def id = response.body().items[0].id + GET("${id}/diff/${otherId}", STRING_ARG) then: - id + verifyJsonResponse OK, '''{ + "leftId": "${json-unit.matches:id}", + "rightId": "${json-unit.matches:id}", + "count": 1, + "label": "Functional Test Model", + "diffs": [ + { + "label": { + "left": "Functional Test Model", + "right": "Functional Test Model 2" + } + } + ] +}''' cleanup: cleanUpData(id) } - void 'I08 : test importing 2 DataModel'() { - when: - POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelXmlImporterService/3.0', [ - finalised : true, - folderId : folderId.toString(), - importAsNewDocumentationVersion: false, - importFile : [ - fileName : 'FT Import', - fileType : MimeType.XML.name, - fileContents: loadTestFile('multiModels', 'xml').toList() - ] - ]) - verifyResponse CREATED, response - def id = response.body().items[0].id - def id2 = response.body().items[1].id + void 'D02 : test diffing branches'() { + given: + // Create base model and finalise + String id = createNewItem(validJson) + PUT("$id/finalise", [versionChangeType: 'Major']) + verifyResponse OK, response - then: - id - id2 + // Create a new branch main + PUT("$id/newBranchModelVersion", [:]) + verifyResponse CREATED, response + String mainId = responseBody().id - cleanup: - cleanUpData(id) - cleanUpData(id2) - } + // Create a new branch test + PUT("$id/newBranchModelVersion", [branchName: 'test']) + verifyResponse CREATED, response + String testId = responseBody().id - void 'E03 : test export simple DataModel'() { - given: - POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ - finalised : false, - folderId : folderId.toString(), - importAsNewDocumentationVersion: false, - importFile : [ - fileName : 'FT Import', - fileType : MimeType.JSON_API.name, - fileContents: loadTestFile('simpleDataModel').toList() - ] - ]) + // Add dataclass test2 to main branch + POST("$mainId/dataClasses", [label: 'test2']) + verifyResponse CREATED, response + // Add dataclass test2 to test branch + POST("$testId/dataClasses", [label: 'test2']) verifyResponse CREATED, response - def id = response.body().items[0].id - String expected = new String(loadTestFile('simpleDataModel')) - .replaceFirst('"exportedBy": "Admin User",', '"exportedBy": "Unlogged User",') - expect: - id + when: 'performing diff' + // just grab the raw json for visual checking + GET("$testId/diff/$mainId", STRING_ARG) + log.debug('{}', jsonResponseBody()) - when: - GET("${id}/export/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/2.0", STRING_ARG) + and: + GET("$testId/diff/$mainId") then: - verifyJsonResponse OK, expected + verifyResponse OK, response + responseBody() cleanup: cleanUpData(id) + cleanUpData(testId) + cleanUpData(mainId) } - void 'E04 : test export complex DataModel'() { + void 'D03 : test diffing branches on modifications'() { given: - POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ - finalised : false, - folderId : folderId.toString(), - importAsNewDocumentationVersion: false, - importFile : [ - fileName : 'FT Import', - fileType : MimeType.JSON_API.name, - fileContents: loadTestFile('complexDataModel').toList() - ] - ]) + // Create base model + String id = createNewItem(validJson) + // Create content + POST("$id/dataClasses", [label: 'parent']) + verifyResponse(CREATED, response) + String parentId = responseBody().id + POST("$id/dataClasses/${parentId}/dataClasses", [label: 'child']) + verifyResponse(CREATED, response) + POST("$id/dataClasses", [label: 'content', description: 'some interesting content']) + verifyResponse(CREATED, response) + + //Finalise + PUT("$id/finalise", [versionChangeType: 'Major']) + verifyResponse OK, response + // Create a new branch main + PUT("$id/newBranchModelVersion", [:]) verifyResponse CREATED, response - def id = response.body().items[0].id - String expected = new String(loadTestFile('complexDataModel')) - .replaceFirst('"exportedBy": "Admin User",', '"exportedBy": "Unlogged User",') + String mainId = responseBody().id - expect: - id + // Create a new branch test + PUT("$id/newBranchModelVersion", [branchName: 'test']) + verifyResponse CREATED, response + String testId = responseBody().id - when: - GET("${id}/export/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/2.0", STRING_ARG) + //Change child DC label + GET("$testId/dataClasses") + verifyResponse(OK, response) + parentId = responseBody().items.find {it.label == 'parent'}.id + String contentId = responseBody().items.find {it.label == 'content'}.id + GET("$testId/dataClasses/${parentId}/dataClasses") + verifyResponse(OK, response) + assert responseBody().items.first().id + assert responseBody().items.first().label == 'child' + String childId = responseBody().items.first().id + PUT("$testId/dataClasses/${parentId}/dataClasses/$childId", [label: 'child edit']) + verifyResponse(OK, response) + // change description of the content + PUT("$testId/dataClasses/$contentId", [description: 'a change to the description']) - then: - verifyJsonResponse OK, expected + when: 'performing diff' + // just grab the raw json for visual checking + GET("$testId/diff/$mainId", STRING_ARG) + log.debug('{}', jsonResponseBody()) - cleanup: - cleanUpData(id) + and: + GET("$testId/diff/$mainId") + + then: + verifyResponse OK, response + responseBody().count == 4 + responseBody().diffs.size() == 2 + responseBody().diffs.first().branchName.left == 'test' + responseBody().diffs.first().branchName.right == VersionAwareConstraints.DEFAULT_BRANCH_NAME + + and: + Map dataClassesDiffs = responseBody().diffs[1].dataClasses + dataClassesDiffs.modified.size() == 2 + + and: + Map contentDiff = dataClassesDiffs.modified.find {it.label == 'content'} + contentDiff.diffs.size() == 1 + contentDiff.diffs.first().description.left == 'a change to the description' + contentDiff.diffs.first().description.right == 'some interesting content' + + and: + Map parentDiff = dataClassesDiffs.modified.find {it.label == 'parent'} + parentDiff.diffs.size() == 1 + parentDiff.diffs.first().dataClasses.deleted.size() == 1 + parentDiff.diffs.first().dataClasses.created.size() == 1 + parentDiff.diffs.first().dataClasses.deleted.first().value.label == 'child edit' + parentDiff.diffs.first().dataClasses.created.first().value.label == 'child' + + cleanup: + cleanUpData(id) + cleanUpData(testId) + cleanUpData(mainId) } - void 'H01 : test getting simple DataModel hierarchy'() { + void 'E01 : test export a single DataModel'() { given: - POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ - finalised : false, - folderId : folderId.toString(), - importAsNewDocumentationVersion: false, - importFile : [ - fileName : 'FT Import', - fileType : MimeType.JSON_API.name, - fileContents: loadTestFile('simpleDataModel').toList() - ] - ]) - verifyResponse CREATED, response - def id = response.body().items[0].id - - expect: - id + String id = createNewItem(validJson) when: - GET("${id}/hierarchy", STRING_ARG) + GET("${id}/export/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/2.0", STRING_ARG) then: verifyJsonResponse OK, '''{ - "childDataClasses": [ - { - "dataClasses": [], - "lastUpdated": "${json-unit.matches:offsetDateTime}", - "dataElements": [], - "domainType": "DataClass", - "availableActions": ["delete","show","update"], - "model": "${json-unit.matches:id}", + "dataModel": { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "lastUpdated": "${json-unit.matches:offsetDateTime}", + "type": "Data Standard", + "documentationVersion": "1.0.0", + "finalised": false, + "authority": { "id": "${json-unit.matches:id}", - "label": "simple", - "breadcrumbs": [ - { - "domainType": "DataModel", - "finalised": false, - "id": "${json-unit.matches:id}", - "label": "Simple Test DataModel" - } - ] + "url": "http://localhost", + "label": "Test Authority" } - ], - "lastUpdated": "${json-unit.matches:offsetDateTime}", - "dataTypes": [], - "domainType": "DataModel", - "documentationVersion": "1.0.0", - "availableActions": ["delete","show","update"], - "branchName":"main", - "finalised": false, - "authority": { + }, + "exportMetadata": { + "exportedBy": "Unlogged User", + "exportedOn": "${json-unit.matches:offsetDateTime}", + "exporter": { + "namespace": "uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter", + "name": "DataModelJsonExporterService", + "version": "2.0" + } + } +}''' + + cleanup: + cleanUpData(id) + } + + void 'E02 : test export multiple DataModels (json only exports first id)'() { + given: + String id = createNewItem(validJson) + String id2 = createNewItem([label: 'Functional Test Model 2']) + + when: + POST('export/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/2.0', + [dataModelIds: [id, id2]], STRING_ARG + ) + + then: + verifyJsonResponse OK, '''{ + "dataModel": { + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "lastUpdated": "${json-unit.matches:offsetDateTime}", + "type": "Data Standard", + "documentationVersion": "1.0.0", + "finalised": false, + "authority": { "id": "${json-unit.matches:id}", "url": "http://localhost", "label": "Test Authority" - }, - "id": "${json-unit.matches:id}", - "label": "Simple Test DataModel", - "type": "Data Standard", - "readableByEveryone": false, - "readableByAuthenticatedUsers": false, - "classifiers": [ - { - "id": "${json-unit.matches:id}", - "label": "test classifier simple", - "lastUpdated": "${json-unit.matches:offsetDateTime}" } - ] + }, + "exportMetadata": { + "exportedBy": "Unlogged User", + "exportedOn": "${json-unit.matches:offsetDateTime}", + "exporter": { + "namespace": "uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter", + "name": "DataModelJsonExporterService", + "version": "2.0" + } + } }''' cleanup: cleanUpData(id) + cleanUpData(id2) } - void 'H02 : test getting complex DataModel hierarchy'() { + void 'I01 : test import basic DataModel'() { given: + String id = createNewItem(validJson) + + GET("${id}/export/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/2.0", STRING_ARG) + verifyResponse OK, jsonCapableResponse + String exportedJsonString = jsonCapableResponse.body() + + expect: + exportedJsonString + + when: POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ finalised : false, + modelName : 'Functional Test Import', folderId : folderId.toString(), importAsNewDocumentationVersion: false, importFile : [ fileName : 'FT Import', fileType : MimeType.JSON_API.name, - fileContents: loadTestFile('complexDataModel').toList() + fileContents: exportedJsonString.bytes.toList() ] + ], STRING_ARG) + + then: + verifyJsonResponse CREATED, '''{ + "count": 1, + "items": [ + { + "domainType": "DataModel", + "id": "${json-unit.matches:id}", + "label": "Functional Test Import", + "type": "Data Standard", + "branchName": "main", + "documentationVersion": "1.0.0" + } + ] + }''' + + cleanup: + cleanUpData(id) + } + + void 'I02 : test import basic DataModel as new documentation version'() { + given: + String id = createNewItem([ + label : 'Functional Test Model', + finalised : true, + modelVersion: Version.from('1.0.0') ]) - verifyResponse CREATED, response - def id = response.body().items[0].id + + GET("${id}/export/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/2.0", STRING_ARG) + verifyResponse OK, jsonCapableResponse + String exportedJsonString = jsonCapableResponse.body() expect: - id + exportedJsonString when: - GET("${id}/hierarchy", STRING_ARG) + POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ + finalised : true, + modelName : 'Functional Test Model', + folderId : folderId.toString(), + importAsNewDocumentationVersion: true, + importFile : [ + fileName : 'FT Import', + fileType : MimeType.JSON_API.name, + fileContents: exportedJsonString.bytes.toList() + ] + ], STRING_ARG) then: - verifyJsonResponse OK, '''{ - "id": "${json-unit.matches:id}", - "domainType": "DataModel", - "label": "Complex Test DataModel", - "availableActions": [ - "delete", - "show", - "update" - ], - "branchName":"main", - "lastUpdated": "${json-unit.matches:offsetDateTime}", - "classifiers": [ - { - "id": "${json-unit.matches:id}", - "label": "test classifier2", - "lastUpdated": "${json-unit.matches:offsetDateTime}" - }, - { - "id": "${json-unit.matches:id}", - "label": "test classifier", - "lastUpdated": "${json-unit.matches:offsetDateTime}" + verifyJsonResponse CREATED, '''{ + "count": 1, + "items": [ + { + "domainType": "DataModel", + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "type": "Data Standard", + "branchName": "main", + "documentationVersion": "2.0.0", + "modelVersion": "1.0.0" + } + ] + }''' + + cleanup: + cleanUpData(id) } - ], - "type": "Data Standard", - "documentationVersion": "1.0.0", - "finalised": false, - "authority": { - "id": "${json-unit.matches:id}", - "url": "http://localhost", - "label": "Test Authority" - }, + + void 'I03 : test import basic DataModel as new branch model version'() { + given: + String id = createNewItem([ + label : 'Functional Test Model', + finalised : true, + modelVersion: Version.from('1.0.0') + ]) + + GET("${id}/export/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/2.0", STRING_ARG) + verifyResponse OK, jsonCapableResponse + String exportedJsonString = jsonCapableResponse.body() + + expect: + exportedJsonString + + when: + POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ + finalised : true, + modelName : 'Functional Test Model', + folderId : folderId.toString(), + importAsNewDocumentationVersion: false, + importAsNewBranchModelVersion : true, + importFile : [ + fileName : 'FT Import', + fileType : MimeType.JSON_API.name, + fileContents: exportedJsonString.bytes.toList() + ] + ], STRING_ARG) + + then: + verifyJsonResponse CREATED, '''{ + "count": 1, + "items": [ + { + "domainType": "DataModel", + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "type": "Data Standard", + "branchName": "main", + "documentationVersion": "1.0.0", + "modelVersion": "2.0.0" + } + ] + }''' + + cleanup: + cleanUpData(id) + } + + + void 'I04 : test import basic DataModel as new main branch model version with another branch version that exists'() { + given: + String id = createNewItem([ + label : 'Functional Test Model', + finalised : true, + modelVersion: Version.from('1.0.0') + ]) + + GET("${id}/export/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/2.0", STRING_ARG) + verifyResponse OK, jsonCapableResponse + String exportedJsonString = jsonCapableResponse.body() + + expect: + exportedJsonString + + when: + POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ + finalised : false, + modelName : 'Functional Test Model', + folderId : folderId.toString(), + importAsNewDocumentationVersion: false, + importAsNewBranchModelVersion : true, + importFile : [ + fileName : 'FT Import', + fileType : MimeType.JSON_API.name, + fileContents: exportedJsonString.bytes.toList() + ] + ], STRING_ARG) + + then: + verifyJsonResponse CREATED, '''{ + "count": 1, + "items": [ + { + "domainType": "DataModel", + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "type": "Data Standard", + "branchName": "main", + "documentationVersion": "1.0.0" + } + ] + }''' + + when: + POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ + finalised : false, + modelName : 'Functional Test Model', + folderId : folderId.toString(), + importAsNewDocumentationVersion: false, + importAsNewBranchModelVersion : true, + importFile : [ + fileName : 'FT Import', + fileType : MimeType.JSON_API.name, + fileContents: exportedJsonString.bytes.toList() + ] + ]) + + then: + verifyResponse CREATED, response + + when: + POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ + finalised : false, + modelName : 'Functional Test Model', + folderId : folderId.toString(), + importAsNewDocumentationVersion: false, + importAsNewBranchModelVersion : true, + newBranchName : 'functionalTest', + importFile : [ + fileName : 'FT Import', + fileType : MimeType.JSON_API.name, + fileContents: exportedJsonString.bytes.toList() + ] + ], STRING_ARG) + + then: + verifyJsonResponse CREATED, '''{ + "count": 1, + "items": [ + { + "domainType": "DataModel", + "id": "${json-unit.matches:id}", + "label": "Functional Test Model", + "type": "Data Standard", + "branchName": "functionalTest", + "documentationVersion": "1.0.0" + } + ] + }''' + + cleanup: + cleanUpData(id) + } + + void 'test delete multiple models'() { + given: + def idstoDelete = [] + (1..4).each {n -> + idstoDelete << createNewItem([ + folder: folderId, + label : UUID.randomUUID().toString() + ]) + } + + when: + DELETE('', [ + ids : idstoDelete, + permanent: false + ], STRING_ARG) + + then: + verifyJsonResponse OK, '''{ + "count": 4, + "items": [ + { + "id": "${json-unit.matches:id}", + "domainType": "DataModel", + "label": "${json-unit.matches:id}", + "type": "Data Standard", + "branchName": "main", + "documentationVersion": "1.0.0", + "deleted": true + }, + { + "id": "${json-unit.matches:id}", + "domainType": "DataModel", + "label": "${json-unit.matches:id}", + "type": "Data Standard", + "branchName": "main", + "documentationVersion": "1.0.0", + "deleted": true + }, + { + "id": "${json-unit.matches:id}", + "domainType": "DataModel", + "label": "${json-unit.matches:id}", + "type": "Data Standard", + "branchName": "main", + "documentationVersion": "1.0.0", + "deleted": true + }, + { + "id": "${json-unit.matches:id}", + "domainType": "DataModel", + "label": "${json-unit.matches:id}", + "type": "Data Standard", + "branchName": "main", + "documentationVersion": "1.0.0", + "deleted": true + } + ] +}''' + + when: + DELETE('', [ + ids : idstoDelete, + permanent: true + ]) + + then: + verifyResponse NO_CONTENT, response + } + + void 'I05 : test importing simple test DataModel'() { + when: + POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ + finalised : true, + folderId : folderId.toString(), + importAsNewDocumentationVersion: false, + importFile : [ + fileName : 'FT Import', + fileType : MimeType.JSON_API.name, + fileContents: loadTestFile('simpleDataModel').toList() + ] + ]) + verifyResponse CREATED, response + def id = response.body().items[0].id + + then: + id + + cleanup: + cleanUpData(id) + } + + void 'I06 : test importing complex test DataModel'() { + when: + POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ + finalised : true, + folderId : folderId.toString(), + importAsNewDocumentationVersion: false, + importFile : [ + fileName : 'FT Import', + fileType : MimeType.JSON_API.name, + fileContents: loadTestFile('complexDataModel').toList() + ] + ]) + verifyResponse CREATED, response + def id = response.body().items[0].id + + then: + id + + cleanup: + cleanUpData(id) + } + + void 'I07 : test importing DataModel with classifiers'() { + when: + POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ + finalised : true, + folderId : folderId.toString(), + importAsNewDocumentationVersion: false, + importFile : [ + fileName : 'FT Import', + fileType : MimeType.JSON_API.name, + fileContents: loadTestFile('fullModelWithClassifiers').toList() + ] + ]) + verifyResponse CREATED, response + def id = response.body().items[0].id + + then: + id + + cleanup: + cleanUpData(id) + } + + void 'I08 : test importing 2 DataModel'() { + when: + POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelXmlImporterService/3.0', [ + finalised : true, + folderId : folderId.toString(), + importAsNewDocumentationVersion: false, + importFile : [ + fileName : 'FT Import', + fileType : MimeType.XML.name, + fileContents: loadTestFile('multiModels', 'xml').toList() + ] + ]) + verifyResponse CREATED, response + def id = response.body().items[0].id + def id2 = response.body().items[1].id + + then: + id + id2 + + cleanup: + cleanUpData(id) + cleanUpData(id2) + } + + void 'E03 : test export simple DataModel'() { + given: + POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ + finalised : false, + folderId : folderId.toString(), + importAsNewDocumentationVersion: false, + importFile : [ + fileName : 'FT Import', + fileType : MimeType.JSON_API.name, + fileContents: loadTestFile('simpleDataModel').toList() + ] + ]) + + verifyResponse CREATED, response + def id = response.body().items[0].id + String expected = new String(loadTestFile('simpleDataModel')) + .replaceFirst('"exportedBy": "Admin User",', '"exportedBy": "Unlogged User",') + + expect: + id + + when: + GET("${id}/export/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/2.0", STRING_ARG) + + then: + verifyJsonResponse OK, expected + + cleanup: + cleanUpData(id) + } + + void 'E04 : test export complex DataModel'() { + given: + POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ + finalised : false, + folderId : folderId.toString(), + importAsNewDocumentationVersion: false, + importFile : [ + fileName : 'FT Import', + fileType : MimeType.JSON_API.name, + fileContents: loadTestFile('complexDataModel').toList() + ] + ]) + + verifyResponse CREATED, response + def id = response.body().items[0].id + String expected = new String(loadTestFile('complexDataModel')) + .replaceFirst('"exportedBy": "Admin User",', '"exportedBy": "Unlogged User",') + + expect: + id + + when: + GET("${id}/export/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.exporter/DataModelJsonExporterService/2.0", STRING_ARG) + + then: + verifyJsonResponse OK, expected + + cleanup: + cleanUpData(id) + } + + void 'H01 : test getting simple DataModel hierarchy'() { + given: + POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ + finalised : false, + folderId : folderId.toString(), + importAsNewDocumentationVersion: false, + importFile : [ + fileName : 'FT Import', + fileType : MimeType.JSON_API.name, + fileContents: loadTestFile('simpleDataModel').toList() + ] + ]) + verifyResponse CREATED, response + def id = response.body().items[0].id + + expect: + id + + when: + GET("${id}/hierarchy", STRING_ARG) + + then: + verifyJsonResponse OK, '''{ + "childDataClasses": [ + { + "dataClasses": [], + "lastUpdated": "${json-unit.matches:offsetDateTime}", + "dataElements": [], + "domainType": "DataClass", + "availableActions": ["delete","show","update"], + "model": "${json-unit.matches:id}", + "id": "${json-unit.matches:id}", + "label": "simple", + "breadcrumbs": [ + { + "domainType": "DataModel", + "finalised": false, + "id": "${json-unit.matches:id}", + "label": "Simple Test DataModel" + } + ] + } + ], + "lastUpdated": "${json-unit.matches:offsetDateTime}", + "dataTypes": [], + "domainType": "DataModel", + "documentationVersion": "1.0.0", + "availableActions": ["delete","show","update"], + "branchName":"main", + "finalised": false, + "authority": { + "id": "${json-unit.matches:id}", + "url": "http://localhost", + "label": "Test Authority" + }, + "id": "${json-unit.matches:id}", + "label": "Simple Test DataModel", + "type": "Data Standard", + "readableByEveryone": false, + "readableByAuthenticatedUsers": false, + "classifiers": [ + { + "id": "${json-unit.matches:id}", + "label": "test classifier simple", + "lastUpdated": "${json-unit.matches:offsetDateTime}" + } + ] +}''' + + cleanup: + cleanUpData(id) + } + + void 'H02 : test getting complex DataModel hierarchy'() { + given: + POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ + finalised : false, + folderId : folderId.toString(), + importAsNewDocumentationVersion: false, + importFile : [ + fileName : 'FT Import', + fileType : MimeType.JSON_API.name, + fileContents: loadTestFile('complexDataModel').toList() + ] + ]) + verifyResponse CREATED, response + def id = response.body().items[0].id + + expect: + id + + when: + GET("${id}/hierarchy", STRING_ARG) + + then: + verifyJsonResponse OK, '''{ + "id": "${json-unit.matches:id}", + "domainType": "DataModel", + "label": "Complex Test DataModel", + "availableActions": [ + "delete", + "show", + "update" + ], + "branchName":"main", + "lastUpdated": "${json-unit.matches:offsetDateTime}", + "classifiers": [ + { + "id": "${json-unit.matches:id}", + "label": "test classifier2", + "lastUpdated": "${json-unit.matches:offsetDateTime}" + }, + { + "id": "${json-unit.matches:id}", + "label": "test classifier", + "lastUpdated": "${json-unit.matches:offsetDateTime}" + } + ], + "type": "Data Standard", + "documentationVersion": "1.0.0", + "finalised": false, + "authority": { + "id": "${json-unit.matches:id}", + "url": "http://localhost", + "label": "Test Authority" + }, "readableByEveryone": false, "readableByAuthenticatedUsers": false, "author": "admin person", @@ -3610,347 +4003,284 @@ class DataModelFunctionalSpec extends ResourceFunctionalSpec { ], "referenceClass": { "id": "${json-unit.matches:id}", - "domainType": "DataClass", - "label": "child", - "model": "${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}" + } + }, + "maxMultiplicity": 1, + "minMultiplicity": 1 + } + ] + } + ] +}''' + + cleanup: + cleanUpData(id) + } + + void 'test diffing 2 complex and simple DataModels'() { + given: + POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ + finalised : false, + folderId : folderId.toString(), + importAsNewDocumentationVersion: false, + importFile : [ + fileName : 'FT Import', + fileType : MimeType.JSON_API.name, + fileContents: loadTestFile('complexDataModel').toList() + ] + ]) + verifyResponse CREATED, response + String complexDataModelId = response.body().items[0].id + + POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ + finalised : false, + folderId : folderId.toString(), + importAsNewDocumentationVersion: false, + importFile : [ + fileName : 'FT Import', + fileType : MimeType.JSON_API.name, + fileContents: loadTestFile('simpleDataModel').toList() + ] + ]) + verifyResponse CREATED, response + String simpleDataModelId = response.body().items[0].id + + expect: + complexDataModelId + simpleDataModelId + + when: + GET("${complexDataModelId}/diff/${simpleDataModelId}", STRING_ARG) + + then: + verifyJsonResponse OK, '''{ + "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" + } + }, + { + "value": { + "id": "${json-unit.matches:id}", + "namespace": "test.com", + "key": "mdk1", + "value": "mdv1" + } + } + ], + "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" + } + } + ] + } + }, + { + "annotations": { + "deleted": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "test annotation 1" + } + }, + { + "value": { + "id": "${json-unit.matches:id}", + "label": "test annotation 2" + } + } + ] + } + }, + { + "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 + } + ] + } + }, + { + "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": "parent", - "domainType": "DataClass" + "label": "Complex Test DataModel", + "domainType": "DataModel", + "finalised": false } - ], - "parentDataClass": "${json-unit.matches:id}" + ] } - }, - "maxMultiplicity": 1, - "minMultiplicity": 1 - } - ] + } + ], + "created": [ + { + "value": { + "id": "${json-unit.matches:id}", + "label": "simple", + "breadcrumbs": [ + { + "id": "${json-unit.matches:id}", + "label": "Simple Test DataModel", + "domainType": "DataModel", + "finalised": false + } + ] + } + } + ] + } } ] }''' - cleanup: - cleanUpData(id) - } - - void 'test diffing 2 complex and simple DataModels'() { - given: - POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ - finalised : false, - folderId : folderId.toString(), - importAsNewDocumentationVersion: false, - importFile : [ - fileName : 'FT Import', - fileType : MimeType.JSON_API.name, - fileContents: loadTestFile('complexDataModel').toList() - ] - ]) - verifyResponse CREATED, response - String complexDataModelId = response.body().items[0].id - - POST('import/uk.ac.ox.softeng.maurodatamapper.datamodel.provider.importer/DataModelJsonImporterService/2.0', [ - finalised : false, - folderId : folderId.toString(), - importAsNewDocumentationVersion: false, - importFile : [ - fileName : 'FT Import', - fileType : MimeType.JSON_API.name, - fileContents: loadTestFile('simpleDataModel').toList() - ] - ]) - verifyResponse CREATED, response - String simpleDataModelId = response.body().items[0].id - - expect: - complexDataModelId - simpleDataModelId - - when: - GET("${complexDataModelId}/diff/${simpleDataModelId}", STRING_ARG) - - 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" - } - }, - { - "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" - } - } - ] - } - }, - { - "annotations": { - "deleted": [ - { - "value": { - "id": "${json-unit.matches:id}", - "label": "test annotation 1" - } - }, - { - "value": { - "id": "${json-unit.matches:id}", - "label": "test annotation 2" - } - } - ] - } - }, - { - "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 - } - ] - } - }, - { - "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 - } - ] - } - } - ] - } - }, - { - "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" - } - ] - } - } - ] - } - } - ] -}''' - cleanup: cleanUpData(complexDataModelId) cleanUpData(simpleDataModelId) 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 69dd4654d7..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 @@ -17,12 +17,21 @@ */ package uk.ac.ox.softeng.maurodatamapper.datamodel -import uk.ac.ox.softeng.maurodatamapper.core.diff.ObjectDiff +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 +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 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.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 import uk.ac.ox.softeng.maurodatamapper.datamodel.item.DataElement @@ -31,26 +40,28 @@ 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.Version +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 org.spockframework.util.Assert import spock.lang.PendingFeature -import spock.lang.Stepwise + +import java.util.function.Predicate @Slf4j @Integration @Rollback -@Stepwise class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { DataModel complexDataModel DataModel simpleDataModel DataModelService dataModelService DataClassService dataClassService + MetadataService metadataService @Override void setupDomainData() { @@ -238,7 +249,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'() { @@ -393,7 +404,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'() { @@ -563,7 +574,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'() { @@ -580,7 +591,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'() { @@ -742,7 +753,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'() { @@ -768,7 +779,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'() { @@ -933,383 +944,303 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { availableBranches.each {it.label == dataModel.label} } - void 'DMSM01 : test finding merge difference between two datamodels'() { given: setupData() - 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.addToDataClasses( - new DataClass(createdByUser: admin, label: 'existingClass') - .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteLeftOnlyFromExistingClass')) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'deleteRightOnlyFromExistingClass')) - ) - 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) + !mergeDiff.isEmpty() + mergeDiff.numberOfDiffs == 15 - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(draftId, 'deleteRightOnlyFromExistingClass')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(draftId, 'deleteRightOnly')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(draftId, 'deleteAndDelete')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(draftId, 'modifyAndDelete')) + then: 'branch name is not a diff' + !mergeDiff.find {it.fieldName == 'branchName'} - checkAndSave dataClassService.findByDataModelIdAndLabel(draftId, 'modifyRightOnly').tap { - description = 'Description' - } - checkAndSave dataClassService.findByDataModelIdAndLabel(draftId, 'deleteAndModify').tap { - description = 'Description' - } - checkAndSave dataClassService.findByDataModelIdAndLabel(draftId, 'modifyAndModifyReturningNoDifference').tap { - description = 'Description' - } - checkAndSave dataClassService.findByDataModelIdAndLabel(draftId, 'modifyAndModifyReturningDifference').tap { - description = 'DescriptionRight' - } + when: 'organisation is a non-conflicting change' + FieldMergeDiff stringFieldDiff = mergeDiff.find {it.fieldName == 'organisation'} - checkAndSave dataClassService.findByDataModelIdAndLabel(draftId, 'existingClass') - .addToDataClasses(new DataClass(createdByUser: admin, label: 'addRightToExistingClass')) + then: + stringFieldDiff.source == 'under test' + stringFieldDiff.target == null + stringFieldDiff.commonAncestor == null + !stringFieldDiff.isMergeConflict() - DataModel draftModel = dataModelService.get(draftId) + when: 'author is a conflicting change' + stringFieldDiff = mergeDiff.find {it.fieldName == 'author'} - 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) + then: + stringFieldDiff.source == 'harry' + stringFieldDiff.target == 'dick' + stringFieldDiff.commonAncestor == 'john' + stringFieldDiff.isMergeConflict() - checkAndSave dataModelService.get(draftId).tap { - description = 'DescriptionRight' - } + when: 'single array change in datatypes' + ArrayMergeDiff dataTypeDiff = mergeDiff.find {it.fieldName == 'dataTypes'} as ArrayMergeDiff - sessionFactory.currentSession.flush() - sessionFactory.currentSession.clear() + then: + dataTypeDiff.deleted.isEmpty() + dataTypeDiff.modified.isEmpty() + dataTypeDiff.created.size() == 1 + dataTypeDiff.created.first().createdIdentifier == 'addSourceOnlyOnlyChangeInArray' + !dataTypeDiff.created.first().isMergeConflict() + !dataTypeDiff.created.first().commonAncestor + !dataTypeDiff.created.first().target - UUID testId = createAndSaveNewBranchModel('test', dataModel) + when: 'metadata has array diffs' + ArrayMergeDiff metadataDiff = mergeDiff.find {it.fieldName == 'metadata'} as ArrayMergeDiff - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(testId, 'deleteLeftOnlyFromExistingClass')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(testId, 'deleteLeftOnly')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(testId, 'deleteAndDelete')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(testId, 'deleteAndModify')) + then: + metadataDiff.source.size() == 1 + metadataDiff.target.size() == 2 + metadataDiff.commonAncestor.size() == 2 + metadataDiff.created.isEmpty() + metadataDiff.deleted.size() == 1 + metadataDiff.deleted.first().deletedIdentifier == 'test.deleteSourceOnly' + metadataDiff.modified.size() == 1 + metadataDiff.modified.first().sourceIdentifier == 'test.modifySourceOnly' + 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 - checkAndSave dataClassService.findByDataModelIdAndLabel(testId, 'modifyLeftOnly').tap { - description = 'Description' - } - checkAndSave dataClassService.findByDataModelIdAndLabel(testId, 'modifyAndDelete').tap { - description = 'Description' - } - checkAndSave dataClassService.findByDataModelIdAndLabel(testId, 'modifyAndModifyReturningNoDifference').tap { - description = 'Description' - } - checkAndSave dataClassService.findByDataModelIdAndLabel(testId, 'modifyAndModifyReturningDifference').tap { - description = 'DescriptionLeft' - } + then: + dataClassesDiff.created.size() == 3 + dataClassesDiff.deleted.size() == 2 + dataClassesDiff.modified.size() == 4 - checkAndSave dataClassService.findByDataModelIdAndLabel(testId, 'existingClass') - .addToDataClasses(new DataClass(createdByUser: admin, label: 'addLeftToExistingClass')) + when: 'created on source side' + CreationMergeDiff creationMergeDiff = dataClassesDiff.created.find {it.createdIdentifier == 'addSourceOnly'} - DataModel testModel = dataModelService.get(testId) + then: + creationMergeDiff + creationMergeDiff.created + !creationMergeDiff.isMergeConflict() + !creationMergeDiff.commonAncestor - checkAndSave new DataClass(createdByUser: admin, label: 'leftParentDataClass', dataModel: testModel) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'leftChildDataClass', dataModel: testModel)) + when: 'created with nested creation on source side only' + creationMergeDiff = dataClassesDiff.created.find {it.createdIdentifier == 'addSourceWithNestedChild'} - 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) + then: + creationMergeDiff + creationMergeDiff.created //TODO more info in gson as we need to include the nested child + !creationMergeDiff.isMergeConflict() + !creationMergeDiff.commonAncestor - 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 == 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', - '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 - } + when: 'modified on source side and deleted on target side' + creationMergeDiff = dataClassesDiff.created.find {it.createdIdentifier == 'modifySourceAndDeleteTarget'} - void 'DMSM02 : test merging diff into draft model'() { - given: - setupData() + then: + creationMergeDiff + creationMergeDiff.created + creationMergeDiff.isMergeConflict() + creationMergeDiff.commonAncestor - 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: 'deleted on source side' + DeletionMergeDiff deleteSourceOnly = dataClassesDiff.deleted.find {it.deletedIdentifier == 'deleteSourceOnly'} + DeletionMergeDiff deleteAndModify = dataClassesDiff.deleted.find {it.deletedIdentifier == 'deleteSourceAndModifyTarget'} then: - dataModel.branchName == VersionAwareConstraints.DEFAULT_BRANCH_NAME + deleteSourceOnly + deleteSourceOnly.deleted + !deleteSourceOnly.isMergeConflict() + deleteSourceOnly.commonAncestor - when: - UUID draftId = createAndSaveNewBranchModel(VersionAwareConstraints.DEFAULT_BRANCH_NAME, dataModel) + and: + deleteAndModify + deleteAndModify.deleted + deleteAndModify.isMergeConflict() + deleteAndModify.commonAncestor + deleteAndModify.mergeModificationDiff - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(draftId, 'modifyAndDelete')) + and: + deleteAndModify.mergeModificationDiff //TODO more info - 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')) + when: 'additions on both with differences' + MergeDiff addBothReturningDifferenceMerge = dataClassesDiff.modified.find {it.sourceIdentifier == 'addBothReturningDifference'} - DataModel draftModel = dataModelService.get(draftId) - checkAndSave new DataClass(createdByUser: admin, label: 'addAndAddReturningDifference', description: 'right', dataModel: draftModel) + 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: 'modified on source side' + MergeDiff modifySourceOnlyMerge = dataClassesDiff.modified.find {it.sourceIdentifier == 'modifySourceOnly'} - checkAndSave dataModelService.get(draftId).tap { - description = 'DescriptionRight' - } + 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' - sessionFactory.currentSession.flush() - sessionFactory.currentSession.clear() - UUID testId = createAndSaveNewBranchModel('test', dataModel) + when: 'modified on both sides' + MergeDiff modifyBothNoDifferenceMerge = dataClassesDiff.modified.find {it.sourceIdentifier == 'modifyBothReturningNoDifference'} + MergeDiff modifyBothWithDifferenceMerge = dataClassesDiff.modified.find {it.sourceIdentifier == 'modifyBothReturningDifference'} - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(testId, 'deleteLeftOnlyFromExistingClass')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(testId, 'deleteLeftOnly')) - dataClassService.delete(dataClassService.findByDataModelIdAndLabel(testId, 'deleteAndModify')) + then: + !modifyBothNoDifferenceMerge - checkAndSave dataClassService.findByDataModelIdAndLabel(testId, 'modifyLeftOnly').tap { - description = 'Description' - } - checkAndSave dataClassService.findByDataModelIdAndLabel(testId, 'modifyAndDelete').tap { - description = 'Description' - } - checkAndSave dataClassService.findByDataModelIdAndLabel(testId, 'modifyAndModifyReturningDifference').tap { - description = 'DescriptionLeft' - } + 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' - checkAndSave dataClassService.findByDataModelIdAndLabel(testId, 'existingClass') - .addToDataClasses(new DataClass(createdByUser: admin, label: 'addLeftToExistingClass')) - DataModel testModel = dataModelService.get(testId) + when: 'nested changes made inside existing class' + MergeDiff existingClassMerge = dataClassesDiff.modified.find {it.sourceIdentifier == 'existingClass'} - checkAndSave new DataClass(createdByUser: admin, label: 'leftParentDataClass', dataModel: testModel) - .addToDataClasses(new DataClass(createdByUser: admin, label: 'leftChildDataClass', dataModel: testModel)) + 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' - checkAndSave new DataClass(createdByUser: admin, label: 'addLeftOnly', dataModel: testModel) - checkAndSave new DataClass(createdByUser: admin, label: 'addAndAddReturningDifference', description: 'left', dataModel: testModel) + when: + ArrayMergeDiff existingClassDiff = existingClassMerge.first() as ArrayMergeDiff - checkAndSave dataModelService.get(testId).tap { - description = 'DescriptionLeft' - } + 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 + } - 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 + void 'DMSM02 : test merging legacy diff into draft model'() { + given: + setupData() + + 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 == 15 when: - DataClass addLeftOnly = dataClassService.findByDataModelIdAndLabel(testId, 'addLeftOnly') - DataClass addAndAddReturningDifference = dataClassService.findByDataModelIdAndLabel(testId, 'addAndAddReturningDifference') - DataClass addLeftToExistingClass = dataClassService.findByDataModelIdAndLabel(testId, 'addLeftToExistingClass') - def patch = new MergeObjectDiffData( - leftId: draft.id, - rightId: test.id, + 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( + new LegacyFieldPatchData( fieldName: 'description', value: 'DescriptionLeft' ), - new MergeFieldDiffData( + new LegacyFieldPatchData( fieldName: 'dataClasses', deleted: [ - new MergeItemData( - id: dataClassService.findByParentAndLabel(draft, deleteAndModify.label).id, - label: deleteAndModify.label + new ItemPatchData( + id: dataClassService.findByParentAndLabel(rightMain, 'deleteSourceAndModifyTarget').id, + label: 'deleteSourceAndModifyTarget' ), - new MergeItemData( - id: dataClassService.findByParentAndLabel(draft, 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(test, 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, + new ObjectPatchData( + targetId: dataClassService.findByParentAndLabel(rightMain, 'addBothReturningDifference').id, + label: 'addBothReturningDifference', diffs: [ - new MergeFieldDiffData( + new LegacyFieldPatchData( fieldName: 'description', value: 'addedDescriptionSource' ) ] ), - new MergeObjectDiffData( - leftId: dataClassService.findByParentAndLabel(draft, existingClass.label).id, - label: existingClass.label, + new ObjectPatchData( + targetId: targetExistingClass.id, + label: 'existingClass', diffs: [ - new MergeFieldDiffData( + new LegacyFieldPatchData( fieldName: "dataClasses", - deleted: [ - new MergeItemData( - id: dataClassService.findByParentAndLabel( - dataClassService.findByParentAndLabel(draft, 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(draft, modifyAndModifyReturningDifference.label).id, - label: modifyAndModifyReturningDifference.label, + new ObjectPatchData( + targetId: dataClassService.findByParentAndLabel(rightMain, 'modifyBothReturningDifference').id, + label: 'modifyBothReturningDifference', diffs: [ - new MergeFieldDiffData( + new LegacyFieldPatchData( fieldName: 'description', - value: 'DescriptionLeft' + value: 'DescriptionSource' ), ] ), - new MergeObjectDiffData( - leftId: dataClassService.findByParentAndLabel(draft, "modifyLeftOnly").id, - label: "modifyLeftOnly", + new ObjectPatchData( + targetId: dataClassService.findByParentAndLabel(rightMain, 'modifySourceOnly').id, + label: 'modifySourceOnly', diffs: [ - new MergeFieldDiffData( + new LegacyFieldPatchData( fieldName: 'description', - value: 'Description' + value: 'DescriptionSource' ) ] @@ -1319,20 +1250,367 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { ) ] ) - def mergedModel = dataModelService.mergeObjectDiffIntoModel(patch, draft, adminSecurityPolicyManager) + then: + check(patch) + + when: + def mergedModel = dataModelService.mergeLegacyObjectPatchDataIntoModel(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' + } + + + 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: @@ -1610,6 +1888,5 @@ class DataModelServiceIntegrationSpec extends BaseDataModelIntegrationSpec { ele2Res ele2Res.size() == 0 } - } 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/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..df83030dff 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.path.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,123 @@ 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" + 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" @@ -239,10 +234,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,152 +251,173 @@ 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('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('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('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('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('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 } /* 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('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('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('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('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('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('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-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/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 ac6a30d393..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 @@ -18,7 +18,7 @@ package uk.ac.ox.softeng.maurodatamapper.datamodel.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.datamodel.DataModel import uk.ac.ox.softeng.maurodatamapper.datamodel.DataModelService @@ -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 { @@ -244,7 +242,12 @@ abstract class DataBindImportAndDefaultExporterServiceSpec 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/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/DataElementSpec.groovy b/mdm-plugin-datamodel/src/test/groovy/uk/ac/ox/softeng/maurodatamapper/datamodel/item/DataElementSpec.groovy index 0a8d300d49..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,8 @@ */ package uk.ac.ox.softeng.maurodatamapper.datamodel.item - +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 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() + } } 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-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..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 @@ -82,6 +82,16 @@ class SubscribedCatalogue implements SecurableResource, EditHistoryAware, Inform SubscribedCatalogue.simpleName } + @Override + String getPathPrefix() { + null + } + + @Override + String getPathIdentifier() { + null + } + @Override String getEditLabel() { "SubscribedCatalogue:${url}" 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..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 @@ -65,6 +65,16 @@ class SubscribedModel implements SecurableResource, EditHistoryAware { SubscribedModel.simpleName } + @Override + String getPathPrefix() { + null + } + + @Override + String getPathIdentifier() { + null + } + @Override String getEditLabel() { "SubscribedModel:${id}" 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 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-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-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 } 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 bf5050d342..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 @@ -19,7 +19,7 @@ package uk.ac.ox.softeng.maurodatamapper.referencedata 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 @@ -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 @@ -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 @@ -95,7 +95,7 @@ class ReferenceDataModel implements Model, ReferenceSummaryM ] static search = { - CallableSearch.call(StandardSearch, delegate) + CallableSearch.call(ModelSearch, delegate) } ReferenceDataModel() { @@ -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() } } @@ -144,7 +149,7 @@ class ReferenceDataModel implements Model, 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/facet/ReferenceSummaryMetadata.groovy b/mdm-plugin-referencedata/grails-app/domain/uk/ac/ox/softeng/maurodatamapper/referencedata/facet/ReferenceSummaryMetadata.groovy index 9db1512709..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 @@ -64,6 +63,16 @@ class ReferenceSummaryMetadata implements MultiFacetItemAware, InformationAware, ReferenceSummaryMetadata.simpleName } + @Override + String getPathPrefix() { + '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 953c1c9ab4..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 = { @@ -52,7 +56,17 @@ class ReferenceSummaryMetadataReport implements CreatorAware { @Override String getDomainType() { - ReferenceSummaryMetadata.simpleName + ReferenceSummaryMetadataReport.simpleName + } + + @Override + String getPathPrefix() { + 'rsmr' + } + + @Override + String getPathIdentifier() { + reportDate.withOffsetSameInstant(ZoneOffset.UTC).format(PATH_FORMATTER) } 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..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 @@ -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 @@ -110,6 +110,10 @@ class ReferenceDataElement implements ModelItem { } @Override - String getDiffIdentifier() { - this.id + String getPathPrefix() { + 'rdv' + } + + @Override + String getPathIdentifier() { + rowNumber } 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) 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 c65c03ac48..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 @@ -93,6 +93,11 @@ abstract class ReferenceDataType implements ModelItem, ReferenceDataType() { } + @Override + String getPathPrefix() { + 'rdt' + } + @Field(index = Index.YES, bridge = @FieldBridge(impl = UUIDBridge)) UUID getModelId() { referenceDataModel.id @@ -132,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/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..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 @@ -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 @@ -90,6 +90,10 @@ class ReferenceEnumerationValue implements ModelItem 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-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..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 @@ -89,4 +89,19 @@ class ReferenceSummaryMetadataService implements MultiFacetItemAwareService getBaseDeleteCriteria() { ReferenceSummaryMetadata.by() } + + @Override + ReferenceSummaryMetadata findByParentIdAndPathIdentifier(UUID parentId, String pathIdentifier) { + ReferenceSummaryMetadata.byMultiFacetAwareItemId(parentId).eq('label', pathIdentifier).get() + } + + @Override + ReferenceSummaryMetadata copy(ReferenceSummaryMetadata facetToCopy, MultiFacetAware multiFacetAwareItemToCopyInto) { + ReferenceSummaryMetadata copy = new ReferenceSummaryMetadata(summaryMetadataType: facetToCopy.summaryMetadataType, createdBy: facetToCopy.createdBy) + facetToCopy.summaryMetadataReports.each {smr -> + 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-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-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/_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/_fullReferenceDataModel.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/_referenceDataModel_full.gson similarity index 80% 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..8fe39ee985 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 @@ -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/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/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/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/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-referencedata/grails-app/views/referenceDataModel/mergeDiff.gson b/mdm-plugin-referencedata/grails-app/views/referenceDataModel/mergeDiff.gson index 5a9466efbd..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.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-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-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..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 @@ -21,13 +21,12 @@ 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 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/provider/JsonReferenceDataImporterExporterServiceSpec.groovy b/mdm-plugin-referencedata/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/referencedata/provider/JsonReferenceDataImporterExporterServiceSpec.groovy index 20385d5dea..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 @@ -195,7 +195,8 @@ class JsonReferenceDataImporterExporterServiceSpec extends BaseReferenceDataMode ObjectDiff diff = referenceDataModelService.getDiffForModels(referenceDataModelService.get(exampleReferenceDataModelId), imported) then: - diff.objectsAreIdentical() + diff.numberOfDiffs == 1 + diff.diffs.find {it.fieldName == 'rule'}.deleted.size() == 1 } void 'RDM03: test empty data import'() { 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 e9ce195ad4..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 @@ -197,7 +197,10 @@ class XmlReferenceDataImporterExporterServiceSpec extends BaseReferenceDataModel ObjectDiff diff = referenceDataModelService.getDiffForModels(referenceDataModelService.get(exampleReferenceDataModelId), imported) then: - diff.objectsAreIdentical() + diff.numberOfDiffs == 3 + diff.diffs.find { it.fieldName == 'rule' }.deleted.size() == 1 + diff.diffs.find { it.fieldName == 'referenceDataTypes' }.modified.first().diffs.first().deleted.size() == 1 + diff.diffs.find { it.fieldName == 'referenceDataElements' }.modified.first().diffs.first().deleted.size() == 1 } void 'RDM03: test empty data import'() { 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-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 00e77ded8c..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,7 +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.referencedata.ReferenceDataModelService +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 @@ -33,7 +33,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 @@ -51,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-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/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..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 @@ -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 @@ -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() { @@ -100,6 +100,11 @@ class CodeSet implements Model { 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 e6ad75c242..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 @@ -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 @@ -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() { @@ -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 87a7250af0..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 @@ -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 @@ -121,6 +121,16 @@ class Term implements ModelItem { Term.simpleName } + @Override + String getPathIdentifier() { + code + } + + @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 94638f5a28..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 @@ -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 @@ -89,6 +89,11 @@ class TermRelationshipType implements ModelItem { TermRelationship.simpleName } + @Override + String getPathPrefix() { + 'tr' + } + def beforeValidate() { label = relationshipType?.label beforeValidateModelItem() @@ -116,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) { @@ -176,6 +180,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 bfeee0e9d3..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 @@ -28,21 +28,20 @@ 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 -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.path.Path 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.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 @@ -54,7 +53,6 @@ class CodeSetService extends ModelService { TermRelationshipTypeService termRelationshipTypeService TermService termService TermRelationshipService termRelationshipService - PathService pathService CodeSetJsonImporterService codeSetJsonImporterService @Override @@ -203,20 +201,20 @@ class CodeSetService extends ModelService { } @Override - CodeSet mergeObjectDiffIntoModel(MergeObjectDiffData modelMergeObjectDiff, CodeSet targetModel, - UserSecurityPolicyManager userSecurityPolicyManager) { + CodeSet mergeLegacyObjectPatchDataIntoModel(ObjectPatchData objectPatchData, CodeSet targetModel, + UserSecurityPolicyManager userSecurityPolicyManager) { - if (!modelMergeObjectDiff.hasDiffs()) return targetModel + if (!objectPatchData.hasPatches()) return targetModel - modelMergeObjectDiff.getValidDiffs().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) } + ModelItemService modelItemService = modelItemServices.find {it.handles(mergeFieldDiff.fieldName)} if (modelItemService) { @@ -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.mergeObjectDiffIntoModelItem(mergeObjectDiffData, modelItem, targetModel, userSecurityPolicyManager) + modelItemService. + mergeLegacyObjectPatchDataIntoModelItem(mergeObjectDiffData, modelItem, targetModel, userSecurityPolicyManager) } } } else { @@ -447,14 +446,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/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/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..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) } @@ -187,4 +188,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..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 @@ -211,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) @@ -453,8 +453,24 @@ 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) { + // 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', legacyHandlingPathIdentifier).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..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 @@ -17,7 +17,9 @@ */ 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 @@ -86,7 +88,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() @@ -126,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) } @@ -203,4 +206,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-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/_fullCodeSet.gson b/mdm-plugin-terminology/grails-app/views/codeSet/_codeSet_full.gson similarity index 69% 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..f877a103f8 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 @@ -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/_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/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/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/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/codeSet/mergeDiff.gson b/mdm-plugin-terminology/grails-app/views/codeSet/mergeDiff.gson index 0fcb90cd72..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.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/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/_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/_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/_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/_fullTerminology.gson b/mdm-plugin-terminology/grails-app/views/terminology/_terminology_full.gson similarity index 75% 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..44a91395be 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 @@ -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/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/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/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 diff --git a/mdm-plugin-terminology/grails-app/views/terminology/mergeDiff.gson b/mdm-plugin-terminology/grails-app/views/terminology/mergeDiff.gson index 4341b7e87f..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.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 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-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 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 50% 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..62cffa1ecd 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,30 @@ * * 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.path.Path 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.util.Version +import uk.ac.ox.softeng.maurodatamapper.terminology.test.BaseTerminologyIntegrationSpec +import uk.ac.ox.softeng.maurodatamapper.version.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.Stepwise import java.time.OffsetDateTime import java.time.ZoneOffset @Slf4j -@Stepwise -class TerminologyPathServiceSpec extends CatalogueItemServiceSpec implements ServiceUnitTest { - - UserSecurityPolicyManager userSecurityPolicyManager +@Integration +@Rollback +class TerminologyPathServiceSpec extends BaseTerminologyIntegrationSpec { + + PathService pathService Terminology terminology1 Term terminology1_term1 @@ -59,7 +55,7 @@ class TerminologyPathServiceSpec extends CatalogueItemServiceSpec implements Ser Terminology terminology4notFinalised Terminology terminology5first - Terminology terminology5second + Terminology terminology5second CodeSet codeSet1 @@ -93,14 +89,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 +114,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 +127,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 +145,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 +163,203 @@ 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('') + 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('') + 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("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: + catalogueItem.label == terminology1_term1.label + catalogueItem.domainType == "Term" + catalogueItem.id == terminology1_term1.id + + /* + Terminology 1 Term 1 + */ + when: 'using the label' + path = Path.from("tm:${terminology1_term1.label}") + 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 2 */ when: - params = ['catalogueItemDomainType': 'terminologies', 'catalogueItemId': terminology1.id.toString(), 'path': "te:|tm:${terminology1_term2.label}"] - catalogueItem = service.findCatalogueItemByPath(PublicAccessSecurityPolicyManager.instance, params) + path = Path.from("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("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("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("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: catalogueItem == null @@ -344,172 +368,179 @@ 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("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('') + 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('') + 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("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("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("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 + + when: + path = Path.from("cs:${codeSet2.pathIdentifier}|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 /* 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("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("tm:${terminology2_term1.pathIdentifier}") + catalogueItem = pathService.findResourceByPathFromRootResource(codeSet1, path) as CatalogueItem then: catalogueItem == null @@ -518,72 +549,129 @@ 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("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 } 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 == 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.equals(terminology4notFinalised.id) - catalogueItem.description.equals(terminology4notFinalised.description) + catalogueItem.id == terminology4finalised.id - } + } - void "test get Terminology by path when there are two finalised versions"() { - Map params + void "FV : test get Terminology by path when there are two finalised versions"() { + 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.equals(terminology5second.id) - catalogueItem.description.equals(terminology5second.description) + 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 == 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 == 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 74208c0a12..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 @@ -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 @@ -167,216 +167,209 @@ 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": 8, + "diffs": [ + { + "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 + } + } + ] + } + ] + } + } + ] }''' } @@ -1218,13 +1211,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 +1252,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 c9ae3b944d..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 @@ -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 @@ -165,7 +165,7 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { "leftId": "${json-unit.matches:id}", "rightId": "${json-unit.matches:id}", "label": "Functional Test Model", - "count": 18, + "count": 17, "diffs": [ { "description": { @@ -176,10 +176,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 +258,7 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "Terminology", - "finalised": false + "finalised": true } ] }, @@ -221,7 +285,7 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { "id": "${json-unit.matches:id}", "label": "Functional Test Model", "domainType": "Terminology", - "finalised": false + "finalised": true } ] }, @@ -232,7 +296,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 +311,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 +321,10 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { } ] }, - "isMergeConflict": true, - "commonAncestorValue": { + "isMergeConflict": false + }, + { + "value": { "id": "${json-unit.matches:id}", "label": "MAD: modifyAndDelete", "breadcrumbs": [ @@ -266,38 +332,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 +369,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 +393,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 +419,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 +432,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 +445,8 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { { "description": { "left": null, - "right": "NewDescription" + "right": "NewDescription", + "isMergeConflict": false } } ] @@ -418,13 +459,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 +473,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 +490,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 +503,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 +516,8 @@ class TerminologyFunctionalSpec extends ResourceFunctionalSpec { { "description": { "left": null, - "right": "NewDescription" + "right": "NewDescription", + "isMergeConflict": false } } ] @@ -559,7 +569,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 +584,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 +605,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 + } + ] } } ] @@ -1311,7 +1308,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"]) @@ -1357,7 +1354,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) @@ -1384,7 +1381,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) @@ -1433,7 +1430,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) @@ -1502,23 +1499,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 @@ -1612,14 +1609,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 @@ -1643,13 +1640,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-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..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 @@ -17,10 +17,11 @@ */ 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 -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 @@ -30,7 +31,6 @@ import org.spockframework.util.Assert @Slf4j @Integration @Rollback -//@Stepwise class TerminologyServiceIntegrationSpec extends BaseTerminologyIntegrationSpec { @Override @@ -357,15 +357,10 @@ 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].commonAncestorValue == VersionAwareConstraints.DEFAULT_BRANCH_NAME + mergeDiff.size() == 0 } } 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 6d4dd91f94..328fe323f4 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 @@ -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 { @@ -160,7 +158,27 @@ abstract class DataBindTerminologyImportAndDefaultExporterServiceSpec extends BaseImportExportTerminologySpec { abstract K getImporterService() 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..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 @@ -29,7 +30,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 @@ -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-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..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 @@ -138,6 +138,16 @@ class CatalogueUser implements Principal, EditHistoryAware, CreatorAware, User { CatalogueUser.simpleName } + @Override + String getPathPrefix() { + '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 b4ea0c9c59..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 @@ -72,6 +72,16 @@ class UserGroup implements EditHistoryAware, SecurableResource, Principal { UserGroup.simpleName } + @Override + String getPathPrefix() { + 'ug' + } + + @Override + String getPathIdentifier() { + name + } + @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..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 @@ -114,6 +114,16 @@ class GroupRole implements EditHistoryAware, PathAware, SecurableResource, Compa GroupRole.simpleName } + @Override + String getPathPrefix() { + '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 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-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)) } 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) 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..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 @@ -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 << FINALISED_EDIT_ACTIONS } 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 << 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 60346972bd..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,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 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] @@ -90,4 +92,6 @@ class ResourceActions { public static final List USER_ADMIN_ACTIONS = [SHOW_ACTION, UPDATE_ACTION, DISABLE_ACTION] + + } 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..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 @@ -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 @@ -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 3dbed0a3b9..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 @@ -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 @@ -76,6 +76,10 @@ class BasicModelItem implements ModelItem, GormEntit BasicModelItem.simpleName } + @Override + String getPathPrefix() { + 'bmi' + } @Override String getEditLabel() { diff --git a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Mergeable.groovy b/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/functional/TestMergeData.groovy similarity index 75% rename from mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Mergeable.groovy rename to mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/functional/TestMergeData.groovy index da57381759..2c67de9806 100644 --- a/mdm-core/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/core/diff/Mergeable.groovy +++ b/mdm-testing-framework/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/test/functional/TestMergeData.groovy @@ -15,13 +15,17 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package uk.ac.ox.softeng.maurodatamapper.core.diff +package uk.ac.ox.softeng.maurodatamapper.test.functional -import grails.compiler.GrailsCompileStatic +/** + * @since 26/07/2021 + */ +class TestMergeData { -@GrailsCompileStatic -abstract class Mergeable { + String source + String target + String commonAncestor - Boolean isMergeConflict - T commonAncestorValue -} \ No newline at end of file + Map sourceMap + Map targetMap +} 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/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..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 @@ -18,10 +18,10 @@ 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 +import uk.ac.ox.softeng.maurodatamapper.version.Version import groovy.util.logging.Slf4j import org.spockframework.util.InternalSpockError 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/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' } + + } +} + 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..abd6c32e2c 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,8 +24,9 @@ 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.util.VersionChangeType +import uk.ac.ox.softeng.maurodatamapper.version.VersionChangeType import grails.gorm.transactions.Transactional import grails.testing.mixin.integration.Integration @@ -35,9 +36,9 @@ 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.nio.charset.Charset import java.util.regex.Pattern import static io.micronaut.http.HttpStatus.BAD_REQUEST @@ -45,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 /** *
@@ -63,7 +65,6 @@ import static io.micronaut.http.HttpStatus.OK
  */
 @Integration
 @Slf4j
-@Stepwise
 class VersionedFolderFunctionalSpec extends UserAccessAndPermissionChangingFunctionalSpec {
 
 
@@ -1711,6 +1712,437 @@ 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 versioned folders (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)
+    }
+
+    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
@@ -1805,6 +2237,7 @@ class VersionedFolderFunctionalSpec extends UserAccessAndPermissionChangingFunct
             'newForkModel',
             'comment',
             'newModelVersion',
+            'finalisedEditActions',
             'newDocumentationVersion',
             'newBranchModelVersion',
             'softDelete',
@@ -2131,4 +2564,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
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..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
@@ -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,384 @@ 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}" - }''' - } + 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 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 - }''' - } + then: "The response is Not Found" + verifyNotFound(response, 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}" - }''' - } + //With ID (no access to the ID'd codeset) + when: + node = makePathNode('tm', SIMPLE_CODESET_NAME) + GET("/api/codeSets/${getSimpleCodeSetId()}/path/${makePath(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 - } - ] - }''' + then: "The response is Not Found" + verifyNotFound(response, getSimpleCodeSetId(), CodeSet) } - 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}" - } - }''' - } - - 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('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) - - 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 + verifyNotFound(response, node) - //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('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('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 'Get DataModel by path and ID when not logged in'() { - String node - + void 'DM01 : Get DataModel by path and ID when not logged in'() { //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,329 +475,649 @@ 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 'PP : 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 + } + } + + 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", + "finalisedReadActions", + "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}" + } + }''' + } + } 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() { 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 39b04829ec..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 @@ -3006,277 +3006,225 @@ class DataModelFunctionalSpec extends ModelUserAccessPermissionChangingAndVersio String getExpectedDiffJson() { '''{ - "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": 18, + "diffs": [ + { + "label": { + "left": "Complex Test DataModel", + "right": "Simple Test DataModel" + } + }, + { + "metadata": { + "deleted": [ + { + "value": { + "id": "${json-unit.matches:id}", + "namespace": "test.com", + "key": "mdk1", + "value": "mdv1" } - }, - { - "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/test", + "key": "mdk1", + "value": "mdv2" } - }, - { - "annotations": { - "deleted": [ - { - "value": { - "id": "${json-unit.matches:id}", - "label": "test annotation 2" - } - }, - { - "value": { - "id": "${json-unit.matches:id}", - "label": "test annotation 1" - } - } - ] + } + ], + "created": [ + { + "value": { + "id": "${json-unit.matches:id}", + "namespace": "test.com/simple", + "key": "mdk2", + "value": "mdv2" } - }, - { - "author": { - "left": "admin person", - "right": null + }, + { + "value": { + "id": "${json-unit.matches:id}", + "namespace": "test.com/simple", + "key": "mdk1", + "value": "mdv1" } - }, - { - "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": "integer", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - } - ] - } - }, - { - "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": "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": "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 - } - ] - } - }, - { - "value": { - "id": "${json-unit.matches:id}", - "label": "content", - "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 - } - ] - } - } - ] + } + ] + } + }, + { + "rule": { + "deleted": [ + { + "value": { + "id": "${json-unit.matches:id}" } - }, - { - "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": "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": "child", - "breadcrumbs": [ - { - "id": "${json-unit.matches:id}", - "label": "Complex Test DataModel", - "domainType": "DataModel", - "finalised": false - }, - { - "id": "${json-unit.matches:id}", - "label": "parent", - "domainType": "DataClass" - } - ] - } - } - ] + } + ] + } + }, + { + "author": { + "left": "admin person", + "right": null + } + }, + { + "organisation": { + "left": "brc", + "right": null + } + }, + { + "dataTypes": { + "deleted": [ + { + "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": "string", + "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": "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 + } + ] + } + }, + { + "value": { + "id": "${json-unit.matches:id}", + "label": "content", + "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 + } + ] + } + } + ] + } + } + ] }''' } } \ No newline at end of file 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:
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/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/terminology/TerminologyFunctionalSpec.groovy b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/terminology/TerminologyFunctionalSpec.groovy
index 25160dbd86..3a7905f3f4 100644
--- a/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/terminology/TerminologyFunctionalSpec.groovy
+++ b/mdm-testing-functional/src/integration-test/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/terminology/TerminologyFunctionalSpec.groovy
@@ -907,7 +907,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
   "leftId": "${json-unit.matches:id}",
   "rightId": "${json-unit.matches:id}",
   "label": "Complex Test Terminology",
-  "count": 111,
+  "count": 112,
   "diffs": [
     {
       "label": {
@@ -921,13 +921,24 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "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"
+            }
+          }
+        ]
+      }
+    },
+    {
+      "rule": {
+        "deleted": [
+          {
+            "value": {
+              "id": "${json-unit.matches:id}"
             }
           }
         ]
@@ -939,7 +950,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT9: Complex Test Term 9",
+              "label": "CTT53: Complex Test Term 53",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -953,7 +964,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT16: Complex Test Term 16",
+              "label": "CTT44: Complex Test Term 44",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -967,7 +978,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT57: Complex Test Term 57",
+              "label": "CTT70: Complex Test Term 70",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -981,7 +992,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT92: Complex Test Term 92",
+              "label": "CTT2: Complex Test Term 2",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -995,7 +1006,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT14: Complex Test Term 14",
+              "label": "CTT88: Complex Test Term 88",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1009,7 +1020,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT25: Complex Test Term 25",
+              "label": "CTT20: Complex Test Term 20",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1023,7 +1034,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT46: Complex Test Term 46",
+              "label": "CTT42: Complex Test Term 42",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1037,7 +1048,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT38: Complex Test Term 38",
+              "label": "CTT91: Complex Test Term 91",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1051,7 +1062,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT24: Complex Test Term 24",
+              "label": "CTT81: Complex Test Term 81",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1065,7 +1076,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT11: Complex Test Term 11",
+              "label": "CTT1: Complex Test Term 1",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1079,7 +1090,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT15: Complex Test Term 15",
+              "label": "CTT31: Complex Test Term 31",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1093,7 +1104,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT61: Complex Test Term 61",
+              "label": "CTT39: Complex Test Term 39",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1107,7 +1118,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT88: Complex Test Term 88",
+              "label": "CTT22: Complex Test Term 22",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1121,7 +1132,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT71: Complex Test Term 71",
+              "label": "CTT41: Complex Test Term 41",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1135,7 +1146,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT6: Complex Test Term 6",
+              "label": "CTT49: Complex Test Term 49",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1149,7 +1160,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT78: Complex Test Term 78",
+              "label": "CTT63: Complex Test Term 63",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1163,7 +1174,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT13: Complex Test Term 13",
+              "label": "CTT100: Complex Test Term 100",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1177,7 +1188,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT42: Complex Test Term 42",
+              "label": "CTT13: Complex Test Term 13",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1205,7 +1216,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT19: Complex Test Term 19",
+              "label": "CTT92: Complex Test Term 92",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1219,7 +1230,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT39: Complex Test Term 39",
+              "label": "CTT101",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1233,7 +1244,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT91: Complex Test Term 91",
+              "label": "CTT58: Complex Test Term 58",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1247,7 +1258,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT48: Complex Test Term 48",
+              "label": "CTT61: Complex Test Term 61",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1261,7 +1272,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT89: Complex Test Term 89",
+              "label": "CTT54: Complex Test Term 54",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1275,7 +1286,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT83: Complex Test Term 83",
+              "label": "CTT48: Complex Test Term 48",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1289,7 +1300,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT73: Complex Test Term 73",
+              "label": "CTT56: Complex Test Term 56",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1303,7 +1314,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT49: Complex Test Term 49",
+              "label": "CTT21: Complex Test Term 21",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1317,7 +1328,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT33: Complex Test Term 33",
+              "label": "CTT30: Complex Test Term 30",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1331,7 +1342,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT80: Complex Test Term 80",
+              "label": "CTT25: Complex Test Term 25",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1345,7 +1356,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT87: Complex Test Term 87",
+              "label": "CTT74: Complex Test Term 74",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1359,7 +1370,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT75: Complex Test Term 75",
+              "label": "CTT11: Complex Test Term 11",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1373,7 +1384,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT32: Complex Test Term 32",
+              "label": "CTT73: Complex Test Term 73",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1387,7 +1398,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT20: Complex Test Term 20",
+              "label": "CTT99: Complex Test Term 99",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1401,7 +1412,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT2: Complex Test Term 2",
+              "label": "CTT62: Complex Test Term 62",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1415,7 +1426,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT69: Complex Test Term 69",
+              "label": "CTT19: Complex Test Term 19",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1429,7 +1440,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT63: Complex Test Term 63",
+              "label": "CTT51: Complex Test Term 51",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1443,7 +1454,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT53: Complex Test Term 53",
+              "label": "CTT83: Complex Test Term 83",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1457,7 +1468,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT77: Complex Test Term 77",
+              "label": "CTT47: Complex Test Term 47",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1471,7 +1482,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT1: Complex Test Term 1",
+              "label": "CTT36: Complex Test Term 36",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1485,7 +1496,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT100: Complex Test Term 100",
+              "label": "CTT29: Complex Test Term 29",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1499,7 +1510,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT54: Complex Test Term 54",
+              "label": "CTT86: Complex Test Term 86",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1513,7 +1524,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT65: Complex Test Term 65",
+              "label": "CTT16: Complex Test Term 16",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1527,7 +1538,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT28: Complex Test Term 28",
+              "label": "CTT90: Complex Test Term 90",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1541,7 +1552,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT74: Complex Test Term 74",
+              "label": "CTT67: Complex Test Term 67",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1555,7 +1566,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT82: Complex Test Term 82",
+              "label": "CTT60: Complex Test Term 60",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1569,7 +1580,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT22: Complex Test Term 22",
+              "label": "CTT3: Complex Test Term 3",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1583,7 +1594,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT4: Complex Test Term 4",
+              "label": "CTT23: Complex Test Term 23",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1597,7 +1608,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT51: Complex Test Term 51",
+              "label": "CTT40: Complex Test Term 40",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1611,7 +1622,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT67: Complex Test Term 67",
+              "label": "CTT78: Complex Test Term 78",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1625,7 +1636,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT84: Complex Test Term 84",
+              "label": "CTT38: Complex Test Term 38",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1639,7 +1650,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT5: Complex Test Term 5",
+              "label": "CTT12: Complex Test Term 12",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1653,7 +1664,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT36: Complex Test Term 36",
+              "label": "CTT17: Complex Test Term 17",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1667,7 +1678,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT95: Complex Test Term 95",
+              "label": "CTT66: Complex Test Term 66",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1681,7 +1692,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT31: Complex Test Term 31",
+              "label": "CTT27: Complex Test Term 27",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1695,7 +1706,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT00: Complex Test Term 00",
+              "label": "CTT14: Complex Test Term 14",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1709,7 +1720,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT30: Complex Test Term 30",
+              "label": "CTT18: Complex Test Term 18",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1723,7 +1734,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT18: Complex Test Term 18",
+              "label": "CTT95: Complex Test Term 95",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1737,7 +1748,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT68: Complex Test Term 68",
+              "label": "CTT28: Complex Test Term 28",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1751,7 +1762,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT29: Complex Test Term 29",
+              "label": "CTT80: Complex Test Term 80",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1765,7 +1776,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT10: Complex Test Term 10",
+              "label": "CTT59: Complex Test Term 59",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1779,7 +1790,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT60: Complex Test Term 60",
+              "label": "CTT94: Complex Test Term 94",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1793,7 +1804,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT94: Complex Test Term 94",
+              "label": "CTT26: Complex Test Term 26",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1807,7 +1818,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT45: Complex Test Term 45",
+              "label": "CTT15: Complex Test Term 15",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1821,7 +1832,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT76: Complex Test Term 76",
+              "label": "CTT24: Complex Test Term 24",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1835,7 +1846,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT8: Complex Test Term 8",
+              "label": "CTT65: Complex Test Term 65",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1849,7 +1860,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT79: Complex Test Term 79",
+              "label": "CTT43: Complex Test Term 43",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1863,7 +1874,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT64: Complex Test Term 64",
+              "label": "CTT72: Complex Test Term 72",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1877,7 +1888,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT96: Complex Test Term 96",
+              "label": "CTT46: Complex Test Term 46",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1891,7 +1902,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT23: Complex Test Term 23",
+              "label": "CTT64: Complex Test Term 64",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1905,7 +1916,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT43: Complex Test Term 43",
+              "label": "CTT85: Complex Test Term 85",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1919,7 +1930,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT98: Complex Test Term 98",
+              "label": "CTT96: Complex Test Term 96",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1933,7 +1944,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT26: Complex Test Term 26",
+              "label": "CTT33: Complex Test Term 33",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1947,7 +1958,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT35: Complex Test Term 35",
+              "label": "CTT10: Complex Test Term 10",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1961,7 +1972,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT34: Complex Test Term 34",
+              "label": "CTT97: Complex Test Term 97",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1975,7 +1986,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT27: Complex Test Term 27",
+              "label": "CTT55: Complex Test Term 55",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -1989,7 +2000,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT7: Complex Test Term 7",
+              "label": "CTT98: Complex Test Term 98",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2003,7 +2014,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT85: Complex Test Term 85",
+              "label": "CTT68: Complex Test Term 68",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2017,7 +2028,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT86: Complex Test Term 86",
+              "label": "CTT82: Complex Test Term 82",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2031,7 +2042,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT41: Complex Test Term 41",
+              "label": "CTT00: Complex Test Term 00",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2045,7 +2056,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT40: Complex Test Term 40",
+              "label": "CTT5: Complex Test Term 5",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2059,7 +2070,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT58: Complex Test Term 58",
+              "label": "CTT75: Complex Test Term 75",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2073,7 +2084,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT52: Complex Test Term 52",
+              "label": "CTT71: Complex Test Term 71",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2087,7 +2098,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT59: Complex Test Term 59",
+              "label": "CTT7: Complex Test Term 7",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2101,7 +2112,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT62: Complex Test Term 62",
+              "label": "CTT35: Complex Test Term 35",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2115,7 +2126,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT81: Complex Test Term 81",
+              "label": "CTT37: Complex Test Term 37",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2129,7 +2140,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT37: Complex Test Term 37",
+              "label": "CTT32: Complex Test Term 32",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2143,7 +2154,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT12: Complex Test Term 12",
+              "label": "CTT8: Complex Test Term 8",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2157,7 +2168,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT17: Complex Test Term 17",
+              "label": "CTT4: Complex Test Term 4",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2171,7 +2182,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT72: Complex Test Term 72",
+              "label": "CTT34: Complex Test Term 34",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2185,7 +2196,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT90: Complex Test Term 90",
+              "label": "CTT57: Complex Test Term 57",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2199,7 +2210,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT3: Complex Test Term 3",
+              "label": "CTT76: Complex Test Term 76",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2213,7 +2224,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT56: Complex Test Term 56",
+              "label": "CTT6: Complex Test Term 6",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2227,7 +2238,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT47: Complex Test Term 47",
+              "label": "CTT77: Complex Test Term 77",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2241,7 +2252,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT97: Complex Test Term 97",
+              "label": "CTT52: Complex Test Term 52",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2255,7 +2266,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT55: Complex Test Term 55",
+              "label": "CTT45: Complex Test Term 45",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2269,7 +2280,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT66: Complex Test Term 66",
+              "label": "CTT93: Complex Test Term 93",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2283,7 +2294,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT44: Complex Test Term 44",
+              "label": "CTT87: Complex Test Term 87",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2297,7 +2308,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT93: Complex Test Term 93",
+              "label": "CTT9: Complex Test Term 9",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2311,7 +2322,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT99: Complex Test Term 99",
+              "label": "CTT79: Complex Test Term 79",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2325,7 +2336,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT21: Complex Test Term 21",
+              "label": "CTT89: Complex Test Term 89",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2339,7 +2350,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT70: Complex Test Term 70",
+              "label": "CTT84: Complex Test Term 84",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2353,7 +2364,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "CTT101",
+              "label": "CTT69: Complex Test Term 69",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2369,7 +2380,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "STT01: Simple Test Term 01",
+              "label": "STT02: Simple Test Term 02",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2383,7 +2394,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "STT02: Simple Test Term 02",
+              "label": "STT01: Simple Test Term 01",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2417,7 +2428,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "is-a-part-of",
+              "label": "is-a",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
@@ -2445,7 +2456,7 @@ class TerminologyFunctionalSpec extends ModelUserAccessPermissionChangingAndVers
           {
             "value": {
               "id": "${json-unit.matches:id}",
-              "label": "is-a",
+              "label": "is-a-part-of",
               "breadcrumbs": [
                 {
                   "id": "${json-unit.matches:id}",
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/ModelUserAccessPermissionChangingAndVersioningFunctionalSpec.groovy b/mdm-testing-functional/src/main/groovy/uk/ac/ox/softeng/maurodatamapper/testing/functional/ModelUserAccessPermissionChangingAndVersioningFunctionalSpec.groovy index fb81531f79..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 @@ -22,8 +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.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 @@ -118,6 +117,7 @@ abstract class ModelUserAccessPermissionChangingAndVersioningFunctionalSpec exte 'show', 'createNewVersions', 'newForkModel', + 'finalisedReadActions' ].sort() } @@ -131,7 +131,9 @@ abstract class ModelUserAccessPermissionChangingAndVersioningFunctionalSpec exte 'newDocumentationVersion', 'newBranchModelVersion', 'softDelete', - 'delete' + 'delete', + 'finalisedEditActions' + ].sort() } @@ -1580,7 +1582,7 @@ abstract class ModelUserAccessPermissionChangingAndVersioningFunctionalSpec exte then: verifyResponse(OK, response) response.body().readableByEveryone == true - response.body().availableActions == ['show'] + response.body().availableActions == ['finalisedReadActions', 'show'] when: 'removing readable by everyone' loginEditor() 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 dd969c3c05..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 @@ -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,7 +32,7 @@ import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus import org.apache.commons.lang3.NotImplementedException import org.junit.Assert -import spock.lang.Stepwise +import org.springframework.beans.factory.annotation.Autowired import static uk.ac.ox.softeng.maurodatamapper.util.GormUtils.checkAndSave @@ -49,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 { @@ -274,11 +275,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 +284,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'] } diff --git a/run-all-tests.sh b/run-all-tests.sh index 01299d1005..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,50 @@ 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 jacocoTestReport +./gradlew --build-cache rootTestReport +#./gradlew --build-cache jacocoTestReport #./gradlew --build-cache staticCodeAnalysis \ No newline at end of file