Skip to content

Commit

Permalink
HSEARCH-3999 Share more code between PojoIndexedTypeIndexingPlan and …
Browse files Browse the repository at this point in the history
…PojoContainedTypeIndexingPlan

Automatic indexing is quite hard to test, because there are lots of
possible combinations. We already have lots of tests, but I think we
missed some code paths because we mostly test indexed types, and more
rarely contained types.

As I don't think it would be reasonable to duplicate existing tests to
also cover contained types, this is the alternative: by sharing more
code between contained types and indexed types, we ensure that if it's
tested for one, it's likely to work for the other, too.
  • Loading branch information
yrodiere committed Sep 24, 2020
1 parent cb512f1 commit 957d194
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 303 deletions.
Expand Up @@ -147,7 +147,7 @@ public boolean requiresSelfReindexing(Set<String> dirtyPaths) {

@Override
public void resolveEntitiesToReindex(PojoReindexingCollector collector, PojoWorkSessionContext<?> sessionContext,
I identifier, Supplier<E> entitySupplier, Set<String> dirtyPaths) {
Object identifier, Supplier<E> entitySupplier, Set<String> dirtyPaths) {
try {
reindexingResolver.resolveEntitiesToReindex( collector, sessionContext.runtimeIntrospector(),
entitySupplier.get(), dirtyPaths );
Expand Down
Expand Up @@ -6,24 +6,165 @@
*/
package org.hibernate.search.mapper.pojo.work.impl;

import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;

import org.hibernate.search.mapper.pojo.automaticindexing.impl.PojoReindexingCollector;
import org.hibernate.search.mapper.pojo.work.spi.PojoWorkSessionContext;

abstract class AbstractPojoTypeIndexingPlan {
/**
* @param <I> The type of identifiers of entities in this plan.
* @param <E> The type of entities in this plan.
* @param <S> The type of per-instance state.
*/
abstract class AbstractPojoTypeIndexingPlan<I, E, S extends AbstractPojoTypeIndexingPlan<I, E, S>.AbstractEntityState> {

final PojoWorkSessionContext<?> sessionContext;

// Use a LinkedHashMap for deterministic iteration
final Map<I, S> statesPerId = new LinkedHashMap<>();

AbstractPojoTypeIndexingPlan(PojoWorkSessionContext<?> sessionContext) {
this.sessionContext = sessionContext;
}

abstract void add(Object providedId, String providedRoutingKey, Object entity);
void add(Object providedId, String providedRoutingKey, Object entity) {
Supplier<E> entitySupplier = typeContext().toEntitySupplier( sessionContext, entity );
I identifier = toIdentifier( providedId, entitySupplier );
getState( identifier ).add( entitySupplier, providedRoutingKey );
}

abstract void update(Object providedId, String providedRoutingKey, Object entity);
void update(Object providedId, String providedRoutingKey, Object entity) {
Supplier<E> entitySupplier = typeContext().toEntitySupplier( sessionContext, entity );
I identifier = toIdentifier( providedId, entitySupplier );
getState( identifier ).update( entitySupplier, providedRoutingKey );
}

abstract void update(Object providedId, String providedRoutingKey, Object entity, String... dirtyPaths);
void update(Object providedId, String providedRoutingKey, Object entity, String... dirtyPaths) {
Supplier<E> entitySupplier = typeContext().toEntitySupplier( sessionContext, entity );
I identifier = toIdentifier( providedId, entitySupplier );
getState( identifier ).update( entitySupplier, providedRoutingKey, dirtyPaths );
}

abstract void delete(Object providedId, String providedRoutingKey, Object entity);
void delete(Object providedId, String providedRoutingKey, Object entity) {
Supplier<E> entitySupplier = typeContext().toEntitySupplier( sessionContext, entity );
I identifier = toIdentifier( providedId, entitySupplier );
getState( identifier ).delete( entitySupplier, providedRoutingKey );
}

abstract void purge(Object providedId, String providedRoutingKey);

void resolveDirty(PojoReindexingCollector containingEntityCollector) {
for ( S state : statesPerId.values() ) {
state.resolveDirty( containingEntityCollector );
}
}

abstract PojoWorkTypeContext<E> typeContext();

abstract I toIdentifier(Object providedId, Supplier<E> entitySupplier);

final S getState(I identifier) {
S state = statesPerId.get( identifier );
if ( state == null ) {
state = createState( identifier );
statesPerId.put( identifier, state );
}
return state;
}

protected abstract S createState(I identifier);

abstract class AbstractEntityState {
final I identifier;
Supplier<E> entitySupplier;

boolean delete;
boolean add;

boolean shouldResolveToReindex;
boolean considerAllDirty;
Set<String> dirtyPaths;

AbstractEntityState(I identifier) {
this.identifier = identifier;
}

void add(Supplier<E> entitySupplier, String providedRoutingKey) {
this.entitySupplier = entitySupplier;
shouldResolveToReindex = true;
add = true;
}

void update(Supplier<E> entitySupplier, String providedRoutingKey) {
doUpdate( entitySupplier, providedRoutingKey );
shouldResolveToReindex = true;
considerAllDirty = true;
dirtyPaths = null;
}

void update(Supplier<E> entitySupplier, String providedRoutingKey, String... dirtyPaths) {
doUpdate( entitySupplier, providedRoutingKey );
shouldResolveToReindex = true;
if ( !considerAllDirty ) {
for ( String dirtyPath : dirtyPaths ) {
addDirtyPath( dirtyPath );
}
}
}

void doUpdate(Supplier<E> entitySupplier, String providedRoutingKey) {
this.entitySupplier = entitySupplier;
if ( !add ) {
delete = true;
add = true;
}
// else: If add is true, either this is already an update (in which case update + update = update)
// or we called add() in the same plan (in which case add + update = add).
// In any case we don't need to change anything.
}

void delete(Supplier<E> entitySupplier, String providedRoutingKey) {
this.entitySupplier = entitySupplier;
if ( add && !delete ) {
// We called add() in the same plan, so the entity didn't exist.
// Don't delete, just cancel the addition.
add = false;
delete = false;
}
else {
// No add or update yet, or already deleted.
// Either way, delete.
add = false;
delete = true;
}

// Reindexing does not make sense for a deleted entity
shouldResolveToReindex = false;
considerAllDirty = false;
dirtyPaths = null;
}

void resolveDirty(PojoReindexingCollector containingEntityCollector) {
if ( shouldResolveToReindex ) {
shouldResolveToReindex = false; // Avoid infinite looping
typeContext().resolveEntitiesToReindex(
containingEntityCollector, sessionContext, identifier, entitySupplier,
considerAllDirty ? null : dirtyPaths
);
}
}

private void addDirtyPath(String dirtyPath) {
if ( dirtyPaths == null ) {
dirtyPaths = new HashSet<>();
}
dirtyPaths.add( dirtyPath );
}
}


}
Expand Up @@ -7,163 +7,52 @@
package org.hibernate.search.mapper.pojo.work.impl;

import java.lang.invoke.MethodHandles;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;

import org.hibernate.search.mapper.pojo.automaticindexing.impl.PojoReindexingCollector;
import org.hibernate.search.mapper.pojo.logging.impl.Log;
import org.hibernate.search.mapper.pojo.work.spi.PojoWorkSessionContext;
import org.hibernate.search.util.common.logging.impl.LoggerFactory;

/**
* @param <E> The contained entity type.
*/
public class PojoContainedTypeIndexingPlan<E> extends AbstractPojoTypeIndexingPlan {
public class PojoContainedTypeIndexingPlan<E>
extends AbstractPojoTypeIndexingPlan<Object, E, PojoContainedTypeIndexingPlan<E>.ContainedEntityState> {

private static final Log log = LoggerFactory.make( Log.class, MethodHandles.lookup() );

private final PojoWorkContainedTypeContext<E> typeContext;

// Use a LinkedHashMap for deterministic iteration
private final Map<Object, ContainedEntityIndexingPlan> indexingPlansPerId = new LinkedHashMap<>();

public PojoContainedTypeIndexingPlan(PojoWorkContainedTypeContext<E> typeContext,
PojoWorkSessionContext<?> sessionContext) {
super( sessionContext );
this.typeContext = typeContext;
}

@Override
void add(Object providedId, String providedRoutingKey, Object entity) {
Supplier<E> entitySupplier = typeContext.toEntitySupplier( sessionContext, entity );
getPlan( providedId ).add( entitySupplier );
}

@Override
void update(Object providedId, String providedRoutingKey, Object entity) {
Supplier<E> entitySupplier = typeContext.toEntitySupplier( sessionContext, entity );
getPlan( providedId ).update( entitySupplier );
void purge(Object providedId, String providedRoutingKey) {
throw log.cannotPurgeNonIndexedContainedType( typeContext.typeIdentifier(), providedId );
}

@Override
void update(Object providedId, String providedRoutingKey, Object entity, String... dirtyPaths) {
Supplier<E> entitySupplier = typeContext.toEntitySupplier( sessionContext, entity );
getPlan( providedId ).update( entitySupplier, dirtyPaths );
PojoWorkContainedTypeContext<E> typeContext() {
return typeContext;
}

@Override
void delete(Object providedId, String providedRoutingKey, Object entity) {
Supplier<E> entitySupplier = typeContext.toEntitySupplier( sessionContext, entity );
getPlan( providedId ).delete( entitySupplier );
Object toIdentifier(Object providedId, Supplier<E> entitySupplier) {
return providedId;
}

@Override
void purge(Object providedId, String providedRoutingKey) {
throw log.cannotPurgeNonIndexedContainedType( typeContext.typeIdentifier(), providedId );
protected ContainedEntityState createState(Object identifier) {
return new ContainedEntityState( identifier );
}

void resolveDirty(PojoReindexingCollector containingEntityCollector) {
for ( ContainedEntityIndexingPlan plan : indexingPlansPerId.values() ) {
plan.resolveDirty( containingEntityCollector );
}
}

private ContainedEntityIndexingPlan getPlan(Object identifier) {
ContainedEntityIndexingPlan plan = indexingPlansPerId.get( identifier );
if ( plan == null ) {
plan = new ContainedEntityIndexingPlan( identifier );
indexingPlansPerId.put( identifier, plan );
}
return plan;
}

private class ContainedEntityIndexingPlan {
private final Object identifier;
private Supplier<E> entitySupplier;

private Boolean createdInThisPlan;

private boolean shouldResolveToReindex;
private boolean considerAllDirty;
private Set<String> dirtyPaths;

private ContainedEntityIndexingPlan(Object identifier) {
this.identifier = identifier;
}

void add(Supplier<E> entitySupplier) {
this.entitySupplier = entitySupplier;
shouldResolveToReindex = true;
if ( createdInThisPlan == null ) {
// No update yet, so we actually did create the entity in this plan
createdInThisPlan = true;
}
}

void update(Supplier<E> entitySupplier) {
doUpdate( entitySupplier );
shouldResolveToReindex = true;
considerAllDirty = true;
dirtyPaths = null;
}

void update(Supplier<E> entitySupplier, String... dirtyPaths) {
doUpdate( entitySupplier );
shouldResolveToReindex = true;
if ( !considerAllDirty ) {
for ( String dirtyPath : dirtyPaths ) {
addDirtyPath( dirtyPath );
}
}
}

void delete(Supplier<E> entitySupplier) {
this.entitySupplier = entitySupplier;
if ( createdInThisPlan == null ) {
// No add or update yet, and we're performing a delete, so we did not create the entity in this plan
createdInThisPlan = false;
}
else if ( createdInThisPlan ) {
/*
* We called the first add() in the same plan, so we don't expect the entity to be contained
* in existing documents.
* Cancel everything.
*/
createdInThisPlan = null;
}

// Reindexing does not make sense for a deleted entity
shouldResolveToReindex = false;
considerAllDirty = false;
dirtyPaths = null;
}

void resolveDirty(PojoReindexingCollector containingEntityCollector) {
if ( shouldResolveToReindex ) {
shouldResolveToReindex = false; // Avoid infinite looping
typeContext.resolveEntitiesToReindex(
containingEntityCollector, sessionContext, identifier, entitySupplier,
considerAllDirty ? null : dirtyPaths
);
}
}

private void doUpdate(Supplier<E> entitySupplier) {
this.entitySupplier = entitySupplier;
if ( createdInThisPlan == null ) {
// No add yet, and we're performing an update, so we did not create the entity in this plan
createdInThisPlan = false;
}
}

private void addDirtyPath(String dirtyPath) {
if ( dirtyPaths == null ) {
dirtyPaths = new HashSet<>();
}
dirtyPaths.add( dirtyPath );
class ContainedEntityState
extends AbstractPojoTypeIndexingPlan<Object, E, ContainedEntityState>.AbstractEntityState {
private ContainedEntityState(Object identifier) {
super( identifier );
}
}

Expand Down

0 comments on commit 957d194

Please sign in to comment.