diff --git a/engine/src/main/java/org/hibernate/search/engine/logging/impl/Log.java b/engine/src/main/java/org/hibernate/search/engine/logging/impl/Log.java index 6ac51186727..513a0878a08 100644 --- a/engine/src/main/java/org/hibernate/search/engine/logging/impl/Log.java +++ b/engine/src/main/java/org/hibernate/search/engine/logging/impl/Log.java @@ -187,7 +187,10 @@ SearchException securityManagerLoadingError(@FormatWith(ClassFormatter.class) Cl @Message(id = ID_OFFSET + 44, value = "Invalid type '%1$s': missing constructor. The type must expose a public constructor with a single parameter of type Map.") SearchException noPublicMapArgConstructor(@FormatWith(ClassFormatter.class) Class classToLoad); - @Message(id = ID_OFFSET + 46, value = "Infinite @IndexedEmbedded recursion involving path '%1$s' on type '%2$s'.") + @Message(id = ID_OFFSET + 46, value = "Cyclic @IndexedEmbedded recursion starting from type '%2$s'." + + " Path starting from that type and ending with a cycle: '%1$s'." + + " A type cannot declare an unrestricted @IndexedEmbedded to itself, even indirectly." + + " To break the cycle, you should consider adding filters to your @IndexedEmbedded: includePaths, includeDepth, ...") SearchException indexedEmbeddedCyclicRecursion(String cyclicRecursionPath, @FormatWith(MappableTypeModelFormatter.class) MappableTypeModel parentTypeModel); diff --git a/engine/src/main/java/org/hibernate/search/engine/reporting/spi/RootFailureCollector.java b/engine/src/main/java/org/hibernate/search/engine/reporting/spi/RootFailureCollector.java index 4937ae57e81..3e8bcb8529d 100644 --- a/engine/src/main/java/org/hibernate/search/engine/reporting/spi/RootFailureCollector.java +++ b/engine/src/main/java/org/hibernate/search/engine/reporting/spi/RootFailureCollector.java @@ -219,11 +219,7 @@ void appendContextTo(StringJoiner joiner) { synchronized void appendFailuresTo(ToStringTreeBuilder builder) { builder.startObject( context.render() ); if ( !failureMessages.isEmpty() ) { - builder.startList( EngineEventContextMessages.INSTANCE.failureReportFailures() ); - for ( String failureMessage : failureMessages ) { - builder.value( failureMessage ); - } - builder.endList(); + builder.attribute( EngineEventContextMessages.INSTANCE.failureReportFailures(), failureMessages ); } appendChildrenFailuresTo( builder ); builder.endObject(); diff --git a/engine/src/test/java/org/hibernate/search/engine/mapper/mapping/building/impl/ConfiguredIndexSchemaManagerNestingContextTest.java b/engine/src/test/java/org/hibernate/search/engine/mapper/mapping/building/impl/ConfiguredIndexSchemaManagerNestingContextTest.java index 34b15d2dba4..ae6d0b2a81b 100644 --- a/engine/src/test/java/org/hibernate/search/engine/mapper/mapping/building/impl/ConfiguredIndexSchemaManagerNestingContextTest.java +++ b/engine/src/test/java/org/hibernate/search/engine/mapper/mapping/building/impl/ConfiguredIndexSchemaManagerNestingContextTest.java @@ -139,10 +139,8 @@ level1Definition, new IndexedEmbeddedPathTracker( level1Definition ), ); } ) .isInstanceOf( SearchException.class ) - .hasMessageContainingAll( - "Infinite @IndexedEmbedded recursion", - "path 'level1.prefix1_level1.prefix1_'", - "type '" + typeModel1Mock.toString() + "'" + .hasMessageContainingAll( "Cyclic @IndexedEmbedded recursion starting from type '" + typeModel1Mock.toString() + "'", + "Path starting from that type and ending with a cycle: 'level1.prefix1_level1.prefix1_'" ); verifyNoOtherInteractionsAndReset(); } @@ -172,10 +170,8 @@ level2Definition, new IndexedEmbeddedPathTracker( level2Definition ), ); } ) .isInstanceOf( SearchException.class ) - .hasMessageContainingAll( - "Infinite @IndexedEmbedded recursion", - "path 'level1.prefix1_level2.prefix2_level1.prefix1_'", - "type '" + typeModel1Mock.toString() + "'" + .hasMessageContainingAll( "Cyclic @IndexedEmbedded recursion starting from type '" + typeModel1Mock.toString() + "'", + "Path starting from that type and ending with a cycle: 'level1.prefix1_level2.prefix2_level1.prefix1_'" ); verifyNoOtherInteractionsAndReset(); } diff --git a/integrationtest/mapper/pojo-base/src/test/java/org/hibernate/search/integrationtest/mapper/pojo/mapping/definition/DependencyIT.java b/integrationtest/mapper/pojo-base/src/test/java/org/hibernate/search/integrationtest/mapper/pojo/mapping/definition/DependencyIT.java index 7ca3d98cfec..cc2189373e4 100644 --- a/integrationtest/mapper/pojo-base/src/test/java/org/hibernate/search/integrationtest/mapper/pojo/mapping/definition/DependencyIT.java +++ b/integrationtest/mapper/pojo-base/src/test/java/org/hibernate/search/integrationtest/mapper/pojo/mapping/definition/DependencyIT.java @@ -683,14 +683,91 @@ public String getDerivedC() { .satisfies( FailureReportUtils.hasFailureReport() .typeContext( DerivedFromCycle.A.class.getName() ) .pathContext( ".derivedA" ) - .failure( "Unable to resolve dependencies of a derived property:" - + " there is a cyclic dependency involving path '.derivedA'" - + " on type '" + DerivedFromCycle.A.class.getName() + "'", + .multilineFailure( "Unable to resolve dependencies of a derived property:" + + " there is a cyclic dependency starting from type '" + DerivedFromCycle.A.class.getName() + "'", + "Derivation chain starting from that type and ending with a cycle:\n" + + "- " + DerivedFromCycle.A.class.getName() + "#.b.derivedB\n" + + "- " + DerivedFromCycle.B.class.getName() + "#.c.derivedC\n" + + "- " + DerivedFromCycle.C.class.getName() + "#.a.derivedA\n", "A derived property cannot be marked as derived from itself", "you should consider disabling automatic reindexing" ) ); } + @Test + @TestForIssue(jiraKey = "HSEARCH-4565") + public void derivedFrom_error_cycle_buried() { + class DerivedFromCycle { + @Indexed + class Zero { + @DocumentId + Integer id; + A a; + @GenericField + @IndexingDependency(derivedFrom = @ObjectPath({ + @PropertyValue(propertyName = "a"), + @PropertyValue(propertyName = "derivedA") + })) + public String getDerivedZero() { + throw new UnsupportedOperationException( "Should not be called" ); + } + } + class A { + B b; + @GenericField + @IndexingDependency(derivedFrom = @ObjectPath({ + @PropertyValue(propertyName = "b"), + @PropertyValue(propertyName = "derivedB") + })) + public String getDerivedA() { + throw new UnsupportedOperationException( "Should not be called" ); + } + } + class B { + C c; + @GenericField + @IndexingDependency(derivedFrom = @ObjectPath({ + @PropertyValue(propertyName = "c"), + @PropertyValue(propertyName = "derivedC") + })) + public String getDerivedB() { + throw new UnsupportedOperationException( "Should not be called" ); + } + } + class C { + A a; + @GenericField + @IndexingDependency(derivedFrom = @ObjectPath({ + @PropertyValue(propertyName = "a"), + @PropertyValue(propertyName = "derivedA") + })) + public String getDerivedC() { + throw new UnsupportedOperationException( "Should not be called" ); + } + } + } + assertThatThrownBy( + () -> setupHelper.start() + .withAnnotatedEntityTypes( DerivedFromCycle.Zero.class ) + .withAnnotatedTypes( DerivedFromCycle.A.class, DerivedFromCycle.B.class, DerivedFromCycle.C.class ) + .setup() + ) + .isInstanceOf( SearchException.class ) + .satisfies( FailureReportUtils.hasFailureReport() + .typeContext( DerivedFromCycle.Zero.class.getName() ) + .pathContext( ".derivedZero" ) + .multilineFailure( "Unable to resolve dependencies of a derived property:" + + " there is a cyclic dependency starting from type '" + DerivedFromCycle.A.class.getName() + "'", + "Derivation chain starting from that type and ending with a cycle:\n" + + "- " + DerivedFromCycle.A.class.getName() + "#.b.derivedB\n" + + "- " + DerivedFromCycle.B.class.getName() + "#.c.derivedC\n" + + "- " + DerivedFromCycle.C.class.getName() + "#.a.derivedA\n", + "A derived property cannot be marked as derived from itself", + "you should consider disabling automatic reindexing" + ) + ); + } + @Test @TestForIssue(jiraKey = "HSEARCH-4423") public void derivedFrom_cycleFalsePositive() { diff --git a/integrationtest/mapper/pojo-base/src/test/java/org/hibernate/search/integrationtest/mapper/pojo/mapping/definition/IndexedEmbeddedBaseIT.java b/integrationtest/mapper/pojo-base/src/test/java/org/hibernate/search/integrationtest/mapper/pojo/mapping/definition/IndexedEmbeddedBaseIT.java index c36e38318b5..520984af69d 100644 --- a/integrationtest/mapper/pojo-base/src/test/java/org/hibernate/search/integrationtest/mapper/pojo/mapping/definition/IndexedEmbeddedBaseIT.java +++ b/integrationtest/mapper/pojo-base/src/test/java/org/hibernate/search/integrationtest/mapper/pojo/mapping/definition/IndexedEmbeddedBaseIT.java @@ -1548,6 +1548,73 @@ class IndexedEntity { + " (@GenericField, @FullTextField, custom bridges, ...) is defined for that type." ) ); } + @Test + public void cycle() { + class Model { + @Indexed(index = INDEX_NAME) + class EntityA { + @DocumentId + Integer id; + @IndexedEmbedded + EntityB b; + } + class EntityB { + Integer id; + @IndexedEmbedded + EntityA a; + } + } + + assertThatThrownBy( () -> setupHelper.start() + .withAnnotatedEntityTypes( Model.EntityA.class ) + .setup() ) + .isInstanceOf( SearchException.class ) + .satisfies( FailureReportUtils.hasFailureReport() + .typeContext( Model.EntityA.class.getName() ) + .pathContext( ".b.a.b" ) + .failure( "Cyclic @IndexedEmbedded recursion starting from type '" + Model.EntityA.class.getName() + "'", + "Path starting from that type and ending with a cycle: 'b.a.b.'", + "A type cannot declare an unrestricted @IndexedEmbedded to itself, even indirectly", + "To break the cycle, you should consider adding filters to your @IndexedEmbedded: includePaths, includeDepth, ..." ) + ); + } + + @Test + public void cycle_nonRoot() { + class Model { + @Indexed(index = INDEX_NAME) + class EntityA { + @DocumentId + Integer id; + @IndexedEmbedded + EntityB b; + } + class EntityB { + Integer id; + @IndexedEmbedded + EntityC c; + } + class EntityC { + Integer id; + @IndexedEmbedded + EntityB b; + } + } + + assertThatThrownBy( () -> setupHelper.start() + .withAnnotatedEntityTypes( Model.EntityA.class ) + .setup() ) + .isInstanceOf( SearchException.class ) + .satisfies( FailureReportUtils.hasFailureReport() + .typeContext( Model.EntityA.class.getName() ) + .pathContext( ".b.c.b.c" ) + .failure( "Cyclic @IndexedEmbedded recursion starting from type '" + Model.EntityB.class.getName() + "'", + "Path starting from that type and ending with a cycle: 'c.b.c.'", + "A type cannot declare an unrestricted @IndexedEmbedded to itself, even indirectly", + "To break the cycle, you should consider adding filters to your @IndexedEmbedded: includePaths, includeDepth, ..." ) + ); + } + private void doTestEmbeddedRuntime(SearchMapping mapping, Function newEntityFunction, Consumer expectedDocumentContributor) { diff --git a/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/AbstractPojoIndexingDependencyCollectorDirectValueNode.java b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/AbstractPojoIndexingDependencyCollectorDirectValueNode.java index dc7ebff0848..7b52ba80631 100644 --- a/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/AbstractPojoIndexingDependencyCollectorDirectValueNode.java +++ b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/AbstractPojoIndexingDependencyCollectorDirectValueNode.java @@ -25,6 +25,7 @@ import org.hibernate.search.mapper.pojo.model.spi.PojoRawTypeModel; import org.hibernate.search.mapper.pojo.model.spi.PojoTypeModel; import org.hibernate.search.util.common.AssertionFailure; +import org.hibernate.search.util.common.data.impl.LinkedNode; import org.hibernate.search.util.common.logging.impl.LoggerFactory; /** @@ -92,8 +93,7 @@ public PojoIndexingDependencyCollectorTypeNode castedType(PojoR public abstract void collectDependency(); - abstract void doCollectDependency( - PojoIndexingDependencyCollectorMonomorphicDirectValueNode initialNodeCollectingDependency); + abstract void doCollectDependency(LinkedNode derivedDependencyPath); @Override final PojoIndexingDependencyCollectorTypeNode lastEntityNode() { diff --git a/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/DerivedDependencyWalkingInfo.java b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/DerivedDependencyWalkingInfo.java new file mode 100644 index 00000000000..5c66f6b9624 --- /dev/null +++ b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/DerivedDependencyWalkingInfo.java @@ -0,0 +1,28 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.mapper.pojo.automaticindexing.building.impl; + +import org.hibernate.search.mapper.pojo.model.path.PojoModelPath; +import org.hibernate.search.mapper.pojo.model.spi.PojoRawTypeModel; + +public final class DerivedDependencyWalkingInfo { + public final PojoIndexingDependencyCollectorMonomorphicDirectValueNode node; + public final PojoRawTypeModel definingTypeModel; + public final PojoModelPath derivedFromPath; + + public DerivedDependencyWalkingInfo(PojoIndexingDependencyCollectorMonomorphicDirectValueNode node, + PojoModelPath derivedFromPath) { + this.node = node; + this.definingTypeModel = node.parentNode.parentNode().typeModel().rawType(); + this.derivedFromPath = derivedFromPath; + } + + @Override + public String toString() { + return definingTypeModel.name() + "#" + derivedFromPath.toPathString(); + } +} diff --git a/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/PojoIndexingDependencyCollectorMonomorphicDirectValueNode.java b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/PojoIndexingDependencyCollectorMonomorphicDirectValueNode.java index e7f2f68cef8..7cddb7deda6 100644 --- a/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/PojoIndexingDependencyCollectorMonomorphicDirectValueNode.java +++ b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/PojoIndexingDependencyCollectorMonomorphicDirectValueNode.java @@ -7,13 +7,14 @@ package org.hibernate.search.mapper.pojo.automaticindexing.building.impl; import java.lang.invoke.MethodHandles; +import java.util.Optional; import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate; import org.hibernate.search.mapper.pojo.logging.impl.Log; import org.hibernate.search.mapper.pojo.model.path.PojoModelPathValueNode; import org.hibernate.search.mapper.pojo.model.path.binding.impl.PojoModelPathBinder; import org.hibernate.search.mapper.pojo.model.path.impl.BoundPojoModelPathValueNode; -import org.hibernate.search.mapper.pojo.model.spi.PojoRawTypeModel; +import org.hibernate.search.util.common.data.impl.LinkedNode; import org.hibernate.search.util.common.logging.impl.LoggerFactory; /** @@ -34,33 +35,21 @@ public class PojoIndexingDependencyCollectorMonomorphicDirectValueNode static PojoIndexingDependencyCollectorMonomorphicDirectValueNode create( PojoIndexingDependencyCollectorPropertyNode parentNode, - BoundPojoModelPathValueNode modelPathFromLastTypeNode, BoundPojoModelPathValueNode modelPathFromLastEntityNode, PojoImplicitReindexingResolverBuildingHelper buildingHelper) { return new PojoIndexingDependencyCollectorMonomorphicDirectValueNode<>( parentNode, - modelPathFromLastTypeNode, modelPathFromLastEntityNode, - Metadata.create( buildingHelper, parentNode, modelPathFromLastTypeNode.getExtractorPath() ), + modelPathFromLastEntityNode, + Metadata.create( buildingHelper, parentNode, modelPathFromLastEntityNode.getExtractorPath() ), buildingHelper ); } - /** - * The path to this node from the last type node, i.e. from the node - * representing the type holding the property from which this value is extracted. - */ - private final BoundPojoModelPathValueNode modelPathFromLastTypeNode; - private final PojoModelPathValueNode unboundModelPathFromLastTypeNode; - PojoIndexingDependencyCollectorMonomorphicDirectValueNode( PojoIndexingDependencyCollectorPropertyNode parentNode, - BoundPojoModelPathValueNode modelPathFromLastTypeNode, BoundPojoModelPathValueNode modelPathFromLastEntityNode, Metadata metadata, PojoImplicitReindexingResolverBuildingHelper buildingHelper) { super( parentNode, modelPathFromLastEntityNode, metadata, buildingHelper ); - this.modelPathFromLastTypeNode = modelPathFromLastTypeNode; - // The path is used for comparisons (equals), so we need it unbound - this.unboundModelPathFromLastTypeNode = modelPathFromLastTypeNode.toUnboundPath(); } @Override @@ -91,41 +80,14 @@ void collectDependency(BoundPojoModelPathValueNode dirtyPathFromEntityT } @Override - void doCollectDependency( - PojoIndexingDependencyCollectorMonomorphicDirectValueNode initialNodeCollectingDependency) { - ReindexOnUpdate composedReindexOnUpdate = initialNodeCollectingDependency == null ? metadata.reindexOnUpdate - : initialNodeCollectingDependency.composeReindexOnUpdate( lastEntityNode(), metadata.reindexOnUpdate ); + void doCollectDependency(LinkedNode derivedDependencyPath) { + ReindexOnUpdate composedReindexOnUpdate = derivedDependencyPath == null ? metadata.reindexOnUpdate + : derivedDependencyPath.last.value.node.composeReindexOnUpdate( lastEntityNode(), metadata.reindexOnUpdate ); if ( ReindexOnUpdate.NO.equals( composedReindexOnUpdate ) ) { // Updates are ignored return; } - if ( initialNodeCollectingDependency != null ) { - PojoRawTypeModel initialType = initialNodeCollectingDependency.modelPathFromLastTypeNode - .getRootType().rawType(); - PojoModelPathValueNode initialValuePath = initialNodeCollectingDependency.unboundModelPathFromLastTypeNode; - PojoRawTypeModel latestType = modelPathFromLastTypeNode.getRootType().rawType(); - PojoModelPathValueNode latestValuePath = unboundModelPathFromLastTypeNode; - if ( initialType.equals( latestType ) && initialValuePath.equals( latestValuePath ) ) { - /* - * We found a cycle in the derived from dependencies. - * This can happen for example if: - * - property "foo" on type A is marked as derived from itself - * - property "foo" on type A is marked as derived from property "bar" on type B, - * which is marked as derived from property "foo" on type "A". - * Even if such a dependency might work in practice at runtime, - * for example because the link A => B never leads to a B that refers to the same A, - * even indirectly, - * we cannot support it here because we need to model dependencies as a static tree, - * which in such case would have an infinite depth. - */ - throw log.infiniteRecursionForDerivedFrom( latestType, latestValuePath ); - } - } - else { - initialNodeCollectingDependency = this; - } - if ( metadata.derivedFrom.isEmpty() ) { parentNode.parentNode().collectDependency( this.modelPathFromLastEntityNode ); } @@ -143,12 +105,45 @@ void doCollectDependency( */ PojoIndexingDependencyCollectorTypeNode lastTypeNode = parentNode.parentNode(); for ( PojoModelPathValueNode path : metadata.derivedFrom ) { + DerivedDependencyWalkingInfo newDerivedDependencyInfo = new DerivedDependencyWalkingInfo( this, path ); + if ( derivedDependencyPath != null ) { + checkForDerivedDependencyCycle( derivedDependencyPath, newDerivedDependencyInfo ); + } + LinkedNode updatedDerivedDependencyPath = + derivedDependencyPath == null ? LinkedNode.of( newDerivedDependencyInfo ) + : derivedDependencyPath.withHead( newDerivedDependencyInfo ); PojoModelPathBinder.bind( lastTypeNode, path, - PojoIndexingDependencyCollectorNode.walker( initialNodeCollectingDependency ) + PojoIndexingDependencyCollectorNode.walker( updatedDerivedDependencyPath ) ); } } } + private void checkForDerivedDependencyCycle(LinkedNode derivedDependencyPath, + DerivedDependencyWalkingInfo newDerivedDependencyInfo) { + Optional> cycle = derivedDependencyPath.findAndReverse( + other -> newDerivedDependencyInfo.definingTypeModel.equals( other.definingTypeModel ) + && newDerivedDependencyInfo.derivedFromPath.equals( other.derivedFromPath ) ); + if ( cycle.isPresent() ) { + /* + * We found a cycle in the derived dependency path. + * This can happen for example if: + * - property "foo" on type A is marked as derived from itself + * - property "foo" on type A is marked as derived from property "bar" on type B, + * which is marked as derived from property "foo" on type "A". + * - property "foo" on type A is marked as derived from property "bar" on type B, + * which is marked as derived from property "foobar" on type "C". + * which is marked as derived from property "bar" on type "B". + * Even if such a dependency might work in practice at runtime, + * for example because the link A => B never leads to a B that refers to the same A, + * even indirectly, + * we cannot support it here because we need to model dependencies as a static tree, + * which in such case would have an infinite depth. + */ + throw log.infiniteRecursionForDerivedFrom( newDerivedDependencyInfo.definingTypeModel, + cycle.get() ); + } + } + } diff --git a/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/PojoIndexingDependencyCollectorNode.java b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/PojoIndexingDependencyCollectorNode.java index 93dbfa3b2bf..18de4087c09 100644 --- a/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/PojoIndexingDependencyCollectorNode.java +++ b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/PojoIndexingDependencyCollectorNode.java @@ -9,6 +9,7 @@ import org.hibernate.search.mapper.pojo.automaticindexing.ReindexOnUpdate; import org.hibernate.search.mapper.pojo.extractor.mapping.programmatic.ContainerExtractorPath; import org.hibernate.search.mapper.pojo.model.path.binding.impl.PojoModelPathWalker; +import org.hibernate.search.util.common.data.impl.LinkedNode; public abstract class PojoIndexingDependencyCollectorNode { @@ -16,8 +17,8 @@ public static Walker walker() { return new Walker( null ); } - static Walker walker(PojoIndexingDependencyCollectorMonomorphicDirectValueNode initialNodeCollectingDependency) { - return new Walker( initialNodeCollectingDependency ); + static Walker walker(LinkedNode derivedDependencyPath) { + return new Walker( derivedDependencyPath ); } final PojoImplicitReindexingResolverBuildingHelper buildingHelper; @@ -63,10 +64,10 @@ static class Walker implements PojoModelPathWalker< PojoIndexingDependencyCollectorPropertyNode, AbstractPojoIndexingDependencyCollectorDirectValueNode > { - private final PojoIndexingDependencyCollectorMonomorphicDirectValueNode initialNodeCollectingDependency; + private final LinkedNode derivedDependencyPath; - Walker(PojoIndexingDependencyCollectorMonomorphicDirectValueNode initialNodeCollectingDependency) { - this.initialNodeCollectingDependency = initialNodeCollectingDependency; + Walker(LinkedNode derivedDependencyPath) { + this.derivedDependencyPath = derivedDependencyPath; } @Override @@ -80,7 +81,7 @@ static class Walker implements PojoModelPathWalker< PojoIndexingDependencyCollectorPropertyNode propertyNode, ContainerExtractorPath extractorPath) { AbstractPojoIndexingDependencyCollectorDirectValueNode node = propertyNode.value( extractorPath ); - node.doCollectDependency( initialNodeCollectingDependency ); + node.doCollectDependency( derivedDependencyPath ); return node; } diff --git a/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/PojoIndexingDependencyCollectorPolymorphicDirectValueNode.java b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/PojoIndexingDependencyCollectorPolymorphicDirectValueNode.java index cccb9301b4e..dbbedbc1642 100644 --- a/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/PojoIndexingDependencyCollectorPolymorphicDirectValueNode.java +++ b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/PojoIndexingDependencyCollectorPolymorphicDirectValueNode.java @@ -11,6 +11,7 @@ import org.hibernate.search.mapper.pojo.extractor.impl.BoundContainerExtractorPath; import org.hibernate.search.mapper.pojo.model.path.impl.BoundPojoModelPathValueNode; +import org.hibernate.search.util.common.data.impl.LinkedNode; /** * A node representing a value in a dependency collector, @@ -28,13 +29,12 @@ public class PojoIndexingDependencyCollectorPolymorphicDirectValueNode static AbstractPojoIndexingDependencyCollectorDirectValueNode create( PojoIndexingDependencyCollectorPropertyNode parentNode, - BoundPojoModelPathValueNode modelPathFromLastTypeNode, BoundPojoModelPathValueNode modelPathFromLastEntityNode, PojoImplicitReindexingResolverBuildingHelper buildingHelper) { List> holderSubTypeNodes = parentNode.parentNode().polymorphic(); String propertyName = parentNode.modelPathFromParentNode().getPropertyModel().name(); - BoundContainerExtractorPath boundExtractorPath = modelPathFromLastTypeNode.getBoundExtractorPath(); + BoundContainerExtractorPath boundExtractorPath = modelPathFromLastEntityNode.getBoundExtractorPath(); List> monomorphicValueNodes = new ArrayList<>(); Metadata parentTypeMetadata = Metadata.create( @@ -60,7 +60,7 @@ static AbstractPojoIndexingDependencyCollectorDirectValueNode creat else { // No need to use polymorphism; just return the value as it is on the (super) holder type. return new PojoIndexingDependencyCollectorMonomorphicDirectValueNode<>( parentNode, - modelPathFromLastTypeNode, modelPathFromLastEntityNode, parentTypeMetadata, buildingHelper + modelPathFromLastEntityNode, parentTypeMetadata, buildingHelper ); } } @@ -92,10 +92,9 @@ void collectDependency(BoundPojoModelPathValueNode dirtyPathFromEntityT } @Override - void doCollectDependency( - PojoIndexingDependencyCollectorMonomorphicDirectValueNode initialNodeCollectingDependency) { + void doCollectDependency(LinkedNode derivedDependencyPath) { for ( PojoIndexingDependencyCollectorMonomorphicDirectValueNode node : monomorphicValueNodes ) { - node.doCollectDependency( initialNodeCollectingDependency ); + node.doCollectDependency( derivedDependencyPath ); } } diff --git a/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/PojoIndexingDependencyCollectorPropertyNode.java b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/PojoIndexingDependencyCollectorPropertyNode.java index 011d7737bc9..d5cf2b933cf 100644 --- a/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/PojoIndexingDependencyCollectorPropertyNode.java +++ b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/building/impl/PojoIndexingDependencyCollectorPropertyNode.java @@ -41,7 +41,6 @@ public AbstractPojoIndexingDependencyCollectorDirectValueNode value( BoundContainerExtractorPath boundExtractorPath) { return PojoIndexingDependencyCollectorPolymorphicDirectValueNode.create( this, - modelPathFromParentNode.value( boundExtractorPath ), modelPathFromLastEntityNode.value( boundExtractorPath ), buildingHelper ); @@ -61,7 +60,6 @@ PojoIndexingDependencyCollectorMonomorphicDirectValueNode monomorphicV BoundContainerExtractorPath boundExtractorPath) { return PojoIndexingDependencyCollectorMonomorphicDirectValueNode.create( this, - modelPathFromParentNode.value( boundExtractorPath ), modelPathFromLastEntityNode.value( boundExtractorPath ), buildingHelper ); diff --git a/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/impl/PojoImplicitReindexingResolverMultiNode.java b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/impl/PojoImplicitReindexingResolverMultiNode.java index 6691a2b6c23..d3d49c87e1e 100644 --- a/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/impl/PojoImplicitReindexingResolverMultiNode.java +++ b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/automaticindexing/impl/PojoImplicitReindexingResolverMultiNode.java @@ -34,11 +34,7 @@ public void close() { @Override public void appendTo(ToStringTreeBuilder builder) { - builder.startList(); - for ( PojoImplicitReindexingResolverNode element : elements ) { - builder.value( element ); - } - builder.endList(); + builder.attribute( null, elements ); } @Override diff --git a/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/logging/impl/Log.java b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/logging/impl/Log.java index 800a53b212c..92f981ef946 100644 --- a/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/logging/impl/Log.java +++ b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/logging/impl/Log.java @@ -16,6 +16,7 @@ import java.util.Set; import org.hibernate.search.engine.backend.types.dsl.IndexFieldTypeOptionsStep; +import org.hibernate.search.mapper.pojo.automaticindexing.building.impl.DerivedDependencyWalkingInfo; import org.hibernate.search.mapper.pojo.common.annotation.impl.SearchProcessingWithContextException; import org.hibernate.search.mapper.pojo.extractor.ContainerExtractor; import org.hibernate.search.mapper.pojo.logging.spi.PojoModelPathFormatter; @@ -28,10 +29,11 @@ import org.hibernate.search.mapper.pojo.model.spi.PojoRawTypeModel; import org.hibernate.search.mapper.pojo.model.spi.PojoTypeModel; import org.hibernate.search.util.common.SearchException; +import org.hibernate.search.util.common.data.impl.LinkedNode; import org.hibernate.search.util.common.logging.impl.ClassFormatter; import org.hibernate.search.util.common.logging.impl.MessageConstants; import org.hibernate.search.util.common.logging.impl.SimpleNameClassFormatter; -import org.hibernate.search.util.common.logging.impl.ToStringTreeAppendableMultilineFormatter; +import org.hibernate.search.util.common.logging.impl.ToStringTreeMultilineFormatter; import org.hibernate.search.util.common.logging.impl.TypeFormatter; import org.hibernate.search.util.common.reporting.EventContext; @@ -223,7 +225,7 @@ SearchException invalidContainerExtractorForType( value = "Type manager for indexed type '%1$s': %2$s") void indexedTypeManager( @FormatWith(PojoTypeModelFormatter.class) PojoRawTypeModel typeModel, - @FormatWith(ToStringTreeAppendableMultilineFormatter.class) PojoIndexedTypeManager typeManager); + @FormatWith(ToStringTreeMultilineFormatter.class) PojoIndexedTypeManager typeManager); @LogMessage(level = Logger.Level.DEBUG) @Message(id = ID_OFFSET + 18, @@ -235,7 +237,7 @@ void indexedTypeManager( value = "Type manager for contained type '%1$s': %2$s") void containedTypeManager( @FormatWith(PojoTypeModelFormatter.class) PojoRawTypeModel typeModel, - @FormatWith(ToStringTreeAppendableMultilineFormatter.class) PojoContainedTypeManager typeManager); + @FormatWith(ToStringTreeMultilineFormatter.class) PojoContainedTypeManager typeManager); @Message(id = ID_OFFSET + 20, value = "Unable to find the inverse side of the association on type '%2$s' at path '%3$s'." @@ -289,7 +291,8 @@ SearchException incorrectTargetTypeForInverseAssociation( @Message(id = ID_OFFSET + 30, value = "Unable to resolve dependencies of a derived property:" - + " there is a cyclic dependency involving path '%2$s' on type '%1$s'." + + " there is a cyclic dependency starting from type '%1$s'.\n" + + "Derivation chain starting from that type and ending with a cycle:%2$s\n" + " A derived property cannot be marked as derived from itself, even indirectly through other " + " derived properties." + " If your model actually contains such cyclic dependency, " @@ -298,7 +301,7 @@ SearchException incorrectTargetTypeForInverseAssociation( ) SearchException infiniteRecursionForDerivedFrom( @FormatWith(PojoTypeModelFormatter.class) PojoRawTypeModel typeModel, - @FormatWith(PojoModelPathFormatter.class) PojoModelPathValueNode path); + @FormatWith(ToStringTreeMultilineFormatter.class) LinkedNode cycle); @Message(id = ID_OFFSET + 31, value = "Unable to apply property mapping:" diff --git a/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/model/path/impl/PojobTypeAndModelPath.java b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/model/path/impl/PojobTypeAndModelPath.java new file mode 100644 index 00000000000..aa8435b5b23 --- /dev/null +++ b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/model/path/impl/PojobTypeAndModelPath.java @@ -0,0 +1,25 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.mapper.pojo.model.path.impl; + +import org.hibernate.search.engine.mapper.model.spi.MappableTypeModel; +import org.hibernate.search.mapper.pojo.model.path.PojoModelPath; + +public final class PojobTypeAndModelPath { + public final MappableTypeModel type; + public final PojoModelPath path; + + public PojobTypeAndModelPath(MappableTypeModel type, PojoModelPath path) { + this.type = type; + this.path = path; + } + + @Override + public String toString() { + return type.name() + "#" + path; + } +} diff --git a/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/processing/impl/PojoIndexingProcessorMultiNode.java b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/processing/impl/PojoIndexingProcessorMultiNode.java index de9463a18d8..0005f06d77b 100644 --- a/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/processing/impl/PojoIndexingProcessorMultiNode.java +++ b/mapper/pojo-base/src/main/java/org/hibernate/search/mapper/pojo/processing/impl/PojoIndexingProcessorMultiNode.java @@ -35,11 +35,7 @@ public void close() { @Override public void appendTo(ToStringTreeBuilder builder) { - builder.startList(); - for ( PojoIndexingProcessor element : elements ) { - builder.value( element ); - } - builder.endList(); + builder.attribute( null, elements ); } @Override diff --git a/util/common/src/main/java/org/hibernate/search/util/common/data/impl/LinkedNode.java b/util/common/src/main/java/org/hibernate/search/util/common/data/impl/LinkedNode.java new file mode 100644 index 00000000000..289552048b4 --- /dev/null +++ b/util/common/src/main/java/org/hibernate/search/util/common/data/impl/LinkedNode.java @@ -0,0 +1,162 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.util.common.data.impl; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.hibernate.search.util.common.impl.Contracts; + +/** + * A very simple, immutable data structure to represent singly linked lists. + * + * @param The type of values stored in the list. + */ +public final class LinkedNode implements Iterable { + public static LinkedNode of(T value) { + return new LinkedNode<>( value, null ); + } + + @SafeVarargs + public static LinkedNode of(T... values) { + Contracts.assertNotNullNorEmpty( values, "values" ); + LinkedNode tail = null; + for ( int i = values.length - 1; i >= 0; i-- ) { + tail = new LinkedNode<>( values[i], tail ); + } + return tail; + } + + public final T value; + private final LinkedNode tail; + + // For quick access + public final LinkedNode last; + + private LinkedNode(T value, LinkedNode tail) { + this.value = value; + this.tail = tail; + this.last = tail == null ? this : tail.last; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append( '[' ); + boolean first = true; + for ( Iterator it = iterator(); it.hasNext(); ) { + if ( first ) { + first = false; + } + else { + sb.append( " => " ); + } + sb.append( it.next() ); + } + return sb.append( ']' ).toString(); + } + + @Override + public boolean equals(Object obj) { + if ( obj == this ) { + return true; + } + if ( !( obj instanceof LinkedNode ) ) { + return false; + } + LinkedNode other = (LinkedNode) obj; + return Objects.equals( value, other.value ) && Objects.equals( tail, other.tail ); + } + + @Override + public int hashCode() { + return Objects.hash( value, tail ); + } + + @Override + public Iterator iterator() { + return new Iterator() { + private LinkedNode next = LinkedNode.this; + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public T next() { + if ( next == null ) { + throw new NoSuchElementException(); + } + T value = next.value; + next = next.tail; + return value; + } + }; + } + + @Override + public Spliterator spliterator() { + return Spliterators.spliteratorUnknownSize( iterator(), Spliterator.IMMUTABLE | Spliterator.ORDERED ); + } + + public Stream stream() { + return StreamSupport.stream( spliterator(), false ); + } + + public LinkedNode withHead(T headValue) { + return new LinkedNode<>( headValue, this ); + } + + /** + * @param valuePredicate A predicate to apply to node values. + * @return An optional containing the path from the found node to the current head, + * i.e. a reversed list of all values + * from the first node to match the given predicate to the current head + * (note: the list is purposely in reversed order compared to {@code this}), + * or an empty optional if no matching value was found. + */ + public Optional> findAndReverse(Predicate valuePredicate) { + return findAndReverse( valuePredicate, this ); + } + + public Optional> findAndReverse(Predicate valuePredicate, LinkedNode head) { + if ( valuePredicate.test( value ) ) { + return Optional.of( head.reverse( null, this ) ); + } + else if ( tail != null ) { + return tail.findAndReverse( valuePredicate, head ); + } + else { + return Optional.empty(); + } + } + + /** + * @param newTail The tail of the new "reversed" node. + * @param lastIncludedNode The last node to include in the reversed list; + * must be in the tail of {@code this}. + * @return A list including all values from {@code lastNode} to {@code this}, + * in reversed order. + */ + private LinkedNode reverse(LinkedNode newTail, LinkedNode lastIncludedNode) { + LinkedNode thisWithNewTail = new LinkedNode<>( value, newTail ); + if ( lastIncludedNode == this ) { + return thisWithNewTail; + } + else { + return tail.reverse( thisWithNewTail, lastIncludedNode ); + } + } +} diff --git a/util/common/src/main/java/org/hibernate/search/util/common/impl/ToStringTreeBuilder.java b/util/common/src/main/java/org/hibernate/search/util/common/impl/ToStringTreeBuilder.java index 22523d555e7..7b6e669a38a 100644 --- a/util/common/src/main/java/org/hibernate/search/util/common/impl/ToStringTreeBuilder.java +++ b/util/common/src/main/java/org/hibernate/search/util/common/impl/ToStringTreeBuilder.java @@ -42,9 +42,28 @@ public ToStringTreeBuilder attribute(String name, Object value) { endStructure( StructureType.OBJECT, style.endObject ); endEntry(); } + else if ( value instanceof Iterable ) { + startList( name ); + for ( Object element : (Iterable) value ) { + value( element ); + } + endList(); + } else { startEntry( name, null ); - builder.append( value ); + if ( value == null ) { + builder.append( value ); + } + else { + String[] lines = value.toString().split( "\n" ); + for ( int i = 0; i < lines.length; i++ ) { + if ( i != 0 ) { + appendNewline(); + appendIndentIfNecessary(); + } + builder.append( lines[i] ); + } + } endEntry(); } return this; diff --git a/util/common/src/main/java/org/hibernate/search/util/common/logging/impl/ToStringTreeAppendableMultilineFormatter.java b/util/common/src/main/java/org/hibernate/search/util/common/logging/impl/ToStringTreeMultilineFormatter.java similarity index 61% rename from util/common/src/main/java/org/hibernate/search/util/common/logging/impl/ToStringTreeAppendableMultilineFormatter.java rename to util/common/src/main/java/org/hibernate/search/util/common/logging/impl/ToStringTreeMultilineFormatter.java index 8ce7e3eecfb..fab8adc7f57 100644 --- a/util/common/src/main/java/org/hibernate/search/util/common/logging/impl/ToStringTreeAppendableMultilineFormatter.java +++ b/util/common/src/main/java/org/hibernate/search/util/common/logging/impl/ToStringTreeMultilineFormatter.java @@ -7,25 +7,24 @@ package org.hibernate.search.util.common.logging.impl; import org.hibernate.search.util.common.impl.ToStringStyle; -import org.hibernate.search.util.common.impl.ToStringTreeAppendable; import org.hibernate.search.util.common.impl.ToStringTreeBuilder; /** - * Used with JBoss Logging's {@link org.jboss.logging.annotations.FormatWith} to display - * {@link ToStringTreeAppendable} objects in log messages. + * Used with JBoss Logging's {@link org.jboss.logging.annotations.FormatWith} to format + * objects using a {@link ToStringTreeBuilder}. */ -public final class ToStringTreeAppendableMultilineFormatter { +public final class ToStringTreeMultilineFormatter { - private final ToStringTreeAppendable appendable; + private final Object object; - public ToStringTreeAppendableMultilineFormatter(ToStringTreeAppendable appendable) { - this.appendable = appendable; + public ToStringTreeMultilineFormatter(Object object) { + this.object = object; } @Override public String toString() { return new ToStringTreeBuilder( ToStringStyle.multilineIndentStructure() ) - .value( appendable ) + .value( object ) .toString(); } } diff --git a/util/common/src/test/java/org/hibernate/search/util/common/data/impl/LinkedNodeTest.java b/util/common/src/test/java/org/hibernate/search/util/common/data/impl/LinkedNodeTest.java new file mode 100644 index 00000000000..ecf44db0cdb --- /dev/null +++ b/util/common/src/test/java/org/hibernate/search/util/common/data/impl/LinkedNodeTest.java @@ -0,0 +1,85 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.util.common.data.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.function.Predicate; + +import org.junit.Test; + +public class LinkedNodeTest { + + @Test + public void testToString() { + assertThat( LinkedNode.of( 42 ).toString() ) + .isEqualTo( "[42]" ); + assertThat( LinkedNode.of( 42, 1, 2, 3 ).toString() ) + .isEqualTo( "[42 => 1 => 2 => 3]" ); + } + + @Test + public void testEqualsAndHashCode() { + assertThat( LinkedNode.of( 42 ) ) + .isEqualTo( LinkedNode.of( 42 ) ); + assertThat( LinkedNode.of( 42 ).hashCode() ) + .isEqualTo( LinkedNode.of( 42 ).hashCode() ); + + assertThat( LinkedNode.of( 42, 1, 2, 3 ).hashCode() ) + .isEqualTo( LinkedNode.of( 42, 1, 2, 3 ).hashCode() ); + assertThat( LinkedNode.of( 42, 1, 2, 3 ).hashCode() ) + .isEqualTo( LinkedNode.of( 42, 1, 2, 3 ).hashCode() ); + + assertThat( LinkedNode.of( 42 ) ) + .isNotEqualTo( LinkedNode.of( 43 ) ); + assertThat( LinkedNode.of( 42 ) ) + .isNotEqualTo( LinkedNode.of( 42, 1 ) ); + assertThat( LinkedNode.of( 42, 1, 2, 3 ) ) + .isNotEqualTo( LinkedNode.of( 42, 1, 32, 3 ) ); + } + + @Test + public void findAndReverse() { + Predicate is42 = Predicate.isEqual( 42 ); + + // First matching is first element + assertThat( LinkedNode.of( 42, 1, 2, 3 ) + .findAndReverse( is42 ) ) + .hasValue( LinkedNode.of( 42 ) ); + + // First matching is somewhere in the middle + assertThat( LinkedNode.of( 1, 42, 2, 3 ) + .findAndReverse( is42 ) ) + .hasValue( LinkedNode.of( 42, 1 ) ); + + // First matching is last element + assertThat( LinkedNode.of( 1, 2, 3, 42 ) + .findAndReverse( is42 ) ) + .hasValue( LinkedNode.of( 42, 3, 2, 1 ) ); + + // No match + assertThat( LinkedNode.of( 1, 2, 3 ) + .findAndReverse( is42 ) ) + .isEmpty(); + + // Multiple matches + assertThat( LinkedNode.of( 1, 42, 42, 2, 3, 42 ) + .findAndReverse( is42 ) ) + .hasValue( LinkedNode.of( 42, 1 ) ); + + // Matching singleton + assertThat( LinkedNode.of( 42 ) + .findAndReverse( is42 ) ) + .hasValue( LinkedNode.of( 42 ) ); + + // Non-matching singleton + assertThat( LinkedNode.of( 1 ) + .findAndReverse( is42 ) ) + .isEmpty(); + } + +} \ No newline at end of file diff --git a/util/common/src/test/java/org/hibernate/search/util/common/impl/ToStringTreeBuilderTest.java b/util/common/src/test/java/org/hibernate/search/util/common/impl/ToStringTreeBuilderTest.java index cb148316568..cdb69fdbea1 100644 --- a/util/common/src/test/java/org/hibernate/search/util/common/impl/ToStringTreeBuilderTest.java +++ b/util/common/src/test/java/org/hibernate/search/util/common/impl/ToStringTreeBuilderTest.java @@ -8,6 +8,8 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.Arrays; + import org.junit.Test; public class ToStringTreeBuilderTest { @@ -17,11 +19,11 @@ public void inlineStyle() { assertThat( toString( ToStringStyle.inlineDelimiterStructure() ) ) .isEqualTo( "foo=value, children={" - + " childrenFoo=23, child1={ child1Foo=customToString, [ foo, 42 ] }, emptyChild={ }," + + " childrenFoo=23, child1={ child1Foo=customToString, [ foo, 42 ], [ foo2, 43 ] }, emptyChild={ }," + " appendable={ attr=val, nested={ attr=val2 } }," + " appendableAsObject={ attr=val, nested={ attr=val2 } }," + " nullAppendable=null," - + " list=[ { name=foo }, object={ name=foo, attr=bar }, { nestedList=[ first, second ], name=bar } ]" + + " list=[ { name=foo }, object={ name=foo, attr=bar }, { nestedList=[ first, second ], name=bar, nestedList2=[ first, second ] } ]" + " }, bar=value" ); assertThat( new ToStringTreeBuilder().toString() ).isEqualTo( "" ); @@ -42,6 +44,10 @@ public void multiLineStyle() { + "\t\t\tfoo\n" + "\t\t\t42\n" + "\t\t]\n" + + "\t\t[\n" + + "\t\t\tfoo2\n" + + "\t\t\t43\n" + + "\t\t]\n" + "\t}\n" + "\temptyChild={\n" + "\t}\n" @@ -72,6 +78,10 @@ public void multiLineStyle() { + "\t\t\t\tsecond\n" + "\t\t\t]\n" + "\t\t\tname=bar\n" + + "\t\t\tnestedList2=[\n" + + "\t\t\t\tfirst\n" + + "\t\t\t\tsecond\n" + + "\t\t\t]\n" + "\t\t}\n" + "\t]\n" + "}\n" @@ -94,6 +104,8 @@ public void multiLineLightStyle() { + " child1Foo: customToString\n" + " - foo\n" + " - 42\n" + + " - foo2\n" + + " - 43\n" + " emptyChild: \n" + " appendable: \n" + " attr: val\n" @@ -113,6 +125,9 @@ public void multiLineLightStyle() { + " - first\n" + " - second\n" + " name: bar\n" + + " nestedList2: \n" + + " - first\n" + + " - second\n" + "bar: value" ); assertThat( new ToStringTreeBuilder( style ).toString() ).isEqualTo( "" ); @@ -136,6 +151,7 @@ public String toString() { .value( "foo" ) .value( 42 ) .endList() + .attribute( null, Arrays.asList( "foo2", 43 ) ) .endObject() .startObject( "emptyChild" ) .endObject() @@ -156,6 +172,7 @@ public String toString() { .value( "second" ) .endList() .attribute( "name", "bar" ) + .attribute( "nestedList2", Arrays.asList( "first", "second" ) ) .endObject() .endList() .endObject() diff --git a/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/reporting/FailureReportChecker.java b/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/reporting/FailureReportChecker.java index 1b2acf5762c..92b6a9579e1 100644 --- a/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/reporting/FailureReportChecker.java +++ b/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/reporting/FailureReportChecker.java @@ -145,7 +145,15 @@ public FailureReportChecker multilineFailure(String... literalStringsContainedIn lastPatternWasFailure = true; elementsToMatch.add( new ElementToMatch( "\\n\\h+-\\h" ) ); for ( String contained : literalStringsContainedInFailureMessageInOrder ) { - elementsToMatch.add( new ElementToMatch( "[\\S\\s]*" + "\\Q" + contained + "\\E" ) ); + String[] lines = contained.split( "\n" ); + for ( int i = 0; i < lines.length; i++ ) { + if ( i == 0 ) { + elementsToMatch.add( new ElementToMatch( ".*(\\n\\h+.*)?" + "\\Q" + lines[i] + "\\E" ) ); + } + else { + elementsToMatch.add( new ElementToMatch( "\\n\\h+" + "\\Q" + lines[i] + "\\E" ) ); + } + } } // Match the rest of the line // We can't match multiple lines here, or we would run the risk of