Skip to content

feat: Support blank nodes in attribute editor for fixed and default values (RDFA-321)#22

Open
spah-soptim wants to merge 6 commits intomainfrom
feature/RDFA-321-attribute-editor-fix
Open

feat: Support blank nodes in attribute editor for fixed and default values (RDFA-321)#22
spah-soptim wants to merge 6 commits intomainfrom
feature/RDFA-321-attribute-editor-fix

Conversation

@spah-soptim
Copy link
Copy Markdown
Member

@spah-soptim spah-soptim commented Feb 23, 2026

Description

Added RDF blank-node support for attribute cims:isFixed / cims:isDefault values, in addition to literals.

  • New AttributeValueNode base class (parent of CIMSIsFixed/CIMSIsDefault) carries value, dataType, and a blankNode flag.
  • Read: new ValueNodeParser replaces the previous literal/blank-node extraction in CIMQuerySolutionParser. The fetcher now passes the dataset Model through so blank-node properties can be enumerated.
  • Write: AttributeFixedDefaultResolver decides per-attribute whether the new value should be literal vs. blank-node - preserves existing shape, otherwise defaults to attributes.newValuesBlankNode (default false).
  • Replace/delete: CIMUpdates now emits a multi-statement UpdateRequest that first cleans orphan blank-node triples (since ?attribute SPO does not reach the inner triples of nested resources), then deletes the attribute, then inserts the new one.
  • AttributeMapper.buildXsdDatatype now goes through XSDDatatypeMapper (now for example xsd:string not xsd:String).
  • Frontend reactive mapper now passes through fixedValue/defaultValue.

Test Checklist

General Behavior

  • Components reload automatically when data changes
  • Editing features are disabled in readonly datasets
  • Dialogs pre-select current dataset/graph
  • Required fields are validated in dialogs
  • Discarding unsaved changes opens a discard cancel confirm dialog

Global MenuBar

  • Navigate to home page works
  • File menu:
    • Import → Graph/SHACL works
    • Export → Graph/SHACL works
    • Share Snapshot works
    • Delete → Dataset/Graph works
  • Edit menu:
    • New → Class works
    • New → Package works
    • Edit/View → Create/Edit/View Ontology works
    • Edit/View → Package works
    • Undo/Redo (Ctrl+Z / Ctrl+Y) works
    • Enable/Disable editing works
    • Manage/View namespaces works
    • Delete → Ontology/Package works
  • View menu:
    • Changelog opens and shows current graph
    • Compare Graphs opens
    • Full SHACL works
  • Help menu:
    • Help link works
    • Submit Feedback link works
    • About navigation works

Welcome Page

  • Navigation to Editor works
  • Tips are displayed
  • Security and data information displayed
  • Copyright and version information displayed

Editor - MenuBar

  • Search function works with all filters (All Datasets, Current Dataset, Current Graph, Current Package)
  • Search finds classes, attributes, associations, packages
  • "Enable Editing" button appears for readonly datasets

Editor - Navigation

  • Hierarchical display (Datasets → Graphs → Packages) works
  • Selection is highlighted
  • Selecting a class does not change dataset/graph/package selection
  • Class selection stays open/highlighted when switching dataset/graph/package
  • Datasets and graphs are collapsible
  • Single click selects; double click or chevron toggles expand/collapse
  • State persists on reload (non-browser)
  • Context menus act on the dataset/graph/package they were opened on
  • Hover labels show prefixes when configured
  • Dataset context menu:
    • Import graph works (disabled in readonly datasets)
    • Share Snapshot works
    • Enable/Disable editing works
    • Manage/View namespaces works
    • Delete dataset works
  • Graph context menu:
    • New package works (disabled in readonly datasets)
    • Undo/Redo works (only enabled when available)
    • Create Ontology
    • Edit Ontology (View Ontology in readonly)
    • Delete Ontology
    • Changelog navigation works
    • Compare dialog works
    • SHACL import/export/full view works (import disabled in readonly datasets)
    • Export graph works
    • Delete graph works (disabled in readonly datasets)
  • Package context menu:
    • Create new class works (disabled in readonly datasets)
    • View/Edit package works
    • Copy URL works
    • Delete package works (disabled for external/default packages and readonly datasets)
  • Class context menu:
    • Open class (editor) works
    • SHACL works
    • Delete class works (disabled in readonly datasets)

Editor - Package View

  • Class diagram displays correctly
  • Loading animation shows while loading
  • Info cards show when no package or no classes available
  • Drag and zoom diagram works
  • "Reset View" button works
  • "Filter View" works
  • Click on class opens class editor

Editor - Class Editor

  • Display and edit class properties: UUID (readonly), Label, Namespace, Package, Derived from, Abstract, Stereotypes, Attributes, Associations, Comment
  • Delete class works
  • Save changes works
  • Discard changes works
  • Attribute Editor works
  • Association Editor works
  • attribute/association SHACL View works
  • Class SHACL View works

Prefixes Page

  • View, add, remove and edit namespaces works

Changelog Page

  • Select graph and display write operations works
  • Operations shown in reverse chronological order
  • Detailed view of changed triples works
  • Restoring graph to a version works

Compare Page

  • Compare two graphs works

@spah-soptim spah-soptim self-assigned this Feb 23, 2026
@spah-soptim spah-soptim added the bug Something isn't working label Feb 23, 2026
@spah-soptim spah-soptim changed the title RDFA321: Support Blank Nodes in Attribute Editor (Fixed/Default values) RDFA-321: Support Blank Nodes in Attribute Editor (Fixed/Default values) Feb 23, 2026
@spah-soptim spah-soptim force-pushed the feature/RDFA-321-attribute-editor-fix branch 2 times, most recently from e463e38 to 95ced91 Compare February 25, 2026 15:12
@spah-soptim spah-soptim marked this pull request as draft March 10, 2026 16:51
@spah-soptim spah-soptim changed the title RDFA-321: Support Blank Nodes in Attribute Editor (Fixed/Default values) feat: Support blank nodes in attribute editor for fixed and default values (RDFA-321) Mar 11, 2026
@spah-soptim spah-soptim force-pushed the feature/RDFA-321-attribute-editor-fix branch from d3a4163 to f5b701b Compare April 27, 2026 06:14
Comment thread backend/src/main/java/org/rdfarchitect/models/cim/data/dto/relations/CIMSIsFixed.java Dismissed
Fixed and default attribute values can be persisted in two equivalent
shapes in the source RDF: as a direct literal triple, or as a blank-node
wrapper (subject -> predicate -> _:blank ; _:blank -> rdfs:Literal ->
literal). Until now the model layer collapsed both shapes into a plain
literal, so editing an attribute backed by a blank-node value silently
dropped that blank node on write.

Introduced a shared `AttributeValueNode` base for `CIMSIsFixed` and `CIMSIsDefault` carrying a `blankNode` flag, and a `ValueNodeParser` utility that turns an `RDFNode` into a `ParsedValue(value, dataType, blankNode)` while validating the blank-node shape (exactly one statement with `rdfs:Literal` predicate and a literal object).
`CIMQuerySolutionParser` takes an optional `Model` so it can re-resolve blank-node properties from the source dataset; `CIMObjectFactory.createCIMAttributeList` and `CIMObjectFetcher.fetchCIMAttributeList` thread that model through.

`CIMUpdates.appendValueNode` emits either a direct literal or a fresh blank-node wrapper depending on `blankNode`. Because removing an attribute via `?attr ?p ?o` does not reach the inner triples of an attached blank-node value, `replaceAttribute`/`replaceAttributes` now return an `UpdateRequest` composed of a separate blank-node cleanup, the SPO delete and the insert; `deleteClass` performs the same cleanup before deleting its attributes. `InMemorySparqlExecutor` gains an `executeSingleUpdate(UpdateRequest)` overload to run those compound requests in a single write transaction.
Adds the resolver that decides, for every attribute being created or replaced, whether its fixed/default value should be persisted as a direct literal or as a blank-node wrapper:

- if the attribute already exists in the graph, the existing shape wins (a blank-node value stays a blank-node value, a literal stays a literal),
- otherwise the new `attributes.newValuesBlankNode` config flag decides; defaults to `false` so behaviour is unchanged for fresh installs.

shape as `AssociationsService` — a single `resolve()` followed by a `AttributeFixedDefaultResolver` is a `@Service` with the config injected via `@Value` constructor parameter. It self-manages a READ transaction when none is active and reads inline when called from inside an existing WRITE transaction (e.g. `UpdateClassService.replaceClass`), so callers don't need transaction boilerplate. `AttributesService` keeps the same single `executeSingleUpdate()`.

`AttributeMapper` now resolves the canonical XSD datatype URI (`xsd:string` instead of `xsd:String`) for fixed/default values via `XSDDatatypeMapper`; the resolver re-resolves it from the graph for non-primitive attribute datatypes.

Frontend: `mapAttributeDtoToReactiveAttribute` passes `fixedValue` and `defaultValue` through so they are available on the reactive object.
Signed-off-by: Jan-Hendrik Spahn <jan-hendrik.spahn@soptim.de>
…te insert

Signed-off-by: Jan-Hendrik Spahn <jan-hendrik.spahn@soptim.de>
…TOCTOU

- CIMUpdates.deleteAttributeValueNodes{,ForClass} now build their SPARQL
  via UpdateBuilder + ExprFactory.isBlank instead of raw string concat,
  so user-controlled classUUID and graphURI flow through Jena's node
  coercion and can no longer be used to inject SPARQL.

- InMemorySparqlExecutor gains an executeSingleUpdate overload that
  takes a Function<Graph, UpdateRequest> and runs build + execute in a
  single WRITE transaction. AttributesService uses it so the
  fixed/default shape resolution and the resulting update happen
  atomically — the previous read-then-write split could see another
  writer change the existing value's shape between the two
  transactions.

- AttributeFixedDefaultResolver no longer self-manages a READ
  transaction; the caller's open transaction is required (and is now
  always a WRITE).

- ValueNodeParser.parseLiteral takes a Literal directly so the contract
  is enforced at the call site.

- Add a comment explaining why insertAttribute uses BIND to force the
  WHERE clause to match exactly once.

Signed-off-by: Jan-Hendrik Spahn <jan-hendrik.spahn@soptim.de>
@spah-soptim spah-soptim force-pushed the feature/RDFA-321-attribute-editor-fix branch from f5b701b to d8ac3d3 Compare April 30, 2026 06:30
Signed-off-by: Jan-Hendrik Spahn <jan-hendrik.spahn@soptim.de>
@spah-soptim spah-soptim marked this pull request as ready for review April 30, 2026 08:13
@spah-soptim spah-soptim removed the request for review from rema-soptim April 30, 2026 08:13
graph.begin(TxnType.WRITE);
var cimClass = classMapper.toCIMObject(newClass);
// resolver inherits the open WRITE txn for its read-only attribute lookup
fixedDefaultResolver.resolve(graph, cimClass.getAttributes());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still find this call questionable. It suggests that either classMapper.toCIMObject or CIMUpdates.replaceClass isn't working properly and needs an external fix.

Comment on lines +38 to +42
public void executeSingleUpdate(
GraphRewindableWithUUIDs graph, UpdateRequest update, String graphUri) {
executeSingleUpdate(graph, graphUri, g -> update);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this method make sense, if it's only used in unit tests?

public void executeSingleUpdate(
GraphRewindableWithUUIDs graph,
String graphUri,
Function<GraphRewindableWithUUIDs, UpdateRequest> updateBuilder) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really see the need to use a lamba. Why not just prepare the argument beforehand and then pass it? And if you need to access the graph beforehand then just don't use the executeSingleUpdate, which makes this overly complicated.

String classUUID,
List<CIMAttribute> attributes) {
var baseUpdate = CIMUpdates.deleteAttributes(prefixMapping, graphURI, classUUID);
var updateRequest = deleteAttributeValueNodesForClass(graphURI, classUUID);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't it make more sense to just enhance the delete Attribute Query instead of doing this separately, since value nodes are part of an attribute.

PrefixMapping prefixMapping, String graphURI, CIMAttribute attribute) {
var baseUpdate = deleteAttribute(prefixMapping, graphURI, attribute.getUuid());
return appendInsertAttribute(baseUpdate, attribute);
var updateRequest = deleteAttributeValueNodes(graphURI, attribute.getUuid());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. this should not be its own method, but part of delete Attribute.

* the properties of an attribute fixed/default value that was returned from the query as a
* blank node. May be {@code null} when blank-node values are not expected.
*/
private final Model valueNodeModel;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CIMQuerySolutionParser should not have a model. Its job is to parse the result of a query, not query the model itself. Instead the query should be extended. For attributes that would be the query created in CIMQueries.getAttributesQuery.

@@ -0,0 +1,128 @@
/*
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you just parse a query result in CIMQuerySolutionParser this file is either obsolete or heavily refactored to parse a query result instead of searching the model itself

graph,
graphUri,
g -> {
fixedDefaultResolver.resolve(g, cimAttribute);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should either inside CIMUpdates.insertAttribute or attributeMapper.toCIMObject

graph,
graphUri,
g -> {
fixedDefaultResolver.resolve(g, cimAttribute);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also

graph,
graphUri,
g -> {
fixedDefaultResolver.resolve(g, attributeCIMObjects);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants