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

com.jetbrains.teamsys.dnq.database.TransientSessionImpl Maven / Gradle / Ivy

/**
 * Copyright 2006 - 2017 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.jetbrains.teamsys.dnq.database;

import jetbrains.exodus.ByteIterable;
import jetbrains.exodus.ExodusException;
import jetbrains.exodus.core.dataStructures.Pair;
import jetbrains.exodus.core.dataStructures.decorators.HashMapDecorator;
import jetbrains.exodus.core.dataStructures.decorators.HashSetDecorator;
import jetbrains.exodus.core.dataStructures.decorators.QueueDecorator;
import jetbrains.exodus.core.dataStructures.hash.HashSet;
import jetbrains.exodus.database.*;
import jetbrains.exodus.database.exceptions.*;
import jetbrains.exodus.entitystore.*;
import jetbrains.exodus.env.ReadonlyTransactionException;
import jetbrains.exodus.query.UniqueKeyIndicesEngine;
import jetbrains.exodus.query.metadata.EntityMetaData;
import jetbrains.exodus.query.metadata.Index;
import jetbrains.exodus.query.metadata.IndexField;
import jetbrains.exodus.query.metadata.ModelMetaData;
import jetbrains.exodus.util.ByteArraySizedInputStream;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;

/**
 */
public class TransientSessionImpl implements TransientStoreSession {

    protected static final Logger logger = LoggerFactory.getLogger(TransientSessionImpl.class);

    private static final String CHILD_TO_PARENT_LINK_NAME = "__CHILD_TO_PARENT_LINK_NAME__";
    private static final String PARENT_TO_CHILD_LINK_NAME = "__PARENT_TO_CHILD_LINK_NAME__";

    protected final TransientEntityStoreImpl store;
    protected final boolean readonly;
    @Nullable
    protected ReadonlyPersistentStoreTransaction txnWhichWasUpgraded;
    protected Runnable upgradeHook;
    protected State state;
    protected boolean quietFlush = false;
    protected TransientChangesTracker changesTracker;
    // stores transient entities that were created for loaded persistent entities to avoid double loading
    private final Map managedEntities;
    private final Queue changes = new QueueDecorator();
    private final int hashCode = (int) (Math.random() * Integer.MAX_VALUE);
    private boolean allowRunnables = true;
    private Throwable stack = null;
    private boolean flushing = false;

    protected TransientSessionImpl(final TransientEntityStoreImpl store, boolean readonly) {
        this.store = store;
        this.readonly = readonly;
        txnWhichWasUpgraded = null;
        this.store.getPersistentStore().beginReadonlyTransaction();
        this.state = State.Open;
        this.managedEntities = new HashMapDecorator();
        if (LoggerFactory.getLogger(TransientEntityStoreImpl.class).isDebugEnabled()) {
            stack = new Throwable();
        }
        initChangesTracker(true);
    }

    public Throwable getStack() {
        return stack;
    }

    private TransientChangesTracker initChangesTracker(boolean readonly) {
        if (this.changesTracker != null) changesTracker.dispose();
        TransientChangesTracker oldChangesTracker = changesTracker;
        final PersistentStoreTransaction snapshot = getSnapshot();
        this.changesTracker = readonly ? new ReadOnlyTransientChangesTrackerImpl(snapshot) : new TransientChangesTrackerImpl(snapshot);

        return oldChangesTracker;
    }

    public void setQueryCancellingPolicy(QueryCancellingPolicy policy) {
        getPersistentTransactionInternal().setQueryCancellingPolicy(policy);
    }

    public QueryCancellingPolicy getQueryCancellingPolicy() {
        return getPersistentTransactionInternal().getQueryCancellingPolicy();
    }

    @NotNull
    public TransientEntityStore getStore() {
        return store;
    }

    @Override
    public boolean isIdempotent() {
        return getPersistentTransaction().isIdempotent();
    }

    @Override
    public boolean isReadonly() {
        return readonly;
    }

    @Override
    public boolean isFinished() {
        return getPersistentTransaction().isFinished();
    }

    @Override
    public void setUpgradeHook(@Nullable final Runnable hook) {
        upgradeHook = hook;
    }

    protected PersistentStoreTransaction getPersistentTransactionInternal() {
        return (PersistentStoreTransaction) store.getPersistentStore().getCurrentTransaction();
    }

    protected void upgradeReadonlyTransactionIfNecessary() {
        final StoreTransaction currentTxn = getPersistentTransactionInternal();
        if (!readonly && currentTxn.isReadonly()) {
            final PersistentEntityStoreImpl persistentStore = getPersistentStore();
            if (persistentStore.getEnvironment().getEnvironmentConfig().getEnvIsReadonly()) {
                throw new ReadonlyTransactionException("Can't upgrade transient transaction in read-only mode");
            }
            if (upgradeHook != null) {
                upgradeHook.run();
            }
            final ReadonlyPersistentStoreTransaction roTxn = (ReadonlyPersistentStoreTransaction) currentTxn;
            final PersistentStoreTransaction newTxn = roTxn.getUpgradedTransaction();
            TxnUtil.registerTransation(persistentStore, newTxn);
            this.changesTracker = this.changesTracker.upgrade();
            txnWhichWasUpgraded = roTxn;
        }
    }

    public String toString() {
        return "transaction [" + hashCode() + "] state [" + state + "]";
    }

    @Override
    public int hashCode() {
        return hashCode;
    }

    @Override
    public boolean equals(Object obj) {
        return obj == this;
    }

    @Override
    public boolean hasChanges() {
        return !changes.isEmpty();
    }

    public boolean isOpened() {
        return state == State.Open;
    }

    public boolean isCommitted() {
        return state == State.Committed;
    }

    public boolean isAborted() {
        return state == State.Aborted;
    }

    void assertOpen(final String action) {
        if (state != State.Open) {
            throw new IllegalStateException("Can't " + action + " in state [" + state + "]");
        }
    }

    @NotNull
    public EntityIterable createPersistentEntityIterableWrapper(@NotNull EntityIterable wrappedIterable) {
        assertOpen("create wrapper");
        // do not wrap twice
        if (wrappedIterable instanceof PersistentEntityIterableWrapper) {
            return wrappedIterable;
        } else {
            return new PersistentEntityIterableWrapper(store, wrappedIterable);
        }
    }

    @Override
    public void revert() {
        if (logger.isDebugEnabled()) {
            logger.debug("Revert transient session " + this);
        }
        assertOpen("revert");

        PersistentStoreTransaction txn = (PersistentStoreTransaction) getPersistentTransactionInternal();
        if (!txn.isReadonly()) {
            managedEntities.clear();
            changes.clear();
        }
        closePersistentSession();
        this.store.getPersistentStore().beginReadonlyTransaction();
        initChangesTracker(true);
    }

    @Override
    public boolean flush() {
        if (store.getThreadSession() != this) {
            throw new IllegalStateException("Can't commit session from another thread.");
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Intermidiate commit transient session " + this);
        }

        assertOpen("flush");

        if (changes.isEmpty()) {
            logger.trace("Nothing to flush.");
        } else {
            flushChanges();
            changes.clear();

            for (TransientEntity r : changesTracker.getRemovedEntities()) {
                managedEntities.remove(r.getId());
            }

            final TransientChangesTracker oldChangesTracker = changesTracker;
            closePersistentSession();
            this.store.getPersistentStore().beginReadonlyTransaction();
            this.changesTracker = new ReadOnlyTransientChangesTrackerImpl(getSnapshot());
            notifyFlushedListeners(oldChangesTracker);
        }
        return true;
    }

    public boolean commit() {
        // flush until no side-effects from listeners
        do {
            flush();
        } while (!changes.isEmpty());

        try {
            changesTracker.dispose();
        } finally {
            try {
                closePersistentSession();
            } finally {
                store.unregisterStoreSession(this);
                state = State.Committed;
            }
        }
        return true;
    }

    public void abort() {
        if (store.getThreadSession() != this) {
            throw new IllegalStateException("Can't abort session that is not current thread session. Current thread session is [" + store.getThreadSession() + "]");
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Abort transient session " + this);
        }
        assertOpen("abort");
        try {
            changesTracker.dispose();
        } finally {
            try {
                closePersistentSession();
            } finally {
                store.unregisterStoreSession(this);
                state = State.Aborted;
            }
        }
    }

    @NotNull
    public StoreTransaction getPersistentTransaction() {
        assertOpen("get persistent transaction");
        return getPersistentTransactionInternal();
    }


    /**
     * Creates transient wrapper for existing persistent entity
     *
     * @param persistent
     * @return
     */
    @NotNull
    public TransientEntity newEntity(@NotNull Entity persistent) {
        if (persistent instanceof TransientEntity) {
            throw new IllegalArgumentException("Can't create transient entity wrapper for another transient entity.");
        }
        assertOpen("create entity");
        return newEntityImpl((PersistentEntity) persistent);
    }

    @NotNull
    public Entity getEntity(@NotNull final EntityId id) {
        assertOpen("get entity");
        TransientEntity e = managedEntities.get(id);
        if (e == null) {
            return newEntity(changesTracker.getSnapshot().getEntity(id));
        } else {
            return e;
        }
    }

    @NotNull
    public EntityId toEntityId(@NotNull final String representation) {
        assertOpen("convert to entity id");
        return getPersistentTransactionInternal().toEntityId(representation);
    }

    @NotNull
    public List getEntityTypes() {
        assertOpen("get entity types");
        return getPersistentTransactionInternal().getEntityTypes();
    }

    @NotNull
    public EntityIterable getAll(@NotNull final String entityType) {
        assertOpen("getAll");
        return new PersistentEntityIterableWrapper(store, getPersistentTransactionInternal().getAll(entityType));
    }

    @NotNull
    public EntityIterable getSingletonIterable(@NotNull final Entity entity) {
        assertOpen("getSingletonIterable");
        return new PersistentEntityIterableWrapper(store, getPersistentTransactionInternal().getSingletonIterable(((TransientEntityImpl) entity).getPersistentEntity()));
    }

    @NotNull
    public EntityIterable find(@NotNull final String entityType, @NotNull final String propertyName, @NotNull final Comparable value) {
        assertOpen("find");
        return new PersistentEntityIterableWrapper(store, getPersistentTransactionInternal().find(entityType, propertyName, value));
    }

    @NotNull
    public EntityIterable find(@NotNull final String entityType, @NotNull final String propertyName, @NotNull final Comparable minValue, @NotNull final Comparable maxValue) {
        assertOpen("find");
        return new PersistentEntityIterableWrapper(store, getPersistentTransactionInternal().find(entityType, propertyName, minValue, maxValue));
    }

    @NotNull
    @Override
    public EntityIterable findIds(@NotNull String entityType, long minValue, long maxValue) {
        assertOpen("findIds");
        return new PersistentEntityIterableWrapper(store, getPersistentTransactionInternal().findIds(entityType, minValue, maxValue));
    }

    public EntityIterable findWithProp(@NotNull final String entityType, @NotNull final String propertyName) {
        assertOpen("findWithProp");
        return new PersistentEntityIterableWrapper(store, getPersistentTransactionInternal().findWithProp(entityType, propertyName));
    }

    @NotNull
    public EntityIterable findStartingWith(@NotNull final String entityType, @NotNull final String propertyName, @NotNull final String value) {
        assertOpen("startsWith");
        return new PersistentEntityIterableWrapper(store, getPersistentTransactionInternal().findStartingWith(entityType, propertyName, value));
    }

    @NotNull
    public EntityIterable findWithBlob(@NotNull final String entityType, @NotNull final String propertyName) {
        assertOpen("findWithBlob");
        return new PersistentEntityIterableWrapper(store, getPersistentTransactionInternal().findWithBlob(entityType, propertyName));
    }

    @NotNull
    public EntityIterable findLinks(@NotNull final String entityType, @NotNull final Entity entity, @NotNull final String linkName) {
        assertOpen("findLinks");
        return new PersistentEntityIterableWrapper(store, getPersistentTransactionInternal().findLinks(entityType, entity, linkName));
    }

    @NotNull
    public EntityIterable findLinks(@NotNull String entityType, @NotNull EntityIterable entities, @NotNull String linkName) {
        assertOpen("findLinks");
        return new PersistentEntityIterableWrapper(store, getPersistentTransactionInternal().findLinks(entityType, entities, linkName));
    }

    @NotNull
    public EntityIterable findWithLinks(@NotNull String entityType, @NotNull String linkName) {
        assertOpen("findWithLinks");
        return new PersistentEntityIterableWrapper(store, getPersistentTransactionInternal().findWithLinks(entityType, linkName));
    }

    @NotNull
    public EntityIterable findWithLinks(@NotNull String entityType,
                                        @NotNull String linkName,
                                        @NotNull String oppositeEntityType,
                                        @NotNull String oppositeLinkName) {
        assertOpen("findWithLinks");
        return new PersistentEntityIterableWrapper(store,
                getPersistentTransactionInternal().findWithLinks(entityType, linkName, oppositeEntityType, oppositeLinkName));
    }

    @NotNull
    public EntityIterable sort(@NotNull final String entityType,
                               @NotNull final String propertyName,
                               final boolean ascending) {
        assertOpen("sort");
        return new PersistentEntityIterableWrapper(store, getPersistentTransactionInternal().sort(entityType, propertyName, ascending));
    }

    @NotNull
    public EntityIterable sort(@NotNull final String entityType,
                               @NotNull final String propertyName,
                               @NotNull final EntityIterable rightOrder,
                               final boolean ascending) {
        assertOpen("sort");
        return new PersistentEntityIterableWrapper(store,
                getPersistentTransactionInternal().sort(entityType, propertyName, rightOrder, ascending));
    }

    @NotNull
    public EntityIterable sortLinks(@NotNull final String entityType,
                                    @NotNull final EntityIterable sortedLinks,
                                    boolean isMultiple,
                                    @NotNull final String linkName,
                                    final @NotNull EntityIterable rightOrder) {
        assertOpen("sortLinks");
        return new PersistentEntityIterableWrapper(store,
                getPersistentTransactionInternal().sortLinks(entityType, sortedLinks, isMultiple, linkName, rightOrder));
    }

    @NotNull
    public EntityIterable sortLinks(@NotNull final String entityType,
                                    @NotNull final EntityIterable sortedLinks,
                                    boolean isMultiple,
                                    @NotNull final String linkName,
                                    final @NotNull EntityIterable rightOrder,
                                    @NotNull final String oppositeEntityType,
                                    @NotNull final String oppositeLinkName) {
        assertOpen("sortLinks");
        return new PersistentEntityIterableWrapper(store,
                getPersistentTransactionInternal().sortLinks(entityType, sortedLinks, isMultiple, linkName, rightOrder, oppositeEntityType, oppositeLinkName));
    }

    @NotNull
    public EntityIterable mergeSorted(@NotNull List sorted, @NotNull Comparator comparator) {
        assertOpen("mergeSorted");
        return new PersistentEntityIterableWrapper(store, getPersistentTransactionInternal().mergeSorted(sorted, comparator));
    }

    @NotNull
    public Sequence getSequence(@NotNull final String sequenceName) {
        assertOpen("get sequence");
        return getPersistentTransactionInternal().getSequence(sequenceName);
    }

    protected void closePersistentSession() {
        if (logger.isDebugEnabled()) {
            logger.debug("Close persistent session for transient session " + this);
        }
        StoreTransaction persistentTxn = getPersistentTransactionInternal();
        if (persistentTxn != null) {
            persistentTxn.abort();
        }
        persistentTxn = txnWhichWasUpgraded;
        if (persistentTxn != null) {
            persistentTxn.abort();
            txnWhichWasUpgraded = null;
        }
    }

    public void quietIntermediateCommit() {
        final boolean qf = quietFlush;
        try {
            this.quietFlush = true;
            flush();
        } finally {
            this.quietFlush = qf;
        }
    }

    @NotNull
    public TransientChangesTracker getTransientChangesTracker() throws IllegalStateException {
        assertOpen("get changes tracker");
        return changesTracker;
    }


    private void replayChanges() {
        initChangesTracker(false);
        // some of manages entities could be deleted
        managedEntities.clear();

        for (MyRunnable c : changes) {
            c.run();
        }
    }

    /**
     * Removes orphans (entities without parents) or returns OrphanException to throw later.
     */
    @NotNull
    private Set removeOrphans() {
        Set orphans = new HashSetDecorator();
        final ModelMetaData modelMetaData = store.getModelMetaData();

        if (modelMetaData == null) {
            return orphans;
        }

        for (TransientEntity e : new ArrayList(changesTracker.getChangedEntities())) {
            if (!e.isRemoved()) {
                EntityMetaData emd = modelMetaData.getEntityMetaData(e.getType());

                if (emd != null && emd.hasAggregationChildEnds() && !EntityMetaDataUtils.hasParent(emd, e, changesTracker)) {
                    if (emd.getRemoveOrphan()) {
                        // has no parent - remove
                        if (logger.isDebugEnabled()) {
                            logger.debug("Remove orphan: " + e);
                        }

                        // we don't want this change to be repeated on flush
                        deleteEntityInternal(e);
                    } else {
                        // has no parent, but orphans shouldn't be removed automatically - exception
                        orphans.add(new OrphanChildException(e, emd.getAggregationChildEnds()));
                    }
                }
            }
        }

        return orphans;
    }

    /**
     * Creates new transient entity
     *
     * @param entityType
     * @return
     */
    @NotNull
    public TransientEntity newEntity(@NotNull final String entityType) {
        assertOpen("create entity");
        upgradeReadonlyTransactionIfNecessary();
        return new TransientEntityImpl(entityType, this.getStore());
    }

    @NotNull
    @Override
    public TransientEntity newEntity(@NotNull final EntityCreator creator) {
        final Entity found = creator.find();
        if (found != null) {
            addEntityCreator((TransientEntityImpl) found, creator);
            return (TransientEntity) found;
        }
        upgradeReadonlyTransactionIfNecessary();
        return new TransientEntityImpl(creator, this.getStore());
    }

    /**
     * Creates local copy of given entity in current session.
     *
     * @param entity
     * @return
     */
    @NotNull
    public TransientEntity newLocalCopy(@NotNull final TransientEntity entity) {
        assertOpen("create local copy");
        if (entity.isReadonly() || entity.isWrapper()) {
            return entity;
        } else if (
            // optimization: inlined entity.isRemoved()
                changesTracker.isRemoved(entity)) {
            EntityRemovedException entityRemovedException = new EntityRemovedException(entity);
            logger.warn("Entity [" + entity + "] was removed by you.");
            throw entityRemovedException;
        } else if (
            // optimization: inlined entity.isNew()
                changesTracker.isNew(entity)) {
            final EntityId entityId = entity.getId();
            if (managedEntities.get(entityId) == entity) {
                // was created in this session and session wasn't reverted
                return entity;
            }
            throw new IllegalStateException("Entity in state New was not created in this session. " + entity);
        } else if (
            // optimization: inlined entity.isSaved()
                changesTracker.isSaved(entity)) {
            final EntityId entityId = entity.getId();
            final TransientEntity localCopy = managedEntities.get(entityId);
            if (localCopy == entity) {
                // was created in this session and session wasn't reverted
                return entity;
            }
            // saved entity from another session or from reverted session - load it from database by id
            // local copy already created?
            if (localCopy != null) {
                if (
                    // optimization: inlined localCopy.isRemoved()
                        changesTracker.isRemoved(localCopy)) {
                    EntityRemovedException entityRemovedException = new EntityRemovedException(entity);
                    logger.warn("Local copy of entity [" + entity + "] was removed by you.");
                    throw entityRemovedException;
                }
                return localCopy;
            }
            try {
                // load persistent entity from database by id
                return newEntity(getPersistentTransactionInternal().getEntity(entityId));
            } catch (EntityRemovedInDatabaseException e) {
                logger.warn("Entity [" + entity + "] was removed in database, can't create local copy.");
                throw e;
            }
        } else {
            throw new IllegalStateException("Can't create local copy of entity (unexpected state) [" + entity + "]");
        }
    }

    /**
     * Checks if entity entity was removed in this transaction or in database
     *
     * @param entity
     * @return true if e was removed, false if it wasn't removed at all
     */
    public boolean isRemoved(@NotNull final Entity entity) {
        EntityId entityId = null;
        if (entity instanceof TransientEntity && state == State.Open) {
            final TransientEntity transientEntity = (TransientEntity) entity;
            if (transientEntity.isWrapper()) {
                return transientEntity.isRemoved();
            }
            // transientEntity.isSaved() inlined:
            if (changesTracker.isSaved(transientEntity)) {
                // saved entity from another session or from reverted session
                entityId = entity.getId();
                final TransientEntity localCopy = managedEntities.get(entityId);
                if (localCopy != transientEntity) {
                    // local copy already created?
                    if (localCopy != null &&
                            // localCopy.isRemoved() inlined:
                            changesTracker.isRemoved(localCopy)) {
                        return true;
                    }
                }
            } else if (
                // transientEntity.isRemoved() inlined:
                    changesTracker.isRemoved(transientEntity)) {
                return true;
            } else if (transientEntity.isReadonly() ||
                    // transientEntity.isNew() inlined:
                    changesTracker.isNew(transientEntity)) {
                return false;
            }
        }
        // load persistent entity from database by id
        if (entityId == null) {
            entityId = entity.getId();
        }
        final PersistentStoreTransaction persistentTxn = getPersistentTransactionInternal();
        return getPersistentStore().getLastVersion(persistentTxn, entityId) < 0;
    }

    /**
     * Checks constraints before save changes
     */
    private void checkBeforeSaveChangesConstraints() {
        final Set exceptions = removeOrphans();

        final ModelMetaData modelMetaData = store.getModelMetaData();

        if (quietFlush || /* for tests only */ modelMetaData == null) {
            if (logger.isWarnEnabled()) {
                logger.warn("Quiet intermediate commit: skip before save changes constraints checking. " + this);
            }
            return;
        }

        if (logger.isTraceEnabled()) {
            logger.trace("Check before save changes constraints. " + this);
        }

        // 0. check incoming links for deleted entities
        exceptions.addAll(ConstraintsUtil.checkIncomingLinks(changesTracker));

        // 1. check associations cardinality
        exceptions.addAll(ConstraintsUtil.checkAssociationsCardinality(changesTracker, modelMetaData));

        // 2. check required properties
        exceptions.addAll(ConstraintsUtil.checkRequiredProperties(changesTracker, modelMetaData));

        // 3. check other property constraints
        exceptions.addAll(ConstraintsUtil.checkOtherPropertyConstraints(changesTracker, modelMetaData));

        // 4. check index fields
        exceptions.addAll(ConstraintsUtil.checkIndexFields(changesTracker, modelMetaData));

        if (exceptions.size() != 0) {
            ConstraintsValidationException e = new ConstraintsValidationException(exceptions);
            store.forAllListeners(new TransientEntityStoreImpl.ListenerVisitor() {
                public void visit(TransientStoreSessionListener listener) {
                    try {
                        listener.afterConstraintsFail(TransientSessionImpl.this, exceptions);
                    } catch (Exception e) {
                        if (logger.isErrorEnabled()) {
                            logger.error("Exception while inside listener [" + listener + "]", e);
                        }
                        // do not rethrow exception, because we are after constaints fail
                    }
                }
            });
            throw e;
        }
    }

    /**
     * Checks custom flush constraints before save changes
     */
    private void executeBeforeFlushTriggers(Set changedEntities) {
        final ModelMetaData modelMetaData = store.getModelMetaData();

        if (quietFlush || /* for tests only */ modelMetaData == null) {
            if (logger.isDebugEnabled()) {
                logger.warn("Quiet intermediate commit: skip before flush triggers. " + this);
            }
            return;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Execute before flush triggers. " + this);
        }

        final Set exceptions = new HashSetDecorator();
        for (TransientEntity entity : changedEntities) {
            if (!entity.isRemoved()) {
                EntityMetaData md = modelMetaData.getEntityMetaData(entity.getType());

                // meta-data may be null for persistent enums
                if (md != null) {
                    try {
                        TransientStoreUtil.getPersistentClassInstance(entity).executeBeforeFlushTrigger(entity);
                    } catch (ConstraintsValidationException cve) {
                        for (DataIntegrityViolationException dive : cve.getCauses()) {
                            exceptions.add(dive);
                        }
                    }
                }
            }
        }

        if (exceptions.size() != 0) {
            ConstraintsValidationException e = new ConstraintsValidationException(exceptions);
            throw e;
        }
    }

    /**
     * Flushes changes
     */
    private void flushChanges() {
        if (flushing) {
            throw new IllegalStateException("Transaction is already being flushed!");
        }
        try {
            flushing = true;
            beforeFlush();
            checkBeforeSaveChangesConstraints();

            notifyBeforeFlushAfterConstraintsCheckListeners();

            final StoreTransaction txn = getPersistentTransactionInternal();
            if (txn.isIdempotent()) {
                return;
            }

            try {
                prepare();
                store.flushLock.lock();
                try {
                    while (true) {
                        if (txn.flush()) {
                            return;
                        }
                        // replay changes
                        replayChanges();
                        //recheck constraints against new database root
                        checkBeforeSaveChangesConstraints();
                        prepare();
                    }
                } finally {
                    store.flushLock.unlock();
                }
            } catch (Throwable exception) {
                if (logger.isInfoEnabled()) {
                    logger.info("Catch exception in flush: " + exception.getMessage());
                }
                if (exception instanceof DataIntegrityViolationException) {
                    txn.revert();
                    replayChanges();
                }
                decodeException(exception);
                throw new IllegalStateException("should never be thrown");
            }
        } finally {
            flushing = false;
        }
    }

    private void prepare() {
        try {
            allowRunnables = false;
            flushIndexes();
        } finally {
            allowRunnables = true;
        }

        if (logger.isTraceEnabled()) {
            logger.trace("Flush persistent transaction in transient session " + this);
        }
    }

    public PersistentStoreTransaction getSnapshot() {
        return ((PersistentStoreTransaction) getPersistentTransaction()).getSnapshot();
    }

    private void flushIndexes() {
        if (TransientStoreUtil.isPostponeUniqueIndexes()) {
            return;
        }

        List> uniqueKeyDeletions = new ArrayList>();
        List> uniqueKeyInsertions = new ArrayList>();

        for (TransientEntity e : changesTracker.getChangedEntities()) {
            if (!e.isRemoved()) {
                // create/update
                Set dirtyIndeces = new HashSetDecorator();
                final Set changedProperties = changesTracker.getChangedProperties(e);
                if (changedProperties != null) {
                    for (String propertyName : changedProperties) {
                        final Set indices = getMetadataIndexes(e, propertyName);
                        if (indices != null) {
                            dirtyIndeces.addAll(indices);
                        }
                    }
                }

                final Map changedLinksDetailed = changesTracker.getChangedLinksDetailed(e);
                if (changedLinksDetailed != null) {
                    for (String propertyName : changedLinksDetailed.keySet()) {
                        final Set indices = getMetadataIndexes(e, propertyName);
                        if (indices != null) {
                            dirtyIndeces.addAll(indices);
                        }
                    }
                }

                for (Index index : dirtyIndeces) {
                    Pair entry = new Pair(e, index);
                    if (!e.isNew()) {
                        uniqueKeyDeletions.add(entry);
                    }
                    uniqueKeyInsertions.add(entry);
                }
            }
        }

        final PersistentStoreTransaction persistentTransaction = (PersistentStoreTransaction) getPersistentTransaction();
        final UniqueKeyIndicesEngine ukiEngine = store.getQueryEngine().getUniqueKeyIndicesEngine();

        for (Pair deletion : uniqueKeyDeletions) {
            TransientEntity e = deletion.getFirst();
            Index index = deletion.getSecond();
            final List originalValues = getIndexFieldsOriginalValues(e, index);
            // ignore null values; work around for JT-43108
            if (!originalValues.contains(null)) {
                ukiEngine.deleteUniqueKey(persistentTransaction, index, originalValues);
            }
        }

        for (Pair insertion : uniqueKeyInsertions) {
            TransientEntity e = insertion.getFirst();
            Index index = insertion.getSecond();
            try {
                ukiEngine.insertUniqueKey(persistentTransaction, index, getIndexFieldsFinalValues(e, index), e);
            } catch (ExodusException ex) {
                throw new ConstraintsValidationException(new UniqueIndexViolationException(e, index));
            }
        }
    }

    @Nullable
    Set>> getIndexesValuesBeforeDelete(TransientEntity e) {
        if (changesTracker.isNew(e)) return null;

        Set>> res = null;
        final EntityMetaData emd = getEntityMetaData(e);
        if (emd != null) {
            for (Index index : emd.getIndexes()) {
                if (res == null) res = new HashSet>>();
                res.add(new Pair>(index, getIndexFieldsOriginalValues(e, index)));
            }
        }

        return res;
    }

    void deleteIndexes(TransientEntity e, @Nullable Set>> indexes) {
        if (indexes == null) return;

        final PersistentStoreTransaction persistentTransaction = (PersistentStoreTransaction) getPersistentTransaction();
        final UniqueKeyIndicesEngine ukiEngine = store.getQueryEngine().getUniqueKeyIndicesEngine();
        for (Pair> index : indexes) {
            try {
                ukiEngine.deleteUniqueKey(persistentTransaction, index.getFirst(), index.getSecond());
            } catch (ExodusException ex) {
                throw new ConstraintsValidationException(new UniqueIndexIntegrityException(e, index.getFirst(), ex));
            }
        }
    }

    private List getIndexFieldsOriginalValues(TransientEntity e, Index index) {
        List res = new ArrayList(index.getFields().size());
        for (IndexField f : index.getFields()) {
            if (f.isProperty()) {
                res.add(getOriginalPropertyValue(e, f.getName()));
            } else {
                res.add(getOriginalLinkValue(e, f.getName()));
            }
        }
        return res;
    }

    @Nullable
    private Comparable getOriginalPropertyValue(TransientEntity e, String propertyName) {
        return e.getPersistentEntity().getSnapshot(changesTracker.getSnapshot()).getProperty(propertyName);
    }

    @Nullable
    private ByteIterable getOriginalRawPropertyValue(TransientEntity e, String propertyName) {
        return e.getPersistentEntity().getSnapshot(changesTracker.getSnapshot()).getRawProperty(propertyName);
    }

    @Nullable
    private String getOriginalBlobStringValue(TransientEntity e, String blobName) {
        return e.getPersistentEntity().getSnapshot(changesTracker.getSnapshot()).getBlobString(blobName);
    }

    @Nullable
    private InputStream getOriginalBlobValue(TransientEntity e, String blobName) {
        return e.getPersistentEntity().getSnapshot(changesTracker.getSnapshot()).getBlob(blobName);
    }

    @Nullable
    private Comparable getOriginalLinkValue(TransientEntity e, String linkName) {
        // get from saved changes, if not - from db
        Map linksDetailed = changesTracker.getChangedLinksDetailed(e);
        if (linksDetailed != null) {
            LinkChange change = linksDetailed.get(linkName);
            if (change != null) {
                switch (change.getChangeType()) {
                    case ADD_AND_REMOVE:
                    case REMOVE:
                        if (change.getRemovedEntitiesSize() != 1) {
                            if (change.getDeletedEntitiesSize() == 1) {
                                return change.getDeletedEntities().iterator().next();
                            }
                            throw new IllegalStateException("Can't determine original link value: " + e.getType() + "." + linkName);
                        }
                        return change.getRemovedEntities().iterator().next();
                    default:
                        throw new IllegalStateException("Incorrect change type for link that is part of index: " + e.getType() + "." + linkName + ": " + change.getChangeType().getName());
                }
            }
        }

        return e.getPersistentEntity().getSnapshot(changesTracker.getSnapshot()).getLink(linkName);
    }

    private List getIndexFieldsFinalValues(TransientEntity e, Index index) {
        List res = new ArrayList(index.getFields().size());
        for (IndexField f : index.getFields()) {
            if (f.isProperty()) {
                res.add(e.getProperty(f.getName()));
            } else {
                res.add(e.getLink(f.getName()));
            }
        }
        return res;
    }

    @Nullable
    private Set getMetadataIndexes(TransientEntity e, String field) {
        EntityMetaData md = getEntityMetaData(e);
        return md == null ? null : md.getIndexes(field);
    }

    @Nullable
    private EntityMetaData getEntityMetaData(TransientEntity e) {
        ModelMetaData mdd = store.getModelMetaData();
        return mdd == null ? null : mdd.getEntityMetaData(e.getType());
    }

    private void beforeFlush() {
        // notify listeners, execute before flush, if were side effects, do the same for side effects

        Set changesDescription = changesTracker.getChangesDescription();
        if (!changesTracker.getChangedEntities().isEmpty()) {
            final Set processedEntities = new HashSetDecorator();
            Set changed = getSideEffects(processedEntities);
            while (true) {
                final int changesSize = changesTracker.getChangedEntities().size();
                notifyBeforeFlushListeners(changesDescription);
                executeBeforeFlushTriggers(changed);
                if (changesSize == changesTracker.getChangedEntities().size()) {
                    break;
                }
                processedEntities.addAll(changed);
                changed = getSideEffects(processedEntities);
                if (changed.isEmpty()) {
                    break;
                }
                changesDescription = new HashSet();
                for (TransientEntity sideEffectEntity : changed) {
                    changesDescription.add(changesTracker.getChangeDescription(sideEffectEntity));
                }
            }
        }
    }

    private Set getSideEffects(Set processedEntities) {
        HashSet sideEffect = new HashSet(changesTracker.getChangedEntities());
        sideEffect.removeAll(processedEntities);
        return sideEffect;
    }

    private static void decodeException(@NotNull Throwable e) {
        if (e instanceof Error) {
            throw (Error) e;
        }

        if (e instanceof RuntimeException) {
            throw (RuntimeException) e;
        }

        throw new RuntimeException(e);
    }

    private void notifyFlushedListeners(TransientChangesTracker oldChangesTracker) {
        final Set changesDescription = oldChangesTracker.getChangesDescription();
        if (changesDescription.isEmpty()) {
            oldChangesTracker.dispose();
            return;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Notify flushed listeners " + this);
        }

        store.forAllListeners(new TransientEntityStoreImpl.ListenerVisitor() {
            public void visit(TransientStoreSessionListener listener) {
                try {
                    listener.flushed(TransientSessionImpl.this, changesDescription);
                } catch (Exception e) {
                    if (logger.isErrorEnabled()) {
                        logger.error("Exception while inside listener [" + listener + "]", e);
                    }
                    // do not rethrow exception
                }
            }
        });

        //explicitly notify EventsMultiplexer - it will dispose changes tracker in async job
        final IEventsMultiplexer ep = store.getEventsMultiplexer();
        if (ep != null) {
            try {
                ep.flushed(this, oldChangesTracker, changesDescription);
            } catch (Exception e) {
                if (logger.isErrorEnabled()) {
                    logger.error("Exception while inside events multiplexer", e);
                }
                oldChangesTracker.dispose();
            }
        } else {
            oldChangesTracker.dispose();
        }
    }

    private void notifyBeforeFlushListeners(final Set changes) {
        if (changes == null || changes.isEmpty()) return;

        if (logger.isDebugEnabled()) {
            logger.debug("Notify before flush listeners " + this);
        }

        store.forAllListeners(new TransientEntityStoreImpl.ListenerVisitor() {
            public void visit(TransientStoreSessionListener listener) {
                try {
                    listener.beforeFlush(TransientSessionImpl.this, changes);
                } catch (Exception e) {
                    if (logger.isErrorEnabled()) {
                        logger.error("Exception while inside listener [" + listener + "]", e);
                    }
                    // rethrow exception, because we are before constraints check
                    decodeException(e);
                }
            }
        });
    }

    @Deprecated
    private void notifyBeforeFlushAfterConstraintsCheckListeners() {
        final Set changesDescr = changesTracker.getChangesDescription();

        if (changesDescr.isEmpty()) return;

        if (logger.isDebugEnabled()) {
            logger.debug("Notify before flush after constraints check listeners " + this);
        }

        // check side effects in listeners
        final int changesCount = changes.size();
        store.forAllListeners(new TransientEntityStoreImpl.ListenerVisitor() {
            public void visit(TransientStoreSessionListener listener) {
                try {
                    listener.beforeFlushAfterConstraintsCheck(TransientSessionImpl.this, changesDescr);
                } catch (Exception e) {
                    if (logger.isErrorEnabled()) {
                        logger.error("Exception while inside listener [" + listener + "]", e);
                    }
                    // do not rethrow exceptipon, because we are after constaints check
                }
            }
        });

        if (changes.size() != changesCount) {
            throw new EntityStoreException("It's not allowed to change database inside listener.beforeFlushAfterConstraintsCheck() method.");
        }
    }

    @NotNull
    protected TransientEntity newEntityImpl(final PersistentEntity persistent) {
        if (persistent instanceof ReadOnlyPersistentEntity) {
            return new ReadonlyTransientEntityImpl((ReadOnlyPersistentEntity) persistent, store);
        }
        final EntityId entityId = persistent.getId();
        TransientEntity e = managedEntities.get(entityId);
        if (e == null) {
            e = new TransientEntityImpl((PersistentEntity) persistent, this.getStore());
            managedEntities.put(entityId, e);
        }
        return e;
    }

    void createEntity(@NotNull final TransientEntityImpl e, @NotNull String type) {
        final PersistentEntity persistentEntity = (PersistentEntity) getPersistentTransaction().newEntity(type);
        e.setPersistentEntity(persistentEntity);
        managedEntities.put(e.getId(), e);
        changesTracker.entityAdded(e);
        addChange(new MyRunnable() {
            public boolean run() {
                return saveEntityInternal(persistentEntity, e);
            }
        });
    }

    private boolean saveEntityInternal(@NotNull final PersistentEntity persistentEntity, @NotNull final TransientEntityImpl e) {
        getPersistentTransaction().saveEntity(persistentEntity);
        managedEntities.put(e.getId(), e);
        changesTracker.entityAdded(e);
        return true;
    }

    void createEntity(@NotNull final TransientEntityImpl e, @NotNull final EntityCreator creator) {
        final PersistentEntity persistentEntity = (PersistentEntity) getPersistentTransaction().newEntity(creator.getType());
        e.setPersistentEntity(persistentEntity);
        addChange(new MyRunnable() {
            @Override
            public boolean run() {
                final Entity found = creator.find();
                if (found == null) {
                    final boolean result = saveEntityInternal(persistentEntity, e);
                    try {
                        allowRunnables = false;
                        creator.created(e);
                    } finally {
                        allowRunnables = true;
                    }
                    return result;
                }
                e.setPersistentEntity(((TransientEntityImpl) found).getPersistentEntity());
                return false;
            }
        });
        managedEntities.put(e.getId(), e);
        changesTracker.entityAdded(e);
        try {
            allowRunnables = false;
            creator.created(e);
        } finally {
            allowRunnables = true;
        }
    }

    private void addEntityCreator(@NotNull final TransientEntityImpl e, @NotNull final EntityCreator creator) {
        addChange(new MyRunnable() {
            @Override
            public boolean run() {
                final Entity found = creator.find();
                if (found != null) {
                    if (!found.equals(e)) {
                        // update existing entity
                        e.setPersistentEntity(((TransientEntityImpl) found).getPersistentEntity());
                    }
                    return false;
                }
                upgradeReadonlyTransactionIfNecessary();
                // somebody deleted our (initially found) entity! we need to create some again
                e.setPersistentEntity((PersistentEntity) getPersistentTransaction().newEntity(creator.getType()));
                changesTracker.entityAdded(e);
                try {
                    allowRunnables = false;
                    creator.created(e);
                } finally {
                    allowRunnables = true;
                }
                return true;
            }
        });
    }

    boolean setProperty(@NotNull final TransientEntity e, @NotNull final String propertyName,
                        @NotNull final Comparable propertyNewValue) {
        return addChangeAndRun(new MyRunnable() {
            @Override
            public boolean run() {
                return setPropertyInternal(e, propertyName, propertyNewValue);
            }
        });
    }

    private boolean setPropertyInternal(@NotNull final TransientEntity e, @NotNull final String propertyName,
                                        @NotNull final Comparable propertyNewValue) {
        if (e.getPersistentEntity().setProperty(propertyName, propertyNewValue)) {
            final Comparable oldValue = getOriginalPropertyValue(e, propertyName);
            if (propertyNewValue == oldValue || propertyNewValue.equals(oldValue)) {
                changesTracker.removePropertyChanged(e, propertyName);
            } else {
                changesTracker.propertyChanged(e, propertyName);
            }
            return true;
        }
        return false;
    }

    boolean deleteProperty(@NotNull final TransientEntity e, @NotNull final String propertyName) {
        return addChangeAndRun(new MyRunnable() {
            @Override
            public boolean run() {
                return deletePropertyInternal(e, propertyName);
            }
        });
    }

    private boolean deletePropertyInternal(@NotNull final TransientEntity e, @NotNull final String propertyName) {
        if (e.getPersistentEntity().deleteProperty(propertyName)) {
            final Comparable oldValue = getOriginalPropertyValue(e, propertyName);
            if (oldValue == null) {
                changesTracker.removePropertyChanged(e, propertyName);
            } else {
                changesTracker.propertyChanged(e, propertyName);
            }
            return true;
        }
        return false;
    }

    void setBlob(@NotNull final TransientEntity e, @NotNull final String blobName, @NotNull final InputStream stream) {
        final ByteArraySizedInputStream copy;
        try {
            copy = ((PersistentEntityStore) store.getPersistentStore()).getBlobVault().cloneStream(stream, true);
        } catch (final IOException ioe) {
            throw new RuntimeException(ioe);
        }
        copy.mark(Integer.MAX_VALUE);
        addChangeAndRun(new MyRunnable() {
            public boolean run() {
                copy.reset();
                e.getPersistentEntity().setBlob(blobName, copy);
                changesTracker.propertyChanged(e, blobName);
                return true;
            }
        });
    }

    void setBlob(@NotNull final TransientEntity e, @NotNull final String blobName, @NotNull final File file) {
        addChangeAndRun(new MyRunnable() {
            public boolean run() {
                e.getPersistentEntity().setBlob(blobName, file);
                changesTracker.propertyChanged(e, blobName);
                return true;
            }
        });
    }

    boolean setBlobString(@NotNull final TransientEntity e, @NotNull final String blobName, @NotNull final String newValue) {
        return addChangeAndRun(new MyRunnable() {
            public boolean run() {
                if (e.getPersistentEntity().setBlobString(blobName, newValue)) {
                    final Comparable oldValue = getOriginalBlobStringValue(e, blobName);
                    if (newValue == oldValue || newValue.equals(oldValue)) {
                        changesTracker.removePropertyChanged(e, blobName);
                    } else {
                        changesTracker.propertyChanged(e, blobName);
                    }
                    return true;
                }
                return false;
            }
        });
    }

    boolean deleteBlob(@NotNull final TransientEntity e, @NotNull final String blobName) {
        return addChangeAndRun(new MyRunnable() {
            public boolean run() {
                if (e.getPersistentEntity().deleteBlob(blobName)) {
                    final InputStream oldValue = getOriginalBlobValue(e, blobName);
                    if (oldValue == null) {
                        changesTracker.removePropertyChanged(e, blobName);
                    } else {
                        changesTracker.propertyChanged(e, blobName);
                    }
                    return true;
                }
                return false;
            }
        });
    }

    boolean setLink(@NotNull final TransientEntity source, @NotNull final String linkName, @NotNull final TransientEntity target) {
        return addChangeAndRun(new MyRunnable() {
            public boolean run() {
                return setLinkInternal(source, linkName, target);
            }
        });
    }

    private boolean setLinkInternal(@NotNull final TransientEntity source, @NotNull final String linkName, @NotNull final TransientEntity target) {
        final TransientEntity oldTarget = (TransientEntity) source.getLink(linkName);
        if (source.getPersistentEntity().setLink(linkName, target.getPersistentEntity())) {
            changesTracker.linkChanged(source, linkName, target, oldTarget, true);
            return true;
        }

        return false;
    }

    boolean addLink(@NotNull final TransientEntity source, @NotNull final String linkName, @NotNull final TransientEntity target) {
        return addChangeAndRun(new MyRunnable() {
            public boolean run() {
                return addLinkInternal(source, linkName, target);
            }
        });
    }

    private boolean addLinkInternal(@NotNull final TransientEntity source, @NotNull final String linkName, @NotNull final TransientEntity target) {
        if (source.getPersistentEntity().addLink(linkName, target.getPersistentEntity())) {
            changesTracker.linkChanged(source, linkName, target, null, true);
            return true;
        }

        return false;
    }

    boolean deleteLink(@NotNull final TransientEntity source, @NotNull final String linkName, @NotNull final TransientEntity target) {
        return addChangeAndRun(new MyRunnable() {
            public boolean run() {
                return deleteLinkInternal(source, linkName, target);
            }
        });
    }

    private boolean deleteLinkInternal(TransientEntity source, String linkName, TransientEntity target) {
        if (source.getPersistentEntity().deleteLink(linkName, target.getPersistentEntity())) {
            changesTracker.linkChanged(source, linkName, target, null, false);
            return true;
        }

        return false;
    }

    void deleteLinks(@NotNull final TransientEntity source, @NotNull final String linkName) {
        addChangeAndRun(new MyRunnable() {
            public boolean run() {
                changesTracker.linksRemoved(source, linkName, source.getLinks(linkName));
                source.getPersistentEntity().deleteLinks(linkName);
                return true;
            }
        });
    }

    boolean deleteEntity(@NotNull final TransientEntity e) {
        return addChangeAndRun(new MyRunnable() {
            public boolean run() {
                return deleteEntityInternal(e);
            }
        });
    }

    private boolean deleteEntityInternal(@NotNull final TransientEntity e) {
        if (TransientStoreUtil.isPostponeUniqueIndexes()) {
            if (e.getPersistentEntity().delete()) {
                changesTracker.entityRemoved(e);
            }
        } else {
            // remember index values first
            final Set>> indexes = getIndexesValuesBeforeDelete(e);
            if (e.getPersistentEntity().delete()) {
                deleteIndexes(e, indexes);
                changesTracker.entityRemoved(e);
            }
        }
        return true;
    }

    void setToOne(@NotNull final TransientEntity source, @NotNull final String linkName, @Nullable final TransientEntity target) {
        addChangeAndRun(new MyRunnable() {
            @Override
            public boolean run() {
                if (target == null) {
                    TransientEntity oldTarget = (TransientEntity) source.getLink(linkName);
                    if (oldTarget != null) {
                        deleteLinkInternal(source, linkName, oldTarget);
                    }
                } else {
                    setLinkInternal(source, linkName, target);
                }
                return true;
            }
        });
    }

    void setManyToOne(@NotNull final TransientEntity many, @NotNull final String manyToOneLinkName,
                      @NotNull final String oneToManyLinkName, @Nullable final TransientEntity one) {
        addChangeAndRun(new MyRunnable() {
            @Override
            public boolean run() {
                final TransientEntity m = newLocalCopySafe(many);
                if (m != null) {
                    final TransientEntity o = newLocalCopySafe(one);
                    final TransientEntity oldOne = (TransientEntity) m.getLink(manyToOneLinkName);
                    if (oldOne != null) {
                        deleteLinkInternal(oldOne, oneToManyLinkName, m);
                        if (o == null) {
                            deleteLinkInternal(m, manyToOneLinkName, oldOne);
                        }
                    }
                    if (o != null) {
                        addLinkInternal(o, oneToManyLinkName, m);
                        setLinkInternal(m, manyToOneLinkName, o);
                    }
                }
                return true;
            }
        });
    }

    void clearOneToMany(@NotNull final TransientEntity one, @NotNull final String manyToOneLinkName, @NotNull final String oneToManyLinkName) {
        addChangeAndRun(new MyRunnable() {
            @Override
            public boolean run() {
                for (final Entity target : one.getLinks(oneToManyLinkName)) {
                    final TransientEntity many = (TransientEntity) target;
                    deleteLinkInternal(one, oneToManyLinkName, many);
                    deleteLinkInternal(many, manyToOneLinkName, one);
                }
                return true;
            }
        });
    }

    public void createManyToMany(@NotNull final TransientEntity e1, @NotNull final String e1Toe2LinkName,
                                 @NotNull final String e2Toe1LinkName, @NotNull final TransientEntity e2) {
        addChangeAndRun(new MyRunnable() {
            @Override
            public boolean run() {
                addLinkInternal(e1, e1Toe2LinkName, e2);
                addLinkInternal(e2, e2Toe1LinkName, e1);
                return true;
            }
        });
    }

    public void clearManyToMany(@NotNull final TransientEntity e1, @NotNull final String e1Toe2LinkName, @NotNull final String e2Toe1LinkName) {
        addChangeAndRun(new MyRunnable() {
            @Override
            public boolean run() {
                for (final Entity target : e1.getLinks(e1Toe2LinkName)) {
                    final TransientEntity e2 = (TransientEntity) target;
                    deleteLinkInternal(e1, e1Toe2LinkName, e2);
                    deleteLinkInternal(e2, e2Toe1LinkName, e1);
                }
                return true;
            }
        });
    }

    public void setOneToOne(@NotNull final TransientEntityImpl e1, @NotNull final String e1Toe2LinkName,
                            @NotNull final String e2Toe1LinkName, @Nullable final TransientEntity e2) {
        addChangeAndRun(new MyRunnable() {
            @Override
            public boolean run() {
                final TransientEntity prevE2 = (TransientEntity) e1.getLink(e1Toe2LinkName);
                if (prevE2 != null) {
                    if (prevE2.equals(e2)) {
                        return true;
                    }
                    deleteLinkInternal(prevE2, e2Toe1LinkName, e1);
                    deleteLinkInternal(e1, e1Toe2LinkName, prevE2);
                }
                if (e2 != null) {
                    final TransientEntity prevE1 = (TransientEntity) e2.getLink(e2Toe1LinkName);
                    if (prevE1 != null) {
                        deleteLinkInternal(prevE1, e1Toe2LinkName, e2);
                    }
                    setLinkInternal(e1, e1Toe2LinkName, e2);
                    setLinkInternal(e2, e2Toe1LinkName, e1);
                }
                return true;
            }
        });
    }

    public void removeOneToMany(@NotNull final TransientEntityImpl one, @NotNull final String manyToOneLinkName,
                                @NotNull final String oneToManyLinkName, @NotNull final TransientEntity many) {
        addChangeAndRun(new MyRunnable() {
            @Override
            public boolean run() {
                final TransientEntity oldOne = (TransientEntity) many.getLink(manyToOneLinkName);
                if (one.equals(oldOne)) {
                    deleteLinkInternal(many, manyToOneLinkName, oldOne);
                }
                deleteLinkInternal(one, oneToManyLinkName, many);
                return true;
            }
        });
    }

    public void removeFromParent(@NotNull final TransientEntity child, @NotNull final String parentToChildLinkName,
                                 @NotNull final String childToParentLinkName) {
        addChangeAndRun(new MyRunnable() {
            @Override
            public boolean run() {
                final TransientEntity parent = (TransientEntity) child.getLink(childToParentLinkName);
                if (parent != null) { // may be changed or removed
                    removeChildFromParentInternal(parent, parentToChildLinkName, childToParentLinkName, child);
                }
                return true;
            }
        });
    }

    public void removeChild(@NotNull final TransientEntityImpl parent, @NotNull final String parentToChildLinkName,
                            @NotNull final String childToParentLinkName) {
        addChangeAndRun(new MyRunnable() {
            @Override
            public boolean run() {
                final TransientEntity child = (TransientEntity) parent.getLink(parentToChildLinkName);
                if (child != null) { // may be changed or removed
                    removeChildFromParentInternal(parent, parentToChildLinkName, childToParentLinkName, child);
                }
                return true;
            }
        });
    }

    private void removeChildFromParentInternal(@NotNull final TransientEntity parent, @NotNull final String parentToChildLinkName,
                                               @Nullable final String childToParentLinkName, @NotNull final TransientEntity child) {
        deleteLinkInternal(parent, parentToChildLinkName, child);
        deletePropertyInternal(child, PARENT_TO_CHILD_LINK_NAME);
        if (childToParentLinkName != null) {
            deleteLinkInternal(child, childToParentLinkName, parent);
            deletePropertyInternal(child, CHILD_TO_PARENT_LINK_NAME);
        }
    }

    public void setChild(@NotNull final TransientEntity parent, @NotNull final String parentToChildLinkName,
                         @NotNull final String childToParentLinkName, @NotNull final TransientEntity child) {
        addChangeAndRun(new MyRunnable() {
            @Override
            public boolean run() {
                if (removeChildFromCurrentParentInternal(child, childToParentLinkName, parentToChildLinkName, parent)) {
                    final TransientEntity oldChild = (TransientEntity) parent.getLink(parentToChildLinkName);
                    if (oldChild != null) {
                        removeChildFromParentInternal(parent, parentToChildLinkName, childToParentLinkName, oldChild);
                    }
                    setLinkInternal(parent, parentToChildLinkName, child);
                    setLinkInternal(child, childToParentLinkName, parent);
                    setPropertyInternal(child, PARENT_TO_CHILD_LINK_NAME, parentToChildLinkName);
                    setPropertyInternal(child, CHILD_TO_PARENT_LINK_NAME, childToParentLinkName);
                }
                return true;
            }
        });
    }

    private boolean removeChildFromCurrentParentInternal(@NotNull final TransientEntity child, @NotNull final String childToParentLinkName,
                                                         @NotNull final String parentToChildLinkName, @NotNull final TransientEntity newParent) {
        final String oldChildToParentLinkName = (String) child.getProperty(CHILD_TO_PARENT_LINK_NAME);
        if (oldChildToParentLinkName != null) {
            if (childToParentLinkName.equals(oldChildToParentLinkName)) {
                final TransientEntity oldParent = (TransientEntity) child.getLink(childToParentLinkName);
                if (oldParent != null) {
                    if (oldParent.equals(newParent)) {
                        return false;
                    }
                    // child to parent link will be owerwritten, so don't delete it directly
                    deleteLinkInternal(oldParent, parentToChildLinkName, child);
                }
            } else {
                final TransientEntity oldParent = (TransientEntity) child.getLink(oldChildToParentLinkName);
                if (oldParent != null) {
                    final String oldParentToChildLinkName = (String) child.getProperty(PARENT_TO_CHILD_LINK_NAME);
                    deleteLinkInternal(oldParent, oldParentToChildLinkName == null ? parentToChildLinkName : oldParentToChildLinkName, child);
                    deleteLinkInternal(child, oldChildToParentLinkName, oldParent);
                }
            }
        }
        return true;
    }

    public void clearChildren(@NotNull final TransientEntity parent, @NotNull final String parentToChildLinkName) {
        addChangeAndRun(new MyRunnable() {
            @Override
            public boolean run() {
                for (final Entity child : parent.getLinks(parentToChildLinkName)) {
                    final String childToParentLinkName = (String) child.getProperty(CHILD_TO_PARENT_LINK_NAME);
                    removeChildFromParentInternal(parent, parentToChildLinkName, childToParentLinkName, (TransientEntity) child);
                }
                return true;
            }
        });
    }

    public void addChild(@NotNull final TransientEntity parent, @NotNull final String parentToChildLinkName,
                         @NotNull final String childToParentLinkName, @NotNull final TransientEntity child) {
        addChangeAndRun(new MyRunnable() {
            @Override
            public boolean run() {
                if (removeChildFromCurrentParentInternal(child, childToParentLinkName, parentToChildLinkName, parent)) {
                    addLinkInternal(parent, parentToChildLinkName, child);
                    setLinkInternal(child, childToParentLinkName, parent);
                    setPropertyInternal(child, PARENT_TO_CHILD_LINK_NAME, parentToChildLinkName);
                    setPropertyInternal(child, CHILD_TO_PARENT_LINK_NAME, childToParentLinkName);
                }
                return true;
            }
        });
    }

    public Entity getParent(@NotNull final TransientEntity child) {
        final String childToParentLinkName = (String) child.getProperty(CHILD_TO_PARENT_LINK_NAME);

        if (childToParentLinkName == null) {
            return null;
        }

        return child.getLink(childToParentLinkName);
    }

    public boolean addChangeAndRun(MyRunnable change) {
        upgradeReadonlyTransactionIfNecessary();
        return addChange(change).run();
    }

    @Override
    public void saveEntity(@NotNull Entity entity) {
        throw new UnsupportedOperationException();
    }

    @Nullable
    private TransientEntity newLocalCopySafe(@Nullable final TransientEntity entity) {
        if (entity == null) {
            return null;
        }
        try {
            return newLocalCopy(entity);
        } catch (EntityRemovedInDatabaseException ignore) {
            return null;
        }
    }

    MyRunnable addChange(@NotNull final MyRunnable change) {
        if (allowRunnables) {
            changes.offer(change);
        }
        return change;
    }

    enum State {
        Open("open"),
        Committed("committed"),
        Aborted("aborted");

        private String name;

        State(String name) {
            this.name = name;
        }
    }

    public interface MyRunnable {
        boolean run();
    }

    private PersistentEntityStoreImpl getPersistentStore() {
        return (PersistentEntityStoreImpl) store.getPersistentStore();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy