Contact Free trial Login

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 each from the 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, by using the MetadataKey to identify what MetadataType is required by the app developer.

The OutputTypeResolver<T> and AttributesTypeResolver<T> interfaces are parameterized with the generic T, which has to match the type of the MetadataKeyId Parameter. For now, you should use type String for the generic because it is the most common MetadataKeyId type. Later, this documentation revisits this generic and explains in detail when the type should change to something different 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 you could also use the provided Configuration and Connection elements.

Using the OutputTypeResolver

Now that we have a TypesKeysResolver and an OutputTypeResolver, we can add output DataSense support on our Operations and Sources. The main restriction to using an OutputTypeResolver along with a given TypesKeysResolver is that both have to belong to the same category, so we can guarantee that the MetadataKey provided by the TypesKeysResolver 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);
  }

}

Here, 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 our 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 very 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

A Component’s full output is not only the payload but instead is a Result<Payload, Attributes>, and that is also taken into account in the dynamic Metadata resolution.

When an Operation or Source output has a dynamic Attributes structure, we can resolve its MetadataType by declaring an AttributesTypeResolver (already implemented in the example of OutputTypeResolver.java) and then adding a reference to it in the Operation or Source declaration.

In this example, our Book entity can be divided into the content of the Book and the attributes of the Book, each having their own structures. This division is done so the app developer can better understand and use the result of the operation, thinking of 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();
  }

}

For Sources, a declaration similar to the one used for the payload 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 User Defined MetadataKey

The case for user-defined MetadataKeys also applies for the Output of a Component. Looking back to the case of a query, we don’t have a pre-defined set of possible MetadataKeys, but instead have a Parameter whose value characterizes the Output type or structure.

For example, in our Database Connector we have the select operation, whose output depends on what entities are being 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 we can see that the Operation returns a List<Map<String, Object>, which makes sense because the result of a select query are multiple record entries, but in the SelectMetadataResolver we are not describing an ArrayType in the getOutputType method, but instead the MetadataType returned represents a single record structure.
Why is that?

Well, since we already know the Operation is returning an ArrayType (List, PagingProvider, etc.), you as a developer only have 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 will allow you greater reuse of the MetadataType resolvers and reduce the amount of code needed.

Take into account that the Attributes resolved will also be the attributes of the elements of the collection, and not the attributes of the Operation’s List output.

Resolving dynamic Output Metadata without MetadataKey

Just as we saw for the 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, in order to declare a keyless resolver we just 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().

  }

}

We use cookies to make interactions with our websites and services easy and meaningful, to better understand how they are used and to tailor advertising. You can read more and make your cookie choices here. By continuing to use this site you are giving us your consent to do this.