<commit_before>/*
 * Copyright 2018, TeamDev. All rights reserved.
 *
 * Redistribution and use in source and/or binary forms, with or without
 * modification, must retain the above copyright notice and the following
 * disclaimer.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package io.spine.server.entity;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.protobuf.Any;
import com.google.protobuf.Message;
import io.spine.annotation.Internal;
import io.spine.base.Identifier;
import io.spine.core.EventContext;
import io.spine.core.EventEnvelope;
import io.spine.core.EventId;
import io.spine.core.Version;
import io.spine.server.entity.TransactionListener.SilentWitness;
import io.spine.validate.AbstractValidatingBuilder;
import io.spine.validate.ValidatingBuilder;
import io.spine.validate.ValidationException;

import java.util.List;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Lists.newLinkedList;
import static io.spine.core.Versions.checkIsIncrement;
import static io.spine.protobuf.AnyPacker.pack;
import static io.spine.server.entity.EntityVersioning.FROM_EVENT;
import static io.spine.server.entity.InvalidEntityStateException.onConstraintViolations;
import static io.spine.util.Exceptions.illegalStateWithCauseOf;
import static java.lang.String.format;

/**
 * The abstract class for the {@linkplain TransactionalEntity entity} transactions.
 *
 * <p>The transaction is a set of changes made to an entity state or entity attributes
 * (e.g. version, lifecycle flags etc).
 *
 * <p>Serves as a buffer, accumulating the changes, intended for the enclosed {@code Entity};
 * the changes are only applied to the actual object upon {@linkplain #commit() commit}.
 *
 * <p>The transaction is injected to the entity, which state should be modified. By doing so,
 * the {@linkplain Transaction#getBuilder() "buffering" builder} is exposed to concrete
 * {@code TransactionalEntity} subclasses. In turn, they receive an ability to change the entity
 * state by modifying {@link TransactionalEntity#getBuilder() entity state builder}.
 *
 * <p>Same applies to the entity lifecycle flags.
 *
 * <p>Version management is performed automatically by the transaction itself. Each event message
 * applied leads to the version increment.
 *
 * @param <I> the type of entity IDs
 * @param <E> the type of entity
 * @param <S> the type of entity state
 * @param <B> the type of a {@code ValidatingBuilder} for the entity state
 * @author <NAME>
 * @author <NAME>
 */
@Internal
public abstract class Transaction<I,
                                  E extends TransactionalEntity<I, S, B>,
                                  S extends Message,
                                  B extends ValidatingBuilder<S, ? extends Message.Builder>> {

    /**
     * The entity, which state and attributes are modified in this transaction.
     */
    private final E entity;

    /**
     * The builder for the entity state.
     *
     * <p>All the state changes made within the transaction go to this {@code Builder},
     * and not to the {@code Entity} itself.
     *
     * <p>The state of the builder is merged to the entity state
     * upon the {@linkplain #commit() commit()}.
     */
    private final B builder;

    /**
     * The {@link LifecycleFlags} which the entity has had before the transaction start.
     *
     * <p>This value is used in order to check if lifecycle flags had been changed in current
     * transaction.
     */
    private final LifecycleFlags initialLifecycleFlags;

    /**
     * The version of the entity, modified within this transaction.
     *
     * <p>All the version changes made within the transaction are stored in this variable,
     * and not in the {@code Entity} itself.
     *
     * <p>This value is propagated to the entity upon the {@linkplain #commit() commit()}.
     */
    private Version version;

    /**
     * The lifecycle flags of the entity, modified within this transaction.
     *
     * <p>All the entity lifecycle changes made within the transaction are stored in this variable,
     * and not in the {@code Entity} itself.
     *
     * <p>This value is set to the entity upon the {@linkplain #commit() commit()}.
     */
    private LifecycleFlags lifecycleFlags;

    /**
     * The flag, which becomes {@code true}, if the state of the entity
     * {@linkplain #commit() has been changed} since it has been
     * {@linkplain RecordBasedRepository#findOrCreate(Object)} loaded or created.
     */
    private boolean stateChanged;

    /**
     * Allows to understand whether this transaction is active.
     *
     * <p>Has {@code true} value since the transaction instance creation
     * until {@linkplain #commit() commit()} is performed.
     */
    private boolean active = false;

    /**
     * An ordered collection of the phases that were propagated in scope of this transaction.
     *
     * <p>Contains all the phases, including failed.
     */
    private final List<Phase<I, E, S, B>> phases = newLinkedList();

    private TransactionListener<I, E, S, B> transactionListener;

    /**
     * Creates a new instance of {@code Transaction} and
     * {@linkplain TransactionalEntity#injectTransaction(Transaction) injects} the newly created
     * transaction into the given {@code entity}.
     *
     * <p>The entity state and attributes are set as starting values for this transaction.
     *
     * @param entity   the entity to create the transaction for
     * @see TransactionListener
     */
    protected Transaction(E entity) {
        checkNotNull(entity);

        this.entity = entity;
        this.builder = entity.builderFromState();
        this.version = entity.getVersion();
        this.lifecycleFlags = entity.getLifecycleFlags();
        this.initialLifecycleFlags = entity.getLifecycleFlags();
        this.active = true;

        this.transactionListener = new SilentWitness<>();

        injectTo(entity);
    }

    /**
     * Acts similar to {@linkplain Transaction#Transaction(TransactionalEntity)
     * an overloaded ctor}, but instead of using the original entity state and version,
     * this transaction will have the passed state and version as a starting point.
     *
     * <p>Note, that the given {@code state} and {@code version} are applied to the actual entity
     * upon commit.
     *
     * @param entity   the target entity to modify within this transaction
     * @param state    the entity state to set
     * @param version  the entity version to set
     */
    protected Transaction(E entity, S state, Version version) {
        this(entity);
        initAll(state, version);
    }

    /**
     * Dispatches the event message and its context to the current entity-in-transaction.
     *
     * <p>This operation is always performed in scope of an active transaction.
     *
     * @param entity  the target entity
     * @param event   the event to dispatch
     */
    protected abstract void dispatch(E entity, EventEnvelope event);

    /**
     * Allows to understand whether this transaction is active.
     *
     * @return {@code true} if the transaction is active, {@code false} otherwise
     */
    @VisibleForTesting
    public boolean isActive() {
        return active;
    }

    LifecycleFlags getLifecycleFlags() {
        return lifecycleFlags;
    }

    E getEntity() {
        return entity;
    }

    /**
     * @return the version of the entity, modified within this transaction
     */
    Version getVersion() {
        return version;
    }

    List<Phase<I, E, S, B>> getPhases() {
        return ImmutableList.copyOf(phases);
    }

    /**
     * Applies all the outstanding modifications to the enclosed entity.
     *
     * @throws InvalidEntityStateException in case the new entity state is not valid
     * @throws IllegalStateException       in case of a generic error
     */
    protected void commit() throws InvalidEntityStateException, IllegalStateException {
        final B builder = getBuilder();

        if (builder.isDirty()) {
            commitChangedState(builder);
        } else {
            commitUnchangedState();
        }
    }

    /**
     * Commits this transaction and sets the new state to the entity.
     *
     * <p>In case if the commit is failed, the transaction is rolled back and the entity keeps
     * the current state.
     *
     * @param builder the {@link ValidatingBuilder} with the new state of the entity
     */
    private void commitChangedState(B builder) {
        try {
            EntityRecord previousRecord = createRecordWith(initialLifecycleFlags);
            S newState = builder.build();
            markStateChanged();
            Version pendingVersion = getVersion();
            beforeCommit(newState, pendingVersion);
            entity.updateState(newState, pendingVersion);
            commitAttributeChanges();
            EntityRecord newRecord = createRecordWith(lifecycleFlags);
            afterCommit(previousRecord, newRecord);
        } catch (ValidationException exception) {  /* Could only happen if the state
                                                      has been injected not using
                                                      the builder setters. */
            final InvalidEntityStateException invalidStateException = of(exception);
            rollback(invalidStateException);

            throw invalidStateException;
        } catch (@SuppressWarnings("OverlyBroadCatchBlock") // Catch all unexpected exceptions.
                 RuntimeException genericException) {
            rollback(genericException);
            throw illegalStateWithCauseOf(genericException);
        } finally {
            releaseTx();
        }
    }

    /**
     * Commits this transaction skipping the entity state update.
     *
     * <p>This method is called when none of the transaction phases has changed the entity state.
     */
    private void commitUnchangedState() {
        EntityRecord previousRecord = createRecordWith(initialLifecycleFlags);
        S unchanged = getEntity().getState();
        Version pendingVersion = getVersion();
        beforeCommit(unchanged, pendingVersion);
        if(!pendingVersion.equals(entity.getVersion())) {
            entity.updateState(unchanged, pendingVersion);
        }
        commitAttributeChanges();
        releaseTx();
        EntityRecord newRecord = createRecordWith(lifecycleFlags);
        afterCommit(previousRecord, newRecord);
    }

    private void beforeCommit(S newState, Version newVersion) {
        E entity = getEntity();
        LifecycleFlags newFlags = getLifecycleFlags();
        transactionListener.onBeforeCommit(entity, newState, newVersion, newFlags);
    }

    private void afterCommit(EntityRecord oldEntity, EntityRecord newEntity) {
        EntityRecordChange change = EntityRecordChange
                .newBuilder()
                .setPreviousValue(oldEntity)
                .setNewValue(newEntity)
                .build();
        transactionListener.onAfterCommit(change);
    }

    /**
     * Cancels the changes made within this transaction and removes the injected transaction object
     * from the enclosed entity.
     *
     * @param cause the reason of the rollback
     */
    void rollback(Throwable cause) {
        final S currentState = currentBuilderState();
        final TransactionListener<I, E, S, B> listener = getListener();
        listener.onTransactionFailed(cause, getEntity(), currentState,
                                     getVersion(), getLifecycleFlags());
        this.active = false;
        entity.releaseTransaction();
    }

    /**
     * Creates an {@link EntityRecord} for the entity under transaction.
     *
     * <p>Since an entity transaction delegates its {@link EntityWithLifecycle#getLifecycleFlags()}
     * method invocations to the transaction, an instance of {@link LifecycleFlags} should be
     * passed as an argument.
     *
     * @param lifecycleFlags flags to include into the record
     * @return new {@link EntityRecord}
     * @see #lifecycleFlags
     * @see #initialLifecycleFlags
     */
    private EntityRecord createRecordWith(LifecycleFlags lifecycleFlags) {
        E entity = getEntity();
        Any entityId = Identifier.pack(entity.getId());
        Version version = entity.getVersion();
        Any state = pack(entity.getState());
        return EntityRecord.newBuilder()
                           .setEntityId(entityId)
                           .setVersion(version)
                           .setState(state)
                           .setLifecycleFlags(lifecycleFlags)
                           .build();
    }

    /**
     * Creates a new {@linkplain Phase transaction phase} for the given event
     * and propagates the phase.
     *
     * <p>In case of an exception, the {@linkplain #rollback(Throwable) transaction rollback}<commit_msg>Add some more details to `createRecordWith` doc.<commit_after>/*
 * Copyright 2018, TeamDev. All rights reserved.
 *
 * Redistribution and use in source and/or binary forms, with or without
 * modification, must retain the above copyright notice and the following
 * disclaimer.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package io.spine.server.entity;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.protobuf.Any;
import com.google.protobuf.Message;
import io.spine.annotation.Internal;
import io.spine.base.Identifier;
import io.spine.core.EventContext;
import io.spine.core.EventEnvelope;
import io.spine.core.EventId;
import io.spine.core.Version;
import io.spine.server.entity.TransactionListener.SilentWitness;
import io.spine.validate.AbstractValidatingBuilder;
import io.spine.validate.ValidatingBuilder;
import io.spine.validate.ValidationException;

import java.util.List;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Lists.newLinkedList;
import static io.spine.core.Versions.checkIsIncrement;
import static io.spine.protobuf.AnyPacker.pack;
import static io.spine.server.entity.EntityVersioning.FROM_EVENT;
import static io.spine.server.entity.InvalidEntityStateException.onConstraintViolations;
import static io.spine.util.Exceptions.illegalStateWithCauseOf;
import static java.lang.String.format;

/**
 * The abstract class for the {@linkplain TransactionalEntity entity} transactions.
 *
 * <p>The transaction is a set of changes made to an entity state or entity attributes
 * (e.g. version, lifecycle flags etc).
 *
 * <p>Serves as a buffer, accumulating the changes, intended for the enclosed {@code Entity};
 * the changes are only applied to the actual object upon {@linkplain #commit() commit}.
 *
 * <p>The transaction is injected to the entity, which state should be modified. By doing so,
 * the {@linkplain Transaction#getBuilder() "buffering" builder} is exposed to concrete
 * {@code TransactionalEntity} subclasses. In turn, they receive an ability to change the entity
 * state by modifying {@link TransactionalEntity#getBuilder() entity state builder}.
 *
 * <p>Same applies to the entity lifecycle flags.
 *
 * <p>Version management is performed automatically by the transaction itself. Each event message
 * applied leads to the version increment.
 *
 * @param <I> the type of entity IDs
 * @param <E> the type of entity
 * @param <S> the type of entity state
 * @param <B> the type of a {@code ValidatingBuilder} for the entity state
 * @author <NAME>
 * @author <NAME>
 */
@Internal
public abstract class Transaction<I,
                                  E extends TransactionalEntity<I, S, B>,
                                  S extends Message,
                                  B extends ValidatingBuilder<S, ? extends Message.Builder>> {

    /**
     * The entity, which state and attributes are modified in this transaction.
     */
    private final E entity;

    /**
     * The builder for the entity state.
     *
     * <p>All the state changes made within the transaction go to this {@code Builder},
     * and not to the {@code Entity} itself.
     *
     * <p>The state of the builder is merged to the entity state
     * upon the {@linkplain #commit() commit()}.
     */
    private final B builder;

    /**
     * The {@link LifecycleFlags} which the entity has had before the transaction start.
     *
     * <p>This value is used in order to check if lifecycle flags had been changed in current
     * transaction.
     */
    private final LifecycleFlags initialLifecycleFlags;

    /**
     * The version of the entity, modified within this transaction.
     *
     * <p>All the version changes made within the transaction are stored in this variable,
     * and not in the {@code Entity} itself.
     *
     * <p>This value is propagated to the entity upon the {@linkplain #commit() commit()}.
     */
    private Version version;

    /**
     * The lifecycle flags of the entity, modified within this transaction.
     *
     * <p>All the entity lifecycle changes made within the transaction are stored in this variable,
     * and not in the {@code Entity} itself.
     *
     * <p>This value is set to the entity upon the {@linkplain #commit() commit()}.
     */
    private LifecycleFlags lifecycleFlags;

    /**
     * The flag, which becomes {@code true}, if the state of the entity
     * {@linkplain #commit() has been changed} since it has been
     * {@linkplain RecordBasedRepository#findOrCreate(Object)} loaded or created.
     */
    private boolean stateChanged;

    /**
     * Allows to understand whether this transaction is active.
     *
     * <p>Has {@code true} value since the transaction instance creation
     * until {@linkplain #commit() commit()} is performed.
     */
    private boolean active = false;

    /**
     * An ordered collection of the phases that were propagated in scope of this transaction.
     *
     * <p>Contains all the phases, including failed.
     */
    private final List<Phase<I, E, S, B>> phases = newLinkedList();

    private TransactionListener<I, E, S, B> transactionListener;

    /**
     * Creates a new instance of {@code Transaction} and
     * {@linkplain TransactionalEntity#injectTransaction(Transaction) injects} the newly created
     * transaction into the given {@code entity}.
     *
     * <p>The entity state and attributes are set as starting values for this transaction.
     *
     * @param entity   the entity to create the transaction for
     * @see TransactionListener
     */
    protected Transaction(E entity) {
        checkNotNull(entity);

        this.entity = entity;
        this.builder = entity.builderFromState();
        this.version = entity.getVersion();
        this.lifecycleFlags = entity.getLifecycleFlags();
        this.initialLifecycleFlags = entity.getLifecycleFlags();
        this.active = true;

        this.transactionListener = new SilentWitness<>();

        injectTo(entity);
    }

    /**
     * Acts similar to {@linkplain Transaction#Transaction(TransactionalEntity)
     * an overloaded ctor}, but instead of using the original entity state and version,
     * this transaction will have the passed state and version as a starting point.
     *
     * <p>Note, that the given {@code state} and {@code version} are applied to the actual entity
     * upon commit.
     *
     * @param entity   the target entity to modify within this transaction
     * @param state    the entity state to set
     * @param version  the entity version to set
     */
    protected Transaction(E entity, S state, Version version) {
        this(entity);
        initAll(state, version);
    }

    /**
     * Dispatches the event message and its context to the current entity-in-transaction.
     *
     * <p>This operation is always performed in scope of an active transaction.
     *
     * @param entity  the target entity
     * @param event   the event to dispatch
     */
    protected abstract void dispatch(E entity, EventEnvelope event);

    /**
     * Allows to understand whether this transaction is active.
     *
     * @return {@code true} if the transaction is active, {@code false} otherwise
     */
    @VisibleForTesting
    public boolean isActive() {
        return active;
    }

    LifecycleFlags getLifecycleFlags() {
        return lifecycleFlags;
    }

    E getEntity() {
        return entity;
    }

    /**
     * @return the version of the entity, modified within this transaction
     */
    Version getVersion() {
        return version;
    }

    List<Phase<I, E, S, B>> getPhases() {
        return ImmutableList.copyOf(phases);
    }

    /**
     * Applies all the outstanding modifications to the enclosed entity.
     *
     * @throws InvalidEntityStateException in case the new entity state is not valid
     * @throws IllegalStateException       in case of a generic error
     */
    protected void commit() throws InvalidEntityStateException, IllegalStateException {
        final B builder = getBuilder();

        if (builder.isDirty()) {
            commitChangedState(builder);
        } else {
            commitUnchangedState();
        }
    }

    /**
     * Commits this transaction and sets the new state to the entity.
     *
     * <p>In case if the commit is failed, the transaction is rolled back and the entity keeps
     * the current state.
     *
     * @param builder the {@link ValidatingBuilder} with the new state of the entity
     */
    private void commitChangedState(B builder) {
        try {
            EntityRecord previousRecord = createRecordWith(initialLifecycleFlags);
            S newState = builder.build();
            markStateChanged();
            Version pendingVersion = getVersion();
            beforeCommit(newState, pendingVersion);
            entity.updateState(newState, pendingVersion);
            commitAttributeChanges();
            EntityRecord newRecord = createRecordWith(lifecycleFlags);
            afterCommit(previousRecord, newRecord);
        } catch (ValidationException exception) {  /* Could only happen if the state
                                                      has been injected not using
                                                      the builder setters. */
            final InvalidEntityStateException invalidStateException = of(exception);
            rollback(invalidStateException);

            throw invalidStateException;
        } catch (@SuppressWarnings("OverlyBroadCatchBlock") // Catch all unexpected exceptions.
                 RuntimeException genericException) {
            rollback(genericException);
            throw illegalStateWithCauseOf(genericException);
        } finally {
            releaseTx();
        }
    }

    /**
     * Commits this transaction skipping the entity state update.
     *
     * <p>This method is called when none of the transaction phases has changed the entity state.
     */
    private void commitUnchangedState() {
        EntityRecord previousRecord = createRecordWith(initialLifecycleFlags);
        S unchanged = getEntity().getState();
        Version pendingVersion = getVersion();
        beforeCommit(unchanged, pendingVersion);
        if(!pendingVersion.equals(entity.getVersion())) {
            entity.updateState(unchanged, pendingVersion);
        }
        commitAttributeChanges();
        releaseTx();
        EntityRecord newRecord = createRecordWith(lifecycleFlags);
        afterCommit(previousRecord, newRecord);
    }

    private void beforeCommit(S newState, Version newVersion) {
        E entity = getEntity();
        LifecycleFlags newFlags = getLifecycleFlags();
        transactionListener.onBeforeCommit(entity, newState, newVersion, newFlags);
    }

    private void afterCommit(EntityRecord oldEntity, EntityRecord newEntity) {
        EntityRecordChange change = EntityRecordChange
                .newBuilder()
                .setPreviousValue(oldEntity)
                .setNewValue(newEntity)
                .build();
        transactionListener.onAfterCommit(change);
    }

    /**
     * Cancels the changes made within this transaction and removes the injected transaction object
     * from the enclosed entity.
     *
     * @param cause the reason of the rollback
     */
    void rollback(Throwable cause) {
        final S currentState = currentBuilderState();
        final TransactionListener<I, E, S, B> listener = getListener();
        listener.onTransactionFailed(cause, getEntity(), currentState,
                                     getVersion(), getLifecycleFlags());
        this.active = false;
        entity.releaseTransaction();
    }

    /**
     * Creates an {@link EntityRecord} for the entity under transaction.
     *
     * <p>Since an entity transaction delegates its {@link EntityWithLifecycle#getLifecycleFlags()}
     * method invocations to the transaction, both that method and the field {@link #lifecycleFlags}
     * evaluate into the same object, namely, the current {@link LifecycleFlags}, possibly changed
     * by this transaction. Thus, the value of {@link LifecycleFlags} should be manually set,
     * depending on the desired result.
     *
     * @param lifecycleFlags flags to include into the record
     * @return new {@link EntityRecord}
     * @see #lifecycleFlags
     * @see #initialLifecycleFlags
     */
    private EntityRecord createRecordWith(LifecycleFlags lifecycleFlags) {
        E entity = getEntity();
        Any entityId = Identifier.pack(entity.getId());
        Version version = entity.getVersion();
        Any state = pack(entity.getState());
        return EntityRecord.newBuilder()
                           .setEntityId(entityId)
                           .setVersion(version)
                           .setState(state)
                           .setLifecycleFlags(lifecycleFlags)
                           .build();
    }

    /**
     * Creates a new {@linkplain Phase transaction phase} for the given event
     * and propagates the phase.
     *
     * <p>In case of an exception, the {@linkplain #rollback(Throwable) transaction rollback}
{'fraction_non_alphanumeric': 0.0809870521143431, 'fraction_numerical': 0.0002869337541695061, 'mean_word_length': 2.7425503355704697, 'pattern_counts': {'":': 0, '<': 77, '<?xml version=': 0, '>': 77, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 16, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2140941', 'n_tokens_mistral': 7001, 'n_tokens_neox': 6794, 'n_words': 2558}