Wiki
Clone wikilimeds-framework / LimeDS Java API
Table of Contents
Note: We assume the reader already has experience with developing for OSGi using BND Tools. If not, we refer to this tutorial as a starting point.
Introduction
LimeDS will scan all OSGi bundles in the BND tools workspace looking for classes that implement LimeDS components. When such a class is found, it is send to an AnnotatedClassParser in the LimeDS BND Plug-In, which will generate LimeDS descriptors (JSON files) for the information found in the annotated classes.
These descriptors are equivalent to the meta-data that is created when building FSlices using the Visual Editor and can be supplied to the LimeDS Component Manager upon installation of the bundle JAR in the OSGi container. The Component Manager then uses this information in combination with the provided implementation (an instance of the supplied class) to create a dynamic, up and running LimeDS component with active life cycle & dependency management, that has access to all the features provided by LimeDS.
The following example shows how such a LimeDS-enabled class can look like.
Quick Usage Example
In line with the Visual Editor tutorial, the example below shows a simple Flow Function implementation that connects to an external data source to fetch parking information and enhances the returned data with up-to-date weather info for each location. This service is made available as a HTTP Web service.
/** * Annotate the class to indicate that it should be processed by LimeDS. For now * each LimeDS component needs to implement the FunctionalSegment interface, but * additional types might be available in the future. */ @Segment public class GetParkingInfo implements FunctionSegment { /* * Class attributes can be annotated with @Configurable to enable using it * as a config property. Default values can be specified as an annotation * attribute. */ @Configurable(defaultValue = "http://datatank.stad.gent/4/mobiliteit/bezettingparkingsrealtime.json"); private String dataUrl; /* * Links to other Segments can be setup using the @Link annotation. * This mechanism is similar to various Inversion of Control implementation * such as Guice. */ @Link(target = "demo.datasources.OpenWeatherMapAPI") private FunctionalSegment weatherGetter; /* * LimeDS also allows to incorporate OSGi services, e.g. here it is used to * inject a service that provides the IBCN restclient Client interface. */ @Service private Client restClient; /* * The apply method provides the actual code that is executed when LimeDS * calls the FunctionalSegment. The platform makes sure that this is only * possible if all the dependencies (links, services) can be resolved. * * Additional annotations can modify the behaviour of the function: * * Using @Cached we can prevent unnecessary load on the external systems * because we know that the parking & weather status will not change * drastically in a time-span of 5 minutes. * * Using @HttpOperation, we expose the FunctionalSegment as a HTTP endpoint * so we can enable communication with external clients. */ @Cached(retentionDuration = 5, retentionDurationUnit = TimeUnit.MINUTES) @HttpOperation(method = HttpMethod.GET, path = "/parkings") @Override public JsonValue apply(JsonValue... input) throws Exception { /* * The injected restClient is used to perform a HTTP Get on the * configured URL. */ JsonArray parkings = restClient.target(dataUrl).get().returnObject(Json.getDeserializer()).getBody().asArray(); /* * Using the weatherGetter Flow Function, we can match each parking * result with current weather info at that location. */ for (JsonValue parking : parkings) { parking.asObject().put("weather", weatherGetter.apply(parking)); } /* * JSON returned here will be written to the HTTP response body if the * function is called over HTTP. */ return parkings; } }
Annotation Reference
ApplySchedule
Use @ApplySchedule
on a class specifying a Segment to configure the delayed or periodical execution of the Segment.
Example 1, execute a function when the framework starts:
@Segment @ApplySchedule("delay 0 seconds") public class InitializerExample implements FunctionalSegment { @Override public JsonValue apply(JsonValue... input) throws Exception { System.out.println("Framework started!"); return null; } }
Example 2, periodically execute a function every day at 1 am:
@Segment @ApplySchedule("every 1 days offset 1 hours") public class PeriodicExecutorExample implements FunctionalSegment { @Override public JsonValue apply(JsonValue... input) throws Exception { System.out.println("It's 1 am, time for bed!"); return null; } }
Cached
Annotate the FunctionalSegment apply method with @Cached
to configure a caching policy for the function. Because the input & output of a FunctionalSegment is always JSON, we were able to implement a generic caching system based on Google Guava.
Example, cache the output of a Segment that gets parking info from a HTTP resource.
@Segment public class GetParkingInfo implements FunctionalSegment { @Service private Client restClient; @Cached(retentionDuration = 5, retentionDurationUnit = TimeUnit.MINUTES) @Override public JsonValue apply(JsonValue...input) throws Exception { return restClient.target("http://datatank.stad.gent/4/mobiliteit/bezettingparkingsrealtime.json").get().returnObject(Json.getDeserializer()).getBody().asArray(); } }
Configurable
Configurable properties for Segments or Segment Factory instance arguments can be created by annotating the class fields with @Configurable
. You can specify a default value for the property by assigning a constant String value (which is converted to the correct type at runtime). The @Configurable
annotation also has an optional parameter "possibleValues" to indicate a limited list with the string representation of the allowed values.
Note that configuration properties only support primitive types (boolean, integer, double, ...), their respective array types and String. The only exception is that Segment links can also be made configurable in order to maximize component reuse (see example 3).
Example 1, create a client API for some Web service with configurable API key and version.
@Segment public class SomeServiceGetter implements FunctionalSegment { private static final String API_ENDPOINT = "http://someservice/api/"; @Configurable private String apiKey; @Configurable(possibleValues = { "v1.1", "v1.2", "v2.0" }) private String apiVersion; @Override public JsonValue apply(JsonValue...input) throws Exception { //Create client context ClientContext context = restClient.target(API_ENDPOINT + apiVersion).withQuery("key", apiKey); //... rest of client code } }
Example 2, create a Segment factory that allows simple HTTP clients to be created based on two instantiation parameters: a) the target URL of the client and b) the HTTP method to use.
@Segment(factory = true) public class Client implements FunctionalSegment { private static final String HTTP_GET = "GET"; private static final String HTTP_PUT = "PUT"; private static final String HTTP_POST = "POST"; private static final String HTTP_DELETE = "DELETE"; @Service private org.ibcn.commons.restclient.api.Client httpClient; @Configurable private String targetUrl; @Configurable(possibleValues = { HTTP_GET, HTTP_PUT, HTTP_POST, HTTP_DELETE }, defaultValue = HTTP_GET) private String method; @Override public JsonValue apply(JsonValue...input) throws Exception { JsonValue request = input.length > 0 ? input[0] : null; ClientContext clientContext = httpClient.target(targetUrl); if (HTTP_GET.equals(method)) { return Json.from(clientContext.get().returnString().getBody()); } else if (HTTP_PUT.equals(method.toUpperCase())) { return Json.from(clientContext.putJson(request.toString()).returnString().getBody()); } else if (HTTP_POST.equals(method.toUpperCase())) { return Json.from(clientContext.postJson(request.toString()).returnString().getBody()); } else if (HTTP_DELETE.equals(method.toUpperCase())) { clientContext.delete().returnNoResult(); } return null; } }
Note that we indicate that this class specifies a factory Segment by setting the factory boolean to true for the @Segment
annotation.
Example 3, a simple Segment factory with a configurable link that does a toUpperCase operation on the String provided by the target of the configured link target.
@Segment(factory = true) public class ToUpperCaseFilter implements FunctionalSegment { @Configurable @Link private FunctionalSegment messageSource; @Override public JsonValue apply(JsonValue...input) throws Exception { return new JsonPrimitive(messageSource.apply(input).asString().toUpperCase()); } }
Documentation
The input and output of a FunctionalSegment can be documented by annotating the implementation class or its apply method with the @Documentation
annotation. Using the type attribute, one can specify if the provided JSON documents the input or the output. A boolean attribute validate can be set to true to indicate that LimeDS should validate the input and/or output using the specified model. For more information on the documentation JSON format, see Guide to JSON Validation.
Example, a documented Segment for which the I/O is validated using the provided schema. The input is an addressline and the function returns the matching geolocation as output.
@Segment public class Geocoder implements FunctionalSegment { //The JSON schema can be defined in the class, but this can be unwieldy for larger schemas. @Documentation(type = DocumentationType.INPUT, validate = true, schema = "{\"addressLine\" : \"String (The input address to be geocoded.)\" }") //That is why we support defining types in a separate file and referencing them here. @Documentation(type = DocumentationType.OUTPUT, validate = true, schemaRef = "examples.types.Geolocation_1.0.0") @Override public JsonValue apply(JsonValue...input) throws Exception { //It is safe to get the addressLine as input is validated! String addressLine = input[0].getString("addressLine"); JsonObject geolocation; //...Do the actual geocoding... return geolocation; } }
To register the type examples.types.Geolocation
with LimeDS (so it can be referenced in the @Documentation
annotations), we need to create a Java package examples.types
containing a file Geolocation.json
. The file contains the JSON type definition, for example:
{ "latitude" : "FloatNumber", "longitude" : "FloatNumber" }
The type mapping is generated by also adding a package-info.java
to the package with the following contents:
/** * This package contains LimeDS type definitions. */ @org.osgi.annotation.versioning.Version("1.0.0") //Specify the version @org.ibcn.limeds.annotations.IncludeTypes("Geolocation.json") //Include the Geolocation typedef package examples.types;
HttpAssertion
A HTTP Assertion can be added to a FunctionalSegment by annotating the implementation class or its apply method with the @HttpAssertion
annotation. The goal of a HTTP Assertion is to model the dependency of the Segment on the availability of some remote HTTP resource. This allows LimeDS to disable the function if the resource is no longer available.
Example, the following FunctionalSegment will only be active if a HTTP head to the supplied target URL can return successfully.
@Segment public class WeatherGetter implements FunctionalSegment { @HttpAssertion(target = "http://api.openweathermap.org") @Override public JsonValue apply(JsonValue...input) throws Exception { //Query OpenWeatherMap and return result... //Will only be called if the assertion can be resolved... } }
Note that LimeDS has various mechanisms for evaluating the HTTP Assertions in an efficient way. If the request rate gets to high, the checking of the HTTP Assertion will only be done once within a configurable window and the results of this assertion will then be applied to all subsequent requests in the window. Furthermore, we only start polling the external resource when it is no longer available.
HttpOperation
A FunctionalSegment can easily be exposed as an HTTP endpoint by annotating its apply method with @HttpOperation
.
Example 1, setup a HTTP endpoint to which JSON can be posted.
@Segment public class WeatherGetter implements FunctionalSegment { @HttpOperation(method = HttpMethod.POST, path = "/test") @Override public JsonValue apply(JsonValue...input) throws Exception { //Print incoming JSON... System.out.println(input[0].toString()); } }
Example 2, setup a HTTP endpoint with a path parameter.
@Segment public class WeatherGetter implements FunctionalSegment { @Service private UserManager userManager; @HttpOperation(method = HttpMethod.GET, path = "/users/{userId}") @Override public JsonValue apply(JsonValue...input) throws Exception { JsonValue request = input[1].get("request"); return userManager.get(request.get("path").getString("userId")); } }
Segment
Annotating a class with @Segment
will result in it being processed by the framework. Using this annotation, several options for the LimeDS component can be set:
- id: The id of the component. If none is specified, LimeDS will use the fully qualified (packages + class name) as the id of the component.
- description: A String describing the purpose of the component. (Optional)
- tags: Each component can have a number of tags. The tags can play a role in the way components are linked (see
@Link
). - serviceProperties: Additional OSGi service properties can be specified for the component.
- additionalInterfaces: This feature can be useful when extending classes, as it allows the component to be exposed as a specific service towards OSGi, without directly implementing the interface.
- factory: This attribute must be set to true if the component is a LimeDS factory.
- export: The component will only be visible for usage in Slices if this attribute is set to true (which is the default value for Java components).
- subscriptions: Event channel IDs to which the component is to be subscribed can be defined here.
Link
LimeDS components can depend on other LimeDS components by using the @Link
annotation. LimeDS will make sure a component is only activated when the link dependency can be resolved. There are two types of link dependencies: one-to-one dependencies (id-based) and one-to-many dependencies (id or tag-based).
Example 1, by depending on the SegmentLister Segment, we can create a function that returns a filtered output of the Segments registered with the LimeDS system.
@Segment public class IBCNComponentGetter implements FunctionalSegment { @Link(id = "limeds.segments.Lister") private FunctionalSegment segmentLister; @Override public JsonValue apply(JsonValue...input) throws Exception { //List all IBCN Components return segmentLister.apply().asArray() .stream() .filter(c -> c.getId().contains("ibcn")) .collect(Json.toArray()); } }
Example 2, multiple @Link
annotations can be added for the same dependency attribute, allowing Segments to depend on a collection of other Segments. In the following example we use this to combine timetable information from a train info provider and a bus info provider to create a function that returns available transfers based on the input geolocation for the next hour.
@Segment public class UpcomingTransfersLister implements FunctionalSegment { @Link(target = "transport.timetables.TrainInfoProvider") @Link(target = "transport.timetables.BusInfoProvider") private Collection < FunctionalSegment> providers; @Override public JsonValue apply(JsonValue...input) throws Exception { JsonObject geoLocation = input[0]; return providers.stream() .flatMap(p -> p.apply(geoLocation).asArray().stream()) .filter(this::isUpcomingTransfer()) .collect(Json.toArray()); } //... }
Example 3, by using a tag-based dependency, we can create a multi-link without configuring specific Segments to be bound (which makes the link more dynamic): any active FunctionalSegment with the tag "timetable-provider" will be added to the dependency collection.
@Segment public class UpcomingTransfersLister implements FunctionalSegment { @Link(tags = { "timetable-provider" }) private Collection < FunctionalSegment> providers; @Override public JsonValue apply(JsonValue...input) throws Exception { JsonObject geoLocation = input[0]; return providers.stream() .flatMap(p -> p.apply(geoLocation).asArray().stream()) .filter(this::isUpcomingTransfer()) .collect(Json.toArray()); } //... }
Service
LimeDS Java components can depend on OSGi services by using the @Service
annotation. LimeDS will make sure a component is only activated when the service dependency can be resolved.
Example 1, LimeDS allows bundles to register a Storage service for providing specific storage implementation through a generic DAO interface. Here we use the MongoDB provider with a Flow Function.
@Segment public class StorageLister implements FunctionalSegment { //We use a service filter to make sure we have to correct implementation @Service(filter = "(storage.type=mongodb)") private Storage storage; @Override public JsonValue apply(JsonValue...input) throws Exception { DAO dao = storage.getDaoInstance("demo"); return dao.findAll(); } }
Example 2, we use the @Service
annotation to count the number of Servlets registerd with OSGi.
@Segment public class ServletCounter implements FunctionalSegment { @Service private Collection < Servlet > servlets; @Override public JsonValue apply(JsonValue...input) throws Exception { return Json.objectBuilder().add("count", servlets.size()); } }
Updated