IndexedAssociationTableManyRelationDescriptor.java

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

import javax.annotation.Nullable;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.codefilarete.reflection.Accessor;
import org.codefilarete.stalactite.sql.result.BeanRelationFixer;

import static org.codefilarete.tool.bean.Objects.preventNull;

/**
 * Container to store information of an indexed *-to-many with association table relation (by a column on the association table)
 *
 * @author Guillaume Mary
 */
public class IndexedAssociationTableManyRelationDescriptor<SRC, TRGT, C extends Collection<TRGT>, SRCID> extends ManyRelationDescriptor<SRC, TRGT, C> {
	
	/**
	 * @param collectionGetter  collection accessor
	 * @param collectionSetter  collection setter
	 * @param collectionFactory collection factory
	 * @param reverseSetter
	 */
	public IndexedAssociationTableManyRelationDescriptor(Accessor<SRC, C> collectionGetter,
														 BiConsumer<SRC, C> collectionSetter,
														 Supplier<C> collectionFactory,
														 @Nullable BiConsumer<TRGT, SRC> reverseSetter,
														 Function<SRC, SRCID> idProvider) {
		super(collectionGetter, collectionSetter, collectionFactory, reverseSetter);
		super.relationFixer = new InMemoryRelationHolder(idProvider);
	}
	
	@Override
	public InMemoryRelationHolder getRelationFixer() {
		return (InMemoryRelationHolder) super.getRelationFixer();
	}
	
	/**
	 * A relation fixer that doesn't set the Collection onto the source bean but keeps it in memory.
	 * Made for collection with persisted order: it is sorted later with {@link #applySort(Set)} (to be called by some afterSelect(..) code),
	 * and set it onto source bean (by collection setter).
	 * Collection is kept in memory thanks to a ThreadLocal because its lifetime is expected to be a select execution,
	 * hence caller is expected to initialize and clean this instance by calling {@link #init()} and {@link #clear()}
	 * before and after selection.
	 *
	 * @see #applySort(Set)
	 * @author Guillaume Mary
	 */
	public class InMemoryRelationHolder implements BeanRelationFixer<SRC, TRGT> {
		
		private class CollectionOrderStorage {
			
			// Note that we use index as key instead of target entities to allow them to appear twice in the same collection (List),
			// Moreover, thanks to this form, it's easy to be applied to final Collection because values() automatically returns the entities as sorted  
			private final Map<Integer /* index */, TRGT> targetPerIndex = new HashMap<>();
		}
		
		/**
		 * Context for indexed collections (List, LinkedHashSet, ...). It will keep the entity index during select between "unrelated" methods/phases:
		 * the index column must be added to the SQL select, read from ResultSet and then, the order is applied to sort the final List, but this
		 * particular feature crosses over layers (entities and SQL) which is not implemented. In such circumstances, ThreadLocal comes to the rescue.
		 * It could be static, but it would lack the TRGTID typing, which leads to some generics errors, so we left it non-static (which is an
		 * acceptable small overhead)
		 */
		// Note that we prefer to store Map<SRCID> instead of IdentityMap for now because it's simpler but requires identifier providers.
		// Could be replaced by IdentityMap<SRC>
		private final ThreadLocal<Map<SRCID, CollectionOrderStorage>> currentSelectedIndexes = new ThreadLocal<>();
		
		private final Function<SRC, SRCID> idProvider;
		
		public InMemoryRelationHolder(Function<SRC, SRCID> idProvider) {
			this.idProvider = idProvider;
		}
		
		public void addIndex(SRCID leftEntityId, TRGT trgt, int index) {
			currentSelectedIndexes.get().get(leftEntityId).targetPerIndex.put(index, trgt);
		}
		
		@Override
		public void apply(SRC source, TRGT input) {
			// we store the relation in memory without setting it onto source entity because we need to sort it later
			currentSelectedIndexes.get().computeIfAbsent(idProvider.apply(source), srcid -> new CollectionOrderStorage());
			// bidirectional assignment
			preventNull(getReverseSetter(), NOOP_REVERSE_SETTER).accept(input, source);
		}
		
		public void init() {
			this.currentSelectedIndexes.set(new HashMap<>());
		}
		
		public void clear() {
			this.currentSelectedIndexes.remove();
		}
		
		/**
		 * Reconciliate {@link Collection} order.
		 * To be called after selecting entities from the database.
		 *
		 * @param result the entities to be sorted
		 */
		public void applySort(Set<? extends SRC> result) {
			result.forEach(src -> {
				CollectionOrderStorage inMemoryCollection = currentSelectedIndexes.get().get(idProvider.apply(src));
				if (inMemoryCollection != null) {  // inMemoryCollection can be null if there's no associated entity in the database
					Map<Integer, TRGT> targetIdPerId = inMemoryCollection.targetPerIndex;
					C relationCollection = getCollectionGetter().apply(src);
					if (relationCollection == null) {
						relationCollection = getCollectionFactory().get();
						getCollectionSetter().accept(src, relationCollection);
					}
					relationCollection.addAll(new LinkedList<>(targetIdPerId.values()));
				}
			});
		}
	}
}