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..1b12ba2d --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/api/dto/rendering/svelteflow/sub/SuperClassDTO.java @@ -0,0 +1,35 @@ +/* + * 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 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; + private String label; + private List attributes; + private List enumEntries; +} 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..a58ed0c4 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMAssociation.java @@ -0,0 +1,82 @@ +/* + * 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..a3557260 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMAttribute.java @@ -0,0 +1,135 @@ +/* + * 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 new file mode 100644 index 00000000..4b0c9f83 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClass.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.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.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; + +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); + } + + 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 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) { + 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 CIMClassCategory.fromResource(getGraphUri(), getModel(), category); + } + + @Override + public List getStereotypes() { + return getStereotypeList(); + } + + @Override + public List getAttributes() { + 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() { + 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() { + 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..16bb0999 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMClassCategory.java @@ -0,0 +1,67 @@ +/* + * 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.RDFSLabel; +import org.rdfarchitect.models.cim.rdf.resources.CIMS; + +import java.util.ArrayList; +import java.util.List; +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); + } + + 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 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(); + 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/CIMEnumEntry.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMEnumEntry.java new file mode 100644 index 00000000..4a4759bc --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMEnumEntry.java @@ -0,0 +1,51 @@ +/* + * 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/CIMModelFacade.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMModelFacade.java new file mode 100644 index 00000000..87c22528 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMModelFacade.java @@ -0,0 +1,114 @@ +/* + * 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.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.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; + +@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 = + 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)); + } + packages.add(new DefaultCIMClassCategory(this.graphUri, this.model)); + 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(); + 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/CIMResource.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMResource.java new file mode 100644 index 00000000..f53a2bf2 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/CIMResource.java @@ -0,0 +1,207 @@ +/* + * 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.RDFNode; +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 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; + +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(); + if (!subject.isLiteral()) { + throw new IllegalStateException("UUID for resource is not a literal."); + } + this.uuid = UUID.fromString(subject.asLiteral().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 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 = node.asLiteral(); + return new RDFSLabel(langLiteral.getString(), langLiteral.getLanguage()); + } + + @Override + public RDFSComment getComment() { + var node = getUniqueJenaPropertyNode(RDFS.comment); + if (node == null) { + return null; + } + 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())); + } + + // 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 res = new ArrayList(); + 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()) { + 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/DefaultCIMClassCategory.java b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/DefaultCIMClassCategory.java new file mode 100644 index 00000000..3afd1e83 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/DefaultCIMClassCategory.java @@ -0,0 +1,80 @@ +/* + * 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.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.CIMS; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +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(CIMS.belongsToCategory)) + .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 new file mode 100644 index 00000000..35d56c4d --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ExternalCIMClass.java @@ -0,0 +1,117 @@ +/* + * 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 org.rdfarchitect.models.cim.rdf.resources.RDFA; + +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() { + if (!this.resource.hasProperty(RDFA.uuid)) { + return null; + } + return UUID.fromString(this.resource.getProperty(RDFA.uuid).getObject().toString()); + } + + @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 new file mode 100644 index 00000000..f4513fa4 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMAssociation.java @@ -0,0 +1,34 @@ +/* + * 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(); + + ICIMClass getRange(); + + 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..406e7260 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMClassCategory.java @@ -0,0 +1,25 @@ +/* + * 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; + +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 new file mode 100644 index 00000000..886eb980 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMEnumEntry.java @@ -0,0 +1,27 @@ +/* + * 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 extends ICIMResource { + + ICIMClass getDomain(); + + CIMSStereotype getStereotype(); +} 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/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..21f35552 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/models/cim/data/dto/facade/ICIMResource.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.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(); +} 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 new file mode 100644 index 00000000..588f1903 --- /dev/null +++ b/backend/src/main/java/org/rdfarchitect/services/rendering/svelteflow/RenderCIMFacadeCollectionSvelteFlowService.java @@ -0,0 +1,407 @@ +/* + * 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 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.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.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.rdf.resources.CIMStereotypes; +import org.rdfarchitect.models.cim.rendering.GraphFilter; +import org.rdfarchitect.services.rendering.RenderCIMFacadeCollectionUseCase; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +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. + */ +@Service +@ConditionalOnProperty( + name = "rendering.renderer", + havingValue = "svelteflow", + matchIfMissing = true) +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"; + + 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(); + } + + 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 new file mode 100644 index 00000000..d9092b92 --- /dev/null +++ b/backend/src/test/java/org/rdfarchitect/models/cim/data/dto/facade/CIMFacadeTest.java @@ -0,0 +1,357 @@ +/* + * 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.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("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() { + 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() { + 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(); + } +} 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..9b037d16 --- /dev/null +++ b/backend/src/test/java/org/rdfarchitect/services/rendering/RenderCIMFacadeCollectionSvelteFlowServiceTest.java @@ -0,0 +1,410 @@ +/* + * 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(); + } +} 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} /> +