/* * 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.config.spring.dsl.model.extension.xml; import static java.lang.String.format; import static java.util.Collections.emptyMap; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; import static org.mule.metadata.api.utils.MetadataTypeUtils.isVoid; import static org.mule.runtime.api.component.ComponentIdentifier.builder; import static org.mule.runtime.config.spring.dsl.model.ApplicationModel.NAME_ATTRIBUTE; import static org.mule.runtime.internal.dsl.DslConstants.CORE_PREFIX; import static org.mule.runtime.internal.dsl.DslConstants.KEY_ATTRIBUTE_NAME; import static org.mule.runtime.internal.dsl.DslConstants.VALUE_ATTRIBUTE_NAME; import org.mule.runtime.api.component.ComponentIdentifier; import org.mule.runtime.api.meta.model.ExtensionModel; import org.mule.runtime.api.meta.model.config.ConfigurationModel; import org.mule.runtime.api.meta.model.operation.HasOperationModels; 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.parameter.ParameterRole; import org.mule.runtime.config.spring.dsl.model.ApplicationModel; import org.mule.runtime.config.spring.dsl.model.ComponentLocationVisitor; import org.mule.runtime.config.spring.dsl.model.ComponentModel; import org.mule.runtime.core.api.Event; import org.mule.runtime.core.api.processor.Processor; import org.mule.runtime.core.el.DataWeaveExpressionLanguageAdaptor; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; /** * A {@link MacroExpansionModuleModel} works tightly with a {@link ApplicationModel} to go over all the registered * {@link ExtensionModel}s that are XML based (see {@link XmlExtensionModelProperty}) looking for code to macro expand. * <p/> * For every occurrence that happens, it will expand the operations. * <p/> * This object works by handling {@link ComponentModel}s directly, consuming the {@link GlobalElementComponentModelModelProperty} * for the "config" elements while the {@link OperationComponentModelModelProperty} for the operations (aka: {@link Processor}s in * the XML file). * * @since 4.0 */ public class MacroExpansionModuleModel { /** * literal that represents the name of the global element for any given module. If the module's name is math, then the value of * this field will name the global element as <math:config ../> */ private static final String MODULE_CONFIG_GLOBAL_ELEMENT_NAME = "config"; private static final String MODULE_OPERATION_CONFIG_REF = "config-ref"; /** * Used to obtain the {@link ComponentIdentifier} element from the <module/>'s original {@ink ComponentModel} to be later added * in the macro expanded element (aka: <module-operation-chain ../>) so that the location set by the {@link ComponentLocationVisitor} * can properly set the paths for every element (even the macro expanded) */ public static final String ORIGINAL_IDENTIFIER = "ORIGINAL_IDENTIFIER"; private final ApplicationModel applicationModel; private final List<ExtensionModel> extensions; /** * From a mutable {@code applicationModel}, it will store it to apply changes when the {@link #expand()} method is executed. * * @param applicationModel to modify given the usages of elements that belong to the {@link ExtensionModel}s contained in the * {@code extensions} map. * @param extensions set with all the loaded {@link ExtensionModel}s from the deployment that will be filtered by looking up * only those that are coming from an XML context through the {@link XmlExtensionModelProperty} property. */ public MacroExpansionModuleModel(ApplicationModel applicationModel, Set<ExtensionModel> extensions) { this.applicationModel = applicationModel; this.extensions = extensions.stream() .filter(extensionModel -> extensionModel.getModelProperty(XmlExtensionModelProperty.class).isPresent()) .collect(toList()); } /** * Goes through the entire xml mule application looking for the message processors that can be expanded, and then takes care of * the global elements. */ public void expand() { for (int i = 0; i < extensions.size(); i++) { for (ExtensionModel extensionModel : extensions) { expand(extensionModel); } } } private void expand(ExtensionModel extensionModel) { final List<ComponentModel> moduleGlobalElements = getModuleGlobalElements(extensionModel); final Set<String> moduleGlobalElementsNames = moduleGlobalElements.stream().map(ComponentModel::getNameAttribute).collect(toSet()); createOperationRefEffectiveModel(extensionModel, moduleGlobalElementsNames); createConfigRefEffectiveModel(extensionModel, moduleGlobalElements, moduleGlobalElementsNames); } private void createOperationRefEffectiveModel(ExtensionModel extensionModel, Set<String> moduleGlobalElementsNames) { HashMap<Integer, ComponentModel> componentModelsToReplaceByIndex = new HashMap<>(); applicationModel.executeOnEveryMuleComponentTree(flowComponentModel -> { for (int i = 0; i < flowComponentModel.getInnerComponents().size(); i++) { ComponentModel operationRefModel = flowComponentModel.getInnerComponents().get(i); ComponentIdentifier identifier = operationRefModel.getIdentifier(); String identifierName = identifier.getName(); if (identifierName.equals(MODULE_CONFIG_GLOBAL_ELEMENT_NAME)) { // config elements will be worked later on, that's why we are skipping this element continue; } if (extensionModel.getXmlDslModel().getPrefix().equals(identifier.getNamespace())) { HasOperationModels hasOperationModels = extensionModel; final Optional<ConfigurationModel> configurationModel = extensionModel.getConfigurationModel(MODULE_CONFIG_GLOBAL_ELEMENT_NAME); if (configurationModel.isPresent()) { hasOperationModels = configurationModel.get(); } Optional<OperationModel> operationModel = hasOperationModels.getOperationModel(identifierName); if (operationModel.isPresent()) { ComponentModel replacementModel = createOperationInstance(operationRefModel, extensionModel, operationModel.get(), moduleGlobalElementsNames); componentModelsToReplaceByIndex.put(i, replacementModel); } else { // as the #executeOnEveryMuleComponentTree goes from bottom to top, before throwing an exception we need to check if // the current operationRefModel's parent is an operation of the current ExtensionModel, meaning that the role of the // parameter is either CONTENT or PRIMARY_CONTENT final ComponentIdentifier parentIdentifier = operationRefModel.getParent().getIdentifier(); final String parentIdentifierName = parentIdentifier.getName(); if (!hasOperationModels.getOperationModel(parentIdentifierName).isPresent()) { throw new IllegalArgumentException(format("The operation '%s' is missing in the module '%s'", identifierName, extensionModel.getName())); } } } } for (Map.Entry<Integer, ComponentModel> entry : componentModelsToReplaceByIndex.entrySet()) { entry.getValue().setParent(flowComponentModel); flowComponentModel.getInnerComponents().add(entry.getKey(), entry.getValue()); flowComponentModel.getInnerComponents().remove(entry.getKey() + 1); } componentModelsToReplaceByIndex.clear(); }); } private void createConfigRefEffectiveModel(ExtensionModel extensionModel, List<ComponentModel> moduleComponentModels, Set<String> moduleGlobalElementsNames) { applicationModel.executeOnEveryMuleComponentTree(componentModel -> { HashMap<ComponentModel, List<ComponentModel>> componentModelsToReplaceByIndex = new HashMap<>(); for (int i = 0; i < componentModel.getInnerComponents().size(); i++) { ComponentModel configRefModel = componentModel.getInnerComponents().get(i); ComponentIdentifier identifier = configRefModel.getIdentifier(); if (extensionModel.getXmlDslModel().getPrefix().equals(identifier.getNamespace())) { Map<String, String> propertiesMap = extractParameters(configRefModel, extensionModel .getConfigurationModel(MODULE_CONFIG_GLOBAL_ELEMENT_NAME) .get() .getAllParameterModels()); final Map<String, String> literalsParameters = getLiteralParameters(propertiesMap, emptyMap()); List<ComponentModel> replacementGlobalElements = createGlobalElementsInstance(configRefModel, moduleComponentModels, moduleGlobalElementsNames, literalsParameters); componentModelsToReplaceByIndex.put(configRefModel, replacementGlobalElements); } } for (Map.Entry<ComponentModel, List<ComponentModel>> entry : componentModelsToReplaceByIndex.entrySet()) { final int componentModelIndex = componentModel.getInnerComponents().indexOf(entry.getKey()); componentModel.getInnerComponents().addAll(componentModelIndex, entry.getValue()); componentModel.getInnerComponents().remove(componentModelIndex + entry.getValue().size()); } }); } private List<ComponentModel> createGlobalElementsInstance(ComponentModel configRefModel, List<ComponentModel> moduleGlobalElements, Set<String> moduleGlobalElementsNames, Map<String, String> literalsParameters) { List<ComponentModel> globalElementsModel = new ArrayList<>(); globalElementsModel.addAll(moduleGlobalElements.stream() .map(globalElementModel -> copyComponentModel(globalElementModel, configRefModel.getNameAttribute(), moduleGlobalElementsNames, literalsParameters)) .collect(Collectors.toList())); ComponentModel muleRootElement = configRefModel.getParent(); globalElementsModel.stream().forEach(componentModel -> { componentModel.setRoot(true); componentModel.setParent(muleRootElement); }); return globalElementsModel; } private List<ComponentModel> getModuleGlobalElements(ExtensionModel extensionModel) { List<ComponentModel> moduleGlobalElements = new ArrayList<>(); Optional<ConfigurationModel> config = extensionModel.getConfigurationModel(MODULE_CONFIG_GLOBAL_ELEMENT_NAME); if (config.isPresent() && config.get().getModelProperty(GlobalElementComponentModelModelProperty.class).isPresent()) { GlobalElementComponentModelModelProperty globalElementComponentModelModelProperty = config.get().getModelProperty(GlobalElementComponentModelModelProperty.class).get(); moduleGlobalElements = globalElementComponentModelModelProperty.getGlobalElements(); } return moduleGlobalElements; } /** * Takes a one liner call to any given message processor, expand it to creating a "module-operation-chain" scope which has the * set of properties, the set of parameters and the list of message processors to execute. * * @param operationRefModel message processor that will be replaced by a scope element named "module-operation-chain". * @param extensionModel extension that holds a possible set of <property/>s that has to be parametrized to the new scope. * @param operationModel operation that provides both the <parameter/>s and content of the <body/> * @param moduleGlobalElementsNames collection with the global components names (such as <http:config name="a"../>, * <file:config name="b"../>, <file:matcher name="c"../> and so on) that are contained within * the <module/> that will be macro expanded * @return a new component model that represents the old placeholder but expanded with the content of the <body/> */ private ComponentModel createOperationInstance(ComponentModel operationRefModel, ExtensionModel extensionModel, OperationModel operationModel, Set<String> moduleGlobalElementsNames) { final OperationComponentModelModelProperty operationComponentModelModelProperty = operationModel.getModelProperty(OperationComponentModelModelProperty.class).get(); final ComponentModel operationModuleComponentModel = operationComponentModelModelProperty .getBodyComponentModel(); List<ComponentModel> bodyProcessors = operationModuleComponentModel.getInnerComponents(); String configRefName = operationRefModel.getParameters().get(MODULE_OPERATION_CONFIG_REF); ComponentModel.Builder processorChainBuilder = new ComponentModel.Builder(); processorChainBuilder .setIdentifier(builder().withNamespace(CORE_PREFIX).withName("module-operation-chain").build()); processorChainBuilder.addParameter("returnsVoid", String.valueOf(isVoid(operationModel.getOutput().getType())), false); Map<String, String> propertiesMap = extractProperties(operationRefModel, extensionModel); Map<String, String> parametersMap = extractParameters(operationRefModel, operationModel.getAllParameterModels()); ComponentModel propertiesComponentModel = getParameterChild(propertiesMap, "module-operation-properties", "module-operation-property-entry"); ComponentModel parametersComponentModel = getParameterChild(parametersMap, "module-operation-parameters", "module-operation-parameter-entry"); processorChainBuilder.addChildComponentModel(propertiesComponentModel); processorChainBuilder.addChildComponentModel(parametersComponentModel); final Map<String, String> literalsParameters = getLiteralParameters(propertiesMap, parametersMap); for (ComponentModel bodyProcessor : bodyProcessors) { processorChainBuilder.addChildComponentModel(copyComponentModel(bodyProcessor, configRefName, moduleGlobalElementsNames, literalsParameters)); } for (Map.Entry<String, Object> customAttributeEntry : operationRefModel.getCustomAttributes().entrySet()) { processorChainBuilder.addCustomAttribute(customAttributeEntry.getKey(), customAttributeEntry.getValue()); } ComponentModel processorChainModel = processorChainBuilder.build(); for (ComponentModel processorChainModelChild : processorChainModel.getInnerComponents()) { processorChainModelChild.setParent(processorChainModel); } final String configFileName = operationComponentModelModelProperty.getOperationComponentModel().getConfigFileName() .orElseThrow(() -> new IllegalArgumentException(format("The is no config file name for the operation [%s] in the module [%s]", operationModel.getName(), extensionModel.getName()))); final Integer lineNumber = operationComponentModelModelProperty.getOperationComponentModel().getLineNumber() .orElseThrow(() -> new IllegalArgumentException(format("The is no line number for the operation [%s] in the module [%s]", operationModel.getName(), extensionModel.getName()))); processorChainBuilder.setConfigFileName(configFileName); processorChainBuilder.setLineNumber(lineNumber); processorChainBuilder.addCustomAttribute(ORIGINAL_IDENTIFIER, operationRefModel.getIdentifier()); return processorChainModel; } /** * @param propertiesMap <property>s that are feed in the current usage of the <module/> * @param parametersMap <param>s that are feed in the current usage of the <module/> * @return a {@link Map} of <property>s and <parameter>s that could be replaced by their literal values, see {@link #copyComponentModel(ComponentModel, String, Set, Map)} */ private Map<String, String> getLiteralParameters(Map<String, String> propertiesMap, Map<String, String> parametersMap) { final Map<String, String> literalsParameters = propertiesMap.entrySet().stream() .filter(entry -> !isExpression(entry.getValue())) .collect(Collectors.toMap(e -> getReplaceableExpression(e.getKey(), DataWeaveExpressionLanguageAdaptor.PROPERTIES), Map.Entry::getValue)); literalsParameters.putAll( parametersMap.entrySet().stream() .filter(entry -> !isExpression(entry.getValue())) .collect(Collectors.toMap( e -> getReplaceableExpression(e.getKey(), DataWeaveExpressionLanguageAdaptor.PARAMETERS), Map.Entry::getValue))); return literalsParameters; } /** * Assembly an expression to validate if the macro expansion of the current <module> can be directly replaced by the literals value * * @param name of the parameter (either a <property> or a <parameter>) * @param prefix binding to append for the expression to be replaced in the <module>'s code * @return the expression that access a variable through a direct binding (aka: a "static expression", as it doesn't use the {@link Event}) */ private String getReplaceableExpression(String name, String prefix) { return "#[" + prefix + "." + name + "]"; } private boolean isExpression(String value) { return value.startsWith("#[") && value.endsWith("]"); } private ComponentModel getParameterChild(Map<String, String> parameters, String wrapperParameters, String entryParameter) { ComponentModel.Builder parametersBuilder = new ComponentModel.Builder(); parametersBuilder .setIdentifier(builder().withNamespace(CORE_PREFIX).withName(wrapperParameters).build()); parameters.forEach((paramName, paramValue) -> { ComponentModel.Builder parameterBuilder = new ComponentModel.Builder(); parameterBuilder.setIdentifier(builder().withNamespace(CORE_PREFIX) .withName(entryParameter).build()); parameterBuilder.addParameter(KEY_ATTRIBUTE_NAME, paramName, false); parameterBuilder.addParameter(VALUE_ATTRIBUTE_NAME, paramValue, false); parametersBuilder.addChildComponentModel(parameterBuilder.build()); }); ComponentModel parametersComponentModel = parametersBuilder.build(); for (ComponentModel parameterComponentModel : parametersComponentModel.getInnerComponents()) { parameterComponentModel.setParent(parametersComponentModel); } return parametersComponentModel; } private Map<String, String> extractProperties(ComponentModel operationRefModel, ExtensionModel extensionModel) { Map<String, String> valuesMap = new HashMap<>(); // extract the <properties> String configParameter = operationRefModel.getParameters().get(MODULE_OPERATION_CONFIG_REF); if (configParameter != null) { ComponentModel configRefComponentModel = applicationModel.getRootComponentModel().getInnerComponents().stream() .filter(componentModel -> componentModel.getIdentifier().getNamespace().equals(extensionModel.getName()) && componentModel.getIdentifier().getName().equals(MODULE_CONFIG_GLOBAL_ELEMENT_NAME) && configParameter.equals(componentModel.getParameters().get(NAME_ATTRIBUTE))) .findFirst() .orElseThrow(() -> new IllegalArgumentException( format("There's no <%s:config> named [%s] in the current mule app", extensionModel.getName(), configParameter))); valuesMap .putAll(extractParameters(configRefComponentModel, extensionModel.getConfigurationModel(MODULE_CONFIG_GLOBAL_ELEMENT_NAME).get() .getAllParameterModels())); } return valuesMap; } /** * Iterates over the collection of {@link ParameterModel}s making a clear distinction between {@link ParameterRole#BEHAVIOUR} * and {@link ParameterRole#CONTENT} or {@link ParameterRole#PRIMARY_CONTENT} roles, where the former maps to simple attributes * while the latter are child elements. * <p/> * If the value of the parameter is missing, then it will try to pick up a default value (also from the * {@link ParameterModel#getDefaultValue()}) * * @param componentModel to look for the values * @param parameters collection of parameters to look for in the parametrized {@link ComponentModel} * @return a {@link Map} with the values to be macro expanded in the final mule application */ private Map<String, String> extractParameters(ComponentModel componentModel, List<ParameterModel> parameters) { Map<String, String> valuesMap = new HashMap<>(); for (ParameterModel parameterExtension : parameters) { String paramName = parameterExtension.getName(); String value = null; switch (parameterExtension.getRole()) { case BEHAVIOUR: if (componentModel.getParameters().containsKey(paramName)) { value = componentModel.getParameters().get(paramName); } break; case CONTENT: case PRIMARY_CONTENT: final Optional<ComponentModel> childComponentModel = componentModel.getInnerComponents().stream() .filter(cm -> paramName.equals(cm.getIdentifier().getName())) .findFirst(); if (childComponentModel.isPresent()) { value = childComponentModel.get().getTextContent(); } break; } if (value == null && (parameterExtension.getDefaultValue() != null)) { value = (String) parameterExtension.getDefaultValue(); } if (value != null) { valuesMap.put(paramName, value); } } return valuesMap; } /** * Goes over the {@code modelToCopy} by consuming the attributes as they are, unless some of them are actually targeting a * global component (such as a configuration), in which it will append the {@code configRefName} to that reference, which will * be the definitive name once the Mule application has been completely macro expanded in the final XML configuration. * * @param modelToCopy original source of truth that comes from the <module/> * @param configRefName name of the configuration being used in the Mule application * @param moduleGlobalElementsNames names of the <module/>s global component that will be macro expanded in the Mule application * @param literalsParameters {@link Map} with all he <property>s and <parameter>s that were feed with a literal value in the Mule * application's code. * @return a transformed {@link ComponentModel} from the {@code modelToCopy}, where the global element's attributes has been * updated accordingly (both global components updates plus the line number, and so on). If the value for some parameter can be * optimized by replacing it for the literal's value, it will be done as well using the {@code literalsParameters} */ private ComponentModel copyComponentModel(ComponentModel modelToCopy, String configRefName, Set<String> moduleGlobalElementsNames, Map<String, String> literalsParameters) { ComponentModel.Builder operationReplacementModel = new ComponentModel.Builder(); operationReplacementModel .setIdentifier(modelToCopy.getIdentifier()) .setTextContent(modelToCopy.getTextContent()); for (Map.Entry<String, Object> entry : modelToCopy.getCustomAttributes().entrySet()) { operationReplacementModel.addCustomAttribute(entry.getKey(), entry.getValue()); } for (Map.Entry<String, String> entry : modelToCopy.getParameters().entrySet()) { String value = calculateAttributeValue(configRefName, moduleGlobalElementsNames, entry.getValue()); final String optimizedValue = literalsParameters.getOrDefault(value, value); operationReplacementModel.addParameter(entry.getKey(), optimizedValue, false); } for (ComponentModel operationChildModel : modelToCopy.getInnerComponents()) { operationReplacementModel.addChildComponentModel( copyComponentModel(operationChildModel, configRefName, moduleGlobalElementsNames, literalsParameters)); } final String configFileName = modelToCopy.getConfigFileName() .orElseThrow(() -> new IllegalArgumentException("The is no config file name for the component to macro expand")); final Integer lineNumber = modelToCopy.getLineNumber() .orElseThrow(() -> new IllegalArgumentException("The is no line number for the component to macro expand")); operationReplacementModel.setConfigFileName(configFileName); operationReplacementModel.setLineNumber(lineNumber); ComponentModel componentModel = operationReplacementModel.build(); for (ComponentModel child : componentModel.getInnerComponents()) { child.setParent(componentModel); } return componentModel; } //TODO MULE-9849: until there's no clear way to check against the ComponentModel using the org.mule.runtime.config.spring.dsl.processor.AbstractAttributeDefinitionVisitor.onReferenceSimpleParameter(), we workaround the issue by checking every <module/>'s global element's name. private String calculateAttributeValue(String configRefNameToAppend, Set<String> moduleGlobalElementsNames, String originalValue) { return moduleGlobalElementsNames.contains(originalValue) ? originalValue.concat("-").concat(configRefNameToAppend) : originalValue; } }