/* * 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.extension.internal.loader; import static com.google.common.base.Preconditions.checkArgument; import static java.lang.Boolean.parseBoolean; import static java.lang.String.format; import static java.lang.String.join; import static java.lang.Thread.currentThread; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.Optional.empty; import static java.util.Optional.of; import static java.util.Optional.ofNullable; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isEmpty; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.mule.runtime.api.i18n.I18nMessageFactory.createStaticMessage; import static org.mule.runtime.api.meta.model.display.LayoutModel.builder; import static org.mule.runtime.api.meta.model.parameter.ParameterRole.BEHAVIOUR; import static org.mule.runtime.config.spring.XmlConfigurationDocumentLoader.schemaValidatingDocumentLoader; import static org.mule.runtime.extension.api.util.XmlModelUtils.createXmlLanguageModel; import com.google.common.collect.ImmutableMap; import org.mule.metadata.api.ClassTypeLoader; import org.mule.metadata.api.model.MetadataType; import org.mule.runtime.api.component.ComponentIdentifier; import org.mule.runtime.api.exception.MuleRuntimeException; import org.mule.runtime.api.meta.Category; import org.mule.runtime.api.meta.MuleVersion; import org.mule.runtime.api.meta.model.ExtensionModel; import org.mule.runtime.api.meta.model.XmlDslModel; import org.mule.runtime.api.meta.model.declaration.fluent.ConfigurationDeclarer; 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.api.meta.model.declaration.fluent.ParameterDeclarer; import org.mule.runtime.api.meta.model.declaration.fluent.ParameterizedDeclarer; import org.mule.runtime.api.meta.model.display.LayoutModel; import org.mule.runtime.api.meta.model.parameter.ParameterRole; import org.mule.runtime.config.spring.XmlConfigurationDocumentLoader; import org.mule.runtime.config.spring.dsl.model.ComponentModel; import org.mule.runtime.config.spring.dsl.model.ComponentModelReader; import org.mule.runtime.config.spring.dsl.model.extension.xml.GlobalElementComponentModelModelProperty; import org.mule.runtime.config.spring.dsl.model.extension.xml.OperationComponentModelModelProperty; import org.mule.runtime.config.spring.dsl.model.extension.xml.XmlExtensionModelProperty; import org.mule.runtime.config.spring.dsl.processor.ConfigLine; import org.mule.runtime.config.spring.dsl.processor.xml.XmlApplicationParser; import org.mule.runtime.core.config.artifact.DefaultArtifactProperties; import org.mule.runtime.core.registry.SpiServiceRegistry; import org.mule.runtime.extension.api.declaration.type.ExtensionsTypeLoaderFactory; import org.mule.runtime.extension.api.exception.IllegalParameterModelDefinitionException; import org.mule.runtime.extension.api.loader.ExtensionLoadingContext; import org.mule.runtime.extension.internal.loader.catalog.loader.xml.TypesCatalogXmlLoader; import org.mule.runtime.extension.internal.loader.catalog.model.TypesCatalog; import org.w3c.dom.Document; import java.io.IOException; import java.net.URL; import java.time.LocalTime; import java.util.Calendar; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; /** * Describes an {@link ExtensionModel} by scanning an XML provided in the constructor * * @since 4.0 */ final class XmlExtensionLoaderDelegate { private static final String PARAMETER_NAME = "name"; private static final String PARAMETER_DEFAULT_VALUE = "defaultValue"; private static final String TYPE_ATTRIBUTE = "type"; private static final String MODULE_NAME = "name"; private static final String MODULE_PREFIX_ATTRIBUTE = "prefix"; private static final String MODULE_NAMESPACE_ATTRIBUTE = "namespace"; private static final String MODULE_NAMESPACE_NAME = "module"; protected static final String CONFIG_NAME = "config"; private static final ClassTypeLoader typeLoader = ExtensionsTypeLoaderFactory.getDefault().createTypeLoader(); private static final Map<String, MetadataType> defaultInputTypes = getCommonTypesBuilder() .build(); private static final Map<String, MetadataType> defaultOutputTypes = getCommonTypesBuilder() .put("void", typeLoader.load(Void.class)) .build(); private static final Map<String, ParameterRole> parameterRoleTypes = ImmutableMap.<String, ParameterRole>builder() .put("BEHAVIOUR", ParameterRole.BEHAVIOUR) .put("CONTENT", ParameterRole.CONTENT) .put("PRIMARY", ParameterRole.PRIMARY_CONTENT) .build(); private static final String CATEGORY = "category"; private static final String VENDOR = "vendor"; private static final String MIN_MULE_VERSION = "minMuleVersion"; private static final String DOC_DESCRIPTION = "doc:description"; private static final String PASSWORD = "password"; private static final String ROLE = "role"; private static final String ATTRIBUTE_USE = "use"; /** * ENUM used to discriminate which type of {@link ParameterDeclarer} has to be created (required or not). * * @see #getParameterDeclarer(ParameterizedDeclarer, Map) */ private enum UseEnum { REQUIRED, OPTIONAL, AUTO } private static ImmutableMap.Builder<String, MetadataType> getCommonTypesBuilder() { return ImmutableMap.<String, MetadataType>builder() .put("string", typeLoader.load(String.class)) .put("boolean", typeLoader.load(Boolean.class)) .put("datetime", typeLoader.load(Calendar.class)) .put("date", typeLoader.load(Date.class)) .put("integer", typeLoader.load(Integer.class)) .put("time", typeLoader.load(LocalTime.class)); } private static ParameterRole getRole(final String role) { if (!parameterRoleTypes.containsKey(role)) { throw new IllegalParameterModelDefinitionException(format("The parametrized role [%s] doesn't match any of the expected types [%s]", role, join(", ", parameterRoleTypes.keySet()))); } return parameterRoleTypes.get(role); } private static final ComponentIdentifier OPERATION_IDENTIFIER = ComponentIdentifier.builder().withNamespace(MODULE_NAMESPACE_NAME).withName("operation").build(); private static final ComponentIdentifier OPERATION_PROPERTY_IDENTIFIER = ComponentIdentifier.builder().withNamespace(MODULE_NAMESPACE_NAME).withName("property").build(); private static final ComponentIdentifier OPERATION_PARAMETERS_IDENTIFIER = ComponentIdentifier.builder().withNamespace(MODULE_NAMESPACE_NAME).withName("parameters").build(); private static final ComponentIdentifier OPERATION_PARAMETER_IDENTIFIER = ComponentIdentifier.builder().withNamespace(MODULE_NAMESPACE_NAME).withName("parameter").build(); private static final ComponentIdentifier OPERATION_BODY_IDENTIFIER = ComponentIdentifier.builder().withNamespace(MODULE_NAMESPACE_NAME).withName("body").build(); private static final ComponentIdentifier OPERATION_OUTPUT_IDENTIFIER = ComponentIdentifier.builder().withNamespace(MODULE_NAMESPACE_NAME).withName("output").build(); private static final ComponentIdentifier MODULE_IDENTIFIER = ComponentIdentifier.builder().withNamespace(MODULE_NAMESPACE_NAME).withName(MODULE_NAMESPACE_NAME) .build(); public static final String XSD_SUFFIX = ".xsd"; private static final String XML_SUFFIX = ".xml"; private static final String TYPES_XML_SUFFIX = "-catalog" + XML_SUFFIX; private final String modulePath; private Optional<TypesCatalog> typesCatalog; /** * @param modulePath relative path to a file that will be loaded from the current {@link ClassLoader}. Non null. */ public XmlExtensionLoaderDelegate(String modulePath) { checkArgument(!isEmpty(modulePath), "modulePath must not be empty"); this.modulePath = modulePath; } public void declare(ExtensionLoadingContext context) { // We will assume the context classLoader of the current thread will be the one defined for the plugin (which is not filtered // and will allow us to access any resource in it URL resource = getResource(modulePath); if (resource == null) { throw new IllegalArgumentException(format("There's no reachable XML in the path '%s'", modulePath)); } try { loadCustomTypes(); } catch (Exception e) { throw new IllegalArgumentException(format("The custom type file [%s] for the module '%s' cannot be read properly", getCustomTypeFilename(), modulePath), e); } Document moduleDocument = getModuleDocument(context, resource); XmlApplicationParser xmlApplicationParser = new XmlApplicationParser(new SpiServiceRegistry(), singletonList(currentThread().getContextClassLoader())); Optional<ConfigLine> parseModule = xmlApplicationParser.parse(moduleDocument.getDocumentElement()); if (!parseModule.isPresent()) { // This happens in org.mule.runtime.config.spring.dsl.processor.xml.XmlApplicationParser.configLineFromElement() throw new IllegalArgumentException(format("There was an issue trying to read the stream of '%s'", resource.getFile())); } ComponentModelReader componentModelReader = new ComponentModelReader(new DefaultArtifactProperties(emptyMap(), emptyMap(), emptyMap())); //TODO MULE-12291: we would, ideally, leave either the relative path (modulePath) or the URL to the current config file, rather than just the name of the file (which will be useless from a tooling POV) final String configFileName = modulePath.substring(modulePath.lastIndexOf("/") + 1); ComponentModel componentModel = componentModelReader.extractComponentDefinitionModel(parseModule.get(), configFileName); loadModuleExtension(context.getExtensionDeclarer(), componentModel); } private URL getResource(String resource) { return currentThread().getContextClassLoader().getResource(resource); } /** * Custom types might not exist for the current module, that's why it's handled with {@link Optional} * * @throws Exception */ private void loadCustomTypes() throws Exception { TypesCatalog typesCatalog = null; final String customTypes = getCustomTypeFilename(); final URL resourceCustomType = getResource(customTypes); if (resourceCustomType != null) { TypesCatalogXmlLoader typesCatalogXmlLoader = new TypesCatalogXmlLoader(); typesCatalog = typesCatalogXmlLoader.load(resourceCustomType); } this.typesCatalog = ofNullable(typesCatalog); } /** * Possible file with the custom types, works by convention. * * @return given a {@code modulePath} such as "module-custom-types.xml" returns "module-custom-types-types.xml". Not null */ private String getCustomTypeFilename() { return modulePath.replace(XML_SUFFIX, TYPES_XML_SUFFIX); } private Document getModuleDocument(ExtensionLoadingContext context, URL resource) { XmlConfigurationDocumentLoader xmlConfigurationDocumentLoader = schemaValidatingDocumentLoader(); try { final Set<ExtensionModel> extensions = new HashSet<>(context.getDslResolvingContext().getExtensions()); return xmlConfigurationDocumentLoader.loadDocument(extensions, resource.getFile(), resource.openStream()); } catch (IOException e) { throw new MuleRuntimeException( createStaticMessage(format("There was an issue reading the stream for the resource %s", resource.getFile()))); } } private void loadModuleExtension(ExtensionDeclarer declarer, ComponentModel moduleModel) { if (!moduleModel.getIdentifier().equals(MODULE_IDENTIFIER)) { throw new MuleRuntimeException(createStaticMessage(format("The root element of a module must be '%s', but found '%s'", MODULE_IDENTIFIER.toString(), moduleModel.getIdentifier().toString()))); } String name = moduleModel.getParameters().get(MODULE_NAME); String version = "4.0"; // TODO(fernandezlautaro): MULE-11010 remove version from ExtensionModel final String category = moduleModel.getParameters().get(CATEGORY); final String vendor = moduleModel.getParameters().get(VENDOR); final String minMuleVersion = moduleModel.getParameters().get(MIN_MULE_VERSION); declarer.named(name) .describedAs(getDescription(moduleModel)) .fromVendor(vendor) .withMinMuleVersion(new MuleVersion(minMuleVersion)) // TODO(fernandezlautaro): MULE-11010 remove minMuleVersion from .onVersion(version) .withCategory(Category.valueOf(category.toUpperCase())) .withXmlDsl(getXmlDslModel(moduleModel, name, version)); declarer.withModelProperty(new XmlExtensionModelProperty()); final Optional<ConfigurationDeclarer> configurationDeclarer = loadPropertiesFrom(declarer, moduleModel); if (configurationDeclarer.isPresent()) { loadOperationsFrom(configurationDeclarer.get(), moduleModel); } else { loadOperationsFrom(declarer, moduleModel); } } private XmlDslModel getXmlDslModel(ComponentModel moduleModel, String name, String version) { final Optional<String> prefix = ofNullable(moduleModel.getParameters().get(MODULE_PREFIX_ATTRIBUTE)); final Optional<String> namespace = ofNullable(moduleModel.getParameters().get(MODULE_NAMESPACE_ATTRIBUTE)); return createXmlLanguageModel(prefix, namespace, name, version); } private String getDescription(ComponentModel componentModel) { return componentModel.getParameters().getOrDefault(DOC_DESCRIPTION, ""); } private List<ComponentModel> extractGlobalElementsFrom(ComponentModel moduleModel) { return moduleModel.getInnerComponents().stream() .filter(child -> !child.getIdentifier().equals(OPERATION_PROPERTY_IDENTIFIER) && !child.getIdentifier().equals(OPERATION_IDENTIFIER)) .collect(Collectors.toList()); } private Optional<ConfigurationDeclarer> loadPropertiesFrom(ExtensionDeclarer declarer, ComponentModel moduleModel) { List<ComponentModel> globalElementsComponentModel = extractGlobalElementsFrom(moduleModel); List<ComponentModel> properties = moduleModel.getInnerComponents().stream() .filter(child -> child.getIdentifier().equals(OPERATION_PROPERTY_IDENTIFIER)) .collect(Collectors.toList()); if (!properties.isEmpty() || !globalElementsComponentModel.isEmpty()) { ConfigurationDeclarer configurationDeclarer = declarer.withConfig(CONFIG_NAME); configurationDeclarer.withModelProperty(new GlobalElementComponentModelModelProperty(globalElementsComponentModel)); properties.stream().forEach(param -> extractProperty(configurationDeclarer, param)); return of(configurationDeclarer); } return empty(); } private void loadOperationsFrom(HasOperationDeclarer declarer, ComponentModel moduleModel) { moduleModel.getInnerComponents().stream() .filter(child -> child.getIdentifier().equals(OPERATION_IDENTIFIER)) .forEach(operationModel -> extractOperationExtension(declarer, operationModel)); } private void extractOperationExtension(HasOperationDeclarer declarer, ComponentModel operationModel) { String operationName = operationModel.getNameAttribute(); OperationDeclarer operationDeclarer = declarer.withOperation(operationName); ComponentModel bodyComponentModel = operationModel.getInnerComponents() .stream() .filter(child -> child.getIdentifier().equals(OPERATION_BODY_IDENTIFIER)).findFirst() .orElseThrow(() -> new IllegalArgumentException(format("The operation '%s' is missing the <body> statement", operationName))); operationDeclarer.withModelProperty(new OperationComponentModelModelProperty(operationModel, bodyComponentModel)); operationDeclarer.describedAs(getDescription(operationModel)); extractOperationParameters(operationDeclarer, operationModel); extractOutputType(operationDeclarer, operationModel); } private void extractOperationParameters(OperationDeclarer operationDeclarer, ComponentModel componentModel) { Optional<ComponentModel> optionalParametersComponentModel = componentModel.getInnerComponents() .stream() .filter(child -> child.getIdentifier().equals(OPERATION_PARAMETERS_IDENTIFIER)).findAny(); if (optionalParametersComponentModel.isPresent()) { optionalParametersComponentModel.get().getInnerComponents() .stream() .filter(child -> child.getIdentifier().equals(OPERATION_PARAMETER_IDENTIFIER)) .forEach(param -> { final String role = param.getParameters().get(ROLE); extractParameter(operationDeclarer, param, getRole(role)); }); } } private void extractProperty(ParameterizedDeclarer parameterizedDeclarer, ComponentModel param) { extractParameter(parameterizedDeclarer, param, BEHAVIOUR); } private void extractParameter(ParameterizedDeclarer parameterizedDeclarer, ComponentModel param, ParameterRole role) { Map<String, String> parameters = param.getParameters(); String receivedInputType = parameters.get(TYPE_ATTRIBUTE); LayoutModel layoutModel = parseBoolean(parameters.get(PASSWORD)) ? builder().asPassword().build() : builder().build(); MetadataType parameterType = extractType(defaultInputTypes, receivedInputType); ParameterDeclarer parameterDeclarer = getParameterDeclarer(parameterizedDeclarer, parameters); parameterDeclarer.describedAs(getDescription(param)) .withLayout(layoutModel) .withRole(role) .ofType(parameterType); } /** * Giving a {@link ParameterDeclarer} for the parameter and the attributes in the {@code parameters}, this method will verify * the rules for the {@link #ATTRIBUTE_USE} where: * <ul> * <li>{@link UseEnum#REQUIRED} marks the attribute as required in the XSD, failing if leaved empty when consuming the * parameter/property. It can not be {@link UseEnum#REQUIRED} if the parameter/property has a {@link #PARAMETER_DEFAULT_VALUE} * attribute</li> * <li>{@link UseEnum#OPTIONAL} marks the attribute as optional in the XSD. Can be {@link UseEnum#OPTIONAL} if the * parameter/property has a {@link #PARAMETER_DEFAULT_VALUE} attribute</li> * <li>{@link UseEnum#AUTO} will default at runtime to {@link UseEnum#REQUIRED} if {@link #PARAMETER_DEFAULT_VALUE} attribute is * absent, otherwise it will be marked as {@link UseEnum#OPTIONAL}</li> * </ul> * * @param parameterizedDeclarer builder to declare the {@link ParameterDeclarer} * @param parameters attributes to consume the values from * @return the {@link ParameterDeclarer}, being created as required or optional with a default value if applies. */ private ParameterDeclarer getParameterDeclarer(ParameterizedDeclarer parameterizedDeclarer, Map<String, String> parameters) { final String parameterName = parameters.get(PARAMETER_NAME); final String parameterDefaultValue = parameters.get(PARAMETER_DEFAULT_VALUE); final UseEnum use = UseEnum.valueOf(parameters.get(ATTRIBUTE_USE)); if (UseEnum.REQUIRED.equals(use) && isNotBlank(parameterDefaultValue)) { throw new IllegalParameterModelDefinitionException(format("The parameter [%s] cannot have the %s attribute set to %s when it has a default value", parameterName, ATTRIBUTE_USE, UseEnum.REQUIRED)); } // Is required if either is marked as REQUIRED or it's marked as AUTO an doesn't have a default value boolean parameterRequired = UseEnum.REQUIRED.equals(use) || (UseEnum.AUTO.equals(use) && isBlank(parameterDefaultValue)); return parameterRequired ? parameterizedDeclarer.onDefaultParameterGroup().withRequiredParameter(parameterName) : parameterizedDeclarer.onDefaultParameterGroup().withOptionalParameter(parameterName) .defaultingTo(parameterDefaultValue); } private void extractOutputType(OperationDeclarer operationDeclarer, ComponentModel componentModel) { ComponentModel outputComponentModel = componentModel.getInnerComponents() .stream() .filter(child -> child.getIdentifier().equals(OPERATION_OUTPUT_IDENTIFIER)).findFirst() .orElseThrow(() -> new IllegalArgumentException("Having an operation without <output> is not supported")); String receivedOutputType = outputComponentModel.getParameters().get(TYPE_ATTRIBUTE); MetadataType metadataType = extractType(defaultOutputTypes, receivedOutputType); operationDeclarer.withOutput().describedAs(getDescription(outputComponentModel)) .ofType(metadataType); operationDeclarer.withOutputAttributes().ofType(typeLoader.load(Void.class)); } private MetadataType extractType(Map<String, MetadataType> types, String receivedType) { Optional<MetadataType> metadataType = empty(); if (types.containsKey(receivedType)) { metadataType = of(types.get(receivedType)); } else if (typesCatalog.isPresent()) { metadataType = typesCatalog.get().resolveType(receivedType); } if (!metadataType.isPresent()) { String errorMessage = format( "should not have reach here. Type obtained [%s] when supported default types are [%s].", receivedType, join(", ", types.keySet())); if (typesCatalog.isPresent()) { errorMessage += format(" Custom types [%s] doesn't have support for the specified [%s] type", getCustomTypeFilename(), receivedType); } throw new IllegalParameterModelDefinitionException(errorMessage); } return metadataType.get(); } }