# MainPersisterStep.java

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

    import java.lang.reflect.Constructor;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Map;
    import java.util.Map.Entry;
    import java.util.Set;
    import java.util.function.Function;
    import javax.annotation.Nullable;

    import org.codefilarete.reflection.AccessorDefinition;
    import org.codefilarete.reflection.PropertyAccessPoint;
    import org.codefilarete.reflection.PropertyMutator;
    import org.codefilarete.reflection.ReadWritePropertyAccessPoint;
    import org.codefilarete.reflection.ValueAccessPointMap;
    import org.codefilarete.reflection.ValueAccessPointSet;
    import org.codefilarete.stalactite.dsl.MappingConfigurationException;
    import org.codefilarete.stalactite.dsl.embeddable.EmbeddableMappingConfiguration.Linkage;
    import org.codefilarete.stalactite.dsl.entity.EntityMappingConfiguration;
    import org.codefilarete.stalactite.dsl.entity.EntityMappingConfiguration.EntityFactoryProvider;
    import org.codefilarete.stalactite.dsl.entity.OptimisticLockOption;
    import org.codefilarete.stalactite.engine.VersioningStrategy;
    import org.codefilarete.stalactite.engine.configurer.AbstractIdentification;
    import org.codefilarete.stalactite.engine.configurer.AbstractIdentification.CompositeKeyIdentification;
    import org.codefilarete.stalactite.engine.configurer.AbstractIdentification.SingleColumnIdentification;
    import org.codefilarete.stalactite.engine.configurer.DefaultComposedIdentifierAssembler;
    import org.codefilarete.stalactite.engine.configurer.NamingConfiguration;
    import org.codefilarete.stalactite.engine.configurer.builder.InheritanceMappingStep.Mapping;
    import org.codefilarete.stalactite.engine.configurer.builder.InheritanceMappingStep.MappingPerTable;
    import org.codefilarete.stalactite.engine.runtime.AbstractVersioningStrategy.VersioningStrategySupport;
    import org.codefilarete.stalactite.engine.runtime.SimpleRelationalEntityPersister;
    import org.codefilarete.stalactite.mapping.ComposedIdMapping;
    import org.codefilarete.stalactite.mapping.DefaultEntityMapping;
    import org.codefilarete.stalactite.mapping.IdMapping;
    import org.codefilarete.stalactite.mapping.SimpleIdMapping;
    import org.codefilarete.stalactite.mapping.id.assembly.ComposedIdentifierAssembler;
    import org.codefilarete.stalactite.mapping.id.assembly.IdentifierAssembler;
    import org.codefilarete.stalactite.mapping.id.assembly.SingleIdentifierAssembler;
    import org.codefilarete.stalactite.mapping.id.manager.AlreadyAssignedIdentifierManager;
    import org.codefilarete.stalactite.mapping.id.manager.IdentifierInsertionManager;
    import org.codefilarete.stalactite.sql.ConnectionConfiguration;
    import org.codefilarete.stalactite.sql.Dialect;
    import org.codefilarete.stalactite.sql.ddl.structure.Column;
    import org.codefilarete.stalactite.sql.ddl.structure.PrimaryKey;
    import org.codefilarete.stalactite.sql.ddl.structure.Table;
    import org.codefilarete.stalactite.sql.result.ColumnedRow;
    import org.codefilarete.tool.Duo;
    import org.codefilarete.tool.Reflections;
    import org.codefilarete.tool.collection.Iterables;
    import org.codefilarete.tool.collection.KeepOrderMap;
    import org.codefilarete.tool.exception.NotImplementedException;
    import org.codefilarete.tool.function.Converter;
    import org.codefilarete.tool.function.Serie;

    import static org.codefilarete.tool.Nullable.nullable;
    import static org.codefilarete.tool.collection.Iterables.first;

    public class MainPersisterStep<C, I> {

    	/**
    	 *
    	 * @param isIdentifyingConfiguration true for a root mapping (will use {@link SingleColumnIdentification#getInsertionManager()}), false for inheritance case (will use {@link SingleColumnIdentification#getIdentificationDefiner()})
    	 * @param targetTable {@link Table} to use by created {@link DefaultEntityMapping}
    	 * @param mapping properties to be managed by created {@link DefaultEntityMapping}
    	 * @param propertiesSetByConstructor properties set by constructor ;), to avoid re-setting them (and even look for a setter for them)
    	 * @param identification {@link SingleColumnIdentification} to use (see isIdentifyingConfiguration)
    	 * @param beanType entity type to be managed by created {@link DefaultEntityMapping}
    	 * @param entityFactoryProvider optional, if null default bean type constructor will be used
    	 * @param <E> entity type
    	 * @param <I> identifier type
    	 * @param <T> {@link Table} type
    	 * @return a new {@link DefaultEntityMapping} built from all arguments
    	 */
    	public static <E, I, T extends Table<T>> DefaultEntityMapping<E, I, T> createEntityMapping(
    			boolean isIdentifyingConfiguration,
    			T targetTable,
    			Map<? extends ReadWritePropertyAccessPoint<E, Object>, ? extends Column<T, Object>> mapping,
    			Map<? extends PropertyMutator<E, Object>, ? extends Column<T, Object>> readOnlyMapping,
    			ValueAccessPointMap<E, ? extends Converter<Object, Object>, PropertyAccessPoint<E, ?>> readConverters,
    			ValueAccessPointMap<E, ? extends Converter<Object, Object>, PropertyAccessPoint<E, ?>> writeConverters,
    			ValueAccessPointSet<E, PropertyAccessPoint<E, ?>> propertiesSetByConstructor,
    			AbstractIdentification<E, I> identification,
    			Class<E> beanType,
    			@Nullable EntityFactoryProvider<E, T> entityFactoryProvider) {
    		return createEntityMapping(
    				isIdentifyingConfiguration,
    				targetTable,
    				mapping,
    				readOnlyMapping,
    				null,	// no versioning
    				readConverters,
    				writeConverters,
    				propertiesSetByConstructor,
    				identification,
    				beanType,
    				entityFactoryProvider);
    	}

    	/**
    	 *
    	 * @param isIdentifyingConfiguration true for a root mapping (will use {@link SingleColumnIdentification#getInsertionManager()}), false for inheritance case (will use {@link SingleColumnIdentification#getIdentificationDefiner()})
    	 * @param targetTable {@link Table} to use by created {@link DefaultEntityMapping}
    	 * @param mapping properties to be managed by created {@link DefaultEntityMapping}
    	 * @param propertiesSetByConstructor properties set by constructor ;), to avoid re-setting them (and even look for a setter for them)
    	 * @param identification {@link SingleColumnIdentification} to use (see isIdentifyingConfiguration)
    	 * @param beanType entity type to be managed by created {@link DefaultEntityMapping}
    	 * @param entityFactoryProvider optional, if null default bean type constructor will be used
    	 * @param <E> entity type
    	 * @param <I> identifier type
    	 * @param <T> {@link Table} type
    	 * @return a new {@link DefaultEntityMapping} built from all arguments
    	 */
    	public static <E, I, T extends Table<T>> DefaultEntityMapping<E, I, T> createEntityMapping(
    			boolean isIdentifyingConfiguration,
    			T targetTable,
    			Map<? extends ReadWritePropertyAccessPoint<E, Object>, ? extends Column<T, Object>> mapping,
    			Map<? extends PropertyMutator<E, Object>, ? extends Column<T, Object>> readOnlyMapping,
    			@Nullable Duo<? extends ReadWritePropertyAccessPoint<E, Object>, ? extends Column<T, Object>> versioningMapping,
    			ValueAccessPointMap<E, ? extends Converter<Object, Object>, PropertyAccessPoint<E, ?>> readConverters,
    			ValueAccessPointMap<E, ? extends Converter<Object, Object>, PropertyAccessPoint<E, ?>> writeConverters,
    			ValueAccessPointSet<E, PropertyAccessPoint<E, ?>> propertiesSetByConstructor,
    			AbstractIdentification<E, I> identification,
    			Class<E> beanType,
    			@Nullable EntityFactoryProvider<E, T> entityFactoryProvider) {

    		PrimaryKey<T, I> primaryKey = targetTable.getPrimaryKey();
    		ReadWritePropertyAccessPoint<E, I> idAccessor = identification.getIdAccessor();
    		AccessorDefinition idDefinition = AccessorDefinition.giveDefinition(idAccessor);
    		// Child class insertion manager is always an "Already assigned" one because parent manages it for her
    		IdentifierInsertionManager<E, I> identifierInsertionManager = isIdentifyingConfiguration
    				? identification.getInsertionManager()
    				: identification.getFallbackInsertionManager();
    		IdMapping<E, I> idMappingStrategy;
    		if (identification instanceof AbstractIdentification.CompositeKeyIdentification) {
    			Map<ReadWritePropertyAccessPoint<I, Object>, Column<Table, Object>> compositeKeyMapping = ((CompositeKeyIdentification<E, I>) identification).getCompositeKeyMapping();
    			Map<ReadWritePropertyAccessPoint<I, Object>, Column<T, Object>> composedKeyMapping = Iterables.map(compositeKeyMapping.entrySet(), Entry::getKey, entry -> targetTable.getColumn(entry.getValue().getName()), KeepOrderMap::new);
    			ComposedIdentifierAssembler<I, T> composedIdentifierAssembler = new DefaultComposedIdentifierAssembler<>(
    					targetTable,
    					(Class<I>) idDefinition.getMemberType(),
    					composedKeyMapping
    			);
    			idMappingStrategy = new ComposedIdMapping<>(idAccessor, (AlreadyAssignedIdentifierManager<E, I>) identifierInsertionManager, composedIdentifierAssembler);
    		} else {
    			idMappingStrategy = new SimpleIdMapping<>(idAccessor, identifierInsertionManager,
    					new SingleIdentifierAssembler<>(first(primaryKey.getColumns())));
    		}

    		Function<ColumnedRow, E> beanFactory;
    		boolean identifierSetByBeanFactory = true;
    		if (entityFactoryProvider == null) {
    			Constructor<E> constructorById = Reflections.findConstructor(beanType, idDefinition.getMemberType());
    			if (constructorById == null) {
    				// No constructor taking identifier as argument, we try with default constructor (no-arg one), and if
    				// not available, throws an exception since we can't find a compatible constructor : user should define one
    				Constructor<E> defaultConstructor = Reflections.findConstructor(beanType);
    				if (defaultConstructor == null) {
    					// we'll lately throw an exception (we could do it now) but the lack of constructor may be due to an abstract class in inheritance
    					// path which currently won't be instanced at runtime (because its concrete subclass will be) so there's no reason to throw
    					// the exception now
    					beanFactory = columnValueProvider -> {
    						throw new MappingConfigurationException("Entity class " + Reflections.toString(beanType) + " doesn't have a compatible accessible constructor,"
    								+ " please implement a no-arg constructor or " + Reflections.toString(idDefinition.getMemberType()) + "-arg constructor");
    					};
    				} else {
    					beanFactory = columnValueProvider -> Reflections.newInstance(defaultConstructor);
    					identifierSetByBeanFactory = false;
    				}
    			} else {
    				// Constructor with identifier as argument found
    				IdentifierAssembler<I, T> identifierAssembler = idMappingStrategy.getIdentifierAssembler();
    				beanFactory = columnValueProvider -> Reflections.newInstance(constructorById, identifierAssembler.assemble(columnValueProvider));
    			}
    		} else {
    			beanFactory = entityFactoryProvider.giveEntityFactory(targetTable);
    			identifierSetByBeanFactory = entityFactoryProvider.isIdentifierSetByFactory();
    		}

    		DefaultEntityMapping<E, I, T> result = new DefaultEntityMapping<>(beanType, targetTable, mapping, readOnlyMapping, versioningMapping, idMappingStrategy, beanFactory, identifierSetByBeanFactory);
    		result.getMainMapping().setReadConverters(readConverters);
    		result.getMainMapping().setWriteConverters(writeConverters);
    		propertiesSetByConstructor.forEach(result::addPropertySetByConstructor);
    		return result;
    	}

    	<T extends Table<T>> SimpleRelationalEntityPersister<C, I, T> buildMainPersister(EntityMappingConfiguration<C, I> entityMappingConfiguration,
    																					 AbstractIdentification<C, I> identification,
    																					 MappingPerTable<C> inheritanceMappingPerTable,
    																					 NamingConfiguration namingConfiguration,
    																					 Dialect dialect,
    																					 ConnectionConfiguration connectionConfiguration) {
    		Mapping<C, T> mainMapping = (Mapping<C, T>) first(inheritanceMappingPerTable.getMappings());
    		SimpleRelationalEntityPersister<C, I, T> mainPersister = buildMainPersister(entityMappingConfiguration, identification, mainMapping, dialect, connectionConfiguration);

    		applyExtraTableConfigurations(entityMappingConfiguration, identification, mainPersister, namingConfiguration, dialect, connectionConfiguration);
    		return mainPersister;
    	}

    	private <T extends Table<T>, V> SimpleRelationalEntityPersister<C, I, T> buildMainPersister(EntityMappingConfiguration<C, I> entityMappingConfiguration,
    																								AbstractIdentification<C, I> identification,
    																								Mapping<C, T> mapping,
    																								Dialect dialect,
    																								ConnectionConfiguration connectionConfiguration) {
    		EntityMappingConfiguration mappingConfiguration = (EntityMappingConfiguration) mapping.getMappingConfiguration();
    		// If there's some mapped inheritance, then the identifying configuration is the one with the join table
    		boolean isIdentifyingConfiguration = entityMappingConfiguration.getInheritanceConfiguration() == null
    				|| !entityMappingConfiguration.getInheritanceConfiguration().isJoinTable();

    		OptimisticLockOption<C, V> optimisticLockOption = (OptimisticLockOption<C, V>) entityMappingConfiguration.getOptimisticLockOption();
    		DefaultEntityMapping<C, I, T> mappingStrategy = createEntityMapping(
    				isIdentifyingConfiguration,
    				mapping.getTargetTable(),
    				mapping.getMapping(),
    				mapping.getReadonlyMapping(),
    				mapping.getVersioningMapping(),
    				mapping.getReadConverters(),
    				mapping.getWriteConverters(),
    				mapping.getPropertiesSetByConstructor(),
    				identification,
    				mappingConfiguration.getEntityType(),
    				mappingConfiguration.getEntityFactoryProvider());

    		VersioningStrategy<C, V> versioningStrategy = null;
    		if (optimisticLockOption != null) {
    			Serie<V> serie = nullable(optimisticLockOption.getSerie()).getOr(() -> findSerie(AccessorDefinition.giveDefinition(optimisticLockOption.getVersionAccessor()).getMemberType()));
    			versioningStrategy = new VersioningStrategySupport<>(optimisticLockOption.getVersionAccessor(), serie);
    		}
    		return new SimpleRelationalEntityPersister<>(mappingStrategy, versioningStrategy, dialect, connectionConfiguration);
    	}

    	private <V> Serie<V> findSerie(Class<V> propertyType) {
    		Serie<V> serie;
    		if (Integer.class.isAssignableFrom(propertyType) || int.class.isAssignableFrom(propertyType)) {
    			serie = (Serie<V>) Serie.INTEGER_SERIE;
    		} else if (Long.class.isAssignableFrom(propertyType) || long.class.isAssignableFrom(propertyType)) {
    			serie = (Serie<V>) Serie.LONG_SERIE;
    		} else if (Date.class.isAssignableFrom(propertyType)) {
    			serie = (Serie<V>) Serie.NOW_SERIE;
    		} else {
    			throw new NotImplementedException("Type of versioned property is not implemented, please provide a "
    					+ Serie.class.getSimpleName() + " for it : " + Reflections.toString(propertyType));
    		}
    		return serie;
    	}

    	private <T extends Table<T>> void applyExtraTableConfigurations(EntityMappingConfiguration<C, I> entityMappingConfiguration,
    																	AbstractIdentification<C, I> identification,
    																	SimpleRelationalEntityPersister<C, I, T> mainPersister,
    																	NamingConfiguration namingConfiguration,
    																	Dialect dialect,
    																	ConnectionConfiguration connectionConfiguration) {
    		Map<String, Set<Linkage>> extraTableLinkages = new HashMap<>();
    		// iterating over mapping from inheritance
    		entityMappingConfiguration.inheritanceIterable().forEach(entityMappingConfiguration1 -> {
    			EntityMappingConfiguration<C, I> mappingConfiguration = (EntityMappingConfiguration<C, I>) entityMappingConfiguration1;
    			List<Linkage> propertiesMapping = mappingConfiguration.getPropertiesMapping().getPropertiesMapping();
    			for (Linkage linkage : propertiesMapping) {
    				if (linkage.getExtraTableName() != null) {
    					extraTableLinkages.computeIfAbsent(linkage.getExtraTableName(), extraTableName -> new HashSet<>()).add(linkage);
    				}
    			}
    		});

    		if (!extraTableLinkages.isEmpty()) {
    			ExtraTableConfigurer<C, I, T> extraTableConfigurer = new ExtraTableConfigurer<>(identification, mainPersister, extraTableLinkages, dialect.getColumnBinderRegistry(), namingConfiguration);
    			extraTableConfigurer.configure(dialect, connectionConfiguration);
    		}
    	}
    }