/*
* 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();
}
}