-
Notifications
You must be signed in to change notification settings - Fork 5
Microservice Implementation
This document provides some details on how to implement a WebProtégé microservice as a Spring Boot application. Familiarity with Spring Boot (and the Spring Framework, in general) is assumed. If you do not know the Spring framework then we suggest that you learn the basics of Spring configurations, beans and dependency injection before proceeding to read this documentation.
A WebProtégé service implementation, as described below, requires Java 16, or higher. We assume that you will be using Maven for dependency management.
You should begin by creating a skeleton Spring Boot Application project. To do this you can use Spring Initializer at https://start.spring.io. A WebProtégé service doesn't require the addition any specific Spring Boot dependencies when creating a project with the Spring Initializer. If you need other dependencies for your service, such as Spring Data MongoDB, you should add what is necessary here.
Most services will require the following WebProtégé-specific libraries as dependencies. You should add these to your pom file in the usual way and you should import the specified Spring configurations as shown below (using an @Imports
annotation on your Spring Boot Application class).
GroupId | ArtifactId | Configuration to Import (see below) |
---|---|---|
edu.stanford.webprotege |
webprotege-common |
edu.stanford.protege.webprotege.common.WebProtegeCommonConfiguration |
edu.stanford.webprotege |
webprotege-ipc |
edu.stanford.protege.webprotege.ipc.WebProtegeIpcApplication |
edu.stanford.webprotege |
webprotege-jackson |
edu.stanford.protege.webprotege.jackson.WebProtegeJacksonApplication |
The following code gives an example of how you can set up your service Spring Boot Application class. In this case our MyServiceApplication imports the three configuration/application classes described in the table above.
@SpringBootApplication
@Import({WebProtegeCommonConfiguration.class,
WebProtegeIpcApplication.class,
WebProtegeJacksonApplication.class})
public class MyServiceApplication {
public static void main(String[] args) {
SpringApplication.run(MyServiceApplication.class, args);
}
}
WebProtégé employs an asynchronous messaging system to facilitate communication among services built on the foundation of Apache Pulsar. Apache Pulsar is an open-source distributed messaging and event streaming platform that is designed to provide reliable and efficient communication between different parts of a software system. It is commonly used for data and event streaming, and it offers features like publish-subscribe and message queuing.
Messages, which consist of a payload and a set of message headers are posted to a channels where they are picked off by listeners who are interested in them and can handle them. You can read more about this on the API Wiki Page.
There are two main styles of interaction that WebProtégé supports:
- Command-Based (Request/Response) – A request message is posted to a channel. Some handler for the request, that is listening to that channel, pulls the request message from the queue for the channel, handles it and then submits a response to a reply channel. The original requester that posted the request listens for responses on the reply channel.
- Event-Based – A service publishes events to a channel and these events can be listened out for and handled by anyother service that is interested in them. This is a one-to-many broadcast pattern.
The webprotege-ipc
dependency contains functionality for executing these asynchronous calls. It takes care of matching up replies to requests using correlation ids and hides away the underlying Apache Pulsar details.
For the Command-Based style of interaction, the edu.stanford.protege:webprotege-common
dependency contains a Request
interface and a Response
interface that concrete request/response classes implement.
The following code shows the request class (implemented as a Java record) for the GetClassFrameReqest
request. This class implements ProjectRequest
, which extends the Request
class and provides the ProjectId
that specifies the target project for the request.
Notice that the request class specifies the channel name as a public constant for use elsewhere (typially by the request handler – see below for an example of this).
// Our GetClassFrameRequest holds a projectId and an owl:Class as the subject.
// These two components identify the class frame to be retrived.
// We actually implement the ProjectRequest interface, a more specific
// inteface than the Request interface, which marks a request pertinent to a particular project.
public record GetClassFrameRequest(ProjectId projectId,
OWLClass subject) implements ProjectRequest<GetClassFrameResponse> {
// The only thing we really have to do to comply with the Request interface
// is to specify the channel on which the request should be made. Channel names
// are arbitrary, however the core WebProtege requests use channel names starting
// with a "webprotege." prefix. You should refrain from using this prefix for non-core
// requests
public static final String CHANNEL = "webprotege.frames.GetClassFrame";
// Returns the channel name for the request
@Override
public String getChannel() {
return CHANNEL;
}
}
In general, when naming your channels, you should use something other than "webprotege" as a prefix as this prefix is used for the core WebProtégé APIs.
The response class for the GetClassFrameReqest
is shown below. The Response
interface is merely a marker interface and this contains no methods to implement.
// Our GetClassFrameResponse simply has the sought after ClassFrame as a component
// and it implements the Response interface
public record GetClassFrameResponse(ClassFrame classFrame) implements Response {
}
For a given type of request (i.e. class of request), you should use an instance of edu.stanford.webprotege.ipc.CommandExecutor
to send the request and receive the response. Instances of CommandExecutor
are created by the Spring framework (using code in the webprotege-ipc
dependency) and can be injected into Java classes where they'll be used.
Suppose we wanted a command executor for executing a GetClassFrameRequest
. We need to define a Spring Bean (as usual, in a Spring configuration) of the type, CommandExecutor<GetClassFrameRequest, GetClassFrameResponse>
. This can be done as follows,
// Define a Spring Bean for the CommandExecutor for the GetClassFrameRequest/Response
// This Bean definition would be part of a Spring configuration.
@Bean
CommandExecutor<GetClassFrameRequest, GetClassFrameResponse> commandExecutorForGetClassFrameRequest() {
// We need to return an instance that takes a reference to the type of the Request. In
// this case, the GetClassFrameRequest.class
return new CommandExecutor<>(GetClassFrameRequest.class);
}
Now, a CommandExecutor<GetClassFrameRequest, GetClassFrameResponse>
can be injected at the point where we need to executor the request. A call to a CommandExecutor will return a CompletableFuture for the response. For example,
// Inject the command executor for the GetClassFrameRequest/Response into our Java class. In practice
// we'd use contructor injection ;)
@Autowired
private CommandExecutor<GetClassFrameRequest, GetClassFrameResponse> executor;
public void doSomething() {
// Create the request to get the class frame for the specified project and specified entity
var request = new GetClassFrameRequest(projectId, entity);
// Execute the request. This returns a CompletableFuture for the response
var futureResponse = executor.execute(request, executionContext);
// There are various (non-blocking) ways to handle CompletableFutures. Here we
// just block using the "get" method until we have the response. We handle any exceptions
// here too. There are two types: An InterruptedException, which comes from our blocking
// call to "get", and an ExecutionException, which contains a reference to anything thrown
// by the handling of the request itself. The ExecutionException contains a cause that is
// intended to be a CommandExecutionException. This exception essentially contains an HTTP
// status code that indicates the problem that occurred.
try {
var clsFrame = futureResponse.get();
} catch (InterruptedException e) {
// Handle Interrupted Exception
} catch (ExecutionException e) {
// Handle Execution Exception.
if(e.getCause() instanceof CommandExecutionException ex) {
var httpStatusCode = ex.getStatusCode();
var httpReasonPhrase = ex.getMessage();
}
else {
// Some other error was thrown
}
}
}
Events in WebProtégé are messages that are posted and handled asynchronously by listeners. Like requests, a specific type of event is posted to a specific channel. For example, the OntologyChanged
is posted to the channel, webprotege.projects.events.OntologyChanged
. Thus, listeners must listen to a particular channel to receive particular types of events.
Events can be handled by creating a Java class that implements edu.stanford.protege.webprotege.ipc.EventHandler
and is marked with the @WebProtegeHandler
annotation. If such a class is placed in a sub-package of the service Spring Boot Application then the handler will be auto-discovered and automatically registered.
Handlers for events are named so that it is possible to have multiple handlers for the same event registered in a single service. The code below shows an example handler that handles OntologyChanged
events.
// To register an event handler we need to create a Java class that implements
// EventHandler and we need mark it with an @WebProtegeHandler annotation.
@WebProtegeHandler
public class MyChangeHander implements EventHandler<OntologyChangedEvent> {
// We need to supply a handler name. The handler name acts as a unique
// identifier for the handler. Handler names should be the same across
// different runs of the application and accross different instances of
// the application.
@Nonnull
@Override
public String getHandlerName() {
return "MyOntologyChangedEventHandler";
}
// The handler needs to specify the channel it is listening to by overriding
// the getChannelName method. Here, we take the channel name form the
// OntologyChangedEvent class
@Nonnull
@Override
public String getChannelName() {
return OntologyChangedEvent.CHANNEL;
}
// Return the class of event that we handle
@Override
public Class<OntologyChangedEvent> getEventClass() {
return OntologyChangedEvent.class;
}
// Handle any OntologyChanged events
@Override
public void handleEvent(OntologyChangedEvent event) {
// Code specific to this event handler
}
}
If your service provides an API for other services to call then you will need to handle these calls. Calls, or rather, commands, are implemented using requests, responses and command handlers.
To handle calls you can can create a class that extends edu.stanford.protege.webprotege.ipc.CommandHandler
(located in the edu.stanford.protege:webprotege-ipc
dependency). This interface is parameterized with request and response classes.
To give you an idea, the example below shows part of the handler for the GetClassFrameRequest
.
@WebProtegeHandler
public class GetClassFrameCommandHandler implements CommandHandler<GetClassFrameRequest, GetClassFrameResponse> {
// Constructor and fields
@Override
public String getChannelName() {
return GetClassFrameRequest.CHANNEL;
}
@Override
public Class<GetClassFrameAction> getRequestClass() {
return GetClassFrameRequest.class;
}
@Override
public Mono<GetClassFrameResponse> handleRequest(GetClassFrameRequest request,
ExecutionContext executionContext) {
// Code to actually handle the request
}
}
First off, the handler is annotated with the WebProtegeHandler
annotation. Next, the command handler specifies the channels that it listens to through the implementation of the getChannelName
method. It als provides the class of supported requests with the getRequestClass
method. Finally, the handleRequest
method does the work of actually handling the request and returning a response. This method has a GetClassFrameRequest
parameter, representing the actual request, and an ExecutionContext
parameter, which contains details of the user making the request.
Command handler classes, such as the one above should simply be implemented in a sub-package of the package containing the Spring Boot application class. The handler will automatically be picked up and registered during component scanning at start-up.
Your service must specify the spring.application.name
configuration property. This property is used by the IPC framework to determine the name of reply channels. You should also set the following properties:
# The application name must be set
spring.application.name=MyApplicationService
# The name of the Pulsar tenant to use. In a production setting this should usually be webprotege
webprotege.pulsar.tenant=webprotege
# URLs for the Pulsar connection:
# The Pulsar HTTP service URL
webprotege.pulsar.serviceHttpUrl=http://localhost:8080
# The Pulsar service URL
webprotege.pulsar.serviceUrl=pulsar://localhost:6650