How to create your own headless API using Liferay's REST Builder (Part 3)

Introduction

[Part 1] of this series (https://liferay.dev/blogs/-/blogs/creating-headless-apis-part-1) leverages Liferay's new REST Builder tools to generate headless APIs. The project has started. In the Reusable Components section, we defined the request and response object definitions: a copy of the Vitamin component and Liferay's Creator component.

In Part 2 of the series, we completed the OpenAPI Yaml file by defining Paths (endpoints) and successfully generated code despite common problems.

In this part, we'll look at how to add implementation code to the appropriate place in the generated code.

Read the generated code

The four modules for which the code was generated are headless-vitamin-api, headless-vitamin-client, headless-vitamin-impl, and headless-vitamin-test.

Let's look at each module individually.

headless-vitamins-api

The API module concept is similar to the Service Builder API module, which includes an interface for resources (services). It also contains specific POJO classes for component types (Vitamin and Creator). They're not just POJOs, the component type class has additional setters that are called by the framework when deserializing objects. Let's take a look at one of the Creator component types.

@JsonIgnore
public void setAdditionalName(
	UnsafeSupplier additionalNameUnsafeSupplier) {

	try {
		additionalName = additionalNameUnsafeSupplier.get();
	}
	catch (RuntimeException re) {
		throw re;
	}
	catch (Exception e) {
		throw new RuntimeException(e);
	}
}

The above code generated is very simple, but don't worry.

VitaminResource is the interface of the resource (service) and is taken from the path defined in the OpenAPI Yaml file. After calling REST Builder, you may notice that new attributes are added to each path in the yaml file's ʻoperationId`, and these values exactly match the methods in the interface.

The generated code alone has too few methods, so we'll share the interface here.

@Generated("")
public interface VitaminResource {

	public Page getVitaminsPage(
			String search, Filter filter, Pagination pagination, Sort[] sorts)
		throws Exception;

	public Vitamin postVitamin(Vitamin vitamin) throws Exception;

	public void deleteVitamin(String vitaminId) throws Exception;

	public Vitamin getVitamin(String vitaminId) throws Exception;

	public Vitamin patchVitamin(String vitaminId, Vitamin vitamin)
		throws Exception;

	public Vitamin putVitamin(String vitaminId, Vitamin vitamin)
		throws Exception;

	public void setContextCompany(Company contextCompany);

}

The path / vitamins that returns an array of vitamin objects is the first methodgetVitaminsPage (). Your own Yaml file doesn't declare a PageVitamin component, but one is inserted in the exported Yaml file.

Other methods in the resource interface match the other paths defined in the Yaml file. Next, I had to add some dependencies to the API module's build.gradle file:

dependencies {
	compileOnly group: "com.fasterxml.jackson.core", name: "jackson-annotations", version: "2.9.9"
	compileOnly group: "com.liferay", name: "com.liferay.petra.function"
	compileOnly group: "com.liferay", name: "com.liferay.petra.string"
	compileOnly group: "com.liferay", name: "com.liferay.portal.vulcan.api"
	compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel"
	compileOnly group: "io.swagger.core.v3", name: "swagger-annotations", version: "2.0.5"
	compileOnly group: "javax.servlet", name: "javax.servlet-api"
	compileOnly group: "javax.validation", name: "validation-api", version: "2.0.1.Final"
	compileOnly group: "javax.ws.rs", name: "javax.ws.rs-api"
	compileOnly group: "org.osgi", name: "org.osgi.annotation.versioning"
}

We also made minor changes to the bnd.bnd file to expose the components and resource interfaces:

Export-Package: com.dnebinger.headless.vitamins.dto.v1_0, \
    com.dnebinger.headless.vitamins.resource.v1_0

headless-vitamins-client

The code in this module builds a Java-based client for calling headless APIs.

The client entry point is in the <package prefix> .client.resource.v1_0 <Component> Resource class. In my case, this is the com.dnebinger.headless.vitamins.client.resource.v1_0.VitaminResource class.

Each path has a static method, each method takes the same arguments and returns the same object. Behind the scenes, each method uses a HttpInvoker instance to call the web service at localhost: 8080 with [email protected] and test login information. If you want to test the remote service or use different login information, you need to edit the <Component> Resource class accordingly to use different values.

It's up to the designer to write the main class and other code to call the client code, but having a complete client library for testing is a great first step!

The headless-vitamins-client module has no external dependencies, but you need to export the package of the bnd.bnd file.

Export-Package: com.dnebinger.headless.vitamins.client.dto.v1_0, \
    com.dnebinger.headless.vitamins.client.resource.v1_0

headless-vitamins-test

Let's skip the headless-vitamins-impl module and briefly describe headless-vitamins-test.

The code generated here provides all the integration tests for the service module and leverages the client module to call remote APIs. In this module, we have two classes, Base <Component> ResourceTestCase and <Component> ResourceTestCase, so we have BaseVitaminResourceTestCase and VitaminResourceTest.

The VitaminResourceTest class is where you add tests that the Base class has not yet implemented. It is a large-scale test to utilize other modules, and is used for error verification when trying to add a duplicate primary key or delete a non-existent object. Basically, this is a test that cannot be covered individually with a simple call to a plain resource method.

The build.gradle file for this module required a lot of additions:


dependencies {
	testIntegrationCompile group: "com.fasterxml.jackson.core", name: "jackson-annotations", version: "2.9.9"
	testIntegrationCompile group: "com.fasterxml.jackson.core", name: "jackson-core", version: "2.9.9"
	testIntegrationCompile group: "com.fasterxml.jackson.core", name: "jackson-databind", version: "2.9.9.1"
	testIntegrationCompile group: "com.liferay", name: "com.liferay.arquillian.extension.junit.bridge", version: "1.0.19"
	testIntegrationCompile group: "com.liferay.portal", name: "com.liferay.portal.kernel"
	testIntegrationCompile project(":modules:headless-vitamins:headless-vitamins-api")
	testIntegrationCompile project(":modules:headless-vitamins:headless-vitamins-client")
	testIntegrationCompile group: "com.liferay", name: "com.liferay.portal.odata.api"
	testIntegrationCompile group: "com.liferay", name: "com.liferay.portal.vulcan.api"
	testIntegrationCompile group: "com.liferay", name: "com.liferay.petra.function"
	testIntegrationCompile group: "com.liferay", name: "com.liferay.petra.string"
	testIntegrationCompile group: "javax.validation", name: "validation-api", version: "2.0.1.Final"
	testIntegrationCompile group: "commons-beanutils", name: "commons-beanutils"
	testIntegrationCompile group: "commons-lang", name: "commons-lang"
	testIntegrationCompile group: "javax.ws.rs", name: "javax.ws.rs-api"
	testIntegrationCompile group: "junit", name: "junit"
	testIntegrationCompile group: "com.liferay.portal", name: "com.liferay.portal.test"
	testIntegrationCompile group: "com.liferay.portal", name: "com.liferay.portal.test.integration"
}

Some of these dependencies are the defaults needed only for classes (junit and liferay test modules), others depend on the project (client and api modules, and possibly other modules). Some trial and error may be required to get a list that meets your requirements.

The bnd.bnd file for this module does not export classes or packages and did not need to be modified.

headless-vitamins-impl

It's finally getting interesting. This is the module that contains the implementation code. REST Builder has generated a lot of starter code. Let's see what it looks like.

com.dnebinger.headless.vitamins.internal.graphql, GraphQL is here! Headless implementations include GraphQL endpoints that expose queries and mutations based on defined paths. Note that GraphQL handles queries and mutation changes by calling <Component> Resource directly, rather than simply proxiing calls to REST implementations commonly found in this type of mix. .. Therefore, you can get GraphQL automatically just by using REST Builder.

com.dnebinger.headless.vitamins.internal.jaxrs.application, where the JAX-RS Application class is stored. It doesn't contain anything particularly interesting, but it does register the application with Liferay's OSGi container.

com.dnebinger.headless.vitamins.internal.resource.v1_0, this is where you make your code fixes.

ʻThe OpenAPIResourceImpl.javaclass is the path to return an OpenAPI yaml file to load into Swagger Hub, for example. For each Resource interface, get the abstract base class Base ResourceImpland the concrete class ResourceImplto do the work. Therefore, there are two classes,BaseVitaminResourceImpl and VitaminResourceImpl`.

If you look at the methods in the base class, you'll see that they are heavily decorated with Swagger and JAX-RS annotations. Let's take a look at one of the getVitaminsPage () methods used to return an array of Vitamin components stored in / vitamins:

@Override
@GET
@Operation(
  description = "Retrieves the list of vitamins and minerals. Results can be paginated, filtered, searched, and sorted."
)
@Parameters(
  value = {
    @Parameter(in = ParameterIn.QUERY, name = "search"),
    @Parameter(in = ParameterIn.QUERY, name = "filter"),
    @Parameter(in = ParameterIn.QUERY, name = "page"),
    @Parameter(in = ParameterIn.QUERY, name = "pageSize"),
    @Parameter(in = ParameterIn.QUERY, name = "sort")
  }
)
@Path("/vitamins")
@Produces({"application/json", "application/xml"})
@Tags(value = {@Tag(name = "Vitamin")})
public Page<Vitamin> getVitaminsPage(
    @Parameter(hidden = true) @QueryParam("search") String search,
    @Context Filter filter, @Context Pagination pagination,
    @Context Sort[] sorts)
  throws Exception {

  return Page.of(Collections.emptyList());
}

How is it?

This is one of the benefits REST Builder brings to us. All annotations are defined in the base class, so you don't have to worry about them.

Let's take a look at the return statement passing Page.of (Collections.emptyList ()). This is the stub method provided by the base class. It doesn't provide a valuable implementation, but it ensures that a value is returned if it is not implemented.

When you're ready to implement this method, add the following method to the VitaminResourceImpl class (currently empty):

@Override
public Page<Vitamin> getVitaminsPage(String search, Filter filter, Pagination pagination, Sort[] sorts) throws Exception {
  List<Vitamin> vitamins = new ArrayList<Vitamin>();
  long totalVitaminsCount = ...;

  // write code here, should add to the list of Vitamin objects

  return Page.of(vitamins, Pagination.of(0, pagination.getPageSize()), totalVitaminsCount);
}

Notice that there are no annotations.

As mentioned earlier, all annotations are included in the overriding method, so all configurations are ready! Therefore, unlike the code generated by the Service Builder, the comment "This file is generated, but please do not modify this file" does not appear anywhere. If you run REST Builder again, you will see the @Generated ("") annotation in all (re) generated classes.

The Base <Component> ResourceImpl class is annotated like this: This is the file that is regenerated every time you run REST Builder. Therefore, do not modify the annotations or method implementations in this file. Make all changes to the <Component> ResourceImpl class.

If you need to change the annotations (** not recommended **), you can do this with the <Component> ResourceImpl class and you need to override the annotations from the base class. Therefore, you need to add some dependencies to the build.gradle file. My file looks like this:

buildscript {
	dependencies {
		classpath group: "com.liferay", name: "com.liferay.gradle.plugins.rest.builder", version: "1.0.21"
	}

	repositories {
		maven {
			url "https://repository-cdn.liferay.com/nexus/content/groups/public"
		}
	}
}

apply plugin: "com.liferay.portal.tools.rest.builder"

dependencies {
	compileOnly group: "com.fasterxml.jackson.core", name: "jackson-annotations", version: "2.9.9"
	compileOnly group: "com.liferay", name: "com.liferay.adaptive.media.api"
	compileOnly group: "com.liferay", name: "com.liferay.adaptive.media.image.api"
	compileOnly group: "com.liferay", name: "com.liferay.headless.common.spi"
	compileOnly group: "com.liferay", name: "com.liferay.headless.delivery.api"
	compileOnly group: "com.liferay", name: "com.liferay.osgi.service.tracker.collections"
	compileOnly group: "com.liferay", name: "com.liferay.petra.function"
	compileOnly group: "com.liferay", name: "com.liferay.petra.string"
	compileOnly group: "com.liferay", name: "com.liferay.portal.odata.api"
	compileOnly group: "com.liferay", name: "com.liferay.portal.vulcan.api"
	compileOnly group: "com.liferay", name: "com.liferay.segments.api"
	compileOnly group: "com.liferay.portal", name: "com.liferay.portal.impl"
	compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel"
	compileOnly group: "io.swagger.core.v3", name: "swagger-annotations", version: "2.0.5"
	compileOnly group: "javax.portlet", name: "portlet-api"
	compileOnly group: "javax.servlet", name: "javax.servlet-api"
	compileOnly group: "javax.validation", name: "validation-api", version: "2.0.1.Final"
	compileOnly group: "javax.ws.rs", name: "javax.ws.rs-api"
	compileOnly group: "org.osgi", name: "org.osgi.service.component", version: "1.3.0"
	compileOnly group: "org.osgi", name: "org.osgi.service.component.annotations"
	compileOnly group: "org.osgi", name: "org.osgi.core"
	compileOnly project(":modules:headless-vitamins:headless-vitamins-api")
}

You don't need to add anything to the bnd.bnd file because all the packages are internal.

Summary

You've reached the point where you can start building your implementation! This time, I'll cut it off.

In Part 1, we created a project, defined reusable components and touched on OpenAPI Yaml. It was. In Part 2, add all the path definitions for the OpenAPI service and use REST Builder to code Generated. In Part 3 (this article), we looked at all the generated code and realized that we didn't have to worry about code changes or implementation code annotations.

Finally, in the final next part, we've added the Service Builder module to the project for data storage and everything Implement the resource method of and use the ServiceBuilder code.

See you again!

https://github.com/dnebing/vitamins

Recommended Posts

How to create your own headless API using Liferay's REST Builder (Part 3)
How to create your own headless API using Liferay's REST Builder (Part 2)
How to create your own headless API using Liferay's REST Builder (Part 4)
How to create your own headless API using Liferay's REST Builder (Part 1)
How to create your own Controller corresponding to / error with Spring Boot
How to create your own annotation in Java and get the value
Let's create a REST API using WildFly Swarm.
[Rails] How to create a graph using lazy_high_charts
How to implement optimistic locking in REST API
How to create hierarchical category data using ancestry
[Java] How to operate List using Stream API
How to create docker-compose
[Forge] How to register your own Entity and Entity Render in 1.13.2
How to create a placeholder part to use in the IN clause
How to deploy jQuery in your Rails app using Webpacker
How to create a service builder portlet in Liferay 7 / DXP
How to play MIDI files using the Java Sound API
How to create an application
How to use Chain API
How to use @Builder (Lombok)
Create your own Java annotations
Introduction to EHRbase 2-REST API
How to create a method
How to authorize using graphql-ruby
How to create a jar file or war file using the jar command
[Rails 6] How to create a dynamic form input screen using cocoon
Easy way to create a mapping class when using the API