# CollectionUpdater.java

    package org.codefilarete.stalactite.engine.runtime;

    import javax.annotation.Nullable;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.Collections;
    import java.util.List;
    import java.util.Set;
    import java.util.function.BiConsumer;
    import java.util.function.Function;

    import org.codefilarete.stalactite.engine.EntityPersister;
    import org.codefilarete.stalactite.engine.diff.AbstractDiff;
    import org.codefilarete.stalactite.engine.diff.CollectionDiffer;
    import org.codefilarete.stalactite.engine.diff.Diff;
    import org.codefilarete.stalactite.engine.diff.State;
    import org.codefilarete.stalactite.engine.listener.UpdateListener.UpdatePayload;
    import org.codefilarete.stalactite.mapping.IdAccessor;
    import org.codefilarete.tool.Duo;
    import org.codefilarete.tool.collection.Iterables;

    /**
     * Class aimed at making the difference of entities of an {@link UpdatePayload} and updating, inserting or deleting them according to difference
     * found between the two collections.
     * Gives entry points to add some more behavior for each of those actions.
     *
     * @param <I> source entity type
     * @param <O> collection item type
     * @param <C> collection type
     * @author Guillaume Mary
     */
    public class CollectionUpdater<I, O, C extends Collection<O>> implements BiConsumer<Duo<I, I>, Boolean> {

    	private final CollectionDiffer<O> differ;

    	private final Function<I, C> collectionGetter;
    	private final BiConsumer<O, I> reverseSetter;
    	protected final EntityWriter<O> elementPersister;
    	private final boolean shouldDeleteRemoved;

    	/**
    	 * Default and simple use case constructor.
    	 * See {@link #CollectionUpdater(Function, EntityPersister, BiConsumer, boolean, Function)} for particular case about id policy
    	 *
    	 * @param collectionGetter getter for collection from source entity
    	 * @param elementPersister target entities persister
    	 * @param reverseSetter setter for applying source entity to target objects, give null if no reverse mapping exists
    	 * @param shouldDeleteRemoved true to delete orphans
    	 */
    	public CollectionUpdater(Function<I, C> collectionGetter,
    							 ConfiguredPersister<O, ?> elementPersister,
    							 @Nullable BiConsumer<O, I> reverseSetter,
    							 boolean shouldDeleteRemoved) {
    		this(collectionGetter, elementPersister, reverseSetter, shouldDeleteRemoved, elementPersister.getMapping()::getId);
    	}

    	/**
    	 * Constructor that lets one defines id policy : in some cases default policy (based on
    	 * {@link IdAccessor#getId(Object)}) is not sufficient, such as when {@link Collection} contains "value type".
    	 *
    	 * @param collectionGetter getter for collection from source entity
    	 * @param elementPersister target entities persister
    	 * @param reverseSetter setter for applying source entity to target entities (used by {@link #onRemovedElements(UpdateContext, AbstractDiff)} to
    	 * 						nullify relation), null accepted if no reverse mapping exists
    	 * @param shouldDeleteRemoved true to delete orphans
    	 * @param idProvider expected to provide identifier of entities, used to store them per their id (in HashMap) to avoid usage of entity equals/hashcode
    	 */
    	public CollectionUpdater(Function<I, C> collectionGetter,
    							 EntityPersister<O, ?> elementPersister,
    							 @Nullable BiConsumer<O, I> reverseSetter,
    							 boolean shouldDeleteRemoved,
    							 Function<O, ?> idProvider) {
    		this.collectionGetter = collectionGetter;
    		this.reverseSetter = reverseSetter;
    		this.elementPersister = new EntityPersisterEntityWriterAdaptor<>(elementPersister);
    		this.shouldDeleteRemoved = shouldDeleteRemoved;
    		this.differ = new CollectionDiffer<>(idProvider);
    	}

    	public CollectionUpdater(Function<I, C> collectionGetter,
    							 EntityWriter<O> elementPersister,
    							 @Nullable BiConsumer<O, I> reverseSetter,
    							 boolean shouldDeleteRemoved,
    							 Function<O, ?> idProvider) {
    		this.collectionGetter = collectionGetter;
    		this.reverseSetter = reverseSetter;
    		this.elementPersister = elementPersister;
    		this.shouldDeleteRemoved = shouldDeleteRemoved;
    		this.differ = new CollectionDiffer<>(idProvider);
    	}

    	public CollectionDiffer<O> getDiffer() {
    		return differ;
    	}

    	@Override
    	public void accept(Duo<I, I> entry, Boolean allColumnsStatement) {
    		C modified = collectionGetter.apply(entry.getLeft());
    		C unmodified = collectionGetter.apply(entry.getRight());
    		UpdateContext updateContext = newUpdateContext(entry);
    		if (modified == null && unmodified == null) {
    			// nothing to do since reference hasn't changed : still null
    		} else if (modified != null && unmodified == null) {
    			modified.forEach(o -> onAddedElements(updateContext, new Diff<>(State.ADDED, null, o)));
    		} else if (modified == null && unmodified != null) {
    			unmodified.forEach(o -> onRemovedElements(updateContext, new Diff<>(State.REMOVED, o, null)));
    		} else {
    			Set<? extends AbstractDiff<O>> diffSet = diff(modified, unmodified);
    			for (AbstractDiff<O> diff : diffSet) {
    				switch (diff.getState()) {
    					case ADDED:
    						onAddedElements(updateContext, diff);
    						break;
    					case HELD:
    						onHeldElements(updateContext, diff);
    						break;
    					case REMOVED:
    						onRemovedElements(updateContext, diff);
    						break;
    				}
    			}
    		}
    		// is there any better order for these statements ?
    		updateTargets(updateContext, allColumnsStatement);
    		deleteTargets(updateContext);
    		insertTargets(updateContext);
    	}

    	/**
    	 * Updates collection entities
    	 * @param updateContext context created by {@link #newUpdateContext(Duo)}
    	 * @param allColumnsStatement indicates if all (mapped) columns of entities must be in statement, else only modified ones will be updated
    	 */
    	protected void updateTargets(UpdateContext updateContext, boolean allColumnsStatement) {
    		List<Duo<O, O>> updateInput = Iterables.collectToList(updateContext.getHeldElements(),
    				(AbstractDiff diff) -> new Duo<>((O) diff.getReplacingInstance(), (O) diff.getSourceInstance()));
    		// NB: update will only be done if necessary by target persister
    		elementPersister.update(updateInput, allColumnsStatement);
    	}

    	/**
    	 * Deletes entities removed from collection (only when orphan removal is asked)
    	 * @param updateContext context created by {@link #newUpdateContext(Duo)}
    	 */
    	protected void deleteTargets(UpdateContext updateContext) {
    		elementPersister.delete(updateContext.getRemovedElements());
    	}

    	/**
    	 * Insert entities added to collection
    	 * @param updateContext context created by {@link #newUpdateContext(Duo)}
    	 */
    	protected void insertTargets(UpdateContext updateContext) {
    		// added entities may be to be inserted or updated, not only inserted if they were already in another Collection
    		elementPersister.persist(updateContext.getAddedElements());
    	}

    	/**
    	 * Method in charge of making the differences between 2 collections of entities (many side type)
    	 */
    	protected Set<? extends AbstractDiff<O>> diff(Collection<O> modified, Collection<O> unmodified) {
    		// Casting to Set is a bit weird here but we should not be in another since List is taken into account upstream through mapping and redirected
    		// to dedicated case (ListCollectionUpdater)
    		return differ.diff(unmodified, modified);
    	}

    	/**
    	 * Methods asked to give a new {@link UpdateContext}. The returned instance will be passed to methods
    	 * {@link #onAddedElements(UpdateContext, AbstractDiff)}, {@link #onHeldElements(UpdateContext, AbstractDiff)} and {@link #onRemovedElements(UpdateContext, AbstractDiff)}.
    	 * Can be overridden to return a subtype and richer {@link UpdateContext}.
    	 *
    	 * @param updatePayload instance given to {@link #accept(Duo, Boolean)}
    	 * @return a new {@link UpdateContext} with given payload
    	 */
    	protected UpdateContext newUpdateContext(Duo<I, I> updatePayload) {
    		return new UpdateContext(updatePayload);
    	}

    	protected void onAddedElements(UpdateContext updateContext, AbstractDiff<O> diff) {
    		updateContext.getAddedElements().add(diff.getReplacingInstance());
    	}

    	protected void onHeldElements(UpdateContext updateContext, AbstractDiff<O> diff) {
    		updateContext.getHeldElements().add(diff);
    	}

    	protected void onRemovedElements(UpdateContext updateContext, AbstractDiff<O> diff) {
    		// we delete only persisted entity to prevent from a not found record
    		if (shouldDeleteRemoved) {
    			if (!elementPersister.isNew(diff.getSourceInstance())) {
    				updateContext.getRemovedElements().add(diff.getSourceInstance());
    			}
    		} else // entity shouldn't be deleted, so we may have to update it
    			if (reverseSetter != null) {
    				// we cut the link between target and source
    				// NB : we don't take versioning into account overall because we can't : how to do it since we miss the unmodified version ?
    				reverseSetter.accept(diff.getSourceInstance(), null);
    				elementPersister.updateById(Collections.singleton(diff.getSourceInstance()));
    			}
    	}

    	protected class UpdateContext {

    		private final Duo<I, I> payload;
    		/** List of many-side entities to be inserted (for massive SQL orders and better debug) */
    		private final List<O> addedElements = new ArrayList<>();
    		/** List of many-side entities to be update (for massive SQL orders and better debug) */
    		private final List<AbstractDiff<O>> heldElements = new ArrayList<>();
    		/** List of many-side entities to be deleted (for massive SQL orders and better debug) */
    		private final List<O> removedElements = new ArrayList<>();

    		public UpdateContext(Duo<I, I> updatePayload) {
    			this.payload = updatePayload;
    		}

    		public Duo<I, I> getPayload() {
    			return payload;
    		}

    		public List<O> getAddedElements() {
    			return addedElements;
    		}

    		public List<AbstractDiff<O>> getHeldElements() {
    			return heldElements;
    		}

    		public List<O> getRemovedElements() {
    			return removedElements;
    		}
    	}

    	/**
    	 * A dedicated interface for this class use case. Avoid to implements too many method coming from
    	 * {@link EntityPersister} in particular.
    	 *
    	 * @param <C> entity type to be managed
    	 * @author Guillaume Mary
    	 */
    	public interface EntityWriter<C> {

    		void update(Iterable<? extends Duo<C, C>> differencesIterable, boolean allColumnsStatement);

    		void delete(Iterable<? extends C> entities);

    		void insert(Iterable<? extends C> entities);

    		void persist(Iterable<? extends C> entities);

    		boolean isNew(C entity);

    		void updateById(Iterable<? extends C> entities);

    	}

    	/**
    	 * Internal adaptor that makes an {@link EntityPersister} become an {@link EntityWriter}.
    	 * Just some methods redirecting to delegate ones.
    	 *
    	 * @param <C> entity type to be managed
    	 * @author Guillaume Mary
    	 */
    	private static class EntityPersisterEntityWriterAdaptor<C> implements EntityWriter<C> {

    		private final EntityPersister<C, ?> delegate;

    		private EntityPersisterEntityWriterAdaptor(EntityPersister<C, ?> delegate) {
    			this.delegate = delegate;
    		}

    		@Override
    		public void update(Iterable<? extends Duo<C, C>> differencesIterable, boolean allColumnsStatement) {
    			delegate.update(differencesIterable, allColumnsStatement);
    		}

    		@Override
    		public void delete(Iterable<? extends C> entities) {
    			delegate.delete(entities);
    		}

    		@Override
    		public void insert(Iterable<? extends C> entities) {
    			delegate.insert(entities);
    		}

    		@Override
    		public void persist(Iterable<? extends C> entities) {
    			delegate.persist(entities);
    		}

    		@Override
    		public boolean isNew(C entity) {
    			return delegate.isNew(entity);
    		}

    		@Override
    		public void updateById(Iterable<? extends C> entities) {
    			delegate.updateById(entities);
    		}
    	}
    }