© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
A. Prakash, S. I. BashaHands- On Liferay DXPhttps://doi.org/10.1007/978-1-4842-8563-3_5

5. Service Builder Concepts

Apoorva Prakash1   and Shaik Inthiyaz Basha2
(1)
BANGALORE, India
(2)
Nellore, AP, India
 

This chapter focuses on the Service Builder. The Service Builder is the easiest way to create a service and a DAO layer for portlets with the help of a master file. A lot of boilerplate code is generated when services are built using this file. This file helps generate the mappings. In this chapter, you learn everything about this process and the different ways to fetch data from the database.

Introduction to the Service Builder

The Service Builder is one tool that makes Liferay stand out in connecting services to custom portlets in the database. It’s easy, accurate, and effective. The Service Builder can generate all necessary objects, classes, and methods for database connectivity with a custom module. In short, you can say that it generates a complete service and a DAO layer. Not only this, but it also generates SQL scripts so that when the portlet is deployed, it will generate the necessary tables in the database. The Service Builder is dependent on a file called service.xml. All magic starts in this file. This file contains all the information needed for service generation. Liferay DXP's Service Builder is slightly different than the older version. Now, it does not generate a service.jar file. Instead, it creates two new OSGi modules. Don’t worry, as you’ll learn about them in an upcoming section of the chapter.

As Liferay’s official documentation explains, “Service Builder is a model-driven code generation tool built by Liferay that allows developers to define custom object models called entities. Service Builder uses object-relational mapping (ORM) to generate a service layer. It provides a clear distinction between the object model and code for the database. These saves developers time in implementing those. It also offers built-in caching support (using Ehcache) to accelerate service execution. Service Builder lets developers use custom SQL and dynamic queries for any complex query depending on business logic. How ORM maps with relations is shown in Figure 5-1.

It is essential to understand that, even though Liferay provides a Service Builder, it’s possible to use other Java-supported database persistence technologies, such as Hibernate.

A model of relations between factors of the object-oriented model and the relational model.

Figure 5-1

ORM and relational model mapping

This section has explained the basics of Service Builder; in the next section, you explore how to build services using Service Builder.

Generating Services

The first thing to understand is that Service Builder is a service.xml file. Using only this file, ORM is defined. You need to specify the entity names and fields, and then, when you build the service, the DAO layer is built.

By default, Liferay expects the service.xml file to reside in the root folder of the service module. However, it is customizable. In Service Builder taxonomy, model classes are referred to as entities. Let’s look at how to create a Liferay Service Builder by using Liferay Developer Studio.
  1. 1.

    Open Liferay Developer Studio to the apress_ws workspace, which you used to create the Liferay portlets in Chapters 3 and 4. You use the same workspace so that all the Liferay-created code is in one place. Right-click the Modules folder and then choose New ➤ Liferay Module Project. (See Figure 5-2.)

     

A screenshot of the Liferay module project with an option to choose a template. The other tabs are project name, build type, and the default location.

Figure 5-2

Liferay Service Builder module creation

  1. 2.

    Select Gradle for the build type, as you are building Gradle-based modules. (See Figure 5-2.)

     
  2. 3.

    Name the Project Template Name service-builder. This template will help you create a Liferay Service Builder. Click Next.

     
  3. 4.

    Once you click Finish, Developer Studio automatically creates your apress service engine, which will contain all the service-related classes and files. The service.xml file is the backbone of the Service Builder. (See Figure 5-3.)

     

A screenshot of the Liferay module project displays a row to enter a package name. The button titled "Finish" is at the bottom.

Figure 5-3

Package name for generating service classes

Let's look at the service.xml file you will use to understand this concept. It’s shown in Listing 5-1. Update the service.xml file in the root directory of the service module. However, when you create the project, there is a default service.xml file with default entries. You can also use the GUI section to update service.xml from the Developer Studio, as shown in Listing 5-1. Once you run the build-service, service.xml is read by the Service Builder and the DAO layer is generated, as shown in Listing 5-1.
<?xml version="1.0"?>
<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 7.4.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_7_4_0.dtd">
<service-builder dependency-injector="ds" package-path="com.handsonliferay.apress_service_builder">
    <Author>Apoorva_Inthiyaz</author>
        <namespace>apress</namespace>
        <entity local-service="true" name="ApressBook" remote-service="true" uuid="true">
                <column name="bookId" primary="true" type="long" />
                <column name="groupId" type="long" />
                <column name="companyId" type="long" />
                <column name="chapterId" type="long" />
                <column name="chapterName" type="String" />
                <column name="createDate" type="Date" />
                <column name="isCoding" type="boolean" />
                <order by="asc">
                        <order-column name="chapterId" />
                </order>
                <!-- Finder methods -->
                <finder name="IsCoding" return-type="Collection">
                        <finder-column name="isCoding" />
                </finder>
                <!-- References -->
                <reference entity="AssetEntry" package-path="com.liferay.portlet.asset" />
                <reference entity="AssetTag" package-path="com.liferay.portlet.asset" />
        </entity>
        <entity local-service="true" name="Emp" remote-service="true" uuid="true">
                <column name="EmpId" primary="true" type="long" />
                <column name="groupId" type="long" />
                <column name="companyId" type="long" />
                <column name="projectId" type="long" />
                <column name="projectName" type="String" />
                <column name="createDate" type="Date" />
                <column name="isCoding" type="boolean" />
                <order by="asc">
                        <order-column name="projectId" />
                </order>
                <!-- Finder methods -->
                <finder name="IsCoding" return-type="Collection">
                        <finder-column name="isCoding" />
                </finder>
                <!-- References -->
                <reference entity="AssetEntry" package-path="com.liferay.portlet.asset" />
                <reference entity="AssetTag" package-path="com.liferay.portlet.asset" />
        </entity>
</service-builder>
Listing 5-1

Sample service.xml

You can also use the GUI section to update service.xml from the Developer Studio, as shown in the following steps.
  1. 1.

    Define global information for the service. These settings are applied to all the services of the entities generated by this service.xml file. For example, package path, author, and so on, as shown in Figure 5-4.

     

A screenshot of a service builder with four components: package path, auto namespace tables, author, and namespace. The list of the outline is to the left.

Figure 5-4

GUI section to update service.xml

  1. 2.
    These fields will appear as follows in service.xml:
    <service-builder dependency-injector="ds" package-path="com.handsonliferay.apress_service_builder">
    <Author>Apoorva_Inthiyaz</author>
    <namespace>apress</namespace>
     
  2. 3.

    Define the service entities. Service entities are models in code and tables in the database. This is the first place where ORM starts taking place. (See Figure 5-5.)

     

A screenshot of a service builder window displays the addition of a book as an entity. The option titled "Add a sample entity" is below the window of entities.

Figure 5-5

Defining service entities in the GUI tool

The service.xml file entry looks as follows:
<entity local-service="true" name="ApressBook" remote-service="true" uuid="true">
  1. 4.

    Define the attributes. Attributes are the columns in the table and member variables for the models. This is the next step of ORM, as shown in Figure 5-6.

     

A screenshot of a service builder displays a window titled "columns" with three columns and four rows. The option titled "Add default columns" is below the columns window.

Figure 5-6

Defining the attributes in the GUI tool

The service.xml entry will look as follows:
         <column name="bookId" primary="true" type="long" />
         <column name="groupId" type="long" />
         <column name="companyId" type="long" />
         <column name="chapterId" type="long" />
         <column name="chapterName" type="String" />
         <column name="createDate" type="Date" />
         <column name="isCoding" type="boolean" />
  1. 5.

    Define relationships between entities.

     
  2. 6.

    Define a default order for the data to be retrieved from the database, which can be ascending or descending. These are in the form of the entities.

    As an example, service.xml is shown here:
            <order by="asc">
                            <order-column name="chapterId" />                </order>
     
  3. 7.

    Define the finder methods to retrieve data from the database based on specified parameters. This could be one object or an array of objects.

    As an example, service.xml is shown here:
    <finder name="IsCoding" return-type="Collection">
                    <finder-column name="isCoding" />
    </finder>
     

Let’s review all this with a complete example.

You will generate service classes with the help of Gradle’s Build Service task, which you can find in the following path. Go to the buildService tab by choosing Gradle Tasks ➤ apress_ws ➤ Module ➤ apress_service_builder ➤ apress_service_builder-service ➤ buildService. (See Figure 5-7.)

A screenshot of a program for generating service classes. The other part, titled Gradle tasks, displays 2 columns for names and descriptions.

Figure 5-7

Generate service classes

In Figure 5-8, you can see all the service classes generated with the help of buildService. It will generate all the service-related classes—such as the wrappers, models, and utils—as well as the SQL and XML files required for the service layer.

A screenshot of a console output titled "Building Book." There are 3 tabs on the top, including Markers, Console, and Progress, and Console is selected.

Figure 5-8

Console output of build service

apress_service_builder contains two sub-modules. One is for the API classes and the other one is for service classes, as shown in Figure 5-9.

A screenshot of the hierarchy of apress underscore service, underscore builder, and apress underscore service, underscore builder, underscore service.

Figure 5-9

Hierarchy of apress_service_builder

Once you deploy apress_service_builder on the Liferay server, it will automatically create the tables shown in Figure 5-11, which are available in the entity section of service.xml with the help tables.sql file, as shown in Figure 5-10.

A screenshot of the project explorer on the left and the S Q L statement for creating a table under title table dot s q l.

Figure 5-10

SQL statement after building the service

A screenshot of the list of tables and columns created under the title "I portal 74." The list also includes indexes and foreign keys.

Figure 5-11

Tables created in the database after deploying the service

This section has explained how to build services using the Service Builder; in the next section, you learn about the code generated by the Service Builder.

Deep Diving Into the Code Generated by the Service Builder

By deep diving into the Liferay Service Builder, you will learn about the different layers of a Liferay service. You can understand this with the help of Figure 5-12.

A three-layered model consists of a column of rectangular boxes labeled model, services, and persistence.

Figure 5-12

Liferay Service Layer

  • Persistence layer: This layer is used to save and retrieve entities to and from the database.

  • Model layer: This layer is used to define objects to represent your project’s entities.

  • Services layer: This is a blank layer ready for you to create your API and business logic.

Customization via Implementation Classes

Entity implementation: This is responsible for customizing the entity; all these classes will have the extension *Impl.Java.

Local service implementation: Local services are the core of the services generated by the Service Builder. As the name suggests, local services should be accessed by any class running in the same context. When you run the Service Builder, it is generated with the name of *LocalServiceImpl. This class is the entity’s local service extension point. All the custom local services you need to write should be written in this class, but you need to access these classes from *LocalServiceUtil. The *LocalServiceUtil class contains only the signature of all the methods implemented in the LocalServiceImpl class, and these are registered in *LocalServiceUtil when you build the service. Whenever you add a new method or modify a method’s parameter, you must build the service again. If it sounds complicated, check out the example in Listings 5-2 and 5-3.
package com.handsonliferay.apress_service_builder.service.impl;
import com.handsonliferay.apress_service_builder.model.ApressBook;
import com.handsonliferay.apress_service_builder.service.base.ApressBookLocalServiceBaseImpl;
import com.liferay.portal.aop.AopService;
import java.util.Collection;
import org.osgi.service.component.annotations.Component;
/**
 * @author Apoorva_Inthiyaz
 */
@Component(
        property = "model.class.name=com.handsonliferay.apress_service_builder.model.ApressBook",
        service = AopService.class
)
public class ApressBookLocalServiceImpl extends ApressBookLocalServiceBaseImpl {
}
Listing 5-2

ApressBookLocalServiceImpl Class

package com.handsonliferay.apress_service_builder.service;
import com.handsonliferay.apress_service_builder.model.ApressBook;
import com.liferay.petra.sql.dsl.query.DSLQuery;
import com.liferay.portal.kernel.dao.orm.DynamicQuery;
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.model.PersistedModel;
import com.liferay.portal.kernel.util.OrderByComparator;
import java.io.Serializable;
import java.util.List;
public class ApressBookLocalServiceUtil {
        public static ApressBook addApressBook(ApressBook apressBook) {
                return getService().addApressBook(apressBook);
        }
        public static ApressBook createApressBook(long bookId) {
                return getService().createApressBook(bookId);
        }
        public static PersistedModel createPersistedModel(
                        Serializable primaryKeyObj)
                throws PortalException {
                return getService().createPersistedModel(primaryKeyObj);
        }
        public static ApressBook deleteApressBook(ApressBook apressBook) {
                return getService().deleteApressBook(apressBook);
        }
        public static ApressBook deleteApressBook(long bookId)
                throws PortalException {
                return getService().deleteApressBook(bookId);
        }
        public static PersistedModel deletePersistedModel(
                        PersistedModel persistedModel)
                throws PortalException {
                return getService().deletePersistedModel(persistedModel);
        }
        public static <T> T dslQuery(DSLQuery dslQuery) {
                return getService().dslQuery(dslQuery);
        }
        public static int dslQueryCount(DSLQuery dslQuery) {
                return getService().dslQueryCount(dslQuery);
        }
        public static DynamicQuery dynamicQuery() {
                return getService().dynamicQuery();
        }
        public static <T> List<T> dynamicQuery(DynamicQuery dynamicQuery) {
                return getService().dynamicQuery(dynamicQuery);
        }
        public static <T> List<T> dynamicQuery(
                DynamicQuery dynamicQuery, int start, int end) {
                return getService().dynamicQuery(dynamicQuery, start, end);
        }
        public static <T> List<T> dynamicQuery(
                DynamicQuery dynamicQuery, int start, int end,
                OrderByComparator<T> orderByComparator) {
                return getService().dynamicQuery(
                        dynamicQuery, start, end, orderByComparator);
        }
        public static long dynamicQueryCount(DynamicQuery dynamicQuery) {
                return getService().dynamicQueryCount(dynamicQuery);
        }
        public static long dynamicQueryCount(
                DynamicQuery dynamicQuery,
                com.liferay.portal.kernel.dao.orm.Projection projection) {
                return getService().dynamicQueryCount(dynamicQuery, projection);
        }
        public static ApressBook fetchApressBook(long bookId) {
                return getService().fetchApressBook(bookId);
        }
      public static ApressBook fetchApressBookByUuidAndGroupId(
                String uuid, long groupId) {
                return getService().fetchApressBookByUuidAndGroupId(uuid, groupId);
        }
        public static java.util.Collection<ApressBook> findByisCoding(
                Boolean iscode) {
                return getService().findByisCoding(iscode);
        }
        public static com.liferay.portal.kernel.dao.orm.ActionableDynamicQuery
                getActionableDynamicQuery() {
                return getService().getActionableDynamicQuery();
        }
        public static ApressBook getApressBook(long bookId) throws PortalException {
                return getService().getApressBook(bookId);
        }
        public static ApressBook getApressBookByUuidAndGroupId(
                        String uuid, long groupId)
                throws PortalException {
                return getService().getApressBookByUuidAndGroupId(uuid, groupId);
        }
        public static List<ApressBook> getApressBooks(int start, int end) {
                return getService().getApressBooks(start, end);
        }
        public static List<ApressBook> getApressBooksByUuidAndCompanyId(
                String uuid, long companyId) {
                return getService().getApressBooksByUuidAndCompanyId(uuid, companyId);
        }
        public static List<ApressBook> getApressBooksByUuidAndCompanyId(
                String uuid, long companyId, int start, int end,
                OrderByComparator<ApressBook> orderByComparator) {
                return getService().getApressBooksByUuidAndCompanyId(
                        uuid, companyId, start, end, orderByComparator);
        }
        public static int getApressBooksCount() {
                return getService().getApressBooksCount();
        }
        public static
                com.liferay.portal.kernel.dao.orm.IndexableActionableDynamicQuery
                        getIndexableActionableDynamicQuery() {
                return getService().getIndexableActionableDynamicQuery();
        }
        public static String getOSGiServiceIdentifier() {
                return getService().getOSGiServiceIdentifier();
        }
        public static PersistedModel getPersistedModel(Serializable primaryKeyObj)
                throws PortalException {
                return getService().getPersistedModel(primaryKeyObj);
        }
        public static ApressBook updateApressBook(ApressBook apressBook) {
                return getService().updateApressBook(apressBook);
        }
        public static ApressBookLocalService getService() {
                return _service;
        }
        private static volatile ApressBookLocalService _service;
}
Listing 5-3

ApressBookLocalServiceUtil Class

Remote Service Implementation

You already know what a local service is. A remote service, on the other hand, is a service that can be accessed from resources running outside of the application context. Liferay DXP allows you to expose web services as JSON and SOAP web services. Even various Liferay services are available in the form of web services.

You can list JSON services using the following URL on your development environment:
http://localhost:8080/api/jsonws/
You can list SOAP web services using the following URL on your development environment:
http://localhost:8080/api/axis

To generate remote services for your custom entities, you must run the Service Builder with the remote-service attribute set to true. After setting this, you need to build the service again, which will generate all the classes, interfaces, and files required to support SOAP and JSON web services. Once the service is built, it provides *ServiceImpl, as shown in Listing 5-4, where you need to write the business logic for implementing remote services. The best practice when implementing a remote service is to add a proper permission check because remotes services are open for access from other applications.

Another best practice is to implement the business logic in *LocalServiceImpl and invoke it from *ServiceImpl after doing the permission check. Listing 5-4 shows an example.
package com.handsonliferay.apress_service_builder.service.impl;
import com.handsonliferay.apress_service_builder.service.base.ApressBookServiceBaseImpl;
import com.liferay.portal.aop.AopService;
import org.osgi.service.component.annotations.Component;
/**
 * @author Apoorva_Inthiyaz
 */
@Component(
        property = {
                "json.web.service.context.name=apress",
                "json.web.service.context.path=ApressBook"
        },
        service = AopService.class
)
public class ApressBookServiceImpl extends ApressBookServiceBaseImpl {
}
Listing 5-4

ApressBookLocalServiceUtil Class

CRUD Operations

CRUD (Create, Read, Update and Delete) operations are used when interacting with persistent storage. They are defined as follows:
  • Create: The Create operation is an INSERT operation in SQL. A new value is inserted into the database.

  • Read: The Read operation is a SELECT operation in SQL. Values are fetched from the database. You’ll see this in detail in the “Finder” section of this chapter.

  • Update: The Update operation is an UPDATE operation in SQL. Inserted values can be modified.

  • Delete: The Delete operation is DELETE in SQL. You can delete the existing values from the database.

Let’s see how these are implemented. To use these services in any module, you need to add dependencies to the build. The Gradle file of the consumer module is shown in Listing 5-5. The apressMVC module has been added, which was created in Chapter 2.
dependencies {
        compileOnly group: "com.liferay.portal", name: "release.dxp.api"
        compileOnly project(":modules:apress_service_builder:apress_service_builder-api")
        compileOnly project(":modules:apress_service_builder:apress_service_builder-service")
        cssBuilder group: "com.liferay", name: "com.liferay.css.builder", version: "3.0.2"
}
Listing 5-5

Adding Service Builder Dependencies to the build.gradle File

compileOnly project(":modules:apress_service_builder:apress_service_builder-api") is used in the build.gradle file, which enables apress_service_builder-api classes to be available to the apressMVC portlet.

compileOnly project(":modules:apress_service_builder:apress_service_builder-service") is used in the build.gradle file, which enables apress_service_builder-service classes to be available.

The example code in Listing 5-6 creates an entry in a table using Liferay Service Builder services.
import com.apress.handsonliferay.constants.ApressMVCPortletKeys;
import com.handsonliferay.apress_service_builder.model.ApressBook;
import com.handsonliferay.apress_service_builder.model.impl.ApressBookImpl;
import com.handsonliferay.apress_service_builder.service.ApressBookLocalServiceUtil;
                ApressBook apressBook = new ApressBookImpl();
                        apressBook.setBookId(CounterLocalServiceUtil.increment());
                        apressBook.setChapterName("Liferay");
                        apressBook.setCreateDate(new Date());
                        apressBook.setIsCoding(true);
                ApressBookLocalServiceUtil.addApressBook(apressBook);
Listing 5-6

Code Snippet to Create an Entry in a Custom Table

Listing 5-7 shows example code that updates the table entry created using Liferay Service Builder services. This example is updating the chapter name HandsOnliferay to an entry whose bookId is 1234.
ApressBook apressBook1 = ApressBookLocalServiceUtil.fetchApressBook(1234);
                apressBook1.setChapterName("HandsOnliferay");
ApressBookLocalServiceUtil.updateApressBook(apressBook1);
Listing 5-7

Code Snippet to Update an Entry in a Custom Table

Listing 5-8 shows example code that deletes an entry from the table created by using Liferay Service Builder services. This example is deleting the entry with its bookId set to 1234 in two different ways.
// Way 1
ApressBookLocalServiceUtil.deleteApressBook(1234);
// Way 2
ApressBook apressBookOb = ApressBookLocalServiceUtil.fetchApressBook(1234);
ApressBookLocalServiceUtil.deleteApressBook(apressBookOb);
Listing 5-8

Code Snippet to Delete an Entry in a Custom Table

Listing 5-9 shows example code that reads an entry from the table created using Liferay Service Builder services. This example reads the entry with a bookId of 1234.
ApressBook apressBook = ApressBookLocalServiceUtil.fetchApressBook(1234);
System.out.println(" Book name :"+ apressBook.getChapterName();
Listing 5-9

Code Snippet to Read an Entry from a Custom Table

Finders

Liferay Service Builder provides a straightforward approach for fetching data from database table columns. These methods are called finder methods. They are easy to implement but have a drawback: they are fit for simple fetch operations, not for complex ones. To generate a finder method, you must add a finder tag to the service.xml file and configure it accordingly.

Data retrieved by the finder methods is in the form of model objects that fulfill the specified criteria. The Service Builder generates several methods based on each finder created for an entity. It creates methods to fetch, find, remove, and count entity instances based on the finder’s parameters.

Let’s see this concept with an example:
  1. 1.

    Write a service.xml file (see Listing 5-10).

     
<service-builder dependency-injector="ds" package-path="com.handsonliferay.apress_service_builder">
    <Author>Apoorva_Inthiyaz</author>
        <namespace>apress</namespace>
        <entity local-service="true" name="ApressBook" remote-service="true" uuid="true">
                <column name="bookId" primary="true" type="long" />
                <column name="groupId" type="long" />
                <column name="companyId" type="long" />
                <column name="chapterId" type="long" />
                <column name="chapterName" type="String" />
                <column name="createDate" type="Date" />
                <column name="isCoding" type="boolean" />
                <order by="asc">
                        <order-column name="chapterId" />
                </order>
                <!-- Finder methods -->
                <finder name="IsCoding" return-type="Collection">
                        <finder-column name="isCoding" />
                </finder>
                <!-- References -->
                <reference entity="AssetEntry" package-path="com.liferay.portlet.asset" />
                <reference entity="AssetTag" package-path="com.liferay.portlet.asset" />
        </entity>
</service-builder>
Listing 5-10

Writing a Finder in Service.xml

  1. 2.

    Write the business logic for the custom finder in the Entity Local Service Impl class.

     
  2. 3.

    After these changes, you need to add buildService to the Service Builder module. (See Listing 5-11.)

     
package com.handsonliferay.apress_service_builder.service.impl;
import com.handsonliferay.apress_service_builder.model.ApressBook;
import com.handsonliferay.apress_service_builder.service.base.ApressBookLocalServiceBaseImpl;
import com.liferay.portal.aop.AopService;
import java.util.Collection;
import org.osgi.service.component.annotations.Component;
/**
 * @author Apoorva_Inthiyaz
 */
@Component(
        property = "model.class.name=com.handsonliferay.apress_service_builder.model.ApressBook",
        service = AopService.class
)
public class ApressBookLocalServiceImpl extends ApressBookLocalServiceBaseImpl {
        public Collection<ApressBook> findByisCoding(Boolean iscode){
                return apressBookPersistence.findByIsCoding(iscode);
        }
}
Listing 5-11

Writing Finder Business Logic in the ApressBookLocalServiceImpl Class

  1. 4.

    Invoke the custom implemented finder in the custom module (see Listing 5-12).

     
Collection<ApressBook> apressBookObj = ApressBookLocalServiceUtil.findByisCoding(false);
Listing 5-12

Invoking the Custom Implemented Finder

Dynamic Query

Liferay allows custom SQL queries to retrieve data from the database. However, in real-world applications, you’ll need to build queries dynamically. (If you do not want to build the query dynamically, you can use Custom SQL, which you’ll see in the next section.) Returning to dynamic queries, Liferay provides the DynamicQuery API. The DynamicQuery API is a wrapper of the Hibernates Criteria API.

When creating a dynamic query, a SQL query is generated by the code using the DynamicQuery API, where you write Java code, not SQL. In the DynamicQuery API, the query is written as Java code, where variables and objects are used instead of tables and columns. These queries are simple to write and implement in comparison to SQL queries.

Listing 5-13 shows an example. This example uses a dynamic query to fetch all the users from a database table called user.
import com.liferay.portal.kernel.dao.orm.DynamicQuery;
import com.liferay.portal.kernel.dao.orm.DynamicQueryFactoryUtil;
import com.liferay.portal.kernel.dao.orm.RestrictionsFactoryUtil;
DynamicQuery userQuery = DynamicQueryFactoryUtil.forClass(User.class, "user",
                                        PortalClassLoaderUtil.getClassLoader());
                        userQuery.add(RestrictionsFactoryUtil.like("user.emailAddress", "test%"));
                        try {
                                List<User> customUsersList = UserLocalServiceUtil.dynamicQuery(userQuery);
                                for (User user : customUsersList) {
                                        System.out.println("ID: " + user.getUserId() + " Name: " + user.getFirstName() + " Email ID: " + user.getEmailAddress());
                                }
                        } catch (SystemException e) {
                        }
Listing 5-13

Dynamic Query Code Snippet for a Custom Table

Custom SQL

The Service Builder generates finder methods that fetch values for the tables. You learned about finders and dynamic queries in the previous sections. Real-world applications are sometimes too complex to be covered by simple finders and dynamic queries. In those cases, custom SQL is implemented.

Custom SQL gives you the liberty to directly execute SQL statements for implementation, but great power comes with great responsibility. Custom SQL comes with the drawback—if the application’s database changes, you may have to modify the query based on the database engine’s query syntax. For instance, if you were using MySQL as a portal database (so all the custom SQL is written in MySQL) and then later you move the database to Oracle, the Custom SQL queries need to be modified according to the Oracle syntax. More complex cases such as joins are the major cases where these are implemented. You learn how to implement Custom SQL in this tutorial.

Liferay custom SQL is supported by the Service Builder method. It helps execute custom complex queries against the database by invoking custom SQL from a finder method in your persistence layer. The Service Builder enables you to generate these interfaces in your finder method.

Follow these steps to do this:
  1. 1.

    Specify your Custom SQL.

    You need to specify it in a particular file so Liferay can access it. The CustomSQLUtil class (from the com.liferay.portal.dao.orm.custom.sql module) retrieves the SQL from a file called default.xml in your service module’s src/main/resources/META-INF/custom-sql/ folder. You must create the custom-sql folder and a default.xml file in that custom-sql folder. The default.xml file must adhere to the format shown in Listing 5-14.

     
<custom-sql>
<sql id="com.apress.handsonliferay.service.persistence.EntryFinder.findByApressbookName">
<![CDATA[
 SELECT AP_Entry.*
    FROM AP_Entry INNER JOIN
AP_Apressbook ON AP_Entry.bookId = AP_Apressbook.bookId
 WHERE (AP_Entry.name LIKE ?) AND (AP_Apressbook.name LIKE ?)
]]>
</sql></custom-sql>
Listing 5-14

The default.xml File for the Custom SQL

  1. 2.

    Implement your custom finder method.

    Service Builder generates the interface for the finder in your API module. Still, you have to create the implementation to implement the finder method in the persistence layer to invoke your custom SQL query. First, you need to create a *FinderImpl class in the service persistence package. In the Apressbook application, you could create an EntryFinderImpl class in the com.apress.handsonliferay.service.persistence.impl package. Your class should extend BasePersistenceImpl<Entry>.

    Run the Service Builder to generate the *Finder interface based on the *FinderImpl class. Modify your *FinderImpl class to make it a component (annotated with @Component) that implements the *Finder interface you just generated. (See Listing 5-15.)

     
@Component(service = EntryFinder.class)
public class EntryFinderImpl extends BasePersistenceImpl<Event>
    implements EntryFinder {
        public List<Entry> findByApressbookName(
                    String entryName, String entryMessage, String guestbookName,
                    int begin, int end) {
                    Session session = null;
                    try {
                        session = openSession();
                        String sql = CustomSQLUtil.get(
                            getClass(),
                            FIND_BY_ENTRYNAME_BOOKNAME);
                        SQLQuery q = session.createSQLQuery(sql);
                        q.setCacheable(false);
                        q.addEntity("AP_Entry", EntryImpl.class);
                        QueryPos qPos = QueryPos.getInstance(q);
                        qPos.add(entryName);
                        qPos.add(entryMessage);
                        qPos.add(guestbookName);
                        return (List<Entry>) QueryUtil.list(q, getDialect(), begin, end);
                    }
                    catch (Exception e) {
                        try {
                            throw new SystemException(e);
                        }
                        catch (SystemException se) {
                            se.printStackTrace();
                        }
                    }
                    finally {
                        closeSession(session);
                    }
                    return null;
                }
                public static final String FIND_BY_ENTRYNAME_BOOKNAME =
                    EntryFinder.class.getName() +
                        ".findByApressbookName";
}
Listing 5-15

EntryFinderImpl for the Custom SQL

  1. 3.

    Access your finder method from the service.

     
public List<Entry> findByApressbookName(String entryName,
            String bookName) throws SystemException {
            return entryFinder.findByApressbookName(String entryName,
                String bookName);
        }

Working with Remote Services

Now that you understand how to create a service and consume it in your controller class, there is another type that can be consumed from other applications as well. These services are referred to as remote services. They come in handy when you need your application to communicate with other applications of ecology. Liferay provides features to expose services as remote components that are straightforward and easy to maintain. Liferay offers two approaches to achieve this, discussed in the following sections.

Headless REST APIs

As their name suggests, these services are not consumed in the application exposing the services. In a way, your application works as a microservice host, a new-age way of working with APIs. These services are RESTful web services, independent of Liferay DXP’s front-end. This is why they are called headless. These APIs fulfill the OpenAPI specification.

Liferay DXP’s headless REST APIs leverage OpenAPI (known initially as Swagger); you don’t need a service catalog. You only need to know the OpenAPI profile to discover the rest of the APIs to begin consuming the web service.

Liferay DXP’s headless APIs are available in SwaggerHub at https://app.swaggerhub.com/organizations/liferayinc.

Each API has its own URL in SwaggerHub.

For example, you can access the delivery API definition at https://app.swaggerhub.com/apis/liferayinc/headless-delivery/v1.0.

Each OpenAPI profile is also deployed dynamically in your portal instance under this schema:

http://[host]:[port]/o/[insert-headless-api]/[version]/openapi.yaml

For example, if you’re running Liferay DXP locally on port 8080, the home URL for discovering the headless delivery API is:

http://localhost:8080/o/headless-delivery/v1.0/openapi.yaml

You must be logged in to access this URL or use basic authentication and a browser. You can also use other tools like Postman, an advanced REST client, or even the curl command from your system console.

Run this curl command to access the home URL:
curl http://localhost:8080/o/headless-delivery/v1.0/openapi.yaml -u [email protected]:test
For example, to invoke specific site blog posting details, you use the following snippet:
curl "http://localhost:8080/o/headless-delivery/v1.0/sites/20124/blog-postings/" -u '[email protected]:test'

Let’s look at this process in more detail with an example.

You will use Blade to create this example.
  1. 1.

    Create a project using Blade, as follows:

     
blade init -v 7.2 books
  1. 2.

    Edit this project’s build.gradle file to get the REST builder Gradle plugin. The REST builder Gradle plugin lets you generate a REST layer that’s defined in the REST builder’s rest-config.yaml and rest-openapi.yaml files. To use this plugin, include it in your build script, as shown in Listing 5-16.

     
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"
Listing 5-16

The Build Script

The REST builder plugin automatically resolves the Liferay REST builder library as a dependency; you have to configure a repository that hosts the library and its transitive dependencies.
repositories {
    maven {
        url "https://repository-cdn.liferay.com/nexus/content/groups/public"
    }
}
This plugin will add one task to the buildREST project. By default, the REST builder plugin creates a configuration called restBuilder and adds a dependency to its most recent version.
dependencies {
    restBuilder group: "com.liferay", name: "com.liferay.portal.tools.rest.builder", version: "1.0.22"
}
  1. 3.

    Quickly check the tasks by using ./gradlew tasks from the command prompt to the books project. The output is shown in Listing 5-17.

     
$ ./gradlew tasks
> Task :tasks
------------------------------------------------------------
All tasks runnable from the root project
------------------------------------------------------------
Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
buildCSS - Build CSS files.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildLang - Runs Liferay Lang Builder to translate language property files.
buildNeeded - Assembles and tests this project and all projects it depends on.
buildREST - Runs Liferay REST Builder.
Listing 5-17

Execution of the Gradle Tasks Command

  1. 4.

    Create a handful of modules.

     
$ cd modules/headless-books
$ blade create -t api -v 7.2 -p com.apress.handsonliferay.headless.books headless-books-api
Successfully created project headless-books-api in books/modules/headless-books
$ blade create -t api -v 7.2 -p com.apress.handsonliferay.headless.books headless-books-impl
Successfully created project headless-books-impl in books/modules/headless-books
$ blade create -t api -v 7.2 -p com.apress.handsonliferay.headless.books headless-books-client
Successfully created project headless-books-client in books/modules/headless-books
$ blade create -t api -v 7.2 -p com.apress.handsonliferay.headless.books headless-books-test
Successfully created project headless-books-test in books/modules/headless-books
  1. 5.

    Create the YAML files that will define the service endpoints. To headless-books-impl, you need to add the rest-config.yaml file, as shown in Listing 5-18. The rest-openapi.yaml file must also be created in your headless-books-impl module.

     
apiDir: "../headless-books-api/src/main/java"
apiPackagePath: " com.apress.handsonliferay.headless.books"
application:
    baseURI: "/headless-books"
    className: "HeadlessBooksApplication"
    name: "Apress.Handsonliferay.Headless.Books"
author: "Apoorva_Inthiyaz"
clientDir: "../headless-books-client/src/main/java"
testDir: "../headless-books-test/src/testIntegration/java"
Listing 5-18

The rest-config.yaml File

Every OpenAPI YAML file has three sections: meta, paths (endpoints), and reusable components (type definitions), and these files are no different; see Listings 5-19 and 5-20.
openapi: 3.0.1
info:
  title: "Headless Books"
  version: v1.0
  description: "API for accessing Book details."
Listing 5-19

The Meta Section of the File

components:
  schemas:
    Book:
      description: Contains all of the data for a single book.
      properties:
        name:
          description: The book name.
          type: string
        id:
          description: The book ID.
          type: string
        chapterNames:
          description: The Chapter names of the book.
           type: string
         creator:
          $ref: "#/components/schemas/Creator"
      type: object
Listing 5-20

Reusable Components

In YAML file indents signify depth, so a line at a higher indent is a child, and a line at the same depth is a sibling. The creator is a reference to another object in the file (that's $ref). When you do have a $ref in the same file, it means you need to include the reference.

Plain Web/REST Services

This is the legacy way to build and consume web services in Liferay DXP, but it is still supported. This lets you use JAX-RS, JAX-WS, or the Service Builder to implement plain REST or SOAP web services. You learned how to implement these services in a previous section of this chapter, where implementation of web service is explained.

When you build services with the Service Builder, all remote-enabled services (i.e., service.xml entities with remote-service="true") are exposed as JSON web services. This is shown in the following snippet:
<entity local-service="true" name="ApressBook" remote-service="true" uuid="true">
To test Liferay’s JSON web service registration process, add a simple method to your Apress services. Edit your ApressBookServiceImpl class as shown in Listing 5-21.
package com.handsonliferay.apress_service_builder.service.impl;
import com.handsonliferay.apress_service_builder.service.base.ApressBookServiceBaseImpl;
import com.liferay.portal.aop.AopService;
import org.osgi.service.component.annotations.Component;
/**
 * @author Apoorva_Inthiyaz
 */
@Component(
        property = {
                "json.web.service.context.name=apress",
                "json.web.service.context.path=ApressBook"
        },
        service = AopService.class
)
public class ApressBookServiceImpl extends ApressBookServiceBaseImpl {
        public String helloWorld(String worldName) {
            return "Hello world: " + worldName;
        }
}
Listing 5-21

ApressBookServiceImpl with a Custom Method

Rebuild the services and redeploy your app’s modules. You can now invoke this service method via JSON.

You can create the mapped URL of an exposed service by following this naming convention:
http://[server]:[port]/api/jsonws/[context-path].[service-class-name]/[service-method-name]
[context-path], [service-class-name] ,[service-method-name] is defined in your *ServiceImpl. Note in Listing 5-21 that the following snippet is used:
property = {
                "json.web.service.context.name=apress",
                "json.web.service.context.path=ApressBook"
        },
This snippet calls the hello-world method:
http://localhost:8080/api/jsonws/apress.ApressBook/hello-world
Listing 5-22 provides different ways of creating these URLs by different annotation values.
package com.handsonliferay.apress_service_builder.service.impl;
import com.handsonliferay.apress_service_builder.service.base.ApressBookServiceBaseImpl;
import com.liferay.portal.aop.AopService;
import com.liferay.portal.kernel.jsonwebservice.JSONWebService;
import org.osgi.service.component.annotations.Component;
/**
 * @author Apoorva_Inthiyaz
 */
@Component(
        property = {
                "json.web.service.context.name=apress",
                "json.web.service.context.path=ApressBook"
        },
        service = AopService.class
)
@JSONWebService("abs")
public class ApressBookServiceImpl extends ApressBookServiceBaseImpl {
        public String helloWorld(String worldName) {
            return "Hello world: " + worldName;
        }
        @JSONWebService(value = "add-book-wow", method = "PUT")
        public boolean addBook() {
                return false;
        }
        @JSONWebService("/add-something-very-specific")
        public boolean addApressBook() {
                return true;
        }
}
Listing 5-22

ApressBookServiceImpl with the addBook Method Ways

This concludes how a service can be exposed from Liferay DXP.

Summary

In this chapter, you learned about the easiest way to create a service and DAO layer for a portlet, which is using the Service Builder. The Service Builder uses the service.xml file to generate the ORM and its methods. You can do CRUD operations using these generated methods. To perform CRUD operations, local and remote services can be used. Local and remote services can be invoked, depending on the context in which the service is being invoked. To fetch data from tables, you can implement finders, dynamic queries, or Custom SQL, depending on the complexity of the query.

In the next chapter, you will go through different ways of customizing Liferay DXP.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset