[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.
The four modules for which the code was generated are headless-vitamin-api
, headless-vitamin-client
, headless-vitamin-impl
, and headless-vitamin-test
.
build.gradle
or bnd.bnd
. It's up to you to add dependencies and export the package. The following sections will share the settings I used, but the set required for implementation will need to be adjusted each time. *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!
headless-vitamins-test
module depends on the headless-vitamins-client
module when testing the service tier. *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
interface, get the abstract base class
Base and the concrete class
to 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.
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