Wiki

Clone wiki

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

limeds-java-init.png

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.

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