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

dev.soffa.foundation.spring.service.OperationDispatcher Maven / Gradle / Ivy

There is a newer version: 0.17.31
Show newest version
package dev.soffa.foundation.spring.service;

import dev.soffa.foundation.annotation.DefaultTenant;
import dev.soffa.foundation.commons.*;
import dev.soffa.foundation.config.OperationsMapping;
import dev.soffa.foundation.context.Context;
import dev.soffa.foundation.context.ContextHolder;
import dev.soffa.foundation.context.DefaultOperationContext;
import dev.soffa.foundation.core.*;
import dev.soffa.foundation.core.action.PublishEvent;
import dev.soffa.foundation.core.model.Serialized;
import dev.soffa.foundation.error.ForbiddenException;
import dev.soffa.foundation.events.OnServiceStarted;
import dev.soffa.foundation.model.Event;
import dev.soffa.foundation.model.TenantId;
import dev.soffa.foundation.multitenancy.TenantHolder;
import dev.soffa.foundation.resource.Resource;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;

@Service
@AllArgsConstructor
public class OperationDispatcher implements Dispatcher, Resource {

    private final ApplicationContext context;
    private final SideEffectsHandler sideEffectsHandler;
    private static final Map DEFAULT_TENANT = new ConcurrentHashMap<>();
    private final AtomicReference operationsMapping = new AtomicReference<>(null);

    private OperationsMapping getOperations() {
        if (operationsMapping.get() == null) {
            operationsMapping.set(context.getBean(OperationsMapping.class));
        }
        return operationsMapping.get();
    }

    @SneakyThrows
    @SuppressWarnings("unchecked")
    @Override
    public  O dispatch(String operationName, Serialized input, String serializedContext) {
        Logger.app.info("Dispatching operation: %s", operationName);
        try {
            Operation op = getOperations().require(operationName);
            I deserialized = null;
            if (input.getData() != null) {
                deserialized = (I) Mappers.JSON_DEFAULT.deserialize(input.getData(), Class.forName(input.getType()));
            }
            Context context = Mappers.JSON_DEFAULT.deserialize(serializedContext, Context.class);
            return invoke(op, deserialized, context);
        }catch (Exception e) {
            Logger.app.error("Error while dispatching operation: %s", operationName, e);
            throw e;
        }
    }

    @Override
    public > O dispatch(Class operationClass, I input, Context ctx) {
        return invoke(getOperations().require(operationClass), input, ctx);
    }

    @Override
    public > O dispatch(Class operationClass, I input) {
        return dispatch(operationClass, input, ContextHolder.require());
    }

    @Override
    public > O invoke(T operation, I input, Context ctx) {

        if (operation == null) {
            return null;
        }
        if (ctx == null) {
            throw new ForbiddenException("No context provided");
        }

        String className = operation.getClass().getSimpleName();

        Logger.app.debug("Invoking operation %s [livemode=%s]", className, ctx.isLiveMode());

        if (!DEFAULT_TENANT.containsKey(className)) {
            DEFAULT_TENANT.put(className, AnnotationUtils.findAnnotation(operation.getClass(), DefaultTenant.class) != null);
        }


        if (DEFAULT_TENANT.get(className)) {
            return TenantHolder.useDefault(() -> apply(operation, input, ctx));
        } else {
            TenantId tenant = ctx.getTenant();
            if (tenant==null) {
                tenant = TenantId.DEFAULT;
            }
            TenantId override = operation.getTenant(input, ctx);
            if (override == null || TenantId.CONTEXT.equals(override)) {
                override = operation.getTenant(ctx);
            }
            if (override != null && !TenantId.CONTEXT.equals(override) && !tenant.equals(override)) {
                tenant = override;
                Logger.platform.debug("Tenant overriden for operation %s: %s", className, tenant);
            }
            return TenantHolder.use(tenant, () -> apply(operation, input, ctx));
        }

    }

    @SneakyThrows
    @Transactional
    protected > O apply(T operation, I input, @NonNull Context ctx) {
        String operationName = getOperations().getOperationId(operation);
        DefaultOperationContext opContext = new DefaultOperationContext(ctx, operation.getClass());
        operation.validate(input, ctx);
        O res = operation.handle(input, opContext);

        if (operation instanceof Recorded) {
            opContext.activity(operationName, null, input);
        }

        if (operation instanceof Broadcast) {
            String pubSubOperation = ctx.getServiceName() + "." + TextUtil.snakeCase(operationName) + ".success";
            opContext.delayed(
                DefaultIdGenerator.uuid(operationName),
                PublishEvent.class,
                new Event(pubSubOperation, Mappers.JSON_DEFAULT.serialize(res))
            );
        }
        boolean shouldEnqueue = !opContext.getSideEffects().isEmpty() && !(operation instanceof OnServiceStarted);
        if (shouldEnqueue) {
            sideEffectsHandler.enqueue(operationName,
                DigestUtil.md5(Mappers.JSON_DEFAULT.serialize(input)),
                opContext.getSideEffects(),
                ctx
            );
        }
        return res;
    }

    @Override
    public > O invoke(Class operationClass, I input, @NonNull Context ctx) {
        return dispatch(operationClass, input, ctx);
    }

    @Override
    public > O invoke(Class operationClass, I input) {
        return dispatch(operationClass, input, ContextHolder.require());
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy