All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.molcikas.photon.blueprints.EntityBlueprintBuilder Maven / Gradle / Ivy

package com.github.molcikas.photon.blueprints;

import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.collections4.multimap.HashSetValuedHashMap;
import org.apache.commons.lang3.StringUtils;
import com.github.molcikas.photon.converters.Converter;
import com.github.molcikas.photon.Photon;
import com.github.molcikas.photon.exceptions.PhotonException;

import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;

/**
 * The builder for creating aggregate entity blueprints.
 */
public class EntityBlueprintBuilder
{
    private final Photon photon;
    private final EntityBlueprintBuilder parentBuilder;
    private final Class entityClass;
    private final String aggregateBlueprintName;
    private final boolean registerBlueprintForSaving;
    private final List mappedClasses;
    private EntityClassDiscriminator entityClassDiscriminator;
    private final List ignoredFields;
    private final Map childEntityBuilders;
    private final Map customFieldHydraters;

    private final TableBlueprintBuilder tableBlueprintBuilder;
    private final List joinedTableBuilders;
    private boolean mainTableInsertedFirst;

    public Class getEntityClass()
    {
        return entityClass;
    }

    public EntityBlueprintBuilder(Class entityClass, Photon photon)
    {
        this(entityClass, null, true, null, photon);
    }

    public EntityBlueprintBuilder(Class entityClass, EntityBlueprintBuilder parentBuilder, Photon photon)
    {
        this(entityClass, null, false, parentBuilder, photon);
    }

    public EntityBlueprintBuilder(Class entityClass, String aggregateBlueprintName, boolean registerBlueprintForSaving, Photon photon)
    {
        this(entityClass, aggregateBlueprintName,
            registerBlueprintForSaving, null, photon);
    }

    private EntityBlueprintBuilder(Class entityClass, String aggregateBlueprintName, boolean registerBlueprintForSaving, EntityBlueprintBuilder parentBuilder, Photon photon)
    {
        this.entityClass = entityClass;
        this.aggregateBlueprintName = aggregateBlueprintName;
        this.registerBlueprintForSaving = registerBlueprintForSaving;
        this.parentBuilder = parentBuilder;
        this.photon = photon;
        this.mappedClasses = new ArrayList<>();
        this.ignoredFields = new ArrayList<>();
        this.childEntityBuilders = new HashMap<>();
        this.customFieldHydraters = new HashMap<>();
        this.tableBlueprintBuilder = new TableBlueprintBuilder(this, photon.getOptions());
        this.joinedTableBuilders = new ArrayList<>();
        this.mainTableInsertedFirst = true;
    }

    /**
     * Adds a super or sub class's fields to the entity, and auto-maps them to database columns. This can be used
     * to simply include fields in a sub or super class, or can be combined with withClassDiscriminator() to implement
     * single-table inheritance.
     *
     * @param mappedClass - the super or sub class whose fields will all be mapped into the entity
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withMappedClass(Class mappedClass)
    {
        if(mappedClasses.stream().noneMatch(m -> Objects.equals(mappedClass, m.getMappedClass())))
        {
            mappedClasses.add(new MappedClassBlueprint(mappedClass, true, null));
        }
        return this;
    }

    /**
     * Adds a super or sub class's fields to the entity, and auto-maps them to database columns. This can be used
     * to simply include fields in a sub or super class, or can be combined with withClassDiscriminator() to implement
     * single-table inheritance.
     *
     * @param mappedClass - the super or sub class whose fields will be mapped into the entity
     * @param includedFields - the list of fields on the super or sub class to map
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withMappedClass(Class mappedClass, List includedFields)
    {
        mappedClasses.add(new MappedClassBlueprint(mappedClass, false, includedFields));
        return this;
    }

    /**
     * A custom discriminator that can be used to dynamically set the type of entity to construct. Typically, this
     * is used to implement single-table inheritance. The column values are provided to the discriminator.
     *
     * @param entityClassDiscriminator - the discriminator
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withClassDiscriminator(EntityClassDiscriminator entityClassDiscriminator)
    {
        this.entityClassDiscriminator = entityClassDiscriminator;
        return this;
    }

    /**
     * The database table name for the entity. This only needs to be set if the entity name is different than the
     * table name.
     * @param tableName - table name
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withTableName(String tableName)
    {
        tableBlueprintBuilder.withTableName(tableName);
        return this;
    }

    /**
     * The id field for the entity. This only needs to be set if the id field is not "id" or "tableNameId" (where
     * tableName is the name of the table.
     * @param idFieldName - the id field name
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withId(String idFieldName)
    {
        tableBlueprintBuilder.withId(idFieldName);
        return this;
    }

    /**
     * Sets the id field and primary key auto increment in a single method. See the individual setters for details.
     * @param idFieldName - the id field name
     * @param isPrimaryKeyAutoIncrement - whether the primary key is auto incrementing (a.k.a. identity column)
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withId(String idFieldName, boolean isPrimaryKeyAutoIncrement)
    {
        tableBlueprintBuilder.withId(idFieldName, isPrimaryKeyAutoIncrement);
        return this;
    }

    /**
     * Sets the id field, column data type, and primary key auto increment in a single method. See the individual
     * setters for details.
     * @param idFieldName - the id field name
     * @param columnDataType - the column data type for the primary key column
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withId(String idFieldName, ColumnDataType columnDataType)
    {
        tableBlueprintBuilder.withId(idFieldName, columnDataType);
        return this;
    }

    /**
     * Sets the id field, column data type, and primary key auto increment in a single method. See the individual
     * setters for details.
     * @param idFieldName - the id field name
     * @param columnDataType - the column data type for the primary key column
     * @param isPrimaryKeyAutoIncrement - whether the primary key is auto incrementing (a.k.a. identity column)
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withId(String idFieldName, ColumnDataType columnDataType, boolean isPrimaryKeyAutoIncrement)
    {
        tableBlueprintBuilder.withId(idFieldName, columnDataType, isPrimaryKeyAutoIncrement);
        return this;
    }

    /**
     * Sets the primary key as auto incrementing (a.k.a. identity column).
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withPrimaryKeyAutoIncrement()
    {
        tableBlueprintBuilder.withPrimaryKeyAutoIncrement();
        return this;
    }

    public EntityBlueprintBuilder withParentTable(String parentTableName)
    {
        tableBlueprintBuilder.withParentTable(parentTableName);
        return this;
    }

    public EntityBlueprintBuilder withParentTable(String parentTableName, String foreignKeyToParent)
    {
        tableBlueprintBuilder.withParentTable(parentTableName, foreignKeyToParent);
        return this;
    }

    /**
     * Sets the column which is a foreign key to the parent entity.
     *
     * If the entity does not have children, then it is not required that this column is mapped to a field on the
     * entity. However, if it is not mapped, then all rows will be deleted and re-inserted on every save since there
     * would be no way to map the entities to existing rows. If the entity has children, then the foreign key to
     * parent must be mapped to a field.
     *
     * @param foreignKeyToParent - the foreign key to parent column
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withForeignKeyToParent(String foreignKeyToParent)
    {
        tableBlueprintBuilder.withForeignKeyToParent(foreignKeyToParent);
        return this;
    }

    /**
     * Sets the column which is a foreign key to the parent entity.
     *
     * If the entity does not have children, then it is not required that this column is mapped to a field on the
     * entity. However, if it is not mapped, then all rows will be deleted and re-inserted on every save since there
     * would be no way to map the entities to existing rows. If the entity has children, then the foreign key to
     * parent must be mapped to a field.
     *
     * @param foreignKeyToParent - the foreign key to parent column
     * @param columnDataType - the column data type for the the foreign key to parent column
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withForeignKeyToParent(String foreignKeyToParent, ColumnDataType columnDataType)
    {
        tableBlueprintBuilder.withForeignKeyToParent(foreignKeyToParent, columnDataType);
        return this;
    }

    /**
     * Sets up a many-to-many relationship, mapping an aggregate to a list of other aggregates.
     *
     * @param fieldName - the field containing the list of foreign aggregate ids
     * @param foreignTableName - the many-to-many intermediate table
     * @param foreignTableJoinColumnName - the foreign table column that joins back to the aggregate
     * @param foreignTableKeyColumnName - the foreign table column that joins to the foreign aggregate
     * @param foreignTableKeyColumnType - the column data type for the foreign table key column
     * @param fieldListItemClass - the class type for the items in the field list.
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withForeignKeyListToOtherAggregate(
        String fieldName,
        String foreignTableName,
        String foreignTableJoinColumnName,
        String foreignTableKeyColumnName,
        ColumnDataType foreignTableKeyColumnType,
        Class fieldListItemClass)
    {
        tableBlueprintBuilder.withForeignKeyListToOtherAggregate(
            fieldName,
            foreignTableName,
            foreignTableJoinColumnName,
            foreignTableKeyColumnName,
            foreignTableKeyColumnType,
            fieldListItemClass
        );
        return this;
    }

    public EntityBlueprintBuilder withDatabaseColumn(String columnName)
    {
        tableBlueprintBuilder.withDatabaseColumn(columnName, columnName);
        return this;
    }

    /**
     * Sets the column data type for a database column. This only needs to be called if the column is being mapped to
     * a non-default type.
     *
     * @param columnName - the database column name
     * @param columnDataType - the database column data type.
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withDatabaseColumn(String columnName, ColumnDataType columnDataType)
    {
        tableBlueprintBuilder.withDatabaseColumn(columnName, columnDataType);
        return this;
    }

    /**
     * Create a custom field-to-column name mapping. This only needs to be called if the column name is not the same
     * as the field name.
     *
     * @param columnName - the database column name
     * @param fieldName - the entity field name
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withDatabaseColumn(String columnName, String fieldName)
    {
        tableBlueprintBuilder.withDatabaseColumn(columnName, fieldName);
        return this;
    }

    /**
     * Create a custom field-to-column name mapping. This only needs to be called if the column name is not the same
     * as the field name.
     *
     * @param columnName - the database column name
     * @param fieldName - the entity field name
     * @param columnDataType - the column data type
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withDatabaseColumn(String columnName, String fieldName, ColumnDataType columnDataType)
    {
        tableBlueprintBuilder.withDatabaseColumn(columnName, fieldName, columnDataType);
        return this;
    }

    /**
     * Creates a database column that is mapped to and from an entity field via a custom value mapper. This method should
     * be used if a database value does not map directly to a field (e.g. if the value consists of multiple fields
     * or a field on a child entity).
     *
     * @param columnName - the database column name
     * @param columnDataType - the database column data type
     * @param entityFieldValueMapping - the mapper that maps the entity value to and from the database column value
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withDatabaseColumn(String columnName, ColumnDataType columnDataType,
                                                     EntityFieldValueMapping entityFieldValueMapping)
    {
        tableBlueprintBuilder.withDatabaseColumn(columnName, columnDataType, entityFieldValueMapping);
        return this;
    }

    /**
     * Creates database columns that are mapped to and from one or more fields via a custom value mapper. This method
     * should be used if a field(s) maps to more than one database column.
     *
     * @param columnDefinitions - the database columns
     * @param compoundEntityFieldValueMapping - the mapper that maps field value(s) to and from the database values
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withDatabaseColumns(
        List columnDefinitions,
        CompoundEntityFieldValueMapping compoundEntityFieldValueMapping)
    {
        tableBlueprintBuilder.withDatabaseColumns(columnDefinitions, compoundEntityFieldValueMapping);
        return this;
    }

    /**
     * Sets a custom converter for hydrating a database value into an entity field value.
     *
     * @param fieldName - the entity field name
     * @param fieldHydrater - the converter for doing the field hydration
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withFieldHydrater(String fieldName, Converter fieldHydrater)
    {
        customFieldHydraters.put(fieldName, fieldHydrater);
        return this;
    }

    /**
     * Sets a custom converter for serializing an entity field value into a database value.
     *
     * @param columnName - the database column name
     * @param databaseColumnSerializer - the converter for serializing values into the database
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withDatabaseColumnSerializer(String columnName, Converter databaseColumnSerializer)
    {
        tableBlueprintBuilder.withDatabaseColumnSerializer(columnName, databaseColumnSerializer);
        return this;
    }

    /**
     * Ignore a field and prevent it from being auto-mapped to a database column.
     *
     * @param fieldName - the entity field name
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withIgnoredField(String fieldName)
    {
        ignoredFields.add(fieldName);
        return this;
    }

    /**
     * Sets the database order to use for sorting the database entities. Defaults to id ascending. If this entity is
     * not the aggregate root, it is highly recommend to include the table column with each column to avoid
     * ambiguity errors (e.g. "MyTable.myColumn DESC" vs. "myColumn DESC").
     *
     * @param orderBySql - The SQL order by clause (excluding the ORDER BY keywords)
     * @return - builder for chaining
     */
    public EntityBlueprintBuilder withOrderBySql(String orderBySql)
    {
        tableBlueprintBuilder.withOrderBySql(orderBySql);
        return this;
    }

    /**
     * Creates a builder that is used to build the blueprint for a child entity.
     *
     * @param childClass - the child entity class
     * @return - the child builder
     */
    public EntityBlueprintBuilder withChild(Class childClass)
    {
        return new EntityBlueprintBuilder(childClass, this, photon);
    }

    public TableBlueprintBuilder withJoinedTable(String tableName, JoinType joinType)
    {
        return new JoinedTableBlueprintBuilder(null, tableName, joinType, this, photon.getOptions());
    }

    public TableBlueprintBuilder withJoinedTable(Class entityClass, JoinType joinType)
    {
        return new JoinedTableBlueprintBuilder(entityClass, null, joinType, this, photon.getOptions());
    }

    public EntityBlueprintBuilder withMainTableInsertedLast()
    {
        this.mainTableInsertedFirst = false;
        return this;
    }

    public EntityBlueprintBuilder addJoinedTable(TableBlueprintBuilder joinedTableBuilder)
    {
        if(!(joinedTableBuilder instanceof JoinedTableBlueprintBuilder))
        {
            throw new PhotonException("addJoinedTable() parameter must be created using withJoinedTable()");
        }
        joinedTableBuilders.add((JoinedTableBlueprintBuilder) joinedTableBuilder);
        return this;
    }

    /**
     * Completes the builder and registers it as a child of the parent entity.
     *
     * @param fieldName - the field name on the parent that references the child entity.
     * @return - the parent builder for chaining
     */
    public EntityBlueprintBuilder addAsChild(String fieldName)
    {
        if(parentBuilder == null)
        {
            throw new PhotonException("Cannot add child to field '%s' because there is no parent entity.", fieldName);
        }
        if(StringUtils.isBlank(tableBlueprintBuilder.getForeignKeyToParent()))
        {
            throw new PhotonException("Cannot add child to parent field '%s' because the child does not have a foreign key to parent set.", fieldName);
        }
        parentBuilder.addChild(fieldName, this);
        return parentBuilder;
    }

    /**
     * Completes the builder and registers the entity with Photon.
     */
    public void register()
    {
        if(photon == null)
        {
            throw new PhotonException("Cannot register entityBlueprint because it is not the aggregate root.");
        }
        photon.registerBuiltAggregateBlueprint(aggregateBlueprintName, registerBlueprintForSaving, build());
    }

    public EntityBlueprint build()
    {
        boolean isSimpleEntity = childEntityBuilders.isEmpty() && joinedTableBuilders.isEmpty();

        Map childEntities = childEntityBuilders
            .entrySet()
            .stream()
            .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().build()));

        MultiValuedMap reflectedFieldsMap = new HashSetValuedHashMap<>();
        reflectedFieldsMap.putAll(entityClass, Arrays.asList(entityClass.getDeclaredFields()));

        for(MappedClassBlueprint mappedClassBlueprint : mappedClasses)
        {
            List reflectedFieldsForMappedClass = mappedClassBlueprint.getIncludedFields();
            for(Field field : reflectedFieldsForMappedClass)
            {
                if(reflectedFieldsMap.values().stream().noneMatch(c -> c.getName().equals(field.getName())))
                {
                    reflectedFieldsMap.put(mappedClassBlueprint.getMappedClass(), field);
                }
            }
        }

        List fields = reflectedFieldsMap
            .entries()
            .stream()
            .filter(entry -> !ignoredFields.contains(entry.getValue().getName()))
            .map(entry -> new FieldBlueprint(
                entry.getValue(),
                childEntities.get(entry.getValue().getName()),
                tableBlueprintBuilder.getForeignKeyListBlueprints().get(entry.getValue().getName()),
                customFieldHydraters.get(entry.getValue().getName()),
                null,
                null
            ))
            .collect(Collectors.toList());

        List parentTableBlueprints = Collections.emptyList();
        if(parentBuilder != null)
        {
            parentTableBlueprints = parentBuilder
                .getTableBlueprintBuilders()
                .stream()
                .map(TableBlueprintBuilder::getTableName)
                .collect(Collectors.toList());
        }

        TableBlueprint tableBlueprint = tableBlueprintBuilder.build(isSimpleEntity, fields, parentTableBlueprints, null, joinedTableBuilders);
        List joinedTableBlueprints = joinedTableBuilders
            .stream()
            .map(t -> t.build(isSimpleEntity, fields, Collections.singletonList(tableBlueprint.getTableName()), tableBlueprint, joinedTableBuilders))
            .collect(Collectors.toList());

        EntityBlueprint entityBlueprint = new EntityBlueprint(
            entityClass,
            entityClassDiscriminator,
            fields,
            tableBlueprint,
            joinedTableBlueprints,
            mainTableInsertedFirst
        );

        childEntities.values().forEach(e -> e.setMainTableBlueprintParent(entityBlueprint.getTableBlueprintsForInsertOrUpdate()));

        return entityBlueprint;
    }

    private List getTableBlueprintBuilders()
    {
        List tableBlueprintBuilders = new ArrayList<>(joinedTableBuilders.size() + 1);
        tableBlueprintBuilders.add(tableBlueprintBuilder);
        tableBlueprintBuilders.addAll(joinedTableBuilders);
        return tableBlueprintBuilders;
    }

    private void addChild(String fieldName, EntityBlueprintBuilder childEntityBlueprint)
    {
        if(childEntityBuilders.containsKey(fieldName))
        {
            throw new PhotonException("EntityBlueprint already contains a child for field '%s'.", fieldName);
        }
        childEntityBuilders.put(fieldName, childEntityBlueprint);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy