/* * 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.util; import static java.lang.String.format; import static java.lang.Thread.currentThread; import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; import static org.apache.commons.collections.CollectionUtils.isEmpty; import static org.mule.metadata.api.utils.MetadataTypeUtils.getTypeId; import static org.mule.runtime.api.dsl.DslResolvingContext.getDefault; import static org.mule.runtime.api.message.Message.of; import static org.mule.runtime.core.DefaultEventContext.create; import static org.mule.runtime.core.api.transaction.TransactionConfig.ACTION_ALWAYS_BEGIN; import static org.mule.runtime.core.api.transaction.TransactionConfig.ACTION_ALWAYS_JOIN; import static org.mule.runtime.core.api.transaction.TransactionConfig.ACTION_JOIN_IF_POSSIBLE; import static org.mule.runtime.core.api.transaction.TransactionConfig.ACTION_NONE; import static org.mule.runtime.core.api.transaction.TransactionConfig.ACTION_NOT_SUPPORTED; import static org.mule.runtime.core.config.MuleManifest.getProductVersion; import static org.mule.runtime.core.util.ClassUtils.withContextClassLoader; import static org.mule.runtime.core.util.UUID.getUUID; import static org.mule.runtime.dsl.api.component.config.DefaultComponentLocation.fromSingleComponent; import static org.mule.runtime.module.extension.internal.loader.java.DefaultJavaExtensionModelLoader.TYPE_PROPERTY_NAME; import static org.mule.runtime.module.extension.internal.loader.java.DefaultJavaExtensionModelLoader.VERSION; import static org.springframework.util.ReflectionUtils.setField; import org.mule.metadata.api.model.ArrayType; import org.mule.metadata.api.model.MetadataType; import org.mule.runtime.api.dsl.DslResolvingContext; import org.mule.runtime.api.message.Message; import org.mule.runtime.api.meta.model.ComponentModel; import org.mule.runtime.api.meta.model.EnrichableModel; import org.mule.runtime.api.meta.model.ExtensionModel; import org.mule.runtime.api.meta.model.ModelProperty; import org.mule.runtime.api.meta.model.XmlDslModel; import org.mule.runtime.api.meta.model.config.ConfigurationModel; import org.mule.runtime.api.meta.model.connection.ConnectionProviderModel; import org.mule.runtime.api.meta.model.declaration.fluent.BaseDeclaration; import org.mule.runtime.api.meta.model.declaration.fluent.ExtensionDeclaration; import org.mule.runtime.api.meta.model.declaration.fluent.OperationDeclaration; import org.mule.runtime.api.meta.model.operation.OperationModel; import org.mule.runtime.api.meta.model.parameter.ParameterModel; import org.mule.runtime.api.meta.model.source.SourceModel; import org.mule.runtime.api.meta.model.util.IdempotentExtensionWalker; import org.mule.runtime.api.util.Reference; import org.mule.runtime.core.api.Event; import org.mule.runtime.core.api.MuleContext; import org.mule.runtime.core.api.construct.FlowConstruct; import org.mule.runtime.core.api.exception.MessagingExceptionHandler; import org.mule.runtime.core.api.lifecycle.LifecycleState; import org.mule.runtime.core.api.transaction.TransactionConfig; import org.mule.runtime.core.internal.metadata.NullMetadataResolverFactory; import org.mule.runtime.core.management.stats.FlowConstructStatistics; import org.mule.runtime.core.util.collection.ImmutableListCollector; import org.mule.runtime.extension.api.annotation.param.ConfigName; import org.mule.runtime.extension.api.connectivity.oauth.OAuthModelProperty; import org.mule.runtime.extension.api.exception.IllegalConfigurationModelDefinitionException; import org.mule.runtime.extension.api.exception.IllegalConnectionProviderModelDefinitionException; import org.mule.runtime.extension.api.exception.IllegalModelDefinitionException; import org.mule.runtime.extension.api.exception.IllegalOperationModelDefinitionException; import org.mule.runtime.extension.api.exception.IllegalSourceModelDefinitionException; import org.mule.runtime.extension.api.metadata.MetadataResolverFactory; import org.mule.runtime.extension.api.runtime.InterceptorFactory; import org.mule.runtime.extension.api.runtime.config.ConfigurationFactory; import org.mule.runtime.extension.api.runtime.connectivity.ConnectionProviderFactory; import org.mule.runtime.extension.api.runtime.operation.Interceptor; import org.mule.runtime.extension.api.runtime.operation.OperationExecutorFactory; import org.mule.runtime.extension.api.runtime.source.SourceFactory; import org.mule.runtime.extension.api.tx.OperationTransactionalAction; import org.mule.runtime.extension.api.tx.SourceTransactionalAction; import org.mule.runtime.module.extension.internal.loader.java.DefaultJavaExtensionModelLoader; import org.mule.runtime.module.extension.internal.loader.java.property.ClassLoaderModelProperty; import org.mule.runtime.module.extension.internal.loader.java.property.ConfigurationFactoryModelProperty; import org.mule.runtime.module.extension.internal.loader.java.property.ConnectionProviderFactoryModelProperty; import org.mule.runtime.module.extension.internal.loader.java.property.ConnectionTypeModelProperty; import org.mule.runtime.module.extension.internal.loader.java.property.ImplementingMethodModelProperty; import org.mule.runtime.module.extension.internal.loader.java.property.InterceptorsModelProperty; import org.mule.runtime.module.extension.internal.loader.java.property.MetadataResolverFactoryModelProperty; import org.mule.runtime.module.extension.internal.loader.java.property.NullSafeModelProperty; import org.mule.runtime.module.extension.internal.loader.java.property.OperationExecutorModelProperty; import org.mule.runtime.module.extension.internal.loader.java.property.RequireNameField; import org.mule.runtime.module.extension.internal.loader.java.property.SourceFactoryModelProperty; import org.mule.runtime.module.extension.internal.runtime.execution.OperationExecutorFactoryWrapper; import org.mule.runtime.module.extension.internal.runtime.resolver.ValueResolver; import com.google.common.collect.ImmutableList; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.function.Function; import java.util.function.Supplier; /** * Utilities for handling {@link ExtensionModel extensions} * * @since 3.7.0 */ public class MuleExtensionUtils { /** * @param componentModel a {@link ComponentModel} * @return Whether the {@code componentModel} returns a list of messages */ public static boolean returnsListOfMessages(ComponentModel componentModel) { MetadataType outputType = componentModel.getOutput().getType(); return outputType instanceof ArrayType && Message.class.getName().equals(getTypeId(((ArrayType) outputType).getType()).orElse(null)); } /** * Returns {@code true} if any of the items in {@code resolvers} return true for the {@link ValueResolver#isDynamic()} method * * @param resolvers a {@link Iterable} with instances of {@link ValueResolver} * @param <T> the generic type of the {@link ValueResolver} items * @return {@code true} if at least one {@link ValueResolver} is dynamic, {@code false} otherwise */ public static <T extends Object> boolean hasAnyDynamic(Iterable<ValueResolver<T>> resolvers) { for (ValueResolver resolver : resolvers) { if (resolver.isDynamic()) { return true; } } return false; } /** * @param parameterModel a {@link ParameterModel} * @return Whether the given parameter is null safe */ public static boolean isNullSafe(ParameterModel parameterModel) { return parameterModel.getModelProperties().stream().anyMatch(p -> p instanceof NullSafeModelProperty); } /** * Returns all the {@link ConnectionProviderModel} instances available for the given {@code configurationModel} plus the ones * globally defined at the {@code extensionModel}. * The {@link List} will first contain those defined at a {@link ConfigurationModel#getConnectionProviders()} level and finally the * ones at {@link ExtensionModel#getConnectionProviders()} * * @param extensionModel the {@link ExtensionModel} which owns the {@code configurationModel} * @param configurationModel a {@link ConfigurationModel} * @return a {@link List}. Might be empty but will never be {@code null} */ public static List<ConnectionProviderModel> getAllConnectionProviders(ExtensionModel extensionModel, ConfigurationModel configurationModel) { return ImmutableList.<ConnectionProviderModel>builder().addAll(configurationModel.getConnectionProviders()) .addAll(extensionModel.getConnectionProviders()).build(); } /** * Whether at least one {@link ConnectionProviderModel} in the given {@cod extensionModel} * supports OAuth authentication * * @param extensionModel a {@link ExtensionModel} * @return {@code true} if a {@link ConnectionProviderModel} exist which is OAuth enabled */ public static boolean supportsOAuth(ExtensionModel extensionModel) { Reference<ConnectionProviderModel> connectionProvider = new Reference<>(); new IdempotentExtensionWalker() { @Override protected void onConnectionProvider(ConnectionProviderModel model) { if (model.getModelProperty(OAuthModelProperty.class).isPresent()) { connectionProvider.set(model); stop(); } } }.walk(extensionModel); return connectionProvider.get() != null; } /** * Creates a new {@link List} of {@link Interceptor interceptors} using the factories returned by * {@link InterceptorsModelProperty} (if present). * * @param model the model on which {@link InterceptorsModelProperty} is to be invoked * @return an immutable {@link List} with instances of {@link Interceptor} */ public static List<Interceptor> createInterceptors(EnrichableModel model) { return model.getModelProperty(InterceptorsModelProperty.class) .map(p -> createInterceptors(p.getInterceptorFactories())) .orElse(ImmutableList.of()); } /** * Adds the given {@code interceptorFactory} to the {@code declaration} as the last interceptor in the list * * @param declaration a {@link BaseDeclaration} * @param interceptorFactory a {@link InterceptorFactory} */ public static void addInterceptorFactory(BaseDeclaration declaration, InterceptorFactory interceptorFactory) { getOrCreateInterceptorModelProperty(declaration).addInterceptorFactory(interceptorFactory); } /** * Adds the given {@code interceptorFactory} to the {@code declaration} at the given {@code position} * * @param declaration a {@link BaseDeclaration} * @param interceptorFactory a {@link InterceptorFactory} * @param position a valid list index */ public static void addInterceptorFactory(BaseDeclaration declaration, InterceptorFactory interceptorFactory, int position) { getOrCreateInterceptorModelProperty(declaration).addInterceptorFactory(interceptorFactory, position); } private static InterceptorsModelProperty getOrCreateInterceptorModelProperty(BaseDeclaration declaration) { InterceptorsModelProperty property = (InterceptorsModelProperty) declaration.getModelProperty(InterceptorsModelProperty.class).orElse(null); if (property == null) { property = new InterceptorsModelProperty(emptyList()); declaration.addModelProperty(property); } return property; } /** * Creates a new {@link List} of {@link Interceptor interceptors} using the {@code interceptorFactories} * * @param interceptorFactories a {@link List} with instances of {@link InterceptorFactory} * @return an immutable {@link List} with instances of {@link Interceptor} */ public static List<Interceptor> createInterceptors(List<InterceptorFactory> interceptorFactories) { if (isEmpty(interceptorFactories)) { return ImmutableList.of(); } return interceptorFactories.stream().map(InterceptorFactory::createInterceptor).collect(new ImmutableListCollector<>()); } public static Event getInitialiserEvent() { return getInitialiserEvent(null); } public static Event getInitialiserEvent(MuleContext muleContext) { FlowConstruct flowConstruct = new FlowConstruct() { // TODO MULE-9076: This is only needed because the muleContext is get from the given flow. @Override public MuleContext getMuleContext() { return muleContext; } @Override public String getServerId() { return "InitialiserServer"; } @Override public String getUniqueIdString() { return getUUID(); } @Override public String getName() { return "InitialiserEventFlow"; } @Override public LifecycleState getLifecycleState() { return null; } @Override public MessagingExceptionHandler getExceptionListener() { return null; } @Override public FlowConstructStatistics getStatistics() { return null; } }; return Event.builder(create(flowConstruct, fromSingleComponent("InitializerEvent"))).message(of(null)).flow(flowConstruct) .build(); } /** * Returns the {@link Method} that was used to declare the given {@code operationDeclaration}. * * @param operationDeclaration a {@link OperationDeclaration} * @return A {@link Method} or {@code null} if the {@code operationDeclaration} was defined by other means */ public static java.util.Optional<Method> getImplementingMethod(OperationDeclaration operationDeclaration) { return operationDeclaration.getModelProperty(ImplementingMethodModelProperty.class) .map(ImplementingMethodModelProperty::getMethod); } /** * If the {@code extensionModel} contains a {@link ClassLoaderModelProperty}, then it returns the {@link ClassLoader} associated * to such property. Otherwise, it returns the current TCCL * * @param extensionModel a {@link ExtensionModel} * @return a {@link ClassLoader} */ public static ClassLoader getClassLoader(ExtensionModel extensionModel) { return extensionModel.getModelProperty(ClassLoaderModelProperty.class).map(ClassLoaderModelProperty::getClassLoader) .orElse(currentThread().getContextClassLoader()); } /** * Executes the given {@code callable} using the {@link ClassLoader} associated to the {@code extensionModel} * * @param extensionModel a {@link ExtensionModel} * @param callable a {@link Callable} * @param <T> the generic type of the {@code callable}'s return type * @return the value returned by the {@code callable} * @throws Exception if the {@code callable} fails to execute */ public static <T> T withExtensionClassLoader(ExtensionModel extensionModel, Callable<T> callable) throws Exception { return withContextClassLoader(getClassLoader(extensionModel), callable); } public static void injectConfigName(EnrichableModel model, Object target, String configName) { model.getModelProperty(RequireNameField.class).ifPresent(property -> { final Field configNameField = property.getConfigNameField(); if (!configNameField.getDeclaringClass().isInstance(target)) { throw new IllegalConfigurationModelDefinitionException( format("field '%s' is annotated with @%s but not defined on an instance of type '%s'", configNameField.toString(), ConfigName.class.getSimpleName(), target.getClass().getName())); } configNameField.setAccessible(true); setField(configNameField, target, configName); }); } /** * Converts the given {@code action} to its equivalent transactional action as defined in {@link TransactionConfig} * * @param action a {@link OperationTransactionalAction} * @return a byte transactional action */ public static byte toActionCode(OperationTransactionalAction action) { switch (action) { case ALWAYS_JOIN: return ACTION_ALWAYS_JOIN; case JOIN_IF_POSSIBLE: return ACTION_JOIN_IF_POSSIBLE; case NOT_SUPPORTED: return ACTION_NOT_SUPPORTED; } throw new IllegalArgumentException("Unsupported action: " + action.name()); } /** * Converts the given {@code action} to its equivalent transactional action as defined in {@link TransactionConfig} * * @param action a {@link SourceTransactionalAction} * @return a byte transactional action */ public static byte toActionCode(SourceTransactionalAction action) { switch (action) { case ALWAYS_BEGIN: return ACTION_ALWAYS_BEGIN; case NONE: return ACTION_NONE; } throw new IllegalArgumentException("Unsupported action: " + action.name()); } /** * Tests the {@code configurationModel} for a {@link ConfigurationFactoryModelProperty} and * returns the contained {@link ConfigurationFactory}. * * @param configurationModel a {@link ConfigurationModel} * @return a {@link ConfigurationFactory} * @throws IllegalConfigurationModelDefinitionException if the {@code configurationModel} doesn't contain such model property */ public static ConfigurationFactory getConfigurationFactory(ConfigurationModel configurationModel) { return fromModelProperty(configurationModel, ConfigurationFactoryModelProperty.class, ConfigurationFactoryModelProperty::getConfigurationFactory, () -> new IllegalConfigurationModelDefinitionException( format("Configuration '%s' does not provide a %s", configurationModel.getName(), ConfigurationFactory.class .getName()))); } /** * Tests the given {@code model} for a {@link MetadataResolverFactoryModelProperty} and if present * it returns the contained {@link MetadataResolverFactory}. If no such property is found, then * a {@link NullMetadataResolverFactory} is returned * * @param model an enriched model * @return a {@link MetadataResolverFactory} */ public static MetadataResolverFactory getMetadataResolverFactory(EnrichableModel model) { return model.getModelProperty(MetadataResolverFactoryModelProperty.class) .map(MetadataResolverFactoryModelProperty::getMetadataResolverFactory) .orElse(new NullMetadataResolverFactory()); } /** * Tests the given {@code operationModel} for a {@link OperationExecutorModelProperty} and if present * it returns the enclosed {@link OperationExecutorFactory}. If no such property is found, then a * {@link IllegalOperationModelDefinitionException} is thrown. * * @param operationModel an {@link OperationModel} * @return a {@link OperationExecutorFactory} * @throws IllegalOperationModelDefinitionException if the operation is not properly enriched */ public static OperationExecutorFactory getOperationExecutorFactory(OperationModel operationModel) { OperationExecutorFactory executorFactory = fromModelProperty(operationModel, OperationExecutorModelProperty.class, OperationExecutorModelProperty::getExecutorFactory, () -> new IllegalOperationModelDefinitionException(format("Operation '%s' does not provide a %s", operationModel.getName(), OperationExecutorFactory.class .getSimpleName()))); return new OperationExecutorFactoryWrapper(executorFactory, createInterceptors(operationModel)); } /** * Tests the given {@code sourceModel} for a {@link SourceFactoryModelProperty} and if present * it returns the enclosed {@link SourceFactory}. If no such property is found, then a * {@link IllegalSourceModelDefinitionException} is thrown * * @param sourceModel a {@link SourceModel} * @return a {@link SourceFactory} * @throws IllegalSourceModelDefinitionException if the source is not properly enriched */ public static SourceFactory getSourceFactory(SourceModel sourceModel) { return fromModelProperty(sourceModel, SourceFactoryModelProperty.class, SourceFactoryModelProperty::getSourceFactory, () -> new IllegalSourceModelDefinitionException( format("Source '%s' does not provide a %s", sourceModel.getName(), SourceFactory.class.getSimpleName()))); } /** * Tests the given {@code connectionProviderModel} for a {@link ConnectionProviderFactoryModelProperty} and if present * it returns the enclosed {@link ConnectionProviderFactory}. If no such property is found, then a * {@link IllegalConnectionProviderModelDefinitionException} is thrown * * @param connectionProviderModel a {@link ConnectionProviderModel} * @return a {@link SourceFactory} * @throws IllegalConnectionProviderModelDefinitionException if the connection provider is not properly enriched */ public static ConnectionProviderFactory getConnectionProviderFactory(ConnectionProviderModel connectionProviderModel) { return fromModelProperty(connectionProviderModel, ConnectionProviderFactoryModelProperty.class, ConnectionProviderFactoryModelProperty::getConnectionProviderFactory, () -> new IllegalConnectionProviderModelDefinitionException( format("Connection Provider '%s' does not provide a %s", connectionProviderModel.getName(), ConnectionProviderFactory.class .getSimpleName()))); } /** * Tests the given {@code connectionProviderModel} for a {@link ConnectionTypeModelProperty} and if present * it returns the enclosed connection type. If no such property is found, then a * {@link IllegalConnectionProviderModelDefinitionException} is thrown * * @param connectionProviderModel a {@link ConnectionProviderModel} * @return a connection {@link Class} * @throws IllegalConnectionProviderModelDefinitionException if the connection provider is not properly enriched */ public static Class<?> getConnectionType(ConnectionProviderModel connectionProviderModel) { return fromModelProperty(connectionProviderModel, ConnectionTypeModelProperty.class, ConnectionTypeModelProperty::getConnectionType, () -> new IllegalConnectionProviderModelDefinitionException( format("Connection Provider '%s' does not specify a connection type", connectionProviderModel .getName()))); } private static <T, P extends ModelProperty> T fromModelProperty(EnrichableModel model, Class<P> modelPropertyType, Function<P, T> map, Supplier<? extends RuntimeException> exceptionSupplier) { return model.getModelProperty(modelPropertyType).map(map).orElseThrow(exceptionSupplier); } /** * Creates an exception that says that no {@link ClassLoader} was specified for the extension of the given {@code extensionName} * * @param extensionName the name of the offending extension * @return an {@link IllegalModelDefinitionException} */ public static IllegalModelDefinitionException noClassLoaderException(String extensionName) { return new IllegalModelDefinitionException("No ClassLoader was specified for extension " + extensionName); } /** * @return the extension's error namespace for a given {@link ExtensionModel} */ public static String getExtensionsErrorNamespace(ExtensionModel extensionModel) { return getExtensionsErrorNamespace(extensionModel.getXmlDslModel()); } /** * @return the extension's error namespace for a given {@link ExtensionDeclaration} */ public static String getExtensionsErrorNamespace(ExtensionDeclaration extensionDeclaration) { return getExtensionsErrorNamespace(extensionDeclaration.getXmlDslModel()); } private static String getExtensionsErrorNamespace(XmlDslModel dslModel) { return dslModel.getPrefix().toUpperCase(); } public static ExtensionModel loadExtension(Class<?> clazz) { return loadExtension(clazz, new HashMap<>()); } public static ExtensionModel loadExtension(Class<?> clazz, Map<String, Object> params) { params.put(TYPE_PROPERTY_NAME, clazz.getName()); params.put(VERSION, getProductVersion()); //TODO MULE-11797: as this utils is consumed from org.mule.runtime.module.extension.internal.capability.xml.schema.AbstractXmlResourceFactory.generateResource(org.mule.runtime.api.meta.model.ExtensionModel), this util should get dropped once the ticket gets implemented. final DslResolvingContext dslResolvingContext = getDefault(emptySet()); return new DefaultJavaExtensionModelLoader().loadExtensionModel(clazz.getClassLoader(), dslResolvingContext, params); } public static String getImplicitConfigurationProviderName(ExtensionModel extensionModel, ConfigurationModel implicitConfigurationModel) { return format("%s-%s-implicit", extensionModel.getName(), implicitConfigurationModel.getName()); } }