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

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