# EntityTreeInflater.java

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

    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Map;
    import java.util.Optional;
    import java.util.Queue;
    import java.util.Set;
    import java.util.function.Function;
    import java.util.function.Supplier;

    import org.apache.commons.collections4.map.LinkedMap;
    import org.codefilarete.stalactite.dsl.MappingConfigurationException;
    import org.codefilarete.stalactite.engine.runtime.load.EntityTreeInflater.NodeVisitor.EntityCreationResult;
    import org.codefilarete.stalactite.engine.runtime.load.JoinRowConsumer.ExcludingJoinRowConsumer;
    import org.codefilarete.stalactite.engine.runtime.load.JoinRowConsumer.ForkJoinRowConsumer;
    import org.codefilarete.stalactite.engine.runtime.load.JoinRowConsumer.RootJoinRowConsumer;
    import org.codefilarete.stalactite.engine.runtime.load.MergeJoinNode.MergeJoinRowConsumer;
    import org.codefilarete.stalactite.engine.runtime.load.PassiveJoinNode.PassiveJoinRowConsumer;
    import org.codefilarete.stalactite.engine.runtime.load.RelationJoinNode.BasicEntityCache;
    import org.codefilarete.stalactite.engine.runtime.load.RelationJoinNode.EntityCache;
    import org.codefilarete.stalactite.engine.runtime.load.RelationJoinNode.RelationJoinRowConsumer;
    import org.codefilarete.stalactite.query.model.Fromable;
    import org.codefilarete.stalactite.query.model.Selectable;
    import org.codefilarete.stalactite.sql.result.ColumnedRow;
    import org.codefilarete.stalactite.sql.result.Row;
    import org.codefilarete.tool.Nullable;
    import org.codefilarete.tool.Reflections;
    import org.codefilarete.tool.ThreadLocals;
    import org.codefilarete.tool.VisibleForTesting;
    import org.codefilarete.tool.collection.Collections;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    import static org.codefilarete.tool.Nullable.nullable;

    /**
     * Bean graph creator from database rows. Based on a tree of {@link ConsumerNode}s which wraps some {@link JoinRowConsumer}.
     *
     * @param <C> main entity type
     * @author Guillaume Mary
     */
    public class EntityTreeInflater<C> {

    	public static final Logger LOGGER = LoggerFactory.getLogger(EntityTreeInflater.class);

    	@SuppressWarnings("java:S5164" /* remove() is called by ThreadLocals.AutoRemoveThreadLocal */)
    	private static final ThreadLocal<EntityTreeInflater.TreeInflationContext> CURRENT_CONTEXT = new ThreadLocal<>();

    	/**
    	 * Gives current {@link TreeInflationContext}. A current one is available during {@link #transform(Iterable, int)} invocation.
    	 *
    	 * @return current {@link TreeInflationContext}, null if you're not invoking it during its lifecycle
    	 */
    	public static EntityTreeInflater.TreeInflationContext currentContext() {
    		return CURRENT_CONTEXT.get();
    	}

    	/**
    	 * All inflaters, mergers and so on, in a tree structure that reflects {@link EntityJoinTree}.
    	 * Made as such to benefit from the possibility to cancel in-depth iteration in {@link #transform(ColumnedRow, TreeInflationContext)} method during relation
    	 * building
    	 */
    	private final ConsumerNode consumerRoot;

    	/**
    	 * Constructor with necessary elements
    	 * @param consumerRoot top level row consumer, the one that will compute root instances
    	 */
    	EntityTreeInflater(ConsumerNode consumerRoot) {
    		this.consumerRoot = consumerRoot;
    	}

    	/**
    	 *
    	 * @param rows rows (coming from database select) to be read to build beans graph
    	 * @param resultSize expected result size, only for resulting list optimization
    	 * @return a set of root beans, built from given rows by asking internal strategy joins to instantiate and complete them
    	 */
    	public Set<C> transform(Iterable<? extends ColumnedRow> rows, int resultSize) {
    		return ThreadLocals.doWithThreadLocal(CURRENT_CONTEXT, TreeInflationContext::new, (Function<EntityTreeInflater.TreeInflationContext, Set<C>>) context ->
    						transform(rows, resultSize, context));
    	}

    	private Set<C> transform(Iterable<? extends ColumnedRow> rows, int resultSize, EntityTreeInflater.TreeInflationContext context) {
    		// we use an "IdentitySet" (doesn't exist directly, but can be done through IdentityLinkedMap) to avoid duplicate entity : with a HashSet
    		// duplicate can happen if equals/hashCode depends on relation, in particular Collection ones, because they are filled from row to row
    		// making hashCode value change. Moreover we need to keep track of the order because query might be sorted through "order by" clause.
    		Set<C> result = java.util.Collections.newSetFromMap(new IdentityLinkedMap<>(resultSize));
    		for (ColumnedRow row : rows) {
    			Nullable<C> newInstance = transform(row, context);
    			newInstance.invoke(result::add);
    		}
    		return result;
    	}

    	Nullable<C> transform(ColumnedRow row, EntityTreeInflater.TreeInflationContext context) {
    		context.setCurrentRow(row);
    		// Algorithm : we iterate depth by depth the tree structure of the joins
    		// We start by hierarchy root.
    		// We process entity of current depth, process the direct relations, then add those relations to depth iterator
    		LOGGER.debug("Creating instance with {}", this.consumerRoot.consumer);

    		ColumnedRow rootRow = currentContext().getDecoder(consumerRoot.consumer.getNode());
    		EntityCreationResult rootEntityCreationResult = getRootEntityCreationResult(rootRow, context);

    		if (rootEntityCreationResult != null) {
    			foreachNode(rootEntityCreationResult.consumers, new NodeVisitor(rootEntityCreationResult.entity) {

    				@Override
    				public EntityCreationResult apply(ConsumerNode join, Object entity) {
    					// processing current depth
    					JoinRowConsumer consumer = join.getConsumer();
    					LOGGER.debug("Consuming {} on object {}", consumer, entity);
    					consumer.beforeRowConsumption(context);
    					ColumnedRow nodeRow = currentContext().getDecoder(consumer.getNode());
    					if (consumer instanceof PassiveJoinNode.PassiveJoinRowConsumer) {
    						((PassiveJoinRowConsumer) consumer).consume(entity, nodeRow);
    						return new EntityCreationResult(entity, join);
    					} else if (consumer instanceof MergeJoinNode.MergeJoinRowConsumer) {
    						((MergeJoinRowConsumer) consumer).mergeProperties(entity, nodeRow);
    						return new EntityCreationResult(entity, join);
    					} else if (consumer instanceof RelationJoinNode.RelationJoinRowConsumer) {
    						Object relatedEntity = ((RelationJoinRowConsumer) consumer).applyRelatedEntity(entity, nodeRow, context);
    						EntityCreationResult result;
    						if (consumer instanceof ForkJoinRowConsumer) {
    							// In case of join-table polymorphism we have to provide the tree branch on which id was found
    							// in order to let created entity filled with right consumers. "Wrong" branches serve no purpose.
    							JoinRowConsumer nextRowConsumer = ((ForkJoinRowConsumer) consumer).giveNextConsumer();
    							if (nextRowConsumer == null) {
    								// means no identifier of polymorphic entity
    								result = new EntityCreationResult(null, (List<ConsumerNode>) null);
    							} else {
    								Optional<ConsumerNode> consumerNode = join.consumers.stream().filter(c -> nextRowConsumer == c.consumer).findFirst();
    								if (!consumerNode.isPresent()) {
    									throw new IllegalStateException("Can't find consumer node for " + nextRowConsumer + " in " + join.consumers);
    								} else {
    									result = new EntityCreationResult(relatedEntity, Arrays.asList(consumerNode.get()));
    								}
    							}
    						} else {
    							result = new EntityCreationResult(relatedEntity, join);
    						}
    						consumer.afterRowConsumption(context);
    						return result;
    					} else {
    						// Developer made something wrong because other types than MergeJoin and RelationJoin are not expected
    						throw new IllegalArgumentException("Unexpected join type, only "
    								+ Reflections.toString(PassiveJoinRowConsumer.class)
    								+ ", " + Reflections.toString(MergeJoinRowConsumer.class)
    								+ " and " + Reflections.toString(RelationJoinRowConsumer.class) + " are handled"
    								+ ", not " + (consumer == null ? "null" : Reflections.toString(consumer.getClass())));
    					}
    				}
    			});
    		}
    		return nullable(rootEntityCreationResult).map(c -> (C) c.entity);
    	}

    	private EntityCreationResult getRootEntityCreationResult(ColumnedRow row, EntityTreeInflater.TreeInflationContext context) {
    		C rootInstance = ((RootJoinRowConsumer<C>) this.consumerRoot.consumer).createRootInstance(row, context);
    		if (rootInstance != null) {
    			if (consumerRoot.consumer instanceof ExcludingJoinRowConsumer) {
    				// In case of polymorphism we have to provide the tree branch on which instance was found
    				// in order to let created entity filled with right consumers. "Wrong" branches serve no purpose.
    				Set<JoinRowConsumer> deadBranches = ((ExcludingJoinRowConsumer) consumerRoot.consumer).giveExcludedConsumers();
    				ArrayList<ConsumerNode> consumerNodes = new ArrayList<>(consumerRoot.consumers);
    				consumerNodes.removeIf(consumer -> deadBranches.contains(consumer.consumer));
    				return new EntityCreationResult(rootInstance, consumerNodes);
    			} else {
    				return new EntityCreationResult(rootInstance, consumerRoot.consumers);
    			}
    		} else {
    			return null;
    		}
    	}

    	@VisibleForTesting
    	void foreachNode(Collection<ConsumerNode> seeds, NodeVisitor nodeVisitor) {
    		Queue<ConsumerNode> joinNodeStack = Collections.newLifoQueue();
    		joinNodeStack.addAll(seeds);
    		// Maintaining entities that will be given to each node : they are entities produced by parent node
    		Map<ConsumerNode, Object> entityPerConsumer = new HashMap<>(10);
    		joinNodeStack.forEach(node -> entityPerConsumer.put(node, nodeVisitor.entityRoot));

    		while (!joinNodeStack.isEmpty()) {
    			ConsumerNode joinNode = joinNodeStack.poll();
    			EntityCreationResult result = nodeVisitor.apply(joinNode, entityPerConsumer.get(joinNode));
    			if (result.getEntity() != null) {
    				List<ConsumerNode> nextConsumers = result.nextConsumers();
    				joinNodeStack.addAll(nextConsumers);
    				nextConsumers.forEach(node -> entityPerConsumer.put(node, result.getEntity()));
    			}
    		}
    	}

    	/**
    	 * Small structure to store {@link JoinRowConsumer} as a tree that reflects {@link EntityJoinTree} input.
    	 */
    	static class ConsumerNode {

    		private final JoinRowConsumer consumer;

    		private final List<ConsumerNode> consumers = new ArrayList<>();

    		ConsumerNode(JoinRowConsumer consumer) {
    			this.consumer = consumer;
    		}

    		public JoinRowConsumer getConsumer() {
    			return consumer;
    		}

    		void addConsumer(ConsumerNode consumer) {
    			this.consumers.add(consumer);
    		}
    	}

    	static abstract class NodeVisitor {

    		private final Object entityRoot;

    		NodeVisitor(Object entityRoot) {
    			this.entityRoot = entityRoot;
    		}

    		/**
    		 * Asks for parentEntity consumption by {@link ConsumerNode}
    		 * @param consumerNode consumer expected to use given entity to constructs, fills, does whatever, with given entity
    		 * @param parentEntity entity on which consumer mechanism may apply
    		 * @return the optional entity created by consumer (as in one-to-one or one-to-many relation), else given parentEntity (not null)
    		 */
    		abstract EntityCreationResult apply(ConsumerNode consumerNode, Object parentEntity);

    		static class EntityCreationResult {

    			private final Object entity;

    			private final List<ConsumerNode> consumers;

    			EntityCreationResult(Object entity, ConsumerNode entityCreator) {
    				this(entity, entityCreator.consumers);
    			}

    			EntityCreationResult(Object entity, List<ConsumerNode> consumers) {
    				this.entity = entity;
    				this.consumers = consumers;
    			}

    			public Object getEntity() {
    				return entity;
    			}

    			List<ConsumerNode> nextConsumers() {
    				return consumers;
    			}
    		}
    	}

    	/**
    	 * Represents a relation between an entity and another one through its join node
    	 */
    	static class RelationIdentifier {

    		protected final Object rootEntity;
    		protected final Class relatedEntityType;
    		protected final Object relatedBeanIdentifier;
    		protected final RelationJoinRowConsumer joinNode;

    		RelationIdentifier(Object rootEntity, Class<?> relatedEntityType, Object relatedBeanIdentifier, RelationJoinRowConsumer joinNode) {
    			this.rootEntity = rootEntity;
    			this.relatedEntityType = relatedEntityType;
    			this.relatedBeanIdentifier = relatedBeanIdentifier;
    			this.joinNode = joinNode;
    		}

    		/**
    		 * Implemented so inflater algorithm can check for already treated relation
    		 * @param o another RelationIdentifier
    		 * @return true if given instance equals this one
    		 */
    		@Override
    		public boolean equals(Object o) {
    			if (this == o) return true;
    			if (!(o instanceof EntityTreeInflater.RelationIdentifier)) return false;

    			RelationIdentifier other = (RelationIdentifier) o;

    			// WARN : this is finely defined according to :
    			// - comparison of root entity is based on instance comparison to avoid being dependent of equals() implementation which may vary during inflation process
    			// - comparison with related bean is based on its identifier with Object equality because it is expected to be simple and comparable type
    			// - comparison of Join Node which stores kind of "relation name" that links current beans (Object equality could be used but using instance matched more our expectation)
    			if (rootEntity != other.rootEntity) return false;
    			if (!relatedEntityType.equals(other.relatedEntityType)) return false;
    			if (!relatedBeanIdentifier.equals(other.relatedBeanIdentifier)) return false;
    			return joinNode == other.joinNode;
    		}

    		@Override
    		public int hashCode() {
    			int result = rootEntity.hashCode();
    			result = 31 * result + relatedEntityType.hashCode();
    			result = 31 * result + relatedBeanIdentifier.hashCode();
    			result = 31 * result + joinNode.hashCode();
    			return result;
    		}
    	}

    	/**
    	 * Container for information used during {@link Row} transformation as a bean graph.
    	 * Accessible from {@link EntityTreeInflater#currentContext()} during its lifecycle:
    	 * - instanced at beginning of {@link Row} transformation ({@link EntityTreeInflater#transform(Iterable, int)}
    	 * - dropped at the end of the method
    	 *
    	 * @implNote made non-static to ease access to the surrounding {@link EntityTreeInflater} instance
    	 */
    	public static class TreeInflationContext {

    		/** Entity cache */
    		private final EntityCache entityCache;

    		/** Storage for treated relations */
    		private final Set<RelationIdentifier> treatedRelations = new HashSet<>();

    		/**
    		 * Currently (Thread-linked) row read by inflater.
    		 * This row is not linked to any node and can be used has a base to construct some other {@link ColumnedRow}
    		 * based on some {@link JoinNode}.
    		 * @see #getDecoder
    		 */
    		private ColumnedRow currentRow;

    		private TreeInflationContext() {
    			this(new BasicEntityCache());
    		}

    		public TreeInflationContext(EntityCache entityCache) {
    			this.entityCache = entityCache;
    		}

    		private TreeInflationContext setCurrentRow(ColumnedRow currentRow) {
    			this.currentRow = currentRow;
    			return this;
    		}

    		/**
    		 * Gives the value provider of current {@link java.sql.ResultSet} row for given {@link JoinNode}.
    		 *
    		 *
    		 * @param joinNode relation node identifier
    		 * @return null if data is null
    		 * @see EntityTreeQueryBuilder#cloneTable(JoinNode)  table clone mechanism.
    		 */
    		 public ColumnedRow getDecoder(JoinNode<?, ?> joinNode) {
    			Fromable table = joinNode.getTable();
    			return new ColumnedRow() {
    				@Override
    				public <E> E get(Selectable<E> key) {
    					Selectable<E> column = table.findColumn(key.getExpression());
    					if (column == null) {
    						// This is more for debugging purpose than for a real production goal, may be removed later
    						throw new MappingConfigurationException("Can't find column for " + key.getExpression() + " in table : " + joinNode.getTable().getName());
    					}
    					return currentRow.get(column);
    				}
    			};
    		}

    		public boolean isTreatedOrAppend(RelationIdentifier relationIdentifier) {
    			boolean alreadyTreated = treatedRelations.contains(relationIdentifier);
    			if (!alreadyTreated) {
    				treatedRelations.add(relationIdentifier);
    			}
    			return !alreadyTreated;
    		}

    		/**
    		 * Expected to retrieve an entity by its class and identifier from cache or instantiates it and put it into the cache
    		 *
    		 * @param clazz the type of the entity
    		 * @param identifier the identifier of the entity (Long, String, ...)
    		 * @param fallbackFactory the "method" that will be called to create the entity when the entity is not in the cache
    		 * @return the existing instance in the cache or a new object
    		 */
    		public <E> E giveEntityFromCache(Class<E> clazz, Object identifier, Supplier<E> fallbackFactory) {
    			return this.entityCache.computeIfAbsent(clazz, identifier, fallbackFactory);
    		}
    	}

    	/**
    	 * A {@link LinkedMap} basing its hash on {@link System#identityHashCode(Object)} one, and comparing key on their reference.
    	 * Made to have a {@link Map} that hashes on identity while keeping insertion order.
    	 *
    	 * @param <K>
    	 * @param <V>
    	 * @author Guillaume Mary
    	 */
    	private static class IdentityLinkedMap<K, V> extends LinkedMap<K, V> {

    		public IdentityLinkedMap(int initialCapacity) {
    			super(initialCapacity);
    		}

    		@Override
    		protected int hash(Object key) {
    			return System.identityHashCode(key);
    		}

    		@Override
    		protected boolean isEqualKey(Object key1, Object key2) {
    			return key1 == key2;
    		}
    	}
    }