1414import java .util .ArrayList ;
1515import java .util .Collection ;
1616import java .util .HashSet ;
17+ import java .util .IdentityHashMap ;
1718import java .util .List ;
1819import java .util .Objects ;
1920import java .util .Set ;
2021import java .util .stream .Collectors ;
2122import java .util .stream .Stream ;
2223
2324import org .eclipse .rdf4j .common .annotation .InternalUseOnly ;
25+ import org .eclipse .rdf4j .common .exception .RDF4JException ;
2426import org .eclipse .rdf4j .model .IRI ;
2527import org .eclipse .rdf4j .model .Literal ;
2628import org .eclipse .rdf4j .model .Model ;
4345import org .eclipse .rdf4j .sail .shacl .SourceConstraintComponent ;
4446import org .eclipse .rdf4j .sail .shacl .ValidationSettings ;
4547import org .eclipse .rdf4j .sail .shacl .ast .StatementMatcher .Variable ;
48+ import org .eclipse .rdf4j .sail .shacl .ast .constraintcomponents .AbstractConstraintComponent ;
4649import org .eclipse .rdf4j .sail .shacl .ast .constraintcomponents .AndConstraintComponent ;
4750import org .eclipse .rdf4j .sail .shacl .ast .constraintcomponents .ClassConstraintComponent ;
4851import org .eclipse .rdf4j .sail .shacl .ast .constraintcomponents .ClosedConstraintComponent ;
@@ -201,7 +204,7 @@ public void toModel(Resource subject, IRI predicate, Model model, Set<Resource>
201204 modelBuilder .subject (getId ());
202205
203206 if (deactivated ) {
204- modelBuilder .add (SHACL .DEACTIVATED , deactivated );
207+ modelBuilder .add (SHACL .DEACTIVATED , true );
205208 }
206209
207210 for (Literal literal : message ) {
@@ -530,16 +533,32 @@ public List<Literal> getDefaultMessage() {
530533 }
531534
532535 @ Override
533- public boolean equals (Object o ) {
536+ public final boolean equals (Object o ) {
534537 if (this == o ) {
535538 return true ;
536539 }
537540 if (!(o instanceof Shape )) {
538541 return false ;
539542 }
540543
544+ return equals ((Shape ) o , new IdentityHashMap <>());
545+ }
546+
547+ @ Override
548+ public boolean equals (ConstraintComponent o , IdentityHashMap <Shape , Shape > comparisonGuard ) {
549+ Object alreadyComparing = comparisonGuard .get (this );
550+ if (alreadyComparing == o ) {
551+ return true ;
552+ }
553+
554+ if (!(o instanceof Shape )) {
555+ return false ;
556+ }
557+
541558 Shape shape = (Shape ) o ;
542559
560+ comparisonGuard .put (this , shape );
561+
543562 if (produceValidationReports != shape .produceValidationReports ) {
544563 return false ;
545564 }
@@ -555,47 +574,130 @@ public boolean equals(Object o) {
555574 if (severity != shape .severity ) {
556575 return false ;
557576 }
558- if (!Objects .equals (constraintComponents , shape .constraintComponents )) {
577+
578+ if (constraintComponents .size () != shape .constraintComponents .size ()) {
559579 return false ;
560580 }
581+
582+ boolean [] matchedRightSide = new boolean [shape .constraintComponents .size ()];
583+
584+ for (int i = 0 ; i < constraintComponents .size (); i ++) {
585+ ConstraintComponent left = constraintComponents .get (i );
586+
587+ if (left == null ) {
588+ continue ;
589+ }
590+
591+ int matchIndex = -1 ;
592+
593+ if (!matchedRightSide [i ]) {
594+ ConstraintComponent right = shape .constraintComponents .get (i );
595+ if (left .equals (right , comparisonGuard )) {
596+ matchIndex = i ;
597+ }
598+ }
599+
600+ if (matchIndex == -1 ) {
601+ for (int j = 0 ; j < shape .constraintComponents .size (); j ++) {
602+ if (matchedRightSide [j ]) {
603+ continue ;
604+ }
605+ ConstraintComponent candidate = shape .constraintComponents .get (j );
606+ if (left .equals (candidate , comparisonGuard )) {
607+ matchIndex = j ;
608+ break ;
609+ }
610+ }
611+ }
612+
613+ if (matchIndex == -1 ) {
614+ return false ;
615+ }
616+
617+ matchedRightSide [matchIndex ] = true ;
618+ }
619+
561620 return true ;
562621 }
563622
564623 @ Override
565- public int hashCode () {
624+ public final int hashCode () {
625+ return hashCode (new IdentityHashMap <>());
626+ }
627+
628+ @ Override
629+ public int hashCode (IdentityHashMap <Shape , Boolean > cycleDetection ) {
630+ if (cycleDetection .put (this , Boolean .TRUE ) != null ) {
631+ throw new ShaclShapeParsingException ("Recursive shape definition detected while computing hashCode" ,
632+ getId ());
633+ }
634+
566635 int result = (produceValidationReports ? 1 : 0 );
567636 result = 31 * result + (target != null ? target .hashCode () : 0 );
568637 result = 31 * result + (deactivated ? 1 : 0 );
569638 result = 31 * result + (message != null ? message .hashCode () : 0 );
570639 result = 31 * result + (severity != null ? severity .hashCode () : 0 );
571- result = 31 * result + (constraintComponents != null ? constraintComponents .hashCode () : 0 );
640+
641+ long temp = 0 ;
642+
643+ for (ConstraintComponent constraintComponent : constraintComponents ) {
644+ int componentHash ;
645+ if (constraintComponent instanceof Shape ) {
646+ componentHash = constraintComponent .hashCode (cycleDetection );
647+ } else if (constraintComponent instanceof AbstractConstraintComponent ) {
648+ componentHash = constraintComponent .hashCode (cycleDetection );
649+ } else {
650+ componentHash = constraintComponent != null ? constraintComponent .hashCode () : 0 ;
651+ }
652+ temp += componentHash ;
653+ }
654+
655+ result = 31 * result + Long .hashCode (temp );
656+
657+ cycleDetection .remove (this );
572658 return result ;
573659 }
574660
575661 public static class Factory {
576662
577663 public static List <ContextWithShape > getShapes (ShapeSource shapeSource , ParseSettings parseSettings ) {
578664
579- List <ContextWithShape > parsed = parse (shapeSource , parseSettings );
580-
581- return getShapes (parsed );
665+ try {
666+ List <ContextWithShape > parsed = parse (shapeSource , parseSettings );
667+ return getShapes (parsed );
668+ } catch (RDF4JException e ) {
669+ logger .error (e .getMessage (), e );
670+ throw e ;
671+ } catch (Throwable e ) {
672+ logger .error ("Unexpected error while parsing shapes" , e );
673+ throw new ShaclShapeParsingException ("Unexpected error while parsing shapes" , e );
674+ }
582675
583676 }
584677
585678 public static List <ContextWithShape > getShapes (List <ContextWithShape > parsed ) {
586- return parsed .stream ()
587- .flatMap (contextWithShapes -> {
588- List <Shape > split = split (contextWithShapes .getShape ());
589- calculateTargetChain (split );
590- calculateIfProducesValidationResult (split );
591- return split .stream ().map (s -> {
592- return new ContextWithShape (contextWithShapes .getDataGraph (),
593- contextWithShapes .getShapeGraph (), s );
594- });
595- })
596- .filter (ContextWithShape ::hasShape )
597- .distinct ()
598- .collect (Collectors .toList ());
679+ try {
680+ return parsed .stream ()
681+ .flatMap (contextWithShapes -> {
682+ List <Shape > split = split (contextWithShapes .getShape ());
683+ calculateTargetChain (split );
684+ calculateIfProducesValidationResult (split );
685+ return split .stream ().map (s -> {
686+ return new ContextWithShape (contextWithShapes .getDataGraph (),
687+ contextWithShapes .getShapeGraph (), s );
688+ });
689+ })
690+ .filter (ContextWithShape ::hasShape )
691+ .distinct ()
692+ .peek (a -> a .getShape ().verifyNotRecursive ())
693+ .collect (Collectors .toList ());
694+ } catch (RDF4JException e ) {
695+ logger .error (e .getMessage (), e );
696+ throw e ;
697+ } catch (Throwable e ) {
698+ logger .error ("Unexpected error while parsing shapes" , e );
699+ throw new ShaclShapeParsingException ("Unexpected error while parsing shapes" , e );
700+ }
599701 }
600702
601703 private static void calculateIfProducesValidationResult (List <Shape > split ) {
@@ -757,6 +859,11 @@ public static List<ContextWithShape> getShapesInContext(ShapeSource shapeSource,
757859
758860 }
759861
862+ private void verifyNotRecursive () {
863+ // calling hashCode will throw an exception if recursion is detected
864+ int i = hashCode (new IdentityHashMap <>());
865+ }
866+
760867 @ Override
761868 public String toString () {
762869 Model statements = toModel (new DynamicModel (new LinkedHashModelFactory ()));
0 commit comments