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.
ServiceBuilder
, you can skip those hassles and spend more time designing your own services. *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.
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 the
com.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.
After creating the Service Builder layer and fixing the headless-vitamins-impl
dependency, the next step is to actually start implementing the method.
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.
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.
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.
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 a
Creator field instead of a
creatorIdfield, but since the
creatorId 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 put
com.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,
StructuredContentis
JournalArticle,
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.
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 of
StructuredContentthat searches for articles by site ID adds the site ID as a query argument. The
flatten` 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 a
PersistedVitamin instance using the primary key value extracted from the Document, and the
PersistedVitaminis passed to
_toVitamin ()` to handle the final conversion.
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.
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