/*
* 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.metadata;
import static java.lang.String.format;
import static java.lang.String.valueOf;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static org.mule.metadata.java.api.utils.JavaTypeUtils.getType;
import static org.mule.runtime.api.metadata.MetadataKeyBuilder.newKey;
import static org.mule.runtime.api.metadata.resolving.FailureCode.INVALID_METADATA_KEY;
import static org.mule.runtime.api.util.Preconditions.checkArgument;
import static org.mule.runtime.extension.api.dsql.DsqlParser.isDsqlQuery;
import static org.mule.runtime.module.extension.internal.util.IntrospectionUtils.getFieldValue;
import org.mule.metadata.api.model.BooleanType;
import org.mule.metadata.api.model.MetadataType;
import org.mule.metadata.api.model.ObjectType;
import org.mule.metadata.api.model.StringType;
import org.mule.metadata.api.visitor.MetadataTypeVisitor;
import org.mule.runtime.api.meta.model.ComponentModel;
import org.mule.runtime.api.meta.model.parameter.ParameterModel;
import org.mule.runtime.api.metadata.MetadataKey;
import org.mule.runtime.api.metadata.MetadataKeyBuilder;
import org.mule.runtime.api.metadata.MetadataResolvingException;
import org.mule.runtime.api.metadata.resolving.FailureCode;
import org.mule.runtime.api.util.Reference;
import org.mule.runtime.core.api.component.Component;
import org.mule.runtime.extension.api.annotation.metadata.MetadataKeyId;
import org.mule.runtime.extension.api.dsql.DsqlParser;
import org.mule.runtime.extension.api.dsql.DsqlQuery;
import org.mule.runtime.extension.api.metadata.NullMetadataKey;
import org.mule.runtime.extension.internal.property.MetadataKeyIdModelProperty;
import org.mule.runtime.extension.internal.property.MetadataKeyPartModelProperty;
import org.mule.runtime.module.extension.internal.loader.java.property.DeclaringMemberModelProperty;
import org.mule.runtime.module.extension.internal.loader.java.property.QueryParameterModelProperty;
import org.mule.runtime.module.extension.internal.runtime.objectbuilder.DefaultObjectBuilder;
import org.mule.runtime.module.extension.internal.runtime.resolver.StaticValueResolver;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Provides an instance of the annotated {@link MetadataKeyId} parameter type. The instance will be populated with all the
* corresponding values of the passed {@link Map} key.
*
* @since 4.0
*/
final class MetadataKeyIdObjectResolver {
private static final DsqlParser dsqlParser = DsqlParser.getInstance();
private final ComponentModel component;
private final List<ParameterModel> keyParts;
public MetadataKeyIdObjectResolver(ComponentModel component) {
checkArgument(component != null, "The ComponentModel cannot be null");
this.component = component;
this.keyParts = getMetadataKeyParts(component);
}
/**
* Given {@link MetadataKey}, return the populated key in the Type that the {@link Component}
* parameter requires.
*
* @param key the {@link MetadataKey} associated to the {@link MetadataKeyId}
* @return a new instance of the {@link MetadataKeyId} parameter {@code type} with the values of the passed {@link MetadataKey}
* @throws MetadataResolvingException if:
* <ul>
* <li>Parameter types is not instantiable</li>
* <li>{@param key} does not provide the required levels</li>
* </ul>
*/
public Object resolve(MetadataKey key) throws MetadataResolvingException {
if (isKeyLess()) {
return NullMetadataKey.ID;
}
final MetadataKeyIdModelProperty keyIdModelProperty = findMetadataKeyIdModelProperty(component);
MetadataType type = keyIdModelProperty.getType();
KeyMetadataTypeVisitor visitor = new KeyMetadataTypeVisitor(key.getId(), getType(type)) {
@Override
protected Map<Field, String> getFieldValuesMap() throws MetadataResolvingException {
return keyToFieldValueMap(key);
}
};
type.accept(visitor);
return visitor.getResultId();
}
/**
* Returns the populated key in the Type that the {@link Component} parameter requires by looking for default values, if no
* {@link MetadataKeyId} is present an empty value is returned since is a key less {@link Component}.
* <p>
* If a key should be built and there is at least one default value missing an {@link IllegalArgumentException} is thrown.
*
* @return a new instance of the {@link MetadataKeyId} parameter {@code type}.
* @throws MetadataResolvingException if the Parameter type is not instantiable.
* @throws IllegalArgumentException if cannot found the required default values for an specified key.
*/
public Object resolve() throws MetadataResolvingException {
if (isKeyLess()) {
return NullMetadataKey.ID;
}
if (!keyParts.stream().allMatch(p -> p.getDefaultValue() != null)) {
throw new IllegalArgumentException("Could not build metadata key from an object that does"
+ " not have a default value for all it's components.");
}
String id = keyParts.get(0).getDefaultValue().toString();
final MetadataKeyIdModelProperty keyIdModelProperty = findMetadataKeyIdModelProperty(component);
MetadataType type = keyIdModelProperty.getType();
KeyMetadataTypeVisitor visitor = new KeyMetadataTypeVisitor(id, getType(type)) {
@Override
protected Map<Field, String> getFieldValuesMap() {
return keyParts.stream()
.filter(p -> p.getModelProperty(DeclaringMemberModelProperty.class).isPresent())
.collect(toMap(p -> p.getModelProperty(DeclaringMemberModelProperty.class).get().getDeclaringField(),
p -> p.getDefaultValue().toString()));
}
};
type.accept(visitor);
return visitor.getResultId();
}
private MetadataKeyBuilder getKeyFromField(Object resolvedKey, DeclaringMemberModelProperty declaringMemberModelProperty)
throws Exception {
return newKey(valueOf(getFieldValue(resolvedKey, declaringMemberModelProperty.getDeclaringField().getName())));
}
/**
* Given a {@link Object} representing the resolved value for a {@link MetadataKey}, generates the {@link MetadataKey} object.
*
* @param resolvedKey
* @return {@link MetadataKey} reconstructed from the resolved object key
* @throws MetadataResolvingException
*/
MetadataKey reconstructKeyFromType(Object resolvedKey) throws MetadataResolvingException {
if (isKeyLess() || resolvedKey == null) {
return new NullMetadataKey();
}
if (keyParts.size() == 1) {
return newKey(valueOf(resolvedKey)).build();
}
MetadataKeyBuilder rootBuilder = null;
MetadataKeyBuilder childBuilder = null;
for (ParameterModel p : keyParts) {
try {
if (p.getModelProperty(DeclaringMemberModelProperty.class).isPresent()) {
MetadataKeyBuilder fieldBuilder =
getKeyFromField(resolvedKey, p.getModelProperty(DeclaringMemberModelProperty.class).get());
if (rootBuilder == null) {
rootBuilder = fieldBuilder;
childBuilder = rootBuilder;
} else {
childBuilder.withChild(fieldBuilder);
childBuilder = fieldBuilder;
}
}
} catch (Exception e) {
throw new MetadataResolvingException("Could not construct Metadata Key part for parameter " + p.getName(),
FailureCode.INVALID_METADATA_KEY, e);
}
}
return rootBuilder != null ? rootBuilder.build() : new NullMetadataKey();
}
private MetadataKeyIdModelProperty findMetadataKeyIdModelProperty(ComponentModel component)
throws MetadataResolvingException {
return component.getModelProperty(MetadataKeyIdModelProperty.class)
.orElseThrow(() -> buildException(format("Component '%s' doesn't have a MetadataKeyId "
+ "parameter associated", component.getName())));
}
private Object instantiateFromFieldValue(Class<?> metadataKeyType, Map<Field, String> fieldValueMap)
throws MetadataResolvingException {
try {
DefaultObjectBuilder objectBuilder = new DefaultObjectBuilder<>(metadataKeyType);
fieldValueMap.forEach((f, v) -> objectBuilder.addPropertyResolver(f.getName(), new StaticValueResolver<String>(v)));
return objectBuilder.build(null);
} catch (Exception e) {
throw buildException(format("MetadataKey object of type '%s' from the component '%s' could not be instantiated",
metadataKeyType.getSimpleName(), component.getName()),
e);
}
}
private Map<Field, String> keyToFieldValueMap(MetadataKey key) throws MetadataResolvingException {
final Map<String, Field> fieldParts = keyParts.stream()
.filter(p -> p.getModelProperty(DeclaringMemberModelProperty.class).isPresent())
.map(p -> p.getModelProperty(DeclaringMemberModelProperty.class).get().getDeclaringField())
.collect(toMap(Field::getName, identity()));
final Map<String, String> currentParts = getCurrentParts(key);
final List<String> missingParts = fieldParts.keySet()
.stream()
.filter(partName -> !currentParts.containsKey(partName))
.collect(toList());
if (!missingParts.isEmpty()) {
throw buildException(format("The given MetadataKey does not provide all the required levels. Missing levels: %s",
missingParts));
}
return currentParts.entrySet().stream().filter(keyEntry -> fieldParts.containsKey(keyEntry.getKey()))
.collect(toMap(keyEntry -> fieldParts.get(keyEntry.getKey()), Map.Entry::getValue));
}
private Map<String, String> getCurrentParts(MetadataKey key) throws MetadataResolvingException {
Map<String, String> keyParts = new HashMap<>();
keyParts.put(key.getPartName(), key.getId());
while (!key.getChilds().isEmpty()) {
if (key.getChilds().size() > 1) {
final List<String> keyNames = key.getChilds().stream().map(MetadataKey::getId).collect(toList());
throw buildException(format("MetadataKey used for Metadata resolution must only have one child per level. "
+ "Key '%s' has %s as children.", key.getId(), keyNames));
}
key = key.getChilds().iterator().next();
keyParts.put(key.getPartName(), key.getId());
}
return keyParts;
}
private MetadataResolvingException buildException(String message) {
return buildException(message, null);
}
private MetadataResolvingException buildException(String message, Exception cause) {
return cause == null ? new MetadataResolvingException(message, INVALID_METADATA_KEY)
: new MetadataResolvingException(message, INVALID_METADATA_KEY, cause);
}
private Optional<QueryParameterModelProperty> getQueryModelProperty() {
return component.getAllParameterModels().stream()
.filter(p -> p.getModelProperty(QueryParameterModelProperty.class).isPresent())
.map(p -> p.getModelProperty(QueryParameterModelProperty.class).get())
.findAny();
}
/**
* @return whether the resolver needs a {@link MetadataKey} or not
*/
public boolean isKeyLess() {
return keyParts.isEmpty();
}
private abstract class KeyMetadataTypeVisitor extends MetadataTypeVisitor {
private final Reference<Object> keyValueHolder = new Reference<>();
private final Reference<MetadataResolvingException> exceptionValueHolder = new Reference<>();
private String id;
private Class metadataKeyType;
public KeyMetadataTypeVisitor(String id, Class metadataKeyType) {
this.id = id;
this.metadataKeyType = metadataKeyType;
}
@Override
protected void defaultVisit(MetadataType metadataType) {
exceptionValueHolder.set(buildException(format("'%s' type is invalid for MetadataKeyId parameters, "
+ "use String type instead. Affecting component: '%s'",
metadataKeyType.getSimpleName(), component.getName())));
}
@Override
public void visitBoolean(BooleanType booleanType) {
keyValueHolder.set(Boolean.valueOf(id));
}
@Override
public void visitString(StringType stringType) {
if (metadataKeyType.isEnum()) {
keyValueHolder.set(Enum.valueOf(metadataKeyType, id));
} else if (getQueryModelProperty().isPresent() && isDsqlQuery(id)) {
DsqlQuery dsqlQuery = dsqlParser.parse(id);
keyValueHolder.set(dsqlQuery);
} else {
keyValueHolder.set(id);
}
}
@Override
public void visitObject(ObjectType objectType) {
try {
keyValueHolder.set(instantiateFromFieldValue(metadataKeyType, getFieldValuesMap()));
} catch (MetadataResolvingException e) {
exceptionValueHolder.set(e);
}
}
protected abstract Map<Field, String> getFieldValuesMap() throws MetadataResolvingException;
public Object getResultId() throws MetadataResolvingException {
if (exceptionValueHolder.get() != null) {
throw exceptionValueHolder.get();
}
return keyValueHolder.get();
}
}
private List<ParameterModel> getMetadataKeyParts(ComponentModel componentModel) {
return componentModel.getAllParameterModels().stream()
.filter(p -> p.getModelProperty(MetadataKeyPartModelProperty.class).isPresent())
.sorted((p1, p2) -> {
Optional<MetadataKeyPartModelProperty> mk1 = p1.getModelProperty(MetadataKeyPartModelProperty.class);
Optional<MetadataKeyPartModelProperty> mk2 = p2.getModelProperty(MetadataKeyPartModelProperty.class);
return mk1.get().getOrder() - mk2.get().getOrder();
})
.collect(toList());
}
}