Skip to content

Conversation

@berezovskyi
Copy link
Contributor

@berezovskyi berezovskyi commented Jan 16, 2026

For RefImpl:

Do not treat a nil base cutoff as a rebase reason if it has been fetched before and had the same rdf:nil value.

This could happen if a server starts with an empty base (legit, afaik) and keeps adding change events to the log, never rebasing on the server side (legit, afaik). The PR prevents an endless rebase loop that continues until the server base becomes non-empty.

For Jazz:

Handle ldp:DirectContainer, not just ldp:Container.

The handling is quite hacky and should be refactored.

Checklist

  • This PR adds an entry to the CHANGELOG. See https://keepachangelog.com/en/1.0.0/ for instructions. Minor edits are exempt.
  • This PR was tested on at least one Lyo OSLC server (comment @oslc-bot /test-all if not sure) or adds unit/integration tests.
  • This PR does NOT break the API
  • Lint checks pass (run mvn package org.openrewrite.maven:rewrite-maven-plugin:run spotless:apply -DskipTests -P'!enforcer' if not, commit & push)

this could happen if a server starts with an empty base (legit, afaik) and keeps adding change events to the log, never rebasing on the server side (legit, afaik)

Signed-off-by: Andrew Berezovskyi <andriib@kth.se>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request fixes a bug in the TRS (Tracked Resource Set) client where a nil base cutoff event was incorrectly treated as a reason to trigger a server rollback exception. The fix enables proper handling of servers that start with an empty base and accumulate change events without performing server-side rebases.

Changes:

  • Refactored the fetchRemoteChangeLogs method to properly handle null and RDF.nil cutoff events
  • Changed from a do-while loop to a while loop with clearer control flow
  • Added upfront checks to determine if all change events should be processed when there's no valid cutoff event

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 263 to 287
private boolean fetchRemoteChangeLogs(ChangeLog currentChangeLog, List<ChangeLog> changeLogs) {
boolean foundChangeEvent = false;
URI previousChangeLog;
do {
if (currentChangeLog != null) {
changeLogs.add(currentChangeLog);
if (lastProcessedChangeEventUri == null || RDF.nil.getURI().equals(lastProcessedChangeEventUri.toString())) {
foundChangeEvent = true;
}

while (currentChangeLog != null) {
changeLogs.add(currentChangeLog);
if (lastProcessedChangeEventUri != null && !RDF.nil.getURI().equals(lastProcessedChangeEventUri.toString())) {
if (ProviderUtil.changeLogContainsEvent(lastProcessedChangeEventUri,
currentChangeLog)) {
foundChangeEvent = true;
break;
}
previousChangeLog = currentChangeLog.getPrevious();
currentChangeLog = trsClient.fetchRemoteChangeLog(previousChangeLog);
} else {
}

URI previousChangeLog = currentChangeLog.getPrevious();
if (previousChangeLog == null || RDF.nil.getURI().equals(previousChangeLog.toString())) {
break;
}
} while (!RDF.nil.getURI().equals(previousChangeLog.toString()));

currentChangeLog = trsClient.fetchRemoteChangeLog(previousChangeLog);
}
return foundChangeEvent;
}
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

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

This fix addresses an important scenario where a server starts with an empty base (cutoff event is null or RDF.nil) and adds change events without rebasing. However, there doesn't appear to be test coverage for this specific scenario. Consider adding a test case that verifies the behavior when lastProcessedChangeEventUri is set to RDF.nil (from an empty base) to ensure this fix works correctly and to prevent regression.

Copilot uses AI. Check for mistakes.
Signed-off-by: Andrew Berezovskyi <andriib@kth.se>
Signed-off-by: Andrew Berezovskyi <andriib@kth.se>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +281 to +284
if (ProviderUtil.isNilUri(lastProcessedChangeEventUri) && ProviderUtil.isNilUri(previousChangeLog)) {
foundChangeEvent = true;
break;
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The new nil-handling branch in fetchRemoteChangeLogs changes rollback/rebase behavior when both the stored cutoff and the changelog previous link are rdf:nil (or null via isNilUri). There doesn't appear to be a unit test covering this path for TrsProviderHandler (existing tests set cutoffEvent to a non-nil URI). Please add/extend a test that exercises the lastProcessedChangeEventUri == rdf:nil + currentChangeLog.previous == rdf:nil case to prevent regressions.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we already do this for the concurrent provider in ConcurrentTrsProviderHandlerTest - we should unify the logic if possible

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.21.0</version>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

should be dep-managed

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

why?

Mainly due to DirectContainer, the current approach is quite hacky

Signed-off-by: Andrew Berezovskyi <andriib@kth.se>
@berezovskyi berezovskyi changed the title fix: do not treat a nil base cutoff as a rebase reason fix(trs): consume RefImpl and Jazz TRS feeds Jan 24, 2026
throws TrsEndpointConfigException, TrsEndpointErrorException, LyoModelException {
final Response.StatusType responseInfo = response.getStatusInfo();
final Response.Status.Family httpCodeType = responseInfo.getFamily();
log.info("HTTP response status: {} {}", responseInfo.getStatusCode(), responseInfo.getReasonPhrase());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

debug

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +148 to +191
Resource containerResource = directContainers.next();
log.debug("Found ldp:DirectContainer at {}", containerResource.getURI());

Base base = new Base();
try {
base.setAbout(new URI(containerResource.getURI()));
} catch (URISyntaxException e) {
throw new LyoModelException(e);
}

// Extract cutoffEvent
if (containerResource.hasProperty(rdFModel.getProperty(TRS_CUTOFF_EVENT))) {
Resource cutoffResource = containerResource.getPropertyResourceValue(
rdFModel.getProperty(TRS_CUTOFF_EVENT)
);
if (cutoffResource != null && cutoffResource.isURIResource()) {
try {
base.setCutoffEvent(new URI(cutoffResource.getURI()));
} catch (URISyntaxException e) {
throw new LyoModelException(e);
}
}
}

// Extract members (try both rdfs:member and ldp:member)
org.apache.jena.rdf.model.Property memberProp = rdFModel.getProperty(RDFS_MEMBER);
if (!containerResource.hasProperty(memberProp)) {
memberProp = rdFModel.getProperty(LDP_MEMBER);
}

var memberStmts = containerResource.listProperties(memberProp);
while (memberStmts.hasNext()) {
var stmt = memberStmts.next();
if (stmt.getObject().isURIResource()) {
try {
base.getMembers().add(new URI(stmt.getObject().asResource().getURI()));
} catch (URISyntaxException e) {
log.warn("Invalid member URI: {}", stmt.getObject());
}
}
}

log.debug("Extracted Base with {} members from DirectContainer", base.getMembers().size());
return base;
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

containerResource.getURI() can be null if the ldp:DirectContainer subject is a blank node; this will cause a NullPointerException when constructing new URI(...). Consider skipping non-URI resources (e.g., check isURIResource() / getURI()!=null) and continuing to the next match, or returning null with a clear error.

Suggested change
Resource containerResource = directContainers.next();
log.debug("Found ldp:DirectContainer at {}", containerResource.getURI());
Base base = new Base();
try {
base.setAbout(new URI(containerResource.getURI()));
} catch (URISyntaxException e) {
throw new LyoModelException(e);
}
// Extract cutoffEvent
if (containerResource.hasProperty(rdFModel.getProperty(TRS_CUTOFF_EVENT))) {
Resource cutoffResource = containerResource.getPropertyResourceValue(
rdFModel.getProperty(TRS_CUTOFF_EVENT)
);
if (cutoffResource != null && cutoffResource.isURIResource()) {
try {
base.setCutoffEvent(new URI(cutoffResource.getURI()));
} catch (URISyntaxException e) {
throw new LyoModelException(e);
}
}
}
// Extract members (try both rdfs:member and ldp:member)
org.apache.jena.rdf.model.Property memberProp = rdFModel.getProperty(RDFS_MEMBER);
if (!containerResource.hasProperty(memberProp)) {
memberProp = rdFModel.getProperty(LDP_MEMBER);
}
var memberStmts = containerResource.listProperties(memberProp);
while (memberStmts.hasNext()) {
var stmt = memberStmts.next();
if (stmt.getObject().isURIResource()) {
try {
base.getMembers().add(new URI(stmt.getObject().asResource().getURI()));
} catch (URISyntaxException e) {
log.warn("Invalid member URI: {}", stmt.getObject());
}
}
}
log.debug("Extracted Base with {} members from DirectContainer", base.getMembers().size());
return base;
while (directContainers.hasNext()) {
Resource containerResource = directContainers.next();
// Skip DirectContainer subjects that are not URI resources (e.g., blank nodes)
if (!containerResource.isURIResource() || containerResource.getURI() == null) {
log.debug("Skipping non-URI ldp:DirectContainer resource: {}", containerResource);
continue;
}
log.debug("Found ldp:DirectContainer at {}", containerResource.getURI());
Base base = new Base();
try {
base.setAbout(new URI(containerResource.getURI()));
} catch (URISyntaxException e) {
throw new LyoModelException(e);
}
// Extract cutoffEvent
if (containerResource.hasProperty(rdFModel.getProperty(TRS_CUTOFF_EVENT))) {
Resource cutoffResource = containerResource.getPropertyResourceValue(
rdFModel.getProperty(TRS_CUTOFF_EVENT)
);
if (cutoffResource != null && cutoffResource.isURIResource()) {
try {
base.setCutoffEvent(new URI(cutoffResource.getURI()));
} catch (URISyntaxException e) {
throw new LyoModelException(e);
}
}
}
// Extract members (try both rdfs:member and ldp:member)
org.apache.jena.rdf.model.Property memberProp = rdFModel.getProperty(RDFS_MEMBER);
if (!containerResource.hasProperty(memberProp)) {
memberProp = rdFModel.getProperty(LDP_MEMBER);
}
var memberStmts = containerResource.listProperties(memberProp);
while (memberStmts.hasNext()) {
var stmt = memberStmts.next();
if (stmt.getObject().isURIResource()) {
try {
base.getMembers().add(new URI(stmt.getObject().asResource().getURI()));
} catch (URISyntaxException e) {
log.warn("Invalid member URI: {}", stmt.getObject());
}
}
}
log.debug("Extracted Base with {} members from DirectContainer", base.getMembers().size());
return base;
}
log.warn("No ldp:DirectContainer with a URI subject found in RDF model");
return null;

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +191
if (!directContainers.hasNext()) {
return null;
}

Resource containerResource = directContainers.next();
log.debug("Found ldp:DirectContainer at {}", containerResource.getURI());

Base base = new Base();
try {
base.setAbout(new URI(containerResource.getURI()));
} catch (URISyntaxException e) {
throw new LyoModelException(e);
}

// Extract cutoffEvent
if (containerResource.hasProperty(rdFModel.getProperty(TRS_CUTOFF_EVENT))) {
Resource cutoffResource = containerResource.getPropertyResourceValue(
rdFModel.getProperty(TRS_CUTOFF_EVENT)
);
if (cutoffResource != null && cutoffResource.isURIResource()) {
try {
base.setCutoffEvent(new URI(cutoffResource.getURI()));
} catch (URISyntaxException e) {
throw new LyoModelException(e);
}
}
}

// Extract members (try both rdfs:member and ldp:member)
org.apache.jena.rdf.model.Property memberProp = rdFModel.getProperty(RDFS_MEMBER);
if (!containerResource.hasProperty(memberProp)) {
memberProp = rdFModel.getProperty(LDP_MEMBER);
}

var memberStmts = containerResource.listProperties(memberProp);
while (memberStmts.hasNext()) {
var stmt = memberStmts.next();
if (stmt.getObject().isURIResource()) {
try {
base.getMembers().add(new URI(stmt.getObject().asResource().getURI()));
} catch (URISyntaxException e) {
log.warn("Invalid member URI: {}", stmt.getObject());
}
}
}

log.debug("Extracted Base with {} members from DirectContainer", base.getMembers().size());
return base;
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

ResIterator/statement iterators from Jena should be closed to avoid resource leaks for some Model implementations. Consider using try/finally (or try-with-resources where applicable) to close directContainers and the memberStmts iterator.

Suggested change
if (!directContainers.hasNext()) {
return null;
}
Resource containerResource = directContainers.next();
log.debug("Found ldp:DirectContainer at {}", containerResource.getURI());
Base base = new Base();
try {
base.setAbout(new URI(containerResource.getURI()));
} catch (URISyntaxException e) {
throw new LyoModelException(e);
}
// Extract cutoffEvent
if (containerResource.hasProperty(rdFModel.getProperty(TRS_CUTOFF_EVENT))) {
Resource cutoffResource = containerResource.getPropertyResourceValue(
rdFModel.getProperty(TRS_CUTOFF_EVENT)
);
if (cutoffResource != null && cutoffResource.isURIResource()) {
try {
base.setCutoffEvent(new URI(cutoffResource.getURI()));
} catch (URISyntaxException e) {
throw new LyoModelException(e);
}
}
}
// Extract members (try both rdfs:member and ldp:member)
org.apache.jena.rdf.model.Property memberProp = rdFModel.getProperty(RDFS_MEMBER);
if (!containerResource.hasProperty(memberProp)) {
memberProp = rdFModel.getProperty(LDP_MEMBER);
}
var memberStmts = containerResource.listProperties(memberProp);
while (memberStmts.hasNext()) {
var stmt = memberStmts.next();
if (stmt.getObject().isURIResource()) {
try {
base.getMembers().add(new URI(stmt.getObject().asResource().getURI()));
} catch (URISyntaxException e) {
log.warn("Invalid member URI: {}", stmt.getObject());
}
}
}
log.debug("Extracted Base with {} members from DirectContainer", base.getMembers().size());
return base;
try {
if (!directContainers.hasNext()) {
return null;
}
Resource containerResource = directContainers.next();
log.debug("Found ldp:DirectContainer at {}", containerResource.getURI());
Base base = new Base();
try {
base.setAbout(new URI(containerResource.getURI()));
} catch (URISyntaxException e) {
throw new LyoModelException(e);
}
// Extract cutoffEvent
if (containerResource.hasProperty(rdFModel.getProperty(TRS_CUTOFF_EVENT))) {
Resource cutoffResource = containerResource.getPropertyResourceValue(
rdFModel.getProperty(TRS_CUTOFF_EVENT)
);
if (cutoffResource != null && cutoffResource.isURIResource()) {
try {
base.setCutoffEvent(new URI(cutoffResource.getURI()));
} catch (URISyntaxException e) {
throw new LyoModelException(e);
}
}
}
// Extract members (try both rdfs:member and ldp:member)
org.apache.jena.rdf.model.Property memberProp = rdFModel.getProperty(RDFS_MEMBER);
if (!containerResource.hasProperty(memberProp)) {
memberProp = rdFModel.getProperty(LDP_MEMBER);
}
var memberStmts = containerResource.listProperties(memberProp);
try {
while (memberStmts.hasNext()) {
var stmt = memberStmts.next();
if (stmt.getObject().isURIResource()) {
try {
base.getMembers().add(new URI(stmt.getObject().asResource().getURI()));
} catch (URISyntaxException e) {
log.warn("Invalid member URI: {}", stmt.getObject());
}
}
}
} finally {
memberStmts.close();
}
log.debug("Extracted Base with {} members from DirectContainer", base.getMembers().size());
return base;
} finally {
directContainers.close();
}

Copilot uses AI. Check for mistakes.
Comment on lines +199 to +202
private String getFixtureBody(String fixtureName) throws IOException {
Path path = Path.of("src/test/resources/fixtures", fixtureName);
return Files.readString(path).replace("http://localhost:8080", baseUri);
}
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

This test reads fixtures via Path.of("src/test/resources/..."), which depends on the working directory and can be flaky in IDEs/build tools. Prefer loading fixtures from the classpath (e.g., getResourceAsStream) to make the test environment-independent.

Copilot uses AI. Check for mistakes.
final Response.StatusType responseInfo = response.getStatusInfo();
final Response.Status.Family httpCodeType = responseInfo.getFamily();
log.info("HTTP response status: {} {}", responseInfo.getStatusCode(), responseInfo.getReasonPhrase());
log.info("HTTP response headers: {}", response.getHeaders());
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

Logging full HTTP response headers at INFO can leak sensitive data (e.g., Authorization/Cookie) and will be very noisy in production. Consider moving this to DEBUG/TRACE and/or redacting sensitive headers before logging.

Suggested change
log.info("HTTP response headers: {}", response.getHeaders());
log.debug("HTTP response headers: {}", response.getHeaders());

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant