Specifying OpenAPI information with Helidon
This is an optional module for the Helidon lab. It is currently not required that this module be completed to enable other optional modules.
Self guided student - video introduction
This video is an introduction to the Helidon Open API (ex Swagger) lab. Depending on your browser settings it may open in this tab / window or open a new one. Once you've watched it please return to this page to continue the labs. [![Helidon Open API lab Introduction Video](https://img.youtube.com/vi/A_gVG2cb308/0.jpg)](https://youtu.be/A_gVG2cb308 "Helidon Open APIlab introduction video") ---Introduction
This is an optional module with coding.
Estimated module duration 20 mins.
Objectives
This module shows us how Helidon supports the creation of OpenAPI documents based on the actual specific configuration of the Helidon microservices, enabling you to be sure that you can get the current API description and not require manual maintenance of the information.
Prerequisites
You need to have completed the Cloud Native support in Helidon module.
Task 1: Why have a self describing API ?
This module is how to get Helidon to self-describe the REST API you are offering. There are several use cases for this, some of those are :
A/ You need the API description in a consistent machine readable form that can be processed by other applications, for example you are using an API gateway and need to define rules for certain endpoints
B/ You want to ensure that the documentation of the REST API actually reflects the implementation, it’s not uncommon for documentation and implementation to be out of sync if they are separately maintained through manual processes.
C/ You can’t find the documentation for a service, or it is a service where you have the jar file, but the documentation was on a since removed website and not even the internet archive can help!
D/ You want to automatically create code based on the API.
To address this the idea of self describing API end points was developed, originally this was developed by an application having a “well known” URL that just returned a manually maintained text document that was distributed as part of the jar file. It then evolved into machine readable data formats, for example the Swagger REST API description format based on JSON, which has since been donated to the community under the name Open API and is now the de-facto data format. For a more detailed history see the Wikipedia Open API page.
OpenAPI describes the data format, but there has also been the development of tooling and programming approaches to the automatic runtime generation of the OpenAPI data based on information in the source code. This means that the OpenAPI documentation delivered by the service is automatically in sync with the actual code of the service itself.
In the case of Microprofile (and this Helidon as a Microprofile implementation) this was done through the MicroProfile OpenAPI specification which defines annotations to be used when generating the OpenAPI documents and associated the control mechanisms (for example to exclude API elements that are not intended to be publicly exposed)
Task 2: Defining the API documentation in your code
When a system like Helidon MP runs the annotations you’ve applied to your code need to be identified so they can be processed. This is done automatically for you by the runtime, and annotation processors are called to perform whatever actions you have specified (e.g. setting up a start / stop on a timer on entry / exit form a method)
There are two ways to identify the annotations in your code:
A/ The runtime can search through every class and method to find them each time the program is started, this can be time consuming if you have a lot of code
B/ When the service is built the build process creates an index (usually packaged into the jar file) that is processed when the service starts, and each annotation can be immediately identified. This means that the code scanning process only happens once at build time and results in a faster startup, though it does mean a slightly longer build phase and also a slightly larger deployment.
In general the costs of building and storing an index are way smaller than those of scanning each time, and this is the approach that Helidon uses when packaging a service using Maven builds, though when working with within Eclipse (which has it’s own built in version of Maven) the compile process only does a scan as otherwise you’d have to build the index each time you saved a file which would slow down the development process (This is why you may see warning messages about missing jandex.idx files when running the micro-servcies in Eclipse)
How do I enable the OpenAPI support in Helidon
To use OpenAPI you need to include the right jar files in your build. That's already been done for you in the Maven pom.xml file when the Helidon parent project was imported, so you don;t need to do anything.To build the index Heldion uses a tool called jandex (Java ANnotaiton inDEXer) to build the index. We will see later in this section how that’s run.
Task 3: Annotating the Storefront
In this module we will be adding annotations to describe the storefront service and the data it consumes and returns. In a production environment you may chose to limit what’s documented and restrict it to only the public API elements intended to be seen outside your project (this will of course be up to you how you do this, but in general it’s good practice not to document something that can’t be seen externally)
You may of course chose to document other services, for example the stockmanager would normally not publicly visible outside the Kubernetes cluster, but you may chose to document it’s API to help building internal clients of the service.
Task 3a: Accessing the OpenAPI documentation
-
Make sure that the storefront microservice is running.
-
Check you can access the generated OpenAPI documentation using curl
<copy>curl http://localhost:8080/openapi</copy>
info:
title: Generated API
version: '1.0'
openapi: 3.0.3
paths:
/minimumChange:
post:
requestBody:
content:
application/json:
schema:
format: int32
type: integer
responses:
'200':
description: OK
get:
responses:
'200':
description: OK
/status:
get:
responses:
'200':
content:
application/json:
schema:
type: object
description: OK
/store/reserveStock:
post:
requestBody:
content:
application/json:
schema:
type: object
responses:
'200':
content:
application/json:
schema:
type: object
description: OK
/store/stocklevel:
get:
responses:
'200':
content:
application/json:
schema:
items: {}
type: array
description: OK
The Open API looks at the code and builds a set of responses based on using Java introspection on the resources, technically this provides information about what the API looks like, but it’s not that useful, for example we don’t know the details of the JSON objects returned, or in what conditions a given response is generated. Also as it uses introspection which is not that efficient, as we will see later we can create an index at compile time that avoids the introspection and is thus more efficient.
Task 3b: Defining the Service itself
Firstly we shall define what the top level service provides, this can be done on any of the classes managed by the content and dependency injection syb-system, but it seems most relevant to place the annotations on the StorefrontApplication class.
- Open the StorefrontApplication.java file, and add the following @OpenAPIDefiniton annotation on the class declaration itself:
<copy>@OpenAPIDefinition(info = @Info(title = "StorefrontApplication", description = "Acts as a simple stock level tool for a post room or similar", version = "0.0.1"))</copy>
The resulting class declaration should look like
@ApplicationScoped
@ApplicationPath("/")
@OpenAPIDefinition(info = @Info(title = "StorefrontApplication", description = "Acts as a simple stock level tool for a post room or similar", version = "0.0.1"))
public class StorefrontApplication extends Application {
Java Imports
You may need to add the following import to the class ```javaExplanation of the annotations
The `@OpenAPIDefinition` indicates that this is defining the API top level details for this project. The `@Info` is an annotation that defines what that information actually is. The title, description and version fields are I hope self explanatory ---- Save the file and restart the storefront Check you can access the generated OpenAPI documentation using curl
<copy>curl http://localhost:8080/openapi</copy>
info:
description: Acts as a simple stock level tool for a post room or similar
title: StorefrontApplication
version: 0.0.1
openapi: 3.0.3
paths:
/minimumChange:
post:
requestBody:
.....
<the rest of the output>
This has now told us what the application itself does - this is a pretty good start.
Didn't see the updates ?
This means that you probably already have a index file, in some situations this can be created if you did a Maven build. For now we're going to just remove it and rely on introspection - we'll see how to create the index file itself for more efficient use later on. - To remove the index file open the Home directory in the desktop. then open the workspace - Click right in the window and chose the "Open Terminal here" option - In the terminal run the following command, be sure to run it exactly as it is below ```bashTask 4: The default OpenAPI document
Now we’ve added an annotation covering the application description let’s look at basic document. There is no need to have the stockmanager running, but if it already is don’t worry.
Task 4a: What does this output mean ?
In summary it means that adding the @OpenAPIDefinition
triggered Helidon to scan for classes references by the application, looking for REST endpoints (@GET
, @POST
etc. annotations). Helidon then builds a OpenAPI document that returns the YAML description. Note that the precise order of the major sections may change (it depends on the order the annotations are processed) so you may see the info:
section before or after the path:
section
- First locate the
info:
section.
info:
description: Acts as a simple stock level tool for a post room or similar
title: StorefrontApplication
version: 0.0.1
This contains the information you supplied to the @OpenAPIDefinition
annotation.
-
The
openapi: 3.0.3
simply defines what version of the OpenAPI document specification this document conforms to. -
Now let’s look at the
paths
section
paths:
/minimumChange:
post:
requestBody:
content:
application/json:
schema:
format: int32
type: integer
responses:
'200':
description: OK
get:
responses:
'200':
description: OK
/status:
get:
responses:
'200':
content:
application/json:
schema:
type: object
description: OK
/store/reserveStock:
post:
requestBody:
content:
application/json:
schema:
type: object
responses:
'200':
content:
application/json:
schema:
type: object
description: OK
/store/stocklevel:
get:
responses:
'200':
content:
application/json:
schema:
items: {}
type: array
description: OK
The paths:
section defines the REST endpoints offered by the service, in this case /minimumChange
, /status
, /store/reserveStock
and /store/stocklevel
for each path it defines the method used (get
, post
) the parameters to the request and the response. Thus we can see that the /store/stocklevel
path returns an array of objects of type application/json
.
Want the output in JSON ?
Helidon can generate the OpenAPI document in yaml (the default) or in JSON. To generate JSON use the Accept header. - In a terminal window type: ```bashTask 4b: To many paths, how do we hide private ones ?
This has given us the entire API, but we may not want people to be using /minimumChange
and /status
is probably not relevant to external callers, and either. (As we’ll see in the Kubernetes sections it’s more for the internal operation of the cluster and availability than something an client would call). So we need a way to remove some end-points from the output.
-
Open the src/main/resources/META-INF/microprofile-config.properties file
-
Uncomment the line
mp.openapi.scan.exclude.classes=com.oracle.labs.helidon.storefront.resources.StatusResource,com.oracle.labs.helidon.storefront.resources.ConfigurationResource
(It’s the last line, so it may be a bit hidden)
This tells Helidon to ignore any paths it finds in the StatusResource
and ConfigurationResource
classes when generating the Open API document.
What about the Rest Client interfaces ? They have matching annotations
Many microservices also talk to other microservices, and so their code may include interfaces representing those microservices. Those annotations may of course include annotations that make it look as it they are a REST endpoint, for example the interface will have `@Path` annotations. Of course you don't want them to be included in the OpenAPI spec of your service (interfaces are after all internal implementation mechanisms, not part of your public API). Fortunately Helidon's OpenAPI implementation seems to automatically ignore any interfaces annotated with RegisterRestClient so you don't have to exclude those by hand. You may have noticed that you haven't seen anything relating to the StockManager interface in the OpenAPI output. ---Other configuration settings for OpenAPI
The Helidon runtime supports a large number of configuration settings that can be used to control the generation of the OpenAPI document, this include the ability to specify packages as well as classes to include / exclude, or if you need finer grained control you can even define filter and model classes that chose exactly which paths will be included or removed. See the OpenAPI documentation (link at the bottom of this module) for the full details. ---Why use the microprofile-config.properties file
It is of course possible to apply this exclusion in any of the config files, but this is an example of a setting that as a developer you probably want to have applied by default in every deployment, after all you're suppressing internal information that you probabaly wouldn't want to be visible to non developers. The microprofile-config.properties file is embedded into the class path, so that will ensure that the default behavior is what you want. If someone want's to they can of course override that in a local file system based config file such as conf/storefront-config.yaml ---Let’s see how this looks.
-
Restart the storefront service (this will cause the the microprofile-config.propeties file to be re-read - it isn’t one of the ones that we configured to be checked for changes)
-
Check out the updated documentation that’s generated
<copy>curl -i http://localhost:8080/openapi</copy>
info:
description: Acts as a simple stock level tool for a post room or similar
title: StorefrontApplication
version: 0.0.1
openapi: 3.0.3
paths:
/store/reserveStock:
post:
requestBody:
content:
application/json:
schema:
type: object
responses:
'200':
content:
application/json:
schema:
type: object
description: OK
/store/stocklevel:
get:
responses:
'200':
content:
application/json:
schema:
items: {}
type: array
description: OK
This looks much better, we can see the details of the core REST API we want to expose, and we’re not polluting it with end-points that in a production system would not be exposed.
Strictly speaking this is all that you need to be able to use the API from a caller perspective, you know what to send and what to expect in return, but it’s not very detailed information, and it doesn’t actually tell you much about what those end-points do (of course this is not completely true here because as a good programmer I’ve tried to use meaningful names).
Task 5: OpenAPI annotations on the REST Methods
Task 5a: Documenting the methods
We should also describe the actual REST API endpoints themselves.
As we’ve excluded the StartResource and ConfigurationResource on the basis that in this case they would not normally be externally visible we only need to document the StorefrontResource.
-
Open the StorefrontResource.java file
-
Add
@Operation
and@APIResponse
annotations as below to the listAllStock method
The result will look like
@Metered(name = "listAllStockMeter", absolute = true)
@Operation(summary = "List stock items", description = "Returns a list of all of the stock items currently held in the database (the list may be empty if there are no items)")
@APIResponse(description = "A set of ItemDetails representing the current data in the database", responseCode = "200", content = @Content(schema = @Schema(name = "ItemDetails", implementation = ItemDetails.class, type = SchemaType.ARRAY), example = "[{\"itemCount\": 10, \"itemName\": \"Pencil\"},"
+ "{\"itemCount\": 50, \"itemName\": \"Eraser\"}," + "{\"itemCount\": 4600, \"itemName\": \"Pin\"},"
+ "{\"itemCount\": 100, \"itemName\": \"Book\"}]"))
public Collection<ItemDetails> listAllStock() {
....
Java Imports
You may need to add the following imports to the class ```javaExplaining the annotations
`@Operation` is basically a description of the REST end point itself, most of the attributes should be self explanatory. There is one useful attribute for the `@Operation` annotation which is the `hidden` attribute. If `hidden` is set to true, for example `@Operation(hidden = true)` then this REST end point will not be included in the generated OpenAPI documentation. This means that you can hide some REST end points in a class without hiding all of them, so in effect a per method version of the `mp.openapi.scan.exclude.classes` property. `@APIResponse` defines what the results of the operation are, hopefully the responseCode indicates the HTTP status code this defines (more on different codes later) For reasons that are unclear this is defined as a String rather than a numeric code, or a StatusType value, I suspect this means you could use the response name rather than just the numeric code, but as most code generators would use the numeric value this seems a bit odd. The content attribute of the `@APIResponse` defines what the method returns, in this case an array of instances of ItemDetails (we specify the name and that will be used later to refer to the ItemDetails, I'm unclear why it can't just extract the name automatically from the implementation class if a name isn't provided though). Note that it also shows some example text, This can be used by some code generation tools to auto generate a "stub" response for you. ---Let’s look at the updated REST API description
-
Stop the existing instance of storefront and restart it, this will do it’s usual scan and the new annotations will be recognized.
-
Get the updated documentation, in a terminal window type
<copy>curl -i http://localhost:8080/openapi</copy>
(The following has been truncated to only include the /store/stocklevel
part of the paths:
section)
/store/stocklevel:
get:
description: Returns a list of all of the stock items currently held in the
database (the list may be empty if there are no items)
responses:
'200':
content:
application/json:
example: '[{"itemCount": 10, "itemName": "Pencil"},{"itemCount": 50,
"itemName": "Eraser"},{"itemCount": 4600, "itemName": "Pin"},{"itemCount":
100, "itemName": "Book"}]'
schema:
items:
type: object
type: array
description: A set of ItemDetails representing the current data in the database
summary: List stock items
We can now see the details of the /store/stocklevel
end point.
Let’s now add annotations to the /store/reserveStock
method that let’s us describe the arguments to the end point
-
Open the StorefrontResource.java file
-
Add the following annotations to the reserveStock method
<copy>@Operation(summary = "Reserves a number of stock items", description = "reserves a number of stock items in the database. The number of stock items being reserved must be greater than the defined minimum change")
@APIResponse(description = "The updated stock details for the item", responseCode = "200", content = @Content(schema = @Schema(name = "ItemDetails", implementation = ItemDetails.class), example = "{\"itemCount\": 10, \"itemName\": \"Pencil\"}"))</copy>
- Add the following annotation to the reserveStock method itemRequest parameter
<copy>@RequestBody(description = "The details of the item being requested", required = true, content = @Content(schema = @Schema(name = "ItemRequest", implementation = ItemRequest.class), example = "{\"requestedItem\",\"Pencil\",\"requestedCount\",5}"))</copy>
The updated method declaration should now look like the following. Note that other annotations for metrics, timers etc. may not be as displayed here depending on what sections of the lab you’ve done. Comments have been omitted to simplify the text
@POST
@Path("/reserveStock")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Timed(name = "reserveStockTimer")
@Fallback(StorefrontFallbackHandler.class)
@Operation(summary = "Reserves a number of stock items", description = "reserves a number of stock items in the database. The number of stock items being reserved must be greater than the defined minimum change")
@APIResponse(description = "The updated stock details for the item", responseCode = "200", content = @Content(schema = @Schema(name = "ItemDetails", implementation = ItemDetails.class), example = "{\"itemCount\": 10, \"itemName\": \"Pencil\"}"))
public ItemDetails reserveStockItem(
@RequestBody(description = "The details of the item being requested", required = true, content = @Content(schema = @Schema(name = "ItemRequest", implementation = ItemRequest.class), example = "{\"requestedItem\",\"Pencil\",\"requestedCount\",5}")) ItemRequest itemRequest)
throws MinimumChangeException, UnknownItemException, NotEnoughItemsException {
Java Imports
You may need to add the following imports to the class ```java-
To re-scan for the annotations stop then restart the storefront instance.
-
Get the updated documentation, in a terminal window type
<copy>curl -i http://localhost:8080/openapi</copy>
(The following has been truncated to only include the reserveStock path in the paths:
section)
/store/reserveStock:
post:
description: reserves a number of stock items in the database. The number of
stock items being reserved must be greater than the defined minimum change
requestBody:
content:
application/json:
example: '{"requestedItem","Pencil","requestedCount",5}'
schema:
type: object
description: The details of the item being requested
required: true
responses:
'200':
content:
application/json:
example: '{"itemCount": 10, "itemName": "Pencil"}'
schema:
type: object
description: The updated stock details for the item
summary: Reserves a number of stock items
We can see that there is a lot more info on the /store/reserveStock
REST endpoint, and also on the argument, we can see that it’s required and also a description, but we don’t actually see the details of the
Task 5c: Documenting the error status codes
Of course not every REST API call returns successfully, there may be problems, for example in the case of the reserveStock
method it might throw a UnknownItemException
In an earlier module we put an @Fallback
annotation on the method directing Helidon to pass exceptions a handler class which converts them into relevant http status codes, in this case an UnknownItemException
is converted into a 404 / Not Found status. But we need a way to document this and the other returns a client may reasonably be expected to handle.
- In the StorefrontResource class Add the following additional
@APIResponse
annotations to the reserveStock method
<copy>@APIResponse(description = "The requested item does not exist", responseCode = "404")
@APIResponse(description = "The requested change does not meet the minimum level required for the change (i.e. is <= the minimumChange value)", responseCode = "406")
@APIResponse(description = "There are not enough of the requested item to fulfil your request", responseCode = "409")</copy>
The updated method declaration should now look like this (comments omitted for clarity)
@POST
@Path("/reserveStock")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Timed(name = "reserveStockTimer")
@Fallback(StorefrontFallbackHandler.class)
@Operation(summary = "Reserves a number of stock items", description = "reserves a number of stock items in the database. The number of stock items being reserved must be greater than the defined minimum change")
@APIResponse(description = "The updated stock details for the item", responseCode = "200", content = @Content(schema = @Schema(name = "ItemDetails", implementation = ItemDetails.class), example = "{\"itemCount\": 10, \"itemName\": \"Pencil\"}"))
@APIResponse(description = "The requested item does not exist", responseCode = "404")
@APIResponse(description = "The requested change does not meet the minimum level required for the change (i.e. is <= the minimumChange value)", responseCode = "406")
@APIResponse(description = "There are not enough of the requested item to fulfil your request", responseCode = "409")
public ItemDetails reserveStockItem(
@RequestBody(description = "The details of the item being requested", required = true, content = @Content(schema = @Schema(name = "ItemRequest", implementation = ItemRequest.class), example = "{\"requestedItem\",\"Pencil\",\"requestedCount\",5}")) ItemRequest itemRequest)
throws MinimumChangeException, UnknownItemException, NotEnoughItemsException {
-
Stop the storefront, re-start it as usual
-
Get the updated documentation, in a terminal window type
<copy>curl -i http://localhost:8080/openapi</copy>
(The following has been truncated to only include the /store/reserveStock
path in the paths:
section)
/store/reserveStock:
post:
description: reserves a number of stock items in the database. The number of
stock items being reserved must be greater than the defined minimum change
requestBody:
content:
application/json:
example: '{"requestedItem","Pencil","requestedCount",5}'
schema:
type: object
description: The details of the item being requested
required: true
responses:
'200':
content:
application/json:
example: '{"itemCount": 10, "itemName": "Pencil"}'
schema:
type: object
description: The updated stock details for the item
'404':
description: The requested item does not exist
'406':
description: The requested change does not meet the minimum level required
for the change (i.e. is <= the minimumChange value)
'409':
description: There are not enough of the requested item to fulfil your request
summary: Reserves a number of stock items
We now have OpenAPI documentation that defines the reasonable error conditions that may be generated.
What API Responses to document ?
As a general rule of thumb you should only document the http status responses your end point might reasonably throw, in the case above that's 200 (OK), 404 / Not Found (when a request is made to reserve an item not in the database) 409 / CONFLICT (when there are not enough items available to reserve) and 406 / Not Acceptable (when the number of items to be reserved is not acceptable due to minimum change restrictions). We have added `@APIResponse` annotations to deal with those as any client could reasonably expect to encounter them, but for codes that may be generated due to internal problems, for example the catch all 500 / Internal Server error and it's related 5xx series of codes we have not documented as a client would not expect to encounter them under normal operation of the call. Here we have documented a the typical set of http status codes that the method can reasonably return, though of course exactly which ones are included in the documentation will vary by end point and your development standards. ---Task 6: Defining our data
You may have noticed that we don;t ac tually have any details of the data being transfered though, the content schema type is just object.
-
Open the ItemRequest class in the com.oracle.labs.helidon.storefront.data package and add @Schema annotations
-
Add the following annotation to the class definition itself
<copy>@Schema(name = "ItemRequest", description = "Details of a Item reservation request")</copy>
- Add the following annotation to the requestedItem field
<copy>@Schema(required = true, description = "Name of the item being requested", example = "Pencil")</copy>
- Add the following annotation to the requestedCount field
<copy>@Schema(name = "ItemRequest", description = "Details of a Item reservation request", example = "{\"requestedItem\", \"Pin\", \"requestedCount\",5}")</copy>
The resulting class looks like :
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "ItemRequest", description = "Details of a Item reservation request")
public class ItemRequest {
@Schema(required = true, description = "Name of the item being requested", example = "Pencil")
private String requestedItem;
@Schema(name = "ItemRequest", description = "Details of a Item reservation request", example = "{\"requestedItem\", \"Pin\", \"requestedCount\",5}")
private int requestedCount;
}
Java Imports
You may need to add the following import to the class ```javaExplaining the annotations
`@Schema` is a commonly used annotation with OpenAPI, it basically is used to identify data objects and their fields. There are a *very* large number of attributes that can be added to a `@Schema` annotation, and you can see common (and I hope self explanatory) ones here. There are also attributes that define minimum and maximum values of an attribute, or define the allowable fields of an enum. These could be used to enable client code to apply data validation checks itself before they get to the service itself. Some annotations like `required` might seem a bit strange, after all in Java there is no concept of optional attributes on a class or method, however it's important to remember that this relates to data being **transfered**, not to data at rest. The Helidon framework will create the **instance** of the class and it's quite reasonable that a class may have a default value for a field. In that case it's not going to be **required** as the REST API request defines a value for that field unless the request want's to override the default. Full details of the `@Schema` annotation are in the Microprofile OpenAPI documentation linked to at the end of this module. ---The ItemDetails class has already been updated for you with the same annotations
-
Stop the storefront, re-start it as usual
-
Get the updated documentation, in a terminal window type
<copy>curl -i http://localhost:8080/openapi</copy>
- Note that it is the same as before - the
@Schema
entries are not showing !
Task 7: The index file
Where is the data we just added ? When we run the storefront and look in the OpenAPI output for anything on ItemDetail and ItemRequest it’s not there! It turns out that we need an index file as the OpenAPI introspection can only take us so far, and doesn’t look at non REST API classes.
We’ve been relying on Java introspection to generate the index used for the OpenAPI documentation, though this is effective (up to a point), and means that the returned OpenAPI document represents your input it’s not that efficient, or complete. In fact you may have noticed content similar to the following in your output
2022.03.03 11:07:55.801 INFO io.helidon.microprofile.openapi.OpenApiCdiExtension !thread!: OpenAPI support could not locate the Jandex index file META-INF/jandex.idx so will build an in-memory index.
This slows your app start-up and, depending on CDI configuration, might omit some type information needed for a complete OpenAPI document.
Consider using the Jandex maven plug-in during your build to create the index and add it to your app.
In a production environment where we care less about the ease of seeing our changes and more about efficient operation we need to build an index of the annotations. As we just saw we also need to do this to ensure that we get all of the OpenAPI annotations processed - not just those relating to the REST API’s.
Unlike the REST processing annotations the OpenAPI processing only operates against a jandex index for non REST API methods, and won’t scan for OpenAPI annotations in the class files (I’m not sure if this is a bug or a feature)
Because of this the jandex index needs to be built to reflect the OpenAPI annotations. To do this you can manually run maven in Eclispe with the process-classes goal. If you run a full maven build it will also get built. To make the development process faster when we save a file in Eclipse we have disabled the creation of the jandex index (Eclipse runs it’s own internal maven processing every time a source code file is saved). This means that the jandex.idx file is not created on file save.
To be more efficient for now we will create a run configuration to run this processing and not run the entire build and packaging process.
Task 7a: Creating the index file
- Select the helidon-labs-storefront project (this is the project title in the explorer) click right -> Run As -> Maven Build …. (Chose the version with the dots)
Update the popup with the following
-
set the Name to be
helidon-labs-storefront (process-classes)
(if you chose something else it’s fine, just remember to use that later on) -
In the Goals enter
process-classes
-
Click Apply then
Close
Now to run a build with this target.
- Click right on the project name (helidon-labs-storefront) in Eclipse, then chose
Run As
thenMaven build
(This is the version without the three dots!)
Depending on the precise eclispe configuration there may be a resulting popup window, do not worry if this is not displayed and the build just continues
- If there is a popup then chose the process-classes option.
- Then click the OK button
In the console tab you’ll see output similar to the following
[INFO] ------------------------------------------------------------------------
[INFO] Detecting the operating system and CPU architecture
[INFO] ------------------------------------------------------------------------
[INFO] os.detected.name: osx
[INFO] os.detected.arch: x86_64
[INFO] os.detected.version: 12.2
[INFO] os.detected.version.major: 12
[INFO] os.detected.version.minor: 2
[INFO] os.detected.classifier: osx-x86_64
[INFO]
[INFO] -----------------< com.oracle.labs.helidon:storefront >-----------------
[INFO] Building storefront 2.4.1
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.7:resources (default-resources) @ storefront ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 3 resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ storefront ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- jandex-maven-plugin:1.0.6:jandex (make-index) @ storefront ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 13.783 s
[INFO] Finished at: 2022-03-03T11:09:19Z
[INFO] ------------------------------------------------------------------------
The version numbers may differ.
Towards the end of the output you can see that the Maven jandex plugin is run.
</details>
Task 7b: Using the index file
Now we have created the index file we can use it.
One key point here, the index file will always be used if it’s present abd the code will not use introspection, this means that now you’ve created it if you make changes to the OpenAPI annotations unless you re-build the jandex.idx file (or delete it) then you will not get your updates changes. To re-build it just run the maven build again with the process-classes target.
-
Run the storefront again
-
Note that this time you don’t get any warning in the output about a missing jandex.idx file
-
Get the updated documentation, in a terminal window type
<copy>curl -i http://localhost:8080/openapi</copy>
Note that there is a new section now called components
as shown below
components:
schemas:
ItemDetails:
required:
- itemCount
- itemName
properties:
itemCount:
description: The number of items listed as being available
example: 10
format: int32
type: integer
itemName:
description: The name of the item
example: Pencil
type: string
description: Details of the item in the database
example:
itemCount: 10
itemName: Pencil
type: object
ItemRequest:
required:
- requestedCount
- requestedItem
properties:
requestedCount:
description: Number of the items being requested, this must be larger than
the minimumChange
example: 5
format: int32
type: integer
requestedItem:
description: Name of the item being requested
example: Pin
type: string
description: Details of a Item reservation request
example: '{"requestedItem", "Pin", "requestedCount",5}'
type: object
The components
section included details of the classes we use to transfer the data using the @Schema
information we provided.
- Look at the
paths
section for/store/reserveStock
/store/reserveStock:
post:
description: reserves a number of stock items in the database. The number of
stock items being reserved must be greater than the defined minimum change
requestBody:
content:
application/json:
example: '{"requestedItem","Pencil","requestedCount",5}'
schema:
$ref: '#/components/schemas/ItemRequest'
description: The details of the item being requested
required: true
responses:
'200':
content:
application/json:
example: '{"itemCount": 10, "itemName": "Pencil"}'
schema:
$ref: '#/components/schemas/ItemDetails'
description: The updated stock details for the item
'404':
description: The requested item does not exist
'406':
description: The requested change does not meet the minimum level required
for the change (i.e. is <= the minimumChange value)
'409':
description: There are not enough of the requested item to fulfil your request
summary: Reserves a number of stock items
Note that the description of the request now has a schema field that refers into the components section (using the name we provided in the@Schema
). The 200
response also does the same.
Task 8 Documenting your security requirements
The OpenAPI document doesn’t specify what’s required security wise. Let’s add the annotations to do that
- At the start of the Storefront class add the following annotations on the Storefront class
<copy>@SecurityScheme(securitySchemeName = "httpBasic", type = SecuritySchemeType.HTTP, scheme = "Basic")
@SecurityRequirement(name = "httpBasic")</copy>
The class should now have the following annotations (commends have been removed for brevity
@Path("/store")
@RequestScoped
@Counted
@Authenticated
@Timeout(value = 15, unit = ChronoUnit.SECONDS)
@Slf4j
@NoArgsConstructor
@SecurityScheme(securitySchemeName = "httpBasic", type = SecuritySchemeType.HTTP, scheme = "Basic")
@SecurityRequirement(name = "httpBasic")
public class StorefrontResource {
Explaining the annotations
`@SecurityScheme` is used to provide a re-usable definition, in this case named `http-basic` whic used the `HTTP` headers as the type of scheme and `Basic` as the actually type. There are many other types available including OAUTH, it's possible to define multiple security schemes inside a `@SecuritySchemes` annotation. This may be needed if some parts of your code use differing sc hemes, for example some may use OAUTH but others basic http. `@SecurityRequirement` specifies the scheme to be used, the name must map onto a SecurityScheme. In this case it applies to the entire class, but it could also be used for a specific method. You can document the need for multiple security schemes using the `@SecurityResuirements` annotation which lets you define multiple schemes of which any can match or `@SecurityRequirementsSets` in which case all the requirements must be satisfied. ----
Save the Storefront.java fail and rebuild using the Maven build sequence (right click on the project then Run As -> Maven Build ….) as described above in Task 7a
-
Run the code again
-
Get the updated documentation, in a terminal window type
<copy>curl -i http://localhost:8080/openapi</copy>
Note that there is a new section in the components
section
components:
securitySchemes:
httpBasic:
scheme: Basic
type: http
This describes the security options for the storefront class as a whole.
If you look at the paths section for one of the paths you will see that it now has a security section that referes tot he security scheme we defineds
/store/reserveStock:
post:
security:
- httpBasic: []
description: reserves a number of stock items in the database. The number of
stock items being reserved must be greater than the defined minimum change
More information
The Microprofile OpenAPI specification is available from the Microprofile open api guthub project page.
End of the module, what’s next ?
You have finished the optional lab Self documenting API support in Helidon.
This is the end of the Helidon labs.
If you are doing the “mega” lab then the next module is the Docker module
If you are only doing the Helidon labs then thank you for your time and attention
Acknowledgements
- Author - Tim Graves, Cloud Native Solutions Architect, Oracle EMEA Cloud Native Application Development specialists Team
- Contributor - Jan Leemans, Director Business Development, EMEA Divisional Technology
- Last Updated By - Tim Graves, May 2023