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

is.codion.framework.model.AbstractEntityEditModel Maven / Gradle / Ivy

There is a newer version: 0.18.22
Show newest version
/*
 * This file is part of Codion.
 *
 * Codion is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Codion is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Codion.  If not, see .
 *
 * Copyright (c) 2009 - 2024, Björn Darri Sigurðsson.
 */
package is.codion.framework.model;

import is.codion.common.db.exception.DatabaseException;
import is.codion.common.event.Event;
import is.codion.common.observer.Observer;
import is.codion.common.state.State;
import is.codion.common.state.StateObserver;
import is.codion.common.value.AbstractValue;
import is.codion.common.value.Value;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.domain.entity.Entities;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.EntityDefinition;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.EntityValidator;
import is.codion.framework.domain.entity.attribute.Attribute;
import is.codion.framework.domain.entity.attribute.AttributeDefinition;
import is.codion.framework.domain.entity.attribute.Column;
import is.codion.framework.domain.entity.attribute.ColumnDefinition;
import is.codion.framework.domain.entity.attribute.ForeignKey;
import is.codion.framework.domain.entity.attribute.ForeignKeyDefinition;
import is.codion.framework.domain.entity.attribute.TransientAttributeDefinition;
import is.codion.framework.domain.entity.exception.ValidationException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

import static is.codion.framework.model.EntityEditEvents.*;
import static java.util.Collections.*;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toMap;

/**
 * A default {@link EntityEditModel} implementation
 */
public abstract class AbstractEntityEditModel implements EntityEditModel {

	private static final Logger LOG = LoggerFactory.getLogger(AbstractEntityEditModel.class);

	private final DefaultEditableEntity editable;
	private final Map entitySearchModels = new HashMap<>();

	private final Events events;
	private final States states;

	/**
	 * Instantiates a new {@link AbstractEntityEditModel} based on the given entity type.
	 * @param entityType the type of the entity to base this {@link AbstractEntityEditModel} on
	 * @param connectionProvider the {@link EntityConnectionProvider} instance
	 */
	protected AbstractEntityEditModel(EntityType entityType, EntityConnectionProvider connectionProvider) {
		EntityDefinition entityDefinition = requireNonNull(connectionProvider).entities().definition(requireNonNull(entityType));
		this.editable = new DefaultEditableEntity(entityDefinition, connectionProvider);
		this.states = new States(entityDefinition.readOnly());
		this.events = new Events(states.postEditEvents);
	}

	@Override
	public final Entities entities() {
		return editable.connectionProvider.entities();
	}

	@Override
	public final EntityDefinition entityDefinition() {
		return editable.entityDefinition;
	}

	@Override
	public final String toString() {
		return getClass() + ", " + editable.entity.entityType();
	}

	@Override
	public final State postEditEvents() {
		return states.postEditEvents;
	}

	@Override
	public final State readOnly() {
		return states.readOnly;
	}

	@Override
	public final State insertEnabled() {
		return states.insertEnabled;
	}

	@Override
	public final State updateEnabled() {
		return states.updateEnabled;
	}

	@Override
	public final State updateMultipleEnabled() {
		return states.updateMultipleEnabled;
	}

	@Override
	public final State deleteEnabled() {
		return states.deleteEnabled;
	}

	@Override
	public final EntityType entityType() {
		return editable.entity.entityType();
	}

	@Override
	public final EntityConnectionProvider connectionProvider() {
		return editable.connectionProvider;
	}

	@Override
	public final EntityConnection connection() {
		return connectionProvider().connection();
	}

	@Override
	public final void replace(ForeignKey foreignKey, Collection entities) {
		replaceForeignKey(requireNonNull(foreignKey), requireNonNull(entities));
	}

	@Override
	public final EditableEntity entity() {
		return editable;
	}

	@Override
	public final  EditableValue value(Attribute attribute) {
		return editable.value(attribute);
	}

	@Override
	public final void validate(Attribute attribute) throws ValidationException {
		editable.validator.get().validate(editable.entity, attribute);
	}

	@Override
	public final void validate(Collection entities) throws ValidationException {
		for (Entity entityToValidate : requireNonNull(entities)) {
			validate(entityToValidate);
		}
	}

	@Override
	public final void validate(Entity entity) throws ValidationException {
		if (entity.entityType().equals(entityType())) {
			editable.validator.get().validate(entity);
		}
		else {
			entity.definition().validator().validate(entity);
		}
	}

	@Override
	public final Entity insert() throws DatabaseException, ValidationException {
		return createInsert().prepare().perform().handle().iterator().next();
	}

	@Override
	public final Collection insert(Collection entities) throws DatabaseException, ValidationException {
		return createInsert(entities).prepare().perform().handle();
	}

	@Override
	public final Entity update() throws DatabaseException, ValidationException {
		return createUpdate().prepare().perform().handle().iterator().next();
	}

	@Override
	public final Collection update(Collection entities) throws DatabaseException, ValidationException {
		return createUpdate(entities).prepare().perform().handle();
	}

	@Override
	public final Entity delete() throws DatabaseException {
		return createDelete().prepare().perform().handle().iterator().next();
	}

	@Override
	public final Collection delete(Collection entities) throws DatabaseException {
		return createDelete(entities).prepare().perform().handle();
	}

	@Override
	public final Insert createInsert() throws ValidationException {
		return new DefaultInsert();
	}

	@Override
	public final Insert createInsert(Collection entities) throws ValidationException {
		return new DefaultInsert(entities);
	}

	@Override
	public final Update createUpdate() throws ValidationException {
		return new DefaultUpdate();
	}

	@Override
	public final Update createUpdate(Collection entities) throws ValidationException {
		return new DefaultUpdate(entities);
	}

	@Override
	public final Delete createDelete() {
		return new DefaultDelete();
	}

	@Override
	public final Delete createDelete(Collection entities) {
		return new DefaultDelete(entities);
	}

	@Override
	public EntitySearchModel createForeignKeySearchModel(ForeignKey foreignKey) {
		entityDefinition().foreignKeys().definition(foreignKey);
		Collection> searchable = entities().definition(foreignKey.referencedType()).columns().searchable();
		if (searchable.isEmpty()) {
			throw new IllegalStateException("No searchable columns defined for entity: " + foreignKey.referencedType());
		}

		return EntitySearchModel.builder(foreignKey.referencedType(), connectionProvider())
						.columns(searchable)
						.singleSelection(true)
						.build();
	}

	@Override
	public final EntitySearchModel foreignKeySearchModel(ForeignKey foreignKey) {
		entityDefinition().foreignKeys().definition(foreignKey);
		synchronized (entitySearchModels) {
			// can't use computeIfAbsent here, see comment in SwingEntityEditModel.foreignKeyComboBoxModel()
			EntitySearchModel entitySearchModel = entitySearchModels.get(foreignKey);
			if (entitySearchModel == null) {
				entitySearchModel = createForeignKeySearchModel(foreignKey);
				entitySearchModels.put(foreignKey, entitySearchModel);
			}

			return entitySearchModel;
		}
	}

	@Override
	public final Observer> beforeInsert() {
		return events.beforeInsert.observer();
	}

	@Override
	public final Observer> afterInsert() {
		return events.afterInsert.observer();
	}

	@Override
	public final Observer> beforeUpdate() {
		return events.beforeUpdate.observer();
	}

	@Override
	public final Observer> afterUpdate() {
		return events.afterUpdate.observer();
	}

	@Override
	public final Observer> beforeDelete() {
		return events.beforeDelete.observer();
	}

	@Override
	public final Observer> afterDelete() {
		return events.afterDelete.observer();
	}

	@Override
	public final Observer afterInsertUpdateOrDelete() {
		return events.afterInsertUpdateOrDelete.observer();
	}

	/**
	 * Inserts the given entities into the database using the given connection
	 * @param entities the entities to insert
	 * @param connection the connection to use
	 * @return the inserted entities
	 * @throws DatabaseException in case of a database exception
	 */
	protected Collection insert(Collection entities, EntityConnection connection) throws DatabaseException {
		return requireNonNull(connection).insertSelect(entities);
	}

	/**
	 * Updates the given entities in the database using the given connection
	 * @param entities the entities to update
	 * @param connection the connection to use
	 * @return the updated entities
	 * @throws DatabaseException in case of a database exception
	 */
	protected Collection update(Collection entities, EntityConnection connection) throws DatabaseException {
		return requireNonNull(connection).updateSelect(entities);
	}

	/**
	 * Deletes the given entities from the database using the given connection
	 * @param entities the entities to delete
	 * @param connection the connection to use
	 * @throws DatabaseException in case of a database exception
	 */
	protected void delete(Collection entities, EntityConnection connection) throws DatabaseException {
		requireNonNull(connection).delete(Entity.primaryKeys(entities));
	}

	/**
	 * For every field referencing the given foreign key values, replaces that foreign key instance with
	 * the corresponding entity from {@code values}, useful when attribute
	 * values have been changed in the referenced entity that must be reflected in the edit model.
	 * @param foreignKey the foreign key attribute
	 * @param values the foreign key entities
	 */
	protected void replaceForeignKey(ForeignKey foreignKey, Collection values) {
		Entity currentForeignKeyValue = editable.entity.entity(foreignKey);
		if (currentForeignKeyValue != null) {
			for (Entity replacementValue : values) {
				if (currentForeignKeyValue.equals(replacementValue)) {
					value(foreignKey).clear();
					value(foreignKey).set(replacementValue);
				}
			}
		}
	}

	/**
	 * Notifies that insert is about to be performed
	 * @param entitiesToInsert the entities about to be inserted
	 * @see #beforeInsert()
	 */
	protected final void notifyBeforeInsert(Collection entitiesToInsert) {
		events.beforeInsert.accept(requireNonNull(entitiesToInsert));
	}

	/**
	 * Notifies that insert has been performed
	 * @param insertedEntities the inserted entities
	 * @see #afterInsert()
	 */
	protected final void notifyAfterInsert(Collection insertedEntities) {
		events.afterInsert.accept(requireNonNull(insertedEntities));
	}

	/**
	 * Notifies that update is about to be performed
	 * @param entitiesToUpdate the entities about to be updated
	 * @see #beforeUpdate()
	 */
	protected final void notifyBeforeUpdate(Map entitiesToUpdate) {
		events.beforeUpdate.accept(requireNonNull(entitiesToUpdate));
	}

	/**
	 * Notifies that update has been performed
	 * @param updatedEntities the updated entities
	 * @see #afterUpdate()
	 */
	protected final void notifyAfterUpdate(Map updatedEntities) {
		events.afterUpdate.accept(requireNonNull(updatedEntities));
	}

	/**
	 * Notifies that delete is about to be performed
	 * @param entitiesToDelete the entities about to be deleted
	 * @see #beforeDelete()
	 */
	protected final void notifyBeforeDelete(Collection entitiesToDelete) {
		events.beforeDelete.accept(requireNonNull(entitiesToDelete));
	}

	/**
	 * Notifies that delete has been performed
	 * @param deletedEntities the deleted entities
	 * @see #afterDelete()
	 */
	protected final void notifyAfterDelete(Collection deletedEntities) {
		events.afterDelete.accept(requireNonNull(deletedEntities));
	}

	/**
	 * Maps the given entities and their updated counterparts to their original primary keys,
	 * assumes a single copy of each entity in the given lists.
	 * @param entitiesBeforeUpdate the entities before update
	 * @param entitiesAfterUpdate the entities after update
	 * @return the updated entities mapped to their respective original primary keys
	 */
	private static Map originalPrimaryKeyMap(Collection entitiesBeforeUpdate,
																															 Collection entitiesAfterUpdate) {
		List entitiesAfterUpdateCopy = new ArrayList<>(entitiesAfterUpdate);
		Map keyMap = new HashMap<>(entitiesBeforeUpdate.size());
		for (Entity entity : entitiesBeforeUpdate) {
			keyMap.put(entity.originalPrimaryKey(), findAndRemove(entity.primaryKey(), entitiesAfterUpdateCopy.listIterator()));
		}

		return unmodifiableMap(keyMap);
	}

	private static Entity findAndRemove(Entity.Key primaryKey, ListIterator iterator) {
		while (iterator.hasNext()) {
			Entity current = iterator.next();
			if (current.primaryKey().equals(primaryKey)) {
				iterator.remove();

				return current;
			}
		}

		return null;
	}

	private interface ValueSupplier {
		 T get(AttributeDefinition attributeDefinition);
	}

	private final class DefaultInsert implements Insert {

		private final Collection entities;
		private final boolean activeEntity;

		private DefaultInsert() throws ValidationException {
			this.entities = entityForInsert();
			this.activeEntity = true;
			states.verifyInsertEnabled();
			validate(entities);
		}

		private DefaultInsert(Collection entities) throws ValidationException {
			this.entities = unmodifiableCollection(new ArrayList<>(entities));
			this.activeEntity = false;
			states.verifyInsertEnabled();
			validate(entities);
		}

		private Collection entityForInsert() {
			Entity toInsert = editable.entity.copy().mutable();
			if (toInsert.definition().primaryKey().generated()) {
				toInsert.clearPrimaryKey();
			}

			return singleton(toInsert);
		}

		@Override
		public Task prepare() {
			notifyBeforeInsert(entities);

			return new InsertTask();
		}

		private final class InsertTask implements Task {

			@Override
			public Result perform() throws DatabaseException {
				LOG.debug("{} - insert {}", this, entities);
				Collection inserted = unmodifiableCollection(insert(entities, connection()));
				if (!entities.isEmpty() && inserted.isEmpty()) {
					throw new DatabaseException("Insert did not return an entity, usually caused by a misconfigured key generator");
				}

				return new InsertResult(inserted);
			}
		}

		private final class InsertResult implements Result {

			private final Collection insertedEntities;

			private InsertResult(Collection insertedEntities) {
				this.insertedEntities = insertedEntities;
			}

			@Override
			public Collection handle() {
				if (activeEntity) {
					editable.setOrDefaults(insertedEntities.iterator().next());
				}
				notifyAfterInsert(insertedEntities);

				return insertedEntities;
			}
		}
	}

	private final class DefaultUpdate implements Update {

		private final Collection entities;

		private DefaultUpdate() throws ValidationException {
			entities = singleton(editable.entity.copy().mutable());
			states.verifyUpdateEnabled(entities.size());
			validate(entities);
			verifyModified(entities);
		}

		private DefaultUpdate(Collection entities) throws ValidationException {
			this.entities = unmodifiableCollection(new ArrayList<>(entities));
			states.verifyUpdateEnabled(entities.size());
			validate(entities);
			verifyModified(entities);
		}

		@Override
		public Task prepare() {
			notifyBeforeUpdate(unmodifiableMap(entities.stream().collect(toMap(Entity::originalPrimaryKey, Function.identity()))));

			return new UpdateTask();
		}

		private void verifyModified(Collection entities) {
			for (Entity entityToUpdate : entities) {
				if (!entityToUpdate.modified()) {
					throw new IllegalArgumentException("Entity is not modified: " + entityToUpdate);
				}
			}
		}

		private final class UpdateTask implements Task {

			@Override
			public Result perform() throws DatabaseException {
				LOG.debug("{} - update {}", this, entities);

				return new UpdateResult(update(entities, connection()));
			}
		}

		private final class UpdateResult implements Result {

			private final Collection updatedEntities;

			private UpdateResult(Collection updatedEntities) {
				this.updatedEntities = updatedEntities;
			}

			@Override
			public Collection handle() {
				Entity entity = editable.get();
				updatedEntities.stream()
								.filter(updatedEntity -> updatedEntity.equals(entity))
								.findFirst()
								.ifPresent(editable::setOrDefaults);
				notifyAfterUpdate(originalPrimaryKeyMap(entities, updatedEntities));

				return updatedEntities;
			}
		}
	}

	private final class DefaultDelete implements Delete {

		private final Collection entities;
		private final boolean activeEntity;

		private DefaultDelete() {
			this.entities = singleton(activeEntity());
			this.activeEntity = true;
			states.verifyDeleteEnabled();
		}

		private DefaultDelete(Collection entities) {
			this.entities = unmodifiableCollection(new ArrayList<>(entities));
			this.activeEntity = false;
			states.verifyDeleteEnabled();
		}

		@Override
		public Task prepare() {
			notifyBeforeDelete(entities);

			return new DeleteTask();
		}

		private Entity activeEntity() {
			Entity copy = editable.entity.copy().mutable();
			copy.revert();

			return copy;
		}

		private final class DeleteTask implements Task {

			@Override
			public Result perform() throws DatabaseException {
				LOG.debug("{} - delete {}", this, entities);
				delete(entities, connection());

				return new DeleteResult(entities);
			}
		}

		private final class DeleteResult implements Result {

			private final Collection deletedEntities;

			private DeleteResult(Collection deletedEntities) {
				this.deletedEntities = deletedEntities;
			}

			@Override
			public Collection handle() {
				if (activeEntity) {
					editable.setOrDefaults(null);
				}
				notifyAfterDelete(deletedEntities);

				return deletedEntities;
			}
		}
	}

	private static final class Events {

		private final Event> beforeInsert = Event.event();
		private final Event> afterInsert = Event.event();
		private final Event> beforeUpdate = Event.event();
		private final Event> afterUpdate = Event.event();
		private final Event> beforeDelete = Event.event();
		private final Event> afterDelete = Event.event();
		private final Event afterInsertUpdateOrDelete = Event.event();

		private Events(StateObserver postEditEvents) {
			afterInsert.addListener(afterInsertUpdateOrDelete);
			afterUpdate.addListener(afterInsertUpdateOrDelete);
			afterDelete.addListener(afterInsertUpdateOrDelete);
			afterInsert.addConsumer(insertedEntities -> {
				if (postEditEvents.get()) {
					inserted(insertedEntities);
				}
			});
			afterUpdate.addConsumer(updatedEntities -> {
				if (postEditEvents.get()) {
					updated(updatedEntities);
				}
			});
			afterDelete.addConsumer(deletedEntities -> {
				if (postEditEvents.get()) {
					deleted(deletedEntities);
				}
			});
		}
	}

	private static final class States {

		private final State readOnly;
		private final State insertEnabled = State.state(true);
		private final State updateEnabled = State.state(true);
		private final State updateMultipleEnabled = State.state(true);
		private final State deleteEnabled = State.state(true);
		private final State postEditEvents = State.state(POST_EDIT_EVENTS.get());

		private States(boolean readOnly) {
			this.readOnly = State.state(readOnly);
		}

		private void verifyInsertEnabled() {
			if (readOnly.get() || !insertEnabled.get()) {
				throw new IllegalStateException("Edit model is readOnly or inserting is not enabled!");
			}
		}

		private void verifyUpdateEnabled(int entityCount) {
			if (readOnly.get() || !updateEnabled.get()) {
				throw new IllegalStateException("Edit model is readOnly or updating is not enabled!");
			}
			if (entityCount > 1 && !updateMultipleEnabled.get()) {
				throw new IllegalStateException("Updating multiple entities is not enabled");
			}
		}

		private void verifyDeleteEnabled() {
			if (readOnly.get() || !deleteEnabled.get()) {
				throw new IllegalStateException("Edit model is readOnly or deleting is not enabled!");
			}
		}
	}

	private static final class DefaultEditableEntity implements EditableEntity {

		private final Map, Event> editEvents = new HashMap<>();
		private final Event> valueChanged = Event.event();
		private final Event changing = Event.event();
		private final Event changed = Event.event();

		private final Map, DefaultEditableValue> editableValues = new HashMap<>();
		private final Map, State> persistValues = new HashMap<>();
		private final Map, State> attributeModified = new HashMap<>();
		private final Map, State> attributeNull = new HashMap<>();
		private final Map, State> attributeValid = new HashMap<>();

		private final EntityDefinition entityDefinition;
		private final EntityConnectionProvider connectionProvider;
		private final State primaryKeyNull = State.state(true);
		private final State entityValid = State.state();
		private final DefaultExists exists;
		private final DefaultModified modified;
		private final StateObserver editing;
		private final Value validator;

		private final Entity entity;

		private DefaultEditableEntity(EntityDefinition entityDefinition, EntityConnectionProvider connectionProvider) {
			this.entityDefinition = entityDefinition;
			this.connectionProvider = connectionProvider;
			this.entity = createEntity(AttributeDefinition::defaultValue);
			this.exists = new DefaultExists(entity.definition());
			this.modified = new DefaultModified();
			this.editing = State.and(exists.exists, modified.modified);
			this.validator = Value.builder()
							.nonNull(entityDefinition.validator())
							.listener(this::updateValidState)
							.build();
			configurePersistentForeignKeys();
		}

		@Override
		public void set(Entity entity) {
			changing.accept(entity);
			setOrDefaults(entity);
		}

		@Override
		public Entity get() {
			return entity.immutable();
		}

		@Override
		public void clear() {
			set(entityDefinition.entity());
		}

		@Override
		public Observer observer() {
			return changed.observer();
		}

		@Override
		public void defaults() {
			set(null);
		}

		@Override
		public void refresh() {
			try {
				if (exists.get()) {
					set(connectionProvider.connection().select(entity.primaryKey()));
				}
			}
			catch (DatabaseException e) {
				throw new RuntimeException(e);
			}
		}

		@Override
		public void revert() {
			entityDefinition.attributes().get().forEach(attribute -> value(attribute).revert());
		}

		@Override
		public Exists exists() {
			return exists;
		}

		@Override
		public Modified modified() {
			return modified;
		}

		@Override
		public StateObserver edited() {
			return editing;
		}

		@Override
		public Observer changing() {
			return changing.observer();
		}

		@Override
		public Observer> valueChanged() {
			return valueChanged.observer();
		}

		@Override
		public boolean nullable(Attribute attribute) {
			return validator.get().nullable(entity, attribute);
		}

		@Override
		public StateObserver isNull(Attribute attribute) {
			return attributeNull.computeIfAbsent(attribute,
							k -> State.state(entity.isNull(attribute))).observer();
		}

		@Override
		public StateObserver isNotNull(Attribute attribute) {
			return attributeNull.computeIfAbsent(attribute,
							k -> State.state(entity.isNull(attribute))).observer().not();
		}

		@Override
		public StateObserver primaryKeyNull() {
			return primaryKeyNull.observer();
		}

		@Override
		public Value validator() {
			return validator;
		}

		@Override
		public StateObserver valid() {
			return entityValid.observer();
		}

		@Override
		public void validate() throws ValidationException {
			validate(entity);
		}

		@Override
		public  EditableValue value(Attribute attribute) {
			entityDefinition.attributes().definition(attribute);

			return (EditableValue) editableValues.computeIfAbsent(attribute, k -> new DefaultEditableValue<>(attribute));
		}

		private void setOrDefaults(Entity entity) {
			Map, Object> affectedAttributes = this.entity.set(entity == null ? createEntity(this::defaultValue) : entity);
			for (Attribute affectedAttribute : affectedAttributes.keySet()) {
				notifyValueChange(affectedAttribute);
			}
			if (affectedAttributes.isEmpty()) {//otherwise notifyValueChange() triggers entity state updates
				updateStates();
			}
			attributeModified.forEach(this::updateAttributeModifiedState);

			changed.accept(entity);
		}

		private  T defaultValue(AttributeDefinition attributeDefinition) {
			if (value(attributeDefinition.attribute()).persist().get()) {
				if (attributeDefinition instanceof ForeignKeyDefinition) {
					return (T) entity.entity((ForeignKey) attributeDefinition.attribute());
				}

				return entity.get(attributeDefinition.attribute());
			}

			return value(attributeDefinition.attribute()).defaultValue().get().get();
		}

		private  void notifyValueEdit(Attribute attribute, T value, Map, Object> dependingValues) {
			notifyValueChange(attribute);
			Event editEvent = (Event) editEvents.get(attribute);
			if (editEvent != null) {
				editEvent.accept(value);
			}
			dependingValues.forEach((dependingAttribute, previousValue) -> {
				Object currentValue = entity.get(dependingAttribute);
				if (!Objects.equals(previousValue, currentValue)) {
					notifyValueEdit((Attribute) dependingAttribute, currentValue, emptyMap());
				}
			});
		}

		private void notifyValueChange(Attribute attribute) {
			updateStates();
			updateAttributeStates(attribute);
			DefaultEditableValue editModelValue = editableValues.get(attribute);
			if (editModelValue != null) {
				editModelValue.valueChanged();
			}
			valueChanged.accept(attribute);
		}

		private void updateStates() {
			exists.update();
			modified.update();
			updateValidState();
			updatePrimaryKeyNullState();
		}

		private  void updateAttributeStates(Attribute attribute) {
			State nullState = attributeNull.get(attribute);
			if (nullState != null) {
				nullState.set(entity.isNull(attribute));
			}
			State validState = attributeValid.get(attribute);
			if (validState != null) {
				validState.set(isValid(attribute));
			}
			State modifiedState = attributeModified.get(attribute);
			if (modifiedState != null) {
				updateAttributeModifiedState(attribute, modifiedState);
			}
		}

		private void validate(Entity entity) throws ValidationException {
			if (entity.entityType().equals(entityDefinition.entityType())) {
				validator.get().validate(entity);
			}
			else {
				entity.definition().validator().validate(entity);
			}
		}

		private boolean isValid(Attribute attribute) {
			try {
				validator.get().validate(entity, attribute);
				return true;
			}
			catch (ValidationException e) {
				return false;
			}
		}

		private void updateAttributeModifiedState(Attribute attribute, State modifiedState) {
			modifiedState.set(exists.predicate.get().test(entity) && entity.modified(attribute));
		}

		private void updateValidState() {
			entityValid.set(validator.get().valid(entity));
		}

		private void updatePrimaryKeyNullState() {
			primaryKeyNull.set(entity.primaryKey().isNull());
		}

		/**
		 * Instantiates a new {@link Entity} using the values provided by {@code valueSupplier}.
		 * Values are populated for {@link ColumnDefinition} and its descendants, {@link ForeignKeyDefinition}
		 * and {@link TransientAttributeDefinition} (excluding its descendants).
		 * If a {@link ColumnDefinition}s underlying column has a default value the attribute is
		 * skipped unless the attribute itself has a default value, which then overrides the columns default value.
		 * @return an entity instance populated with default values
		 * @see ColumnDefinition.Builder#columnHasDefaultValue()
		 * @see ColumnDefinition.Builder#defaultValue(Object)
		 */
		private Entity createEntity(ValueSupplier valueSupplier) {
			Entity newEntity = entityDefinition.entity();
			addColumnValues(valueSupplier, newEntity);
			addTransientValues(valueSupplier, newEntity);
			addForeignKeyValues(valueSupplier, newEntity);

			newEntity.save();

			return newEntity;
		}

		private void addColumnValues(ValueSupplier valueSupplier, Entity newEntity) {
			entityDefinition.columns().definitions().stream()
							//these are set via their respective parent foreign key
							.filter(columnDefinition -> !entityDefinition.foreignKeys().foreignKeyColumn(columnDefinition.attribute()))
							.filter(columnDefinition -> !columnDefinition.columnHasDefaultValue() || columnDefinition.hasDefaultValue())
							.map(columnDefinition -> (AttributeDefinition) columnDefinition)
							.forEach(attributeDefinition -> newEntity.put(attributeDefinition.attribute(), valueSupplier.get(attributeDefinition)));
		}

		private void addTransientValues(ValueSupplier valueSupplier, Entity newEntity) {
			entityDefinition.attributes().definitions().stream()
							.filter(TransientAttributeDefinition.class::isInstance)
							.filter(attributeDefinition -> !attributeDefinition.derived())
							.map(attributeDefinition -> (AttributeDefinition) attributeDefinition)
							.forEach(attributeDefinition -> newEntity.put(attributeDefinition.attribute(), valueSupplier.get(attributeDefinition)));
		}

		private void addForeignKeyValues(ValueSupplier valueSupplier, Entity newEntity) {
			entityDefinition.foreignKeys().definitions().forEach(foreignKeyDefinition ->
							newEntity.put(foreignKeyDefinition.attribute(), valueSupplier.get(foreignKeyDefinition)));
		}

		private void configurePersistentForeignKeys() {
			if (PERSIST_FOREIGN_KEYS.get()) {
				entityDefinition.foreignKeys().get().forEach(foreignKey ->
								value(foreignKey).persist().set(foreignKeyWritable(foreignKey)));
			}
		}

		private boolean foreignKeyWritable(ForeignKey foreignKey) {
			return foreignKey.references().stream()
							.map(ForeignKey.Reference::column)
							.map(entityDefinition.columns()::definition)
							.filter(ColumnDefinition.class::isInstance)
							.map(ColumnDefinition.class::cast)
							.anyMatch(columnDefinition -> !columnDefinition.readOnly());
		}

		private final class DefaultExists implements Exists {

			private final State exists = State.state(false);
			private final Value> predicate;

			private DefaultExists(EntityDefinition definition) {
				predicate = Value.builder()
								.nonNull(definition.exists())
								.listener(this::update)
								.build();
			}

			@Override
			public Value> predicate() {
				return predicate;
			}

			@Override
			public StateObserver not() {
				return exists.not();
			}

			@Override
			public Boolean get() {
				return exists.get();
			}

			@Override
			public boolean addListener(Runnable listener) {
				return exists.addListener(listener);
			}

			@Override
			public boolean removeListener(Runnable listener) {
				return exists.removeListener(listener);
			}

			@Override
			public boolean addConsumer(Consumer consumer) {
				return exists.addConsumer(consumer);
			}

			@Override
			public boolean removeConsumer(Consumer consumer) {
				return exists.removeConsumer(consumer);
			}

			@Override
			public boolean addWeakListener(Runnable listener) {
				return exists.addWeakListener(listener);
			}

			@Override
			public boolean removeWeakListener(Runnable listener) {
				return exists.removeWeakListener(listener);
			}

			@Override
			public boolean addWeakConsumer(Consumer consumer) {
				return exists.addWeakConsumer(consumer);
			}

			@Override
			public boolean removeWeakConsumer(Consumer consumer) {
				return exists.removeWeakConsumer(consumer);
			}

			private void update() {
				exists.set(predicate.get().test(entity));
			}
		}

		private final class DefaultModified implements Modified {

			private final State modified = State.state();
			private final Value> predicate = Value.builder()
							.nonNull((Predicate) Entity::modified)
							.listener(this::update)
							.build();

			@Override
			public Value> predicate() {
				return predicate;
			}

			@Override
			public StateObserver not() {
				return modified.not();
			}

			@Override
			public Boolean get() {
				return modified.get();
			}

			@Override
			public void update() {
				modified.set(predicate.get().test(entity));
			}

			@Override
			public boolean addListener(Runnable listener) {
				return modified.addListener(listener);
			}

			@Override
			public boolean removeListener(Runnable listener) {
				return modified.removeListener(listener);
			}

			@Override
			public boolean addConsumer(Consumer consumer) {
				return modified.addConsumer(consumer);
			}

			@Override
			public boolean removeConsumer(Consumer consumer) {
				return modified.removeConsumer(consumer);
			}

			@Override
			public boolean addWeakListener(Runnable listener) {
				return modified.addWeakListener(listener);
			}

			@Override
			public boolean removeWeakListener(Runnable listener) {
				return modified.removeWeakListener(listener);
			}

			@Override
			public boolean addWeakConsumer(Consumer consumer) {
				return modified.addWeakConsumer(consumer);
			}

			@Override
			public boolean removeWeakConsumer(Consumer consumer) {
				return modified.removeWeakConsumer(consumer);
			}
		}

		private final class DefaultEditableValue extends AbstractValue implements EditableValue {

			private final Attribute attribute;
			private final Value> defaultValue;

			private DefaultEditableValue(Attribute attribute) {
				this.attribute = attribute;
				this.defaultValue = Value.builder()
									.nonNull((Supplier) entityDefinition.attributes().definition(attribute)::defaultValue)
									.build();
			}

			@Override
			public void revert() {
				if (modified().get()) {
					super.set(entity.original(attribute));
				}
			}


			@Override
			public State persist() {
				return persistValues.computeIfAbsent(attribute, k -> State.state());
			}

			@Override
			public StateObserver valid() {
				return attributeValid.computeIfAbsent(attribute,
								k -> State.state(isValid(attribute))).observer();
			}

			@Override
			public StateObserver modified() {
				return attributeModified.computeIfAbsent(attribute,
								k -> State.state(exists.get() && entity.modified(attribute))).observer();
			}

			@Override
			public Observer edited() {
				return ((Event) editEvents.computeIfAbsent(attribute, k -> Event.event())).observer();
			}

			@Override
			public Value> defaultValue() {
				return defaultValue;
			}

			@Override
			protected T getValue() {
				return entity.get(attribute);
			}

			@Override
			protected void setValue(T value) {
				Map, Object> dependingValues = dependingValues(attribute);
				T previousValue = entity.put(attribute, value);
				if (!Objects.equals(value, previousValue)) {
					notifyValueEdit(attribute, value, dependingValues);
				}
			}

			private Map, Object> dependingValues(Attribute attribute) {
				return dependingValues(attribute, new LinkedHashMap<>());
			}

			private Map, Object> dependingValues(Attribute attribute, Map, Object> dependingValues) {
				addDependingDerivedAttributes(attribute, dependingValues);
				if (attribute instanceof Column) {
					addDependingForeignKeys((Column) attribute, dependingValues);
				}
				else if (attribute instanceof ForeignKey) {
					addDependingReferencedColumns((ForeignKey) attribute, dependingValues);
				}

				return dependingValues;
			}

			private void addDependingDerivedAttributes(Attribute attribute, Map, Object> dependingValues) {
				entityDefinition.attributes().derivedFrom(attribute).forEach(derivedAttribute -> {
					dependingValues.put(derivedAttribute, entity.get(derivedAttribute));
					addDependingDerivedAttributes(derivedAttribute, dependingValues);
				});
			}

			private void addDependingForeignKeys(Column column, Map, Object> dependingValues) {
				entityDefinition.foreignKeys().definitions(column).forEach(foreignKeyDefinition ->
								dependingValues.put(foreignKeyDefinition.attribute(), entity.get(foreignKeyDefinition.attribute())));
			}

			private void addDependingReferencedColumns(ForeignKey foreignKey, Map, Object> dependingValues) {
				foreignKey.references().forEach(reference ->
								dependingValues.put(reference.column(), entity.get(reference.column())));
			}

			private void valueChanged() {
				notifyListeners();
			}
		}
	}
}