Skip to content

Microservice Implementation

Matthew Horridge edited this page Oct 21, 2023 · 1 revision

WebProtégé Service 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.

Requirements

A WebProtégé service implementation, as described below, requires Java 16, or higher. We assume that you will be using Maven for dependency management.

Spring Boot Project Set Up

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.

Basic Dependencies and Application Configuration

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 @Importsannotation 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);
    }
}

Inter-Service Communication

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:

  1. 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.
  2. 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.

Request and Response Classes

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 Responseinterface 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 {
}

Executing Requests and Receiving Responses

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-ipcdependency) 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.
    var clsFrame = futureResponse.get();
}

Handling Events

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 @WebProtegeHandlerannotation. 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
    }
}

Handling Calls From Other Services

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.

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.

Mandatory Configuration Properties

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