# OneToManyWithIndexedMappedAssociationEngine.java

    package org.codefilarete.stalactite.engine.runtime.onetomany;

    import javax.annotation.Nullable;
    import java.util.Collection;
    import java.util.Collections;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.IdentityHashMap;
    import java.util.LinkedHashSet;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    import java.util.function.BiConsumer;
    import java.util.function.Function;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;

    import org.codefilarete.stalactite.engine.diff.AbstractDiff;
    import org.codefilarete.stalactite.engine.diff.IndexedDiff;
    import org.codefilarete.stalactite.engine.listener.SelectListener;
    import org.codefilarete.stalactite.engine.runtime.CollectionUpdater;
    import org.codefilarete.stalactite.engine.runtime.ConfiguredRelationalPersister;
    import org.codefilarete.stalactite.engine.runtime.load.AbstractJoinNode;
    import org.codefilarete.stalactite.engine.runtime.load.EntityJoinTree;
    import org.codefilarete.stalactite.engine.runtime.onetomany.IndexedMappedManyRelationDescriptor.InMemoryRelationHolder;
    import org.codefilarete.stalactite.mapping.Mapping.ShadowColumnValueProvider;
    import org.codefilarete.stalactite.query.model.Fromable;
    import org.codefilarete.stalactite.sql.ddl.structure.Column;
    import org.codefilarete.stalactite.sql.ddl.structure.Key;
    import org.codefilarete.stalactite.sql.ddl.structure.Table;
    import org.codefilarete.tool.Duo;
    import org.codefilarete.tool.Reflections;
    import org.codefilarete.tool.ThreadLocals;
    import org.codefilarete.tool.collection.Arrays;
    import org.codefilarete.tool.collection.Iterables;
    import org.codefilarete.tool.trace.MutableInt;

    import static org.codefilarete.stalactite.engine.runtime.onetomany.AbstractOneToManyWithAssociationTableEngine.INDEXED_COLLECTION_FIRST_INDEX_VALUE;
    import static org.codefilarete.tool.Nullable.nullable;

    /**
     * @author Guillaume Mary
     */
    public class OneToManyWithIndexedMappedAssociationEngine<SRC, TRGT, SRCID, TRGTID, C extends Collection<TRGT>, RIGHTTABLE extends Table<RIGHTTABLE>>
    		extends OneToManyWithMappedAssociationEngine<SRC, TRGT, SRCID, TRGTID, C, RIGHTTABLE> {

    	/** Column that stores index value, owned by reverse side table (table of targetPersister) */
    	private final Column<RIGHTTABLE, Integer> indexColumn;

    	public OneToManyWithIndexedMappedAssociationEngine(ConfiguredRelationalPersister<TRGT, TRGTID> targetPersister,
    													   IndexedMappedManyRelationDescriptor<SRC, TRGT, C, SRCID, TRGTID> manyRelationDefinition,
    													   ConfiguredRelationalPersister<SRC, SRCID> sourcePersister,
    													   Set<Column<RIGHTTABLE, ?>> mappedReverseColumns,
    													   Column<RIGHTTABLE, Integer> indexColumn,
    													   Function<SRCID, Map<Column<RIGHTTABLE, ?>, ?>> reverseColumnsValueProvider) {
    		super(targetPersister, manyRelationDefinition, sourcePersister, mappedReverseColumns, reverseColumnsValueProvider);
    		this.indexColumn = indexColumn;
    	}

    	@Override
    	public IndexedMappedManyRelationDescriptor<SRC, TRGT, C, SRCID, TRGTID> getManyRelationDescriptor() {
    		return (IndexedMappedManyRelationDescriptor<SRC, TRGT, C, SRCID, TRGTID>) manyRelationDescriptor;
    	}

    	@Override
    	public <T1 extends Table<T1>, T2 extends Table<T2>> String addSelectCascade(Key<T1, SRCID> sourcePrimaryKey,
    																				boolean loadSeparately) {
    		// we add target subgraph joins to main persister
    		Set<Column<RIGHTTABLE, ?>> columnsToSelect = new HashSet<>(targetPersister.<RIGHTTABLE>getMainTable().getPrimaryKey().getColumns());
    		columnsToSelect.add(indexColumn);
    		String relationJoinNodeName = targetPersister.joinAsMany(EntityJoinTree.ROOT_JOIN_NAME, sourcePersister, manyRelationDescriptor.getCollectionProvider(), sourcePrimaryKey, (Key<RIGHTTABLE, SRCID>) manyRelationDescriptor.getReverseColumn(),
    				manyRelationDescriptor.getRelationFixer(),
    				(columnedRow) -> {
    					TRGTID identifier = targetPersister.getMapping().getIdMapping().getIdentifierAssembler().assemble(columnedRow);
    					Integer targetEntityIndex = columnedRow.get(indexColumn);
    					return identifier + "-" + targetEntityIndex;
    				},
    				columnsToSelect,
    				true,
    				loadSeparately);

    		addIndexSelection(relationJoinNodeName);

    		// we must trigger subgraph event on loading of our own graph, this is mainly for event that initializes things because given ids
    		// are not those of their entity
    		SelectListener<TRGT, TRGTID> targetSelectListener = targetPersister.getPersisterListener().getSelectListener();
    		sourcePersister.addSelectListener(new SelectListener<SRC, SRCID>() {
    			@Override
    			public void beforeSelect(Iterable<SRCID> ids) {
    				// since ids are not those of its entities, we should not pass them as argument, this will only initialize things if needed
    				targetSelectListener.beforeSelect(Collections.emptyList());
    			}

    			@Override
    			public void afterSelect(Set<? extends SRC> result) {
    				Set<TRGT> collect = Iterables.stream(result).flatMap(src -> nullable(manyRelationDescriptor.getCollectionGetter().apply(src))
    						.map(Collection::stream)
    						.getOr(Stream.empty()))
    						.collect(Collectors.toSet());
    				targetSelectListener.afterSelect(collect);
    			}

    			@Override
    			public void onSelectError(Iterable<SRCID> ids, RuntimeException exception) {
    				// since ids are not those of its entities, we should not pass them as argument
    				targetSelectListener.onSelectError(Collections.emptyList(), exception);
    			}
    		});

    		return relationJoinNodeName;
    	}

    	private void addIndexSelection(String joinNodeName) {
    		// Implementation note: 2 possibilities
    		// - keep object indexes and put sorted beans in a temporary List, then add them all to the target List
    		// - keep object indexes and sort the target List through a comparator of indexes
    		// The latter is used because target List is already filled by the relationFixer
    		// If we use the former we must change the relation fixer and keep a temporary List. Seems a bit more complex.
    		// May be changed if any performance issue is noticed
    		sourcePersister.addSelectListener(new SelectListener<SRC, SRCID>() {
    			@Override
    			public void beforeSelect(Iterable<SRCID> ids) {
    				InMemoryRelationHolder relationFixer = (InMemoryRelationHolder) manyRelationDescriptor.getRelationFixer();
    				relationFixer.init();
    			}

    			@Override
    			public void afterSelect(Set<? extends SRC> result) {
    				InMemoryRelationHolder relationFixer = (InMemoryRelationHolder) manyRelationDescriptor.getRelationFixer();
    				relationFixer.applySort(result);
    				cleanContext();
    			}

    			@Override
    			public void onSelectError(Iterable<SRCID> ids, RuntimeException exception) {
    				cleanContext();
    			}

    			private void cleanContext() {
    				InMemoryRelationHolder relationFixer = (InMemoryRelationHolder) manyRelationDescriptor.getRelationFixer();
    				relationFixer.clear();
    			}
    		});
    		AbstractJoinNode<TRGT, Fromable, Fromable, TRGTID> join = (AbstractJoinNode<TRGT, Fromable, Fromable, TRGTID>) sourcePersister.getEntityJoinTree().getJoin(joinNodeName);
    		join.setConsumptionListener((trgt, columnValueProvider) -> {
    			InMemoryRelationHolder relationFixer = (InMemoryRelationHolder) manyRelationDescriptor.getRelationFixer();
    			Map<TRGTID, Integer> indexPerTargetId = relationFixer.getCurrentSelectedIndexes();
    			// indexPerTargetId may not be present because its mechanism was added on persisterListener which is the one of the source bean
    			// so in case of entity loading from its own persister (targetPersister) ThreadLocal is not available
    			if (indexPerTargetId != null) {
    				// Indexing column is not defined in targetPersister.getMapping().getRowTransformer() but is present in row
    				// because it was read from ResultSet
    				int index = columnValueProvider.get(indexColumn);
    				TRGTID relationOwnerId = targetPersister.getMapping().getIdMapping().getIdentifierAssembler().assemble(columnValueProvider);
    				indexPerTargetId.put(relationOwnerId, index);
    			}
    		});
    	}

    	@Override
    	public void addInsertCascade(ConfiguredRelationalPersister<TRGT, TRGTID> targetPersister) {
    		// For a List and a given manner to get its owner (so we can deduce index value), we configure persistence to keep index value in database
    		addIndexInsertion();
    		super.addInsertCascade(targetPersister);
    	}

    	/**
    	 * Adds a "listener" that will amend insertion of the index column filled with its value
    	 */
    	private <TARGETTABLE extends Table<TARGETTABLE>> void addIndexInsertion() {
    		// we declare the indexing column as a silent one, then AfterInsertCollectionCascader will insert it
    		Function<SRC, C> collectionGetter = this.manyRelationDescriptor.getCollectionGetter();
    		ShadowColumnValueProvider<TRGT, TARGETTABLE> indexValueProvider = new ShadowColumnValueProvider<TRGT, TARGETTABLE>() {
    			@Override
    			public boolean accept(TRGT entity) {
    				// NB: source entity must be taken on relation storage context, not through manyRelationDefinition.getReverseGetter()
    				// because it doesn't cover all cases, moreover relation storage context is maintained for foreign key
    				// management which is make more sense that index strategy rely on it too
    				SRC sourceEntity = giveRelationStorageContext().get(entity);
    				return
    						// Source entity can be null if target was removed from the collection, then an SQL update is required to set its reference
    						// column to null as well as its indexing column
    						sourceEntity == null
    								// Case of polymorphic inheritance with an abstract one-to-many relation redefined on each subclass (think to
    								// AbstractQuestion and mapOneToMany(AbstractQuestion::getChoices, ..) declared for single and multiple choice question):
    								// we get several source persister which are quite the same at a slighlty difference on collection getter : due to JVM
    								// serialization of method reference, it keeps original generic type somewhere in the serialized form of method reference
    								// (SerializedLambda.instantiatedMethodType), then applying this concrete class (when looking for target entity index in
    								// collection) and not the abstract one, which produces a ClassCastException. As a consequence we must check that
    								// collection getter matches given entity (which is done through source persister, because there's no mean to do it
    								// with collection getter).
    								|| sourcePersister.getMapping().getClassToPersist().isInstance(sourceEntity);
    			}

    			@Override
    			public Set<Column<TARGETTABLE, ?>> getColumns() {
    				return Collections.singleton((Column) indexColumn);
    			}

    			@Override
    			public Map<Column<TARGETTABLE, ?>, ?> giveValue(TRGT target) {
    				SRC source = giveRelationStorageContext().get(target);
    				Integer targetEntityIndex;
    				if (source == null) {
    					// index can be null if target entity has been removed from source, no exception to be thrown here
    					// since it's a normal case
    					targetEntityIndex = null;
    				} else {
    					targetEntityIndex = computeTargetIndex(source, target);
    				}
    				Map<Column<TARGETTABLE, ?>, Object> result = new HashMap<>();
    				result.put((Column) indexColumn, targetEntityIndex);
    				return result;
    			}

    			/**
    			 * Finds the index of target instance in the one-to-many collection of source entity.
    			 * Supports {@link List} and {@link LinkedHashSet} collection type. Else an exception is thrown.
    			 *
    			 * @param source an entity that owns the one-to-many relation
    			 * @param target an entity expected to be in one-to-many relation
    			 * @return the index of target instance in one-to-many relation
    			 */
    			private int computeTargetIndex(SRC source, TRGT target) {
    				int result;
    				C apply = collectionGetter.apply(source);
    				if (apply instanceof List) {
    					result = ((List<?>) apply).indexOf(target) + INDEXED_COLLECTION_FIRST_INDEX_VALUE;
    				} else if (apply instanceof LinkedHashSet) {
    					MutableInt counter = new MutableInt(INDEXED_COLLECTION_FIRST_INDEX_VALUE - 1);
    					for (Object o : apply) {
    						counter.increment();
    						if (o == target) {
    							break;
    						}
    					}
    					result = counter.getValue();
    				} else {
    					throw new UnsupportedOperationException("Index computation is not supported for " + Reflections.toString(apply.getClass()));
    				}
    				return result;
    			}
    		};

    		targetPersister.<TARGETTABLE>getMapping().addShadowColumnInsert(indexValueProvider);
    		targetPersister.<TARGETTABLE>getMapping().addShadowColumnUpdate(indexValueProvider);
    	}

    	@Override
    	protected void addTargetInstancesUpdateCascader(boolean shouldDeleteRemoved) {
    		BiConsumer<Duo<SRC, SRC>, Boolean> collectionUpdater = new ListCollectionUpdater<>(
    				this.manyRelationDescriptor.getCollectionGetter(),
    				this.targetPersister,
    				this.manyRelationDescriptor.getReverseSetter(),
    				shouldDeleteRemoved,
    				this.targetPersister.getMapping()::getId,
    				indexColumn);
    		sourcePersister.getPersisterListener().addUpdateListener(new AfterUpdateTrigger<>(collectionUpdater));
    	}

    	private static class ListCollectionUpdater<SRC, TRGT, ID, C extends Collection<TRGT>, TARGETTABLE extends Table<TARGETTABLE>> extends CollectionUpdater<SRC, TRGT, C> {

    		/**
    		 * Context for indexed mapped List. Will keep bean index during insert between "unrelated" methods/phases :
    		 * indexes must be computed then applied into SQL order, but this particular feature crosses over layers (entities
    		 * and SQL) which is not implemented. In such circumstances, ThreadLocal comes to the rescue.
    		 * Could be static, but would lack the TRGT typing, which leads to some generics errors, so left non static (acceptable small overhead)
    		 */
    		@SuppressWarnings("java:S5164" /* remove() is called by ThreadLocals.AutoRemoveThreadLocal */)
    		private final ThreadLocal<Map<TRGT, Integer>> currentUpdatableListIndex = new ThreadLocal<>();

    		/**
    		 * Context for indexed mapped List. Will keep bean index during update between "unrelated" methods/phases :
    		 * indexes must be computed then applied into SQL order, but this particular feature crosses over layers (entities
    		 * and SQL) which is not implemented. In such circumstances, ThreadLocal comes to the rescue.
    		 * Could be static, but would lack the TRGT typing, which leads to some generics errors, so left non static (acceptable small overhead)
    		 */
    		@SuppressWarnings("java:S5164" /* remove() is called by ThreadLocals.AutoRemoveThreadLocal */)
    		private final ThreadLocal<Map<TRGT, Integer>> currentInsertableListIndex = new ThreadLocal<>();
    		private final Column<TARGETTABLE, Integer> indexingColumn;

    		private ListCollectionUpdater(Function<SRC, C> collectionGetter,
    									  ConfiguredRelationalPersister<TRGT, ID> targetPersister,
    									  @Nullable BiConsumer<TRGT, SRC> reverseSetter,
    									  boolean shouldDeleteRemoved,
    									  Function<TRGT, ?> idProvider,
    									  Column<TARGETTABLE, Integer> indexingColumn) {
    			super(collectionGetter, targetPersister, reverseSetter, shouldDeleteRemoved, idProvider);
    			this.indexingColumn = indexingColumn;
    			addShadowIndexInsert(targetPersister);
    			addShadowIndexUpdate(targetPersister);

    		}

    		private void addShadowIndexUpdate(ConfiguredRelationalPersister<TRGT, ID> targetPersister) {
    			targetPersister.<TARGETTABLE>getMapping().addShadowColumnInsert(new ShadowColumnValueProvider<TRGT, TARGETTABLE>() {

    				@Override
    				public boolean accept(TRGT entity) {
    					return currentUpdatableListIndex.get() != null && currentUpdatableListIndex.get().containsKey(entity);
    				}

    				@Override
    				public Set<Column<TARGETTABLE, ?>> getColumns() {
    					return Arrays.asHashSet(indexingColumn);
    				}

    				@Override
    				public Map<Column<TARGETTABLE, ?>, ?> giveValue(TRGT bean) {
    					Map<Column<TARGETTABLE, ?>, Object> result = new HashMap<>();
    					result.put(indexingColumn, currentUpdatableListIndex.get().get(bean));
    					return result;
    				}
    			});
    		}

    		private void addShadowIndexInsert(ConfiguredRelationalPersister<TRGT, ID> targetPersister) {
    			// adding index insert/update to strategy
    			targetPersister.<TARGETTABLE>getMapping().addShadowColumnInsert(new ShadowColumnValueProvider<TRGT, TARGETTABLE>() {

    				@Override
    				public boolean accept(TRGT entity) {
    					return currentInsertableListIndex.get() != null && currentInsertableListIndex.get().containsKey(entity);
    				}

    				@Override
    				public Set<Column<TARGETTABLE, ?>> getColumns() {
    					return Arrays.asHashSet(indexingColumn);
    				}

    				@Override
    				public Map<Column<TARGETTABLE, ?>, ?> giveValue(TRGT bean) {
    					Map<Column<TARGETTABLE, ?>, Object> result = new HashMap<>();
    					result.put(indexingColumn, currentInsertableListIndex.get().get(bean));
    					return result;
    				}
    			});
    		}

    		@Override
    		protected Set<? extends AbstractDiff<TRGT>> diff(Collection<TRGT> modified, Collection<TRGT> unmodified) {
    			return getDiffer().diffOrdered(unmodified, modified);
    		}

    		@Override
    		protected UpdateContext newUpdateContext(Duo<SRC, SRC> updatePayload) {
    			return new IndexedMappedAssociationUpdateContext(updatePayload);
    		}

    		@Override
    		protected void onAddedElements(UpdateContext updateContext, AbstractDiff<TRGT> diff) {
    			super.onAddedElements(updateContext, diff);
    			addNewIndexToContext((IndexedDiff<TRGT>) diff, (IndexedMappedAssociationUpdateContext) updateContext);
    		}

    		@Override
    		protected void onHeldElements(UpdateContext updateContext, AbstractDiff<TRGT> diff) {
    			super.onHeldElements(updateContext, diff);
    			addNewIndexToContext((IndexedDiff<TRGT>) diff, (IndexedMappedAssociationUpdateContext) updateContext);
    		}

    		private void addNewIndexToContext(IndexedDiff<TRGT> diff, IndexedMappedAssociationUpdateContext updateContext) {
    			Set<Integer> minus = Iterables.minus(diff.getReplacerIndexes(), diff.getSourceIndexes());
    			Integer index = Iterables.first(minus);
    			if (index != null) {
    				updateContext.getIndexUpdates().put(diff.getReplacingInstance(), index);
    			}
    		}

    		@Override
    		protected void insertTargets(UpdateContext updateContext) {
    			// we ask for entities insert as super does but we surround it by a ThreadLocal to fulfill List indexes which is required by
    			// the shadow column inserter (List indexes are given by default CollectionUpdater algorithm)
    			ThreadLocals.doWithThreadLocal(currentInsertableListIndex, ((IndexedMappedAssociationUpdateContext) updateContext)::getIndexUpdates,
    					(Runnable) () -> super.insertTargets(updateContext));
    		}

    		@Override
    		protected void updateTargets(UpdateContext updateContext, boolean allColumnsStatement) {
    			// we ask for entities update as super does but we surround it by a ThreadLocal to fulfill List indexes which is required by
    			// the shadow column updater (List indexes are given by default CollectionUpdater algorithm)
    			ThreadLocals.doWithThreadLocal(currentUpdatableListIndex, ((IndexedMappedAssociationUpdateContext) updateContext)::getIndexUpdates,
    					(Runnable) () -> super.updateTargets(updateContext, allColumnsStatement));
    		}

    		class IndexedMappedAssociationUpdateContext extends UpdateContext {

    			/** New indexes per entity */
    			private final Map<TRGT, Integer> indexUpdates = new IdentityHashMap<>();

    			public IndexedMappedAssociationUpdateContext(Duo<SRC, SRC> updatePayload) {
    				super(updatePayload);
    			}

    			public Map<TRGT, Integer> getIndexUpdates() {
    				return indexUpdates;
    			}
    		}
    	}

    }