# InheritanceMappingStep.java

    package org.codefilarete.stalactite.engine.configurer.builder;

    import java.util.HashMap;
    import java.util.Map;
    import java.util.function.Consumer;

    import org.codefilarete.reflection.AccessorDefinition;
    import org.codefilarete.reflection.PropertyAccessPoint;
    import org.codefilarete.reflection.PropertyMutator;
    import org.codefilarete.reflection.ReadWritePropertyAccessPoint;
    import org.codefilarete.reflection.ValueAccessPoint;
    import org.codefilarete.reflection.ValueAccessPointMap;
    import org.codefilarete.reflection.ValueAccessPointSet;
    import org.codefilarete.stalactite.dsl.MappingConfigurationException;
    import org.codefilarete.stalactite.dsl.embeddable.EmbeddableMappingConfiguration;
    import org.codefilarete.stalactite.dsl.embeddable.EmbeddableMappingConfiguration.Linkage;
    import org.codefilarete.stalactite.dsl.entity.EntityMappingConfiguration;
    import org.codefilarete.stalactite.dsl.entity.EntityMappingConfiguration.InheritanceConfiguration;
    import org.codefilarete.stalactite.dsl.entity.OptimisticLockOption;
    import org.codefilarete.stalactite.dsl.naming.ColumnNamingStrategy;
    import org.codefilarete.stalactite.dsl.naming.UniqueConstraintNamingStrategy;
    import org.codefilarete.stalactite.engine.configurer.builder.embeddable.EmbeddableMapping;
    import org.codefilarete.stalactite.engine.configurer.builder.embeddable.EmbeddableMappingBuilder;
    import org.codefilarete.stalactite.sql.ddl.structure.Column;
    import org.codefilarete.stalactite.sql.ddl.structure.Table;
    import org.codefilarete.stalactite.sql.statement.binder.ColumnBinderRegistry;
    import org.codefilarete.tool.Duo;
    import org.codefilarete.tool.Reflections;
    import org.codefilarete.tool.VisibleForTesting;
    import org.codefilarete.tool.collection.Iterables;
    import org.codefilarete.tool.collection.KeepOrderSet;
    import org.codefilarete.tool.function.Converter;
    import org.codefilarete.tool.function.Functions;
    import org.codefilarete.tool.function.Hanger.Holder;

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

    /**
     * Collect properties mapping from inheritance in a form of {@link MappingPerTable}
     *
     * @param <C>
     * @param <I>
     * @author Guillaume Mary
     */
    public class InheritanceMappingStep<C, I> {

    	/**
    	 * Gives embedded (non relational) properties mapping, including those from super classes
    	 *
    	 * @return the mapping between property accessor and their column in target tables, never null
    	 */
    	@VisibleForTesting
    	<T extends Table<T>> MappingPerTable<C> collectPropertiesMappingFromInheritance(EntityMappingConfiguration<C, I> entityMappingConfiguration,
    																					Map<EntityMappingConfiguration, Table> tableMap,
    																					ColumnBinderRegistry columnBinderRegistry,
    																					ColumnNamingStrategy columnNamingStrategy,
    																					UniqueConstraintNamingStrategy uniqueConstraintNamingStrategy) {
    		MappingPerTable<C> result = new MappingPerTable<>();

    		InheritanceMappingCollector<C, I, T> mappingCollector = new InheritanceMappingCollector<>(result, columnBinderRegistry, columnNamingStrategy, uniqueConstraintNamingStrategy);
    		visitInheritedEmbeddableMappingConfigurations(entityMappingConfiguration,
    				new Consumer<EntityMappingConfiguration>() {
    					private boolean initMapping = false;

    					@Override
    					public void accept(EntityMappingConfiguration entityMappingConfiguration) {
    						mappingCollector.currentKey = entityMappingConfiguration;
    						if (initMapping) {
    							mappingCollector.init();
    						}

    						mappingCollector.currentTable = (T) tableMap.get(entityMappingConfiguration);
    						mappingCollector.accept(entityMappingConfiguration);

    						// we must reinit mapping when table changes (which is a join table case), then mapping doesn't target always the same Map
    						initMapping = nullable(entityMappingConfiguration.getInheritanceConfiguration())
    								.map(InheritanceConfiguration::isJoiningTables).getOr(false);
    					}
    				}, embeddableMappingConfiguration -> {
    					mappingCollector.mappedSuperClass = true;
    					mappingCollector.accept(embeddableMappingConfiguration);
    				});
    		return result;
    	}

    	/**
    	 * Visits parent {@link EntityMappingConfiguration} of given entity mapping configuration (including itself), this is an optional operation
    	 * because given configuration may not have a direct entity ancestor.
    	 * Then visits mapped super classes as {@link EmbeddableMappingConfiguration} of the last visited {@link EntityMappingConfiguration}, optional
    	 * operation too.
    	 * This is because inheritance can only have 2 paths:
    	 * - first an optional inheritance from some other entity
    	 * - then an optional inheritance from some mapped super class
    	 *
    	 * @param entityConfigurationConsumer
    	 * @param mappedSuperClassConfigurationConsumer
    	 */
    	void visitInheritedEmbeddableMappingConfigurations(EntityMappingConfiguration<C, I> entityMappingConfiguration,
    													   Consumer<EntityMappingConfiguration> entityConfigurationConsumer,
    													   Consumer<EmbeddableMappingConfiguration> mappedSuperClassConfigurationConsumer) {
    		// iterating over mapping from inheritance
    		Holder<EntityMappingConfiguration> lastMapping = new Holder<>();
    		// iterating over mapping from inheritance
    		entityMappingConfiguration.inheritanceIterable().forEach(configuration -> {
    			entityConfigurationConsumer.accept(configuration);
    			lastMapping.set(configuration);
    		});
    		if (lastMapping.get().getPropertiesMapping().getMappedSuperClassConfiguration() != null) {
    			// iterating over mapping from mapped super classes
    			lastMapping.get().getPropertiesMapping().getMappedSuperClassConfiguration().inheritanceIterable().forEach(mappedSuperClassConfigurationConsumer);
    		}
    	}

    	public static class Mapping<C, T extends Table<T>> {
    		private final Object /* EntityMappingConfiguration, EmbeddableMappingConfiguration, SubEntityMappingConfiguration */ mappingConfiguration;
    		private final T targetTable;
    		private final Map<ReadWritePropertyAccessPoint<C, Object>, Column<T, Object>> mapping;
    		private final Map<PropertyMutator<C, Object>, Column<T, Object>> readonlyMapping;
    		private final Duo<ReadWritePropertyAccessPoint<C, Object>, Column<T, Object>> versioningMapping;
    		private final ValueAccessPointSet<C, PropertyAccessPoint<C, ?>> propertiesSetByConstructor = new ValueAccessPointSet<>();
    		private final boolean mappedSuperClass;
    		private final ValueAccessPointMap<C, Converter<Object, Object>, PropertyAccessPoint<C, ?>> readConverters;
    		private final ValueAccessPointMap<C, Converter<Object, Object>, PropertyAccessPoint<C, ?>> writeConverters;

    		public Mapping(Object mappingConfiguration,
    					   T targetTable,
    					   Map<? extends ReadWritePropertyAccessPoint<C, Object>, ? extends Column<T, Object>> mapping,
    					   Map<? extends PropertyMutator<C, Object>, ? extends Column<T, Object>> readonlyMapping,
    					   Duo<? extends ReadWritePropertyAccessPoint<C, ?>, ? extends Column<T, ?>> versioningMapping,
    					   ValueAccessPointMap<C, ? extends Converter<Object, Object>, PropertyAccessPoint<C, ?>> readConverters,
    					   ValueAccessPointMap<C, ? extends Converter<Object, Object>, PropertyAccessPoint<C, ?>> writeConverters,
    					   boolean mappedSuperClass) {
    			this.mappingConfiguration = mappingConfiguration;
    			this.targetTable = targetTable;
    			this.mapping = (Map<ReadWritePropertyAccessPoint<C, Object>, Column<T, Object>>) mapping;
    			this.readonlyMapping = (Map<PropertyMutator<C, Object>, Column<T, Object>>) readonlyMapping;
    			this.versioningMapping = (Duo<ReadWritePropertyAccessPoint<C, Object>, Column<T, Object>>) versioningMapping;
    			this.readConverters = (ValueAccessPointMap<C, Converter<Object, Object>, PropertyAccessPoint<C, ?>>) readConverters;
    			this.writeConverters = (ValueAccessPointMap<C, Converter<Object, Object>, PropertyAccessPoint<C, ?>>) writeConverters;
    			this.mappedSuperClass = mappedSuperClass;
    		}

    		public Object getMappingConfiguration() {
    			return mappingConfiguration;
    		}

    		public boolean isMappedSuperClass() {
    			return mappedSuperClass;
    		}

    		public EmbeddableMappingConfiguration<C> giveEmbeddableConfiguration() {
    			return (EmbeddableMappingConfiguration<C>) (this.mappingConfiguration instanceof EmbeddableMappingConfiguration
    					? this.mappingConfiguration
    					: (this.mappingConfiguration instanceof EntityMappingConfiguration ?
    					((EntityMappingConfiguration<C, T>) this.mappingConfiguration).getPropertiesMapping()
    					: null));
    		}

    		public T getTargetTable() {
    			return targetTable;
    		}

    		public Map<ReadWritePropertyAccessPoint<C, Object>, Column<T, Object>> getMapping() {
    			return mapping;
    		}

    		public Map<PropertyMutator<C, Object>, Column<T, Object>> getReadonlyMapping() {
    			return readonlyMapping;
    		}

    		public Duo<ReadWritePropertyAccessPoint<C, Object>, Column<T, Object>> getVersioningMapping() {
    			return versioningMapping;
    		}

    		public ValueAccessPointMap<C, Converter<Object, Object>, PropertyAccessPoint<C, ?>> getReadConverters() {
    			return readConverters;
    		}

    		public ValueAccessPointMap<C, Converter<Object, Object>, PropertyAccessPoint<C, ?>> getWriteConverters() {
    			return writeConverters;
    		}

    		public ValueAccessPointSet<C, PropertyAccessPoint<C, ?>> getPropertiesSetByConstructor() {
    			return propertiesSetByConstructor;
    		}
    	}

    	public static class MappingPerTable<C> {

    		private final KeepOrderSet<Mapping<C, ?>> mappings = new KeepOrderSet<>();

    		<T extends Table<T>> Mapping<C, T> add(
    				Object /* EntityMappingConfiguration, EmbeddableMappingConfiguration, SubEntityMappingConfiguration */ mappingConfiguration,
    				T table,
    				Map<ReadWritePropertyAccessPoint<C, Object>, Column<T, Object>> mapping,
    				Map<PropertyMutator<C, Object>, Column<T, Object>> readonlyMapping,
    				Duo<ReadWritePropertyAccessPoint<C, ?>, Column<T, ?>> versioningMapping,
    				ValueAccessPointMap<C, ? extends Converter<Object, Object>, PropertyAccessPoint<C, ?>> readConverters,
    				ValueAccessPointMap<C, ? extends Converter<Object, Object>, PropertyAccessPoint<C, ?>> writeConverters,
    				boolean mappedSuperClass) {
    			Mapping<C, T> newMapping = new Mapping<>(mappingConfiguration, table,
    					mapping, readonlyMapping, versioningMapping,
    					readConverters,
    					writeConverters,
    					mappedSuperClass);
    			this.mappings.add(newMapping);
    			return newMapping;
    		}

    		<T extends Table<T>> Map<ReadWritePropertyAccessPoint<C, Object>, Column<T, Object>> giveMapping(T table) {
    			Mapping<C, T> foundMapping = (Mapping<C, T>) Iterables.find(this.mappings, m -> m.getTargetTable().equals(table));
    			if (foundMapping == null) {
    				throw new IllegalArgumentException("Can't find table '" + table.getAbsoluteName()
    						+ "' in " + Iterables.collectToList(this.mappings, Functions.chain(Mapping::getTargetTable, Table::getAbsoluteName)).toString());
    			}
    			return foundMapping.mapping;
    		}

    		/**
    		 * @return tables found during inheritance iteration (hence in "ascending" order)
    		 */
    		KeepOrderSet<Table> giveTables() {
    			return Iterables.collect(this.mappings, Mapping::getTargetTable, KeepOrderSet::new);
    		}

    		public KeepOrderSet<Mapping<C, ?>> getMappings() {
    			return mappings;
    		}

    	}

    	private static class InheritanceMappingCollector<C, I, T extends Table<T>> implements Consumer<EmbeddableMappingConfiguration<C>> {

    		private final MappingPerTable<C> result;

    		private T currentTable;

    		private Map<ReadWritePropertyAccessPoint<C, Object>, Column<T, Object>> currentColumnMap;

    		private Map<PropertyMutator<C, Object>, Column<T, Object>> currentReadonlyColumnMap;

    		private Duo<ReadWritePropertyAccessPoint<C, ?>, Column<T, ?>> currentVersioningMapping;

    		private final ValueAccessPointMap<C, Converter<Object, Object>, PropertyAccessPoint<C, ?>> readConverters;

    		private final ValueAccessPointMap<C, Converter<Object, Object>, PropertyAccessPoint<C, ?>> writeConverters;

    		private Mapping<C, T> currentMapping;

    		private Object currentKey;

    		private boolean mappedSuperClass;

    		private final ColumnBinderRegistry columnBinderRegistry;
    		private final ColumnNamingStrategy columnNamingStrategy;
    		private final UniqueConstraintNamingStrategy uniqueConstraintNamingStrategy;

    		InheritanceMappingCollector(MappingPerTable<C> result,
    									ColumnBinderRegistry columnBinderRegistry,
    									ColumnNamingStrategy columnNamingStrategy,
    									UniqueConstraintNamingStrategy uniqueConstraintNamingStrategy) {
    			this.result = result;
    			this.uniqueConstraintNamingStrategy = uniqueConstraintNamingStrategy;
    			this.currentColumnMap = new HashMap<>();
    			this.currentReadonlyColumnMap = new HashMap<>();
    			this.readConverters = new ValueAccessPointMap<>();
    			this.writeConverters = new ValueAccessPointMap<>();
    			this.mappedSuperClass = false;
    			this.columnBinderRegistry = columnBinderRegistry;
    			this.columnNamingStrategy = columnNamingStrategy;
    		}

    		public void init() {
    			// we can't clear those maps since they are given to some other objects, thus clearing them will impact other objects
    			this.currentColumnMap = new HashMap<>();
    			this.currentReadonlyColumnMap = new HashMap<>();
    			this.readConverters.clear();
    			this.writeConverters.clear();
    			this.currentMapping = null;
    		}

    		public void accept(EntityMappingConfiguration<C, I> entityMappingConfiguration) {
    			// we collection the versioning strategy before calling accept(..) to fill currentVersioningMapping that will be passed to the result
    			OptimisticLockOption<C, ?> optimisticLockOption = entityMappingConfiguration.getOptimisticLockOption();
    			if (optimisticLockOption != null) {
    				AccessorDefinition versioningDefinition = AccessorDefinition.giveDefinition(optimisticLockOption.getVersionAccessor());
    				String versioningColumnName = this.columnNamingStrategy.giveName(versioningDefinition);
    				boolean isColumnNullable = !Reflections.isPrimitiveType(versioningDefinition.getMemberType());
    				// Column addition should be shared in EmbeddableMappingBuilder but the class is shared by different use cases for which the
    				// versioning is not relevant, so this particularity is left here
    				Column<T, ?> versioningColumn = currentTable.addColumn(versioningColumnName, versioningDefinition.getMemberType(), null, isColumnNullable);
    				this.currentVersioningMapping = new Duo<>(optimisticLockOption.getVersionAccessor(), versioningColumn);
    			}

    			accept(entityMappingConfiguration.getPropertiesMapping());
    		}

    		@Override
    		public void accept(EmbeddableMappingConfiguration<C> embeddableMappingConfiguration) {
    			EmbeddableMappingBuilder<C, T> embeddableMappingBuilder = new EmbeddableMappingBuilder<>(embeddableMappingConfiguration,
    					this.currentTable,
    					this.columnBinderRegistry,
    					this.columnNamingStrategy,
    					this.uniqueConstraintNamingStrategy);
    			EmbeddableMapping<C, T> propertiesMapping = embeddableMappingBuilder.build();
    			ValueAccessPointSet<C, ValueAccessPoint<C>> localMapping = new ValueAccessPointSet<>(currentColumnMap.keySet());
    			propertiesMapping.getMapping().keySet().forEach(propertyAccessor -> {
    				if (localMapping.contains(propertyAccessor)) {
    					throw new MappingConfigurationException(AccessorDefinition.toString(propertyAccessor) + " is mapped twice");
    				}
    			});
    			propertiesMapping.getReadonlyMapping().keySet().forEach(propertyAccessor -> {
    				if (localMapping.contains(propertyAccessor)) {
    					throw new MappingConfigurationException(AccessorDefinition.toString(propertyAccessor) + " is mapped twice");
    				}
    			});
    			currentColumnMap.putAll(propertiesMapping.getMapping());
    			currentReadonlyColumnMap.putAll(propertiesMapping.getReadonlyMapping());
    			readConverters.putAll(propertiesMapping.getReadConverters());
    			writeConverters.putAll(propertiesMapping.getWriteConverters());
    			if (currentMapping == null) {
    				currentMapping = result.add(preventNull(currentKey, embeddableMappingConfiguration), currentTable,
    						// Note that we clone maps because ours are reused while iterating
    						currentColumnMap, currentReadonlyColumnMap, currentVersioningMapping,
    						new ValueAccessPointMap<>(readConverters),
    						new ValueAccessPointMap<>(writeConverters),
    						mappedSuperClass);
    			} else {
    				currentMapping = result.add(embeddableMappingConfiguration, currentTable,
    						// Note that we clone maps because ours are reused while iterating
    						currentColumnMap, currentReadonlyColumnMap, currentVersioningMapping,
    						new ValueAccessPointMap<>(readConverters),
    						new ValueAccessPointMap<>(writeConverters),
    						mappedSuperClass);
    				currentMapping.getMapping().putAll(currentColumnMap);
    			}
    			embeddableMappingConfiguration.getPropertiesMapping().stream()
    					.filter(Linkage::isSetByConstructor).map(Linkage::getAccessor).forEach(currentMapping.propertiesSetByConstructor::add);
    		}
    	}
    }