Liferay Dynamic Data List is a powerful tool to create simple CRUD applications without writing any code. But it is not powerful enough to create a real dynamic application, because simple CRUD actions do not contain business logic. For example, if we want to execute some custom code before/after adding or updating a record and perhaps want to make the custom code configurable for the end user, Liferay does not support these use cases out of the box.
The whole idea is:
- Let the user provide some JavaScript functions to implement the custom logic.
- Save these functions somewhere as a definition of the dynamic data list, for example, in “description” field.
- Override existing Liferay service with a hook.
- Inside hook, use Java Scripting API to call the configured JavaScript functions, like:
// create a script engine manager
ScriptEngineManager factory = new ScriptEngineManager();
// create a JavaScript engine
ScriptEngine engine = factory.getEngineByName("JavaScript");
// evaluate JavaScript code from String
engine.eval("print('Hello, World')");
Here is an example:
Scenario: we want to create a dynamic form, it has “User ID” and “User Name” fields. The form has a custom logic, when the user is adding a new record, we are suppose to replace an empty user name with “Steve Jobs”. If the user is updating a record, we also have to replace an empty user name with “Tim Cook”. The most important part is, this custom logic should be configurable for the end user.
Solution:
- Configure a dynamic data list, it has two fields, field labels are “User ID” and “User Name”. Corresponding field names are “id” and “name”; Here is the “Data Definitions” screen:
- Let’s use the description field to save our configurable custom logic. Input this code in the “description” field and save it.
function beforeAdd(fields) {
if(fields.get("name").getValue().isEmpty()) {
fields.get("name").setValue("Steve Jobs");
}
}
function beforeUpdate(fields) {
if(fields.get("name").getValue().isEmpty()) {
fields.get("name").setValue("Tim Cook");
}
}
Yes, this is a piece of JavaScript code. We are going to call those two JavaScript functions from Java. - Create a Liferay hook, overwrite a service com.liferay.portlet.dynamicdatalists.service.DDLRecordService with a class named DDLRecordServiceImplHook. The new class (DDLRecordServiceImplHook ) extends com.liferay.portlet.dynamicdatalists.service.DDLRecordServiceWrapper.
- Create an interface named IScriptAction in the hook project, it defines two action methods:
public void beforeAdd(com.liferay.portlet.dynamicdatamapping.storage.Fields fields);
public void beforeUpdate(com.liferay.portlet.dynamicdatamapping.storage.Fields fields);
The JavaScript provided by the user has to “implement” this interface. That is why we have two JavaScript functions (beforeAdd and beforeUpdate) in the configured JavaScript Code. - In DDLRecordServiceImplHook class, overwrite these two methods:
//updateRecord method//addRecord method
public com.liferay.portlet.dynamicdatalists.model.DDLRecord addRecord(long groupId, long recordSetId, int displayIndex,
com.liferay.portlet.dynamicdatamapping.storage.Fields fields, com.liferay.portal.service.ServiceContext serviceContext)
throws com.liferay.portal.kernel.exception.PortalException, com.liferay.portal.kernel.exception.SystemException;
public com.liferay.portlet.dynamicdatalists.model.DDLRecord updateRecord(long recordId, boolean majorVersion, int displayIndex,
com.liferay.portlet.dynamicdatamapping.storage.Fields fields, boolean mergeFields, com.liferay.portal.service.ServiceContext serviceContext)
throws com.liferay.portal.kernel.exception.PortalException, com.liferay.portal.kernel.exception.SystemException; - In the addRecord method, add these lines of code before calling super class’s addRecord method:
//Get record set
DDLRecordSet recordSet = DDLRecordSetServiceUtil.getRecordSet(ParamUtil.getLong(serviceContext, "recordSetId"));
//Get configured script from “Description”
String script = recordSet.getDDMStructure().getDescriptionCurrentValue();
if (!script.isEmpty()) {
ScriptEngine engine = new ScriptEngineManager().getEngineByName("javascript");
engine.eval(script);
if (engine instanceof javax.script.Invocable) {
IScriptAction scriptAction = ((Invocable) engine).getInterface(IScriptAction.class);
scriptAction.beforeAdd(fields);
}
} - Make corresponding changes to the updateRecord method, call scriptAction.beforeUpdate method before calling super class’s updateRecord method.
- Deploy the hood and test it.
Advantage:
- Custom logic can be configured by the end user dynamically.
- Java Scripting API supports many script languages like Ruby, Groovy, and JavaScript and etc. User has enough option to implement their dynamic logic.
- No need to add any extra library.
- Script can interact with java objects, java can call script, and the script can call java. Script can modify java object parameters.
Disadvantage:
- End user has to be able to implement the custom logic with script languages.
- The configured JavaScript code may have security issue.