Blogs

Blogs

Creating Headless APIs (Part 3)

Use Liferay's REST Builder tool to generate your own Headless APIs.

Introduction

In part 1 of this series, we started a project to leverage Liferay's new REST Builder tool for generating Headless APIs. We defined the Reusable Components section, the section where we define our request and response objects, namely the Vitamin component and a copy of Liferay's Creator component.

In part 2 of the series, we finished the OpenAPI Yaml file by defining our paths (the endpoints), then moved on to code generation where we encountered and solved some common problems. We wrapped with successfully generating code.

In this part, we're going to take a look at the generated code and how we will add in our implementation code where we need it. Let's get cracking!

Looking at the Generated Code

So we have four modules where code has been generated: headless-vitamins-api, headless-vitamins-client, headless-vitamins-impl, and headless-vitamins-test.

Although REST Builder generates code, it does not modify the build.gradle files nor the bnd.bnd files. It will be up to you to add dependencies and export packages. In the sections below I'll share the settings I used, but you'll need to come up with the set necessary for your implementation.

Let's look at each module individually...

headless-vitamins-api

The API module is similar in concept to a Service Builder API module, it contains our interface for our resource (our service), and it also has concrete POJO classes for our component types, Vitamin and Creator.

Well, they're more than just pure POJOs... Our component type classes have additional setters that will be invoked by the framework when deserializing our object. Let's take a look at one from the Creator component type:

@JsonIgnore
public void setAdditionalName(
	UnsafeSupplier additionalNameUnsafeSupplier) {

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

Pretty tame. Since it's generated code, you don't really need to worry about these guys, but I wanted to highlight one so you wouldn't be surprised.

Our VitaminResource is the interface for our resource (aka service). It also is generated, and it comes from the paths defined in our OpenAPI Yaml file. You might notice, after invoking the REST Builder, our yaml file has new attributes added on each path for the operationId, those values match exactly the methods in our interface.

Since we have so few methods, I'll just 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);

}

Our /vitamins path, the one that returns the array of Vitamin objects? That's for the first method, the getVitaminsPage() method. We won't have a PageVitamin component declared in our own Yaml file, but in the exported Yaml file there will be one injected in there for us.

Our other methods in the resource interface match up with the other paths that are defined in the Yaml file.

I needed to add some dependencies to my build.gradle file for the API module:

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"
}

In order to expose my components and resource interface, I also made a small change to the bnd.bnd file:

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 invoking the Headless API.

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

There's a static method for each of our paths, and each method takes the same args and returns the same objects.

Behind the scenes each method will use an HttpInvoker instance to call the web service on localhost:8080 using the test@liferay.com and test credentials. If you are wanting to test a remote service or use different credentials, you'll need to hand-edit the <Component>Resource class to use the different values.

It's up to you to build a main class or other code to invoke the client code, but having a full client library for testing is a great start!

The generated headless-vitamins-test module relies on the headless-vitamins-client module for testing the service layer.

The headless-vitamins-client module does not have any external dependencies, but you do need to export the packages in 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

We're going to skip the headless-vitamins-impl module and briefly cover the headless-vitamins-test.

The generated code here provides all of the integration tests for your service modules. It leverages the client module for invoking the remote APIs.

In this module we get two classes, a Base<Component>ResourceTestCase and a <Component>ResourceTest, so I have BaseVitaminResourceTestCase and VitaminResourceTest.

The VitaminResourceTest class is where I would go to add any additional tests that I wanted to include that the Base class doesn't already implement for me. They'd be larger-scale tests to maybe take advantage of other modules, error validation when trying to add duplicate primary keys or delete an object that doesn't exist.

Basically any testing that the simple invocation of the raw resource methods individually cannot cover.

My build.gradle file for this module took 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 will be defaults necessary just for the classes (i.e. junit and the liferay test modules), others will depend upon your projects (i.e. the client and api modules, perhaps other modules if you need them). You may have to go a few rounds getting the list that works for you.

My bnd.bnd file in this module did not require modification since I'm not going to export any of the classes or packages.

headless-vitamins-impl

Finally we get to the fun one. This is the module where your implementation code. The REST Builder has done a decent job of generating a lot of the starter code for us; let's take a look at what we get.

com.dnebinger.headless.vitamins.internal.graphql - Yeah, that's right, GraphQL Baby! Your headless implementation includes a GraphQL endpoint exposing your queries and mutations based upon your defined paths. Note that the GraphQL is not merely proxying a call to the REST implementation that you often see with this kind of mix; no, in this implementation GraphQL is invoking your <Component>Resource directly to handle the query and mutation changes. So just by using REST Builder, you will automatically get GraphQL too!

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

com.dnebinger.headless.vitamins.internal.resource.v1_0 - This is the package where we'll be modifying code...

You'll get an OpenAPIResourceImpl.java class, this is the path that takes care of returning the OpenAPI yaml file that you would load, for instance, into Swagger Hub.

For each <Component>Resource interface you have, you'll get an abstract Base<Component>ResourceImpl base class and a concrete <Component>ResourceImpl class for you to do your work in.

So I have a BaseVitaminResourceImpl class and a VitaminResourceImpl.

If you check out a method in the base class, you'll see it is decorated like crazy with annotations for Swagger and JAX-RS. Let's check the one for the getVitaminsPage() method, the one that is on /vitamins and is used to return the array of Vitamin components:

@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());
}

Like, ick, right?

Well, that's one of the advantages of what REST Builder is going to do for us. Since all of the annotations are defined in the base class, we just don't need to worry about them...

See that return statement, the one that is passing Page.of(Collections.emptyList())? So this is the stub method the base class provides; it doesn't provide a worthwhile implementation, but it does ensure that a value is returned in case we don't implement it.

So when we are ready to implement this method, we'll go into the VitaminResourceImpl class (currently empty) and add the following method:

@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);
}

No Annotations! Like I said, the annotations are all in the method we're overriding so we get all of the configuration ready for us!

So unlike Service Builder generated code, you're not going to see a bunch of "This file is generated, do not modify this file" comments everywhere. You will see the @Generated("") annotation on all classes which will be [re-]generated when you run REST Builder again.

Our Base<Component>ResourceImpl class is annotated like this. It is a generated file that will be re-written every time you run REST Builder. So don't mess around with the annotations or methods or method implementations in this file, keep all of your modifications in the <Component>ResourceImpl class.

If you do need to tamper with the annotations (I wouldn't recommend it), you should be able to do this in the <Component>ResourceImpl class and they should override the annotations from the base class.

So our build.gradle file needs some dependencies added. My full file is:

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")
}

All of the packages are internal, so I don't need anything in my bnd.bnd file.

Conclusion

Why are we stopping? We're just getting to the point where we can start building out the implementations!

Well, it's a good point to stop...

In part 1 we created the project and started our OpenAPI Yaml by defining our Reusable components.

In part 2 we added all of the path definitions for our OpenAPI service and used REST Builder to generate the code.

Here in part 3 we reviewed all of the code that was generated for us, including touching on where we make code modifications and how we won't have to worry about the annotations in our implementation code.

In the final part for this series, we're going to add in a Service Builder module to the project for data storage, then we're going to implement all of our resource methods to take advantage of the ServiceBuilder code.

See you there!

https://github.com/dnebing/vitamins

Blogs