Contact Us 1-800-596-4880

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.)

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

}

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 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().

  }

}