Blogs
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:
- It is good to understand the background and purpose of Custom Liferay Framework from Part-1 of this blog series.
- 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):
- We provide an implementation to a specific service.
- We extend the base implementation (Abstraction).
- 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.
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:
- Create a service contract by defining an interface. (Custom Form Field Type)
- Implement a base version of the interface. (Optional, we will see why)
- Create multiple field types by providing implementation to the service created earlier.
- 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"; } }
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.
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:
-
Create a class to extend BaseCustomFormField
-
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)
-
Create a class to extend BaseCustomFormField
-
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. -
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.