From 9d66325caae63e0b8f185f2a324d53663a0c6edd Mon Sep 17 00:00:00 2001 From: edcrichton Date: Thu, 9 Oct 2025 17:07:08 +0100 Subject: [PATCH] Path ensures that only Model path nodes may have a modelIdentifier Methods that replace the functionality from 'PathStringUtils': finding the last PathNode with a prefix, getting the last PathNode, getModelIdentifier Path.setModelIdentifier sets the Model Identifier of the first Model PathNode in the Path PathNode holds a list of prefixes that may hold modelIdentifier Path.setModelIdentifier recreates the pathString Updated PathControllerIntegrationSpec: pathApi.getResourceByPath must use a valid path string rather than just a label PathMethodsTest replicates all of the correct tests from PathStringUtils using Path instead PathService uses Path methods in place of PathStringUtils methods, and Path in place of pathString --- .../service/dataflow/DataflowService.groovy | 5 +- .../maurodata/service/path/PathService.groovy | 63 ++++++------------- .../path/PathControllerIntegrationSpec.groovy | 8 +-- .../org/maurodata/domain/model/Path.groovy | 41 ++++++++++++ .../org/maurodata/util/PathStringUtils.groovy | 59 ----------------- .../PathMethodsTest.groovy} | 32 +++++----- 6 files changed, 84 insertions(+), 124 deletions(-) delete mode 100644 mauro-domain/src/main/groovy/org/maurodata/util/PathStringUtils.groovy rename mauro-domain/src/test/groovy/org/maurodata/test/domain/{util/PathStringUtilsTest.groovy => model/PathMethodsTest.groovy} (57%) diff --git a/mauro-api/src/main/groovy/org/maurodata/service/dataflow/DataflowService.groovy b/mauro-api/src/main/groovy/org/maurodata/service/dataflow/DataflowService.groovy index 9431be1de..8518fe625 100644 --- a/mauro-api/src/main/groovy/org/maurodata/service/dataflow/DataflowService.groovy +++ b/mauro-api/src/main/groovy/org/maurodata/service/dataflow/DataflowService.groovy @@ -27,7 +27,6 @@ import org.maurodata.security.AccessControlService import org.maurodata.service.core.AdministeredItemService import org.maurodata.service.path.PathService import org.maurodata.service.plugin.PluginService -import org.maurodata.util.PathStringUtils import org.maurodata.utils.importer.ImporterUtils @CompileStatic @@ -136,7 +135,7 @@ class DataflowService extends AdministeredItemService { protected List getImportDataClass(List dataClasses, DataModel model) { return dataClasses.collect {dC -> - String resourceLabel = PathStringUtils.getItemSubPath(dC.pathPrefix, dC.path.pathString) + String resourceLabel = dC.path.findLastPathNodeByPrefix(dC.pathPrefix).identifier DataClass importDataClass = findImportDataClassByLabel(resourceLabel, model) pathRepository.readParentItems(importDataClass) //perhaps not necessary to cal the full path but doing it anyway importDataClass.updatePath() @@ -165,7 +164,7 @@ class DataflowService extends AdministeredItemService { protected List getImportDataElements(List dataElements, DataModel model) { return dataElements.collect {dE -> - String resourceLabel = PathStringUtils.getItemSubPath(dE.pathPrefix, dE.path.pathString) + String resourceLabel = dE.path.findLastPathNodeByPrefix(dE.pathPrefix).identifier List importElementsWithSameLabel = model.dataElements.findAll { it.label == resourceLabel } diff --git a/mauro-api/src/main/groovy/org/maurodata/service/path/PathService.groovy b/mauro-api/src/main/groovy/org/maurodata/service/path/PathService.groovy index 94bff6221..389a96e96 100644 --- a/mauro-api/src/main/groovy/org/maurodata/service/path/PathService.groovy +++ b/mauro-api/src/main/groovy/org/maurodata/service/path/PathService.groovy @@ -13,14 +13,8 @@ import org.maurodata.domain.security.Role import org.maurodata.persistence.cache.AdministeredItemCacheableRepository import org.maurodata.persistence.model.PathRepository import org.maurodata.security.AccessControlService +import org.maurodata.domain.model.Path -import static org.maurodata.util.PathStringUtils.getCOLON -import static org.maurodata.util.PathStringUtils.getDISCARD_AFTER_VERSION -import static org.maurodata.util.PathStringUtils.getItemSubPath -import static org.maurodata.util.PathStringUtils.getREMOVE_VERSION_DELIM -import static org.maurodata.util.PathStringUtils.getVersionFromPath -import static org.maurodata.util.PathStringUtils.lastSubPath -import static org.maurodata.util.PathStringUtils.splitBy @CompileStatic @Slf4j @@ -34,7 +28,8 @@ class PathService implements AdministeredItemReader { @Inject AccessControlService accessControlService - AdministeredItem getResourceByPath(String domainType, String path) { + AdministeredItem getResourceByPath(String domainType, String pathString) { + Path path = new Path(pathString) AdministeredItem item = findResourceByPath(domainType, path) ErrorHandler.handleErrorOnNullObject(HttpStatus.NOT_FOUND, item, "Item with DomainType $domainType not found with path: $path") @@ -44,7 +39,7 @@ class PathService implements AdministeredItemReader { } - AdministeredItem getResourceByPathFromResource(String domainType, UUID domainId, String path){ + AdministeredItem getResourceByPathFromResource(String domainType, UUID domainId, String pathString){ AdministeredItem fromModel = findAdministeredItem(domainType, domainId) ErrorHandler.handleErrorOnNullObject(HttpStatus.NOT_FOUND, fromModel, "Model $domainType, $domainId not found") @@ -52,12 +47,14 @@ class PathService implements AdministeredItemReader { updateDerivedProperties(fromModel) //verify model path in the input path - if (!path.contains(fromModel.path.pathString)) { - ErrorHandler.handleError(HttpStatus.NOT_FOUND, "Path $path does not belong to $domainType, $domainId") + if (!pathString.contains(fromModel.path.pathString)) { + ErrorHandler.handleError(HttpStatus.NOT_FOUND, "Path $pathString does not belong to $domainType, $domainId") } - Tuple2 itemDomainTypeAndPath = getItemDomainTypeAndPath(path) - AdministeredItem administeredItem = findResourceByPath(itemDomainTypeAndPath.first, path) - ErrorHandler.handleErrorOnNullObject(HttpStatus.NOT_FOUND, administeredItem, "Item with $itemDomainTypeAndPath.first and label $itemDomainTypeAndPath.v2 not found") + Path path = new Path(pathString) + Path.PathNode lastPathNode = path.lastPathNode() + String itemDomainType = getDomainTypeFromPathPrefix(lastPathNode.prefix) + AdministeredItem administeredItem = findResourceByPath(itemDomainType, path) + ErrorHandler.handleErrorOnNullObject(HttpStatus.NOT_FOUND, administeredItem, "Item with ${lastPathNode.prefix} and label ${lastPathNode.identifier} not found") accessControlService.checkRole(Role.READER, administeredItem) updateDerivedProperties(administeredItem) administeredItem @@ -77,33 +74,13 @@ class PathService implements AdministeredItemReader { * @return the admin item, given the full path including versioning */ - protected AdministeredItem findResourceByPath(String domainType, String path) { + protected AdministeredItem findResourceByPath(String domainType, Path path) { String pathPrefix = getPathPrefixForDomainType(domainType) - String domainPath = getItemSubPath(pathPrefix, path) - String versionString = getVersionFromPath(path) + String domainPath = path.findLastPathNodeByPrefix(pathPrefix).identifier + String versionString = path.modelIdentifier return findItemForPath(domainType, domainPath, versionString, path) } - - /** - * - * @param path fullPath - * @return the last path domainType and label/subPath - */ - protected Tuple2 getItemDomainTypeAndPath(String path) { - String itemPath = lastSubPath(path) - //extract the item domain type from given input path - String[] itemParts = splitBy(itemPath, COLON) - if (itemParts.size() != 2){ - ErrorHandler.handleError(HttpStatus.UNPROCESSABLE_ENTITY, "bad path $path") - } - String itemDomainType = getDomainTypeFromPathPrefix(itemParts[0]) - String subPathOnly = itemParts[1].find(DISCARD_AFTER_VERSION) ?: itemParts[1] - - String itemSubPath = subPathOnly.replaceAll(REMOVE_VERSION_DELIM, '') - new Tuple2(itemDomainType, itemSubPath) - } - protected String getPathPrefixForDomainType(String domainType) { AdministeredItemCacheableRepository repo = repositoryService.administeredItemCacheableRepositories.find { it.handles(domainType) @@ -129,24 +106,24 @@ class PathService implements AdministeredItemReader { item } - protected AdministeredItem findItemForPath(String domainType, String domainPath, String versionString, String fullPath) { + protected AdministeredItem findItemForPath(String domainType, String domainPath, String versionString, Path path) { AdministeredItemCacheableRepository repository = getAdministeredItemRepository(domainType) - List items = repository.findAllByLabel(domainPath) + List items = repository.findAllByLabel(domainPath) as List if (items.isEmpty()) { - null + return null } AdministeredItem item if (items.size() == 1) { item = items[0] as AdministeredItem } else { if (!versionString) { - log.warn("No version found in fullpath: $fullPath; returning 1st item") - items.first() + log.warn("No version found in path: ${path.toString()}; returning 1st item") + return items.first() } item = (items as List).find { pathRepository.readParentItems(it) it.updatePath() - it.path?.pathString?.contains(versionString) + it.path?.modelIdentifier == versionString } } item diff --git a/mauro-api/src/test/groovy/org/maurodata/path/PathControllerIntegrationSpec.groovy b/mauro-api/src/test/groovy/org/maurodata/path/PathControllerIntegrationSpec.groovy index 913b37c3b..b98c413f1 100644 --- a/mauro-api/src/test/groovy/org/maurodata/path/PathControllerIntegrationSpec.groovy +++ b/mauro-api/src/test/groovy/org/maurodata/path/PathControllerIntegrationSpec.groovy @@ -59,7 +59,7 @@ class PathControllerIntegrationSpec extends CommonDataSpec { void 'test getResource by path -path not found -shouldThrowException'() { when: - pathApi.getResourceByPath('datamodel', 'not known label') + pathApi.getResourceByPath('datamodel', 'dm:not known label') then: HttpStatusException exception = thrown() @@ -68,14 +68,14 @@ class PathControllerIntegrationSpec extends CommonDataSpec { void 'test getResource by path -unknown domainType -shouldThrowException'() { when: - pathApi.getResourceByPath('whatisthis', EXPECTED_LABEL) + pathApi.getResourceByPath('whatisthis', "dm:${EXPECTED_LABEL}" ) then: HttpStatusException exception = thrown() exception.status == HttpStatus.NOT_FOUND } - void 'test getResource by Path from Resource -should find resource'() { + void 'test getResource by Path from Resource1 -should find resource'() { DataModel dataModel = dataModelApi.create(folderId, dataModelPayload('datamodel label ')) DataType dataType = dataTypeApi.create(dataModel.id, dataTypesPayload('label for datatype', DataType.DataTypeKind.ENUMERATION_TYPE)) DataClass dataClass = dataClassApi.create(dataModel.id, dataClassPayload('label for dataclass')) @@ -124,7 +124,7 @@ class PathControllerIntegrationSpec extends CommonDataSpec { //todo: fixme the version info is not part of the pathsstring for modelitems so unable to match exact version - void 'test getResource by Path from Resource -should find resource'() { + void 'test getResource by Path from Resource2 -should find resource'() { DataModel dataModel = dataModelApi.create(folderId, dataModelPayload('datamodel label ')) DataClass dataClass = dataClassApi.create(dataModel.id, dataClassPayload('label for dataclass')) diff --git a/mauro-domain/src/main/groovy/org/maurodata/domain/model/Path.groovy b/mauro-domain/src/main/groovy/org/maurodata/domain/model/Path.groovy index 8bca39a10..b4b5a177b 100644 --- a/mauro-domain/src/main/groovy/org/maurodata/domain/model/Path.groovy +++ b/mauro-domain/src/main/groovy/org/maurodata/domain/model/Path.groovy @@ -45,6 +45,9 @@ class Path { } void updatePathString() { + final String modelIdentifier = getModelIdentifier() + nodes.each {PathNode pathNode -> pathNode.modelIdentifier = null} + setModelIdentifier(modelIdentifier) pathString = nodes.collect {it.toString()}.join('|') } @@ -144,6 +147,42 @@ class Path { trimmed.pathString } + @Transient + PathNode findLastPathNodeByPrefix(final String prefix) { + for (int p = nodes.size() - 1; p >= 0; p--) { + final PathNode pathNode = nodes.get(p) + if (pathNode.prefix == prefix) {return pathNode} + } + return null + } + + @Transient + PathNode lastPathNode() { + if (nodes.isEmpty()) {return null} + return nodes.get(nodes.size() - 1) + } + + @Transient + String getModelIdentifier() { + for (int p = 0, n = nodes.size(); p < n; p++) { + final PathNode pathNode = nodes.get(p) + if (!PathNode.canHaveModelIdentifier.contains(pathNode.prefix)) {continue} + if (pathNode.modelIdentifier) {return pathNode.modelIdentifier} + } + return null + } + + void setModelIdentifier(final String modelIdentifier) { + nodes.each {PathNode pathNode -> pathNode.modelIdentifier = null} + for (int p = 0, n = nodes.size(); p < n; p++) { + final PathNode pathNode = nodes.get(p) + if (!PathNode.canHaveModelIdentifier.contains(pathNode.prefix)) {continue} + pathNode.modelIdentifier = modelIdentifier + break + } + pathString = nodes.collect {it.toString()}.join('|') + } + static class PathNode { String prefix String identifier @@ -161,6 +200,8 @@ class Path { value } + static List canHaveModelIdentifier = ['dm','vf','csc','cs','te'] + static PathNode from(String str) { Pattern nodePattern = ~/^(?\w\w\w?):(?[^@$]*)(\$(?[^@]*))?(@(?.*))?$/ Matcher matcher = str =~ nodePattern diff --git a/mauro-domain/src/main/groovy/org/maurodata/util/PathStringUtils.groovy b/mauro-domain/src/main/groovy/org/maurodata/util/PathStringUtils.groovy deleted file mode 100644 index 9f97ee4fe..000000000 --- a/mauro-domain/src/main/groovy/org/maurodata/util/PathStringUtils.groovy +++ /dev/null @@ -1,59 +0,0 @@ -package org.maurodata.util - -import groovy.transform.CompileStatic -import io.micronaut.http.HttpStatus -import jakarta.inject.Singleton -import org.maurodata.ErrorHandler - -@Singleton -@CompileStatic -class PathStringUtils { - - static final String VERTICAL_BAR_ESCAPE = "\\|" - static final String COLON = ":" - static final String BRANCH_DELIMITER = '$' - static final String DISCARD_AFTER_VERSION = ~/.*\$/ - static final String DISCARD_BEFORE_VERSION = ~/\$(.+)/ - static final String REMOVE_VERSION_DELIM = '\\$.*' - /** - * - * @param path eg "fo:soluta eum architecto|dm:modi unde est$matrix|dc:est quasi vel|de:new data element label" - * pathPrefix eg 'de', - * - * path: "fo:soluta eum architecto|dm:modi unde est$1.0.0|dc:est quasi vel|dc:est sed hic", - * pathPrefix: dc - * returns last : "est sed hic" (ie child) - * - * split each subpath '|' - * find subpath matching pathPrefix - * @return the subpath eg "new data element label" - */ - static String getItemSubPath(String pathPrefix, String fullPath) { - String[] pathSubPaths = splitBy(fullPath, VERTICAL_BAR_ESCAPE).reverse() - String subPathAndPrefix = pathSubPaths.find {it.startsWith("$pathPrefix:")} - if (!subPathAndPrefix) ErrorHandler.handleError(HttpStatus.NOT_FOUND, "Path starting with $pathPrefix not found") - String subPath = subPathAndPrefix - "$pathPrefix:" - // Discard version after subpath if any - String subPathOnly = subPath.find(DISCARD_AFTER_VERSION) ?: subPath - //find up to and including $ eg branch - subPathOnly - BRANCH_DELIMITER - } - - static String lastSubPath(String subPath) { - splitBy(subPath, VERTICAL_BAR_ESCAPE).last() - } - - static String[] splitBy(String path, String separator) { - path.split(separator) - } - - static String getVersionFromPath(String fullPath) { - String[] parts = fullPath.split(VERTICAL_BAR_ESCAPE) - String version - parts.each { - version = it.find(DISCARD_BEFORE_VERSION) {match, captured -> captured} ?: version - } - version - } - -} diff --git a/mauro-domain/src/test/groovy/org/maurodata/test/domain/util/PathStringUtilsTest.groovy b/mauro-domain/src/test/groovy/org/maurodata/test/domain/model/PathMethodsTest.groovy similarity index 57% rename from mauro-domain/src/test/groovy/org/maurodata/test/domain/util/PathStringUtilsTest.groovy rename to mauro-domain/src/test/groovy/org/maurodata/test/domain/model/PathMethodsTest.groovy index c589a852d..5e6ff075d 100644 --- a/mauro-domain/src/test/groovy/org/maurodata/test/domain/util/PathStringUtilsTest.groovy +++ b/mauro-domain/src/test/groovy/org/maurodata/test/domain/model/PathMethodsTest.groovy @@ -1,21 +1,22 @@ -package org.maurodata.test.domain.util +package org.maurodata.test.domain.model + +import org.maurodata.domain.model.Path import io.micronaut.test.extensions.spock.annotation.MicronautTest -import org.maurodata.util.PathStringUtils import spock.lang.Specification import spock.lang.Unroll @MicronautTest -class PathStringUtilsTest extends Specification { +class PathMethodsTest extends Specification { @Unroll - void 'test getItemSubPath, for #pathPrefix, #fullPath'() { + void 'test findLastPathNodeByPrefix, for #pathPrefix, #fullPath'() { when: - String subPath = PathStringUtils.getItemSubPath(pathPrefix, fullPath) + Path.PathNode pathNode = new Path(fullPath).findLastPathNodeByPrefix(pathPrefix) then: - subPath - subPath == expectedSubPath + pathNode + pathNode.identifier == expectedSubPath where: pathPrefix | fullPath | expectedSubPath @@ -30,19 +31,20 @@ class PathStringUtilsTest extends Specification { } @Unroll - void 'test getVersionFromPath for #fullPath'() { + void 'test modelIdentifier for #fullPath'() { when: - String version = PathStringUtils.getVersionFromPath(fullPath) + String modelIdentifier = new Path(fullPath).modelIdentifier then: - version == expectedVersion + modelIdentifier == expectedVersion where: - fullPath | expectedVersion - "fo:soluta eum architecto|dm:modi unde est\$matrix|dc:est quasi vel|de:new data element label\$2.0.0" | "2.0.0" - "dm:BC_Bloods\$2.0.0" | "2.0.0" - "fo:soluta eum architecto" | null - "fo:soluta eum architecto|te:Dewey Decimal Classification v22\$main" | "main" + fullPath | expectedVersion + 'fo:soluta eum architecto|dm:modi unde est$matrix|dc:est quasi vel|de:new data element label$2.0.0' | "matrix" + 'dm:BC_Bloods$2.0.0' | "2.0.0" + 'fo:soluta eum architecto' | null + 'fo:soluta eum architecto|te:Dewey Decimal Classification v22$main' | "main" + 'fo:soluta eum architecto|vf:versionio de folder$main|te:Dewey Decimal Classification v22$main' | "main" } }