Custom Liferay Framework - Part 2

Implement your own Custom Liferay Framework over a simple use case and understand the possibilities , capabilities that it unlocks.

Part-2 | How to Custom Liferay Framework?

Implement your own Custom Liferay Framework over a simple use case and understand the possibilities and capabilities that open up. Continuing on top of the perspectives and ideas discussed in Part 1 of the blog series, we will try to implement our own Custom Liferay Framework for a simple use case picked up from one of the Liferay OOTB feature, Liferay Forms.

​​​​​​​

Implementing Custom Liferay Framework (Powered  by OSGI and Software Design Patterns)

As we know, Building a Custom Liferay Framework for any custom developed complex module will enable us to follow a defined way to extend and customize them. Which in-turn will definitely help us to avoid chaos in the future. This is proven to be a great approach if we analyze Liferay's roadmap. One such feature is the Dispatch Framework which is build on top of the Scheduler Engine. Ideally Liferay Quartz Schedulers were used, but now there is a framework and can be availed - Dispatch Framework, will make another interesting topic of its own. For now, let us focus on our Custom Liferay Framework.

Considering our exploration, let us try to build our own simplified Custom Forms (Similar to Liferay Forms) application. Limiting the scope to only the filed types of Liferay forms application. We will focus more on how we can design & build our application with a custom framework so that the below are possible:

  • A defined way to add new features. Extending Capability
  • A defined way to customize the existing features. Customization

This is exactly how we extend or customize Liferay out of the box feature, Don't we? We implement a set of classes and voila!!!, the features are implemented/customized. Thanks to the OSGI Service Registry Pattern and a set of few other Software design patterns implemented, the complexity of extending and customizing is drastically reduced.

Prerequisite:

  1. It is good to understand the background and purpose of Custom Liferay Framework from Part-1 of this blog series.
  2. It is expected to understand how custom form field types are implemented for Liferay Forms since we will try to create the same design using the Whiteboard pattern (OSGI Service Registry) and the factory pattern.
    ​​​​Reference Link: How to implement custom form fields in Liferay Forms?

Key takeaways from the above link, in order to add a new form field type (extension):

  1. We provide an implementation to a specific service.
  2. We extend the base implementation (Abstraction).
  3. Provide implementation for the required methods defined such as render etc.

This is how exactly most of the Liferay OOTB features are customized or extended. Now let us try to design and create our simple custom forms application. The application will just display the list of field types available and their details as screenshot below.



Image-1: Custom Forms Application displaying the list of available field types and their details in a simple view.

That's the whole of this application, no drag and drop no CRUD operations, this application just displays the form fields available and the details of the same. This simplicity that we limit ourselves is to understand how to enable a framework, how can we extend or customize like other Liferay OOTB features? and not to recreate the entire Liferay Forms Application.

The starting point of the thought process is to identify the singular modular entity or functionality that we would like to build the framework around. In our case we have just one such entity which is the CustomFormField. Our application is nothing but a collection of Field Types, we render all the field types as displayed above. Ideally once the framework is created for this entity, we should ideally be able to customize and extend the custom form field types as with any other Liferay OOTB feature.

Note: In case if many entities are involved, the relationship between them is determined as per the requirement. 

Since we have identified the singular entity for which we would like to implement a framework.(Frankly its just OSGI Service Registry and Service Tracker in action, other design patterns can be introduced to address any software design problems identified during design phase).

We proceed by implementing the below steps:

  1. Create a service contract by defining an interface. (Custom Form Field Type)
  2. Implement a base version of the interface. (Optional, we will see why)
  3. Create multiple field types by providing implementation to the service created earlier.
  4. Create a service tracker utility to track all the registered service.

We focus only on the perspective of implementing a framework and not in implementing the contract for the interface or the render mechanisms. Source code is available at the end of the blog post.

Step-1: Create a service contract by defining an interface for CustomFormElement

 

package com.liferay.custom.forms.api; 

public interface CustomFormElement 

{ 
    // Renders the actual form field type public 
    String renderFormElement(); 

    // Icon to represent the form field type
    public String getIcon(); 

    // Just a custom flag to display a badge in the UI 
    public default boolean isOutOfTheBox() { return Boolean.FALSE; } 

    //Name of the custom form field type 
    public String getCustomFormElementName(); 

    //Description of the custom form field type 
    public String getCustomFormElementDescription(); 

    //Inject a javascript functionality 
    public String loadJavascriptConfiguration(); 

}

Step-2: Implement a base version of the interface. (Optional)

The optional step can help us to achieve abstraction if required at all. Practical use cases such as enforcing a particular behavior for all the components which will be created for the service declared, can be implemented over here.

In our use case, a placeholder is kept to inject a JavaScript function call and to set a default icon to all the implementations provided for the service CustomFieldElement. So in case if a CustomFieldType does not have an icon declared, the default icon will be picked up from the base implementation BaseCustomFormElement

package com.liferay.custom.forms.impl;

import com.liferay.custom.forms.api.CustomFormElement;

public abstract class BaseCustomFormElement implements CustomFormElement {

    @Override
    public String loadJavascriptConfiguration() 
    {
        return "print-console-log";
    }

    @Override
    public String getIcon() 
    {
        return "container";
    }

}
Note: This is not defined as a OSGI Component as it does not provide a full fledged implementation of the Service in focus. This just provides a layer of Abstraction.

Step-3 Create multiple field types by providing implementation to the service created earlier

As per the application screenshot shared earlier, we have implemented two field types Input Text and Button (files can be found in the source code shared at the end). But the focus has to be on the component configuration implemented, this is where all the magic and fun kicks in. We can also consider the Input Text and the Button as the out of the box implementation inside our custom application.

 @Component(
          immediate = true,
          property = {

            "custom.form.field.type.service.key=input-text",
            "custom.form.field.type.service.ranking=0",
            "custom.form.field.type.icon=embed",
            "custom.form.field.type.name=Input Text",
            "custom.form.field.type.description=This is a input text field in my custom form app. Use it wisely."
          },
          service = CustomFormElement.class
        )

public class TextBoxCustomFormFieldElement extends BaseCustomFormElement 

This registers the implementation for the service in the OSGI Service Registry because of the service assignment in the configuration. Extending the BaseImplementation (BaseCustomFormElement) enables the abstracted implementation to be enforced.

 

Step-4 Create a service tracker utility to track all the registered service

All the above steps have focused on establishing a contract and fulfilling the contract. By end of step-3 our OSGI container registry will have all the service implementations registered for the service CustomFormField. In our case its Input Text and Button as per the screenshot shared earlier, they are indeed considered as the OOTB feature of our custom application as well.

The view.jsp utilizes a util function which implements a Service Tracker which tracks all the services registered in the OSGI Service Registry. The render logic is implemented to iterate all the services fetched by the Service Tracker and print their implementation details in a understandable format.

package com.liferay.custom.forms.util;

import com.liferay.custom.forms.api.CustomFormElement;

import java.util.LinkedHashMap;
import java.util.SortedMap;
import java.util.TreeMap;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceReference;
import org.osgi.util.tracker.ServiceTracker;
import org.osgi.util.tracker.ServiceTrackerCustomizer;

public class CustomFormFieldServiceTrackerUtil extends ServiceTracker<CustomFormElement, CustomFormElement> {

    
    public CustomFormFieldServiceTrackerUtil(BundleContext context, Class<CustomFormElement> clazz,
            ServiceTrackerCustomizer<CustomFormElement, CustomFormElement> customizer) {
        super(context, clazz, customizer);
    }

    public static SortedMap<String, CustomFormElement> getRegisteredCustomFieldMap() {

        SortedMap<String, CustomFormElement> registeredServiceList = new TreeMap<String, CustomFormElement>();
        LinkedHashMap<String,Integer> serviceRankingMap = new LinkedHashMap<>();
        Bundle bundle = FrameworkUtil.getBundle(CustomFormFieldServiceTrackerUtil.class);
        CustomFormFieldServiceTrackerUtil _customFormFieldServiceTracker = new CustomFormFieldServiceTrackerUtil(
                bundle.getBundleContext(), CustomFormElement.class, null);
        _customFormFieldServiceTracker.open();
        SortedMap<ServiceReference<CustomFormElement>, CustomFormElement> _trackedServiceList = _customFormFieldServiceTracker
                .getTracked();
        _trackedServiceList.forEach((_serviceReference, _customFormElement) -> {
            String customFormElementKey = _serviceReference.getProperty("custom.form.field.type.service.key").toString();
            int serviceRanking = Integer.parseInt(_serviceReference.getProperty("custom.form.field.type.service.ranking").toString());
            
            
            
            if(serviceRanking == 0)
            {
                registeredServiceList.put(customFormElementKey,_customFormElement);
                serviceRankingMap.put(customFormElementKey,serviceRanking);
            }
            else if(serviceRanking > serviceRankingMap.get(customFormElementKey))
            {
                registeredServiceList.put(customFormElementKey,_customFormElement);
                serviceRankingMap.put(customFormElementKey,serviceRanking);
                                
            }       
        });

        _customFormFieldServiceTracker.close();
        return registeredServiceList;

    }  
}

 

That's it!!!, we have created a custom framework which now supports extension and customization like any other Liferay OOTB feature. But how?

 

Understanding Our Implementation

Let's do a 'not so very deep dive' into our implementation.

Step-1: Declaring an interface enabled us with few perspectives, ability to provide multiple implementations to the contract declared via OSGI components. Which can also be considered as a case of factory pattern. Hence this paves way for the ability to extend our entity, we can implement/extend new feature/field types just by providing a component implementation to the interface as we did in Step-3

Step-2: Providing a base implementations is pretty straight forward, as mentioned earlier we can utilize this layer of code to enforce business logic's, load default configurations etc. This is how exactly Liferay utilizes abstraction in all the Base implementations we can find in Service Builder generated class files. (Have a look at them, if you haven't till now.)

Step-3: The component configuration implemented enables us to register our implementations in the OSGI Service registry to be later tracked by the Service Tracker. But a prep for enabling customization of existing feature, in our case our local OOTB field types input text and button is done as part of the component configuration, yes the service rank property defined in the component configuration.

 "custom.form.field.type.service.ranking=0",

Step-4: The Service Tracker is what helps us to complete the cycle that started, we define a contract -> implement components (OSGI Services) -> Track them using the service tracker. The service implementation fetched by the tracker can be utilized to view the implementations. But again for the last time lets focus on the conditional block below. This is the block which enables us from customizing any existing feature based on the service ranking.

So the service tracker is coded to pick up the service implementation with the highest service ranking as it is with any other OOTB feature as well.

if(serviceRanking == 0)
{
    registeredServiceList.put(customFormElementKey,_customFormElement);
    serviceRankingMap.put(customFormElementKey,serviceRanking);

}

else if(serviceRanking > serviceRankingMap.get(customFormElementKey))
{
    registeredServiceList.put(customFormElementKey,_customFormElement);
    serviceRankingMap.put(customFormElementKey,serviceRanking);
}


What Next?

We have a custom framework in implemented based on OSGI Service Registry, Whiteboard, Factory and Abstraction for our custom forms application. Based on our requirement in hand we can combine other design patterns along with this such as Adapter pattern (Liferay forms uses this in their data adapters).

Since we have a custom framework in place, we can write our own document for anyone to customize or extend our custom forms application. (Similar to Liferay Documentation)

How to add a new form field type:

  1. Create a class to extend BaseCustomFormField​​​​​
  2. Add Component configuration

    ​​​​​​​@Component(
              immediate = true,
              property = {
                    ​​​​​​​"custom.form.field.type.service.key=**field key**",
                    "custom.form.field.type.service.ranking=0",
                    "custom.form.field.type.icon=**clay-icon-name**",
                    "custom.form.field.type.name=**Name of the form field**",
                    "custom.form.field.type.description=**Description**"
              },
              service = CustomFormElement.class
            )                  

Example:


Image-2: A new form field type has been added following the above mentioned steps.


 

How to customize existing form field type: (Any OOTB Feature)

  1.  Create a class to extend BaseCustomFormField
  2. Add Component configuration with two conditions for achieving customization:
    The service key has to match the key of the service to be customized.
    The service ranking has to be a value higher than 0. If multiple services are there, the implementation with the highest service ranking value will be considered.
  3. Component Configuration:
     
    	@Component( immediate = true, property = 
    	    {
        	    "custom.form.field.type.service.key=**field key of the item to be customized**",
            	"custom.form.field.type.service.ranking=100",
            	"custom.form.field.type.icon=**clay-icon-name**",
            	"custom.form.field.type.name=**Name of the form field**",
            	"custom.form.field.type.description=**Description**"
        	},
    
    	service = CustomFormElement.class )

 
Example:


Image-3: Local input field OOTB has been customized by following the above mentioned steps

 

GoGo Shell Component List


Image-4: Gogo shell console depicting the list of services registered and the info of the customized component (Input Box with Service ranking 100)

By understanding how Liferay utilizes OSGI Framework and other patterns we were able to create our own Custom Liferay Framework. The implementation we did is a very small scale and simple version, this can be amplified and designed as per the requirement in hand.

Source Code

Link to source code - Custom Forms Module discussed in this blog.