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

io.fluxcapacitor.javaclient.modeling.Entity Maven / Gradle / Ivy

There is a newer version: 0.1072.0
Show newest version
/*
 * Copyright (c) Flux Capacitor IP B.V. or its affiliates. All Rights Reserved.
 *
 * 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 io.fluxcapacitor.javaclient.modeling;

import com.fasterxml.jackson.annotation.JsonIgnore;
import io.fluxcapacitor.common.api.HasMetadata;
import io.fluxcapacitor.common.api.Metadata;
import io.fluxcapacitor.common.api.modeling.Relationship;
import io.fluxcapacitor.common.reflection.ReflectionUtils;
import io.fluxcapacitor.javaclient.common.Message;
import io.fluxcapacitor.javaclient.common.serialization.DeserializingMessage;
import io.fluxcapacitor.javaclient.publishing.routing.RoutingKey;

import java.beans.Transient;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static io.fluxcapacitor.common.reflection.ReflectionUtils.getAnnotatedPropertyValue;
import static io.fluxcapacitor.common.reflection.ReflectionUtils.hasProperty;
import static io.fluxcapacitor.common.reflection.ReflectionUtils.readProperty;
import static io.fluxcapacitor.javaclient.common.Message.asMessage;
import static java.lang.String.format;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Optional.ofNullable;

public interface Entity {

    ThreadLocal loading = ThreadLocal.withInitial(() -> false);
    ThreadLocal applying = ThreadLocal.withInitial(() -> false);
    String AGGREGATE_ID_METADATA_KEY = "$aggregateId";
    String AGGREGATE_TYPE_METADATA_KEY = "$aggregateType";
    String AGGREGATE_SN_METADATA_KEY = "$sequenceNumber";

    static boolean isLoading() {
        return loading.get();
    }

    static boolean isApplying() {
        return applying.get();
    }

    static String getAggregateId(HasMetadata message) {
        return message.getMetadata().get(AGGREGATE_ID_METADATA_KEY);
    }

    static boolean hasSequenceNumber(HasMetadata message) {
        return message.getMetadata().containsKey(AGGREGATE_SN_METADATA_KEY);
    }

    static Class getAggregateType(HasMetadata message) {
        return Optional.ofNullable(message.getMetadata().get(AGGREGATE_TYPE_METADATA_KEY))
                .map(c -> ReflectionUtils.classForName(c, null)).orElse(null);
    }

    static Long getSequenceNumber(HasMetadata message) {
        return Optional.ofNullable(message.getMetadata().get(AGGREGATE_SN_METADATA_KEY))
                .map(Long::parseLong).orElse(null);
    }

    Object id();

    Class type();

    Entity withType(Class type);

    T get();

    @Transient
    default boolean isEmpty() {
        return get() == null;
    }

    @Transient
    default boolean isPresent() {
        return get() != null;
    }

    default Entity ifPresent(UnaryOperator> action) {
        if (get() == null) {
            return this;
        }
        return action.apply(this);
    }

    default  Optional mapIfPresent(Function, U> action) {
        if (get() == null) {
            return Optional.empty();
        }
        return Optional.ofNullable(action.apply(this));
    }

    String idProperty();

    Entity parent();

    default List> ancestors() {
        List> result = new ArrayList<>();
        for (Entity p = parent(); p != null; p = p.parent()) {
            result.add(p);
        }
        return result.reversed();
    }

    default  A ancestorValue(Class ancestorType) {
        for (Entity ancestor = this; ancestor != null; ancestor = ancestor.parent()) {
            if (ancestorType.isAssignableFrom(ancestor.type())) {
                return ancestorType.cast(ancestor.get());
            }
        }
        return null;
    }

    Collection aliases();

    @JsonIgnore
    default boolean isRoot() {
        return parent() == null;
    }

    @SuppressWarnings("rawtypes")
    default Entity root() {
        return Optional.ofNullable(parent()).map(Entity::root).orElse(this);
    }

    default String lastEventId() {
        return root().lastEventId();
    }

    default Long lastEventIndex() {
        return root().lastEventIndex();
    }

    default Long highestEventIndex() {
        return ofNullable(lastEventIndex()).or(
                () -> ofNullable(previous()).map(Entity::highestEventIndex)).orElse(null);
    }

    default Entity withEventIndex(Long index, String messageId) {
        return root().withEventIndex(index, messageId).findEntity(id(), type());
    }

    default Entity withSequenceNumber(long sequenceNumber) {
        return root().withSequenceNumber(sequenceNumber).findEntity(id(), type());
    }

    default Instant timestamp() {
        return root().timestamp();
    }

    default long sequenceNumber() {
        return root().sequenceNumber();
    }

    default Aggregate rootAnnotation() {
        return DefaultEntityHelper.getRootAnnotation(root().type());
    }

    default Entity previous() {
        return root().previous().findEntity(id(), type());
    }

    default Entity playBackToEvent(String eventId) {
        return playBackToCondition(aggregate -> Objects.equals(eventId, aggregate.lastEventId()))
                .orElseThrow(() -> new IllegalStateException(format(
                        "Could not load aggregate %s of type %s for event %s. Aggregate (%s) started at event %s",
                        id(), type().getSimpleName(), eventId, this, lastEventId())));
    }

    default Optional> playBackToCondition(Predicate> condition) {
        Entity result = this;
        while (result != null && !condition.test(result)) {
            result = result.previous();
        }
        return Optional.ofNullable(result);
    }

    Collection> entities();

    default Stream> allEntities() {
        return Stream.concat(Stream.of(this), entities().stream().flatMap(Entity::allEntities));
    }

    @SuppressWarnings("unchecked")
    default  Optional> getEntity(Object entityId) {
        return entityId == null ? Optional.empty() : allEntities().filter(
                e -> entityId.equals(e.id()) || e.aliases().contains(entityId)).findFirst().map(e -> (Entity) e);
    }

    default Set relationships() {
        if (get() == null) {
            return Collections.emptySet();
        }
        String id = id().toString();
        String type = type().getName();
        return allEntities().filter(e -> e.id() != null)
                .flatMap(e -> Stream.concat(Stream.of(e.id()), e.aliases().stream()))
                .map(entityId -> Relationship.builder()
                        .entityId(entityId.toString()).aggregateType(type).aggregateId(id).build())
                .collect(Collectors.toSet());
    }

    default Set associations(Entity previous) {
        Set result = new HashSet<>(relationships());
        result.removeAll(previous.relationships());
        return result;
    }

    default Set dissociations(Entity previous) {
        Set result = new HashSet<>(previous.relationships());
        result.removeAll(relationships());
        return result;
    }

    Entity update(UnaryOperator function);

    default Entity apply(Object... events) {
        return apply(List.of(events));
    }

    default Entity apply(Collection events) {
        Entity result = this;
        for (Object event : events) {
            result = result.apply(event);
        }
        return result;
    }

    default Entity apply(Object event) {
        return event instanceof DeserializingMessage d ? apply(d) : apply(asMessage(event));
    }

    default Entity apply(Object event, Metadata metadata) {
        return apply(new Message(event, metadata));
    }

    default Entity apply(DeserializingMessage eventMessage) {
        return apply(eventMessage.toMessage());
    }

    Entity apply(Message eventMessage);

    Entity commit();

     Entity assertLegal(Object command) throws E;

    default Entity assertAndApply(Object payloadOrMessage) {
        return assertLegal(payloadOrMessage).apply(payloadOrMessage);
    }

    default Entity assertAndApply(Object payload, Metadata metadata) {
        return assertAndApply(new Message(payload, metadata));
    }

    default Entity assertAndApply(Object... events) {
        return assertAndApply(List.of(events));
    }

    default Entity assertAndApply(Collection events) {
        Entity result = this;
        for (Object event : events) {
            result = result.assertAndApply(event);
        }
        return result;
    }

    default Iterable> possibleTargets(Object payload) {
        if (payload == null) {
            return emptyList();
        }
        for (Entity e : entities()) {
            if (e.isPossibleTarget(payload, false)) {
                return singletonList(e);
            }
        }
        List> result = new ArrayList<>();
        for (Entity e : entities()) {
            if (e.isPossibleTarget(payload, true)) {
                result.add(e);
            }
        }
        return switch (result.size()) {
            case 0 -> emptyList();
            case 1 -> result.subList(0, 1);
            default -> {
                result.sort(Comparator.>comparingInt(Entity::depth).reversed());
                yield result.subList(0, 1);
            }
        };
    }

    default int depth() {
        Entity parent = parent();
        return parent == null ? 0 : 1 + parent.depth();
    }

    private boolean isPossibleTarget(Object message, boolean includeEmpty) {
        if (message == null) {
            return false;
        }
        String idProperty = idProperty();
        if (idProperty == null) {
            return includeEmpty;
        }
        Object payload = message instanceof Message ? ((Message) message).getPayload() : message;
        Object id = id();
        if (id == null) {
            return includeEmpty && isEmpty() && hasProperty(idProperty, payload);
        }
        if (readProperty(idProperty, payload)
                .or(() -> getAnnotatedPropertyValue(payload, RoutingKey.class)).map(id::equals).orElse(false)) {
            return true;
        }
        if (isPresent() && !hasProperty(idProperty, payload)) {
            for (Entity e : entities()) {
                if (e.isPresent() && e.isPossibleTarget(message, includeEmpty)) {
                    return true;
                }
            }
        }
        return false;
    }

    @SuppressWarnings("unchecked")
    private  Entity findEntity(Object id, Class type) {
        return (Entity) allEntities().filter(e -> Objects.equals(e.id(), id) && e.type().isAssignableFrom(type))
                .findFirst().orElse(null);
    }

    @FunctionalInterface
    interface Validator {
        void validate(T model) throws E;
    }
}