From 764d8d69ecffad222c0125cff9c0ba347d9c0a03 Mon Sep 17 00:00:00 2001 From: Maximilian Reisch Date: Thu, 16 Jul 2026 12:28:49 +0200 Subject: [PATCH 1/5] created structure for cim collection facade --- .../rendering/svelteflow/sub/NodeDataDTO.java | 1 + .../svelteflow/sub/SuperClassDTO.java | 29 ++++ .../models/cim/data/dto/facade/CIMClass.java | 71 ++++++++ .../cim/data/dto/facade/CIMResource.java | 152 ++++++++++++++++++ .../cim/data/dto/facade/ICIMAssociation.java | 32 ++++ .../cim/data/dto/facade/ICIMAttribute.java | 38 +++++ .../models/cim/data/dto/facade/ICIMClass.java | 37 +++++ .../data/dto/facade/ICIMClassCategory.java | 24 +++ .../cim/data/dto/facade/ICIMEnumEntry.java | 28 ++++ .../cim/data/dto/facade/ICIMResource.java | 36 +++++ 10 files changed, 448 insertions(+) create mode 100644 backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/SuperClassDTO.java create mode 100644 backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClass.java create mode 100644 backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMResource.java create mode 100644 backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMAssociation.java create mode 100644 backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMAttribute.java create mode 100644 backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClass.java create mode 100644 backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClassCategory.java create mode 100644 backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMEnumEntry.java create mode 100644 backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMResource.java diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/NodeDataDTO.java b/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/NodeDataDTO.java index 349e89ea..123a95e9 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/NodeDataDTO.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/NodeDataDTO.java @@ -33,4 +33,5 @@ public class NodeDataDTO { private List stereotypes; private List attributes; private List enumEntries; + private List superClasses; } diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/SuperClassDTO.java b/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/SuperClassDTO.java new file mode 100644 index 00000000..349d82d1 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/SuperClassDTO.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.api.dto.rendering.svelteflow.sub; + +import java.util.List; +import java.util.UUID; + +public class SuperClassDTO { + + private UUID uuid; + private String label; + private List attributes; + private List enumEntries; +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClass.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClass.java new file mode 100644 index 00000000..092e0374 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClass.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.models.cim.data.dto.facade; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDFS; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class CIMClass extends CIMResource implements ICIMClass { + + public CIMClass(String graphUri, Model model, UUID uuid) { + super(graphUri, model, uuid); + } + + public CIMClass(String graphUri, Model model, Resource resource) { + super(graphUri, model, resource); + } + + @Override + public List getSuperClasses() { + var resources = this.getJenaProperties(RDFS.subClassOf); + var superClasses = new ArrayList(); + for(var res : resources){ + superClasses.add(new CIMClass(this.getGraphUri(), this.getModel(), res)); + } + return superClasses; + } + + @Override + public ICIMClassCategory getBelongsToCategory() { + } + + @Override + public List getStereotypes() { + return List.of(); + } + + @Override + public List getAttributes() { + return List.of(); + } + + @Override + public List getAssociations() { + return List.of(); + } + + @Override + public List getEnumEntries() { + return List.of(); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMResource.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMResource.java new file mode 100644 index 00000000..fccadd19 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMResource.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.models.cim.data.dto.facade; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import org.rdfarchitect.models.cim.data.dto.relations.RDFSComment; +import org.rdfarchitect.models.cim.data.dto.relations.RDFSLabel; +import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; +import org.rdfarchitect.models.cim.rdf.resources.RDFA; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +abstract class CIMResource implements ICIMResource { + + private final String graphUri; + + private final Model model; + + private final UUID uuid; + + protected CIMResource(String graphUri, Model model, UUID uuid) { + this.graphUri = graphUri; + this.model = model; + this.uuid = uuid; + } + + protected CIMResource(String graphUri, Model model, Resource resource) { + this.graphUri = graphUri; + this.model = model; + var uuidObjects = this.model.listObjectsOfProperty(resource, RDFA.uuid).toList(); + if(uuidObjects.isEmpty()){ + throw new IllegalStateException("No uuid found for resource"); + } + if(uuidObjects.size() > 1){ + throw new IllegalStateException("Multiple uuids found for single resource."); + } + var subject = uuidObjects.getFirst().asLiteral(); + if(!subject.isLiteral()){ + throw new IllegalStateException("UUID for resource is not a literal."); + } + this.uuid = UUID.fromString(subject.getLexicalForm()); + } + + protected Model getModel() { + return this.model; + } + + @Override + public UUID getUuid(){ + return uuid; + } + + @Override + public String getGraphUri() { + return this.graphUri; + } + + @Override + public URI getUri(){ + return new URI(getJenaResource().getURI()); + } + + @Override + public RDFSLabel getLabel(){ + var resource = getRequiredUniqueJenaProperty(RDFS.label); + if(!resource.isLiteral()){ + throw new IllegalStateException("Label for resource with UUID " + uuid + " is not a literal."); + } + var langLiteral = resource.asLiteral(); + return new RDFSLabel(langLiteral.getString(), langLiteral.getLanguage()); + } + + @Override + public RDFSComment getComment(){ + var resource = getUniqueJenaProperty(RDFS.comment); + if(resource == null){ + return null; + } + if(!resource.isLiteral()){ + throw new IllegalStateException("Comment for resource with UUID " + uuid + " is not a literal."); + } + var langLiteral = resource.asLiteral(); + return new RDFSComment(langLiteral.getString(), new URI(langLiteral.getDatatype().getURI())); + } + + + // helper + + protected Resource getJenaResource(){ + var subjectsWithUuid = model.listSubjectsWithProperty(RDFA.uuid, uuid.toString()).toList(); + if(subjectsWithUuid.isEmpty()){ + throw new IllegalStateException("Resource with UUID " + uuid + " not found."); + } + if(subjectsWithUuid.size() > 1){ + throw new IllegalStateException("Multiple uris found for " + uuid + "."); + } + var subject = subjectsWithUuid.getFirst(); + if(!subject.isURIResource()){ + throw new IllegalStateException("Resource with UUID " + uuid + " is not an URI resource."); + } + return subject; + } + + protected List getJenaProperties(Property property){ + var resource = getJenaResource(); + var properties = model.listObjectsOfProperty(resource, property).toList(); + var res = new ArrayList(); + for(var prop : properties){ + res.add(prop.asResource()); + } + return res; + } + + protected Resource getUniqueJenaProperty(Property property){ + var properties = getJenaProperties(property); + if(properties.isEmpty()){ + return null; + } + if(properties.size() > 1){ + throw new IllegalStateException("Multiple " + property + " properties found for resource with UUID " + uuid + "."); + } + return properties.getFirst(); + } + + protected Resource getRequiredUniqueJenaProperty(Property property){ + var propertyResource = getUniqueJenaProperty(property); + if(propertyResource == null){ + throw new IllegalStateException("Required property " + property + " not found for resource with UUID " + uuid + "."); + } + return propertyResource; + } +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMAssociation.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMAssociation.java new file mode 100644 index 00000000..f87b3ed4 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMAssociation.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.models.cim.data.dto.facade; + +import org.rdfarchitect.models.cim.data.dto.relations.CIMSAssociationUsed; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSMultiplicity; + +public interface ICIMAssociation extends ICIMResource{ + + CIMSMultiplicity getMultiplicity(); + + ICIMClass getDomain(); + + ICIMAssociation getInverseAssociation(); + + CIMSAssociationUsed getAssociationUsed(); +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMAttribute.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMAttribute.java new file mode 100644 index 00000000..2c1b3f57 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMAttribute.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.models.cim.data.dto.facade; + +import org.rdfarchitect.models.cim.data.dto.relations.CIMSIsDefault; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSIsFixed; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSMultiplicity; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; + +public interface ICIMAttribute extends ICIMResource { + + ICIMClass getDomain(); + + CIMSMultiplicity getMultiplicity(); + + ICIMClass getDataType(); + + CIMSStereotype getStereotype(); + + CIMSIsFixed getFixed(); + + CIMSIsDefault getDefault(); +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClass.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClass.java new file mode 100644 index 00000000..a6a07f05 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClass.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.models.cim.data.dto.facade; + +import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; + +import java.util.List; + +public interface ICIMClass extends ICIMResource { + + List getSuperClasses(); + + ICIMClassCategory getBelongsToCategory(); + + List getStereotypes(); + + List getAttributes(); + + List getAssociations(); + + List getEnumEntries(); +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClassCategory.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClassCategory.java new file mode 100644 index 00000000..86415940 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClassCategory.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.models.cim.data.dto.facade; + +import java.util.UUID; + +public interface ICIMClassCategory extends ICIMResource{ + +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMEnumEntry.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMEnumEntry.java new file mode 100644 index 00000000..d2b1f22e --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMEnumEntry.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.models.cim.data.dto.facade; + + +import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; + +public interface ICIMEnumEntry { + + ICIMClass domain(); + + CIMSStereotype getStereotype(); +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMResource.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMResource.java new file mode 100644 index 00000000..c87fb81f --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMResource.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.models.cim.data.dto.facade; + +import org.rdfarchitect.models.cim.data.dto.relations.RDFSComment; +import org.rdfarchitect.models.cim.data.dto.relations.RDFSLabel; +import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; +import java.util.UUID; + +public interface ICIMResource { + + UUID getUuid(); + + String getGraphUri(); + + URI getUri(); + + RDFSLabel getLabel(); + + RDFSComment getComment(); +} From 33f1f613ca432f52cde06e4d170a4c7d6823ab9a Mon Sep 17 00:00:00 2001 From: Maximilian Reisch Date: Thu, 16 Jul 2026 13:25:29 +0200 Subject: [PATCH 2/5] finished impl of facade Signed-off-by: Maximilian Reisch --- .../cim/data/dto/facade/CIMAssociation.java | 73 +++++ .../cim/data/dto/facade/CIMAttribute.java | 122 ++++++++ .../models/cim/data/dto/facade/CIMClass.java | 51 ++- .../cim/data/dto/facade/CIMClassCategory.java | 33 ++ .../cim/data/dto/facade/CIMEnumEntry.java | 50 +++ .../cim/data/dto/facade/CIMResource.java | 57 +++- .../cim/data/dto/facade/ExternalCIMClass.java | 111 +++++++ .../cim/data/dto/facade/ICIMAssociation.java | 2 + .../data/dto/facade/ICIMClassCategory.java | 2 - .../cim/data/dto/facade/ICIMEnumEntry.java | 4 +- .../cim/data/dto/facade/CIMFacadeTest.java | 295 ++++++++++++++++++ 11 files changed, 778 insertions(+), 22 deletions(-) create mode 100644 backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMAssociation.java create mode 100644 backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMAttribute.java create mode 100644 backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClassCategory.java create mode 100644 backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMEnumEntry.java create mode 100644 backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ExternalCIMClass.java create mode 100644 backend/src/test/java/org/rdfarchitect/models/cim/data/dto/facade/CIMFacadeTest.java diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMAssociation.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMAssociation.java new file mode 100644 index 00000000..78a990ee --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMAssociation.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.models.cim.data.dto.facade; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDFS; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSAssociationUsed; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSMultiplicity; +import org.rdfarchitect.models.cim.rdf.resources.CIMS; +import java.util.UUID; + +public class CIMAssociation extends CIMResource implements ICIMAssociation { + + public CIMAssociation(String graphUri, Model model, UUID uuid) { + super(graphUri, model, uuid); + } + + public CIMAssociation(String graphUri, Model model, Resource resource) { + super(graphUri, model, resource); + } + + @Override + public CIMSMultiplicity getMultiplicity() { + var multiplicity = getRequiredUniqueJenaProperty(CIMS.multiplicity); + return new CIMSMultiplicity(multiplicity.getURI()); + } + + @Override + public ICIMClass getDomain() { + var domain = getRequiredUniqueJenaProperty(RDFS.domain); + return CIMClass.fromResource(getGraphUri(), getModel(), domain); + } + + @Override + public ICIMClass getRange() { + var range = getRequiredUniqueJenaProperty(RDFS.range); + return CIMClass.fromResource(getGraphUri(), getModel(), range); + } + + @Override + public ICIMAssociation getInverseAssociation() { + var inverse = getRequiredUniqueJenaProperty(CIMS.inverseRoleName); + return new CIMAssociation(getGraphUri(), getModel(), inverse); + } + + @Override + public CIMSAssociationUsed getAssociationUsed() { + var node = getUniqueJenaPropertyNode(CIMS.associationUsed); + if(node == null){ + throw new IllegalStateException("Required property " + CIMS.associationUsed + " not found for association with UUID " + getUuid() + "."); + } + if(!node.isLiteral()){ + throw new IllegalStateException("AssociationUsed for association with UUID " + getUuid() + " is not a literal."); + } + return new CIMSAssociationUsed(node.asLiteral().getLexicalForm()); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMAttribute.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMAttribute.java new file mode 100644 index 00000000..b7714bb5 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMAttribute.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.models.cim.data.dto.facade; + +import org.apache.jena.rdf.model.Literal; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; +import org.apache.jena.vocabulary.RDFS; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSIsDefault; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSIsFixed; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSMultiplicity; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; +import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; +import org.rdfarchitect.models.cim.rdf.resources.CIMS; +import java.util.UUID; + +public class CIMAttribute extends CIMResource implements ICIMAttribute { + + private static final Property LITERAL_WRAPPER_PROPERTY = + ResourceFactory.createProperty(RDFS.Literal.getURI()); + + public CIMAttribute(String graphUri, Model model, UUID uuid) { + super(graphUri, model, uuid); + } + + public CIMAttribute(String graphUri, Model model, Resource resource) { + super(graphUri, model, resource); + } + + @Override + public ICIMClass getDomain() { + var domain = getRequiredUniqueJenaProperty(RDFS.domain); + return CIMClass.fromResource(getGraphUri(), getModel(), domain); + } + + @Override + public CIMSMultiplicity getMultiplicity() { + var multiplicity = getRequiredUniqueJenaProperty(CIMS.multiplicity); + return new CIMSMultiplicity(multiplicity.getURI()); + } + + @Override + public ICIMClass getDataType() { + var dataType = getUniqueJenaProperty(CIMS.datatype); + if(dataType == null){ + dataType = getUniqueJenaProperty(RDFS.range); + } + if(dataType == null){ + throw new IllegalStateException("No " + CIMS.datatype + " or " + RDFS.range + " found for attribute with UUID " + getUuid() + "."); + } + return CIMClass.fromResource(getGraphUri(), getModel(), dataType); + } + + @Override + public CIMSStereotype getStereotype() { + var stereotypes = getStereotypeList(); + if(stereotypes.isEmpty()){ + throw new IllegalStateException("Required property " + CIMS.stereotype + " not found for attribute with UUID " + getUuid() + "."); + } + return stereotypes.getFirst(); + } + + @Override + public CIMSIsFixed getFixed() { + var valueNode = readValueNode(CIMS.isFixed); + if(valueNode == null){ + return null; + } + return new CIMSIsFixed(valueNode.value(), valueNode.dataType(), valueNode.blankNode()); + } + + @Override + public CIMSIsDefault getDefault() { + var valueNode = readValueNode(CIMS.isDefault); + if(valueNode == null){ + return null; + } + return new CIMSIsDefault(valueNode.value(), valueNode.dataType(), valueNode.blankNode()); + } + + private record ValueNode(String value, URI dataType, boolean blankNode) {} + + private ValueNode readValueNode(Property property) { + var node = getUniqueJenaPropertyNode(property); + if(node == null){ + return null; + } + if(node.isLiteral()){ + return toValueNode(node.asLiteral(), false); + } + if(node.isAnon()){ + var inner = node.asResource().getProperty(LITERAL_WRAPPER_PROPERTY); + if(inner != null && inner.getObject().isLiteral()){ + return toValueNode(inner.getObject().asLiteral(), true); + } + } + return null; + } + + private static ValueNode toValueNode(Literal literal, boolean blankNode) { + var dataTypeUri = literal.getDatatypeURI(); + var dataType = dataTypeUri == null || dataTypeUri.isEmpty() ? null : new URI(dataTypeUri); + return new ValueNode(literal.getLexicalForm(), dataType, blankNode); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClass.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClass.java index 092e0374..a85d277c 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClass.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClass.java @@ -19,8 +19,13 @@ import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; +import org.rdfarchitect.models.cim.rdf.resources.CIMS; +import org.rdfarchitect.models.cim.rdf.resources.CIMStereotypes; +import org.rdfarchitect.models.cim.rdf.resources.RDFA; +import org.rdfarchitect.models.cim.relations.model.properties.CIMPropertyUtils; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -35,37 +40,73 @@ public CIMClass(String graphUri, Model model, Resource resource) { super(graphUri, model, resource); } + public static ICIMClass fromResource(String graphUri, Model model, Resource resource) { + if(model.contains(resource, RDFA.uuid)){ + return new CIMClass(graphUri, model, resource); + } + return new ExternalCIMClass(graphUri, model, resource); + } + @Override public List getSuperClasses() { var resources = this.getJenaProperties(RDFS.subClassOf); var superClasses = new ArrayList(); for(var res : resources){ - superClasses.add(new CIMClass(this.getGraphUri(), this.getModel(), res)); + superClasses.add(fromResource(this.getGraphUri(), this.getModel(), res)); } return superClasses; } @Override public ICIMClassCategory getBelongsToCategory() { + var category = getUniqueJenaProperty(CIMS.belongsToCategory); + if(category == null){ + return null; + } + return new CIMClassCategory(getGraphUri(), getModel(), category); } @Override public List getStereotypes() { - return List.of(); + return getStereotypeList(); } @Override public List getAttributes() { - return List.of(); + var attributes = new ArrayList(); + for(var property : listDirectProperties()){ + if(CIMPropertyUtils.isAttribute(property)){ + attributes.add(new CIMAttribute(getGraphUri(), getModel(), property)); + } + } + return attributes; } @Override public List getAssociations() { - return List.of(); + var associations = new ArrayList(); + for(var property : listDirectProperties()){ + if(CIMPropertyUtils.isAssociation(property)){ + associations.add(new CIMAssociation(getGraphUri(), getModel(), property)); + } + } + return associations; } @Override public List getEnumEntries() { - return List.of(); + var jenaResource = getJenaResource(); + if(!jenaResource.hasProperty(CIMS.stereotype, CIMStereotypes.enumeration)){ + return List.of(); + } + var entries = new ArrayList(); + for(var entry : getModel().listSubjectsWithProperty(RDF.type, jenaResource).toList()){ + entries.add(new CIMEnumEntry(getGraphUri(), getModel(), entry)); + } + return entries; + } + + private List listDirectProperties() { + return getModel().listSubjectsWithProperty(RDFS.domain, getJenaResource()).toList(); } } diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClassCategory.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClassCategory.java new file mode 100644 index 00000000..02e3863e --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClassCategory.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.models.cim.data.dto.facade; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.Resource; +import java.util.UUID; + +public class CIMClassCategory extends CIMResource implements ICIMClassCategory { + + public CIMClassCategory(String graphUri, Model model, UUID uuid) { + super(graphUri, model, uuid); + } + + public CIMClassCategory(String graphUri, Model model, Resource resource) { + super(graphUri, model, resource); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMEnumEntry.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMEnumEntry.java new file mode 100644 index 00000000..21d99bd2 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMEnumEntry.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.models.cim.data.dto.facade; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDF; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; +import java.util.UUID; + +public class CIMEnumEntry extends CIMResource implements ICIMEnumEntry { + + public CIMEnumEntry(String graphUri, Model model, UUID uuid) { + super(graphUri, model, uuid); + } + + public CIMEnumEntry(String graphUri, Model model, Resource resource) { + super(graphUri, model, resource); + } + + @Override + public ICIMClass getDomain() { + var domain = getRequiredUniqueJenaProperty(RDF.type); + return CIMClass.fromResource(getGraphUri(), getModel(), domain); + } + + @Override + public CIMSStereotype getStereotype() { + var stereotypes = getStereotypeList(); + if(stereotypes.isEmpty()){ + return null; + } + return stereotypes.getFirst(); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMResource.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMResource.java index fccadd19..ac9265af 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMResource.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMResource.java @@ -19,12 +19,14 @@ import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.RDFNode; import org.apache.jena.rdf.model.Resource; -import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; import org.rdfarchitect.models.cim.data.dto.relations.RDFSComment; import org.rdfarchitect.models.cim.data.dto.relations.RDFSLabel; import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; +import org.rdfarchitect.models.cim.rdf.resources.CIMS; import org.rdfarchitect.models.cim.rdf.resources.RDFA; import java.util.ArrayList; import java.util.List; @@ -54,11 +56,11 @@ protected CIMResource(String graphUri, Model model, Resource resource) { if(uuidObjects.size() > 1){ throw new IllegalStateException("Multiple uuids found for single resource."); } - var subject = uuidObjects.getFirst().asLiteral(); + var subject = uuidObjects.getFirst(); if(!subject.isLiteral()){ throw new IllegalStateException("UUID for resource is not a literal."); } - this.uuid = UUID.fromString(subject.getLexicalForm()); + this.uuid = UUID.fromString(subject.asLiteral().getLexicalForm()); } protected Model getModel() { @@ -82,24 +84,27 @@ public URI getUri(){ @Override public RDFSLabel getLabel(){ - var resource = getRequiredUniqueJenaProperty(RDFS.label); - if(!resource.isLiteral()){ + var node = getUniqueJenaPropertyNode(RDFS.label); + if(node == null){ + throw new IllegalStateException("Required property " + RDFS.label + " not found for resource with UUID " + uuid + "."); + } + if(!node.isLiteral()){ throw new IllegalStateException("Label for resource with UUID " + uuid + " is not a literal."); } - var langLiteral = resource.asLiteral(); + var langLiteral = node.asLiteral(); return new RDFSLabel(langLiteral.getString(), langLiteral.getLanguage()); } @Override public RDFSComment getComment(){ - var resource = getUniqueJenaProperty(RDFS.comment); - if(resource == null){ + var node = getUniqueJenaPropertyNode(RDFS.comment); + if(node == null){ return null; } - if(!resource.isLiteral()){ + if(!node.isLiteral()){ throw new IllegalStateException("Comment for resource with UUID " + uuid + " is not a literal."); } - var langLiteral = resource.asLiteral(); + var langLiteral = node.asLiteral(); return new RDFSComment(langLiteral.getString(), new URI(langLiteral.getDatatype().getURI())); } @@ -122,15 +127,41 @@ protected Resource getJenaResource(){ } protected List getJenaProperties(Property property){ - var resource = getJenaResource(); - var properties = model.listObjectsOfProperty(resource, property).toList(); var res = new ArrayList(); - for(var prop : properties){ + for(var prop : getJenaPropertyNodes(property)){ res.add(prop.asResource()); } return res; } + protected List getJenaPropertyNodes(Property property){ + var resource = getJenaResource(); + return model.listObjectsOfProperty(resource, property).toList(); + } + + protected RDFNode getUniqueJenaPropertyNode(Property property){ + var nodes = getJenaPropertyNodes(property); + if(nodes.isEmpty()){ + return null; + } + if(nodes.size() > 1){ + throw new IllegalStateException("Multiple " + property + " properties found for resource with UUID " + uuid + "."); + } + return nodes.getFirst(); + } + + protected List getStereotypeList(){ + var stereotypes = new ArrayList(); + for(var node : getJenaPropertyNodes(CIMS.stereotype)){ + if(node.isLiteral()){ + stereotypes.add(new CIMSStereotype(node.asLiteral().getLexicalForm())); + } else if(node.isURIResource()){ + stereotypes.add(new CIMSStereotype(node.asResource().getURI())); + } + } + return stereotypes; + } + protected Resource getUniqueJenaProperty(Property property){ var properties = getJenaProperties(property); if(properties.isEmpty()){ diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ExternalCIMClass.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ExternalCIMClass.java new file mode 100644 index 00000000..af5e273c --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ExternalCIMClass.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.models.cim.data.dto.facade; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDFS; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; +import org.rdfarchitect.models.cim.data.dto.relations.RDFSComment; +import org.rdfarchitect.models.cim.data.dto.relations.RDFSLabel; +import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; +import java.util.List; +import java.util.UUID; + +public class ExternalCIMClass implements ICIMClass { + + private final String graphUri; + + private final Model model; + + private final Resource resource; + + public ExternalCIMClass(String graphUri, Model model, Resource resource) { + if(!resource.isURIResource()){ + throw new IllegalStateException("External class resource is not an URI resource."); + } + this.graphUri = graphUri; + this.model = model; + this.resource = resource; + } + + @Override + public UUID getUuid() { + return null; + } + + @Override + public String getGraphUri() { + return graphUri; + } + + @Override + public URI getUri() { + return new URI(resource.getURI()); + } + + @Override + public RDFSLabel getLabel() { + var statement = model.getProperty(resource, RDFS.label); + if(statement == null || !statement.getObject().isLiteral()){ + return new RDFSLabel(getUri().getSuffix()); + } + var langLiteral = statement.getObject().asLiteral(); + return new RDFSLabel(langLiteral.getString(), langLiteral.getLanguage()); + } + + @Override + public RDFSComment getComment() { + var statement = model.getProperty(resource, RDFS.comment); + if(statement == null || !statement.getObject().isLiteral()){ + return null; + } + var langLiteral = statement.getObject().asLiteral(); + return new RDFSComment(langLiteral.getString(), new URI(langLiteral.getDatatype().getURI())); + } + + @Override + public List getSuperClasses() { + return List.of(); + } + + @Override + public ICIMClassCategory getBelongsToCategory() { + return null; + } + + @Override + public List getStereotypes() { + return List.of(); + } + + @Override + public List getAttributes() { + return List.of(); + } + + @Override + public List getAssociations() { + return List.of(); + } + + @Override + public List getEnumEntries() { + return List.of(); + } +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMAssociation.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMAssociation.java index f87b3ed4..a993107e 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMAssociation.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMAssociation.java @@ -26,6 +26,8 @@ public interface ICIMAssociation extends ICIMResource{ ICIMClass getDomain(); + ICIMClass getRange(); + ICIMAssociation getInverseAssociation(); CIMSAssociationUsed getAssociationUsed(); diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClassCategory.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClassCategory.java index 86415940..d29bb68b 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClassCategory.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClassCategory.java @@ -17,8 +17,6 @@ package org.rdfarchitect.models.cim.data.dto.facade; -import java.util.UUID; - public interface ICIMClassCategory extends ICIMResource{ } diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMEnumEntry.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMEnumEntry.java index d2b1f22e..95328929 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMEnumEntry.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMEnumEntry.java @@ -20,9 +20,9 @@ import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; -public interface ICIMEnumEntry { +public interface ICIMEnumEntry extends ICIMResource { - ICIMClass domain(); + ICIMClass getDomain(); CIMSStereotype getStereotype(); } diff --git a/backend/src/test/java/org/rdfarchitect/models/cim/data/dto/facade/CIMFacadeTest.java b/backend/src/test/java/org/rdfarchitect/models/cim/data/dto/facade/CIMFacadeTest.java new file mode 100644 index 00000000..e50e97b3 --- /dev/null +++ b/backend/src/test/java/org/rdfarchitect/models/cim/data/dto/facade/CIMFacadeTest.java @@ -0,0 +1,295 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.models.cim.data.dto.facade; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import org.apache.jena.vocabulary.XSD; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSAssociationUsed; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSMultiplicity; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; +import org.rdfarchitect.models.cim.rdf.resources.CIMS; +import org.rdfarchitect.models.cim.rdf.resources.CIMStereotypes; +import org.rdfarchitect.models.cim.rdf.resources.RDFA; + +import java.util.UUID; + +class CIMFacadeTest { + + private static final String GRAPH_URI = "http://graph#"; + private static final String NS = "http://example.com#"; + + private static final UUID CATEGORY_UUID = UUID.fromString("00000000-0000-0000-0000-000000000001"); + private static final UUID SWITCH_UUID = UUID.fromString("00000000-0000-0000-0000-000000000002"); + private static final UUID BREAKER_UUID = UUID.fromString("00000000-0000-0000-0000-000000000003"); + private static final UUID ATTRIBUTE_UUID = UUID.fromString("00000000-0000-0000-0000-000000000004"); + private static final UUID TERMINAL_UUID = UUID.fromString("00000000-0000-0000-0000-000000000005"); + private static final UUID ASSOCIATION_UUID = UUID.fromString("00000000-0000-0000-0000-000000000006"); + private static final UUID INVERSE_UUID = UUID.fromString("00000000-0000-0000-0000-000000000007"); + private static final UUID ENUM_UUID = UUID.fromString("00000000-0000-0000-0000-000000000008"); + private static final UUID ENTRY_UUID = UUID.fromString("00000000-0000-0000-0000-000000000009"); + + private Model model; + + @BeforeEach + void setUp() { + model = ModelFactory.createDefaultModel(); + + var category = model.createResource(NS + "CorePackage"); + category.addProperty(RDF.type, CIMS.classCategory); + category.addProperty(RDFA.uuid, CATEGORY_UUID.toString()); + category.addProperty(RDFS.label, model.createLiteral("Core", "en")); + + var switchClass = model.createResource(NS + "Switch"); + switchClass.addProperty(RDF.type, RDFS.Class); + switchClass.addProperty(RDFA.uuid, SWITCH_UUID.toString()); + switchClass.addProperty(RDFS.label, model.createLiteral("Switch", "en")); + + var externalBase = model.createResource(NS + "ExternalBase"); + + var breaker = model.createResource(NS + "Breaker"); + breaker.addProperty(RDF.type, RDFS.Class); + breaker.addProperty(RDFA.uuid, BREAKER_UUID.toString()); + breaker.addProperty(RDFS.label, model.createLiteral("Breaker", "en")); + breaker.addProperty(RDFS.comment, "A breaker"); + breaker.addProperty(RDFS.subClassOf, switchClass); + breaker.addProperty(RDFS.subClassOf, externalBase); + breaker.addProperty(CIMS.belongsToCategory, category); + breaker.addProperty(CIMS.stereotype, CIMStereotypes.concrete); + breaker.addProperty(CIMS.stereotype, CIMStereotypes.entsoe); + + var attribute = model.createResource(NS + "Breaker.value"); + attribute.addProperty(RDF.type, RDF.Property); + attribute.addProperty(RDFA.uuid, ATTRIBUTE_UUID.toString()); + attribute.addProperty(RDFS.label, model.createLiteral("value", "en")); + attribute.addProperty(CIMS.stereotype, CIMStereotypes.attribute); + attribute.addProperty(RDFS.domain, breaker); + attribute.addProperty(CIMS.multiplicity, model.createResource(CIMS.namespace + "M:0..1")); + attribute.addProperty(CIMS.datatype, model.createResource(XSD.xstring.getURI())); + attribute.addProperty(CIMS.isFixed, "fixedValue"); + var wrapper = model.createResource(); + wrapper.addProperty(ResourceFactory.createProperty(RDFS.Literal.getURI()), "defaultValue"); + attribute.addProperty(CIMS.isDefault, wrapper); + + var terminal = model.createResource(NS + "Terminal"); + terminal.addProperty(RDF.type, RDFS.Class); + terminal.addProperty(RDFA.uuid, TERMINAL_UUID.toString()); + terminal.addProperty(RDFS.label, model.createLiteral("Terminal", "en")); + + var association = model.createResource(NS + "Breaker.Terminals"); + association.addProperty(RDF.type, RDF.Property); + association.addProperty(RDFA.uuid, ASSOCIATION_UUID.toString()); + association.addProperty(RDFS.label, model.createLiteral("Terminals", "en")); + association.addProperty(RDFS.domain, breaker); + association.addProperty(RDFS.range, terminal); + association.addProperty(CIMS.multiplicity, model.createResource(CIMS.namespace + "M:0..n")); + association.addProperty(CIMS.associationUsed, "Yes"); + + var inverse = model.createResource(NS + "Terminal.Breaker"); + inverse.addProperty(RDF.type, RDF.Property); + inverse.addProperty(RDFA.uuid, INVERSE_UUID.toString()); + inverse.addProperty(RDFS.label, model.createLiteral("Breaker", "en")); + inverse.addProperty(RDFS.domain, terminal); + inverse.addProperty(RDFS.range, breaker); + inverse.addProperty(CIMS.associationUsed, "No"); + inverse.addProperty(CIMS.inverseRoleName, association); + association.addProperty(CIMS.inverseRoleName, inverse); + + var phaseCode = model.createResource(NS + "PhaseCode"); + phaseCode.addProperty(RDF.type, RDFS.Class); + phaseCode.addProperty(RDFA.uuid, ENUM_UUID.toString()); + phaseCode.addProperty(RDFS.label, model.createLiteral("PhaseCode", "en")); + phaseCode.addProperty(CIMS.stereotype, CIMStereotypes.enumeration); + + var entry = model.createResource(NS + "PhaseCode.A"); + entry.addProperty(RDF.type, phaseCode); + entry.addProperty(RDFA.uuid, ENTRY_UUID.toString()); + entry.addProperty(RDFS.label, model.createLiteral("A", "en")); + entry.addProperty(CIMS.stereotype, CIMStereotypes.enumLiteral); + } + + private CIMClass breaker() { + return new CIMClass(GRAPH_URI, model, BREAKER_UUID); + } + + @Test + @DisplayName("resolves label, comment, uri and graph uri of a class") + void classBaseProperties() { + var breaker = breaker(); + + assertThat(breaker.getUuid()).isEqualTo(BREAKER_UUID); + assertThat(breaker.getGraphUri()).isEqualTo(GRAPH_URI); + assertThat(breaker.getUri().toString()).isEqualTo(NS + "Breaker"); + assertThat(breaker.getLabel().getValue()).isEqualTo("Breaker"); + assertThat(breaker.getLabel().getLang()).isEqualTo("en"); + assertThat(breaker.getComment().getValue()).isEqualTo("A breaker"); + } + + @Test + @DisplayName("returns internal super classes as CIMClass and external ones as ExternalCIMClass") + void superClasses() { + var superClasses = breaker().getSuperClasses(); + + assertThat(superClasses).hasSize(2); + assertThat(superClasses) + .filteredOn(CIMClass.class::isInstance) + .singleElement() + .satisfies(internal -> assertThat(internal.getUuid()).isEqualTo(SWITCH_UUID)); + assertThat(superClasses) + .filteredOn(ExternalCIMClass.class::isInstance) + .singleElement() + .satisfies(external -> { + assertThat(external.getUuid()).isNull(); + assertThat(external.getUri().toString()).isEqualTo(NS + "ExternalBase"); + assertThat(external.getLabel().getValue()).isEqualTo("ExternalBase"); + assertThat(external.getComment()).isNull(); + }); + } + + @Test + @DisplayName("resolves the category a class belongs to") + void belongsToCategory() { + var category = breaker().getBelongsToCategory(); + + assertThat(category).isNotNull(); + assertThat(category.getUuid()).isEqualTo(CATEGORY_UUID); + assertThat(category.getLabel().getValue()).isEqualTo("Core"); + } + + @Test + @DisplayName("returns null when a class belongs to no category") + void belongsToNoCategory() { + var terminal = new CIMClass(GRAPH_URI, model, TERMINAL_UUID); + + assertThat(terminal.getBelongsToCategory()).isNull(); + } + + @Test + @DisplayName("returns uri and literal stereotypes") + void stereotypes() { + var stereotypes = breaker().getStereotypes(); + + assertThat(stereotypes) + .containsExactlyInAnyOrder( + new CIMSStereotype(CIMStereotypes.concreteString), + new CIMSStereotype(CIMStereotypes.entsoeString)); + } + + @Test + @DisplayName("returns only direct attributes with all their relations") + void attributes() { + var attributes = breaker().getAttributes(); + + assertThat(attributes).hasSize(1); + var attribute = attributes.getFirst(); + assertThat(attribute.getUuid()).isEqualTo(ATTRIBUTE_UUID); + assertThat(attribute.getDomain().getUuid()).isEqualTo(BREAKER_UUID); + assertThat(attribute.getMultiplicity()) + .isEqualTo(new CIMSMultiplicity(CIMS.namespace + "M:0..1")); + assertThat(attribute.getStereotype()) + .isEqualTo(new CIMSStereotype(CIMStereotypes.attributeString)); + assertThat(attribute.getDataType()).isInstanceOf(ExternalCIMClass.class); + assertThat(attribute.getDataType().getUri().toString()).isEqualTo(XSD.xstring.getURI()); + } + + @Test + @DisplayName("reads fixed values from direct literals and default values from blank-node wrappers") + void fixedAndDefaultValues() { + var attribute = breaker().getAttributes().getFirst(); + + var fixed = attribute.getFixed(); + assertThat(fixed).isNotNull(); + assertThat(fixed.getValue()).isEqualTo("fixedValue"); + assertThat(fixed.isBlankNode()).isFalse(); + + var defaultValue = attribute.getDefault(); + assertThat(defaultValue).isNotNull(); + assertThat(defaultValue.getValue()).isEqualTo("defaultValue"); + assertThat(defaultValue.isBlankNode()).isTrue(); + } + + @Test + @DisplayName("returns only direct associations with domain, range, inverse and associationUsed") + void associations() { + var associations = breaker().getAssociations(); + + assertThat(associations).hasSize(1); + var association = associations.getFirst(); + assertThat(association.getUuid()).isEqualTo(ASSOCIATION_UUID); + assertThat(association.getDomain().getUuid()).isEqualTo(BREAKER_UUID); + assertThat(association.getRange().getUuid()).isEqualTo(TERMINAL_UUID); + assertThat(association.getMultiplicity()) + .isEqualTo(new CIMSMultiplicity(CIMS.namespace + "M:0..n")); + assertThat(association.getAssociationUsed()).isEqualTo(new CIMSAssociationUsed("Yes")); + + var inverse = association.getInverseAssociation(); + assertThat(inverse).isNotNull(); + assertThat(inverse.getUuid()).isEqualTo(INVERSE_UUID); + assertThat(inverse.getDomain().getUuid()).isEqualTo(TERMINAL_UUID); + assertThat(inverse.getAssociationUsed()).isEqualTo(new CIMSAssociationUsed("No")); + } + + @Test + @DisplayName("throws for attributes and associations missing required properties") + void requiredProperties() { + var bareUuid = UUID.fromString("00000000-0000-0000-0000-00000000000a"); + var bare = model.createResource(NS + "Breaker.bare"); + bare.addProperty(RDF.type, RDF.Property); + bare.addProperty(RDFA.uuid, bareUuid.toString()); + + var attribute = new CIMAttribute(GRAPH_URI, model, bareUuid); + assertThatThrownBy(attribute::getDomain).isInstanceOf(IllegalStateException.class); + assertThatThrownBy(attribute::getMultiplicity).isInstanceOf(IllegalStateException.class); + assertThatThrownBy(attribute::getDataType).isInstanceOf(IllegalStateException.class); + assertThatThrownBy(attribute::getStereotype).isInstanceOf(IllegalStateException.class); + + var association = new CIMAssociation(GRAPH_URI, model, bareUuid); + assertThatThrownBy(association::getRange).isInstanceOf(IllegalStateException.class); + assertThatThrownBy(association::getInverseAssociation) + .isInstanceOf(IllegalStateException.class); + assertThatThrownBy(association::getAssociationUsed) + .isInstanceOf(IllegalStateException.class); + assertThatThrownBy(association::getMultiplicity).isInstanceOf(IllegalStateException.class); + } + + @Test + @DisplayName("returns enum entries for enumeration classes and none for regular classes") + void enumEntries() { + var phaseCode = new CIMClass(GRAPH_URI, model, ENUM_UUID); + + var entries = phaseCode.getEnumEntries(); + assertThat(entries).hasSize(1); + var entry = entries.getFirst(); + assertThat(entry.getUuid()).isEqualTo(ENTRY_UUID); + assertThat(entry.getDomain().getUuid()).isEqualTo(ENUM_UUID); + assertThat(entry.getStereotype()) + .isEqualTo(new CIMSStereotype(CIMStereotypes.enumLiteralString)); + + assertThat(breaker().getEnumEntries()).isEmpty(); + } +} From a0bb047941b4c2fafa6e1c864d734b9a1604152c Mon Sep 17 00:00:00 2001 From: Maximilian Reisch Date: Thu, 16 Jul 2026 13:25:29 +0200 Subject: [PATCH 3/5] continued impl --- .../models/cim/data/dto/facade/CIMClass.java | 2 +- .../cim/data/dto/facade/CIMClassCategory.java | 22 +++++ .../cim/data/dto/facade/CIMModelFacade.java | 81 +++++++++++++++++ .../dto/facade/DefaultCIMClassCategory.java | 76 ++++++++++++++++ .../cim/data/dto/facade/ExternalCIMClass.java | 6 +- .../data/dto/facade/ICIMClassCategory.java | 3 + .../cim/data/dto/facade/ICIMModelFacade.java | 32 +++++++ ...rCIMFacadeCollectionSvelteFlowService.java | 86 +++++++++++++++++++ 8 files changed, 306 insertions(+), 2 deletions(-) create mode 100644 backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMModelFacade.java create mode 100644 backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/DefaultCIMClassCategory.java create mode 100644 backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMModelFacade.java create mode 100644 backend/src/main/java/org/rdfarchitect/services/rendering/svelteflow/RenderCIMFacadeCollectionSvelteFlowService.java diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClass.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClass.java index a85d277c..86e05336 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClass.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClass.java @@ -63,7 +63,7 @@ public ICIMClassCategory getBelongsToCategory() { if(category == null){ return null; } - return new CIMClassCategory(getGraphUri(), getModel(), category); + return CIMClassCategory.fromResource(getGraphUri(), getModel(), category); } @Override diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClassCategory.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClassCategory.java index 02e3863e..919158df 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClassCategory.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClassCategory.java @@ -17,8 +17,13 @@ package org.rdfarchitect.models.cim.data.dto.facade; +import org.apache.jena.graph.NodeFactory; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Resource; +import org.rdfarchitect.models.cim.rdf.resources.CIMS; +import org.rdfarchitect.models.cim.rdf.resources.RDFA; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; public class CIMClassCategory extends CIMResource implements ICIMClassCategory { @@ -30,4 +35,21 @@ public CIMClassCategory(String graphUri, Model model, UUID uuid) { public CIMClassCategory(String graphUri, Model model, Resource resource) { super(graphUri, model, resource); } + + public static ICIMClassCategory fromResource(String graphUri, Model model, Resource resource) { + if(resource == null){ + return new DefaultCIMClassCategory(graphUri, model); + } + return new CIMClassCategory(graphUri, model, resource); + } + + @Override + public List getClasses() { + var resourcesInPackage = getModel().listSubjectsWithProperty(CIMS.belongsToCategory, this.getJenaResource()).toList(); + var classes = new ArrayList(); + for(var resource : resourcesInPackage){ + classes.add(CIMClass.fromResource(getGraphUri(), getModel(), resource)); + } + return classes; + } } diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMModelFacade.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMModelFacade.java new file mode 100644 index 00000000..02670a1c --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMModelFacade.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.models.cim.data.dto.facade; + +import lombok.RequiredArgsConstructor; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; +import org.rdfarchitect.models.cim.rdf.resources.CIMS; +import org.rdfarchitect.models.cim.rdf.resources.CIMStereotypes; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@RequiredArgsConstructor +public class CIMModelFacade implements ICIMModelFacade { + + private final String graphUri; + + private final Model model; + + @Override + public String getGraphUri() { + return this.graphUri; + } + + @Override + public List getCIMClasses() { + var classResources = model.listSubjectsWithProperty(RDF.type, RDFS.Class).toList(); + var classes = new ArrayList(); + for (var classResource : classResources) { + classes.add(new CIMClass(this.graphUri, this.model, classResource)); + } + return classes; + } + + @Override + public List getCIMClassCategories() { + var packageResources = model.listSubjectsWithProperty(RDF.type, CIMS.classCategory).toList(); + var packages = new ArrayList(); + for (var packageResource : packageResources) { + packages.add(CIMClassCategory.fromResource(this.graphUri, this.model, packageResource)); + } + packages.add(new DefaultCIMClassCategory(this.graphUri, this.model)); + return packages; + } + + public List getCIMAttributes() { + var attributeResources = model.listSubjectsWithProperty(CIMS.stereotype, CIMStereotypes.attribute).toList(); + var attributes = new ArrayList(); + for (var attributeResource : attributeResources) { + attributes.add(new CIMAttribute(this.graphUri, this.model, attributeResource)); + } + return attributes; + } + + public List getCIMAssociations() { + var associationResources = model.listSubjectsWithProperty(CIMS.inverseRoleName).toList(); + var associations = new ArrayList(); + for (var associationResource : associationResources) { + associations.add(new CIMAssociation(this.graphUri, this.model, associationResource)); + } + return associations; + } +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/DefaultCIMClassCategory.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/DefaultCIMClassCategory.java new file mode 100644 index 00000000..d7034c4f --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/DefaultCIMClassCategory.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.models.cim.data.dto.facade; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import org.rdfarchitect.models.cim.data.dto.relations.RDFSComment; +import org.rdfarchitect.models.cim.data.dto.relations.RDFSLabel; +import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; + +public class DefaultCIMClassCategory implements ICIMClassCategory { + + private final String graphUri; + + private final Model model; + + public DefaultCIMClassCategory(String graphUri, Model model) { + this.graphUri = graphUri; + this.model = model; + } + + @Override + public UUID getUuid() { + return null; + } + + @Override + public String getGraphUri() { + return graphUri; + } + + @Override + public URI getUri() { + return null; + } + + @Override + public RDFSLabel getLabel() { + return new RDFSLabel("default"); + } + + @Override + public RDFSComment getComment() { + return null; + } + + + @Override + public List getClasses() { + var classResources = model.listSubjectsWithProperty(RDF.type, RDFS.Class).filterDrop(resource -> resource.hasProperty(RDFS.subClassOf)).toList(); + var classes = new ArrayList(); + for (var classResource : classResources){ + classes.add(new CIMClass(graphUri, model, classResource)); + } + return classes; + } +} diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ExternalCIMClass.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ExternalCIMClass.java index af5e273c..42177664 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ExternalCIMClass.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ExternalCIMClass.java @@ -24,6 +24,7 @@ import org.rdfarchitect.models.cim.data.dto.relations.RDFSComment; import org.rdfarchitect.models.cim.data.dto.relations.RDFSLabel; import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; +import org.rdfarchitect.models.cim.rdf.resources.RDFA; import java.util.List; import java.util.UUID; @@ -46,7 +47,10 @@ public ExternalCIMClass(String graphUri, Model model, Resource resource) { @Override public UUID getUuid() { - return null; + if(!this.resource.hasProperty(RDFA.uuid)){ + return null; + } + return UUID.fromString(this.resource.getProperty(RDFA.uuid).getObject().toString()); } @Override diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClassCategory.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClassCategory.java index d29bb68b..1e573955 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClassCategory.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClassCategory.java @@ -17,6 +17,9 @@ package org.rdfarchitect.models.cim.data.dto.facade; +import java.util.List; + public interface ICIMClassCategory extends ICIMResource{ + List getClasses(); } diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMModelFacade.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMModelFacade.java new file mode 100644 index 00000000..7ea0aace --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMModelFacade.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.models.cim.data.dto.facade; + +import java.util.List; +import java.util.UUID; + +public interface ICIMModelFacade { + + String getGraphUri(); + + List getCIMClasses(); + + List getCIMClassCategories(); + + ICIMClassCategory getCIMClassCategory(UUID uuid); +} diff --git a/backend/src/main/java/org/rdfarchitect/services/rendering/svelteflow/RenderCIMFacadeCollectionSvelteFlowService.java b/backend/src/main/java/org/rdfarchitect/services/rendering/svelteflow/RenderCIMFacadeCollectionSvelteFlowService.java new file mode 100644 index 00000000..df2ce475 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/services/rendering/svelteflow/RenderCIMFacadeCollectionSvelteFlowService.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.services.rendering.svelteflow; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.apache.jena.rdf.model.Model; +import org.rdfarchitect.api.dto.dl.RenderingLayoutData; +import org.rdfarchitect.api.dto.rendering.RenderingDataDTO; +import org.rdfarchitect.api.dto.rendering.svelteflow.SvelteFlowDTO; +import org.rdfarchitect.api.dto.rendering.svelteflow.sub.AttributeDTO; +import org.rdfarchitect.api.dto.rendering.svelteflow.sub.EdgeDTO; +import org.rdfarchitect.api.dto.rendering.svelteflow.sub.EdgeDataDTO; +import org.rdfarchitect.api.dto.rendering.svelteflow.sub.EnumEntryDTO; +import org.rdfarchitect.api.dto.rendering.svelteflow.sub.NodeDTO; +import org.rdfarchitect.api.dto.rendering.svelteflow.sub.NodeDataDTO; +import org.rdfarchitect.api.dto.rendering.svelteflow.sub.PositionDTO; +import org.rdfarchitect.models.cim.data.dto.CIMAssociation; +import org.rdfarchitect.models.cim.data.dto.CIMClass; +import org.rdfarchitect.models.cim.data.dto.CIMCollection; +import org.rdfarchitect.models.cim.data.dto.facade.CIMModelFacade; +import org.rdfarchitect.models.cim.data.dto.facade.DefaultCIMClassCategory; +import org.rdfarchitect.models.cim.data.dto.facade.ICIMClass; +import org.rdfarchitect.models.cim.data.dto.facade.ICIMClassCategory; +import org.rdfarchitect.models.cim.data.dto.facade.ICIMModelFacade; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSAssociationUsed; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSMultiplicity; +import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; +import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; +import org.rdfarchitect.models.cim.rdf.resources.CIMStereotypes; +import org.rdfarchitect.models.cim.rendering.GraphFilter; +import org.rdfarchitect.models.cim.rendering.RenderingUtils; +import org.rdfarchitect.services.dl.select.FetchRenderingLayoutDataUseCase; +import org.rdfarchitect.services.rendering.DiagramToCIMCollectionConverterUseCase; +import org.rdfarchitect.services.rendering.RenderCIMCollectionUseCase; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +/** + * Converts a {@link CIMCollection} to a DTO Record that contains two JSON arrays with nodes and + * edges used to render a UML diagram using the JavaScript library SvelteFlow. + */ +@RequiredArgsConstructor +@Service +@ConditionalOnProperty( + name = "rendering.renderer", + havingValue = "svelteflow", + matchIfMissing = true) +public class RenderCIMFacadeCollectionSvelteFlowService { + + private final FetchRenderingLayoutDataUseCase fetchRenderingLayoutDataUseCase; + + private final DiagramToCIMCollectionConverterUseCase converter; + + // CONSTANTS FOR SVELTEFLOW CUSTOM NODE/EDGE TYPES + private static final String CLASS_NODE_TYPE = "class"; + private static final String INHERITANCE_EDGE_TYPE = "inheritance"; + private static final String ASSOCIATION_EDGE_TYPE = "association"; + + public RenderingDataDTO renderUML(ICIMModelFacade cimModel, GraphFilter filter, RenderingLayoutData layoutData) { + var pack = cimModel.getCIMClassCategory(filter.getPackageUUID() == null ? null : UUID.fromString(filter.getPackageUUID())); + var classes = pack.getClasses(); + + return null; + } +} From 275c79265ce834a13daeae24e8c9c6c338780277 Mon Sep 17 00:00:00 2001 From: Maximilian Reisch Date: Fri, 17 Jul 2026 10:51:23 +0200 Subject: [PATCH 4/5] refactored rendering to use new facade to include superclasses Signed-off-by: Maximilian Reisch --- .../svelteflow/sub/SuperClassDTO.java | 6 + .../cim/data/dto/facade/CIMClassCategory.java | 10 + .../cim/data/dto/facade/CIMModelFacade.java | 31 +- .../dto/facade/DefaultCIMClassCategory.java | 3 +- .../services/GetRenderingDataService.java | 14 +- .../RenderCIMFacadeCollectionUseCase.java | 40 ++ ...rCIMFacadeCollectionSvelteFlowService.java | 368 +++++++++++++++- .../cim/data/dto/facade/CIMFacadeTest.java | 43 ++ ...FacadeCollectionSvelteFlowServiceTest.java | 411 ++++++++++++++++++ 9 files changed, 894 insertions(+), 32 deletions(-) create mode 100644 backend/src/main/java/org/rdfarchitect/services/rendering/RenderCIMFacadeCollectionUseCase.java create mode 100644 backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMFacadeCollectionSvelteFlowServiceTest.java diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/SuperClassDTO.java b/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/SuperClassDTO.java index 349d82d1..1b12ba2d 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/SuperClassDTO.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/SuperClassDTO.java @@ -17,9 +17,15 @@ package org.rdfarchitect.api.dto.rendering.svelteflow.sub; +import lombok.Builder; +import lombok.Data; + import java.util.List; import java.util.UUID; +/** DTO representing a super class of a SvelteFlow class node including its inherited members. */ +@Data +@Builder public class SuperClassDTO { private UUID uuid; diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClassCategory.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClassCategory.java index 919158df..0f77ef13 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClassCategory.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClassCategory.java @@ -20,6 +20,8 @@ import org.apache.jena.graph.NodeFactory; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDFS; +import org.rdfarchitect.models.cim.data.dto.relations.RDFSLabel; import org.rdfarchitect.models.cim.rdf.resources.CIMS; import org.rdfarchitect.models.cim.rdf.resources.RDFA; import java.util.ArrayList; @@ -43,6 +45,14 @@ public static ICIMClassCategory fromResource(String graphUri, Model model, Resou return new CIMClassCategory(graphUri, model, resource); } + @Override + public RDFSLabel getLabel() { + if (getUniqueJenaPropertyNode(RDFS.label) == null) { + return new RDFSLabel(getUri().getSuffix()); + } + return super.getLabel(); + } + @Override public List getClasses() { var resourcesInPackage = getModel().listSubjectsWithProperty(CIMS.belongsToCategory, this.getJenaResource()).toList(); diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMModelFacade.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMModelFacade.java index 02670a1c..2338a632 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMModelFacade.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMModelFacade.java @@ -19,12 +19,16 @@ import lombok.RequiredArgsConstructor; import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Resource; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; import org.rdfarchitect.models.cim.rdf.resources.CIMS; import org.rdfarchitect.models.cim.rdf.resources.CIMStereotypes; +import org.rdfarchitect.models.cim.rdf.resources.RDFA; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; import java.util.UUID; @@ -52,7 +56,13 @@ public List getCIMClasses() { @Override public List getCIMClassCategories() { - var packageResources = model.listSubjectsWithProperty(RDF.type, CIMS.classCategory).toList(); + var packageResources = new LinkedHashSet( + model.listSubjectsWithProperty(RDF.type, CIMS.classCategory).toList()); + model.listObjectsOfProperty(CIMS.belongsToCategory) + .filterKeep(RDFNode::isURIResource) + .mapWith(RDFNode::asResource) + .filterKeep(resource -> resource.hasProperty(RDFA.uuid)) + .forEach(packageResources::add); var packages = new ArrayList(); for (var packageResource : packageResources) { packages.add(CIMClassCategory.fromResource(this.graphUri, this.model, packageResource)); @@ -61,6 +71,25 @@ public List getCIMClassCategories() { return packages; } + @Override + public ICIMClassCategory getCIMClassCategory(UUID uuid) { + if (uuid == null) { + return new DefaultCIMClassCategory(this.graphUri, this.model); + } + var categoryResources = model.listSubjectsWithProperty(RDFA.uuid, uuid.toString()) + .filterKeep(this::isClassCategory) + .toList(); + if (categoryResources.isEmpty()) { + return null; + } + return new CIMClassCategory(this.graphUri, this.model, categoryResources.getFirst()); + } + + private boolean isClassCategory(Resource resource) { + return resource.hasProperty(RDF.type, CIMS.classCategory) + || model.contains(null, CIMS.belongsToCategory, resource); + } + public List getCIMAttributes() { var attributeResources = model.listSubjectsWithProperty(CIMS.stereotype, CIMStereotypes.attribute).toList(); var attributes = new ArrayList(); diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/DefaultCIMClassCategory.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/DefaultCIMClassCategory.java index d7034c4f..e965eb35 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/DefaultCIMClassCategory.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/DefaultCIMClassCategory.java @@ -26,6 +26,7 @@ import org.rdfarchitect.models.cim.data.dto.relations.RDFSComment; import org.rdfarchitect.models.cim.data.dto.relations.RDFSLabel; import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; +import org.rdfarchitect.models.cim.rdf.resources.CIMS; public class DefaultCIMClassCategory implements ICIMClassCategory { @@ -66,7 +67,7 @@ public RDFSComment getComment() { @Override public List getClasses() { - var classResources = model.listSubjectsWithProperty(RDF.type, RDFS.Class).filterDrop(resource -> resource.hasProperty(RDFS.subClassOf)).toList(); + var classResources = model.listSubjectsWithProperty(RDF.type, RDFS.Class).filterDrop(resource -> resource.hasProperty(CIMS.belongsToCategory)).toList(); var classes = new ArrayList(); for (var classResource : classResources){ classes.add(new CIMClass(graphUri, model, classResource)); diff --git a/backend/src/main/java/org/rdfarchitect/services/GetRenderingDataService.java b/backend/src/main/java/org/rdfarchitect/services/GetRenderingDataService.java index 7ed990bb..5a1cb292 100644 --- a/backend/src/main/java/org/rdfarchitect/services/GetRenderingDataService.java +++ b/backend/src/main/java/org/rdfarchitect/services/GetRenderingDataService.java @@ -21,15 +21,16 @@ import org.apache.jena.graph.Graph; import org.apache.jena.query.ReadWrite; +import org.apache.jena.rdf.model.ModelFactory; import org.rdfarchitect.api.dto.dl.RenderingLayoutData; import org.rdfarchitect.api.dto.rendering.RenderingDataDTO; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.dl.queries.select.DLObjectFetcher; +import org.rdfarchitect.models.cim.data.dto.facade.CIMModelFacade; import org.rdfarchitect.models.cim.rendering.GraphFilter; import org.rdfarchitect.rdf.graph.GraphUtils; -import org.rdfarchitect.services.rendering.GraphToCIMCollectionConverterUseCase; -import org.rdfarchitect.services.rendering.RenderCIMCollectionUseCase; +import org.rdfarchitect.services.rendering.RenderCIMFacadeCollectionUseCase; import org.springframework.stereotype.Service; import java.util.UUID; @@ -39,8 +40,7 @@ public class GetRenderingDataService implements GetRenderingDataUseCase { private final DatabasePort databasePort; - private final GraphToCIMCollectionConverterUseCase converter; - private final RenderCIMCollectionUseCase renderer; + private final RenderCIMFacadeCollectionUseCase renderer; @Override public RenderingDataDTO getRenderingData( @@ -64,7 +64,9 @@ public RenderingDataDTO getRenderingData( RenderingLayoutData.builder().classLayoutingData(classLayoutingData).build(); } - var cimCollection = converter.convert(rdfGraphCopy, graphIdentifier, filter); - return renderer.renderUML(cimCollection, layoutData); + var cimModel = + new CIMModelFacade( + graphIdentifier.graphUri(), ModelFactory.createModelForGraph(rdfGraphCopy)); + return renderer.renderUML(cimModel, filter, layoutData); } } diff --git a/backend/src/main/java/org/rdfarchitect/services/rendering/RenderCIMFacadeCollectionUseCase.java b/backend/src/main/java/org/rdfarchitect/services/rendering/RenderCIMFacadeCollectionUseCase.java new file mode 100644 index 00000000..855570b0 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/services/rendering/RenderCIMFacadeCollectionUseCase.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.services.rendering; + +import org.rdfarchitect.api.dto.dl.RenderingLayoutData; +import org.rdfarchitect.api.dto.rendering.RenderingDataDTO; +import org.rdfarchitect.models.cim.data.dto.facade.ICIMModelFacade; +import org.rdfarchitect.models.cim.rendering.GraphFilter; + +/** + * Converts a {@link ICIMModelFacade} to a DTO that contains data required to render a UML diagram. + */ +public interface RenderCIMFacadeCollectionUseCase { + + /** + * Generates the rendering data for a CIM model using pre-fetched layout data. + * + * @param cimModel the CIM model facade to read the diagram content from + * @param filter filter deciding which parts of the model are rendered + * @param layoutData pre-fetched diagram layout data (may be null) + * @return a dto that contains all data required to render a UML diagram for the given model + */ + RenderingDataDTO renderUML( + ICIMModelFacade cimModel, GraphFilter filter, RenderingLayoutData layoutData); +} diff --git a/backend/src/main/java/org/rdfarchitect/services/rendering/svelteflow/RenderCIMFacadeCollectionSvelteFlowService.java b/backend/src/main/java/org/rdfarchitect/services/rendering/svelteflow/RenderCIMFacadeCollectionSvelteFlowService.java index df2ce475..2b3a5784 100644 --- a/backend/src/main/java/org/rdfarchitect/services/rendering/svelteflow/RenderCIMFacadeCollectionSvelteFlowService.java +++ b/backend/src/main/java/org/rdfarchitect/services/rendering/svelteflow/RenderCIMFacadeCollectionSvelteFlowService.java @@ -17,13 +17,15 @@ package org.rdfarchitect.services.rendering.svelteflow; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; -import lombok.RequiredArgsConstructor; -import org.apache.jena.rdf.model.Model; +import java.util.stream.Collectors; import org.rdfarchitect.api.dto.dl.RenderingLayoutData; import org.rdfarchitect.api.dto.rendering.RenderingDataDTO; import org.rdfarchitect.api.dto.rendering.svelteflow.SvelteFlowDTO; @@ -34,53 +36,371 @@ import org.rdfarchitect.api.dto.rendering.svelteflow.sub.NodeDTO; import org.rdfarchitect.api.dto.rendering.svelteflow.sub.NodeDataDTO; import org.rdfarchitect.api.dto.rendering.svelteflow.sub.PositionDTO; -import org.rdfarchitect.models.cim.data.dto.CIMAssociation; -import org.rdfarchitect.models.cim.data.dto.CIMClass; -import org.rdfarchitect.models.cim.data.dto.CIMCollection; -import org.rdfarchitect.models.cim.data.dto.facade.CIMModelFacade; -import org.rdfarchitect.models.cim.data.dto.facade.DefaultCIMClassCategory; +import org.rdfarchitect.api.dto.rendering.svelteflow.sub.SuperClassDTO; +import org.rdfarchitect.models.cim.data.dto.facade.ICIMAssociation; import org.rdfarchitect.models.cim.data.dto.facade.ICIMClass; -import org.rdfarchitect.models.cim.data.dto.facade.ICIMClassCategory; import org.rdfarchitect.models.cim.data.dto.facade.ICIMModelFacade; import org.rdfarchitect.models.cim.data.dto.relations.CIMSAssociationUsed; import org.rdfarchitect.models.cim.data.dto.relations.CIMSMultiplicity; import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; -import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; import org.rdfarchitect.models.cim.rdf.resources.CIMStereotypes; import org.rdfarchitect.models.cim.rendering.GraphFilter; -import org.rdfarchitect.models.cim.rendering.RenderingUtils; -import org.rdfarchitect.services.dl.select.FetchRenderingLayoutDataUseCase; -import org.rdfarchitect.services.rendering.DiagramToCIMCollectionConverterUseCase; -import org.rdfarchitect.services.rendering.RenderCIMCollectionUseCase; +import org.rdfarchitect.services.rendering.RenderCIMFacadeCollectionUseCase; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; /** - * Converts a {@link CIMCollection} to a DTO Record that contains two JSON arrays with nodes and + * Converts a {@link ICIMModelFacade} to a DTO Record that contains two JSON arrays with nodes and * edges used to render a UML diagram using the JavaScript library SvelteFlow. */ -@RequiredArgsConstructor @Service @ConditionalOnProperty( name = "rendering.renderer", havingValue = "svelteflow", matchIfMissing = true) -public class RenderCIMFacadeCollectionSvelteFlowService { - - private final FetchRenderingLayoutDataUseCase fetchRenderingLayoutDataUseCase; - - private final DiagramToCIMCollectionConverterUseCase converter; +public class RenderCIMFacadeCollectionSvelteFlowService implements RenderCIMFacadeCollectionUseCase { // CONSTANTS FOR SVELTEFLOW CUSTOM NODE/EDGE TYPES private static final String CLASS_NODE_TYPE = "class"; private static final String INHERITANCE_EDGE_TYPE = "inheritance"; private static final String ASSOCIATION_EDGE_TYPE = "association"; - public RenderingDataDTO renderUML(ICIMModelFacade cimModel, GraphFilter filter, RenderingLayoutData layoutData) { - var pack = cimModel.getCIMClassCategory(filter.getPackageUUID() == null ? null : UUID.fromString(filter.getPackageUUID())); - var classes = pack.getClasses(); + private static final String DEFAULT_PACKAGE = "default"; + + @Override + public RenderingDataDTO renderUML( + ICIMModelFacade cimModel, GraphFilter filter, RenderingLayoutData layoutData) { + var classes = selectClasses(cimModel, filter); + if (classes.isEmpty()) { + return createEmptyDiagram(); + } + + var renderContext = + new RenderContext( + new ArrayList<>(classes.values()), + classes.values().stream() + .map(ICIMClass::getUuid) + .collect(Collectors.toSet()), + filter, + layoutData); + + var nodes = assembleNodeDTOList(renderContext); + var edges = assembleEdgeDTOList(renderContext); + + return SvelteFlowDTO.builder().nodes(nodes).edges(edges).build(); + } + + private SvelteFlowDTO createEmptyDiagram() { + return SvelteFlowDTO.builder().nodes(List.of()).edges(List.of()).build(); + } + + private Map selectClasses(ICIMModelFacade cimModel, GraphFilter filter) { + var classes = new LinkedHashMap(); + + if (!CollectionUtils.isEmpty(filter.getAllowedUUIDs())) { + var allowedUUIDs = + filter.getAllowedUUIDs().stream() + .map(UUID::fromString) + .collect(Collectors.toSet()); + for (var cimClass : cimModel.getCIMClasses()) { + if (allowedUUIDs.contains(cimClass.getUuid())) { + classes.put(cimClass.getUri().toString(), cimClass); + } + } + return classes; + } + + var category = cimModel.getCIMClassCategory(resolvePackageUUID(filter)); + if (category == null) { + return classes; + } + for (var cimClass : category.getClasses()) { + classes.put(cimClass.getUri().toString(), cimClass); + } + + if (filter.isIncludeRelationsToExternalPackages()) { + addExternallyRelatedClasses(cimModel, filter, classes); + } + + return classes; + } + + private UUID resolvePackageUUID(GraphFilter filter) { + if (filter.getPackageUUID() == null || filter.getPackageUUID().equals(DEFAULT_PACKAGE)) { + return null; + } + return UUID.fromString(filter.getPackageUUID()); + } + + private void addExternallyRelatedClasses( + ICIMModelFacade cimModel, GraphFilter filter, Map classes) { + var classesInPackage = List.copyOf(classes.values()); + + if (filter.isIncludeAssociations()) { + for (var cimClass : classesInPackage) { + for (var association : cimClass.getAssociations()) { + addExternallyRelatedClass(classes, association.getRange()); + } + } + } + + if (filter.isIncludeInheritance()) { + var packageUris = classes.keySet(); + for (var cimClass : classesInPackage) { + for (var superClass : cimClass.getSuperClasses()) { + addExternallyRelatedClass(classes, superClass); + } + } + for (var cimClass : cimModel.getCIMClasses()) { + if (classes.containsKey(cimClass.getUri().toString())) { + continue; + } + var extendsPackageClass = + cimClass.getSuperClasses().stream() + .anyMatch( + superClass -> + packageUris.contains( + superClass.getUri().toString())); + if (extendsPackageClass) { + classes.put(cimClass.getUri().toString(), cimClass); + } + } + } + } + + private void addExternallyRelatedClass(Map classes, ICIMClass cimClass) { + if (cimClass.getUuid() == null || classes.containsKey(cimClass.getUri().toString())) { + return; + } + classes.put(cimClass.getUri().toString(), cimClass); + } + + private List assembleNodeDTOList(RenderContext renderContext) { + List nodeDTOs = new ArrayList<>(); + for (var cimClass : renderContext.classes()) { + nodeDTOs.add(assembleNodeDTO(renderContext, cimClass)); + } + return nodeDTOs; + } + + private NodeDTO assembleNodeDTO(RenderContext renderContext, ICIMClass cimClass) { + var dop = + renderContext.layoutingData() != null + ? renderContext + .layoutingData() + .getClassLayoutingData() + .get(cimClass.getUuid()) + : null; + + var positionDTO = + dop != null + ? PositionDTO.builder() + .x(dop.getPosition().getX()) + .y(dop.getPosition().getY()) + .z(dop.getPosition().getZ()) + .build() + : PositionDTO.builder().x(0).y(0).z(0).build(); + + var nodeDataDTO = + NodeDataDTO.builder() + .graphUri(cimClass.getGraphUri()) + .label(cimClass.getLabel().getValue()) + .belongsToCategory( + cimClass.getBelongsToCategory() != null + ? cimClass.getBelongsToCategory().getLabel().getValue() + : null) + .stereotypes(getClassStereotypes(cimClass)) + .attributes(getClassAttributes(renderContext, cimClass)) + .enumEntries(getClassEnumEntries(renderContext, cimClass)) + .superClasses(getClassSuperClasses(renderContext, cimClass)) + .build(); + + return NodeDTO.builder() + .id(cimClass.getUuid()) + .type(CLASS_NODE_TYPE) + .position(positionDTO) + .data(nodeDataDTO) + .build(); + } + + private List getClassAttributes( + RenderContext renderContext, ICIMClass cimClass) { + if (!renderContext.filter().isIncludeAttributes()) { + return List.of(); + } - return null; + List attributeDTOs = new ArrayList<>(); + for (var cimAttribute : cimClass.getAttributes()) { + attributeDTOs.add( + AttributeDTO.builder() + .label(cimAttribute.getLabel().getValue()) + .type(cimAttribute.getDataType().getLabel().getValue()) + .multiplicity(extractMultiplicityString(cimAttribute.getMultiplicity())) + .build()); + } + return attributeDTOs; } + + private List getClassStereotypes(ICIMClass cimClass) { + var stereotypes = cimClass.getStereotypes(); + var stereotypesToRender = new ArrayList(); + + if (CollectionUtils.isEmpty(stereotypes) + || !stereotypes.contains(new CIMSStereotype(CIMStereotypes.concrete.toString()))) { + stereotypesToRender.add("abstract"); + } + + for (var stereotype : stereotypes) { + if (!stereotype.toString().equals(CIMStereotypes.concrete.toString())) { + String stereotypeToAdd = stereotype.toString(); + if (stereotype.toString().equals(CIMStereotypes.enumeration.toString())) { + stereotypeToAdd = CIMStereotypes.enumeration.getLocalName(); + } + stereotypesToRender.add(stereotypeToAdd); + } + } + + stereotypesToRender.sort(String::compareTo); + + return stereotypesToRender; + } + + private List getClassEnumEntries( + RenderContext renderContext, ICIMClass cimClass) { + if (!renderContext.filter().isIncludeEnumEntries()) { + return List.of(); + } + + List enumEntries = new ArrayList<>(); + for (var cimEnumEntry : cimClass.getEnumEntries()) { + enumEntries.add( + EnumEntryDTO.builder().label(cimEnumEntry.getLabel().getValue()).build()); + } + return enumEntries; + } + + private List getClassSuperClasses( + RenderContext renderContext, ICIMClass cimClass) { + var superClassDTOs = new ArrayList(); + var visitedUris = new HashSet(); + visitedUris.add(cimClass.getUri().toString()); + + var queue = new ArrayDeque<>(cimClass.getSuperClasses()); + while (!queue.isEmpty()) { + var superClass = queue.poll(); + if (!visitedUris.add(superClass.getUri().toString())) { + continue; + } + superClassDTOs.add( + SuperClassDTO.builder() + .uuid(superClass.getUuid()) + .label(superClass.getLabel().getValue()) + .attributes(getClassAttributes(renderContext, superClass)) + .enumEntries(getClassEnumEntries(renderContext, superClass)) + .build()); + queue.addAll(superClass.getSuperClasses()); + } + + return superClassDTOs; + } + + private List assembleEdgeDTOList(RenderContext renderContext) { + List edgeDTOList = new ArrayList<>(); + edgeDTOList.addAll(assembleInheritanceEdgeDTOList(renderContext)); + edgeDTOList.addAll(assembleAssociationEdgeDTOList(renderContext)); + return edgeDTOList; + } + + private List assembleInheritanceEdgeDTOList(RenderContext renderContext) { + if (!renderContext.filter().isIncludeInheritance()) { + return List.of(); + } + + List inheritanceEdgeDTOList = new ArrayList<>(); + for (var cimClass : renderContext.classes()) { + for (var superClass : cimClass.getSuperClasses()) { + if (superClass.getUuid() == null + || !renderContext.nodeUUIDs().contains(superClass.getUuid())) { + continue; + } + inheritanceEdgeDTOList.add( + EdgeDTO.builder() + .id(UUID.randomUUID().toString()) + .type(INHERITANCE_EDGE_TYPE) + .source(cimClass.getUuid()) + .target(superClass.getUuid()) + .data(null) + .build()); + } + } + return inheritanceEdgeDTOList; + } + + private List assembleAssociationEdgeDTOList(RenderContext renderContext) { + if (!renderContext.filter().isIncludeAssociations()) { + return List.of(); + } + + List associationEdgeDTOList = new ArrayList<>(); + var handledAssociationUris = new HashSet(); + for (var cimClass : renderContext.classes()) { + for (var from : cimClass.getAssociations()) { + if (handledAssociationUris.contains(from.getUri().toString())) { + continue; + } + var rangeUUID = from.getRange().getUuid(); + if (rangeUUID == null || !renderContext.nodeUUIDs().contains(rangeUUID)) { + continue; + } + var to = from.getInverseAssociation(); + + associationEdgeDTOList.add(assembleAssociationEdgeDTO(cimClass, from, to)); + + handledAssociationUris.add(from.getUri().toString()); + handledAssociationUris.add(to.getUri().toString()); + } + } + return associationEdgeDTOList; + } + + private EdgeDTO assembleAssociationEdgeDTO( + ICIMClass sourceClass, ICIMAssociation from, ICIMAssociation to) { + var edgeDataDTO = + EdgeDataDTO.builder() + .fromMultiplicity(extractMultiplicityString(from.getMultiplicity())) + .toMultiplicity(extractMultiplicityString(to.getMultiplicity())) + .useToAssociation(getAssociationUsedValue(from.getAssociationUsed())) + .useFromAssociation(getAssociationUsedValue(to.getAssociationUsed())) + .build(); + + return EdgeDTO.builder() + .id(UUID.randomUUID().toString()) + .type(ASSOCIATION_EDGE_TYPE) + .source(sourceClass.getUuid()) + .target(from.getRange().getUuid()) + .data(edgeDataDTO) + .build(); + } + + private String extractMultiplicityString(CIMSMultiplicity multiplicity) { + return multiplicity.getUri().getSuffix().replace("M:", ""); + } + + private boolean getAssociationUsedValue(CIMSAssociationUsed associationUsed) { + var associationUsedValue = associationUsed.toString(); + return switch (associationUsedValue) { + case "Yes" -> true; + case "No" -> false; + default -> + throw new IllegalArgumentException( + "Unexpected associationUsed value: " + associationUsedValue); + }; + } + + private record RenderContext( + List classes, + Set nodeUUIDs, + GraphFilter filter, + RenderingLayoutData layoutingData) {} } diff --git a/backend/src/test/java/org/rdfarchitect/models/cim/data/dto/facade/CIMFacadeTest.java b/backend/src/test/java/org/rdfarchitect/models/cim/data/dto/facade/CIMFacadeTest.java index e50e97b3..2add0db3 100644 --- a/backend/src/test/java/org/rdfarchitect/models/cim/data/dto/facade/CIMFacadeTest.java +++ b/backend/src/test/java/org/rdfarchitect/models/cim/data/dto/facade/CIMFacadeTest.java @@ -277,6 +277,49 @@ void requiredProperties() { assertThatThrownBy(association::getMultiplicity).isInstanceOf(IllegalStateException.class); } + @Test + @DisplayName("resolves a class category by uuid and falls back to the default category") + void classCategoryLookup() { + var facade = new CIMModelFacade(GRAPH_URI, model); + + var category = facade.getCIMClassCategory(CATEGORY_UUID); + assertThat(category.getUuid()).isEqualTo(CATEGORY_UUID); + assertThat(category.getLabel().getValue()).isEqualTo("Core"); + assertThat(category.getClasses()) + .extracting(ICIMClass::getUuid) + .containsExactly(BREAKER_UUID); + + var defaultCategory = facade.getCIMClassCategory(null); + assertThat(defaultCategory).isInstanceOf(DefaultCIMClassCategory.class); + assertThat(defaultCategory.getClasses()) + .extracting(ICIMClass::getUuid) + .containsExactlyInAnyOrder(SWITCH_UUID, TERMINAL_UUID, ENUM_UUID); + + assertThat(facade.getCIMClassCategory(UUID.randomUUID())).isNull(); + } + + @Test + @DisplayName("resolves an external category that is only referenced via belongsToCategory") + void externalClassCategory() { + var externalUuid = UUID.fromString("00000000-0000-0000-0000-00000000000b"); + var externalCategory = model.createResource(NS + "ExternalPackage"); + externalCategory.addProperty(RDFA.uuid, externalUuid.toString()); + model.getResource(NS + "Terminal").addProperty(CIMS.belongsToCategory, externalCategory); + + var facade = new CIMModelFacade(GRAPH_URI, model); + + var category = facade.getCIMClassCategory(externalUuid); + assertThat(category).isNotNull(); + assertThat(category.getLabel().getValue()).isEqualTo("ExternalPackage"); + assertThat(category.getClasses()) + .extracting(ICIMClass::getUuid) + .containsExactly(TERMINAL_UUID); + + assertThat(facade.getCIMClassCategories()) + .extracting(ICIMClassCategory::getUuid) + .contains(CATEGORY_UUID, externalUuid); + } + @Test @DisplayName("returns enum entries for enumeration classes and none for regular classes") void enumEntries() { diff --git a/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMFacadeCollectionSvelteFlowServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMFacadeCollectionSvelteFlowServiceTest.java new file mode 100644 index 00000000..6b05e48a --- /dev/null +++ b/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMFacadeCollectionSvelteFlowServiceTest.java @@ -0,0 +1,411 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * 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. + * + */ + +package org.rdfarchitect.services.rendering; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import org.apache.jena.vocabulary.XSD; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.rdfarchitect.api.dto.rendering.svelteflow.SvelteFlowDTO; +import org.rdfarchitect.api.dto.rendering.svelteflow.sub.AttributeDTO; +import org.rdfarchitect.api.dto.rendering.svelteflow.sub.EnumEntryDTO; +import org.rdfarchitect.api.dto.rendering.svelteflow.sub.NodeDTO; +import org.rdfarchitect.api.dto.rendering.svelteflow.sub.SuperClassDTO; +import org.rdfarchitect.models.cim.data.dto.facade.CIMModelFacade; +import org.rdfarchitect.models.cim.rdf.resources.CIMS; +import org.rdfarchitect.models.cim.rdf.resources.CIMStereotypes; +import org.rdfarchitect.models.cim.rdf.resources.RDFA; +import org.rdfarchitect.models.cim.rendering.GraphFilter; +import org.rdfarchitect.services.rendering.svelteflow.RenderCIMFacadeCollectionSvelteFlowService; + +import java.util.List; +import java.util.UUID; + +class RenderCIMFacadeCollectionSvelteFlowServiceTest { + + private static final String GRAPH_URI = "http://graph#"; + private static final String NS = "http://example.com#"; + + private static final UUID CORE_UUID = UUID.fromString("00000000-0000-0000-0000-000000000001"); + private static final UUID OTHER_UUID = UUID.fromString("00000000-0000-0000-0000-000000000002"); + private static final UUID ROOT_UUID = UUID.fromString("00000000-0000-0000-0000-000000000003"); + private static final UUID BASE_UUID = UUID.fromString("00000000-0000-0000-0000-000000000004"); + private static final UUID CHILD_UUID = UUID.fromString("00000000-0000-0000-0000-000000000005"); + private static final UUID TERMINAL_UUID = + UUID.fromString("00000000-0000-0000-0000-000000000006"); + private static final UUID ENUM_UUID = UUID.fromString("00000000-0000-0000-0000-000000000007"); + private static final UUID LOOSE_UUID = UUID.fromString("00000000-0000-0000-0000-000000000008"); + private static final UUID EXTERNAL_CAT_UUID = + UUID.fromString("00000000-0000-0000-0000-000000000009"); + private static final UUID REMOTE_UUID = + UUID.fromString("00000000-0000-0000-0000-00000000000a"); + + private final RenderCIMFacadeCollectionSvelteFlowService renderer = + new RenderCIMFacadeCollectionSvelteFlowService(); + + private Model model; + private CIMModelFacade facade; + + @BeforeEach + void setUp() { + model = ModelFactory.createDefaultModel(); + facade = new CIMModelFacade(GRAPH_URI, model); + + var core = addCategory("CorePackage", "Core", CORE_UUID); + var other = addCategory("OtherPackage", "Other", OTHER_UUID); + + var root = addClass("Root", ROOT_UUID, core); + addAttribute("Root.rootAttr", "rootAttr", root, "M:0..1"); + + var base = addClass("Base", BASE_UUID, core); + base.addProperty(RDFS.subClassOf, root); + addAttribute("Base.baseAttr", "baseAttr", base, "M:0..1"); + + var externalBase = model.createResource(NS + "ExternalBase"); + + var child = addClass("Child", CHILD_UUID, core); + child.addProperty(RDFS.subClassOf, base); + child.addProperty(RDFS.subClassOf, externalBase); + child.addProperty(CIMS.stereotype, CIMStereotypes.concrete); + addAttribute("Child.childAttr", "childAttr", child, "M:1..1"); + + var phaseCode = addClass("PhaseCode", ENUM_UUID, core); + phaseCode.addProperty(CIMS.stereotype, CIMStereotypes.enumeration); + var entry = model.createResource(NS + "PhaseCode.A"); + entry.addProperty(RDF.type, phaseCode); + entry.addProperty(RDFA.uuid, UUID.randomUUID().toString()); + entry.addProperty(RDFS.label, model.createLiteral("A", "en")); + entry.addProperty(CIMS.stereotype, CIMStereotypes.enumLiteral); + + var terminal = addClass("Terminal", TERMINAL_UUID, other); + + var association = model.createResource(NS + "Child.Terminals"); + association.addProperty(RDF.type, RDF.Property); + association.addProperty(RDFA.uuid, UUID.randomUUID().toString()); + association.addProperty(RDFS.label, model.createLiteral("Terminals", "en")); + association.addProperty(RDFS.domain, child); + association.addProperty(RDFS.range, terminal); + association.addProperty(CIMS.multiplicity, model.createResource(CIMS.namespace + "M:0..n")); + association.addProperty(CIMS.associationUsed, "Yes"); + + var inverse = model.createResource(NS + "Terminal.Child"); + inverse.addProperty(RDF.type, RDF.Property); + inverse.addProperty(RDFA.uuid, UUID.randomUUID().toString()); + inverse.addProperty(RDFS.label, model.createLiteral("Child", "en")); + inverse.addProperty(RDFS.domain, terminal); + inverse.addProperty(RDFS.range, child); + inverse.addProperty(CIMS.multiplicity, model.createResource(CIMS.namespace + "M:1..1")); + inverse.addProperty(CIMS.associationUsed, "No"); + inverse.addProperty(CIMS.inverseRoleName, association); + association.addProperty(CIMS.inverseRoleName, inverse); + + addClass("Loose", LOOSE_UUID, null); + + var externalCategory = model.createResource(NS + "ExternalPackage"); + externalCategory.addProperty(RDFA.uuid, EXTERNAL_CAT_UUID.toString()); + addClass("Remote", REMOTE_UUID, externalCategory); + } + + private Resource addCategory(String localName, String label, UUID uuid) { + var category = model.createResource(NS + localName); + category.addProperty(RDF.type, CIMS.classCategory); + category.addProperty(RDFA.uuid, uuid.toString()); + category.addProperty(RDFS.label, model.createLiteral(label, "en")); + return category; + } + + private Resource addClass(String label, UUID uuid, Resource category) { + var cimClass = model.createResource(NS + label); + cimClass.addProperty(RDF.type, RDFS.Class); + cimClass.addProperty(RDFA.uuid, uuid.toString()); + cimClass.addProperty(RDFS.label, model.createLiteral(label, "en")); + if (category != null) { + cimClass.addProperty(CIMS.belongsToCategory, category); + } + return cimClass; + } + + private void addAttribute( + String localName, String label, Resource domain, String multiplicity) { + var attribute = model.createResource(NS + localName); + attribute.addProperty(RDF.type, RDF.Property); + attribute.addProperty(RDFA.uuid, UUID.randomUUID().toString()); + attribute.addProperty(RDFS.label, model.createLiteral(label, "en")); + attribute.addProperty(CIMS.stereotype, CIMStereotypes.attribute); + attribute.addProperty(RDFS.domain, domain); + attribute.addProperty(CIMS.multiplicity, model.createResource(CIMS.namespace + multiplicity)); + attribute.addProperty(CIMS.datatype, model.createResource(XSD.xstring.getURI())); + } + + private GraphFilter coreFilter() { + var filter = new GraphFilter(true); + filter.setPackageUUID(CORE_UUID.toString()); + return filter; + } + + private NodeDTO nodeByLabel(SvelteFlowDTO diagram, String label) { + return diagram.getNodes().stream() + .filter(node -> node.getData().getLabel().equals(label)) + .findFirst() + .orElseThrow(); + } + + @Test + @DisplayName("renders all package classes plus externally related classes as nodes") + void rendersPackageClasses() { + var result = (SvelteFlowDTO) renderer.renderUML(facade, coreFilter(), null); + + assertThat(result.getNodes()) + .extracting(node -> node.getData().getLabel()) + .containsExactlyInAnyOrder("Root", "Base", "Child", "PhaseCode", "Terminal"); + assertThat(nodeByLabel(result, "Child").getId()).isEqualTo(CHILD_UUID); + assertThat(nodeByLabel(result, "Child").getData().getBelongsToCategory()) + .isEqualTo("Core"); + assertThat(nodeByLabel(result, "Terminal").getData().getBelongsToCategory()) + .isEqualTo("Other"); + } + + @Test + @DisplayName("renders direct attributes and stereotypes of a class") + void rendersDirectAttributesAndStereotypes() { + var result = (SvelteFlowDTO) renderer.renderUML(facade, coreFilter(), null); + + var childData = nodeByLabel(result, "Child").getData(); + assertThat(childData.getStereotypes()).doesNotContain("abstract"); + assertThat(childData.getAttributes()) + .singleElement() + .satisfies( + attribute -> { + assertThat(attribute.getLabel()).isEqualTo("childAttr"); + assertThat(attribute.getType()).isEqualTo("string"); + assertThat(attribute.getMultiplicity()).isEqualTo("1..1"); + }); + + var baseData = nodeByLabel(result, "Base").getData(); + assertThat(baseData.getStereotypes()).contains("abstract"); + assertThat(baseData.getAttributes()) + .extracting(AttributeDTO::getLabel) + .containsExactly("baseAttr"); + } + + @Test + @DisplayName("renders the transitive super class chain with inherited attributes") + void rendersInheritedProperties() { + var result = (SvelteFlowDTO) renderer.renderUML(facade, coreFilter(), null); + + var superClasses = nodeByLabel(result, "Child").getData().getSuperClasses(); + assertThat(superClasses) + .extracting(SuperClassDTO::getLabel) + .containsExactlyInAnyOrder("Base", "Root", "ExternalBase"); + + var baseSuperClass = + superClasses.stream() + .filter(superClass -> superClass.getLabel().equals("Base")) + .findFirst() + .orElseThrow(); + assertThat(baseSuperClass.getUuid()).isEqualTo(BASE_UUID); + assertThat(baseSuperClass.getAttributes()) + .extracting(AttributeDTO::getLabel) + .containsExactly("baseAttr"); + + var rootSuperClass = + superClasses.stream() + .filter(superClass -> superClass.getLabel().equals("Root")) + .findFirst() + .orElseThrow(); + assertThat(rootSuperClass.getAttributes()) + .extracting(AttributeDTO::getLabel) + .containsExactly("rootAttr"); + + var externalSuperClass = + superClasses.stream() + .filter(superClass -> superClass.getLabel().equals("ExternalBase")) + .findFirst() + .orElseThrow(); + assertThat(externalSuperClass.getUuid()).isNull(); + assertThat(externalSuperClass.getAttributes()).isEmpty(); + } + + @Test + @DisplayName("renders enum entries for enumeration classes") + void rendersEnumEntries() { + var result = (SvelteFlowDTO) renderer.renderUML(facade, coreFilter(), null); + + var enumData = nodeByLabel(result, "PhaseCode").getData(); + assertThat(enumData.getStereotypes()).contains("abstract", "enumeration"); + assertThat(enumData.getEnumEntries()) + .extracting(EnumEntryDTO::getLabel) + .containsExactly("A"); + } + + @Test + @DisplayName("renders inheritance edges between internal classes only") + void rendersInheritanceEdges() { + var result = (SvelteFlowDTO) renderer.renderUML(facade, coreFilter(), null); + + var inheritanceEdges = + result.getEdges().stream() + .filter(edge -> edge.getType().equals("inheritance")) + .toList(); + assertThat(inheritanceEdges) + .extracting(edge -> List.of(edge.getSource(), edge.getTarget())) + .containsExactlyInAnyOrder( + List.of(CHILD_UUID, BASE_UUID), List.of(BASE_UUID, ROOT_UUID)); + } + + @Test + @DisplayName("renders a single association edge per association pair") + void rendersAssociationEdges() { + var result = (SvelteFlowDTO) renderer.renderUML(facade, coreFilter(), null); + + var associationEdges = + result.getEdges().stream() + .filter(edge -> edge.getType().equals("association")) + .toList(); + assertThat(associationEdges).hasSize(1); + var edge = associationEdges.getFirst(); + assertThat(edge.getSource()).isEqualTo(CHILD_UUID); + assertThat(edge.getTarget()).isEqualTo(TERMINAL_UUID); + assertThat(edge.getData().getFromMultiplicity()).isEqualTo("0..n"); + assertThat(edge.getData().getToMultiplicity()).isEqualTo("1..1"); + assertThat(edge.getData().isUseToAssociation()).isTrue(); + assertThat(edge.getData().isUseFromAssociation()).isFalse(); + } + + @Test + @DisplayName("omits attributes and enum entries when disabled by the filter") + void filterDisablesAttributesAndEnumEntries() { + var filter = coreFilter(); + filter.setIncludeAttributes(false); + filter.setIncludeEnumEntries(false); + + var result = (SvelteFlowDTO) renderer.renderUML(facade, filter, null); + + var childData = nodeByLabel(result, "Child").getData(); + assertThat(childData.getAttributes()).isEmpty(); + assertThat(childData.getSuperClasses()) + .allSatisfy(superClass -> assertThat(superClass.getAttributes()).isEmpty()); + assertThat(nodeByLabel(result, "PhaseCode").getData().getEnumEntries()).isEmpty(); + } + + @Test + @DisplayName("omits inheritance edges when disabled by the filter") + void filterDisablesInheritance() { + var filter = coreFilter(); + filter.setIncludeInheritance(false); + + var result = (SvelteFlowDTO) renderer.renderUML(facade, filter, null); + + assertThat(result.getEdges()).noneMatch(edge -> edge.getType().equals("inheritance")); + } + + @Test + @DisplayName("omits association edges when disabled by the filter") + void filterDisablesAssociations() { + var filter = coreFilter(); + filter.setIncludeAssociations(false); + + var result = (SvelteFlowDTO) renderer.renderUML(facade, filter, null); + + assertThat(result.getNodes()) + .extracting(node -> node.getData().getLabel()) + .doesNotContain("Terminal"); + assertThat(result.getEdges()).noneMatch(edge -> edge.getType().equals("association")); + } + + @Test + @DisplayName("omits classes of other packages when external relations are disabled") + void filterDisablesExternalRelations() { + var filter = coreFilter(); + filter.setIncludeRelationsToExternalPackages(false); + + var result = (SvelteFlowDTO) renderer.renderUML(facade, filter, null); + + assertThat(result.getNodes()) + .extracting(node -> node.getData().getLabel()) + .containsExactlyInAnyOrder("Root", "Base", "Child", "PhaseCode"); + assertThat(result.getEdges()).noneMatch(edge -> edge.getType().equals("association")); + } + + @Test + @DisplayName("renders classes without a category for the default package") + void rendersDefaultPackage() { + var filter = new GraphFilter(true); + filter.setPackageUUID("default"); + + var result = (SvelteFlowDTO) renderer.renderUML(facade, filter, null); + + assertThat(result.getNodes()) + .extracting(node -> node.getData().getLabel()) + .containsExactly("Loose"); + } + + @Test + @DisplayName("renders classes of an external category that has only a uuid") + void rendersExternalCategory() { + var filter = new GraphFilter(true); + filter.setPackageUUID(EXTERNAL_CAT_UUID.toString()); + + var result = (SvelteFlowDTO) renderer.renderUML(facade, filter, null); + + assertThat(result.getNodes()) + .extracting(node -> node.getData().getLabel()) + .containsExactly("Remote"); + assertThat(nodeByLabel(result, "Remote").getData().getBelongsToCategory()) + .isEqualTo("ExternalPackage"); + } + + @Test + @DisplayName("renders only the classes specified by allowedUUIDs") + void rendersAllowedUUIDsOnly() { + var filter = new GraphFilter(true); + filter.setAllowedUUIDs(List.of(CHILD_UUID.toString(), BASE_UUID.toString())); + + var result = (SvelteFlowDTO) renderer.renderUML(facade, filter, null); + + assertThat(result.getNodes()) + .extracting(node -> node.getData().getLabel()) + .containsExactlyInAnyOrder("Base", "Child"); + assertThat(result.getEdges()) + .singleElement() + .satisfies( + edge -> { + assertThat(edge.getType()).isEqualTo("inheritance"); + assertThat(edge.getSource()).isEqualTo(CHILD_UUID); + assertThat(edge.getTarget()).isEqualTo(BASE_UUID); + }); + } + + @Test + @DisplayName("returns an empty diagram for an empty model") + void rendersEmptyModel() { + var emptyFacade = new CIMModelFacade(GRAPH_URI, ModelFactory.createDefaultModel()); + + var result = (SvelteFlowDTO) renderer.renderUML(emptyFacade, coreFilter(), null); + + assertThat(result.getNodes()).isEmpty(); + assertThat(result.getEdges()).isEmpty(); + } +} From e65e3d7b4e3c85fa25a16210f397d1712de73f3b Mon Sep 17 00:00:00 2001 From: Maximilian Reisch Date: Fri, 17 Jul 2026 16:47:49 +0200 Subject: [PATCH 5/5] added frontend display for inherited properties Signed-off-by: Maximilian Reisch --- .../cim/data/dto/facade/CIMAssociation.java | 17 ++- .../cim/data/dto/facade/CIMAttribute.java | 35 ++++-- .../models/cim/data/dto/facade/CIMClass.java | 28 +++-- .../cim/data/dto/facade/CIMClassCategory.java | 12 +- .../cim/data/dto/facade/CIMEnumEntry.java | 3 +- .../cim/data/dto/facade/CIMModelFacade.java | 18 +-- .../cim/data/dto/facade/CIMResource.java | 106 +++++++++++------- .../dto/facade/DefaultCIMClassCategory.java | 15 ++- .../cim/data/dto/facade/ExternalCIMClass.java | 12 +- .../cim/data/dto/facade/ICIMAssociation.java | 2 +- .../data/dto/facade/ICIMClassCategory.java | 2 +- .../cim/data/dto/facade/ICIMEnumEntry.java | 1 - .../cim/data/dto/facade/ICIMResource.java | 1 + ...rCIMFacadeCollectionSvelteFlowService.java | 25 +++-- .../cim/data/dto/facade/CIMFacadeTest.java | 47 +++++--- ...FacadeCollectionSvelteFlowServiceTest.java | 9 +- .../svelteflow/components/ClassNode.svelte | 21 ++++ frontend/src/routes/UserSettingDialog.svelte | 6 + 18 files changed, 237 insertions(+), 123 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMAssociation.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMAssociation.java index 78a990ee..a58ed0c4 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMAssociation.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMAssociation.java @@ -23,6 +23,7 @@ import org.rdfarchitect.models.cim.data.dto.relations.CIMSAssociationUsed; import org.rdfarchitect.models.cim.data.dto.relations.CIMSMultiplicity; import org.rdfarchitect.models.cim.rdf.resources.CIMS; + import java.util.UUID; public class CIMAssociation extends CIMResource implements ICIMAssociation { @@ -62,11 +63,19 @@ public ICIMAssociation getInverseAssociation() { @Override public CIMSAssociationUsed getAssociationUsed() { var node = getUniqueJenaPropertyNode(CIMS.associationUsed); - if(node == null){ - throw new IllegalStateException("Required property " + CIMS.associationUsed + " not found for association with UUID " + getUuid() + "."); + if (node == null) { + throw new IllegalStateException( + "Required property " + + CIMS.associationUsed + + " not found for association with UUID " + + getUuid() + + "."); } - if(!node.isLiteral()){ - throw new IllegalStateException("AssociationUsed for association with UUID " + getUuid() + " is not a literal."); + if (!node.isLiteral()) { + throw new IllegalStateException( + "AssociationUsed for association with UUID " + + getUuid() + + " is not a literal."); } return new CIMSAssociationUsed(node.asLiteral().getLexicalForm()); } diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMAttribute.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMAttribute.java index b7714bb5..a3557260 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMAttribute.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMAttribute.java @@ -29,6 +29,7 @@ import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; import org.rdfarchitect.models.cim.rdf.resources.CIMS; + import java.util.UUID; public class CIMAttribute extends CIMResource implements ICIMAttribute { @@ -59,11 +60,18 @@ public CIMSMultiplicity getMultiplicity() { @Override public ICIMClass getDataType() { var dataType = getUniqueJenaProperty(CIMS.datatype); - if(dataType == null){ + if (dataType == null) { dataType = getUniqueJenaProperty(RDFS.range); } - if(dataType == null){ - throw new IllegalStateException("No " + CIMS.datatype + " or " + RDFS.range + " found for attribute with UUID " + getUuid() + "."); + if (dataType == null) { + throw new IllegalStateException( + "No " + + CIMS.datatype + + " or " + + RDFS.range + + " found for attribute with UUID " + + getUuid() + + "."); } return CIMClass.fromResource(getGraphUri(), getModel(), dataType); } @@ -71,8 +79,13 @@ public ICIMClass getDataType() { @Override public CIMSStereotype getStereotype() { var stereotypes = getStereotypeList(); - if(stereotypes.isEmpty()){ - throw new IllegalStateException("Required property " + CIMS.stereotype + " not found for attribute with UUID " + getUuid() + "."); + if (stereotypes.isEmpty()) { + throw new IllegalStateException( + "Required property " + + CIMS.stereotype + + " not found for attribute with UUID " + + getUuid() + + "."); } return stereotypes.getFirst(); } @@ -80,7 +93,7 @@ public CIMSStereotype getStereotype() { @Override public CIMSIsFixed getFixed() { var valueNode = readValueNode(CIMS.isFixed); - if(valueNode == null){ + if (valueNode == null) { return null; } return new CIMSIsFixed(valueNode.value(), valueNode.dataType(), valueNode.blankNode()); @@ -89,7 +102,7 @@ public CIMSIsFixed getFixed() { @Override public CIMSIsDefault getDefault() { var valueNode = readValueNode(CIMS.isDefault); - if(valueNode == null){ + if (valueNode == null) { return null; } return new CIMSIsDefault(valueNode.value(), valueNode.dataType(), valueNode.blankNode()); @@ -99,15 +112,15 @@ private record ValueNode(String value, URI dataType, boolean blankNode) {} private ValueNode readValueNode(Property property) { var node = getUniqueJenaPropertyNode(property); - if(node == null){ + if (node == null) { return null; } - if(node.isLiteral()){ + if (node.isLiteral()) { return toValueNode(node.asLiteral(), false); } - if(node.isAnon()){ + if (node.isAnon()) { var inner = node.asResource().getProperty(LITERAL_WRAPPER_PROPERTY); - if(inner != null && inner.getObject().isLiteral()){ + if (inner != null && inner.getObject().isLiteral()) { return toValueNode(inner.getObject().asLiteral(), true); } } diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClass.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClass.java index 86e05336..4b0c9f83 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClass.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClass.java @@ -22,10 +22,12 @@ import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; +import org.rdfarchitect.models.cim.data.dto.relations.RDFSLabel; import org.rdfarchitect.models.cim.rdf.resources.CIMS; import org.rdfarchitect.models.cim.rdf.resources.CIMStereotypes; import org.rdfarchitect.models.cim.rdf.resources.RDFA; import org.rdfarchitect.models.cim.relations.model.properties.CIMPropertyUtils; + import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -41,17 +43,25 @@ public CIMClass(String graphUri, Model model, Resource resource) { } public static ICIMClass fromResource(String graphUri, Model model, Resource resource) { - if(model.contains(resource, RDFA.uuid)){ + if (model.contains(resource, RDFA.uuid)) { return new CIMClass(graphUri, model, resource); } return new ExternalCIMClass(graphUri, model, resource); } + @Override + public RDFSLabel getLabel() { + if (getUniqueJenaPropertyNode(RDFS.label) == null) { + return new RDFSLabel(getUri().getSuffix()); + } + return super.getLabel(); + } + @Override public List getSuperClasses() { var resources = this.getJenaProperties(RDFS.subClassOf); var superClasses = new ArrayList(); - for(var res : resources){ + for (var res : resources) { superClasses.add(fromResource(this.getGraphUri(), this.getModel(), res)); } return superClasses; @@ -60,7 +70,7 @@ public List getSuperClasses() { @Override public ICIMClassCategory getBelongsToCategory() { var category = getUniqueJenaProperty(CIMS.belongsToCategory); - if(category == null){ + if (category == null) { return null; } return CIMClassCategory.fromResource(getGraphUri(), getModel(), category); @@ -74,8 +84,8 @@ public List getStereotypes() { @Override public List getAttributes() { var attributes = new ArrayList(); - for(var property : listDirectProperties()){ - if(CIMPropertyUtils.isAttribute(property)){ + for (var property : listDirectProperties()) { + if (CIMPropertyUtils.isAttribute(property)) { attributes.add(new CIMAttribute(getGraphUri(), getModel(), property)); } } @@ -85,8 +95,8 @@ public List getAttributes() { @Override public List getAssociations() { var associations = new ArrayList(); - for(var property : listDirectProperties()){ - if(CIMPropertyUtils.isAssociation(property)){ + for (var property : listDirectProperties()) { + if (CIMPropertyUtils.isAssociation(property)) { associations.add(new CIMAssociation(getGraphUri(), getModel(), property)); } } @@ -96,11 +106,11 @@ public List getAssociations() { @Override public List getEnumEntries() { var jenaResource = getJenaResource(); - if(!jenaResource.hasProperty(CIMS.stereotype, CIMStereotypes.enumeration)){ + if (!jenaResource.hasProperty(CIMS.stereotype, CIMStereotypes.enumeration)) { return List.of(); } var entries = new ArrayList(); - for(var entry : getModel().listSubjectsWithProperty(RDF.type, jenaResource).toList()){ + for (var entry : getModel().listSubjectsWithProperty(RDF.type, jenaResource).toList()) { entries.add(new CIMEnumEntry(getGraphUri(), getModel(), entry)); } return entries; diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClassCategory.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClassCategory.java index 0f77ef13..16bb0999 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClassCategory.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClassCategory.java @@ -17,13 +17,12 @@ package org.rdfarchitect.models.cim.data.dto.facade; -import org.apache.jena.graph.NodeFactory; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Resource; import org.apache.jena.vocabulary.RDFS; import org.rdfarchitect.models.cim.data.dto.relations.RDFSLabel; import org.rdfarchitect.models.cim.rdf.resources.CIMS; -import org.rdfarchitect.models.cim.rdf.resources.RDFA; + import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -39,7 +38,7 @@ public CIMClassCategory(String graphUri, Model model, Resource resource) { } public static ICIMClassCategory fromResource(String graphUri, Model model, Resource resource) { - if(resource == null){ + if (resource == null) { return new DefaultCIMClassCategory(graphUri, model); } return new CIMClassCategory(graphUri, model, resource); @@ -55,9 +54,12 @@ public RDFSLabel getLabel() { @Override public List getClasses() { - var resourcesInPackage = getModel().listSubjectsWithProperty(CIMS.belongsToCategory, this.getJenaResource()).toList(); + var resourcesInPackage = + getModel() + .listSubjectsWithProperty(CIMS.belongsToCategory, this.getJenaResource()) + .toList(); var classes = new ArrayList(); - for(var resource : resourcesInPackage){ + for (var resource : resourcesInPackage) { classes.add(CIMClass.fromResource(getGraphUri(), getModel(), resource)); } return classes; diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMEnumEntry.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMEnumEntry.java index 21d99bd2..4a4759bc 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMEnumEntry.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMEnumEntry.java @@ -21,6 +21,7 @@ import org.apache.jena.rdf.model.Resource; import org.apache.jena.vocabulary.RDF; import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; + import java.util.UUID; public class CIMEnumEntry extends CIMResource implements ICIMEnumEntry { @@ -42,7 +43,7 @@ public ICIMClass getDomain() { @Override public CIMSStereotype getStereotype() { var stereotypes = getStereotypeList(); - if(stereotypes.isEmpty()){ + if (stereotypes.isEmpty()) { return null; } return stereotypes.getFirst(); diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMModelFacade.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMModelFacade.java index 2338a632..87c22528 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMModelFacade.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMModelFacade.java @@ -18,15 +18,16 @@ package org.rdfarchitect.models.cim.data.dto.facade; import lombok.RequiredArgsConstructor; + import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.RDFNode; import org.apache.jena.rdf.model.Resource; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; -import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; import org.rdfarchitect.models.cim.rdf.resources.CIMS; import org.rdfarchitect.models.cim.rdf.resources.CIMStereotypes; import org.rdfarchitect.models.cim.rdf.resources.RDFA; + import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; @@ -56,8 +57,9 @@ public List getCIMClasses() { @Override public List getCIMClassCategories() { - var packageResources = new LinkedHashSet( - model.listSubjectsWithProperty(RDF.type, CIMS.classCategory).toList()); + var packageResources = + new LinkedHashSet( + model.listSubjectsWithProperty(RDF.type, CIMS.classCategory).toList()); model.listObjectsOfProperty(CIMS.belongsToCategory) .filterKeep(RDFNode::isURIResource) .mapWith(RDFNode::asResource) @@ -76,9 +78,10 @@ public ICIMClassCategory getCIMClassCategory(UUID uuid) { if (uuid == null) { return new DefaultCIMClassCategory(this.graphUri, this.model); } - var categoryResources = model.listSubjectsWithProperty(RDFA.uuid, uuid.toString()) - .filterKeep(this::isClassCategory) - .toList(); + var categoryResources = + model.listSubjectsWithProperty(RDFA.uuid, uuid.toString()) + .filterKeep(this::isClassCategory) + .toList(); if (categoryResources.isEmpty()) { return null; } @@ -91,7 +94,8 @@ private boolean isClassCategory(Resource resource) { } public List getCIMAttributes() { - var attributeResources = model.listSubjectsWithProperty(CIMS.stereotype, CIMStereotypes.attribute).toList(); + var attributeResources = + model.listSubjectsWithProperty(CIMS.stereotype, CIMStereotypes.attribute).toList(); var attributes = new ArrayList(); for (var attributeResource : attributeResources) { attributes.add(new CIMAttribute(this.graphUri, this.model, attributeResource)); diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMResource.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMResource.java index ac9265af..f53a2bf2 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMResource.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMResource.java @@ -28,6 +28,7 @@ import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; import org.rdfarchitect.models.cim.rdf.resources.CIMS; import org.rdfarchitect.models.cim.rdf.resources.RDFA; + import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -50,14 +51,14 @@ protected CIMResource(String graphUri, Model model, Resource resource) { this.graphUri = graphUri; this.model = model; var uuidObjects = this.model.listObjectsOfProperty(resource, RDFA.uuid).toList(); - if(uuidObjects.isEmpty()){ + if (uuidObjects.isEmpty()) { throw new IllegalStateException("No uuid found for resource"); } - if(uuidObjects.size() > 1){ + if (uuidObjects.size() > 1) { throw new IllegalStateException("Multiple uuids found for single resource."); } var subject = uuidObjects.getFirst(); - if(!subject.isLiteral()){ + if (!subject.isLiteral()) { throw new IllegalStateException("UUID for resource is not a literal."); } this.uuid = UUID.fromString(subject.asLiteral().getLexicalForm()); @@ -68,7 +69,7 @@ protected Model getModel() { } @Override - public UUID getUuid(){ + public UUID getUuid() { return uuid; } @@ -78,105 +79,128 @@ public String getGraphUri() { } @Override - public URI getUri(){ + public URI getUri() { return new URI(getJenaResource().getURI()); } @Override - public RDFSLabel getLabel(){ + public RDFSLabel getLabel() { var node = getUniqueJenaPropertyNode(RDFS.label); - if(node == null){ - throw new IllegalStateException("Required property " + RDFS.label + " not found for resource with UUID " + uuid + "."); - } - if(!node.isLiteral()){ - throw new IllegalStateException("Label for resource with UUID " + uuid + " is not a literal."); + if (node == null) { + throw new IllegalStateException( + "Required property " + + RDFS.label + + " not found for resource with UUID " + + uuid + + "."); + } + if (!node.isLiteral()) { + throw new IllegalStateException( + "Label for resource with UUID " + uuid + " is not a literal."); } var langLiteral = node.asLiteral(); return new RDFSLabel(langLiteral.getString(), langLiteral.getLanguage()); } @Override - public RDFSComment getComment(){ + public RDFSComment getComment() { var node = getUniqueJenaPropertyNode(RDFS.comment); - if(node == null){ + if (node == null) { return null; } - if(!node.isLiteral()){ - throw new IllegalStateException("Comment for resource with UUID " + uuid + " is not a literal."); + if (!node.isLiteral()) { + throw new IllegalStateException( + "Comment for resource with UUID " + uuid + " is not a literal."); } var langLiteral = node.asLiteral(); - return new RDFSComment(langLiteral.getString(), new URI(langLiteral.getDatatype().getURI())); + return new RDFSComment( + langLiteral.getString(), new URI(langLiteral.getDatatype().getURI())); } - // helper - protected Resource getJenaResource(){ + protected Resource getJenaResource() { var subjectsWithUuid = model.listSubjectsWithProperty(RDFA.uuid, uuid.toString()).toList(); - if(subjectsWithUuid.isEmpty()){ + if (subjectsWithUuid.isEmpty()) { throw new IllegalStateException("Resource with UUID " + uuid + " not found."); } - if(subjectsWithUuid.size() > 1){ - throw new IllegalStateException("Multiple uris found for " + uuid + "."); + if (subjectsWithUuid.size() > 1) { + throw new IllegalStateException("Multiple uris found for " + uuid + "."); } var subject = subjectsWithUuid.getFirst(); - if(!subject.isURIResource()){ - throw new IllegalStateException("Resource with UUID " + uuid + " is not an URI resource."); + if (!subject.isURIResource()) { + throw new IllegalStateException( + "Resource with UUID " + uuid + " is not an URI resource."); } return subject; } - protected List getJenaProperties(Property property){ + protected List getJenaProperties(Property property) { var res = new ArrayList(); - for(var prop : getJenaPropertyNodes(property)){ + for (var prop : getJenaPropertyNodes(property)) { res.add(prop.asResource()); } return res; } - protected List getJenaPropertyNodes(Property property){ + protected List getJenaPropertyNodes(Property property) { var resource = getJenaResource(); return model.listObjectsOfProperty(resource, property).toList(); } - protected RDFNode getUniqueJenaPropertyNode(Property property){ + protected RDFNode getUniqueJenaPropertyNode(Property property) { var nodes = getJenaPropertyNodes(property); - if(nodes.isEmpty()){ + if (nodes.isEmpty()) { return null; } - if(nodes.size() > 1){ - throw new IllegalStateException("Multiple " + property + " properties found for resource with UUID " + uuid + "."); + if (nodes.size() > 1) { + throw new IllegalStateException( + "Multiple " + + property + + " properties found for resource with UUID " + + uuid + + "."); } return nodes.getFirst(); } - protected List getStereotypeList(){ + protected List getStereotypeList() { var stereotypes = new ArrayList(); - for(var node : getJenaPropertyNodes(CIMS.stereotype)){ - if(node.isLiteral()){ + for (var node : getJenaPropertyNodes(CIMS.stereotype)) { + if (node.isLiteral()) { stereotypes.add(new CIMSStereotype(node.asLiteral().getLexicalForm())); - } else if(node.isURIResource()){ + } else if (node.isURIResource()) { stereotypes.add(new CIMSStereotype(node.asResource().getURI())); } } return stereotypes; } - protected Resource getUniqueJenaProperty(Property property){ + protected Resource getUniqueJenaProperty(Property property) { var properties = getJenaProperties(property); - if(properties.isEmpty()){ + if (properties.isEmpty()) { return null; } - if(properties.size() > 1){ - throw new IllegalStateException("Multiple " + property + " properties found for resource with UUID " + uuid + "."); + if (properties.size() > 1) { + throw new IllegalStateException( + "Multiple " + + property + + " properties found for resource with UUID " + + uuid + + "."); } return properties.getFirst(); } - protected Resource getRequiredUniqueJenaProperty(Property property){ + protected Resource getRequiredUniqueJenaProperty(Property property) { var propertyResource = getUniqueJenaProperty(property); - if(propertyResource == null){ - throw new IllegalStateException("Required property " + property + " not found for resource with UUID " + uuid + "."); + if (propertyResource == null) { + throw new IllegalStateException( + "Required property " + + property + + " not found for resource with UUID " + + uuid + + "."); } return propertyResource; } diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/DefaultCIMClassCategory.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/DefaultCIMClassCategory.java index e965eb35..3afd1e83 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/DefaultCIMClassCategory.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/DefaultCIMClassCategory.java @@ -17,9 +17,6 @@ package org.rdfarchitect.models.cim.data.dto.facade; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; import org.apache.jena.rdf.model.Model; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; @@ -28,6 +25,10 @@ import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; import org.rdfarchitect.models.cim.rdf.resources.CIMS; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + public class DefaultCIMClassCategory implements ICIMClassCategory { private final String graphUri; @@ -64,12 +65,14 @@ public RDFSComment getComment() { return null; } - @Override public List getClasses() { - var classResources = model.listSubjectsWithProperty(RDF.type, RDFS.Class).filterDrop(resource -> resource.hasProperty(CIMS.belongsToCategory)).toList(); + var classResources = + model.listSubjectsWithProperty(RDF.type, RDFS.Class) + .filterDrop(resource -> resource.hasProperty(CIMS.belongsToCategory)) + .toList(); var classes = new ArrayList(); - for (var classResource : classResources){ + for (var classResource : classResources) { classes.add(new CIMClass(graphUri, model, classResource)); } return classes; diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ExternalCIMClass.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ExternalCIMClass.java index 42177664..35d56c4d 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ExternalCIMClass.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ExternalCIMClass.java @@ -25,6 +25,7 @@ import org.rdfarchitect.models.cim.data.dto.relations.RDFSLabel; import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; import org.rdfarchitect.models.cim.rdf.resources.RDFA; + import java.util.List; import java.util.UUID; @@ -37,7 +38,7 @@ public class ExternalCIMClass implements ICIMClass { private final Resource resource; public ExternalCIMClass(String graphUri, Model model, Resource resource) { - if(!resource.isURIResource()){ + if (!resource.isURIResource()) { throw new IllegalStateException("External class resource is not an URI resource."); } this.graphUri = graphUri; @@ -47,7 +48,7 @@ public ExternalCIMClass(String graphUri, Model model, Resource resource) { @Override public UUID getUuid() { - if(!this.resource.hasProperty(RDFA.uuid)){ + if (!this.resource.hasProperty(RDFA.uuid)) { return null; } return UUID.fromString(this.resource.getProperty(RDFA.uuid).getObject().toString()); @@ -66,7 +67,7 @@ public URI getUri() { @Override public RDFSLabel getLabel() { var statement = model.getProperty(resource, RDFS.label); - if(statement == null || !statement.getObject().isLiteral()){ + if (statement == null || !statement.getObject().isLiteral()) { return new RDFSLabel(getUri().getSuffix()); } var langLiteral = statement.getObject().asLiteral(); @@ -76,11 +77,12 @@ public RDFSLabel getLabel() { @Override public RDFSComment getComment() { var statement = model.getProperty(resource, RDFS.comment); - if(statement == null || !statement.getObject().isLiteral()){ + if (statement == null || !statement.getObject().isLiteral()) { return null; } var langLiteral = statement.getObject().asLiteral(); - return new RDFSComment(langLiteral.getString(), new URI(langLiteral.getDatatype().getURI())); + return new RDFSComment( + langLiteral.getString(), new URI(langLiteral.getDatatype().getURI())); } @Override diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMAssociation.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMAssociation.java index a993107e..f4513fa4 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMAssociation.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMAssociation.java @@ -20,7 +20,7 @@ import org.rdfarchitect.models.cim.data.dto.relations.CIMSAssociationUsed; import org.rdfarchitect.models.cim.data.dto.relations.CIMSMultiplicity; -public interface ICIMAssociation extends ICIMResource{ +public interface ICIMAssociation extends ICIMResource { CIMSMultiplicity getMultiplicity(); diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClassCategory.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClassCategory.java index 1e573955..406e7260 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClassCategory.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClassCategory.java @@ -19,7 +19,7 @@ import java.util.List; -public interface ICIMClassCategory extends ICIMResource{ +public interface ICIMClassCategory extends ICIMResource { List getClasses(); } diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMEnumEntry.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMEnumEntry.java index 95328929..886eb980 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMEnumEntry.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMEnumEntry.java @@ -17,7 +17,6 @@ package org.rdfarchitect.models.cim.data.dto.facade; - import org.rdfarchitect.models.cim.data.dto.relations.CIMSStereotype; public interface ICIMEnumEntry extends ICIMResource { diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMResource.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMResource.java index c87fb81f..21f35552 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMResource.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMResource.java @@ -20,6 +20,7 @@ import org.rdfarchitect.models.cim.data.dto.relations.RDFSComment; import org.rdfarchitect.models.cim.data.dto.relations.RDFSLabel; import org.rdfarchitect.models.cim.data.dto.relations.uri.URI; + import java.util.UUID; public interface ICIMResource { diff --git a/backend/src/main/java/org/rdfarchitect/services/rendering/svelteflow/RenderCIMFacadeCollectionSvelteFlowService.java b/backend/src/main/java/org/rdfarchitect/services/rendering/svelteflow/RenderCIMFacadeCollectionSvelteFlowService.java index 2b3a5784..588f1903 100644 --- a/backend/src/main/java/org/rdfarchitect/services/rendering/svelteflow/RenderCIMFacadeCollectionSvelteFlowService.java +++ b/backend/src/main/java/org/rdfarchitect/services/rendering/svelteflow/RenderCIMFacadeCollectionSvelteFlowService.java @@ -17,15 +17,6 @@ package org.rdfarchitect.services.rendering.svelteflow; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; import org.rdfarchitect.api.dto.dl.RenderingLayoutData; import org.rdfarchitect.api.dto.rendering.RenderingDataDTO; import org.rdfarchitect.api.dto.rendering.svelteflow.SvelteFlowDTO; @@ -50,6 +41,16 @@ import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + /** * Converts a {@link ICIMModelFacade} to a DTO Record that contains two JSON arrays with nodes and * edges used to render a UML diagram using the JavaScript library SvelteFlow. @@ -59,7 +60,8 @@ name = "rendering.renderer", havingValue = "svelteflow", matchIfMissing = true) -public class RenderCIMFacadeCollectionSvelteFlowService implements RenderCIMFacadeCollectionUseCase { +public class RenderCIMFacadeCollectionSvelteFlowService + implements RenderCIMFacadeCollectionUseCase { // CONSTANTS FOR SVELTEFLOW CUSTOM NODE/EDGE TYPES private static final String CLASS_NODE_TYPE = "class"; @@ -224,8 +226,7 @@ private NodeDTO assembleNodeDTO(RenderContext renderContext, ICIMClass cimClass) .build(); } - private List getClassAttributes( - RenderContext renderContext, ICIMClass cimClass) { + private List getClassAttributes(RenderContext renderContext, ICIMClass cimClass) { if (!renderContext.filter().isIncludeAttributes()) { return List.of(); } diff --git a/backend/src/test/java/org/rdfarchitect/models/cim/data/dto/facade/CIMFacadeTest.java b/backend/src/test/java/org/rdfarchitect/models/cim/data/dto/facade/CIMFacadeTest.java index 2add0db3..d9092b92 100644 --- a/backend/src/test/java/org/rdfarchitect/models/cim/data/dto/facade/CIMFacadeTest.java +++ b/backend/src/test/java/org/rdfarchitect/models/cim/data/dto/facade/CIMFacadeTest.java @@ -22,7 +22,6 @@ import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; -import org.apache.jena.rdf.model.Resource; import org.apache.jena.rdf.model.ResourceFactory; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; @@ -44,13 +43,19 @@ class CIMFacadeTest { private static final String GRAPH_URI = "http://graph#"; private static final String NS = "http://example.com#"; - private static final UUID CATEGORY_UUID = UUID.fromString("00000000-0000-0000-0000-000000000001"); + private static final UUID CATEGORY_UUID = + UUID.fromString("00000000-0000-0000-0000-000000000001"); private static final UUID SWITCH_UUID = UUID.fromString("00000000-0000-0000-0000-000000000002"); - private static final UUID BREAKER_UUID = UUID.fromString("00000000-0000-0000-0000-000000000003"); - private static final UUID ATTRIBUTE_UUID = UUID.fromString("00000000-0000-0000-0000-000000000004"); - private static final UUID TERMINAL_UUID = UUID.fromString("00000000-0000-0000-0000-000000000005"); - private static final UUID ASSOCIATION_UUID = UUID.fromString("00000000-0000-0000-0000-000000000006"); - private static final UUID INVERSE_UUID = UUID.fromString("00000000-0000-0000-0000-000000000007"); + private static final UUID BREAKER_UUID = + UUID.fromString("00000000-0000-0000-0000-000000000003"); + private static final UUID ATTRIBUTE_UUID = + UUID.fromString("00000000-0000-0000-0000-000000000004"); + private static final UUID TERMINAL_UUID = + UUID.fromString("00000000-0000-0000-0000-000000000005"); + private static final UUID ASSOCIATION_UUID = + UUID.fromString("00000000-0000-0000-0000-000000000006"); + private static final UUID INVERSE_UUID = + UUID.fromString("00000000-0000-0000-0000-000000000007"); private static final UUID ENUM_UUID = UUID.fromString("00000000-0000-0000-0000-000000000008"); private static final UUID ENTRY_UUID = UUID.fromString("00000000-0000-0000-0000-000000000009"); @@ -163,12 +168,13 @@ void superClasses() { assertThat(superClasses) .filteredOn(ExternalCIMClass.class::isInstance) .singleElement() - .satisfies(external -> { - assertThat(external.getUuid()).isNull(); - assertThat(external.getUri().toString()).isEqualTo(NS + "ExternalBase"); - assertThat(external.getLabel().getValue()).isEqualTo("ExternalBase"); - assertThat(external.getComment()).isNull(); - }); + .satisfies( + external -> { + assertThat(external.getUuid()).isNull(); + assertThat(external.getUri().toString()).isEqualTo(NS + "ExternalBase"); + assertThat(external.getLabel().getValue()).isEqualTo("ExternalBase"); + assertThat(external.getComment()).isNull(); + }); } @Test @@ -218,7 +224,8 @@ void attributes() { } @Test - @DisplayName("reads fixed values from direct literals and default values from blank-node wrappers") + @DisplayName( + "reads fixed values from direct literals and default values from blank-node wrappers") void fixedAndDefaultValues() { var attribute = breaker().getAttributes().getFirst(); @@ -277,6 +284,18 @@ void requiredProperties() { assertThatThrownBy(association::getMultiplicity).isInstanceOf(IllegalStateException.class); } + @Test + @DisplayName("falls back to the uri suffix for classes without a label") + void classLabelFallback() { + var dataTypeResource = model.getResource(XSD.xstring.getURI()); + dataTypeResource.addProperty(RDFA.uuid, "00000000-0000-0000-0000-00000000000c"); + + var dataType = breaker().getAttributes().getFirst().getDataType(); + + assertThat(dataType).isInstanceOf(CIMClass.class); + assertThat(dataType.getLabel().getValue()).isEqualTo("string"); + } + @Test @DisplayName("resolves a class category by uuid and falls back to the default category") void classCategoryLookup() { diff --git a/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMFacadeCollectionSvelteFlowServiceTest.java b/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMFacadeCollectionSvelteFlowServiceTest.java index 6b05e48a..9b037d16 100644 --- a/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMFacadeCollectionSvelteFlowServiceTest.java +++ b/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMFacadeCollectionSvelteFlowServiceTest.java @@ -59,8 +59,7 @@ class RenderCIMFacadeCollectionSvelteFlowServiceTest { private static final UUID LOOSE_UUID = UUID.fromString("00000000-0000-0000-0000-000000000008"); private static final UUID EXTERNAL_CAT_UUID = UUID.fromString("00000000-0000-0000-0000-000000000009"); - private static final UUID REMOTE_UUID = - UUID.fromString("00000000-0000-0000-0000-00000000000a"); + private static final UUID REMOTE_UUID = UUID.fromString("00000000-0000-0000-0000-00000000000a"); private final RenderCIMFacadeCollectionSvelteFlowService renderer = new RenderCIMFacadeCollectionSvelteFlowService(); @@ -155,7 +154,8 @@ private void addAttribute( attribute.addProperty(RDFS.label, model.createLiteral(label, "en")); attribute.addProperty(CIMS.stereotype, CIMStereotypes.attribute); attribute.addProperty(RDFS.domain, domain); - attribute.addProperty(CIMS.multiplicity, model.createResource(CIMS.namespace + multiplicity)); + attribute.addProperty( + CIMS.multiplicity, model.createResource(CIMS.namespace + multiplicity)); attribute.addProperty(CIMS.datatype, model.createResource(XSD.xstring.getURI())); } @@ -181,8 +181,7 @@ void rendersPackageClasses() { .extracting(node -> node.getData().getLabel()) .containsExactlyInAnyOrder("Root", "Base", "Child", "PhaseCode", "Terminal"); assertThat(nodeByLabel(result, "Child").getId()).isEqualTo(CHILD_UUID); - assertThat(nodeByLabel(result, "Child").getData().getBelongsToCategory()) - .isEqualTo("Core"); + assertThat(nodeByLabel(result, "Child").getData().getBelongsToCategory()).isEqualTo("Core"); assertThat(nodeByLabel(result, "Terminal").getData().getBelongsToCategory()) .isEqualTo("Other"); } diff --git a/frontend/src/lib/rendering/svelteflow/components/ClassNode.svelte b/frontend/src/lib/rendering/svelteflow/components/ClassNode.svelte index 9dc76c4a..3ba92db1 100644 --- a/frontend/src/lib/rendering/svelteflow/components/ClassNode.svelte +++ b/frontend/src/lib/rendering/svelteflow/components/ClassNode.svelte @@ -64,6 +64,7 @@ const stereotypes = $derived(data.stereotypes); const attributes = $derived(data.attributes); const enumEntries = $derived(data.enumEntries); + const inheritedGroups = $derived([...(data.superClasses ?? [])].reverse()); const cursorClass = $derived(dragging ? "cursor-move" : "cursor-pointer"); @@ -139,6 +140,26 @@
+ {#if userSettings.get("showInheritedProperties", true) && inheritedGroups.length > 0} + {#each inheritedGroups as superClass} +
+ {superClass.label} +
+ {#each superClass.attributes ?? [] as attr} +
+ {attr.label}: {attr.type}  [{attr.multiplicity}] +
+ {/each} + {#each superClass.enumEntries ?? [] as enumEntry} +
+ {enumEntry.label ?? enumEntry} +
+ {/each} + {#if (superClass.attributes?.length ?? 0) === 0 && (superClass.enumEntries?.length ?? 0) === 0} +
-
+ {/if} + {/each} + {/if} {#if attributes && attributes.length > 0} {#if isCrossProfileDiagram} {#each groupByGraphURI(attributes) as group} diff --git a/frontend/src/routes/UserSettingDialog.svelte b/frontend/src/routes/UserSettingDialog.svelte index 33c7085c..6c98ec2f 100644 --- a/frontend/src/routes/UserSettingDialog.svelte +++ b/frontend/src/routes/UserSettingDialog.svelte @@ -30,6 +30,7 @@ defaultExportFormat: supportedRDFMediaTypes[0].mimeType, showPackagePrefix: false, useColoredPropertiesInMergedView: true, + showInheritedProperties: true, normalizeComments: true, }; @@ -94,6 +95,11 @@ bind:value={localSettings["useColoredPropertiesInMergedView"]} labelFirst={false} /> +