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

io.vlingo.lattice.model.sourcing.Sourced Maven / Gradle / Ivy

Go to download

Tooling for reactive Domain-Driven Design projects that are highly concurrent. Includes compute grid, actor caching, spaces, cross-node cluster messaging, CQRS, and Event Sourcing support.

There is a newer version: 1.7.5
Show newest version
// Copyright © 2012-2020 VLINGO LABS. All rights reserved.
//
// This Source Code Form is subject to the terms of the
// Mozilla Public License, v. 2.0. If a copy of the MPL
// was not distributed with this file, You can obtain
// one at https://mozilla.org/MPL/2.0/.

package io.vlingo.lattice.model.sourcing;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiConsumer;
import java.util.function.Supplier;

import io.vlingo.actors.Actor;
import io.vlingo.actors.Stoppable;
import io.vlingo.actors.testkit.TestContext;
import io.vlingo.actors.testkit.TestState;
import io.vlingo.common.Completes;
import io.vlingo.common.Outcome;
import io.vlingo.lattice.model.ApplyFailedException;
import io.vlingo.lattice.model.ApplyFailedException.Applicable;
import io.vlingo.lattice.model.CompletionSupplier;
import io.vlingo.symbio.Metadata;
import io.vlingo.symbio.Source;
import io.vlingo.symbio.State;
import io.vlingo.symbio.store.Result;
import io.vlingo.symbio.store.StorageException;
import io.vlingo.symbio.store.journal.Journal;
import io.vlingo.symbio.store.journal.Journal.AppendResultInterest;

/**
 * Abstract base for all concrete types that support journaling and application of
 * {@code Source} instances. Provides abstracted {@code Journal} and and state
 * transition control for my concrete extender.
 * @param  the concrete type that is being sourced
 */
public abstract class Sourced extends Actor implements AppendResultInterest {
  private static final Map>>,Map>, BiConsumer, Source>>> registeredConsumers =
          new ConcurrentHashMap<>();

  private TestContext testContext;
  private int currentVersion;
  private SourcedTypeRegistry.Info journalInfo;
  private AppendResultInterest interest;

  /**
   * Register the means to apply {@code sourceType} instances for state transition
   * of {@code sourcedType} by means of a given {@code consumer}.
   * @param sourcedType the concrete {@code Class} type to which sourceType instances are applied
   * @param sourceType the concrete {@code Class} type to apply
   * @param consumer the {@code BiConsumer} used to perform the application of sourceType
   * @param  the type {@code >} of the sourced entity to apply to
   * @param  the type {@code >} of the source to be applied
   */
  @SuppressWarnings("unchecked")
  public static , SOURCE extends Source> void registerConsumer(
          final Class sourcedType,
          final Class sourceType,
          final BiConsumer consumer) {

    Map>, BiConsumer, Source>> sourcedTypeMap =
            registeredConsumers.get(sourcedType);

    if (sourcedTypeMap == null) {
      sourcedTypeMap = new ConcurrentHashMap<>();
      registeredConsumers.put((Class>>) sourcedType, sourcedTypeMap);
    }

    sourcedTypeMap.put((Class>) sourceType, (BiConsumer, Source>) consumer);
  }
  /*
   * @see io.vlingo.actors.Actor#start()
   */
  @Override
  public void start() {
    super.start();

    restore();
  }

  /*
   * @see io.vlingo.actors.Actor#viewTestStateInitialization(io.vlingo.actors.testkit.TestContext)
   */
  @Override
  public void viewTestStateInitialization(final TestContext context) {
    if (context != null) {
      testContext = context;
      testContext.initialReferenceValueOf(new CopyOnWriteArrayList<>());
    }
  }

  /*
   * @see io.vlingo.actors.Actor#viewTestState()
   */
  @Override
  public TestState viewTestState() {
    final TestState testState = new TestState();
    testState.putValue("applied", testContext.referenceValue());
    return testState;
  }

  /**
   * Construct my default state.
   */
  protected Sourced() {
    this.currentVersion = 0;
    this.journalInfo = info();
    this.interest = selfAs(AppendResultInterest.class);
  }

  /**
   * Apply all of the given {@code sources} to myself, which includes appending
   * them to my journal and reflecting the representative changes to my state.
   * @param sources the {@code List>} to apply
   */
  final protected void apply(final List> sources) {
    apply(sources, metadata(), null);
  }

  /**
   * Answer {@code Completes}, applying all of the given {@code sources} to myself,
   * which includes appending them to my journal and reflecting the representative changes
   * to my state, followed by the execution of a possible {@code andThen}.
   * @param sources the {@code List>} to apply
   * @param andThen the {@code Supplier} executed following the application of sources
   * @param  the return type of the andThen {@code Supplier}
   * @return {@code Completes}
   */
  final protected  Completes apply(final List> sources, final Supplier andThen) {
    return apply(sources, metadata(), andThen);
  }

  /**
   * Answer {@code Completes}, applying all of the given {@code sources} to myself,
   * which includes appending them to my journal and reflecting the representative changes
   * to my state, followed by the execution of a possible {@code andThen}.
   * @param sources the {@code List>} to apply
   * @param metadata the Metadata to apply along with source
   * @param andThen the {@code Supplier} executed following the application of sources
   * @param  the return type of the andThen {@code Supplier}
   * @return {@code Completes}
   */
  final protected  Completes apply(final List> sources, final Metadata metadata, final Supplier andThen) {
    beforeApply(sources);
    final Journal journal = journalInfo.journal();
    stowMessages(AppendResultInterest.class);
    journal.appendAllWith(streamName(), nextVersion(), sources, metadata, snapshot(), interest, CompletionSupplier.supplierOrNull(andThen, completesEventually()));
    return andThen == null ? null : completes();
  }

  /**
   * Apply the given {@code source} to myself, which includes appending it
   * to my journal and reflecting the representative changes to my state.
   * @param source the {@code Source} to apply
   */
  final protected void apply(final Source source) {
    apply(source, metadata(), null);
  }

  /**
   * Answer {@code Completes}, applying the given {@code source} to myself, which
   * includes appending it to my journal and reflecting the representative changes to my
   * state, followed by the execution of a possible {@code andThen}.
   * @param source the {@code Source} to apply
   * @param andThen the {@code Supplier} executed following the application of sources
   * @param  the return type of the andThen {@code Supplier}
   * @return {@code Completes}
   */
  final protected  Completes apply(final Source source, final Supplier andThen) {
    return apply(source, metadata(), andThen);
  }

  /**
   * Answer {@code Completes}, applying the given {@code source} to myself, which
   * includes appending it to my journal and reflecting the representative changes to my
   * state, followed by the execution of a possible {@code andThen}.
   * @param source the {@code Source} to apply
   * @param metadata the Metadata to apply along with source
   * @param andThen the {@code Supplier} executed following the application of sources
   * @param  the return type of the andThen {@code Supplier}
   * @return {@code Completes}
   */
  final protected  Completes apply(final Source source, final Metadata metadata, final Supplier andThen) {
    final List> toApply = wrap(source);
    beforeApply(toApply);
    final Journal journal = journalInfo.journal();
    stowMessages(AppendResultInterest.class);
    journal.appendAllWith(streamName(), nextVersion(), toApply, metadata, snapshot(), interest, CompletionSupplier.supplierOrNull(andThen, completesEventually()));
    return andThen == null ? null : completes();
  }

  /**
   * Answer a {@code List>} from the varargs {@code sources}.
   * @param sources the varargs {@code Source} of sources to answer as a {@code List>}
   * @return {@code List>}
   */
  @SuppressWarnings("unchecked")
  protected List> asList(final Source... sources) {
    return Arrays.asList(sources);
  }

  /**
   * Received after the full asynchronous evaluation of each {@code apply()}.
   * Override if notification is desired.
   */
  protected void afterApply() { }

  /**
   * Answer {@code Optional} that should be thrown
   * and handled by my {@code Supervisor}, unless it is empty. The default
   * behavior is to answer the given {@code exception}, which will be thrown.
   * Must override to change default behavior.
   * @param exception the ApplyFailedException
   * @return {@code Optional}
   */
  protected Optional afterApplyFailed(final ApplyFailedException exception) {
    return Optional.of(exception);
  }

  /**
   * Received prior to the evaluation of each {@code apply()} and by
   * default adds all applied {@code Source} {@code sources} to the
   * {@code TestContext reference}, if currently testing. The concrete
   * extender may override to implement different or additional behavior.
   * @param sources the {@code List>} ready to be applied
   */
  protected void beforeApply(final List> sources) {
    // override to be informed prior to apply evaluation
    if (testContext != null) {
      final List> all = testContext.referenceValue();
      all.addAll(sources);
      testContext.referenceValueTo(all);
    }
  }

  /**
   * Answer my {@code currentVersion}, which if zero indicates that the receiver
   * is being initially constructed or reconstituted.
   * @return int
   */
  protected int currentVersion() {
    return currentVersion;
  }

  /**
   * Answer my {@code Metadata}.
   * Must override if {@code Metadata} is to be supported.
   * @return Metadata
   */
  protected Metadata metadata() {
    return Metadata.nullMetadata();
  }

  /**
   * Answer my next version, which if one greater then my {@code currentVersion}.
   * @return int
   */
  protected int nextVersion() {
    return currentVersion + 1;
  }

  /**
   * Restores the initial state of the receiver by means of the {@code snapshot}.
   * Must override if snapshots are to be supported.
   * @param snapshot the {@code SNAPSHOT} holding the {@code Sourced} initial state
   * @param currentVersion the int current version of the receiver; may be helpful in determining
   * @param  the type of the snapshot
   */
  protected  void restoreSnapshot(final SNAPSHOT snapshot, final int currentVersion) {
    // OVERIDE FOR SNAPSHOT SUPPORT
  }

  /**
   * Answer a valid {@code SNAPSHOT} state instance if a snapshot should
   * be taken and persisted along with applied {@code Source} instance(s).
   * Must override if snapshots are to be supported.
   * @param  the type of the snapshot
   * @return {@code SNAPSHOT}
   */
  protected  SNAPSHOT snapshot() {
    return null;
  }

  /**
   * Answer my stream name. Must override.
   * @return String
   */
  protected abstract String streamName();

  /**
   * Answer a representation of a number of segments as a
   * composite stream name. The implementor of {@code streamName()}
   * would use this method if the its stream name is built from segments.
   * @param separator the String separator the insert between segments
   * @param streamNameSegments the varargs String of one or more segments
   * @return String
   */
  protected String streamNameFrom(final String separator, final String... streamNameSegments) {
    final StringBuilder builder = new StringBuilder();
    builder.append(streamNameSegments[0]);
    for (int idx = 1; idx < streamNameSegments.length; ++idx) {
      builder.append(separator).append(streamNameSegments[idx]);
    }
    return builder.toString();
  }

  //==================================
  // AppendResultInterest
  //==================================



  /**
   * FOR INTERNAL USE ONLY.
   */
  @Override
  public final  void appendResultedIn(
          final Outcome outcome, final String streamName, final int streamVersion,
          final Source source, final Optional snapshot, final Object supplier) {
      this.appendResultedIn(outcome, streamName, streamVersion, source, Metadata.nullMetadata(), snapshot, supplier);
  }

  /**
   * FOR INTERNAL USE ONLY.
   */
  @Override
  public final  void appendAllResultedIn(final Outcome outcome, final String streamName,
          final int streamVersion, final List> sources, final Optional snapshot, final Object supplier) {
    this.appendAllResultedIn(outcome, streamName, streamVersion, sources, Metadata.nullMetadata(), snapshot, supplier);
  }


  /**
   * FOR INTERNAL USE ONLY.
   */
  @Override
  public final  void appendResultedIn(
          final Outcome outcome, final String streamName, final int streamVersion,
          final Source source, final Metadata metadata, final Optional snapshot, final Object supplier) {
    //TODO handle metadata
    outcome
      .andThen(result -> {
        restoreSnapshot(snapshot, currentVersion);
        applyResultVersioned(source);
        afterApply();
        completeUsing(supplier);
        disperseStowedMessages();
        return result;
      })
      .otherwise(cause -> {
        final Applicable applicable = new Applicable<>(null, Arrays.asList(source), metadata, (CompletionSupplier) supplier);
        final String message = "Source (count 1) not appended for: " + type() + "(" + streamName() + ") because: " + cause.result + " with: " + cause.getMessage();
        final ApplyFailedException exception = new ApplyFailedException(applicable, message, cause);
        final Optional maybeException = afterApplyFailed(exception);
        disperseStowedMessages();
        if (maybeException.isPresent()) {
          logger().error(message, maybeException.get());
          throw maybeException.get();
        }
        logger().error(message, exception);
        return cause.result;
      });
  }

  /**
   * FOR INTERNAL USE ONLY.
   */
  @Override
  @SuppressWarnings({ "unchecked", "rawtypes" })
  public final  void appendAllResultedIn(final Outcome outcome, final String streamName,
          final int streamVersion,final List> sources, final Metadata metadata,
          final Optional snapshot, final Object supplier) {
    //TODO handle metadata
    outcome
      .andThen(result -> {
        restoreSnapshot(snapshot, currentVersion);
        for (final Source source : sources) {
          applyResultVersioned(source);
        }
        afterApply();
        completeUsing(supplier);
        disperseStowedMessages();
        return result;
      })
      .otherwise(cause -> {
        final Applicable applicable = new Applicable(null, sources, metadata, (CompletionSupplier) supplier);
        final String message = "Source (count " + sources.size() + ") not appended for: " + type() + "(" + streamName() + ") because: " + cause.result + " with: " + cause.getMessage();
        final ApplyFailedException exception = new ApplyFailedException(applicable, message, cause);
        final Optional maybeException = afterApplyFailed(exception);
        disperseStowedMessages();
        if (maybeException.isPresent()) {
          logger().error(message, maybeException.get());
          throw maybeException.get();
        }
        logger().error(message, exception);
        return cause.result;
      });
  }

  //==================================
  // internal implementation
  //==================================

  /**
   * Apply an individual {@code source} onto my concrete extender by means of
   * the {@code BiConsumer} of its registered {@code sourcedTypeMap}.
   * @param source the {@code Source} to apply
   */
  private  void applyResultVersioned(final Source source) {
    applySource(source);

    ++currentVersion;
  }

  private  void applySource(final Source source) {
    Class type = getClass();

    BiConsumer, Source> consumer = null;

    while (type != Sourced.class) {
      final Map>, BiConsumer, Source>> sourcedTypeMap =
              registeredConsumers.get(type);

      if (sourcedTypeMap != null) {
        consumer = sourcedTypeMap.get(source.getClass());
        if (consumer != null) {
          consumer.accept(this, source);
          break;
        }
      }

      type = type.getSuperclass();
    }

    if (consumer == null) {
      throw new IllegalStateException("No such Sourced type.");
    }
  }

  /**
   * Given that the {@code supplier} is non-null, execute it by completing the {@code CompletionSupplier}.
   * @param supplier the {@code CompletionSupplier} or null
   */
  private void completeUsing(final Object supplier) {
    if (supplier != null) {
      ((CompletionSupplier) supplier).complete();
    }
  }

  private SourcedTypeRegistry.Info info() {
    try {
      return stage().world().resolveDynamic(SourcedTypeRegistry.INTERNAL_NAME, SourcedTypeRegistry.class).info(getClass());
    } catch (Exception e) {
      final String message = getClass().getSimpleName() + ": Info not registered with SourcedTypeRegistry.";
      logger().error(message);
      throw new IllegalStateException(message);
    }
  }

  /**
   * Restore the state of my concrete extender from a possibly snaptshot
   * and stream of events.
   */
  private void restore() {
    stowMessages(Stoppable.class);

    journalInfo.journal.streamReader(getClass().getSimpleName())
      .andThenTo(reader -> reader.streamFor(streamName()))
      .andThenConsume(stream -> {
        restoreSnapshot(stream.snapshot);
        restoreFrom(journalInfo.entryAdapterProvider.asSources(stream.entries), stream.streamVersion);
        disperseStowedMessages();
      })
      .otherwiseConsume(stream -> {
        disperseStowedMessages();
      })
      .recoverFrom(cause -> {
        disperseStowedMessages();
        final String message = "Stream not recovered for: " + type() + "(" + streamName() + ") because: " + cause.getMessage();
        throw new StorageException(Result.Failure, message, cause);
      });
  }

  /**
   * Restore the state of my concrete extender from the {@code stream} and
   * set my {@code currentVersion}.
   * @param stream the {@code List> stream} from which state is restored
   * @param currentVersion the int to set as my currentVersion
   */
  private void restoreFrom(final List> stream, final int currentVersion) {
    for (final Source source : stream) {
      applySource(source);
    }

    this.currentVersion = currentVersion;
  }

  /**
   * Restores the initial state of the receiver by means of the {@code snapshot}.
   * @param snapshot the {@code Optional} holding the {@code Sourced} initial state
   */
  private void restoreSnapshot(final State snapshot) {
    if (snapshot != null && !snapshot.isNull()) {
      restoreSnapshot(journalInfo.stateAdapterProvider.fromRaw(snapshot), currentVersion);
    }
  }

  /**
   * Answer my type name.
   * @return String
   */
  private String type() {
    return getClass().getSimpleName();
  }

  /**
   * Answer {@code source} wrapped in a {@code List>}.
   * @param source the {@code Source} to wrap
   * @return {@code List>}
   */
  private List> wrap(final Source source) {
    return Arrays.asList(source);
  }

  /**
   * Answer {@code sources} wrapped in a {@code List>}.
   * @param sources the {@code Source[]} to wrap
   * @return {@code List>}
   */
  @SuppressWarnings("unused")
  private List> wrap(final Source[] sources) {
    return Arrays.asList(sources);
  }
}