/*
* Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com
* The software in this package is published under the terms of the CPAL v1.0
* license, a copy of which has been included with this distribution in the
* LICENSE.txt file.
*/
package org.mule.runtime.module.extension.internal.loader.java;
import static java.lang.String.format;
import static java.util.stream.Collectors.toList;
import static org.mule.runtime.extension.api.util.ExtensionMetadataTypeUtils.isInputStream;
import static org.mule.runtime.module.extension.internal.util.IntrospectionUtils.getGenerics;
import static org.mule.runtime.module.extension.internal.util.IntrospectionUtils.getMethodReturnAttributesType;
import static org.mule.runtime.module.extension.internal.util.IntrospectionUtils.getMethodReturnType;
import static org.mule.runtime.module.extension.internal.util.IntrospectionUtils.isVoid;
import org.mule.metadata.api.model.MetadataType;
import org.mule.runtime.api.meta.model.declaration.fluent.Declarer;
import org.mule.runtime.api.meta.model.declaration.fluent.ExtensionDeclarer;
import org.mule.runtime.api.meta.model.declaration.fluent.HasOperationDeclarer;
import org.mule.runtime.api.meta.model.declaration.fluent.OperationDeclarer;
import org.mule.runtime.extension.api.annotation.Extensible;
import org.mule.runtime.extension.api.annotation.ExtensionOf;
import org.mule.runtime.extension.api.annotation.Streaming;
import org.mule.runtime.extension.api.annotation.execution.Execution;
import org.mule.runtime.extension.api.connectivity.TransactionalConnection;
import org.mule.runtime.extension.api.exception.IllegalOperationModelDefinitionException;
import org.mule.runtime.extension.api.exception.IllegalParameterModelDefinitionException;
import org.mule.runtime.extension.api.runtime.process.CompletionCallback;
import org.mule.runtime.extension.api.runtime.streaming.PagingProvider;
import org.mule.runtime.extension.internal.property.PagedOperationModelProperty;
import org.mule.runtime.module.extension.internal.loader.java.property.ExtendingOperationModelProperty;
import org.mule.runtime.module.extension.internal.loader.java.property.ImplementingMethodModelProperty;
import org.mule.runtime.module.extension.internal.loader.java.property.OperationExecutorModelProperty;
import org.mule.runtime.module.extension.internal.loader.java.type.ExtensionParameter;
import org.mule.runtime.module.extension.internal.loader.java.type.MethodElement;
import org.mule.runtime.module.extension.internal.loader.java.type.OperationContainerElement;
import org.mule.runtime.module.extension.internal.loader.java.type.WithOperationContainers;
import org.mule.runtime.module.extension.internal.loader.utils.ParameterDeclarationContext;
import org.mule.runtime.module.extension.internal.runtime.execution.ReflectiveOperationExecutorFactory;
import org.mule.runtime.module.extension.internal.util.IntrospectionUtils;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.core.ResolvableType;
/**
* Helper class for declaring operations through a {@link DefaultJavaModelLoaderDelegate}
*
* @since 4.0
*/
final class OperationModelLoaderDelegate extends AbstractModelLoaderDelegate {
private static final String OPERATION = "Operation";
private final Map<MethodElement, OperationDeclarer> operationDeclarers = new HashMap<>();
OperationModelLoaderDelegate(DefaultJavaModelLoaderDelegate delegate) {
super(delegate);
}
void declareOperations(ExtensionDeclarer extensionDeclarer, HasOperationDeclarer declarer,
WithOperationContainers operationContainers) {
operationContainers.getOperationContainers()
.forEach(operationContainer -> declareOperations(extensionDeclarer, declarer, operationContainer));
}
void declareOperations(ExtensionDeclarer extensionDeclarer, HasOperationDeclarer declarer,
OperationContainerElement operationsContainer) {
declareOperations(extensionDeclarer, declarer, operationsContainer.getDeclaringClass(), operationsContainer.getOperations(),
true);
}
void declareOperations(ExtensionDeclarer extensionDeclarer,
HasOperationDeclarer declarer,
final Class<?> methodOwnerClass,
List<MethodElement> operations,
boolean supportsConfig) {
for (MethodElement operationMethod : operations) {
Class<?> declaringClass = methodOwnerClass != null ? methodOwnerClass : operationMethod.getDeclaringClass();
checkOperationIsNotAnExtension(declaringClass);
final Method method = operationMethod.getMethod();
final Optional<ExtensionParameter> configParameter = loader.getConfigParameter(operationMethod);
final Optional<ExtensionParameter> connectionParameter = loader.getConnectionParameter(operationMethod);
if (loader.isInvalidConfigSupport(supportsConfig, configParameter, connectionParameter)) {
throw new IllegalOperationModelDefinitionException(format(
"Operation '%s' is defined at the extension level but it requires a config. "
+ "Remove such parameter or move the operation to the proper config",
method.getName()));
}
HasOperationDeclarer actualDeclarer =
(HasOperationDeclarer) loader.selectDeclarerBasedOnConfig(extensionDeclarer, (Declarer) declarer, configParameter,
connectionParameter);
if (operationDeclarers.containsKey(operationMethod)) {
actualDeclarer.withOperation(operationDeclarers.get(operationMethod));
continue;
}
final OperationDeclarer operation = actualDeclarer.withOperation(operationMethod.getAlias())
.withModelProperty(new ImplementingMethodModelProperty(method))
.withModelProperty(new OperationExecutorModelProperty(new ReflectiveOperationExecutorFactory<>(declaringClass,
method)));
loader.addExceptionEnricher(operationMethod, operation);
processComponentConnectivity(operation, operationMethod, operationMethod);
if (!processNonBlockingOperation(operation, operationMethod)) {
operation.blocking(true);
operation.withOutputAttributes().ofType(getMethodReturnAttributesType(method, loader.getTypeLoader()));
if (isAutoPaging(operationMethod)) {
operation.supportsStreaming(true).withOutput().ofType(getMethodReturnType(method, loader.getTypeLoader()));
addPagedOperationModelProperty(operationMethod, operation, supportsConfig);
processPagingTx(operation, method);
} else {
final MetadataType outputType = getMethodReturnType(method, loader.getTypeLoader());
operation.withOutput().ofType(outputType);
handleByteStreaming(method, operation, outputType);
}
}
addExecutionType(operation, operationMethod);
ParameterDeclarationContext declarationContext = new ParameterDeclarationContext(OPERATION, operation.getDeclaration());
loader.getMethodParametersLoader().declare(operation, operationMethod.getParameters(), declarationContext);
calculateExtendedTypes(declaringClass, method, operation);
operationDeclarers.put(operationMethod, operation);
}
}
private void handleByteStreaming(Method method, OperationDeclarer operation, MetadataType outputType) {
operation.supportsStreaming(isInputStream(outputType) || method.getAnnotation(Streaming.class) != null);
}
private boolean processNonBlockingOperation(OperationDeclarer operation, MethodElement operationMethod) {
List<ExtensionParameter> callbackParameters = operationMethod.getParameters().stream()
.filter(p -> CompletionCallback.class.equals(p.getType().getDeclaringClass()))
.collect(toList());
if (callbackParameters.isEmpty()) {
return false;
}
if (callbackParameters.size() > 1) {
throw new IllegalOperationModelDefinitionException(format(
"Operation '%s' defines more than one %s parameters. Only one is allowed",
operationMethod.getAlias(),
CompletionCallback.class.getSimpleName()));
}
if (!isVoid(operationMethod.getMethod())) {
throw new IllegalOperationModelDefinitionException(format(
"Operation '%s' has a parameter of type %s but is not void. Non-blocking operations have to be declared as void and the "
+ "return type provided through the callback",
operationMethod.getAlias(),
CompletionCallback.class.getSimpleName()));
}
ExtensionParameter callbackParameter = callbackParameters.get(0);
java.lang.reflect.Parameter methodParameter = (java.lang.reflect.Parameter) callbackParameter.getDeclaringElement();
List<MetadataType> genericTypes = getGenerics(methodParameter.getParameterizedType(), loader.getTypeLoader());
if (genericTypes.isEmpty()) {
throw new IllegalParameterModelDefinitionException(format("Generics are mandatory on the %s parameter of Operation '%s'",
CompletionCallback.class.getSimpleName(),
operationMethod.getAlias()));
}
operation.withOutput().ofType(genericTypes.get(0));
operation.withOutputAttributes().ofType(genericTypes.get(1));
operation.blocking(false);
handleByteStreaming(operationMethod.getMethod(), operation, genericTypes.get(0));
return true;
}
private void addExecutionType(OperationDeclarer operationDeclarer, MethodElement operationMethod) {
operationMethod.getAnnotation(Execution.class).ifPresent(a -> operationDeclarer.withExecutionType(a.value()));
}
private void checkOperationIsNotAnExtension(Class<?> operationType) {
if (operationType.isAssignableFrom(getExtensionType()) || getExtensionType().isAssignableFrom(operationType)) {
throw new IllegalOperationModelDefinitionException(
format("Operation class '%s' cannot be the same class (nor a derivative) of the extension class '%s",
operationType.getName(), getExtensionType().getName()));
}
}
private void calculateExtendedTypes(Class<?> actingClass, Method method, OperationDeclarer operation) {
ExtensionOf extensionOf = method.getAnnotation(ExtensionOf.class);
if (extensionOf == null) {
extensionOf = actingClass.getAnnotation(ExtensionOf.class);
}
if (extensionOf != null) {
operation.withModelProperty(new ExtendingOperationModelProperty(extensionOf.value()));
} else if (isExtensible()) {
operation.withModelProperty(new ExtendingOperationModelProperty(getExtensionType()));
}
}
private void addPagedOperationModelProperty(MethodElement operationMethod, OperationDeclarer operation,
boolean supportsConfig) {
if (!supportsConfig) {
throw new IllegalOperationModelDefinitionException(format(
"Paged operation '%s' is defined at the extension level but it requires a config, since connections "
+ "are required for paging",
operationMethod.getName()));
}
operation.withModelProperty(new PagedOperationModelProperty());
operation.requiresConnection(true);
}
private void processPagingTx(OperationDeclarer operation, Method method) {
ResolvableType connectionType = IntrospectionUtils.getMethodType(method).getGeneric(0);
operation.transactional(TransactionalConnection.class.isAssignableFrom(connectionType.getRawClass()));
}
private boolean isAutoPaging(MethodElement operationMethod) {
return PagingProvider.class.isAssignableFrom(operationMethod.getReturnType());
}
private boolean isExtensible() {
return getExtensionType().getAnnotation(Extensible.class) != null;
}
}