[JAVA] How to create your own headless API using Liferay's REST Builder (Part 4)

Introduction

This blog series, which aims to generate your own headless API using Liferay's REST Builder tools, is finally over!

In Part 1, create new projects and modules, headless through a description of reusable components. We have started creating an OpenAPI Yaml file that defines the service. In Part 2, you'll find common issues with adding paths and REST Builder-generated code and addressing them. After going through the workarounds of, I completed the OpenAPI Yaml file. In Part 3, review all the generated code, understand the build, and where to find the implementation code. I learned how to add to.

In this chapter, which will be the last in the series, we will create the ServiceBuilder (SB) layer required for value persistence and pay close attention to the parts that need to be implemented in support of the headless API.

Creating a Service Builder layer

Use Service Builder for the persistence layer. I won't go into all the details of this process, but I'll focus on what I'll add to improve the convenience of the headless API.

The most complex aspect of the service part is the / vitamins path, which gets all the" vitamins ", which is the easiest to feel at first glance.

Why is it so difficult? This is because we need to consider the following points according to the Liferay model:

-** Search support . This is done through the index, so you need to index the SB entity. - Permission support . The new search implementation supports permissions by default and should be supported. -- Sorting support ** for results determined by the caller. -** Filtering search results using special characters * * -** Search result pagination support . The number of pages is determined by the caller. - Remote Services **. Call the permission checker at the right time.

To achieve all of this, you need to make sure that the entity is indexed. Please see here for the confirmation method.

The new index supports permissions by default, so you need to add permissions to the entity. Reference article: https://portal.liferay.dev/docs/7-2/frameworks/-/knowledge_base/f/defining-application-permissions

I named the component "Vitamin" so I decided not to use Vitamin in the Service Builder. Otherwise, you'll have to include the package everywhere. I decided to call the entity PersistedVitamin instead. This allows you to distinguish between the DTO class used by Headless and the actual persisted entity managed by the Service Builder.

List filter, search and sort support

The rest of this section describes adding support for list filtering, searching, and sorting using Liferay's supported mechanisms. This section may not apply if you do not support list filtering, searching, or sorting, or if you only need support for one or the other and do not use Liferay's approach.

Many of Liferay's list methods, such as / v1.0 / message-board-threads / {messageBoardThreadId} / message-board-messages, have many of Liferay's list methods to support search, filter, sort, paging, and field restrictions. There are additional attributes that can be provided in the query.

All Liferay documentation on these details:

Some points not mentioned in the above document are filters, sorts, and searches that require you to use the entity's search index.

For example, a search is performed by adding one or more keywords to the query. These are entered in the index query to search for entity matches.

Filtering is also managed by adjusting index search queries. To apply a filter to one or more fields in a component, those fields must be in the search index. In addition, the fields described in the other sections below require ʻOData EntityModel`.

Sorting is also managed by adjusting the index search query. To sort by one or more fields in a component, those fields must be in the search index. In addition, you need to index using the ʻaddKeywordSortable ()method of thecom.liferay.portal.kernel.search.Document interface, and add sortable fields to the ʻOData EntityModel implementation mentioned later. need to do it.

With the above in mind, special attention should be paid to the search definition of custom entities:

--Use ModelDocumentContributor to add important texts and keywords to get the right search hits. --Use ModelDocumentContributor to add a field that supports filtering. --Use ModelDocumentContributor to add a sortable keyword field.

Implementation of VitaminResourceImpl method

After creating the Service Builder layer and fixing the headless-vitamins-impl dependency, the next step is to actually start implementing the method.

Implementation of deleteVitamin ()

Let's start with a simple deleteVitamin () method. VitaminResourceImpl extends the method from the base class (with all annotations) and calls the service layer:

@Override
public void deleteVitamin(@NotNull String vitaminId) throws Exception {
  // super easy case, just pass through to the service layer.
  _persistedVitaminService.deletePersistedVitamin(vitaminId);
}

We recommend that you use only remote services, not local services, to handle entity persistence. why? Seeing if a user has permission to delete a "vitamin" record is just your last line of defense.

You can use the OAuth2 scope to perform controls and block activity, but it's difficult for an administrator to configure the OAuth2 scope correctly, and even if I'm an administrator myself, I can get the scope correctly every time. I don't think.

By using a remote service with permission checks, you don't have to worry about scope integrity. Even if the administrator (I) disables the OAuth2 scope, the remote service will block the operation unless the user has the proper privileges.

Conversion process

Before we dive into some of the implementation methods in more detail, we need to discuss the conversion from the backend ServiceBuilder entity to the headless component returned.

At this time, Liferay has not established a standard for handling entity-to-component conversions. The Liferay source headless-delivery-impl module does the conversion in one direction, while the headless-admin-user-impl module handles the conversion differently.

For convenience, here is a technique based on the headless-admin-user-impl technique. There may be different, more effective methods, or you may prefer the headless-delivery-impl method. Liferay may also come up with a standard way to support conversions in the next release.

Although it says that it needs to be converted, it is not tied to any particular method. Liferay may come up with something better, but it's up to you to adapt to the new method or take your own.

Therefore, you must be able to convert from Persisted Vitamin to a Vitamin component to return it as part of a headless API definition. Create the method _toVitamin () in the class VitaminResourceImpl:

protected Vitamin _toVitamin(PersistedVitamin pv) throws Exception {
  return new Vitamin() {{
    creator = CreatorUtil.toCreator(_portal, _userLocalService.getUser(pv.getUserId()));
    articleId = pv.getArticleId();
    group = pv.getGroupName();
    description = pv.getDescription();
    id = pv.getSurrogateId();
    name = pv.getName();
    type = _toVitaminType(pv.getType());
    attributes = ListUtil.toArray(pv.getAttributes(), VALUE_ACCESSOR);
    chemicalNames = ListUtil.toArray(pv.getChemicalNames(), VALUE_ACCESSOR);
    properties = ListUtil.toArray(pv.getProperties(), VALUE_ACCESSOR);
    risks = ListUtil.toArray(pv.getRisks(), VALUE_ACCESSOR);
    symptoms = ListUtil.toArray(pv.getSymptoms(), VALUE_ACCESSOR);
  }};
}

First, I have to apologize for using the double brace instantiation ... I also recognize it as an anti-pattern. But my goal was to follow the "Liferay method" laid out in the headless-admin-user-impl module, which was the pattern Liferay used. Liferay doesn't use the Builder pattern very often, so I think double-brace instantiation is being used instead.

In my own taste, I also follow the Builder and Fluent patterns to simplify the creation of objects. After all, Intellij makes it easy for me to create a Builder class for me.

This method is implemented by the external CreatorUtil class (copied from Liferay code), the _toVitaminType () method that converts the internal integer code to a component enum, and the ListUtil's toArray () method. Use VALUE_ACCESSOR to process some internal objects into a String array.

In short, this method can handle the transformations that need to be performed in the actual method implementation.

Implementation of getVitamin ()

Let's look at another simple getVitamin () method. This method returns a single entity with vitaminId.

@Override
public Vitamin getVitamin(@NotNull String vitaminId) throws Exception {
  // fetch the entity class...
  PersistedVitamin pv = _persistedVitaminService.getPersistedVitamin(vitaminId);

  return _toVitamin(pv);
}

Here, we get the PersistedVitamin instance from the service layer, but pass the obtained object to the_toVitamin ()method to convert it.

Implementations of postVitamin (), patchVitamin (), and putVitamin ()

I think you're already tired of seeing the patterns, so let's take a look at them all together.

postVitamin () is a POST method for / vitamins and represents the creation of a new entity.

patchVitamin () is a PATCH method of / vitamins / {vitaminId} that represents patching an existing entity (leaving other existing properties, only the value specified for the input object). To change).

putVitamin () is the PUT method of / vitamins / {vitaminId}, which represents the replacement of an existing entity, replacing all persistent values with the passed values, even if the field is null or empty.

Since we created the ServiceBuilder layer and customized it for these entry points, the implementation in the VitaminResourceImpl class looks very lightweight.

@Override
public Vitamin postVitamin(Vitamin v) throws Exception {
  PersistedVitamin pv = _persistedVitaminService.addPersistedVitamin(
      v.getId(), v.getName(), v.getGroup(), v.getDescription(), _toTypeCode(v.getType()), v.getArticleId(), v.getChemicalNames(),
      v.getProperties(), v.getAttributes(), v.getSymptoms(), v.getRisks(), _getServiceContext());

  return _toVitamin(pv);
}

@Override
public Vitamin patchVitamin(@NotNull String vitaminId, Vitamin v) throws Exception {
  PersistedVitamin pv = _persistedVitaminService.patchPersistedVitamin(vitaminId,
      v.getId(), v.getName(), v.getGroup(), v.getDescription(), _toTypeCode(v.getType()), v.getArticleId(), v.getChemicalNames(),
      v.getProperties(), v.getAttributes(), v.getSymptoms(), v.getRisks(), _getServiceContext());

  return _toVitamin(pv);
}

@Override
public Vitamin putVitamin(@NotNull String vitaminId, Vitamin v) throws Exception {
  PersistedVitamin pv = _persistedVitaminService.updatePersistedVitamin(vitaminId,
      v.getId(), v.getName(), v.getGroup(), v.getDescription(), _toTypeCode(v.getType()), v.getArticleId(), v.getChemicalNames(),
      v.getProperties(), v.getAttributes(), v.getSymptoms(), v.getRisks(), _getServiceContext());

  return _toVitamin(pv);
}

As you can see, it is very lightweight.

You need a ServiceContext to go to the service layer. Liferay provides com.liferay.headless.common.spi.service.context.ServiceContextUtil. It only has the methods needed to create a ServiceContext. This is the context starter, just add additional information such as company ID and current user ID. So I wrapped all this in the _getServiceContext () method. Future versions of REST Builder will get new context variables to make it easier to get a valid ServiceContext.

All my ServiceBuilder methods pass and use expanded parameters that everyone knows about ServiceBuilder. The PersistedValue instance returned from the method call is passed to_toVitamin ()for conversion and returned.

The above is a simple solution. We also need to explain the getVitaminsPage () method, but before that we need to explain ʻEntityModels`.

EntityModels

Earlier we talked about how Liferay uses search indexes to support list filtering, searching, and sorting. We also explained that the fields that can be used for filtering and sorting must be part of the component's ʻEntityModel definition. Fields of components that are not part of ʻEntityModel cannot be filtered or sorted.

As an additional side effect, ʻEntityModel` exposes these fields from the search index for filtering and sorting, so you don't need to connect these fields to component fields.

For example, in the ʻEntityModel definition, you can add an entry for creatorIdthat filters the user ID of the search index. The component definition may contain aCreator field instead of a creatorIdfield, but since thecreatorId is part of the ʻEntityModel, it can be used for both filtering and sorting.

Therefore, you need to build a ʻEntityModel that defines the fields that support both filtering and sorting. Use your existing Liferay utility to put together the ʻEntityModel class:

public class VitaminEntityModel implements EntityModel {
  public VitaminEntityModel() {
    _entityFieldsMap = Stream.of(
        // chemicalNames is a string array of the chemical names of the vitamins/minerals
        new CollectionEntityField(
            new StringEntityField(
                "chemicalNames", locale -> Field.getSortableFieldName("chemicalNames"))),
        
        // we'll support filtering based upon user creator id.
        new IntegerEntityField("creatorId", locale -> Field.USER_ID),
        
        // sorting/filtering on name is okay too
        new StringEntityField(
            "name", locale -> Field.getSortableFieldName(Field.NAME)),
        
        // as is sorting/filtering on the vitamin group
        new StringEntityField(
            "group", locale -> Field.getSortableFieldName("vitaminGroup")),
        
        // and the type (vitamin, mineral, other).
        new StringEntityField(
            "type", locale -> Field.getSortableFieldName("vType"))
    ).collect(
        Collectors.toMap(EntityField::getName, Function.identity())
    );
  }

  @Override
  public Map<String, EntityField> getEntityFieldsMap() {
    return _entityFieldsMap;
  }

  private final Map<String, EntityField> _entityFieldsMap;
}

The field name comes from the name used in the service layer's PersistedVitaminModelDocumentContributor class to add the field value.

Included definitions for chemicalNames, Field.USER_ID, Field.NAME, vitaminGroup, and vType Fields from the search index. Of the definitions, the creatorId field used by the filter does not exist as a field in the vitamin component definition.

Other fields that are part of the Vitamin component feel like I don't need to allow the rest of the sorting or filtering. This type of decision is usually determined by requirements.

Liferay stores these classes in an internal package, the ʻodata.entity.v1_0package, so I putcom.dnebinger.headless.delivery.internal.odata.entity.v1_0` as the file to put in my case. I'm waiting.

Now that the class is ready, we also need to decorate the VitaminResourceImpl class to make sure we can serve ʻEntityModel` correctly.

The required changes are:

--The <Component> ResourceImpl class is an implementation of the com.liferay.portal.vulcan.resource.EntityModelResource interface. --Implementation of the getEntityModel () method that returns a ʻEntityModel` instance in the class.

My VitaminEntityModel is very simple and not very dynamic, so the implementation looks like this:

public class VitaminResourceImpl extends BaseVitaminResourceImpl 
    implements EntityModelResource {

  private VitaminEntityModel _vitaminEntityModel = new VitaminEntityModel();

  @Override
  public EntityModel getEntityModel(MultivaluedMap multivaluedMap) throws Exception {
    return _vitaminEntityModel;
  }

Please note that this is not a common implementation. Liferay's component resource implementation class has a much more complex and dynamic ʻEntityModel generation, which is due to the complexity of the associated entities (for example, StructuredContentisJournalArticle, DDM structure. , A jumble of templates`).

So don't just copy and execute the method. It may work in your case, but not in other cases. For more complex scenarios, check out the Liferay implementation of the ʻEntityModel class and the getEntityModel () `method of the component resource implementation.

Implementation of getVitaminsPage ()

This is probably the most complicated implementation. It's not difficult in itself, it depends on many other things.

The Liferay list processing feature here comes from the search index, not the database. Therefore, the entity must be indexed.

It's also a method that supports filter, search, and sort parameters, and you need to index the entity. And as we saw earlier, filters and sorts also depend on the ʻEntityModel` class.

Finally, because we're calling the Liferay method, the implementation itself is pretty opaque and out of control. The end result is:

public Page<Vitamin> getVitaminsPage(String search, Filter filter, Pagination pagination, Sort[] sorts) throws Exception {
  return SearchUtil.search(
    booleanQuery -> {
      // does nothing, we just need the UnsafeConsumer<BooleanQuery, Exception> method
    },
    filter, PersistedVitamin.class, search, pagination,
    queryConfig -> queryConfig.setSelectedFieldNames(
      Field.ENTRY_CLASS_PK),
    searchContext -> searchContext.setCompanyId(contextCompany.getCompanyId()),
    document -> _toVitamin(
      _persistedVitaminService.getPersistedVitamin(
        GetterUtil.getLong(document.get(Field.ENTRY_CLASS_PK)))),
    sorts);
}

We are using the SearchUtil.search () method, which knows all the ways to do it.

The first argument is the ʻUnsafeConsumer class, which is basically responsible for fine-tuning the booleanQueryas needed by the entity. I didn't need it here, but Liferay's headless delivery module has an example. The version ofStructuredContentthat searches for articles by site ID adds the site ID as a query argument. Theflatten` parameter fine-tunes the query to find certain filters, those of these types.

The filter, search, and pagination arguments taken from the headless layer are passed as-is. The results are applied to a Boolean query, the results are filtered and searched, and pagination gives the page-equivalent results.

queryConfig returns only the primary key value and does not request any other field data. You need the ServiceBuilder entity because you are not converting from the search index Document.

The penultimate argument is another ʻUnsafeFunctionthat applies the document-to-component type conversion. This implementation fetches aPersistedVitamin instance using the primary key value extracted from the Document, and the PersistedVitaminis passed to_toVitamin ()` to handle the final conversion.

Remaining work

You've done all the coding, but you're not done.

Rerun the buildREST command. Now that we've added the methods to the VitaminResourceImpl method, I'd like to have some test cases that can be applied to them.

Then you need to build and deploy the module to clean up any open references, deployment issues, and so on. Deploy the vitamins-api and vitamins-service to the ServiceBuilder layer and the vitamins-headless-api and vitamins-headless-impl modules to the Headless layer.

When they are ready, you need to drop them into the headless-vitamins-test module and run all the test cases (if you're missing, you can recreate them too).

When you're all set, you might want to publish the Headless API to Swagger Hub for others to use.

The Yaml file created for REST Builder is not used. Instead, in your browser [http: // localhost: 8080 / o / headless-vitamins / v1.0 / openapi.yaml](http: // localhost: 8080 / o / headless-vitamins / v1.0 / openapi.yaml] ) Is specified and the file is used for transmission. All required parts are placed and additional components such as the PageVitamin type are added.

Summary

Create a new headless validation workspace and module in Part 1 and use OpenAPI Yaml for REST Builder to finally generate code I started the file.

In Part 2 we added the path definition and completed the REST Builder OpenAPI Yaml file. While facing REST Builder build errors, I understood some of the common format errors that could cause build errors, fixed them, and used REST Builder to successfully generate code.

In Part 3, I reviewed all the generated code in all modules and showed where the changes would be made.

In Part 4 (this chapter), you'll create a Service Builder layer to support resource permissions (for checking permissions on remote services) and entity indexing (Liferay's headless infrastructure list filter / search / sort capabilities). To do) included. Next, I explained how to handle the entity-to-component conversion by flushing the VitaminResourceImpl method, and the ʻEntityModel` class needed to facilitate filtering and sorting.

We've tested everything and probably published the API to SwaggerHub for everyone to enjoy. It's been a long way, but it was really interesting to me. I hope you enjoy it.

Once again, here is the repository for this blog series: 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 read your own YAML file (*****. Yml) in Java
[Forge] How to register your own Entity and Entity Render in 1.13.2
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