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

io.vanillabp.springboot.adapter.wiring.AbstractTaskWiring Maven / Gradle / Ivy

There is a newer version: 1.1.3
Show newest version
package io.vanillabp.springboot.adapter.wiring;

import io.vanillabp.spi.service.BpmnProcess;
import io.vanillabp.spi.service.MultiInstanceElement;
import io.vanillabp.spi.service.MultiInstanceIndex;
import io.vanillabp.spi.service.MultiInstanceTotal;
import io.vanillabp.spi.service.NoResolver;
import io.vanillabp.spi.service.TaskParam;
import io.vanillabp.spi.service.WorkflowService;
import io.vanillabp.springboot.adapter.Connectable;
import io.vanillabp.springboot.adapter.SpringBeanUtil;
import io.vanillabp.springboot.parameters.MethodParameter;
import io.vanillabp.springboot.parameters.MethodParameterFactory;
import io.vanillabp.springboot.utils.MutableStream;
import io.vanillabp.springboot.utils.TriFunction;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public abstract class AbstractTaskWiring {

    protected final ApplicationContext applicationContext;

    protected final SpringBeanUtil springBeanUtil;

    protected final M methodParameterFactory;
    
    public AbstractTaskWiring(
            final ApplicationContext applicationContext,
            final SpringBeanUtil springBeanUtil,
            final M methodParameterFactory) {

        this.applicationContext = applicationContext;
        this.springBeanUtil = springBeanUtil;
        this.methodParameterFactory = methodParameterFactory;
        
    }
    
    protected abstract Class getAnnotationType();

    private void connectConnectableToBean(
            final T connectable,
            final StringBuilder tested,
            final StringBuilder matching,
            final AtomicInteger matchingMethods,
            final String beanName,
            final Object bean,
            final BiFunction methodMatchesTaskDefinition,
            final BiFunction methodMatchesElementId,
            final BiFunction> validateParameters,
            final ConnectBean connect) {
        
        final Class beanClass = determineBeanClass(bean);
        
        Arrays
                .stream(beanClass.getMethods())
                .flatMap(method -> Arrays
                        .stream(method.getAnnotationsByType(getAnnotationType()))
                        .map(annotation -> Map.entry(method, annotation)))
                .peek(m -> {
                    if (!tested.isEmpty()) {
                        tested.append(", ");
                    }
                    tested.append(m.getKey().toString());
                })
                .filter(m -> methodMatchesTaskDefinition.apply(m.getKey(), m.getValue())
                        || methodMatchesElementId.apply(m.getKey(), m.getValue()))
                .peek(m -> {
                    if (!matching.isEmpty()) {
                        matching.append(", ");
                    }
                    matching.append(m.getKey().toString());
                })
                .filter(m -> matchingMethods.getAndIncrement() == 0)
                .map(m -> Map.entry(m.getKey(), validateParameters.apply(m.getKey(), m.getValue())))
                .forEach(m -> connect.connect(bean, m.getKey(), m.getValue()));
        
    }
    
    protected boolean methodMatchesTaskDefinition(
            final T connectable,
            final Method method,
            final A annotation) {
        
        if (!StringUtils.hasText(connectable.getTaskDefinition())) {
            return false;
        }

        return false;
        
    }
    
    protected boolean wireTask(
            final T connectable,
            final boolean allowNoMethodFound,
            final BiFunction methodMatchesTaskDefinition,
            final BiFunction methodMatchesElementId,
            final BiFunction> validateParameters,
            final ConnectBean connect) {
        
        final var tested = new StringBuilder();
        final var matching = new StringBuilder();
        final var matchingMethods = new AtomicInteger(0);

        springBeanUtil
                .getWorkflowAnnotatedBeans()
                .entrySet()
                .stream()
                .filter(bean -> isAboutConnectableProcess(
                        connectable.getBpmnProcessId(),
                        bean.getValue()))
                .forEach(bean -> {
                    connectConnectableToBean(
                            connectable,
                            tested,
                            matching,
                            matchingMethods,
                            bean.getKey(),
                            bean.getValue(),
                            methodMatchesTaskDefinition,
                            methodMatchesElementId,
                            validateParameters,
                            connect);
                });

        if (matchingMethods.get() > 1) {
            throw new RuntimeException(
                    "More than one method annotated with @"
                    + getAnnotationType().getName()
                    + " is matching task having "
                    + (StringUtils.hasText(connectable.getTaskDefinition())
                            ? "task-definition '" + connectable.getTaskDefinition()
                            : "element-id '" + connectable.getElementId())
                    + "' of process '"
                    + connectable.getBpmnProcessId()
                    + "': "
                    + matching);
        }
        
        if (matchingMethods.get() == 0) {
            if (allowNoMethodFound) {
                return false;
            }
            
            throw new RuntimeException(
                    "No public method annotated with @"
                    + getAnnotationType().getName()
                    + " is matching task having "
                    + (StringUtils.hasText(connectable.getTaskDefinition())
                            ? "task-definition '" + connectable.getTaskDefinition()
                            : "no task-definition but element-id '" + connectable.getElementId())
                    + "' of process '"
                    + connectable.getBpmnProcessId()
                    + "'. Tested for: "
                    + tested);
        }
        
        return true;

    }
    
    protected boolean isAboutConnectableProcess(
            final String bpmnProcessId,
            final Object bean) {
        
        final var beanClass = determineBeanClass(bean);
        final var workflowServiceAnnotations = beanClass.getAnnotationsByType(WorkflowService.class);

        return Arrays
                .stream(workflowServiceAnnotations)
                .flatMap(workflowServiceAnnotation ->
                        Stream.concat(
                                Stream.of(workflowServiceAnnotation.bpmnProcess()),
                                Arrays.stream(workflowServiceAnnotation.secondaryBpmnProcesses())))
                .anyMatch(annotation -> annotation.bpmnProcessId().equals(bpmnProcessId)
                        || (annotation.bpmnProcessId().equals(BpmnProcess.USE_CLASS_NAME)
                                && bpmnProcessId.equals(beanClass.getSimpleName())));

    }

    protected Class determineBeanClass(
            final Object bean) {
        
        final var proxyClass = bean.getClass();
        final var result = AopUtils.getTargetClass(bean);
        if (result != proxyClass) {
            return result;
        }
        return ClassUtils.getUserClass(bean);
        
    }
    
    protected boolean methodMatchesElementId(
            final T connectable,
            final Method method,
            final A annotation) {
        
        if (method.getName().equals(connectable.getElementId())) {
            return true;
        }

        return false;
        
    }
    
    protected List validateParameters(
            final Method method,
            @SuppressWarnings("unchecked") final TriFunction... map) {

        final var result = new LinkedList();
        final var unknown = new StringBuffer();

        final var index = new AtomicInteger(-1);
        
        final var parameters = MutableStream
                .from(Arrays.stream(method.getParameters()));
        
        parameters
                .apply(s -> s.peek(param -> index.incrementAndGet()));
        
        // apply all parameter filters
        Arrays
                .stream(map)
                .forEach(mapper -> {
                    parameters.apply(s -> s.filter(parameter -> {
                        final var mapped = mapper.apply(method, parameter, index.get());
                        if (mapped == null) {
                            return true;
                        }
                        result.add(mapped);
                        return false;
                    }));
                });
        
        parameters
                .getStream()
                .forEach(param -> {
                    if (!unknown.isEmpty()) {
                        unknown.append(", ");
                    }
                    unknown.append(index.get());
                    unknown.append(" (");
                    unknown.append(param.getType());
                    unknown.append(' ');
                    unknown.append(param.getName());
                    unknown.append(")");
                });
        
        if (!unknown.isEmpty()) {
            throw new RuntimeException(
                    "Unexpected parameter(s) in method '"
                    + method.getName()
                            + "': "
                    + unknown);
        }
        
        return result;

    }

    protected MethodParameter validateWorkflowAggregateParameter(
            final Class workflowAggregateClass,
            final Method method,
            final Parameter parameter,
            final int index) {
        
        if (workflowAggregateClass == null) {
            return null;
        }
        
        final var isWorkflowAggregate = workflowAggregateClass.isAssignableFrom(
                parameter.getType());
        if (!isWorkflowAggregate) {
            return null;
        }

        return methodParameterFactory
                .getWorkflowAggregateMethodParameter(
                        index,
                        parameter.getName());
        
    }

    protected MethodParameter validateTaskParam(
            final Method method,
            final Parameter parameter,
            final int index) {
        
        final var taskParamAnnotation = parameter.getAnnotation(TaskParam.class);
        if (taskParamAnnotation == null) {
            return null;
        }

        return methodParameterFactory
                .getTaskParameter(
                        index,
                        parameter.getName(),
                        taskParamAnnotation.value());
        
    }

    protected MethodParameter validateMultiInstanceTotal(
            final Method method,
            final Parameter parameter,
            final int index) {
        
        final var miTotalAnnotation = parameter.getAnnotation(MultiInstanceTotal.class);
        if (miTotalAnnotation == null) {
            return null;
        }

        return methodParameterFactory
                .getMultiInstanceTotalMethodParameter(
                        index,
                        parameter.getName(),
                        miTotalAnnotation.value());
        
    }

    protected MethodParameter validateMultiInstanceIndex(
            final Method method,
            final Parameter parameter,
            final int index) {

        final var miIndexAnnotation = parameter.getAnnotation(MultiInstanceIndex.class);
        if (miIndexAnnotation == null) {
            return null;
        }

        return methodParameterFactory
                .getMultiInstanceIndexMethodParameter(
                        index,
                        parameter.getName(),
                        miIndexAnnotation.value());
        
    }
    
    protected MethodParameter validateMultiInstanceElement(
            final Method method,
            final Parameter parameter,
            final int index) {
        
        final var miElementAnnotation = parameter.getAnnotation(MultiInstanceElement.class);
        if (miElementAnnotation == null) {
            return null;
        }

        if (!miElementAnnotation.resolverBean().equals(NoResolver.class)) {

            final var resolver = applicationContext
                    .getBean(miElementAnnotation.resolverBean());

            return methodParameterFactory
                    .getResolverBasedMultiInstanceMethodParameter(
                            index,
                            parameter.getName(),
                            resolver);

        } else if (!MultiInstanceElement.USE_RESOLVER.equals(miElementAnnotation.value())) {

            return methodParameterFactory
                    .getMultiInstanceElementMethodParameter(
                            index,
                            parameter.getName(),
                            miElementAnnotation.value());
            
        } else {
            
            throw new RuntimeException(
                    "Either attribute 'value' or 'resolver' of annotation @"
                    + MultiInstanceElement.class.getSimpleName()
                    + " has to be defined. Missing both at parameter "
                    + parameter.getName()
                    + " of method "
                    + method);
            
        }
        
    }
    
    protected Map.Entry, Class> determineAndValidateWorkflowAggregateAndServiceClass(
            final String bpmnProcessId) {

        final var tested = new StringBuilder();
        
        final var matchingServices = springBeanUtil
                .getWorkflowAnnotatedBeans()
                .entrySet()
                .stream()
                .peek(bean -> {
                    if (!tested.isEmpty()) {
                        tested.append(", ");
                    }
                    tested.append(bean.getKey());
                })
                .filter(bean -> isAboutConnectableProcess(bpmnProcessId, bean.getValue()))
                .map(bean -> determineWorkflowAggregateClass(bean.getValue()))
                .filter(this::isExtendingWorkflowServicePort)
                .collect(Collectors.groupingBy(
                        Entry::getValue,
                        Collectors.mapping(Entry::getKey, Collectors.toList())));

        if (matchingServices.isEmpty()) {
            throw new RuntimeException(
                    "No bean annotated with @WorkflowService(bpmnProcessId=\""
                    + bpmnProcessId
                    + "\"). Tested for: "
                    + tested);
        }
        
        if (matchingServices.size() != 1) {
            
            final var found = new StringBuilder();
            matchingServices
                    .entrySet()
                    .stream()
                    .peek(entry -> {
                        if (!found.isEmpty()) {
                            found.append("; ");
                        }
                        found.append(entry.getKey().getName());
                        found.append(" by ");
                    })
                    .flatMap(entry -> entry.getValue().stream())
                    .forEach(matchingService -> {
                        if (!found.isEmpty()) {
                            found.append(", ");
                        }
                        found.append(matchingService.getName());
                    });
            throw new RuntimeException(
                    "Multiple beans annotated with @WorkflowService(bpmnProcessId=\""
                    + bpmnProcessId
                    + "\") found having different generic parameters, but should all be the same: "
                    + found);
            
        }
        
        final var matchingService = matchingServices
                .entrySet()
                .iterator()
                .next();

        return Map.entry(
                matchingService.getKey(),
                matchingService.getValue().get(0));
        
    }
    
    protected Entry, Class> determineWorkflowAggregateClass(
            final Object bean) {

        final var serviceClass = determineBeanClass(bean);

        final var aggregateClassNames = new LinkedList();
        
        final var workflowAggregateClass = Arrays
                .stream(serviceClass.getAnnotationsByType(WorkflowService.class))
                .collect(Collectors.groupingBy(annotation -> annotation.workflowAggregateClass()))
                .keySet()
                .stream()
                .peek(aggregateClass -> aggregateClassNames.add(aggregateClass.getName()))
                .findFirst()
                .get();
        
        return Map.entry(
                serviceClass,
                workflowAggregateClass);

    }
    
    private boolean isExtendingWorkflowServicePort(
            final Entry, Class> classes) {

        return classes != null;

    }

    protected boolean isPrimaryProcessWiring(
            final String workflowModuleId,
            final String bpmnProcessId,
            final Class workflowServiceClass) {

        final var primaryBpmnProcessIds = Arrays
                .stream(workflowServiceClass.getAnnotationsByType(WorkflowService.class))
                .map(WorkflowService::bpmnProcess)
                .map(bpmnProcess -> bpmnProcess.bpmnProcessId().equals(BpmnProcess.USE_CLASS_NAME)
                        ? workflowServiceClass.getSimpleName()
                        : bpmnProcess.bpmnProcessId())
                .toList();
        if (primaryBpmnProcessIds.size() > 1) {
            final var bpmnProcessIds = primaryBpmnProcessIds
                    .stream()
                    .collect(Collectors.joining("', '"));
            throw new RuntimeException("In class '"
                    + workflowServiceClass.getName()
                    + (StringUtils.hasText(workflowModuleId)
                        ? ""
                        : "' of workflow module '")
                    + "' there is more than one @BpmnProcess annotation having "
                    + "set attribute 'primary' as true: '"
                    + bpmnProcessIds
                    + "'. Please have a look into "
                    + "the attribute's JavaDoc to learn about its meaning.");
        }
        
        return primaryBpmnProcessIds.get(0).equals(bpmnProcessId);
        
    }
    
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy