Defining services

Services are configurations that can be reused across several classes. Talend Component Kit comes with a predefined set of services that you can easily use.

You can still define your own services under the service node of your component project. By default, the Component Kit Starter generates a dedicated class in your project in which you can implement services.

Built-in services

The framework provides built-in services that you can inject by type in components and actions.

Lisf of built-in services

Type Description

org.talend.sdk.component.api.service.cache.LocalCache

Provides a small abstraction to cache data that does not need to be recomputed very often. Commonly used by actions for UI interactions.

org.talend.sdk.component.api.service.dependency.Resolver

Allows to resolve a dependency from its Maven coordinates. It can either try to resolve a local file or (better) creates for you a preinitialized classloader.

javax.json.bind.Jsonb

A JSON-B instance. If your model is static and you don’t want to handle the serialization manually using JSON-P, you can inject that instance.

javax.json.spi.JsonProvider

A JSON-P instance. Prefer other JSON-P instances if you don’t exactly know why you use this one.

javax.json.JsonBuilderFactory

A JSON-P instance. It is recommended to use this one instead of a custom one to optimize memory usage and speed.

javax.json.JsonWriterFactory

A JSON-P instance. It is recommended to use this one instead of a custom one to optimize memory usage and speed.

javax.json.JsonReaderFactory

A JSON-P instance. It is recommended to use this one instead of a custom one to optimize memory usage and speed.

javax.json.stream.JsonParserFactory

A JSON-P instance. It is recommended to use this one instead of a custom one to optimize memory usage and speed.

javax.json.stream.JsonGeneratorFactory

A JSON-P instance. It is recommended to use this one instead of a custom one to optimize memory usage and speed.

org.talend.sdk.component.api.service.dependency.Resolver

Allows to resolve files from Maven coordinates (like dependencies.txt for component). Note that it assumes that the files are available in the component Maven repository.

org.talend.sdk.component.api.service.injector.Injector

Utility to inject services in fields marked with @Service.

org.talend.sdk.component.api.service.factory.ObjectFactory

Allows to instantiate an object from its class name and properties.

org.talend.sdk.component.api.service.record.RecordBuilderFactory

Allows to instantiate a record.

org.talend.sdk.component.api.service.record.RecordPointerFactory

Allows to instantiate a RecordPointer which enables to extract a data from a Record based on jsonpointer specification.

org.talend.sdk.component.api.service.record.RecordService

Some utilities to create records from another one. It is typically what is used when you want to add an entry in a record and passthrough the other ones. It also provides a nice RecordVisitor API for advanced cases.

org.talend.sdk.component.api.service.configuration.LocalConfiguration

Represents the local configuration that can be used during the design.

It is not recommended to use it for the runtime because the local configuration is usually different and the instances are distinct.

You can also use the local cache as an interceptor with @Cached

Every interface that extends HttpClient and that contains methods annotated with @Request

Lets you define an HTTP client in a declarative manner using an annotated interface.

See the Using HttpClient for more details.

All these injected services are serializable, which is important for big data environments. If you create the instances yourself, you cannot benefit from these features, nor from the memory optimization done by the runtime. Prefer reusing the framework instances over custom ones.

LocalConfiguration

The local configuration uses system properties and the environment (replacing dots per underscores) to look up the values. You can also put a TALEND-INF/local-configuration.properties file with default values. This allows to use the local_configuration:<key> syntax in @Ui annotation. Here is an example to read the default value of a property from the configuration:

@Option
@DefaultValue("local_configuration:myfamily.model.key")
private String value;
Ensure your key is unique across all components to avoid global overrides on the JVM. In practice, it is strongly recommended to always use the family as a prefix.
Also note that you can use @Configuration("prefix") to inject a mapping of the LocalConfiguration in a component. It uses the same rules as for any configuration object. If you prefer to inject you configuration in a service, ensure to wrap it in a Supplier to always have an up to date version.

If you want to ignore the local-configuration.properties, you can set the system property: talend.component.configuration.${componentPluginId}.ignoreLocalConfiguration=true.

Here a sample @Configuration model:

@Data // from lombok, optional
public class MyConfig {
  @Option
  private String defaultUrl;
}

Here is how to use it from a service:

@Service
public class ConfiguredService {
  @Configuration("myprefix")
  private Supplier<MyConfig> config;
}

And finally, here is how to use it in a component:

@Service
public class ConfiguredComponent {
  public ConfiguredComponent(@Configuration("myprefix") final MyConfig config) {
    // ...
  }
}
it is recommended to convert this configuration in a runtime model in components to avoid to transport more than desired during the job distribution.

Using HttpClient

You can access the API reference in the Javadocs.

The HttpClient usage is described in this section by using the REST API example below. Assuming that it requires a basic authentication header:

GET /api/records/{id}

-

POST /api/records

JSON payload to be created: {"id":"some id", "data":"some data"}

To create an HTTP client that is able to consume the REST API above, you need to define an interface that extends HttpClient.

The HttpClient interface lets you set the base for the HTTP address that the client will hit.

The base is the part of the address that needs to be added to the request path to hit the API. It is now possible, and recommended, to use @Base annotation.

Every method annotated with @Request in the interface defines an HTTP request. Every request can have a @Codec parameter that allows to encode or decode the request/response payloads.

You can ignore the encoding/decoding for String and Void payloads.
public interface APIClient extends HttpClient {
    @Request(path = "api/records/{id}", method = "GET")
    @Codec(decoder = RecordDecoder.class) //decoder =  decode returned data to Record class
    Record getRecord(@Header("Authorization") String basicAuth, @Path("id") int id);

    /** same with base as parameter */
    @Request(path = "api/records/{id}", method = "GET")
    @Codec(decoder = RecordDecoder.class) //decoder =  decode returned data to Record class
    Record getRecord(@Header("Authorization") String basicAuth, @Base String base, @Path("id") int id);

    @Request(path = "api/records", method = "POST")
    @Codec(encoder = RecordEncoder.class, decoder = RecordDecoder.class) //encoder = encode record to fit request format (json in this example)
    Record createRecord(@Header("Authorization") String basicAuth, Record record);
}
The interface should extend HttpClient.

In the codec classes (that implement Encoder/Decoder), you can inject any of your service annotated with @Service or @Internationalized into the constructor. Internationalization services can be useful to have internationalized messages for errors handling.

The interface can be injected into component classes or services to consume the defined API.

@Service
public class MyService {

    private APIClient client;

    public MyService(...,APIClient client){
        //...
        this.client = client;
        client.base("http://localhost:8080");// init the base of the api, often in a PostConstruct or init method
    }

    //...
    // Our get request
    Record rec =  client.getRecord("Basic MLFKG?VKFJ", 100);
    // or
    Record rec1 =  client.getRecord("Basic MLFKG?VKFJ", "http://localhost:8080", 100);

    //...
    // Our post request
    Record newRecord = client.createRecord("Basic MLFKG?VKFJ", new Record());
}
By default, /+json are mapped to JSON-P and /+xml to JAX-B if the model has a @XmlRootElement annotation.

Customizing HTTP client requests

For advanced cases, you can customize the Connection by directly using @UseConfigurer on the method. It calls your custom instance of Configurer. Note that you can use @ConfigurerOption in the method signature to pass some Configurer configurations.

For example, if you have the following Configurer:

public class BasicConfigurer implements Configurer {
    @Override
    public void configure(final Connection connection, final ConfigurerConfiguration configuration) {
        final String user = configuration.get("username", String.class);
        final String pwd = configuration.get("password", String.class);
        connection.withHeader(
            "Authorization",
            Base64.getEncoder().encodeToString((user + ':' + pwd).getBytes(StandardCharsets.UTF_8)));
    }
}

You can then set it on a method to automatically add the basic header with this kind of API usage:

public interface APIClient extends HttpClient {
    @Request(path = "...")
    @UseConfigurer(BasicConfigurer.class)
    Record findRecord(@ConfigurerOption("username") String user, @ConfigurerOption("password") String pwd);
}
Built-In configurer

The framework provides in the component-api an OAuth1.Configurer which can be used as an example of configurer implementation. It expects a single OAuth1.Configuration parameter to be passed to the request as a @ConfigurationOption.

Here is a sample showing how it can be used:

public interface OAuth1Client extends HttpClient {
    @Request(path = "/oauth1")
    @UseConfigurer(OAuth1.Configurer.class)
    String get(@ConfigurerOption("oauth1") final OAuth1.Configuration configuration);
}

Big data streams

By default, the client loads in memory the payload. In case of big payloads, it can consume too much memory. For these cases, you can get the payload as an InputStream:

public interface APIClient extends HttpClient {
    @Request(path = "/big/http/data")
    InputStream getData();
}
You can use the Response wrapper, or not.

Internationalizing services

Internationalization requires following several best practices:

  • Storing messages using ResourceBundle properties file in your component module.

  • The location of the properties is in the same package than the related components and is named Messages. For example, org.talend.demo.MyComponent uses org.talend.demo.Messages[locale].properties.

  • Use the internationalization API for your own messages.

Internationalization API

The Internationalization API is the mechanism to use to internationalize your own messages in your own components.

The principle of the API is to design messages as methods returning String values and get back a template using a ResourceBundle named Messages and located in the same package than the interface that defines these methods.

To ensure your internationalization API is identified, you need to mark it with the @Internationalized annotation:

package org.superbiz;

@Internationalized (1)
public interface Translator {

    String message();

    String templatizedMessage(String arg0, int arg1); (2)

    String localized(String arg0, @Language Locale locale); (3)

    String localized(String arg0, @Language String locale); (4)
}
1 @Internationalized allows to mark a class as an internationalized service.
2 You can pass parameters. The message uses the MessageFormat syntax to be resolved, based on the ResourceBundle template.
3 You can use @Language on a Locale parameter to specify manually the locale to use. Note that a single value is used (the first parameter tagged as such).
4 @Language also supports the String type.

The corresponding Messages.properties placed in the org/superbiz resource folder contains the following:

org.superbiz.Translator.message = Some message
org.superbiz.Translator.templatizedMessage = Some message with string {0} and with number {1}
org.superbiz.Translator.localized = Some other message with string {0}

# or the short version

Translator.message = Some message
Translator.templatizedMessage = Some message with string {0} and with number {1}
Translator.localized = Some other message with string {0}

Providing actions for consumers

In some cases you can need to add some actions that are not related to the runtime. For example, enabling users of the plugin/library to test if a connection works properly.

To do so, you need to define an @Action, which is a method with a name (representing the event name), in a class decorated with @Service:

@Service
public class MyDbTester {
    @Action(family = "mycomp", value = "test")
    public Status doTest(final IncomingData data) {
        return ...;
    }
}
Services are singleton. If you need some thread safety, make sure that they match that requirement. Services should not store any status either because they can be serialized at any time. Status are held by the component.

Services can be used in components as well (matched by type). They allow to reuse some shared logic, like a client. Here is a sample with a service used to access files:

@Emitter(family = "sample", name = "reader")
public class PersonReader implements Serializable {
    // attributes skipped to be concise

    public PersonReader(@Option("file") final File file,
                        final FileService service) {
        this.file = file;
        this.service = service;
    }

    // use the service
    @PostConstruct
    public void open() throws FileNotFoundException {
        reader = service.createInput(file);
    }

}

The service is automatically passed to the constructor. It can be used as a bean. In that case, it is only necessary to call the service method.

Particular action types

Some common actions need a clear contract so they are defined as API first-class citizen. For example, this is the case for wizards or health checks. Here is the list of the available actions:

Available Output

Provide the output flows by some condition

  • Type: available_output

  • API: @org.talend.sdk.component.api.service.outputs.AvailableOutputFlows

  • Returned type: java.util.Collection

  • Sample:

{

}

Close Connection

Mark an action works for closing runtime connection, returning a close helper object which do real close action. The functionality is for the Studio only, studio will use the close object to close connection for existed connection, and no effect for cloud platform.

  • Type: close_connection

  • API: @org.talend.sdk.component.api.service.connection.CloseConnection

  • Returned type: org.talend.sdk.component.api.service.connection.CloseConnectionObject

  • Sample:

{
 "connection": "..."
}

Create Connection

Mark an action works for creating runtime connection, returning a runtime connection object like jdbc connection if database family. Its parameter MUST be a datastore. Datastore is configuration type annotated with @DataStore. The functionality is for the Studio only, studio will use the runtime connection object when use existed connection, and no effect for cloud platform.

  • Type: create_connection

  • API: @org.talend.sdk.component.api.service.connection.CreateConnection

Discoverdataset

This class marks an action that explore a connection to retrieve potential datasets.

  • Type: discoverdataset

  • API: @org.talend.sdk.component.api.service.discovery.DiscoverDataset

  • Returned type: org.talend.sdk.component.api.service.discovery.DiscoverDatasetResult

  • Sample:

{
 "datasetDescriptionList": "..."
}

Dynamic Dependencies

Mark a method as returning a list of dynamic dependencies with GAV formatting.

  • Type: dynamic_dependencies

  • API: @org.talend.sdk.component.api.service.dependency.DynamicDependencies

  • Returned type: java.util.List

  • Sample:

{

}

Dynamic Values

Mark a method as being useful to fill potential values of a string option for a property denoted by its value. You can link a field as being completable using @Proposable(value). The resolution of the completion action is then done through the component family and value of the action. The callback doesn’t take any parameter.

  • Type: dynamic_values

  • API: @org.talend.sdk.component.api.service.completion.DynamicValues

  • Returned type: org.talend.sdk.component.api.service.completion.Values

  • Sample:

{
  "items":[
    {
      "id":"value",
      "label":"label"
    }
  ]
}

Healthcheck

This class marks an action doing a connection test

  • Type: healthcheck

  • API: @org.talend.sdk.component.api.service.healthcheck.HealthCheck

  • Returned type: org.talend.sdk.component.api.service.healthcheck.HealthCheckStatus

  • Sample:

{
  "comment":"Something went wrong",
  "status":"KO"
}

Schema

Mark an action as returning a discovered schema. Its parameter MUST be a dataset. Dataset is configuration type annotated with @DataSet. If component has multiple datasets, then dataset used as action parameter should have the same identifier as this @DiscoverSchema.

  • Type: schema

  • API: @org.talend.sdk.component.api.service.schema.DiscoverSchema

  • Returned type: org.talend.sdk.component.api.record.Schema

  • Sample:

{
  "entries":[
    {
      "comment":"The column 1",
      "errorCapable":false,
      "metadata":false,
      "name":"column1",
      "nullable":false,
      "props":{

      },
      "rawName":"column 1",
      "type":"STRING",
      "valid":true
    },
    {
      "comment":"The int column",
      "errorCapable":false,
      "metadata":false,
      "name":"column2",
      "nullable":false,
      "props":{

      },
      "rawName":"column 2",
      "type":"INT",
      "valid":true
    }
  ],
  "metadata":[
  ],
  "props":{
    "talend.fields.order":"column1,column2"
  },
  "type":"RECORD"
}

Schema Extended

Mark a method as returning a Schema resulting from a connector configuration and some other parameters.Parameters can be an incoming schema and/or an outgoing branch.`value' name should match the connector’s name.

  • Type: schema_extended

  • API: @org.talend.sdk.component.api.service.schema.DiscoverSchemaExtended

  • Returned type: org.talend.sdk.component.api.record.Schema

  • Sample:

{
  "entries":[
    {
      "comment":"The column 1",
      "errorCapable":false,
      "metadata":false,
      "name":"column1",
      "nullable":false,
      "props":{

      },
      "rawName":"column 1",
      "type":"STRING",
      "valid":true
    },
    {
      "comment":"The int column",
      "errorCapable":false,
      "metadata":false,
      "name":"column2",
      "nullable":false,
      "props":{

      },
      "rawName":"column 2",
      "type":"INT",
      "valid":true
    }
  ],
  "metadata":[
  ],
  "props":{
    "talend.fields.order":"column1,column2"
  },
  "type":"RECORD"
}

Schema Mapping

Mark a method as returning a database mapping from a connector @DataStore configuration. Use this annotation if database mapping can be dynamic and @DatabaseMapping.Mapping is set to custom. The functionality is for the Studio only.

  • Type: schema_mapping

  • API: @org.talend.sdk.component.api.service.schema.DatabaseSchemaMapping

  • Returned type: java.lang.String

  • Sample:

{
 "value": "..."
 "coder": "..."
 "hash": "..."
 "hashIsZero": "..."
 "serialVersionUID": "..."
 "COMPACT_STRINGS": "..."
 "serialPersistentFields": "..."
 "REPL": "..."
 "CASE_INSENSITIVE_ORDER": "..."
 "LATIN1": "..."
 "UTF16": "..."
}

Suggestions

Mark a method as being useful to fill potential values of a string option. You can link a field as being completable using @Suggestable(value). The resolution of the completion action is then done when the user requests it (generally by clicking on a button or entering the field depending the environment).

  • Type: suggestions

  • API: @org.talend.sdk.component.api.service.completion.Suggestions

  • Returned type: org.talend.sdk.component.api.service.completion.SuggestionValues

  • Sample:

{
  "cacheable":false,
  "items":[
    {
      "id":"value",
      "label":"label"
    }
  ]
}

Update

This class marks an action returning a new instance replacing part of a form/configuration.

  • Type: update

  • API: @org.talend.sdk.component.api.service.update.Update

User

Extension point for custom UI integrations and custom actions.

  • Type: user

  • API: @org.talend.sdk.component.api.service.Action

Validation

Mark a method as being used to validate a configuration.

this is a server validation so only use it if you can’t use other client side validation to implement it.
  • Type: validation

  • API: @org.talend.sdk.component.api.service.asyncvalidation.AsyncValidation

  • Returned type: org.talend.sdk.component.api.service.asyncvalidation.ValidationResult

  • Sample:

{
  "comment":"Something went wrong",
  "status":"KO"
}
Built In Actions

These actions are provided - or not - by the application the UI runs within.

always ensure you don’t require this action in your component.

built_in_suggestable

Mark the decorated field as supporting suggestions, i.e. dynamically get a list of valid values the user can use. It is however different from @Suggestable by looking up the implementation in the current application and not the services. Finally, it is important to note that it can do nothing in some environments too and that there is no guarantee the specified action is supported.

  • API: @org.talend.sdk.component.api.configuration.action.BuiltInSuggestable

Internationalization

Internationalization is supported through the injection of the $lang parameter, which allows you to get the correct locale to use with an @Internationalized service:

public SuggestionValues findSuggestions(@Option("someParameter") final String param,
                                        @Option("$lang") final String lang) {
    return ...;
}
You can combine the $lang option with the @Internationalized and @Language parameters.

Services and interceptors

For common concerns such as caching, auditing, and so on, you can use an interceptor-like API. It is enabled on services by the framework.

An interceptor defines an annotation marked with @Intercepts, which defines the implementation of the interceptor (InterceptorHandler).

For example:

@Intercepts(LoggingHandler.class)
@Target({ TYPE, METHOD })
@Retention(RUNTIME)
public @interface Logged {
    String value();
}

The handler is created from its constructor and can take service injections (by type). The first parameter, however, can be BiFunction<Method, Object[], Object>, which represents the invocation chain if your interceptor can be used with others.

If you make a generic interceptor, pass the invoker as first parameter. Otherwise you cannot combine interceptors at all.

Here is an example of interceptor implementation for the @Logged API:

public class LoggingHandler implements InterceptorHandler {
    // injected
    private final BiFunction<Method, Object[], Object> invoker;
    private final SomeService service;

    // internal
    private final ConcurrentMap<Method, String> loggerNames = new ConcurrentHashMap<>();

    public CacheHandler(final BiFunction<Method, Object[], Object> invoker, final SomeService service) {
        this.invoker = invoker;
        this.service = service;
    }

    @Override
    public Object invoke(final Method method, final Object[] args) {
        final String name = loggerNames.computeIfAbsent(method, m -> findAnnotation(m, Logged.class).get().value());
        service.getLogger(name).info("Invoking {}", method.getName());
        return invoker.apply(method, args);
    }
}

This implementation is compatible with interceptor chains because it takes the invoker as first constructor parameter and it also takes a service injection. Then, the implementation simply does what is needed, which is logging the invoked method in this case.

The findAnnotation annotation, inherited from InterceptorHandler, is an utility method to find an annotation on a method or class (in this order).

Defining a custom API

It is possible to extend the Component API for custom front features.

What is important here is to keep in mind that you should do it only if it targets not portable components (only used by the Studio or Beam).

It is recommended to create a custom xxxx-component-api module with the new set of annotations.

Extending the UI

To extend the UI, add an annotation that can be put on @Option fields, and that is decorated with @Ui. All its members are then put in the metadata of the parameter. For example:

@Ui
@Target(TYPE)
@Retention(RUNTIME)
public @interface MyLayout {
}
Scroll to top