Introduction
This tutorial covers the basics about the Holon platform JAX-RS
and Swagger/OpenAPI support to create and document the API of RESTful web services.
What You’ll learn
-
How to deal with the
PropertyBox
type JSON serialization and deserialization. -
How to use the Holon Platform Swagger integration to auto-configure a Swagger/OpenAPI version 3 API documentation endpoint with
PropertyBox
type support.
See the Swagger V2 integration example to use the Swagger specification version 2. |
What You’ll need
-
About 15 minutes
-
A favorite text editor or IDE
-
JDK 1.8 or later
Reference documentation and source code
-
See Holon JAX-RS module reference manual for detailed documentation
-
The source code which refers to this tutorial is available on GitHub in the Holon Platform examples repository.
Project setup
We’ll use Maven to create and setup the example project of this tutorial. So, create a standard Maven project and use the following decalrations in your pom.xml
to obtain the required Holon Platform artifacts:
2. Use a holon.platform.version
property to declare the Holon Platform version number in the properties
section, for example:
<properties>
<holon.platform.version>5.2.6</holon.platform.version>
</properties>
See Getting started to know about the last Holon Platform release version. |
2. Import the Holon Platform BOM in the dependencyManagement
section, using the last platform version:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.holon-platform</groupId>
<artifactId>bom</artifactId>
<version>${holon.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Step #1: The property model
We’re going to create a basic RESTful web service for the CRUD management of a simple product entity, represented by the Product
property model defined as follows:
public interface Product {
public static final NumericProperty<Long> ID = NumericProperty.longType("id").message("Product ID")
.messageCode("product.id");
public static final StringProperty SKU = StringProperty.create("sku").message("SKU").messageCode("product.sku");
public static final StringProperty DESCRIPTION = StringProperty.create("description").message("Description")
.messageCode("product.description");
public static final StringProperty CATEGORY = StringProperty.create("category").message("Category")
.messageCode("product.category");
public static final NumericProperty<Double> UNIT_PRICE = NumericProperty.doubleType("price").message("Price")
.messageCode("product.price")
// not negative value validator
.withValidator(Validator.notNegative());
public static final BooleanProperty WITHDRAWN = BooleanProperty.create("withdrawn")
// set a property value converter from Integer model type to Boolean
.converter(PropertyValueConverter.numericBoolean(Integer.class));
// Product property set
public static final PropertySet<?> PRODUCT = PropertySet.of(ID, SKU, DESCRIPTION, CATEGORY, UNIT_PRICE, WITHDRAWN);
// "products" DataTarget
public static final DataTarget<?> TARGET = DataTarget.named("products");
}
See the Property model tutorial to learn the basics about the Holon Platform Property model.
|
Step #2: Data persistence
To persist and manage the product data we’ll use the Datastore
API with the JDBC Datastore implementation and H2 as relational database engine. The JDBC Datastore will be auto-configured using Spring Boot.
So we need to add the following dependencies to the project’s pom
:
<!-- Holon JDBC Datastore with HikariCP starter -->
<dependency>
<groupId>com.holon-platform.jdbc</groupId>
<artifactId>holon-starter-jdbc-datastore-hikaricp</artifactId>
</dependency>
<!-- H2 database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.196</version>
</dependency>
And to configure the JDBC DataSource
using the application.yml
Spring Boot configuration properties:
spring:
datasource:
url: "jdbc:h2:mem:test"
username: "sa"
The products
table is created using a schema.sql`script file, which is discovered and executed automatically by Spring Boot and contains the `products
table declaration:
create table products (
id bigint primary key auto_increment,
sku varchar(100) not null,
description varchar(500),
category varchar(10),
price double,
withdrawn integer default 0
)
See the Datastores tutorial to learn the basics about the Holon Platform Datastore API.
|
Step #3: The JAX-RS endpoint
Next we create a simple RESTful API for product data management, using JAX-RS.
To enable the Holon Platform JAX-RS support, we’ll use one of the available Spring Boot starters, in this example we choosed the starter for JAX-RS setup using Jersey as implementation, Jackson as JSON provider and Tomcat a embedded servlet container.
This configuration is obtained simply adding the following dependency to the project’s pom
:
<dependency>
<groupId>com.holon-platform.jaxrs</groupId>
<artifactId>holon-starter-jersey</artifactId>
</dependency>
Now you want to create a ProductEndpoint
class which represents the JAX-RS resource that will be exposed as a RESTful API, providing the following operations:
-
[GET]
/products
: lists all available products -
[GET]
/products/{id}
: get a product by id -
[POST]
/products
: create a product -
[PUT]
/products
: update a product -
[DELETE]
/products/{id}
: delete a product by id
Each API operation relies on the PropertyBox
interface to accept and provide product data, using JSON as data interchange format.
The Holon Platform JAX-RS Spring Boot integration automatically enables the JSON serialization and deserialization support for the PropertyBox
type.
It is only required to provide the PropertySet
to use to deserialize a PropertyBox
when it’s used as JAX-RS method parameter. This is achieved through the @PropertySetRef
annotation, used to declare the reference to the PropertySet
instance, expected as a static class field. In this example, the class which contains the product property set is the Product
model class.
The Spring’s dependency injection system is used to obtain the Datastore
reference in the JAX-RS endpoint, so the ProductEndpoint
class will be declared as a Spring bean, using the @Component
annotation.
The Holon Platform JAX-RS Spring Boot integration provides by default the automatic detection of the JAX-RS resources declared as Spring beans, so no additional configuration is required to register the ProductEndpoint
resource in the Jersey server.
So the ProductEndpoint
class will look like this:
@Component
@Path("/products")
public class ProductEndpoint {
@Inject
private Datastore datastore;
/*
* Get a list of products PropertyBox in JSON.
*/
@GET
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
public List<PropertyBox> getProducts() {
return datastore.query().target(TARGET).list(PRODUCT);
}
/*
* Get a product PropertyBox in JSON.
*/
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response getProduct(@PathParam("id") Long id) {
return datastore.query().target(TARGET).filter(ID.eq(id)).findOne(PRODUCT).map(p -> Response.ok(p).build())
.orElse(Response.status(Status.NOT_FOUND).build());
}
/*
* Create a product. The @PropertySetRef must be used to declare the request PropertyBox property set.
*/
@POST
@Path("/")
@Consumes(MediaType.APPLICATION_JSON)
public Response addProduct(@PropertySetRef(Product.class) PropertyBox product) {
if (product == null) {
return Response.status(Status.BAD_REQUEST).entity("Missing product").build();
}
datastore.save(TARGET, product, DefaultWriteOption.BRING_BACK_GENERATED_IDS);
return Response.created(URI.create("/api/products/" + product.getValue(ID))).build();
}
/*
* Update a product. The @PropertySetRef must be used to declare the request PropertyBox property set.
*/
@PUT
@Path("/")
@Consumes(MediaType.APPLICATION_JSON)
public Response updateProduct(@PropertySetRef(Product.class) PropertyBox product) {
if (product == null) {
return Response.status(Status.BAD_REQUEST).entity("Missing product").build();
}
if (!product.getValueIfPresent(ID).isPresent()) {
return Response.status(Status.BAD_REQUEST).entity("Missing product id").build();
}
return datastore.query().target(TARGET).filter(ID.eq(product.getValue(ID))).findOne(PRODUCT).map(p -> {
datastore.save(TARGET, product);
return Response.noContent().build();
}).orElse(Response.status(Status.NOT_FOUND).build());
}
/*
* Delete a product by id.
*/
@DELETE
@Path("/{id}")
@Consumes(MediaType.APPLICATION_JSON)
public Response deleteProduct(@PathParam("id") Long id) {
datastore.bulkDelete(TARGET).filter(ID.eq(id)).execute();
return Response.noContent().build();
}
}
Step #4: The Application class
Finally, you want to create a simple Spring Boot entry point class:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Running the Application
class, we’ll get the RESTful API up and listening to the default 8080
port, ready to accept HTTP requests.
Step #5: Using Swagger to document the API
In order to enable the Swagger/OpenAPI V3 API documentation provisioning, we’re going to use the Holon Platform Swagger Spring Boot integration, adding the following dependency to the project’s pom
:
<dependency>
<groupId>com.holon-platform.jaxrs</groupId>
<artifactId>holon-jaxrs-swagger-v3</artifactId>
</dependency>
The holon-jaxrs-swagger-v2 artifact provides Swagger specification version 2 integration. See the Swagger V2 integration example and the Swagger integration documenation for details.
|
Now, we want to "decorate" the JAX-RS ProductEndpoint
resource methods using the Swagger annotations, to provide a complete API documentation. We’ll add annotations like @Operation
(to provide the operation description) and @ApiResponses
(to document the operation response HTTP status codes) to the JAX-RS resource methods:
@Component
@Path("/products")
public class ProductEndpoint {
@Inject
private Datastore datastore;
/*
* Get a list of products PropertyBox in JSON.
*/
@Operation(summary = "Get all the products") (1)
@GET
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
public List<PropertyBox> getProducts() {
return datastore.query().target(TARGET).list(PRODUCT);
}
/*
* Get a product PropertyBox in JSON.
*/
@Operation(summary = "Get a product by id")
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response getProduct(@Parameter(name = "The product id") @PathParam("id") Long id) { (2)
return datastore.query().target(TARGET).filter(ID.eq(id)).findOne(PRODUCT).map(p -> Response.ok(p).build())
.orElse(Response.status(Status.NOT_FOUND).build());
}
/*
* Create a product. The @PropertySetRef must be used to declare the request PropertyBox property set.
*/
@Operation(summary = "Create a product")
@ApiResponses(@ApiResponse(responseCode = "201", description = "Product created")) (3)
@POST
@Path("/")
@Consumes(MediaType.APPLICATION_JSON)
public Response addProduct(@PropertySetRef(Product.class) PropertyBox product) {
if (product == null) {
return Response.status(Status.BAD_REQUEST).entity("Missing product").build();
}
datastore.save(TARGET, product, DefaultWriteOption.BRING_BACK_GENERATED_IDS);
return Response.created(URI.create("/api/products/" + product.getValue(ID))).build();
}
/*
* Update a product. The @PropertySetRef must be used to declare the request PropertyBox property set.
*/
@Operation(summary = "Update a product")
@ApiResponses({ @ApiResponse(responseCode = "204", description = "Product updated"),
@ApiResponse(responseCode = "404", description = "Product not found") })
@PUT
@Path("/{id}")
@Consumes(MediaType.APPLICATION_JSON)
public Response updateProduct(@PropertySetRef(Product.class) PropertyBox product) {
if (product == null) {
return Response.status(Status.BAD_REQUEST).entity("Missing product").build();
}
if (!product.getValueIfPresent(ID).isPresent()) {
return Response.status(Status.BAD_REQUEST).entity("Missing product id").build();
}
return datastore.query().target(TARGET).filter(ID.eq(product.getValue(ID))).findOne(PRODUCT).map(p -> {
datastore.save(TARGET, product);
return Response.noContent().build();
}).orElse(Response.status(Status.NOT_FOUND).build());
}
/*
* Delete a product by id.
*/
@Operation(summary = "Delete a product")
@ApiResponses(@ApiResponse(responseCode = "204", description = "Product deleted"))
@DELETE
@Path("/{id}")
@Consumes(MediaType.APPLICATION_JSON)
public Response deleteProduct(@Parameter(name = "The product id to delete") @PathParam("id") Long id) {
datastore.bulkDelete(TARGET).filter(ID.eq(id)).execute();
return Response.noContent().build();
}
}
1 | API operation description |
2 | API parameter description |
3 | API rensponse description |
The Swagger/OpenAPI documentation will be automatically configured using Spring Boot and by default will be mapped to the /api-docs path. So it will be available by performing a GET
request to a URL like this:
http://localhost:8080/api-docs
By default, a query parameter endpoint type is configured, so a type
query parameter can be provided to specify the API documentation format to obtain, which can be either json
or yaml
:
http://localhost:8080/api-docs?type=yaml
The Swagger documentation endpoints can be furtherly configured using the holon.swagger.*
application configuration properties, declared for example in the application.yml
file.
We can decalre the API title and version as follows:
holon:
swagger:
title: "Example API docs"
version: "v1"
By default, the API documentation endpoint is mapped to the /api-docs path. This can be changed using the holon.swagger.path
property:
holon:
swagger:
path: "docs"
title: "Example API docs"
version: "v1"
The API documentation endpoint is now mapped to the /docs path:
http://localhost:8080/docs
A complete set of configuration properties is available to configure the Swagger API definition and endpoints, see the Holon Swagger integration reference manual for details.
Step #5: Declaring a Swagger Model
Using the PropertyBox
structure with the JAX-RS API can result in a data type representation that is too much generic in the Swagger API documentation. A PropertyBox
type is always represented by a JSON
object, with each property of the PropertyBox
property set represented as a JSON
object attribute. But this way, different PropertyBox
API parameters or response types (with different property sets) cannot be distinguished at a glance.
To overcome this problem, a PropertyBox
type with a specific property set can be declared as a Swagger model, with a name and a reference in the Swagger’s documentation models section.
To declare a PropertySet
as a Swagger model, the Holon Platform @ApiPropertySetModel
annotation can be used, which an be used in conjuction with the @PropertySetRef
annotation to declare a Swagger model name (and an optional description) to be created and used in the API documentation.
Since the @ApiPropertySetModel
and the @PropertySetRef
will always be used together, we’ll create a new Java annotation to declare the product model of this example, which will be meta-annotated with the two required annotations:
@Target({ ElementType.PARAMETER, ElementType.TYPE_USE })
@Retention(RetentionPolicy.RUNTIME)
@PropertySetRef(Product.class) (1)
@ApiPropertySetModel(value = "Product", description = "Product model") (2)
public @interface ProductModel {
}
1 | The product PropertySet declaration |
2 | The Swagger model definition |
Now we can use the ProductModel
annotation to specify the PropertyBox
API parameters and response types Swagger model in the JAX-RS methods of the ProductEndpoint
resource class:
@Component
@Path("/products")
public class ProductEndpoint {
@Inject
private Datastore datastore;
/*
* Get a list of products PropertyBox in JSON.
*/
@Operation(summary = "Get all the products")
@ProductModel (1)
@GET
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
public List<PropertyBox> getProducts() {
return datastore.query().target(TARGET).list(PRODUCT);
}
/*
* Get a product PropertyBox in JSON.
*/
@Operation(summary = "Get a product by id")
@ProductModel
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response getProduct(@Parameter(name = "The product id") @PathParam("id") Long id) {
return datastore.query().target(TARGET).filter(ID.eq(id)).findOne(PRODUCT).map(p -> Response.ok(p).build())
.orElse(Response.status(Status.NOT_FOUND).build());
}
/*
* Create a product. The @PropertySetRef must be used to declare the request PropertyBox property set.
*/
@Operation(summary = "Create a product")
@ApiResponses(@ApiResponse(responseCode = "201", description = "Product created"))
@POST
@Path("/")
@Consumes(MediaType.APPLICATION_JSON)
public Response addProduct(@ProductModel PropertyBox product) { (2)
if (product == null) {
return Response.status(Status.BAD_REQUEST).entity("Missing product").build();
}
datastore.save(TARGET, product, DefaultWriteOption.BRING_BACK_GENERATED_IDS);
return Response.created(URI.create("/api/products/" + product.getValue(ID))).build();
}
/*
* Update a product. The @PropertySetRef must be used to declare the request PropertyBox property set.
*/
@Operation(summary = "Update a product")
@ApiResponses({ @ApiResponse(responseCode = "204", description = "Product updated"),
@ApiResponse(responseCode = "404", description = "Product not found") })
@PUT
@Path("/{id}")
@Consumes(MediaType.APPLICATION_JSON)
public Response updateProduct(@ProductModel PropertyBox product) {
if (product == null) {
return Response.status(Status.BAD_REQUEST).entity("Missing product").build();
}
if (!product.getValueIfPresent(ID).isPresent()) {
return Response.status(Status.BAD_REQUEST).entity("Missing product id").build();
}
return datastore.query().target(TARGET).filter(ID.eq(product.getValue(ID))).findOne(PRODUCT).map(p -> {
datastore.save(TARGET, product);
return Response.noContent().build();
}).orElse(Response.status(Status.NOT_FOUND).build());
}
/*
* Delete a product by id.
*/
@Operation(summary = "Delete a product")
@ApiResponses(@ApiResponse(responseCode = "204", description = "Product deleted"))
@DELETE
@Path("/{id}")
@Consumes(MediaType.APPLICATION_JSON)
public Response deleteProduct(@Parameter(name = "The product id to delete") @PathParam("id") Long id) {
datastore.bulkDelete(TARGET).filter(ID.eq(id)).execute();
return Response.noContent().build();
}
}
1 | Response type model definition |
2 | Parameter type model definition |
Now the Swagger API documentation will include the Product
schema definition.
Summary
You’ve learned how to use the Holon Platform JAX-RS modules to auto-configure a simple RESTful web services, using the PropertyBox
type and Datastore
API to represent and manage a data entity.
Furthermore, you’ve learned how to use the Holon Platform Swagger integration to auto-generate and provide the API documentation and to declare a Swagger model definition for a PropertySet
.
See the Holon Swagger integration documenation for more information and to learn, for example, how to declare more the one API listing endpoint using API groups.
See also
The source code of this tutorial is available on GitHub.
Related examples:
See the Holon Platform tutorials to explore other available tutorials.