Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.apache.jena.riot.system.PrefixEntry;
import org.rdfarchitect.database.GraphIdentifier;
import org.rdfarchitect.models.cim.data.dto.relations.uri.URI;
import org.rdfarchitect.models.cim.rdf.resources.RDFA;
import org.rdfarchitect.services.ExpandURIUseCase;
import org.rdfarchitect.services.shacl.SHACLExportUseCase;
import org.rdfarchitect.services.shacl.SHACLGenerateUseCase;
Expand Down Expand Up @@ -90,7 +91,7 @@ public String getGeneratedSHACLAsString(
var result =
shaclGenerateUseCase.exportGeneratedSHACLGraph(
new GraphIdentifier(datasetName, extendedGraphURI),
PrefixEntry.create("rdfash", "http://www.example.com/shacl#"));
PrefixEntry.create(RDFA.NS_PREFIX_SHACL, RDFA.NS_URI_SHACL));

logger.info(
"Sending response to GET request: \"/api/datasets/{{}}/graphs/{{}}/shacl/generate/string\" to \"{}\".",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.apache.jena.graph.Triple;
import org.apache.jena.graph.compose.CompositionBase;
import org.apache.jena.graph.impl.SimpleEventManager;
import org.apache.jena.shared.PrefixMapping;
import org.apache.jena.sparql.graph.GraphFactory;
import org.apache.jena.util.iterator.ExtendedIterator;
import org.jetbrains.annotations.NotNull;
Expand All @@ -49,21 +50,31 @@ public class DeltaCompressible extends CompositionBase {
/** -- GETTER -- Answer the graph of all triples removed. */
@Getter private Graph deletions;

/**
* Records prefix changes as a delta over the base graph's prefix mapping, mirroring the way
* {@link #additions} and {@link #deletions} record triple changes.
*/
private DeltaPrefixMapping prefixMapping;

@Getter private final UUID versionId = UUID.randomUUID();

public DeltaCompressible(@NotNull Graph base) {
super();
this.base = base;
this.additions = GraphMemFactory.createDefaultGraph();
this.deletions = GraphMemFactory.createDefaultGraph();
this.prefixMapping = new DeltaPrefixMapping(base.getPrefixMapping());
}

public void compress() {
var newBase = GraphFactory.createDefaultGraph();
GraphUtil.add(newBase, this.find());
// Fold the prefix delta into the new base, then start a fresh (empty) prefix delta.
newBase.getPrefixMapping().setNsPrefixes(getPrefixMapping().getNsPrefixMap());
base = newBase;
additions = GraphMemFactory.createDefaultGraph();
deletions = GraphMemFactory.createDefaultGraph();
prefixMapping = new DeltaPrefixMapping(base.getPrefixMapping());
}

/** Add the triple to the graph, ie add it to the additions, remove it from the removals. */
Expand Down Expand Up @@ -95,6 +106,22 @@ protected ExtendedIterator<Triple> graphBaseFind(Triple t) {
return SimpleEventManager.notifyingRemove(this, iterator);
}

/**
* Returns the delta-aware prefix mapping. Writes are recorded as additions/deletions over the
* base prefix mapping; reads fold the delta over the base.
*/
@Override
public PrefixMapping getPrefixMapping() {
return prefixMapping;
}

/**
* @return {@code true} if any prefix has been added or removed relative to the base
*/
public boolean hasPrefixChanges() {
return prefixMapping.hasChanges();
}

@Override
public void close() {
super.close();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/*
* 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.rdf.graph;

import org.apache.jena.shared.JenaException;
import org.apache.jena.shared.PrefixMapping;
import org.apache.jena.shared.impl.PrefixMappingImpl;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;

/**
* A {@link PrefixMapping} implementation that records prefix changes as deltas similar to the
* {@link org.rdfarchitect.rdf.graph.wrapper.RDFGraphDelta}
*/
public class DeltaPrefixMapping implements PrefixMapping {

private static final Pattern PREFIX_PATTERN = Pattern.compile("([A-Za-z][A-Za-z0-9-_.]*)?");

private final PrefixMapping base;
private final Map<String, String> additions = new HashMap<>();
private final Map<String, String> deletions = new HashMap<>();
private boolean locked = false;

public DeltaPrefixMapping(PrefixMapping base) {
this.base = base;
}

// -------------------------------------------------------------------------
// Delta access — used by DeltaCompressible to detect and fold changes
// -------------------------------------------------------------------------

/**
* @return an unmodifiable view of the prefix additions relative to the base
*/
public Map<String, String> getAddedPrefixes() {
return Map.copyOf(additions);
}

/**
* @return an unmodifiable view of the prefix names removed relative to the base
*/
public Map<String, String> getDeletedPrefixes() {
return Map.copyOf(deletions);
}

/**
* @return {@code true} if any prefix has been added or removed relative to the base
*/
public boolean hasChanges() {
return !additions.isEmpty() || !deletions.isEmpty();
}

/**
* Builds a plain {@link PrefixMapping} snapshot of the current folded state (base − deletions +
* additions). Read-only queries delegate to this so resolution behaves exactly like Jena.
*/
private PrefixMapping folded() {
return new PrefixMappingImpl().setNsPrefixes(getNsPrefixMap());
}

private void checkUnlocked() {
if (locked) {
throw new JenaException("Attempted to modify a locked PrefixMapping.");
}
}

private void validatePrefix(String prefix, String uri) {
Objects.requireNonNull(uri, "null URIs are prohibited as arguments to setNsPrefix");
if (!PREFIX_PATTERN.matcher(prefix).matches()) {
throw new PrefixMapping.IllegalPrefixException(prefix);
}
}

// -------------------------------------------------------------------------
// Writes — record the change as a delta
// -------------------------------------------------------------------------

@Override
public PrefixMapping setNsPrefix(String prefix, String uri) {
checkUnlocked();
validatePrefix(prefix, uri);
// Mirror DeltaCompressible#performAdd: setting a prefix to its base value is not a change.
if (uri.equals(base.getNsPrefixURI(prefix))) {
additions.remove(prefix);
deletions.remove(prefix);
return this;
}
additions.put(prefix, uri);
deletions.remove(prefix);
return this;
}

@Override
public PrefixMapping removeNsPrefix(String prefix) {
checkUnlocked();
additions.remove(prefix);
String baseUri = base.getNsPrefixURI(prefix);
if (baseUri != null) {
deletions.put(prefix, baseUri);
}
return this;
}

@Override
public PrefixMapping clearNsPrefixMap() {
checkUnlocked();
additions.clear();
deletions.clear();
deletions.putAll(base.getNsPrefixMap());
return this;
}

@Override
public PrefixMapping setNsPrefixes(PrefixMapping other) {
return setNsPrefixes(other.getNsPrefixMap());
}

@Override
public PrefixMapping setNsPrefixes(Map<String, String> other) {
checkUnlocked();
other.forEach(this::setNsPrefix);
return this;
}

@Override
public PrefixMapping withDefaultMappings(PrefixMapping other) {
checkUnlocked();
other.getNsPrefixMap()
.forEach(
(prefix, uri) -> {
if (getNsPrefixURI(prefix) == null && getNsURIPrefix(uri) == null) {
setNsPrefix(prefix, uri);
}
});
return this;
}

// -------------------------------------------------------------------------
// Reads — fold the delta over the base: base − deletions + additions
// -------------------------------------------------------------------------

@Override
public Map<String, String> getNsPrefixMap() {
Map<String, String> folded = new HashMap<>(base.getNsPrefixMap());
deletions.keySet().forEach(folded::remove);
folded.putAll(additions);
return folded;
}

@Override
public String getNsPrefixURI(String prefix) {
if (additions.containsKey(prefix)) {
return additions.get(prefix);
}
if (deletions.containsKey(prefix)) {
return null;
}
return base.getNsPrefixURI(prefix);
}

@Override
public String getNsURIPrefix(String uri) {
return folded().getNsURIPrefix(uri);
}

@Override
public String expandPrefix(String prefixed) {
return folded().expandPrefix(prefixed);
}

@Override
public String shortForm(String uri) {
return folded().shortForm(uri);
}

@Override
public String qnameFor(String uri) {
return folded().qnameFor(uri);
}

@Override
public int numPrefixes() {
return getNsPrefixMap().size();
}

@Override
public boolean samePrefixMappingAs(PrefixMapping other) {
return getNsPrefixMap().equals(other.getNsPrefixMap());
}

@Override
public PrefixMapping lock() {
this.locked = true;
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,9 @@ protected void compressBase() {
* @return True if the current graph is the same as the last version, otherwise false.
*/
protected boolean noChangesInTransaction() {
return currentDelta.getAdditions().isEmpty() && currentDelta.getDeletions().isEmpty();
return currentDelta.getAdditions().isEmpty()
&& currentDelta.getDeletions().isEmpty()
&& !currentDelta.hasPrefixChanges();
}

// Graph
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,9 @@ public DeltaCompressible getLastDelta() {

@Override
public boolean hasChanges() {
return !currentDelta.getAdditions().isEmpty() || !currentDelta.getDeletions().isEmpty();
return !currentDelta.getAdditions().isEmpty()
|| !currentDelta.getDeletions().isEmpty()
|| currentDelta.hasPrefixChanges();
}

// -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,23 +81,14 @@ public class SHACLStoringService

@Override
public void replaceCustomSHACLGraph(GraphIdentifier graphIdentifier, Graph shacl) {
var normalized = GraphUtils.normalizeBlankNodes(shacl);
try (var ctx = databasePort.getGraphWithContext(graphIdentifier).begin(ReadWrite.WRITE)) {
var storedGraph = ctx.getCustomSHACL();

var toDelete = storedGraph.find().filterKeep(t -> !normalized.contains(t)).toList();
var toAdd = normalized.find().filterKeep(t -> !storedGraph.contains(t)).toList();

if (toDelete.isEmpty() && toAdd.isEmpty()) {
return;
}

toDelete.forEach(storedGraph::delete);
toAdd.forEach(storedGraph::add);

storedGraph.clear();
var storedModel = ModelFactory.createModelForGraph(storedGraph);
var newModel = ModelFactory.createModelForGraph(GraphUtils.normalizeBlankNodes(shacl));
storedModel.clearNsPrefixMap();
storedModel.setNsPrefixes(ModelFactory.createModelForGraph(normalized));
storedModel.add(newModel);
storedModel.setNsPrefixes(newModel);

ctx.commit("Replace custom SHACL");
}
Expand Down
Loading