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.
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
andname
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 ishttp://localhost:8081/shpquiz/{serviceName}
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:
-
The service type for the
getServiceType
method -
The endpoint path of the service for the
getEndpointPath
method -
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:
-
The maximum number of products in a service for the
getMaxAllowedProductCount
method. ReturnOptionalInt.empty()
to indicate that you don’t want any limitation. -
The maximum number of data in a product for the
getMaxAllowedDataCount
method. Again, returnOptionalInt.empty()
if you don’t want limitations. -
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
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
@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.
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
.
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 |
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 |
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 |
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.
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}")
.
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.
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:
-
The
TLfnDataRepository
.
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
.
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.
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.
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 |
---|---|
Indicates that a request has been sent to a running |
|
Indicates that a |
It’s possible that you need to publish both a |
You can publish a TLfnServiceAccessEvent
by calling the method publishServiceAccessEvent
and passing it the HttpServletRequest
that triggered the request and the accessed service
.
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
.
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.
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. |