public class OutputEntityResolver
implements OutputTypeResolver<String>, AttributesTypeResolver<String> {
@Override
public String getCategoryName() {
return "Records";
}
@Override
public String getResolverName() {
return "OutputEntityResolver";
}
@Override
public MetadataType getOutputType(MetadataContext context, String key)
throws MetadataResolvingException, ConnectionException {
switch (key) {
case "Author_id":
return context.getTypeLoader().load(AuthorResult.class);
case "BookList_id":
return context.getTypeLoader().load(BookListResult.class);
case "Book_id":
return context.getTypeLoader().load(BookResult.class);
default:
throw new MetadataResolvingException("Unknown key:" + key, INVALID_METADATA_KEY);
}
}
@Override
public MetadataType getAttributesType(MetadataContext context, String key)
throws MetadataResolvingException, ConnectionException {
if ("Book_id".equals(key)){
return context.getTypeLoader().load(BookAttributes.class);
}
// Only Books have Attributes information
return context.getTypeBuilder().nullType().build();
}
}
Output Metadata
Output metadata is the type resolution for the result of a component. Each component can provide either static or dynamic metadata for the payload and attributes, isolated from each other.
Declaring an OutputTypeResolver and AttributesTypeResolver
Both OutputTypeResolver
and AttributesTypeResolver
implementations handle the requests for resolving a MetadataType
based on the information provided by the MetadataContext
and most importantly, using the MetadataKey
to identify the MetadataType
required by the end user (who is creating a Mule app). Refer to What is the Metadata of a Component for more information about the MetadataType
.
The OutputTypeResolver<T>
and AttributesTypeResolver<T>
interfaces are parameterized with the generic T
, which must match the type of the MetadataKeyId
parameter. The examples here use type String
for the generic because it is the most common MetadataKeyId
type. (Other documentation will revisit this generic and go in detail about when the type should change to something other than String
.)
The example above uses the MetadataContext
only to obtain the typeLoader
for describing a MetadataType based on a Java class, but it could also use the provided Configuration and Connection elements.
Using the OutputTypeResolver
After you have a TypeKeysResolver
and an OutputTypeResolver
, you can add output DataSense support to our Operations and Sources. The main restriction to using an OutputTypeResolver
with a given TypeKeysResolver
is that both must belong to the same category
, which guarantees that the MetadataKey
provided by the TypeKeysResolver
can be resolved to a MetadataType
by the OutputTypeResolver
:
Adding DataSense for the Result Payload
public class FetchOperations {
@OutputResolver(output = OutputEntityResolver.class)
public Map<String,Object> get(@Connection MyConnection connection,
@MetadataKeyId(EntityKeysResolver.class) String entityKind){
return connection.getClient().fetch(entityKind);
}
}
Above, the MetadataType for the output is resolved dynamically based on the entityKind
that is being fetched, so if the entityKind
is configured with the Author_id
key provided by the EntityKeysResolver
, then the OutputEntityResolver
will be invoked with that MetadataKey
, and the output for the get
operation will be resolved to an AuthorResult
ObjectType.
A source can use the same output type resolution for describing the MetadataType of the object being dispatched to the flow, and its declaration is similar:
@MetadataScope(keysResolver = EntityKeysResolver.class,
outputResolver = OutputEntityResolver.class)
public class ListenerSource extends Source<Map<String, Object>, Void> {
@MetadataKeyId
@Parameter
public String type;
@Connection
private ConnectionProvider<MetadataConnection> connection;
@Override
public void onStart(SourceCallback<Map<String, Object>, Void> sourceCallback) throws MuleException {
//...
}
@Override
public void onStop() {
//...
}
}
Both sources and operations have the same lifecycle for resolving the output type. The first step is to configure the MetadataKeyId
, and then the OutputTypeResolver
is invoked with that key in order to resolve the resulting entity MetadataType.
Adding DataSense for the Result Attributes
Dynamic metadata resolution takes into account that a component’s full output is a Result<Payload, Attributes>
, not just the payload.
When an operation or source output has a dynamic attributes structure, you can resolve its MetadataType by declaring an AttributesTypeResolver
(already implemented in the OutputTypeResolver.java
example) and then adding a reference to it in the operation or source declaration.
In the next example, the Book
entity can be divided into the content and attributes of the Book, each with its own structures. This division is done so the end user can better understand and use the result of the operation. They can think about the data (which is the content of the Book that goes in the payload) and the metadata that characterizes the payload (the attributes of the Book):
public class FetchOperationsWithAttributes {
@OutputResolver(output = OutputEntityResolver.class,
attributes = OutputEntityResolver.class)
public Result<Object, Object> get(@Connection MyConnection connection,
@MetadataKeyId(EntityKeysResolver.class) String entityKind){
if ("Book_id".equals(entityKind)){
Book book = (Book)connection.getClient().fetch(entityKind);
return Result.<Object, Object>builder()
.output(book.content())
.attributes(book.attributes())
.build();
}
return Result.<Object, Object>builder()
.output(connection.getClient().fetch(entityKind))
.build();
}
}
Sources have a declaration similar to the one used for the payload, but it adds
an attributesResolver
reference:
@MetadataScope(keysResolver = EntityKeysResolver.class,
outputResolver = OutputEntityResolver.class,
attributesResolver = OutputEntityResolver.class)
public class ListenerSource extends Source<Map<String, Object>, Object> {
@MetadataKeyId
@Parameter
public String type;
//...
}
Output Metadata with a User-Defined MetadataKey
The case for user-defined MetadataKeys also applies for the output of a component. In the case of a query, you do not have a predefined set of possible MetadataKeys. Instead, you have a parameter with a value that characterizes the output type or structure.
For example, the Database connector has the select
operation. Its output depends on what entities are queried:
@OutputResolver(output = SelectMetadataResolver.class)
public List<Map<String, Object>> select(@MetadataKeyId String sql, @Config DbConnector connector){
// ...
}
With the SelectMetadataResolver
declared as:
public class SelectMetadataResolver extends BaseDbMetadataResolver implements OutputTypeResolver<String> {
@Override
public String getCategoryName() {
return "DbCategory";
}
@Override
public String getResolverName() {
return "SelectResolver";
}
@Override
public MetadataType getOutputType(MetadataContext context, String query)
throws MetadataResolvingException, ConnectionException {
if (isEmpty(query)) {
throw new MetadataResolvingException("No Metadata available for an empty query", FailureCode.INVALID_METADATA_KEY);
}
ResultSetMetaData statementMetaData = getStatementMetadata(context, parseQuery(query));
if (statementMetaData == null) {
throw new MetadataResolvingException(format("Driver did not return metadata for the provided SQL: [%s]", query),
FailureCode.INVALID_METADATA_KEY);
}
ObjectTypeBuilder record = context.getTypeBuilder().objectType();
Map<String, MetadataType> recordModels = resolveRecordModels(statementMetaData);
recordModels.entrySet()
.forEach(e -> record.addField().key(e.getKey()).value(e.getValue()));
return record.build();
}
}
List Metadata Automatic Wrapping
In the select
example, you can see that the operation returns a List<Map<String, Object>
. This makes sense because the result of a select query is multiple record entries, but the SelectMetadataResolver
does not describe an ArrayType in the getOutputType
method. Instead the returned MetadataType represents a single record
structure.
Why is that?
As you might know already, the operation is returning an ArrayType (List, PagingProvider, and so on), so you only need to describe the generic
type of the array. The output and attributes TypeResolvers always resolve the MetadataType of the elements of the collection and not the collection type itself. This allows for greater reuse of the MetadataType resolvers and reduces the amount of code needed.
Take into account that the resolved attributes will also be the attributes of the elements of the collection, not attributes of the operation’s List
output.
Resolving Dynamic Output Metadata without MetadataKey
As with input, the output of an operation can be resolved without a specific MetadataKey
, being the dynamic type affected by the Configuration or Connection
of the Component. Again, to declare a keyless resolver, you simply skip the MetadataKeyId
parameter and ignore the MetadataKey in the TypeResolvers:
public class UserTypeResolver implements OutputTypeResolver, AttributesTypeResolver {
@Override
public String getCategoryName() {
return "User";
}
@Override
public MetadataType getOutputType(MetadataContext context, Object key)
throws MetadataResolvingException, ConnectionException {
// The `key` parameter will be `null` if the fetch is performed
// as a `KeyLess` Metadata resolution. We'll just ignore it.
String schema = getUserSchema(context);
return new JsonTypeLoader(schema).load("http://demo.user")
.orElseThrow(() -> new MetadataResolvingException("No Metadata is available for the User",
FailureCode.NO_DYNAMIC_TYPE_AVAILABLE));
}
@Override
public MetadataType getAttributesType(MetadataContext context, Object key)
throws MetadataResolvingException, ConnectionException {
// The `key` parameter will be `null` if the fetch is performed
// as a `KeyLess` Metadata resolution. We'll just ignore it.
String schema = getUserSchema(context);
return new JsonTypeLoader(schema).load("http://demo.attributes")
.orElseThrow(() -> new MetadataResolvingException("No Metadata is available for the User Attributes",
FailureCode.NO_DYNAMIC_TYPE_AVAILABLE));
}
private String getUserSchema(MetadataContext context) throws MetadataResolvingException, ConnectionException {
return context.<DemoConnection>getConnection()
.orElseThrow(() -> new MetadataResolvingException("A connection is required to resolve Metadata but none was provided",
FailureCode.INVALID_CONFIGURATION))
.describeUser();
}
}
public class UserOperations {
@OutputResolver(output = UserTypeResolver.class, attributes=UserTypeResolver.class)
public Result<Map<String,Object>, Object> getUser(@Connection DemoConnection connection){
User user = connection.getUser();
return Result.<Map<String,Object>, Object>.builder()
.output(user.personalInfo())
.attributes(user.accountInfo())
.build().
}
}