# AssociationRecordMapping.java

    package org.codefilarete.stalactite.engine.configurer;

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

    import org.codefilarete.reflection.AccessorChain;
    import org.codefilarete.reflection.Accessors;
    import org.codefilarete.reflection.PropertyAccessor;
    import org.codefilarete.reflection.ReversibleAccessor;
    import org.codefilarete.stalactite.engine.runtime.AssociationRecord;
    import org.codefilarete.stalactite.engine.runtime.AssociationTable;
    import org.codefilarete.stalactite.mapping.DefaultEntityMapping;
    import org.codefilarete.stalactite.mapping.ComposedIdMapping;
    import org.codefilarete.stalactite.mapping.IdAccessor;
    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.query.model.Selectable;
    import org.codefilarete.stalactite.sql.ddl.structure.Column;
    import org.codefilarete.stalactite.sql.ddl.structure.Table;
    import org.codefilarete.stalactite.sql.result.ColumnedRow;
    import org.codefilarete.tool.Reflections;
    import org.codefilarete.tool.collection.Maps;
    import org.danekja.java.util.function.serializable.SerializableBiConsumer;
    import org.danekja.java.util.function.serializable.SerializableFunction;

    /**
     * @author Guillaume Mary
     */
    public class AssociationRecordMapping<
    		ASSOCIATIONTABLE extends AssociationTable<ASSOCIATIONTABLE, LEFTTABLE, RIGHTTABLE, LEFTID, RIGHTID>,
    		LEFTTABLE extends Table<LEFTTABLE>,
    		RIGHTTABLE extends Table<RIGHTTABLE>,
    		LEFTID,
    		RIGHTID>
    		extends DefaultEntityMapping<AssociationRecord, AssociationRecord, ASSOCIATIONTABLE> {

    	/**
    	 * Computes a mapping from the {@link AssociationRecord} class to the association table's column.
    	 * Handles both single-key and composite-key identifiers.
    	 *
    	 * @param identifierColumnMapping identifier column mapping
    	 * @param identifierAssembler identifier assembler
    	 * @param valueAccessor value accessor
    	 * @param valueMutator value mutator
    	 * @return a mapping between the association record property and the association table's column.
    	 * @param <ASSOCIATIONTABLE> association table type
    	 * @param <TARGETTABLE> target table type, which can be the left or right one
    	 * @param <ID> identifier type (simple type or complex one)
    	 */
    	private static <ASSOCIATIONTABLE extends Table<ASSOCIATIONTABLE>, TARGETTABLE extends Table<TARGETTABLE>, ID>
    	Map<ReversibleAccessor<AssociationRecord, Object>, Column<ASSOCIATIONTABLE, Object>> mapping(
    			Map<Column<TARGETTABLE, ?>, Column<ASSOCIATIONTABLE, ?>> identifierColumnMapping,
    			IdentifierAssembler<ID, TARGETTABLE> identifierAssembler,
    			SerializableFunction<AssociationRecord, Object> valueAccessor,
    			SerializableBiConsumer<AssociationRecord, Object> valueMutator) {
    		Map<ReversibleAccessor<AssociationRecord, Object>, Column<ASSOCIATIONTABLE, Object>> result = new HashMap<>();
    		if (identifierAssembler instanceof SingleIdentifierAssembler) {
    			ReversibleAccessor<AssociationRecord, Object> reversibleAccessor = PropertyAccessor.fromMethodReference(valueAccessor, valueMutator);
    			Column<TARGETTABLE, ID> column = ((SingleIdentifierAssembler<ID, TARGETTABLE>) identifierAssembler).getColumn();
    			Column<ASSOCIATIONTABLE, ID> associationtableColumn = (Column<ASSOCIATIONTABLE, ID>) identifierColumnMapping.get(column);
    			result.put(reversibleAccessor, (Column<ASSOCIATIONTABLE, Object>) associationtableColumn);
    		} else if (identifierAssembler instanceof DefaultComposedIdentifierAssembler) {
    			DefaultComposedIdentifierAssembler<ID, TARGETTABLE> leftIdentifierAssembler1 = (DefaultComposedIdentifierAssembler<ID, TARGETTABLE>) identifierAssembler;
    			Map<ReversibleAccessor<ID, ?>, Column<TARGETTABLE, ?>> mapping = leftIdentifierAssembler1.getMapping();
    			mapping.forEach((accessor, column) -> {
    				AccessorChain<AssociationRecord, Object> propertyAccessor = new AccessorChain<>(Accessors.accessor(valueAccessor), accessor);
    				propertyAccessor.setNullValueHandler(new AccessorChain.ValueInitializerOnNullValue((accessor1, aClass) -> Reflections.newInstance(leftIdentifierAssembler1.getDefaultConstructor())));
    				result.put(propertyAccessor, (Column<ASSOCIATIONTABLE, Object>) identifierColumnMapping.get(column));
    			});
    		}
    		return result;
    	}

    	public AssociationRecordMapping(ASSOCIATIONTABLE targetTable,
    									IdentifierAssembler<LEFTID, LEFTTABLE> leftIdentifierAssembler,
    									IdentifierAssembler<RIGHTID, RIGHTTABLE> rightIdentifierAssembler) {
    		this(targetTable, leftIdentifierAssembler, rightIdentifierAssembler, targetTable.getLeftIdentifierColumnMapping(), targetTable.getRightIdentifierColumnMapping());
    	}

    	private AssociationRecordMapping(ASSOCIATIONTABLE targetTable,
    									IdentifierAssembler<LEFTID, LEFTTABLE> leftIdentifierAssembler,
    									IdentifierAssembler<RIGHTID, RIGHTTABLE> rightIdentifierAssembler,
    									Map<Column<LEFTTABLE, ?>, Column<ASSOCIATIONTABLE, ?>> leftIdentifierColumnMapping,
    									Map<Column<RIGHTTABLE, ?>, Column<ASSOCIATIONTABLE, ?>> rightIdentifierColumnMapping
    									) {
    		super(AssociationRecord.class,
    				targetTable,
    				Maps.putAll(
    						mapping(leftIdentifierColumnMapping, leftIdentifierAssembler, AssociationRecord::getLeft, AssociationRecord::setLeft),
    						mapping(rightIdentifierColumnMapping, rightIdentifierAssembler, AssociationRecord::getRight, AssociationRecord::setRight)),
    				new ComposedIdMapping<AssociationRecord, AssociationRecord>(
    						new IdAccessor<AssociationRecord, AssociationRecord>() {
    							@Override
    							public AssociationRecord getId(AssociationRecord associationRecord) {
    								return associationRecord;
    							}

    							@Override
    							public void setId(AssociationRecord associationRecord, AssociationRecord identifier) {
    								associationRecord.setLeft(identifier.getLeft());
    								associationRecord.setRight(identifier.getRight());
    							}
    						}, new AlreadyAssignedIdentifierManager<>(AssociationRecord.class, AssociationRecord::markAsPersisted, AssociationRecord::isPersisted),
    						new ComposedIdentifierAssembler<AssociationRecord, ASSOCIATIONTABLE>(targetTable) {
    							@Override
    							public AssociationRecord assemble(ColumnedRow columnValueProvider) {
    								LEFTID leftid = leftIdentifierAssembler.assemble(new ColumnedRow() {
    									@Override
    									public <E> E get(Selectable<E> column) {
    										return (E) columnValueProvider.get(leftIdentifierColumnMapping.get(column));
    									}
    								});
    								RIGHTID rightid = rightIdentifierAssembler.assemble(new ColumnedRow() {
    									@Override
    									public <E> E get(Selectable<E> column) {
    										return (E) columnValueProvider.get(rightIdentifierColumnMapping.get(column));
    									}
    								});
    								// we should not return an id if any (both expected in fact) value is null
    								if (leftid == null || rightid == null) {
    									return null;
    								} else {
    									return new AssociationRecord(leftid, rightid);
    								}
    							}

    							@Override
    							public Map<Column<ASSOCIATIONTABLE, ?>, Object> getColumnValues(AssociationRecord id) {
    								Map<Column<LEFTTABLE, ?>, ?> leftValues = leftIdentifierAssembler.getColumnValues((LEFTID) id.getLeft());
    								Map<Column<RIGHTTABLE, ?>, ?> rightValues = rightIdentifierAssembler.getColumnValues((RIGHTID) id.getRight());
    								Map<Column<ASSOCIATIONTABLE, ?>, Object> result = new HashMap<>();
    								leftValues.forEach((key, value) -> result.put(leftIdentifierColumnMapping.get(key), value));
    								rightValues.forEach((key, value) -> result.put(rightIdentifierColumnMapping.get(key), value));
    								return result;
    							}
    						}) {

    					@Override
    					public boolean isNew(AssociationRecord entity) {
    						return !entity.isPersisted();
    					}
    				});
    	}
    }