# SecondPhaseRelationLoader.java

    package org.codefilarete.stalactite.engine.runtime;

    import java.util.ArrayDeque;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Map;
    import java.util.Queue;
    import java.util.Set;
    import java.util.function.Function;

    import org.codefilarete.tool.Nullable;
    import org.codefilarete.tool.Reflections;
    import org.codefilarete.tool.collection.Iterables;
    import org.codefilarete.stalactite.engine.SelectExecutor;
    import org.codefilarete.stalactite.engine.listener.SelectListener;
    import org.codefilarete.stalactite.sql.result.BeanRelationFixer;

    /**
     * @author Guillaume Mary
     */
    public class SecondPhaseRelationLoader<SRC, TRGT, ID> implements SelectListener<SRC, ID> {

    	private final BeanRelationFixer<SRC, TRGT> beanRelationFixer;
    	private final ThreadLocal<Queue<Set<RelationIds<Object, Object, Object>>>> relationIdsHolder;

    	public SecondPhaseRelationLoader(BeanRelationFixer<SRC, TRGT> beanRelationFixer, ThreadLocal<Queue<Set<RelationIds<Object, Object, Object>>>> relationIdsHolder) {
    		this.beanRelationFixer = beanRelationFixer;
    		this.relationIdsHolder = relationIdsHolder;
    	}

    	@Override
    	public void beforeSelect(Iterable<ID> ids) {
    		Queue<Set<RelationIds<Object, Object, Object>>> existingSet = relationIdsHolder.get();
    		if (existingSet == null) {
    			existingSet = new ArrayDeque<>();
    			relationIdsHolder.set(existingSet);
    		}
    		existingSet.add(new HashSet<>());
    	}

    	@Override
    	public void afterSelect(Set<? extends SRC> result) {
    		selectTargetEntities(result);
    		relationIdsHolder.remove();
    	}

    	/**
    	 * Mainly created to clarify types with TRGTID as parameter
    	 *
    	 * @param sourceEntities main entities, those that have the relation
    	 * @param <TRGTID> target identifier type
    	 */
    	private <TRGTID> void selectTargetEntities(Iterable<? extends SRC> sourceEntities) {
    		Map<SelectExecutor<TRGT, TRGTID>, Set<TRGTID>> selectsToExecute = new HashMap<>();
    		Map<SelectExecutor<TRGT, TRGTID>, Function<TRGT, TRGTID>> idAccessors = new HashMap<>();
    		Map<SRC, Set<TRGTID>> targetIdPerSource = new HashMap<>();
    		Set<RelationIds<SRC, TRGT, TRGTID>> relationIds = ((Queue<Set<RelationIds<SRC, TRGT, TRGTID>>>) (Queue) relationIdsHolder.get()).poll();
    		// we remove null targetIds (Target Ids may be null if relation is nullified) because
    		// - selecting entities with null id is nonsense
    		// - it prevents from generating SQL "in ()" which is invalid
    		// - it prevents from NullPointerException when applying target to source
    		relationIds.stream().filter(r -> !isDefaultValue(r.getTargetId())).forEach(r -> {
    			idAccessors.putIfAbsent(r.getSelectExecutor(), r.getIdAccessor());
    			targetIdPerSource.computeIfAbsent(r.getSource(), k -> new HashSet<>()).add(r.getTargetId());
    			selectsToExecute.computeIfAbsent(r.getSelectExecutor(), k -> new HashSet<>()).add(r.getTargetId());
    		});

    		// we load target entities from their ids, and map them per their loader
    		Map<SelectExecutor, Set<TRGT>> targetsPerSelector = new HashMap<>();
    		selectsToExecute.forEach((selectExecutor, ids) -> targetsPerSelector.put(selectExecutor, selectExecutor.select(ids)));
    		// then we apply them onto their source entities, to remember which target applies to which source, we use target id
    		Map<TRGTID, TRGT> targetPerId = new HashMap<>();
    		targetsPerSelector.forEach((selector, loadedTargets) -> targetPerId.putAll(Iterables.map(loadedTargets, idAccessors.get(selector))));
    		sourceEntities.forEach(src -> Nullable.nullable(targetIdPerSource.get(src))    // source may not have targetIds if the relation is null
    				.invoke(targetIds -> targetIds.forEach(targetId -> beanRelationFixer.apply(src, targetPerId.get(targetId)))));
    	}

    	public static boolean isDefaultValue(Object value) {
    		return value == null || Reflections.PRIMITIVE_DEFAULT_VALUES.get(value.getClass()) == value;
    	}

    	@Override
    	public void onSelectError(Iterable<ID> ids, RuntimeException exception) {
    		relationIdsHolder.remove();
    	}
    }