ValidatorService.java
/*
* Copyright (c) 2007-2026 MetaSolutions AB
*
* 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.entrystore.rest.springboot.service;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.ValidatingValueFactory;
import org.eclipse.rdf4j.repository.Repository;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.eclipse.rdf4j.repository.RepositoryException;
import org.eclipse.rdf4j.repository.sail.SailRepository;
import org.eclipse.rdf4j.sail.memory.MemoryStore;
import org.entrystore.rest.springboot.model.exception.BadRequestException;
import org.entrystore.rest.springboot.model.exception.InternalServerErrorException;
import org.entrystore.rest.springboot.util.GraphUtil;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
@Slf4j
@Service
public class ValidatorService {
private static final ValueFactory VF = new ValidatingValueFactory();
private static final int MAX_IRI_LENGTH_IN_ERROR = 200;
private static final int MAX_ERRORS_IN_RESPONSE = 50;
public void validate(String rdfBody, String mediaType) {
Model graph = GraphUtil.deserializeGraph(rdfBody, mediaType);
assertAllIrisValid(graph);
// Catches Statements the parser produced but a Sail rejects; preserves legacy Restlet validator parity.
assertWritableToTripleStore(graph);
}
private static void assertAllIrisValid(Model graph) {
Set<String> errors = new LinkedHashSet<>();
Set<String> seen = new HashSet<>();
for (Statement s : graph) {
if (errors.size() >= MAX_ERRORS_IN_RESPONSE) {
break;
}
validateIri(s.getSubject(), seen, errors);
validateIri(s.getPredicate(), seen, errors);
validateIri(s.getObject(), seen, errors);
validateIri(s.getContext(), seen, errors);
}
if (!errors.isEmpty()) {
throw new BadRequestException(String.join("; ", errors));
}
}
private static void validateIri(Value value, Set<String> seen, Set<String> errors) {
if (errors.size() >= MAX_ERRORS_IN_RESPONSE) {
return;
}
String iriString = extractIriCandidate(value);
if (iriString == null || !seen.add(iriString)) {
return;
}
try {
VF.createIRI(iriString);
} catch (IllegalArgumentException e) {
errors.add("Invalid IRI: " + StringUtils.abbreviate(iriString, MAX_IRI_LENGTH_IN_ERROR));
}
}
private static String extractIriCandidate(Value value) {
return switch (value) {
case IRI iri -> iri.stringValue();
case Literal lit -> {
String label = lit.getLabel();
yield (label.startsWith("http://") || label.startsWith("https://")) ? label : null;
}
case null, default -> null;
};
}
private static void assertWritableToTripleStore(Model graph) {
Repository repo = new SailRepository(new MemoryStore());
try {
repo.init();
try (RepositoryConnection rc = repo.getConnection()) {
rc.add(graph);
}
} catch (RepositoryException e) {
throw new InternalServerErrorException("Validator repository could not be initialized or written to", e);
} finally {
try {
repo.shutDown();
} catch (RepositoryException e) {
log.warn("Failed to shut down validator MemoryStore", e);
}
}
}
}