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
@@ -0,0 +1,137 @@
/*
* Copyright Cool RDF project
*
* 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 cool.rdf.formatter;

import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

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.RDFNode;
import org.apache.jena.rdf.model.Resource;
import org.apache.jena.shared.PrefixMapping;

import cool.rdf.formatter.blanknode.BlankNodeMetadata;

final class BlankNodeComparator implements Comparator<Resource> {

private static final int MAX_DEPTH = 3;
private static final int CACHE_SIZE = 100;

private final Model model;
private final PrefixMapping prefixMapping;
private final BlankNodeMetadata blankNodeMetadata;
private final boolean preserveBlankNodeLabelsAndOrdering;
private final Map<Resource, String[]> keys = new LinkedHashMap<>( CACHE_SIZE, 0.75f, true ) {
@Override
protected boolean removeEldestEntry( final Map.Entry<Resource, String[]> eldest ) {
return size() > CACHE_SIZE;
}
};

BlankNodeComparator( final Model model, final PrefixMapping prefixMapping,
final BlankNodeMetadata blankNodeMetadata, final boolean preserveBlankNodeLabelsAndOrdering ) {
this.model = model;
this.prefixMapping = prefixMapping;
this.blankNodeMetadata = blankNodeMetadata;
this.preserveBlankNodeLabelsAndOrdering = preserveBlankNodeLabelsAndOrdering;
}

@Override
public int compare( final Resource left, final Resource right ) {
if ( left.equals( right ) ) {
return 0;
}
if ( preserveBlankNodeLabelsAndOrdering ) {
final Long leftOrder = blankNodeMetadata.getOrder( left.asNode() );
final Long rightOrder = blankNodeMetadata.getOrder( right.asNode() );
if ( leftOrder != null || rightOrder != null ) {
return Optional.ofNullable( leftOrder ).orElse( Long.MAX_VALUE )
.compareTo( Optional.ofNullable( rightOrder ).orElse( Long.MAX_VALUE ) );
}
}
for ( int depth = 1; depth <= MAX_DEPTH; depth++ ) {
final int result = key( left, depth ).compareTo( key( right, depth ) );
if ( result != 0 ) {
return result;
}
}
return 0;
}

private String key( final Resource resource, final int depth ) {
final String[] keysByDepth = keys.computeIfAbsent( resource, ignored -> new String[MAX_DEPTH] );
String key = keysByDepth[depth - 1];
if ( key == null ) {
key = blankNodeKey( resource, depth, new HashSet<>() );
keysByDepth[depth - 1] = key;
}
return key;
}

private String rdfNodeKey( final RDFNode node, final int depth, final Set<Resource> visitedBlankNodes ) {
if ( node.isURIResource() ) {
return "0U:" + prefixMapping.shortForm( node.asResource().getURI() );
}
if ( node.isLiteral() ) {
final Literal literal = node.asLiteral();
return "2L:" + literal.getDatatypeURI() + "|" + literal.getLanguage() + "|" + literal.getLexicalForm();
}
if ( node.isAnon() ) {
return blankNodeKey( node.asResource(), depth, visitedBlankNodes );
}
return "3N:";
}

private String blankNodeKey( final Resource resource, final int depth,
final Set<Resource> visitedBlankNodes ) {
if ( depth <= 0 ) {
return "1B";
}
if ( visitedBlankNodes.contains( resource ) ) {
return "1BCYCLE";
}

final Set<Resource> nextVisitedBlankNodes = new HashSet<>( visitedBlankNodes );
nextVisitedBlankNodes.add( resource );
final List<String> outgoing = model.listStatements( resource, null, (RDFNode) null )
.toList()
.stream()
.map( statement -> "S:" + predicateKey( statement.getPredicate() ) + "="
+ rdfNodeKey( statement.getObject(), depth - 1, nextVisitedBlankNodes ) )
.sorted()
.toList();
final List<String> incoming = model.listStatements( null, null, resource )
.toList()
.stream()
.map( statement -> "O:" + rdfNodeKey( statement.getSubject(), depth - 1,
nextVisitedBlankNodes ) + "=" + predicateKey( statement.getPredicate() ) )
.sorted()
.toList();
return "1B[" + String.join( ";", outgoing ) + "][" + String.join( ";", incoming ) + "]";
}

private String predicateKey( final Property predicate ) {
return prefixMapping.shortForm( predicate.getURI() );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public record FormattingStyle(
@RecordBuilder.Initializer( "DEFAULT_MAX_LINE_LENGTH" ) int maxLineLength,
@RecordBuilder.Initializer( "DEFAULT_TRIM_TRAILING_WHITESPACE" ) boolean trimTrailingWhitespace,
@RecordBuilder.Initializer( "DEFAULT_KEEP_UNUSED_PREFIXES" ) boolean keepUnusedPrefixes,
@RecordBuilder.Initializer( "DEFAULT_PRESERVE_BLANK_NODE_LABELS_AND_ORDERING" ) boolean preserveBlankNodeLabelsAndOrdering,
@RecordBuilder.Initializer( "DEFAULT_PREFIX_ORDER" ) List<String> prefixOrder,
@RecordBuilder.Initializer( "DEFAULT_SUBJECT_ORDER" ) List<Resource> subjectOrder,
@RecordBuilder.Initializer( "DEFAULT_PREDICATE_ORDER" ) List<Property> predicateOrder,
Expand Down Expand Up @@ -130,6 +131,7 @@ public record FormattingStyle(
public static final int DEFAULT_MAX_LINE_LENGTH = 100;
public static final boolean DEFAULT_TRIM_TRAILING_WHITESPACE = true;
public static final boolean DEFAULT_KEEP_UNUSED_PREFIXES = false;
public static final boolean DEFAULT_PRESERVE_BLANK_NODE_LABELS_AND_ORDERING = true;
public static final List<String> DEFAULT_PREFIX_ORDER = List.of(
"rdf",
"rdfs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
Expand Down Expand Up @@ -58,6 +59,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import cool.rdf.core.model.RdfModel;
import cool.rdf.core.model.RdfPrefix;
import cool.rdf.formatter.blanknode.BlankNodeMetadata;
import cool.rdf.formatter.blanknode.BlankNodeOrderAwareTurtleParser;
Expand Down Expand Up @@ -172,13 +174,21 @@ private void process( final String content, final ByteArrayOutputStream outputSt
if ( style.charset() == FormattingStyle.Charset.UTF_8_BOM ) {
writeByteOrderMark( outputStream );
}
final BlankNodeOrderAwareTurtleParser.ParseResult result = BlankNodeOrderAwareTurtleParser.parseModel( content );
final Model model = result.model();
final BlankNodeMetadata blankNodeMetadata = result.blankNodeMetadata();
if ( style.preserveBlankNodeLabelsAndOrdering() ) {
final BlankNodeOrderAwareTurtleParser.ParseResult result = BlankNodeOrderAwareTurtleParser.parseModel( content );
final Model model = result.model();
final BlankNodeMetadata blankNodeMetadata = result.blankNodeMetadata();
final PrefixMapping prefixMapping = buildPrefixMapping( model );
final RDFNodeComparatorFactory rdfNodeComparatorFactory = new RDFNodeComparatorFactory( prefixMapping,
blankNodeMetadata );
doFormat( model, prefixMapping, rdfNodeComparatorFactory, blankNodeMetadata, outputStream );
return;
}

final Model model = RdfModel.fromTurtle( content );
final PrefixMapping prefixMapping = buildPrefixMapping( model );
final RDFNodeComparatorFactory rdfNodeComparatorFactory = new RDFNodeComparatorFactory( prefixMapping,
blankNodeMetadata );
doFormat( model, prefixMapping, rdfNodeComparatorFactory, blankNodeMetadata, outputStream );
final RDFNodeComparatorFactory rdfNodeComparatorFactory = new RDFNodeComparatorFactory( prefixMapping );
doFormat( model, prefixMapping, rdfNodeComparatorFactory, BlankNodeMetadata.gotNothing(), outputStream );
}

private void writeByteOrderMark( final OutputStream outputStream ) {
Expand Down Expand Up @@ -223,10 +233,12 @@ private void doFormat( final Model model, final PrefixMapping prefixMapping, fin
.prefixMapping( prefixMapping )
.rdfNodeComparatorFactory( rdfNodeComparatorFactory )
.blankNodeMetadata( blankNodeMetadata )
.blankNodeComparator( new BlankNodeComparator( model, prefixMapping, blankNodeMetadata,
style.preserveBlankNodeLabelsAndOrdering() ) )
.build();
final State initialState = buildInitialState( context, outputStream );
final State prefixesWritten = writePrefixes( initialState );
final List<Statement> statements = determineStatements( model, rdfNodeComparatorFactory );
final List<Statement> statements = determineStatements( context );
final State namedResourcesWritten = writeNamedResources( prefixesWritten, statements );
final State allResourcesWritten = writeAnonymousResources( namedResourcesWritten );
final State finalState = style.insertFinalNewline() ? allResourcesWritten.newLine() : allResourcesWritten;
Expand All @@ -239,7 +251,7 @@ private State writeAnonymousResources( final State state ) {
final List<Resource> sortedAnonymousIdentifiedResources = state.identifiedAnonymousResources
.keySet()
.stream()
.sorted( state.context().rdfNodeComparatorFactory().comparator() )
.sorted( rdfNodeComparator( state ) )
.toList();
for ( final Resource resource : sortedAnonymousIdentifiedResources ) {
if ( !resource.listProperties().hasNext() ) {
Expand Down Expand Up @@ -268,18 +280,19 @@ private State writeNamedResources( final State state, final List<Statement> stat
return currentState;
}

private List<Statement> determineStatements( final Model model,
final RDFNodeComparatorFactory rdfNodeComparatorFactory ) {
private List<Statement> determineStatements( final Context context ) {
final Model model = context.model();
final Comparator<RDFNode> rdfNodeOrder = rdfNodeComparator( context );
final Stream<Statement> wellKnownSubjects = style.subjectOrder().stream().flatMap( subjectType -> statements( model, RDF.type,
subjectType )
.stream()
.sorted( Comparator.comparing( Statement::getSubject, rdfNodeComparatorFactory.comparator() ) ) );
.sorted( Comparator.comparing( Statement::getSubject, rdfNodeOrder ) ) );

final Stream<Statement> otherSubjects = statements( model ).stream()
.filter( statement -> !( statement.getPredicate().equals( RDF.type )
&& statement.getObject().isResource()
&& style.subjectOrder().contains( statement.getObject().asResource() ) ) )
.sorted( Comparator.comparing( Statement::getSubject, rdfNodeComparatorFactory.comparator() ) );
.sorted( Comparator.comparing( Statement::getSubject, rdfNodeOrder ) );

return Stream.concat( wellKnownSubjects, otherSubjects )
.filter( statement -> !( statement.getSubject().isAnon()
Expand All @@ -291,7 +304,9 @@ private State buildInitialState( final Context context, final OutputStream outpu
State currentState = new State( context, outputStream );
int i = 0;
final Set<String> blankNodeLabelsInInput = context.blankNodeMetadata().getAllBlankNodeLabels();
for ( final Resource r : anonymousResourcesThatNeedAnId( context.model(), currentState ) ) {
for ( final Resource r : anonymousResourcesThatNeedAnId( context.model(), currentState ).stream()
.sorted( rdfNodeComparator( currentState ) )
.toList() ) {
// use original label if present
String s = context.blankNodeMetadata().getLabel( r.asNode() );
if ( s == null ) {
Expand All @@ -312,9 +327,9 @@ private State buildInitialState( final Context context, final OutputStream outpu
*
* @param model the input model
* @param currentState the state
* @return the set of anonymous resources that are referred to more than once
* @return the ordered list of anonymous resources that are referred to more than once
*/
private Set<Resource> anonymousResourcesThatNeedAnId( final Model model, final State currentState ) {
private List<Resource> anonymousResourcesThatNeedAnId( final Model model, final State currentState ) {
final Set<Resource> identifiedResources = new HashSet<>( currentState.identifiedAnonymousResources.keySet() );
// needed for cycle detection
final Set<Resource> candidates = model.listObjects().toList().stream()
Expand All @@ -325,22 +340,23 @@ private Set<Resource> anonymousResourcesThatNeedAnId( final Model model, final S
final List<Resource> candidatesInOrder = Stream.concat(
currentState.context().blankNodeMetadata().getLabeledBlankNodes()
.stream()
.sorted( currentState.context().rdfNodeComparatorFactory().comparator() ),
.sorted( rdfNodeComparator( currentState ) ),
candidates
.stream()
.sorted( currentState.context().rdfNodeComparatorFactory().comparator() ) )
.sorted( rdfNodeComparator( currentState ) ) )
.toList();
final List<Resource> newlyIdentifiedResources = new ArrayList<>();
for ( final Resource candidate : candidatesInOrder ) {
if ( identifiedResources.contains( candidate ) ) {
continue;
}
if ( statements( model, null, candidate ).size() > 1 || hasBlankNodeCycle( model, candidate,
identifiedResources ) ) {
identifiedResources.add( candidate );
newlyIdentifiedResources.add( candidate );
}
}
identifiedResources.removeAll( currentState.identifiedAnonymousResources.keySet() );
return identifiedResources;
return newlyIdentifiedResources;
}

private boolean hasBlankNodeCycle( final Model model, final Resource start,
Expand Down Expand Up @@ -821,8 +837,9 @@ private State writeProperty( final Resource subject, final Property predicate, f

int index = 0;
State currentState = predicateWrittenOnce;
for ( final RDFNode object : objects.stream().sorted( objectOrder.thenComparing(
state.context().rdfNodeComparatorFactory().comparator() ) ).toList() ) {
for ( final RDFNode object : objects.stream()
.sorted( objectComparator( state ) )
.toList() ) {
final boolean lastObject = index == objects.size() - 1;
final State predicateWritten = useComma ? currentState : writeProperty( predicate, currentState );

Expand Down Expand Up @@ -866,6 +883,31 @@ private State writeProperty( final Resource subject, final Property predicate, f
return currentState;
}

private Comparator<RDFNode> objectComparator( final State state ) {
final Comparator<RDFNode> rdfNodeOrder = rdfNodeComparator( state );
final Comparator<RDFNode> nonBlankObjectOrder = objectOrder.thenComparing( rdfNodeOrder );
return ( left, right ) -> left.isAnon() && right.isAnon()
? rdfNodeOrder.compare( left, right )
: nonBlankObjectOrder.compare( left, right );
}

private Comparator<RDFNode> rdfNodeComparator( final State state ) {
return rdfNodeComparator( state.context() );
}

private Comparator<RDFNode> rdfNodeComparator( final Context context ) {
final Comparator<RDFNode> fallback = context.rdfNodeComparatorFactory().comparator();
return ( left, right ) -> {
if ( left.equals( right ) ) {
return 0;
}
if ( left.isAnon() && right.isAnon() ) {
return context.blankNodeComparator().compare( left.asResource(), right.asResource() );
}
return fallback.compare( left, right );
};
}

class NodeFormatterSink implements AWriter {
StringBuffer buffer = new StringBuffer();

Expand Down Expand Up @@ -929,7 +971,8 @@ public record Context(
Comparator<Property> predicateOrder,
PrefixMapping prefixMapping,
RDFNodeComparatorFactory rdfNodeComparatorFactory,
BlankNodeMetadata blankNodeMetadata
BlankNodeMetadata blankNodeMetadata,
BlankNodeComparator blankNodeComparator
) {}

@RecordBuilder
Expand Down
Loading