LuciadFusion ships with built-in service types for serving geospatial data, which support a wide range of clients. These service types include OGC standards such as WMS, WFS, OGC 3D Tiles, and Hexagon-designed standards such as Luciad Panoramics and ERDAS APOLLO ECWP.

On top of the built-in service types, you can also add your own service types to LuciadFusion. This feature allows you to use a custom protocol for serving geospatial data managed in LuciadFusion Studio. As a result, you gain access to a vast range of options for getting your data to clients.

In this tutorial, we go over the steps you need to set up a custom service type to LuciadFusion. LuciadFusion comes with a SHP quiz service sample that implements those steps. We use the sample as an illustration of adding custom service types throughout the article. This tutorial requires a basic understanding of the Spring framework.

Goal of the SHP Quiz service type

LuciadFusion ships with a sample that demonstrates how to add a custom service type. Because this tutorial uses the quiz sample as an example, we start by explaining what this service type has to offer to users.

The SHP Quiz service type takes SHP files, and creates quizzes from them. It uses the information in the SHP files to create and ask quiz questions about the shapes.

In LuciadFusion Studio, you can create a SHP Quiz service from SHP data. When users navigate to the endpoint of the service, they see a list of quizzes, one quiz for each SHP file in the service.

Quiz list
Figure 1. Quiz list page
Quiz question
Figure 2. Quiz question page
Quiz answer
Figure 3. Quiz answer page

When the users select a quiz, the SHP Quiz service selects four random elements from the SHP file, and gets the labels of the elements. It also generates an image for one of the four elements. The service then asks the users which label matches the image.

When users select one of the labels, the service tells them if they selected the correct label. They can then ask the SHP Quiz service for another question.

You can try out the SHP Quiz service yourself by running start.jar from the LuciadFusion installation directory. Next, select the Adding a custom service type to LuciadFusion (SHP Quiz) sample in the launcher, and click Run. The sample makes the custom SHP Quiz service type available in LuciadFusion.

Next, you can log on to LuciadFusion Studio at http://localhost:8081/studio/index.html. Add the samples/resources/Data/Shp/Europe/europe.shp data set or the samples/resources/Data/Shp/Usa/states.shp data set to LuciadFusion, for instance. Next, create a service of the SHP Quiz service type from the data set.

Navigate to the endpoint URL of the new service in your web browser to test the SHP Quiz service.

You can find all code related to the SHP Quiz sample in the samples/src/samples/fusion/platform/shpquiz folder of the LuciadFusion installation directory.

General guidelines for the integration of services in LuciadFusion

Before writing any code for a custom service type, you need to understand how LuciadFusion integrates services. For a full explanation, see the LuciadFusion services integration article.

For a custom service type, you need to consider these points:

  • You must give some basic information about your service type to LuciadFusion Studio.

  • You have full control over the service endpoint of a custom service type. This means that you must handle the interaction with the client and fetch the necessary information from LuciadFusion Studio.

  • It’s important to think about the mapping of the products in a service to the structure of the custom service type. There’s no requirement to make this mapping, though. You can also limit the number of products and data sets in a product. See Adding a service type configuration for more information.

  • If it’s suitable to your custom service type, use the title and name property to identify a layer, coverage, or similar grouping to clients of the service.

Product mapping to a SHP Quiz service

For the SHP Quiz service type:

  • One LuciadFusion product maps to one quiz in the service.

  • A SHP Quiz service can have several quizzes, with each quiz containing exactly one SHP file.

  • The title of the product serves as the title of the quiz.

  • We use the name of the product if we need to identify a specific quiz in the SHP Quiz service.

  • The endpoint path of the SHP Quiz service is /shpquiz, so the endpoint URL is http://localhost:8081/shpquiz/{serviceName}

service mapping shp quiz
Figure 4. LuciadFusion Studio SHP Quiz service mapping

Now that we know how services are integrated in LuciadFusion, we can go over the steps you need to take to add your own service type.

Setting up the development environment

To get started with the implementation of a custom service type, we set up the development environment. To develop extensions for LuciadFusion, an IDE is the most convenient. See the Integrate LuciadFusion into your IDE article to learn how to set up the IDE. When you have completed the setup, you can run LuciadFusion from you IDE using the LuciadFusion Platform main class: TLfnFusionPlatformApplication.

In the next step, we configure LuciadFusion so that it can pick up the extra code we are going to add. LuciadFusion uses the Spring framework, so we must inform the Spring framework where to find the extra LuciadFusion code. To specify which extra packages the Spring framework must scan, you can use the fusion.config.additionalScanPackages configuration property. Set this property in the config/fusion.common.yml file, or create a new Spring profile.

The SHP Quiz service type sample adds this property to a new profile: application-fusion.sample.shpquiz.yml, available in the config folder of the LuciadFusion installation directory.

Adding a service type configuration

LuciadFusion Studio must fetch information from the service type when a user creates a service of that type. It needs that information to validate the data that a user adds to the service, and to construct the endpoint URL. That’s why every service type must have a configuration in LuciadFusion that describes this information.

To define this configuration, create a class that implements ILfnServiceTypeConfiguration. Make sure to add the package of this class to the Spring application context using the additionalScanPackages property. You must mark the class with @Component so that the Spring framework creates a bean of it, and LuciadFusion can pick it up from the Spring application context.

A class implementing ILfnServiceTypeConfiguration needs to have implementations for 3 methods:

  1. The service type for the getServiceType method

  2. The endpoint path of the service for the getEndpointPath method

  3. A product validator for the getProductValidator method

Implement getServiceType()

The service type is a string identifier for that service type. We use it when we create a service in the LuciadFusion Studio web application or REST API. LuciadFusion stores the identifier together with the new service, so that it can look up the configuration for that service later.

The service type must be unique among all service types. You can’t use the strings wms, wfs, ogc3dtiles, or any of the other strings for built-in service types. Try to use only characters that are safe for URLs. It’s also best to use only lower-case characters and keep the service type short. With these limitations in mind, choose a value that correctly describes your service type.

The SHP Quiz service type uses shp-quiz as the identifier.

Implement getEndpointPath()

The endpoint path is the path that clients for your service connect to. You must implement the logic for the protocol of your service type at this path later. See Adding the service endpoint.

Because a user can create more than one service of your custom type, you need to make the endpoint path unique for each service, using the aServiceName argument. The users choose the service name when they create the service in LuciadFusion Studio, and LuciadFusion ensures that it’s unique among all services of the same type.

The SHP Quiz service type uses /shpquiz/{serviceName} as the endpoint path. For a SHP Quiz service with name tutorial running on a local installation of LuciadFusion, the endpoint URL is http://localhost:8081/shpquiz/tutorial.

Implement getProductValidator()

A product validator ensures that services of your custom type accept supported data only. It can also limit the number of products, and the amount of data in a product for a service type. You need to provide a class that implements the ILfnServiceTypeProductValidator interface as product validator.

When you implement a product validator, it’s important to know what type of data your service type accepts. You also need to think about how you map data and products from LuciadFusion Studio to the data structure of your service type. For your product mapping, you may need to define a limitation on the number of products in a service, or the amount of data in a product.

A class implementing ILfnServiceTypeProductValidator needs to have this information:

  1. The maximum number of products in a service for the getMaxAllowedProductCount method. Return OptionalInt.empty() to indicate that you don’t want any limitation.

  2. The maximum number of data in a product for the getMaxAllowedDataCount method. Again, return OptionalInt.empty() if you don’t want limitations.

  3. A validation result for a list of data sets in a product, for the validateProductData method. LuciadFusion uses this method to validate if the service can add a product. Return a validation error if you conclude that the list contains data that your service type can’t accept. For the SHP Quiz Service, we return a validation error if the list has non-SHP data.

The SHP Quiz service type allows more than one product, because each product maps to a quiz, and a service can have more than one quiz. It only accepts one SHP file for each quiz, so the number of data sets in a product is limited to one.

These two code samples show the implementations of the ILfnServiceTypeProductValidator and ILfnServiceTypeConfiguration interfaces for the SHP Quiz service type.

Expand this section to view the SHP Quiz ILfnServiceTypeProductValidator implementation

SHP Quiz ILfnServiceTypeProductValidator implementation
public class ShpQuizServiceTypeProductValidator implements ILfnServiceTypeProductValidator {

  @Override
  public OptionalInt getMaxAllowedProductCount() {
    // No limit is placed on the amount of products since a SHP Quiz service can have multiple quizzes (a product maps to a quiz in the SHP Quiz service).
    return OptionalInt.empty();
  }

  @Override
  public OptionalInt getMaxAllowedDataCount() {
    // Only one data is allowed per product since the SHP Quiz uses 1 SHP data per quiz.
    return OptionalInt.of(1);
  }

  @Override
  public TLcdValidationResult validateProductData(List<TLfnData> aDataList) {
    TLcdValidationResult validationResult = TLcdValidationResult.ok();
    TLcdSHPModelDecoder2 shpDecoder = new TLcdSHPModelDecoder2();

    if (aDataList.stream().anyMatch(data -> !shpDecoder.canDecodeSource(data.getDataSource()))) {
      // Only SHP data is accepted in a SHP Quiz service.
      // Return a validation error for non-SHP data.
      validationResult.addError("Only SHP data can be added to a SHP QUIZ service");
    }
    return validationResult;
  }

}

Expand this section to view the SHP Quiz ILfnServiceTypeConfiguration implementation

SHP Quiz ILfnServiceTypeConfiguration implementation
@Component
public class ShpQuizServiceTypeConfiguration implements ILfnServiceTypeConfiguration {

  @Override
  public String getServiceType() {
    return "shp-quiz";
  }

  @Override
  public ILfnServiceTypeProductValidator getProductValidator() {
    return new ShpQuizServiceTypeProductValidator();
  }

  @Override
  public String getEndpointPath(String aServiceName) {
    return "/shpquiz/" + aServiceName;
  }
}

If you start LuciadFusion now, you see the custom service type as an option in the Service type list of the Create Service form. You must still implement the service endpoint for the custom service type, though. We cover that in the next section.

create service shp quiz
Figure 5. Create service form

Adding the service endpoint

The service endpoint is the endpoint that clients use to access the service. You’re free to implement this endpoint however you want. LuciadFusion offers a way to access the necessary data and products for a service that suits almost any implementation. Using the Spring MVC framework and defining a controller is the most straightforward way, We focus on that approach in this tutorial.

Creating a controller for the service endpoint

To create a controller for the service endpoint, you must create a class and annotate it with the Spring MVC annotation @Controller. You also need to give it a @RequestMapping annotation, and set the path for the controller. The path corresponds to the path you’ve set in the ILfnServiceTypeConfiguration of your service type. This tells the Spring framework to send requests sent to the endpoint path of your service type to the controller.

The endpoint path of the SHP Quiz service type is /shpquiz. The path in the @RequestMapping is therefore set to /shpquiz.

SHP Quiz controller. (from samples/fusion/platform/shpquiz/controller/ShpQuizController.java)
@Controller
@RequestMapping(path = "/shpquiz")
public class ShpQuizController {

If you want to build a REST API for your service, you can use a Spring REST controller as the controller for your endpoint. Use a @RestController annotation instead of the @Controller annotation.

Make sure to update the additionalScanPackages property if you put your controller in a different package than your ILfnServiceTypeConfiguration implementation.

If you don’t want to add every package you create to the additionalScanPackages property, you can create a configuration class annotated with @Configuration, and use the @Import or @ComponentScan annotations to add other classes and packages. If you do so, you only need to add the package of the configuration class to the additionalScanPackages property.

Defining service endpoint handlers

You need to define some handlers in the controller to handle the requests sent to your service endpoint path. How many handlers you need depends on how your service interacts with a client.

You must define one handler at least to handle requests sent to a specific service. You need that handler because LuciadFusion uses the service name to construct the endpoint URL of the service. To do so, add a method annotated with the @GetMapping(path = "/{serviceName}") annotation. The return type of the method depends on how you implement your service. The SHP Quiz service uses the ModelAndView object to combine the model with a web interaction view.

When you’re implementing a REST API, the return type typically is ResponseEntity.

When you navigate to the endpoint URL of a SHP Quiz service, you see all available quizzes in the service. The controller has a quizList method with the @GetMappingpath = "/{serviceName}") annotation to handle this.

Quiz list handler. (from samples/fusion/platform/shpquiz/controller/ShpQuizController.java)
@GetMapping(path = "/{serviceName}")
public ModelAndView quizList(HttpServletRequest aRequest, @PathVariable(name = "serviceName") String aServiceName) {

The SHP Quiz service type needs two more handlers: one for the quiz question page, and one for the page showing the answer.

The quiz question page displays a random question for a quiz to the page visitor. Because a quiz maps to a product, we annotate the handler with @GetMapping(path = "/{serviceName}/{productName}").

Quiz question handler. (from samples/fusion/platform/shpquiz/controller/ShpQuizController.java)
@GetMapping(path = "/{serviceName}/{productName}")
public ModelAndView quizQuestion(HttpServletRequest aRequest, @PathVariable(name = "serviceName") String aServiceName, @PathVariable(name = "productName") String aProductName) {

The handler for the answer page has the annotation @GetMapping(path = "/{serviceName}/{productName}/answer"). The method also has a parameter to identify the question, and the index of the chosen answer.

Quiz answer handler. (from samples/fusion/platform/shpquiz/controller/ShpQuizController.java)
@GetMapping(path = "/{serviceName}/{productName}/answer")
public ModelAndView quizAnswer(HttpServletRequest aRequest, @PathVariable(name = "serviceName") String aServiceName, @PathVariable(name = "productName") String aProductName, @RequestParam(name = "id") String aQuestionId, @RequestParam(name = "answer") int aSelectedAnswer) {

Once you have defined your handlers, you need to access services, products, and data in LuciadFusion Studio to implement the handlers. For example, to list all the quizzes, the SHP Quiz service controller needs to access the products that are in a service. To get that access, you need to use the repositories that LuciadFusion makes available in the Spring application context.

Configuring access to your endpoint handlers

Spring Security blocks off access to all endpoints by default, you are therefore required to register a SecurityFilterChain which configures the authentication for your handlers.

To simplify this process LuciadFusion Platform automatically configures the authentication of registered service types. The configuration is applied based on the value of the configuration property fusion.security.serviceAutenticationRequired. When this property is set to true LuciadFusion Platform configures every service type to require authentication, when it is set to false LuciadFusion Platform enables anonymous access to all service types. Read our How to configure access to services article for an example on how to configure more fine-grained access to services.

This configuration is applied to the endpoint pattern returned by the getEndPointPatterns method in ILfnServiceTypeConfiguration. By default, this method returns the result of calling getEndpointPath with value **.. In the case of the shpquiz service type, this results in the pattern /shpquiz/**, which matches all requests handled by our ShpQuizController. We therefore do not need to override this behavior for our SHP Quiz service. If the default value returned by getEndpointPath does not match your endpoint handlers, you need to override this method.

Using the LuciadFusion Repositories

LuciadFusion makes repositories available in the Spring application context to request information in LuciadFusion Studio.

You need to use these repositories when you implement a custom service:

To get access to those repositories from your endpoint implementation, you can use dependency injection. The SHP Quiz controller autowires the repositories in the constructor by annotating it with @Autowired.

Autowire repositories. (from samples/fusion/platform/shpquiz/controller/ShpQuizController.java)
private final TLfnServiceRepository fServiceRepository;
private final TLfnProductRepository fProductRepository;
private final TLfnDataRepository fDataRepository;

// used to publish service and product access events
private final TLfnAccessEventPublisher fAccessEventPublisher;

@Autowired
public ShpQuizController(TLfnServiceRepository aServiceRepository,
                         TLfnProductRepository aProductRepository,
                         TLfnDataRepository aDataRepository,
                         TLfnAccessEventPublisher aAccessEventPublisher) {
  fServiceRepository = aServiceRepository;
  fProductRepository = aProductRepository;
  fDataRepository = aDataRepository;
  fAccessEventPublisher = aAccessEventPublisher;

The first step in handling an incoming request in your endpoint implementation typically consists of looking up the corresponding service to verify that it exists and that it’s running. The findByTypeAndName method on the TLfnServiceRepository allows you to do that. You get an Optional back, which is empty if the service doesn’t exist. It’s a good idea to return a "service not found" error or something similar in this case. If the service does exist, extract it from the Optional and check the state of the service.

You should only handle requests for running services. If it’s not running, a LuciadFusion user has stopped the service, or it’s in an unready state. You can return the same error that you return when a service doesn’t exist.

The SHP Quiz service controller implements this lookup, as illustrated by the following code snippet. The method returns a ModelAndView in case of an error, because the SHP Quiz service makes use of views. This might not be applicable to your implementation.

Lookup service in the TLfnServiceRepository. (from samples/fusion/platform/shpquiz/controller/ShpQuizController.java)
Optional<TLfnService> serviceOptional = fServiceRepository.findByTypeAndName("shp-quiz", aServiceName);
if (!serviceOptional.isPresent() || serviceOptional.get().getStatus() != ELfnServiceStatus.RUNNING) {
  ModelAndView notFoundView = new ModelAndView("NotFoundShpQuiz");
  notFoundView.addObject("notFoundMessage", "Quiz service with name \"" + aServiceName + "\" not found");
  notFoundView.setStatus(HttpStatus.NOT_FOUND);
  return notFoundView;
}

What you do after looking up the corresponding service depends on what you need in your implementation.

Most likely, you need to look up the products in a service. The TLfnProductRepository provides the method findByServiceId for this. You can also use the findByServiceIdAndProductName method if you already have the product name as part of your incoming request.

To look up the data in a specific product, use the findByProductId method on the TLfnDataRepository repository. You can then decode the data into an ILcdModel using an ILcdModelDecoder if necessary.

The SHP Quiz service type uses the product repository to look up all products in a service when it’s generating the list of quizzes. When it handles a request for the quiz question or answer page, it looks up the related product using the repository. To generate the quiz questions, it needs to read the elements in the SHP data. It uses the data repository to look up the data in a product and then decodes the data into a model. The product validator for the SHP Quiz service limits the number of data sets in a product to one. Therefore, the quiz service uses only the first element in the returned list.

Lookup and decode data. (from samples/fusion/platform/shpquiz/controller/ShpQuizController.java)
List<TLfnData> dataList = fDataRepository.findByProductId(product.getId());
TLfnData data = dataList.get(0);

TLcdSHPModelDecoder2 decoder = new TLcdSHPModelDecoder2();
ILcdModel model = decoder.decodeSource(data.getDataSource());

Publishing service and product access events

Custom services should publish service and product access events whenever a service instance is handling a request. LuciadFusion makes the TLfnAccessEventPublisher bean available in the Spring application context. Use this event publisher to publish access events for your custom services. This table describes the difference between the access event types:

Event type Description

TLfnServiceAccessEvent

Indicates that a request has been sent to a running service instance. Publish this event as soon as you successfully retrieve a service from the TLfnServiceRepository while handling a request in your service endpoint handler.

TLfnProductAccessEvent

Indicates that a service instance has returned information about product, or data within that product, while handling a request. For example, when a WMS service handles a GetMap request, it publishes a product access event for the product with the data of the requested map. Send this event only when you’re reasonably sure that the request will be handled successfully.

It’s possible that you need to publish both a TLfnServiceAccessEvent and a TLfnProductAccessEvent while you’re handling a single request.

You can publish a TLfnServiceAccessEvent by calling the method publishServiceAccessEvent and passing it the HttpServletRequest that triggered the request and the accessed service.

Publish service access event. (from samples/fusion/platform/shpquiz/controller/ShpQuizController.java)
private void publishServiceAccessEvent(HttpServletRequest aRequest, TLfnService aService) {
  // publish TLfnServiceAccessEvent
  fAccessEventPublisher.publishServiceAccessEvent(aRequest, aService);
}

You can publish a TLfnProductAccessEvent by calling the method publishProductAccessEvent and passing it the HttpServletRequest that triggered the request, the requested product, and the service with the requested product.

Publish product access event. (from samples/fusion/platform/shpquiz/controller/ShpQuizController.java)
private void publishProductAccessEvent(HttpServletRequest aRequest, TLfnService aService, TLfnProduct aProduct) {
  // publish TLfnProductAccessEvent
  fAccessEventPublisher.publishProductAccessEvent(aRequest, aService, aProduct);
}

Caching for performance

Information caching helps speed up the handling of requests by your service endpoint. Sometimes, the cached information relates to a data set, product, or service in LuciadFusion Studio, such as a model decoded from a data in the service, or metadata generated from the products in the service.

When that is the case, you must make sure that you invalidate the cached data when the corresponding data, product, or service receives an update in LuciadFusion. All data, products, and services returned by the repositories have a version attribute that you can access using the getVersion() method. The version increases every time that the data, product or service updates. Storing the version of the related data, product, or service while the information is cached, allows you to check if the information in the cache is outdated.

The SHP Quiz service type caches the decoded data so that it doesn’t need to decode the data each time it needs to generate a new quiz question. The Question Service instance that holds the cached data also stores the version number of the related product when it’s created. You must compare the version number to the version number of the product returned by the repository to know if you can still use the Question Service.

Verify cached Quiz Question Service. (from samples/fusion/platform/shpquiz/controller/ShpQuizController.java)
ShpQuizQuestionService questionService = fQuestionServiceCache.get(product.getId());
if (questionService == null || product.getVersion() > questionService.getVersion()) {
  // Dispose of the outdated service
  if (questionService != null) {
    questionService.dispose();
  }

  // Use the data repository to lookup the data inside the product.
  // Since only one SHP data is allowed per product by the ShpQuizServiceTypeProductValidator, take only the first element of the returned list.
  // The data source can be decoded into a model.
  List<TLfnData> dataList = fDataRepository.findByProductId(product.getId());
  TLfnData data = dataList.get(0);

  TLcdSHPModelDecoder2 decoder = new TLcdSHPModelDecoder2();
  ILcdModel model = decoder.decodeSource(data.getDataSource());

  // Create a new question service and add it to the cache.
  questionService = new ShpQuizQuestionService(product.getVersion(), model);
  fQuestionServiceCache.put(product.getId(), questionService);
}

ShpQuizQuestion quizQuestion = questionService.generateNewQuizQuestion();

You can use a Least Recently Used (LRU) Cache to store cache information. The benefits of such a cache are that it limits the amount of cached data, while you can also efficiently re-use the cached data.

We’ve now discussed all the requirements you must fulfill to add a custom service type to LuciadFusion.

You can also use the custom services capabilities of LuciadFusion to offer LuciadLightspeed API as a web service. Check out the TEA service sample if you’re interested in that.

Full sample code

For all the code related to the SHP Quiz sample, see the samples/src/samples/fusion/platform/shpquiz folder of the LuciadFusion installation directory.