© 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_6

6. Liferay Customization

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

Portlets are the main component when it comes to creating any portal. Liferay is no different. Themes are developed to give these portlets headers and footers so they have a project-specific look and feel. But what if you have an out-of-box Liferay DXP portlet that matches your requirements, and a minor tweak can make it serve your project-specific business needs? What should you do in that case? To address this scenario, Liferay DXP provides an approach to customize behavior. These customizations range from simple JSP changes and extensions to a level where you can modify the overall behavior of the Liferay DXP portal. This chapter explains how it’s done.

Overriding Language Keys

Internationalization (i18n) and localization can be achieved quickly in Liferay, by using language property files. Language property files are nothing but resource bundles with key and value pairs. These values are replaced when the key is found in the implemented code at runtime. These property files can be used in custom portlets as well. There are two types of languages that can be modified: global language properties and module language properties. The following sections discuss both.

Global Language Property

Say you need to replace the word “user” across the portal with the word “employee.” This is an example of a global language property. To do this, you need to create a resource bundle service component.

The following example illustrates this concept.
  1. 1.

    Determine the language keys to override. You need to determine the keys that you want to override from the source in the /portal-impl/src/content/Language[xx_XX].properties path from a portal-impl.jar bundle. For example: lang.user.name.required.field.names=Required-Last-Name.

     
  2. 2.

    Override the keys in a new language properties file. Listing 6-1 shows how to change the lang.​user.​name.required.field.names key value from last-name to Required-Last-Name.

     
  3. 3.

    Create a resource bundle service component, as shown in Listing 6-1.

     
package com.apress.handsonliferay.portlet;
import com.liferay.portal.kernel.language.UTF8Control;
import java.util.Enumeration;
import java.util.ResourceBundle;
import org.osgi.service.component.annotations.Component;
/**
 * @author Inthiyaz_Apoorva
 */
@Component(
            property = { "language.id=en_US" },
            service = ResourceBundle.class
)
public class RosourceBundlePortlet extends ResourceBundle {
        @Override
        protected Object handleGetObject(String key) {
                 return _resourceBundle.getObject(key);
        }
        @Override
        public Enumeration<String> getKeys() {
                return _resourceBundle.getKeys();
        }
        private final ResourceBundle _resourceBundle =
                        ResourceBundle.getBundle("content.Language_en_US", UTF8Control.INSTANCE);
}
Listing 6-1

ResourceBundle Class

The @Component annotation shown in Listing 6-1 declares it an OSGi ResourceBundle service component. Its language.id property designates it for the en_US locale.
@Component(
property = { "language.id=en_US" },
service = ResourceBundle.class
)
The code shown in Listing 6-2 is helpful for illustrating resource bundle assignment.
private final ResourceBundle _resourceBundle =
ResourceBundle.getBundle("content.Language_en_US", UTF8Control.INSTANCE);
Listing 6-2

Resource Bundle Assignment

Module Language Property

To customize a module’s language property, you need to create and prioritize the module’s resource bundle.

Let’s look at this more closely with an example.
  1. 1.

    Find the module and its metadata and language keys. As shown in Figures 6-1 and 6-2, you will use the Gogo shell to achieve this. Choose Control Panel ➤ Gogo Shell.

     

A screenshot of the Gogo shell. The top tab is titled Command, and the button below the command bar is titled execute and get the output.

Figure 6-1

Gogo shell’s lb command

A screenshot of the Gogo shell. The top bar reads header 1535, and the button below the command bar is labeled execute. and get the output of the bundle headers

Figure 6-2

Gogo shell’s headers command

  1. 2.

    In Figure 6-2, note the bundle symbolic name and the servlet context name and version. You’ll use these to create the resource bundle loader later in the process.

    For example, here are those for the Liferay Blogs Web module:

     
Bundle symbolic name: com.apress.handsonliferay
Bundle version: 1.0.0
  1. 3.

    Next, find the module’s JAR file so you can examine its language keys. Then write your custom language key values. To achieve this, you need to write a new module to hold a resource bundle loader and keys. In that module’s path, within the src/main/resources/content folder, create language properties files for the locale whose keys you want to override. In the language properties file, specify your language key overrides.

     
  2. 4.

    Prioritize your module’s resource bundle. Now that your language keys are in place, use OSGi manifest headers to specify the language keys for the target module. To compliment the target module’s resource bundle, you’ll aggregate your resource bundle with the target module’s resource bundle. You’ll list your module first to prioritize its resource bundle over the target module resource bundle.

     

This section has explained how language keys are modified. In the next section, you see how to customize JSPs.

Customizing JSPs

Customizing JSPs is the basic customization process in Liferay. This lets you modify the existing JSPs of the Liferay portal and any out-of-box portlets. Customizing JSPs lets developers leverage the out-of-the-box Liferay DXP functionality and customize them based on business requirements. This results in minimizing development efforts. There are two approaches for JSPs customization, using Liferay APIs and overriding a JSP using an OSGi fragment. They are both discussed in the following sections.

Customization JSPs with Liferay APIs

There are various approaches to customizing portlets and Liferay core JSPs; out of them, Liferay DXP’s API approach is considered the easiest and best. Even the Liferay official documentation highly recommends the API-based approach. This can be achieved with one of the methods discussed in the following sections.

Dynamic Includes

As the name suggests, this approach adds contents dynamically, which works with the help of dynamic include tags. This is easy to implement and fits where you need to add more code to the existing JSPs. However, it comes with a limitation: its usage is limited to the JSPs that use dynamic-include tags or classes that inherit IncludeTag. Every JSP contains a placeholder liferay-util:dynamic-include tag, which is an extension point for inserting content such as HTML and JavaScript. There are cases when this placeholder is not present in JSPs; in such instances, you need to use another customization approach.

The code examples in Listings 6-3 and 6-4 show how to achieve this approach. Add extra compile-only dependencies to the build file of the newly created custom module.
dependencies {
        compileOnly group: "com.liferay.portal", name: "release.dxp.api"
        cssBuilder group: "com.liferay", name: "com.liferay.css.builder", version: "3.0.2"
        compileOnly group: "javax.portlet", name: "portlet-api", version: "2.0"
        compileOnly group: "javax.servlet", name: "javax.servlet-api", version: "3.0.1"
        compileOnly group: "com.liferay", name: "com.liferay.petra.string", version: "1.0.0"
        compileOnly group: "com.liferay. ", name: "com.liferay.portal.kernel", version: "2.0.0"
        compileOnly group: "org.osgi", name: "osgi.cmpn", version: "6.0.0"
}
Listing 6-3

The Build File for Dynamic Include

package com.apress.handsonliferay.portlet;
import com.liferay.portal.kernel.servlet.taglib.DynamicInclude;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.osgi.service.component.annotations.Component;
/**
 * @author Inthiyaz_Apoorva
 */
@Component(
                immediate = true,
                service = DynamicInclude.class
        )
public class MyBlogsDynamicIncludePortlet implements DynamicInclude {
        @Override
        public void include(
                        HttpServletRequest request, HttpServletResponse response,
                        String key)
                throws IOException {
                PrintWriter printWriter = response.getWriter();
                printWriter.println("<h2>Added by Blogs Dynamic Include!</h2><br />");
        }
        @Override
        public void register(DynamicIncludeRegistry dynamicIncludeRegistry) {
                dynamicIncludeRegistry.register(
                        "com.liferay.blogs.web#/blogs/view_entry.jsp#pre");
        }
}
Listing 6-4

Dynamic Include Implementation Class

The DynamicInclude interface implementation is shown in Listing 6-4, and it contains the include() and register() methods. By overriding these methods, you can achieve this functionality.

Portlet Filters

Using this approach, a JSP is not modified directly; instead, the portlet request and response are modified to simulate the customization. Before a portlet request or response is processed, the filter intercepts and modifies it. The main advantage of portlet filters is that they provide access to alter the JSP’s complete content, unlike a dynamic include, where you can append some content.

Listing 6-5 shows the example code to achieve this. Add extra compile-only dependencies in the build file of the newly created module.

An example of the implementation is shown in Listing 6-6.
dependencies {
        compileOnly group: "com.liferay.portal", name: "release.dxp.api"
        cssBuilder group: "com.liferay", name: "com.liferay.css.builder", version: "3.0.2"
        compileOnly group: "javax.portlet", name: "portlet-api", version: "2.0"
    compileOnly group: "javax.servlet", name: "javax.servlet-api", version: "3.0.1"
    compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel", version: "2.0.0"
    compileOnly group: "org.osgi", name: "osgi.cmpn", version: "6.0.0"
}
Listing 6-5

The Build File for Portlet Filters

package com.apress.handsonliferay.portlet;
import com.liferay.portal.kernel.model.PortletFilter;
import com.liferay.portal.kernel.util.PortletKeys;
import java.io.IOException;
import javax.portlet.PortletException;
import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;
import javax.portlet.filter.FilterChain;
import javax.portlet.filter.FilterConfig;
import javax.portlet.filter.RenderFilter;
import javax.portlet.filter.RenderResponseWrapper;
import org.osgi.service.component.annotations.Component;
/**
 * @author Inthiyaz_Apoorva
 */
@Component(
                 immediate = true,
                    property = {
                            "javax.portlet.name=" + PortletKeys.BLOGS
                    },
                    service = PortletFilter.class
)
public class MyBlogRenderFilterPortlet implements RenderFilter {
        @Override
        public void init(FilterConfig filterConfig) throws PortletException {
                // TODO Auto-generated method stub
        }
        @Override
        public void destroy() {
                // TODO Auto-generated method stub
        }
        @Override
        public void doFilter(RenderRequest request, RenderResponse response, FilterChain chain)
                        throws IOException, PortletException {
                 RenderResponseWrapper renderResponseWrapper = new RenderResponseWrapper(response);
                chain.doFilter(request, renderResponseWrapper);
                String text = renderResponseWrapper.toString();
                if (text != null) {
                    String interestingText = "<input  class="field form-control"";
                    int index = text.lastIndexOf(interestingText);
                    if (index >= 0) {
                        String newText1 = text.substring(0, index);
                        String newText2 = " <p>Added by MyBlogs Render Filter!</p> ";
                        String newText3 = text.substring(index);
                        String newText = newText1 + newText2 + newText3;
                        response.getWriter().write(newText);
                    }
                }
        }
}
Listing 6-6

The Portlet Filter Implementation Class

Using OSGi Fragments or a Custom JSP Bag

Liferay strongly recommends customizing JSPs using Liferay DXP’s APIs, which you learned about in the previous section. Overriding a JSP using an OSGi fragment or a custom JSP bag comes with limitations, they don’t guarantee anything, and there are times when they fail miserably. If there is an issue with the code, the error will appear at runtime. Liferay’s official documentation suggests this approach must be used as a last resort.

Using an OSGi Fragment

As it is said, “with great power comes great responsibility.” OSGi fragments are an excellent example of this. They are mighty, and they allow you to modify JSPs completely, leading to the customization of the widget’s complete look and feel and functionality. They can also make a module fail. If the Liferay DXP patch that modifies a JSP is installed, it can cause this fragment to fail.

An OSGi fragment that overrides a JSP requires these two things:
  1. 1.

    Declare the fragment host. The following code declares the fragment host:

    "Fragment-Host: com.liferay.login.web;bundle-version="[1.0.0,1.0.1)"

     
  2. 2.

    Provide the overridden JSP. In the overridden JSP, you have two possible naming conventions for targeting the host’s original JSP: portal and original.

    For example, if the original JSP is in the /META-INF/resources/login.jsp folder, the fragment bundle should contain a JSP with the same path, using this pattern:

    <liferay-util:include

    page="/login.original.jsp" (or login.portal.jsp)

    servletContext="<%= application %>"

    />

    You must ensure that you mimic the host module’s folder structure when overriding its JAR. For example:

    my-jsp-fragment/src/main/resources/META-INF/resources/login.jsp

     

If your fragment uses an internal package from the fragment host, continue using it, but you must explicitly exclude the package from the bundle’s Import-Package OSGi manifest header.

This Import-Package header, for example, excludes packages that match com.liferay.portal.search.web.internal.*.

Using a Custom JSP Bag

Customization with a custom JSP bag has the same limitation as the OSGi fragment method. Custom JSP bag methods can be used to customize Liferay DXP core JSPs.

This custom JSP Bag module must satisfy the criteria of providing and specifying a custom JSP for the JSP you’re extending.
  1. 1.
    Create JSPs to override the Liferay DXP core JSPs. For example, if you’re overriding portal-web/docroot/html/common/themes/bottom-ext.jsp, you have to place your custom JSP in the following path of your custom module:
    apressMVC/src/main/resources/META-INF/jsps/html/common/themes/bottom-ext.jsp
     
Note

If you use a location other than the Liferay core, you have to assign that location to a -includeresource: META-INF/jsps= directive in your module’s bnd.bnd file. For example, if you place custom JSPs in a folder called src/META-INF/custom_jsps in your module, you would specify the following in your bnd.bnd file: -includeresource: META-INF/jsps=src/META-INF/custom_jsps. This includes a CustomJspBag implementation for serving the custom JSPs.

Listing 6-7 shows example code that will help you achieve the functionality for CustomJspBags. You can write a class with this code.
package com.apress.handsonliferay.portlet;
import com.liferay.portal.deploy.hot.CustomJspBag;
import com.liferay.portal.kernel.url.URLContainer;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
@Component(
            immediate = true,
            property = {
                "context.id=BladeCustomJspBag",
                "context.name=Test Custom JSP Bag",
                "service.ranking:Integer=100"
            }
        )
public class MycustomJSPBAG implements CustomJspBag {
        private Bundle _bundle;
        private List<String> _customJsps;
        @Override
        public String getCustomJspDir() {
                 return "META-INF/jsps/";
        }
        @Activate
        protected void activate(BundleContext bundleContext) {
                _bundle = bundleContext.getBundle();
                _customJsps = new ArrayList<>();
                Enumeration<URL> entries = _bundle.findEntries(
                        getCustomJspDir(), "*.jsp", true);
                while (entries.hasMoreElements()) {
                        URL url = entries.nextElement();
                        _customJsps.add(url.getPath());
                }
        }
        @Override
        public List<String> getCustomJsps() {
                return _customJsps;
        }
        @Override
        public URLContainer getURLContainer() {
                return _urlContainer;
        }
        private final URLContainer _urlContainer = new URLContainer() {
            @Override
            public URL getResource(String name) {
                return _bundle.getEntry(name);
            }
            @Override
            public Set<String> getResources(String path) {
                Set<String> paths = new HashSet<>();
                for (String entry : _customJsps) {
                    if (entry.startsWith(path)) {
                       paths.add(entry);
                    }
                }
                return paths;
            }
        };
        @Override
        public boolean isCustomJspGlobal() {
                  return true;
        }
}
Listing 6-7

The CustomJSPBag Implementation Class

This sample code should help you understand the CustomJspBag.

There is one more concept called ExtendJSP, which can also be used to override JSPs.

If you want to add something to a Liferay core JSP, you have to create an empty JSP with a postfix of -ext jsp and then override that instead of the whole JSP, which will help avoid the confusion and not mess up the actual code. This approach keeps things more straightforward and stable and helps prevent breaking your customization.

Using this concept, you only rely on the original JSP, including the -ext.jsp file.

For example, open portal-web/docroot/html/common/themes/bottom.jsp and scroll to the end. You’ll see this:
<liferay-util:include page="/html/common/themes/bottom-ext.jsp" />

If you must add something to bottom.jsp, you must override bottom-ext.jsp.

This section has explained how to customize JSPs. In the next section, you learn how to customize services using wrappers.

Customizing Services Using Wrappers

There are cases in real-world applications where you need certain extra functionality on top of the out-of-box Liferay features. For example, you saw how to change the word “user” to the word “employee” using the language property key. Now assume you are developing a portal that serves as a solution for employees. In this case, the User entity needs to store an Employee ID, which is not a Liferay DXP-provided field in the User entity. To accommodate this scenario, you need to modify the User model. Liferay DXP’s service wrappers provide easy-to-use extension points for customizing OOB services.

To create a module for customizing services using wrappers, you need to create a servicewrapper module using the Service Wrapper template.

You learn how to do this with the help of an example, which is shown in Figures 6-3 and 6-4 and Listings 6-8 and 6-9.

A screenshot of the steps involved in developing a service wrapper module for the Liferay module project. The project name, location, build type, and template name are all part of the process.

Figure 6-3

Creating the servicewrapper module

A screenshot of the Liferay module project's window for selecting a service name. Component class name and package name are the other tabs.

Figure 6-4

Selecting a user service to override

Click the Next button in the popup window shown in Figure 6-3.

package com.handsonliferay.servicewrapper;
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.model.User;
import com.liferay.portal.kernel.service.ServiceContext;
import com.liferay.portal.kernel.service.ServiceWrapper;
import com.liferay.portal.kernel.service.UserLocalService;
import com.liferay.portal.kernel.service.UserLocalServiceWrapper;
import java.util.Locale;
import java.util.Map;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
 * @author Apoorva_Inthiyaz
 */
@Component(
        immediate = true,
        property = {
        },
        service = ServiceWrapper.class
)
public class UserServiceOverride extends UserLocalServiceWrapper {
        public UserServiceOverride() {
                super(null);
        }
}
Listing 6-8

The UserServiceOverride Class

package com.handsonliferay.servicewrapper;
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.model.User;
import com.liferay.portal.kernel.service.ServiceContext;
import com.liferay.portal.kernel.service.ServiceWrapper;
import com.liferay.portal.kernel.service.UserLocalService;
import com.liferay.portal.kernel.service.UserLocalServiceWrapper;
import java.util.Locale;
import java.util.Map;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
 * @author Apoorva_Inthiyaz
 */
@Component(
        immediate = true,
        property = {
        },
        service = ServiceWrapper.class
)
public class UserServiceOverride extends UserLocalServiceWrapper {
        public UserServiceOverride() {
                super(null);
        }
        @Override
                public User addUser(long creatorUserId, long companyId, boolean autoPassword, String password1, String password2,boolean autoScreenName, String screenName, String emailAddress, Locale locale, String firstName,String middleName, String lastName, long prefixId, long suffixId, boolean male, int birthdayMonth,int birthdayDay, int birthdayYear, String jobTitle, long[] groupIds, long[] organizationIds, long[] roleIds,long[] userGroupIds, boolean sendEmail, ServiceContext serviceContext) throws PortalException {
                        // TODO Auto-generated method stub
                System.out.println(" User First Name "+firstName);
                System.out.println(" User middle Name "+middleName);
                System.out.println("User Last Name "+lastName);
                        return super.addUser(creatorUserId, companyId, autoPassword, password1, password2, autoScreenName, screenName,emailAddress, locale, firstName, middleName, lastName, prefixId, suffixId, male, birthdayMonth, birthdayDay,birthdayYear, jobTitle, groupIds, organizationIds, roleIds, userGroupIds, sendEmail, serviceContext);
                }
        @Override
        public User getUser(long userId) throws PortalException {
            System.out.println("Getting user by id " + userId);
            return super.getUser(userId);
        }
        @Reference(unbind = "-")
        private void serviceSetter(UserLocalService userLocalService) {
            setWrappedService(userLocalService);
        }
}
Listing 6-9

The Customized User Service Wrapper Using the UserServiceOverride Class

This section has explained how to customize services using wrappers; in the next section, you learn how to customize OSGi services.

Customizing OSGi Services

Let’s rewind a bit to the OSGi chapter, where you learned that all the components are registered as services in the OSGi service registry. All the services builder services existing inside portal implementations (portal-impl) are standard spring beans implementations, which Liferay makes available as OSGi services.

This customization approach is tricky because it involves identifying the service extension point for service modification. This can be done with the help of the Gogo shell. Once the service is identified, you need to create a new module with a custom service. And finally, you need to configure the OSGi component to use the newly created service. An important point to note here is the service rank; the custom module must have a higher ranking to make the custom module override a service.

Let’s look at an example. Here are the steps to customize an OSGI service:
  1. 1.

    Get the service and service reference details.

    Use the scr:info [componentName] command in the Gogo shell and then follow these steps:
    1. a.

      Copy the service interface name.

       
    2. b.

      Copy the existing service name.

       
    3. c.

      Gather any reference configuration details (if reconfiguration is necessary).

       
     
  2. 2.
    Create a custom service. Follow these steps:
    1. a.

      Create a module.

       
    2. b.

      Create a custom service class to implement the service interface you want.

       
    3. c.

      Ensure that the declarative services component is the best match for the reference to the service interface.

       
    4. d.
      If you want to use the existing service implementation, declare a field that uses a declarative services reference to the existing service. Use the component.name key for that. For example:
      @Reference  (
              target="(component.name=override.my.service.reference.service.impl.SomeServiceImpl)"
              )
      private SomeService _defaultService;
       
    5. e.

      Override the interface methods if required.

       
    6. f.

      Deploy the module to register the custom service.

       
     
  3. 3.
    Configure components to use your custom service. Liferay DXP’s Configuration Admin lets you use configuration files to swap service references on the fly.
    1. a.

      You need to create a system configuration file with the [component].config naming convention, replacing [component] with the component’s name.

       
    2. b.

      Add a reference entry to the [reference].target=[filter] file.

       
    3. c.

      Use [Liferay_Home]/osgi/configs to deploy the configuration file.

       
     

This section has explained how to customize OSGi services. In the next section, you see how to customize MVC commands.

Customizing MVC Commands

Chapter 3 covered MVC commands and explained how they work. To summarize, MVC commands allow you to break a single controller class into several classes for each kind of portlet action, making the code more manageable. You learned about the controller class, which is essentially a component class, because of the @component annotation. These classes are registered as OSGi components in the component registry. The basic concept is to create a service with custom code and at a higher ranking that overrides the command. While customizing MVCActionCommand and MVCResourceCommand, you add custom logic to the methods, whereas for MVCRenderCommand, you can redirect users to a different JSP by adding more logic.

Let’s look at this more closely with an example.

Here are the steps for adding logic to MVC commands:
  1. 1.

    Implement the interface:

    public class MVC_Action_Override extends BaseMVCActionCommand

     
  2. 2.

    Publish this as a component:

     
@Component(
                immediate = true,
                property = {
                "javax.portlet.name=" + BlogsPortletKeys.BLOGS_ADMIN,
                "mvc.command.name=/blogs/edit_entry",
                "service.ranking:Integer=100"
                },
        service = MVCActionCommand.class
)
  1. 3.

    Refer to the original implementation:

     
@Reference(target= "(component.name=com.liferay.blogs.web.internal.portlet.action.EditEntryMVCActionCommand)")
protected MVCActionCommand mvcActionCommand;
  1. 4.

    Add the logic and call the original:

     
@Override
protected void doProcessAction(ActionRequest actionRequest, ActionResponse actionResponse) throws Exception {
                // Add your custom logic in this method
                mvcActionCommand.processAction(actionRequest, actionResponse);
        }
You can see complete implementations of the MVC_Action_Override class in Listing 6-10, the MVC_Resource_Override class in Listing 6-11, and the MVC_Render_Override class in Listing 6-12.
package com.handsonliferay.mvccommandoverride.portlet;
import com.liferay.blogs.constants.BlogsPortletKeys;
import com.liferay.portal.kernel.portlet.bridges.mvc.BaseMVCActionCommand;
import com.liferay.portal.kernel.portlet.bridges.mvc.MVCActionCommand;
import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
 * @author Apoorva_Inthiyaz
 */
@Component(
        immediate = true,
        property = {
                "javax.portlet.name=" + BlogsPortletKeys.BLOGS_ADMIN,
                "mvc.command.name=/blogs/edit_entry",
                "service.ranking:Integer=100"
                },
        service = MVCActionCommand.class
)
public class MVC_Action_Override extends BaseMVCActionCommand {
        @Override
        protected void doProcessAction(ActionRequest actionRequest, ActionResponse actionResponse) throws Exception {
                // Add your custom logic in this method
                mvcActionCommand.processAction(actionRequest, actionResponse);
        }
        @Reference(
                    target = "(component.name=com.liferay.blogs.web.internal.portlet.action.EditEntryMVCActionCommand)")
                protected MVCActionCommand mvcActionCommand;
}
Listing 6-10

MVC_Action_Override Complete Class

package com.handsonliferay.mvccommandoverride.portlet;
import com.liferay.login.web.constants.LoginPortletKeys;
import com.liferay.portal.kernel.portlet.bridges.mvc.MVCResourceCommand;
import javax.portlet.PortletException;
import javax.portlet.ResourceRequest;
import javax.portlet.ResourceResponse;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
 * @author Apoorva_Inthiyaz
 */
@Component(
            property = {
                "javax.portlet.name=" + LoginPortletKeys.LOGIN,
                "mvc.command.name=/login/captcha"
            },
            service = MVCResourceCommand.class
)
public class MVC_Resource_Override implements MVCResourceCommand{
    @Reference(target =
            "(component.name=com.liferay.login.web.internal.portlet.action.CaptchaMVCResourceCommand)")
        protected MVCResourceCommand mvcResourceCommand;
        @Override
        public boolean serveResource(ResourceRequest resourceRequest, ResourceResponse resourceResponse)
                        throws PortletException {
                 System.out.println("Serving login captcha image");
                return mvcResourceCommand.serveResource(resourceRequest, resourceResponse);
        }
}
Listing 6-11

MVC_Resource_Override Complete Class

package com.handsonliferay.mvccommandoverride.portlet;
import com.liferay.blogs.constants.BlogsPortletKeys;
import com.liferay.portal.kernel.portlet.bridges.mvc.MVCRenderCommand;
import com.liferay.portal.kernel.portlet.bridges.mvc.constants.MVCRenderConstants;
import com.liferay.portal.kernel.util.PortalUtil;
import javax.portlet.PortletException;
import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
 * @author Apoorva_Inthiyaz
 */
@Component(
            immediate = true,
            property = {
                "javax.portlet.name=" + BlogsPortletKeys.BLOGS,
                "javax.portlet.name=" + BlogsPortletKeys.BLOGS_ADMIN,
                "javax.portlet.name=" + BlogsPortletKeys.BLOGS_AGGREGATOR,
                "mvc.command.name=/blogs/edit_entry"
            },
            service = MVCRenderCommand.class
        )
public class MVC_Render_Override implements MVCRenderCommand {
         @Override
            public String render(
                RenderRequest renderRequest, RenderResponse renderResponse) throws
                    PortletException {
                System.out.println("Rendering custom_edit_entry.jsp");
                RequestDispatcher requestDispatcher =
                    servletContext.getRequestDispatcher("/custom_edit_entry.jsp");
                try {
                    HttpServletRequest httpServletRequest =
                        PortalUtil.getHttpServletRequest(renderRequest);
                    HttpServletResponse httpServletResponse =
                        PortalUtil.getHttpServletResponse(renderResponse);
                    requestDispatcher.include
                        (httpServletRequest, httpServletResponse);
                } catch (Exception e) {
                    throw new PortletException
                        ("Unable to include custom_edit_entry.jsp", e);
                }
                return MVCRenderConstants.MVC_PATH_VALUE_SKIP_DISPATCH;
            }
            @Reference(target = "(osgi.web.symbolicname=com.custom.code.web)")
            protected ServletContext servletContext;
            @Reference(target = "(component.name=com.liferay.blogs.web.internal.portlet.action.EditEntryMVCRenderCommand)")
            protected MVCRenderCommand mvcRenderCommand;
}
Listing 6-12

MVC_Render_Override Complete Class

This section has explained how to customize MVC commands. In the next section, you learn how to customize models using model listeners.

Customizing Models Using Model Listeners

Model listeners are classes that listen and invoke alongside model persistent methods and execute certain business logic. Model listeners are generally lightweight logic execution. They must implement the ModelListener interface. Depending on the configuration, this logic execution may occur before or after the model persistence method invocation. However, there are more ways to listen than using the Before and After.
  • onBeforeAddAssociation(), onAfterAddAssociation(), onBeforeCreate(), and onAfterCreate(): These methods are invoked when a CREATE operation is intercepted on a model. onBeforeAddAssociation() and onAfterAddAssociation() are used when mapping exists between two entities. These methods can be used to execute logic before/after an association record is added. For one table, onBeforeCreate() and onAfterCreate() are invoked after/before the creation of a single table record.

  • onBeforeUpdate() and onAfterUpdate():These methods are invoked when an UPDATE operation is intercepted.

  • onBeforeRemoveAssociation(), onAfterRemoveAssociation(), onBeforeRemove(), and onAfterRemove(): These methods are invoked when a DELETE operation is intercepted on a model. onBeforeRemoveAssociation() and onAfterRemoveAssociation() are used when mapping tables exist between two entities. These methods can be used to execute logic before/after an association record is deleted. For one table, onBeforeRemove() and onAfterRemove() are invoked before/after the creation of a single table record.

You can understand this process with the help of Figure 6-5.

A simulation of model listener events. The sequence is as follows: 1. Action. 2. Prior to that, model listeners. After that, model action and listeners.

Figure 6-5

The order of the model listener evets

In a previous example, you saw how to add the Employee Id field to the User entity. Now let’s look at the same example of an employee, but this time you need to print an audit log in the logging file whenever a new user is added to the Liferay database. Listing 6-13 shows a sample implementation.
package com.handsonliferay.mvccommandoverride.portlet;
import com.handsonliferay.apress_service_builder.model.ApressBook;
import com.liferay.portal.kernel.model.BaseModel;
import com.liferay.portal.kernel.model.BaseModelListener;
import com.liferay.portal.kernel.model.ModelListener;
import org.osgi.service.component.annotations.Component;
/**
 * @author Apoorva_Inthiyaz
 */
@Component(
            immediate = true,
            service = ModelListener.class
)
public class CustomEntityListener extends BaseModelListener<ApressBook>{
}
Listing 6-13

Custom Entity Listener Class

This section has explained how to customize MVC commands and customizing models using model listeners. In the next section, you see how to implement Expando attributes.

Expando Attributes

Expando attributes, as the name suggests, are used to expand something. In Liferay, Expando attributes are often referred to as custom fields. These fields are essentially extra fields that can be added to Liferay’s OOB entities. The best part of using this Liferay feature is that these fields can be added without modifying the table but by doing it logically. The data saved in these fields is available on-demand, similar to other preexisting fields’ data.

These custom fields can be added to the control panel as well. Expando also provides APIs to manage tables, columns, rows, and values programmatically. Liferay’s database contains four tables to save these custom attribute values for persistence—expandotable, expandorow, expandocolumn and expandovalue—as shown in Figures 6-6 through 6-9.

A screenshot of the expando table's contents. Columns, indexes, foreign keys, and triggers are among the contents.

Figure 6-6

The expandotable table view

A screenshot of the content of the expando row table. The contents include columns, indexes, foreign keys, and triggers.

Figure 6-7

The expandorow table view

A screenshot of the contents of the expando column table. The contents include columns, indexes, foreign keys, and triggers. The columns tab is selected.

Figure 6-8

The expandocolumn table view

A screenshot of the contents of the expando value table. The contents include columns, indexes, and foreign keys.

Figure 6-9

The expandovalue table view

Custom fields can be different types and hold various values, such as text fields (indexed or secret), integers, selection of multiple values, and many more. Once you’ve created a field, you cannot change its type.

Expando is a compelling feature of Liferay, and it adds a lot of flexibility for developers to utilize existing entities and add features on top of them. The following example programmatically operates on a custom field. You can see the list of all the packages used while programmatically implementing Expando in Listing 6-14, while Listing 6-15 has a complete implementation of Expando in an action class.
import com.liferay.expando.kernel.model.ExpandoColumn;
import com.liferay.expando.kernel.model.ExpandoColumnConstants;
import com.liferay.expando.kernel.model.ExpandoRow;
import com.liferay.expando.kernel.model.ExpandoTable;
import com.liferay.expando.kernel.model.ExpandoTableConstants;
import com.liferay.expando.kernel.model.ExpandoValue;
import com.liferay.expando.kernel.service.ExpandoColumnLocalServiceUtil;
import com.liferay.expando.kernel.service.ExpandoRowLocalServiceUtil;
import com.liferay.expando.kernel.service.ExpandoTableLocalServiceUtil;
import com.liferay.expando.kernel.service.ExpandoValueLocalServiceUtil;
Listing 6-14

Packages for Expando

public class ApressMVCPortlet extends MVCPortlet {
        public void doView(
            RenderRequest renderRequest, RenderResponse renderResponse)
    throws IOException, PortletException {
           System.out.println("START... Process Expando...");
            ThemeDisplay themeDisplay = (ThemeDisplay) renderRequest.getAttribute(WebKeys.THEME_DISPLAY);
            ExpandoTable userExpandoTable = getOrAddExpandoTable(themeDisplay.getCompanyId(), User.class.getName(),
                        ExpandoTableConstants.DEFAULT_TABLE_NAME);
           System.out.println("User Expando Table ID : " + userExpandoTable.getTableId());
            ExpandoColumn designationExpandoColumn = getOrAddExpandoColumn(themeDisplay.getCompanyId(), User.class.getName(),
                        ExpandoTableConstants.DEFAULT_TABLE_NAME, "Designation", userExpandoTable);
           System.out.println("Designation Expando Column ID : " + designationExpandoColumn.getColumnId());
           System.out.println("DONE... Process Expando...");
            include(viewTemplate, renderRequest, renderResponse);
        }
        public ExpandoTable getOrAddExpandoTable(long companyId, String className, String tableName) {
            ExpandoTable expandoTable = null;
            try {
                expandoTable = ExpandoTableLocalServiceUtil.getDefaultTable(companyId, className);
            } catch (NoSuchTableException e) {
                try {
                    expandoTable = ExpandoTableLocalServiceUtil.addTable(companyId, className, tableName);
                } catch (Exception e1) {
                   }
            } catch (Exception e) {
                System.out.println(e);
            }
            return expandoTable;
        }
        public ExpandoColumn getOrAddExpandoColumn(long companyId, String className, String tableName, String columnName,
                ExpandoTable expandoTable) {
            ExpandoColumn exandoColumn = null;
            try {
                exandoColumn = ExpandoColumnLocalServiceUtil.getColumn(companyId, className, tableName, columnName);
                if (exandoColumn == null) {
                    exandoColumn = ExpandoColumnLocalServiceUtil.addColumn(expandoTable.getTableId(), columnName,
                            ExpandoColumnConstants.STRING, StringPool.BLANK);
                }
            } catch (SystemException e) {
                System.out.println(e);
            } catch (PortalException e) {
                System.out.println(e);
            }
            return exandoColumn;
        }
        public ExpandoRow getOrAddExpandoRow(long tableId,long rowId , long classPK) {
                ExpandoRow expandoRow = null;
                try {
                        expandoRow = ExpandoRowLocalServiceUtil.getRow(rowId);
                        if(expandoRow ==null) {
                        expandoRow        = ExpandoRowLocalServiceUtil.addRow(tableId, classPK);
                        }
                } catch (PortalException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                }
                return expandoRow;
        }
        public void getExpandoValue(long companyId,long classNameId, long tableName) {
                ExpandoValue expandoValue =  ExpandoValueLocalServiceUtil.getValue(companyId, classNameId, tableName);
        }
}
Listing 6-15

Implementation of Expando in a Controller Class

This section has explained how to implement Expando attributes. In the next section, you see how to implement post- and pre-actions.

Pre and Post-Actions

Pre- and post-actions are essential features and come in handy when you need to execute certain logic before (pre) or after (post) the execution of the auction event. This process of writing pre- and post-actions is also called event customization. The following is the list of portal events that can be customized using this approach:
  • application. startup.events, application.shutdown.events

  • login.events.pre, login.events.post

  • logout.events.pre, logout.events.post

  • servlet.service.events.pre, servlet.service.events.post

  • servlet.session.create.events, servlet.session.destroy.events

Portal.properties contain information about portal events, which is why you need to extend the portal.properties file to implement this. Hooks are instrumental in implementing these scenarios. They are good for triggering custom actions on common portal events, such as user logins or system startups. The actions for each of these events are defined in portal.properties, so you need to extend this file to create a custom action. Hooks make this a simple task.

Let’s look at this more deeply with an example. Figure 6-10 shows how to create a component class.

A screenshot of the Liferay component class creation process. The windows on the screen are project explorer, new and Liferay component class, console script, and Gradle tasks.

Figure 6-10

Menu to create a Liferay component class

Right-click the module and then choose New ➤ Liferay Component Class, as shown in Figure 6-10.

A screenshot of the New Liferay component's template selection. The project and package names, component class names, and template are filled in the information bars.

Figure 6-11

Selecting the component class template for a pre-action

Figure 6-11 shows how to select any project where you want to create a component class. You need to provide the package name and your custom component class name. Then you need to select Login Pre Action from the list of templates provided by your Liferay Developer Studio. Once you click Finish, you can view the Login Pre-Action, as shown in Listing 6-16.
package com.handsonliferay.preaction;
import com.liferay.portal.kernel.events.ActionException;
import com.liferay.portal.kernel.events.LifecycleAction;
import com.liferay.portal.kernel.events.LifecycleEvent;
import org.osgi.service.component.annotations.Component;
@Component(
        immediate = true,
        property = {
                "key=login.events.pre"
        },
        service = LifecycleAction.class
)
public class MyPreAction implements LifecycleAction {
        @Override
        public void processLifecycleEvent(LifecycleEvent lifecycleEvent)
                throws ActionException {
                System.out.println("login.event.pre=" + lifecycleEvent);
        }
}
Listing 6-16

Login Pre-Action Component Class

The processLifecycleEvent method will be executed before the login action is performed:
@Component(
        immediate = true,
        property = {
                "key=login.events.pre"
        },
        service = LifecycleAction.class
)
The component section from the class shows which type of action class you are using, pre or post. If you want to create an action for post, your key value will change, as shown in Listing 6-17.
package com.handsonliferay.postaction;
import com.liferay.portal.kernel.events.ActionException;
import com.liferay.portal.kernel.events.LifecycleAction;
import com.liferay.portal.kernel.events.LifecycleEvent;
import org.osgi.service.component.annotations.Component;
@Component(
        immediate = true,
        property = {
                "key=login.events.post"
        },
        service = LifecycleAction.class
)
public class MyPostAction implements LifecycleAction {
        @Override
        public void processLifecycleEvent(LifecycleEvent lifecycleEvent)
                throws ActionException {
                System.out.println("login.event.post=" + lifecycleEvent);
        }
}
Listing 6-17

Login Post-Action Component Class

This section has explained how to implement post- and pre-actions. In the next section, you see how to customize search.

Customizing Search

Search is a new term in the book and has not been covered in any previous sections. Search basically involves a search engine that uses algorithms and indexes to fetch relevant results based on entered search terms. A scoring method is used to decide the relevancy of the results, which is referred to as ranking. The higher the rank, the more relevant the result is. It helps to have a high-speed mechanism to calculate rank every time the user enters a new search term, which is why this cannot be performed in the database. Search engines use indexes for this purpose which are fast for these kinds of tasks. These indexes store data per the search execution and querying logic.

Elasticsearch and Solr search are supported search engines in Liferay DXP, Elasticsearch being the default one. These search engines can be deployed on differents server or on the same server. The first approach is called remote mode, while the second is called embedded. Liferay recommends using remote search mode for production.

To enable search in your custom modules, Liferay provides a search API. To make your custom model (hereafter referred to as the custom asset) searchable, you need to save it in the Liferay search index. To achieve this, you must make sure it implements a model document contributor.

There is much more to understand about search, and it could fill a whole book. So, without deviating too much from the point, let’s look at a simple example of how you can enable search in a custom module.

A screenshot of the New Liferay component's index template selection. The project and package names, component class names, and template are filled in the information bars.

Figure 6-12

Selecting the component class template for indexer

Figure 6-12 shows how to select any project where you want to create a component class. You need to provide the package name and your custom component class name. Then you need to select Indexer Post Processor from the list of templates provided by your Liferay Developer Studio. Once you click Finish, you can view MyIndexerPostProcessor, as shown in Listing 6-18.
package com.handsonliferay.indexer;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.search.BooleanQuery;
import com.liferay.portal.kernel.search.Document;
import com.liferay.portal.kernel.search.IndexerPostProcessor;
import com.liferay.portal.kernel.search.SearchContext;
import com.liferay.portal.kernel.search.Summary;
import com.liferay.portal.kernel.search.filter.BooleanFilter;
import java.util.Locale;
import org.osgi.service.component.annotations.Component;
@Component(
        immediate = true,
        property = {
                "indexer.class.name=com.liferay.portal.model.User"
        },
        service = IndexerPostProcessor.class
)
public class MyIndexerPostProcessor implements IndexerPostProcessor {
        @Override
        public void postProcessContextBooleanFilter(
                        BooleanFilter booleanFilter, SearchContext searchContext)
                throws Exception {
                if (_log.isInfoEnabled()) {
                        _log.info("postProcessContextBooleanFilter");
                }
        }
        @Override
        public void postProcessDocument(Document document, Object obj)
                throws Exception {
                if (_log.isInfoEnabled()) {
                        _log.info("postProcessDocument");
                }
        }
        @Override
        public void postProcessFullQuery(
                        BooleanQuery fullQuery, SearchContext searchContext)
                throws Exception {
                if (_log.isInfoEnabled()) {
                        _log.info("postProcessFullQuery");
                }
        }
        @Override
        public void postProcessSearchQuery(
                        BooleanQuery searchQuery, BooleanFilter booleanFilter,
 SearchContext searchContext)
                throws Exception {
                if (_log.isInfoEnabled()) {
                        _log.info("postProcessSearchQuery");
                }
        }
        @Override
        public void postProcessSummary(
                Summary summary, Document document, Locale locale, String snippet) {
                if (_log.isInfoEnabled()) {
                        _log.info("postProcessSummary");
                }
        }
        private static final Log _log = LogFactoryUtil.getLog(
                MyIndexerPostProcessor.class);
}
Listing 6-18

MyIndexerPostProcessor Component Default Class

Listing 6-18 shows MyIndexerPostProcessor implementing the IndexerPostProcessor interface, which is provided to customize search queries and documents before they’re sent to the search engine, and to customize result summaries when they’re returned to end users. This basic demonstration prints a message in the log when one of the *IndexerPostProcessor methods is called.

You must add a logging category to the portal to see this sample’s messages in Liferay DXP’s log. Navigate to Control Panel ➤ Configuration ➤ Server Administration and choose Log Levels ➤ Add Category. Then fill out the form as follows:
  1. 1.

    Logger name: com.handsonliferay.indexer.MyIndexerPostProcessor

     
  2. 2.
    Log level: INFO
    @Component(
            immediate = true,
            property = {
                    "indexer.class.name=com.liferay.portal.model.User"
            },
            service = IndexerPostProcessor.class
    )

    The component section from the component class is written for the User model. If you want to write an indexer for a custom model, you have to use the indexer class name of the custom model.

    For example:
    property = {
    "indexer.class.name=com.handsonliferay.apress_service_builder.model.ApressBook
            },
     

Summary

This is the last chapter of this book, and you have seen how to customize Liferay differently. You have seen how to customize UI with different methods. Further, you learned about the customization of action classes and services using wrappers, MVC action commands, and models. Finally, you learned how to customize Liferay events and add your module in Search. These all come in very handy in real-world applications.

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

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