Writing a Form Storage Adapter
Available: Liferay DXP 7.3 and Liferay DXP 7.2 versions that include the fix for LPS-97208 (planned for Liferay DXP 7.2 SP3).
By default, forms are stored as JSON in Liferay DXP’s database. This example shows you how to implement a new storage adapter to inject custom logic into a form record persistence event.
First you’ll see how the default storage adapter saves form records in the Liferay DXP database as JSON content. Then you’ll add logic to store each Form Record on the file system.
Examine a Running DDM Storage Adapter
To see how storage adapters work, deploy an example and then add some form data using the example adapter.
Deploy the Example
Start a new Liferay instance by running
docker run -it -m 8g -p 8080:8080 liferay/portal:7.4.3.120-ga120
Sign in to Liferay at http://localhost:8080. Use the email address test@liferay.com and the password test. When prompted, change the password to learn.
Then, follow these steps:
-
Download and unzip the DDM Storage Adapter project.
curl https://resources.learn.liferay.com/dxp/latest/en/process-automation/forms/developer-guide/liferay-r2f1.zip -O
unzip liferay-r2f1.zip
-
From the module root, build and deploy.
./gradlew deploy -Ddeploy.docker.container.id=$(docker ps -lq)
tipThis command is the same as copying the deployed jars to /opt/liferay/osgi/modules on the Docker container.
-
Confirm the deployment in the Liferay Docker container console.
STARTED com.acme.r2f1.impl_1.0.0 [1009]
Use the Deployed Storage Adapter
-
Open your browser to http://localhost:8080.
-
Go to the Forms application in Site Menu → Content & Data → Forms.
-
Click the Add button () to open the Form Builder.
-
In the Form Builder view, click the Options button () and open the Settings window.
-
Under Select a Storage Type, choose the R2F1 Dynamic Data Mapping Storage Adapter type and click Done.
-
Add a Text Field to the form, publish the form, and submit it a few times.
-
To verify the form data were persisted, go to the Form’s Records:
From Site Menu → Content → Forms, click the Form’s Actions button (), then View Entries.
-
Additionally, logging is provided in each CRUD method to demonstrate that the sample’s methods are being invoked.
WARN [http-nio-8080-exec-5][R2F1DDMStorageAdapter:82] Acme storage adapter's save method was invoked
Understand the Extension Point
The example contains only one class: R2F1DDMStorageAdapter
, a service implementing a DDMStorageAdapter
to provide logic for storing Form Entries. The deployed example currently just wraps the default JSON implementation: DefaultDDMStorageAdapter
. Later, you’ll add file system storage to the code that’s already here.
Register the Adapter Class with the OSGi Container
The DDMFileSystemStorageAdapter
implements the DDMStorageAdapter
interface, but must be registered as an OSGi service:
@Component(
property = "ddm.storage.adapter.type=r2f1-ddm-storage-adapter",
service = DDMStorageAdapter.class
)
public class R2F1DDMStorageAdapter implements DDMStorageAdapter {
The r2f1-ddm-storage-adapter
key is localized into the value R2F1 Dynamic Data Mapping Storage Adapter
by the src/main/resources/content/Language.properties
file and the Provide-Capability
header in the bnd.bnd
file.
The service
component property registers your implementation as a DDMStorageAdapter
service.
The property ddm.storage.adapter.type
provides an identifier so that your service is registered as a unique DDMStorageAdapter
implementation. Other services can now reference it like this:
@Reference(target = "(ddm.storage.adapter.type=r2f1-ddm-storage-adapter)")
private DDMStorageAdapter defaultWrapperDDMStorageAdapter;
Understand the DDMStorageAdapter Interface
The interface requires three methods to handle CRUD operations on form records: delete
, get
, and save
(which also handles update logic).
public DDMStorageAdapterDeleteResponse delete(
DDMStorageAdapterDeleteRequest ddmStorageAdapterDeleteRequest)
throws StorageException;
public DDMStorageAdapterGetResponse get(
DDMStorageAdapterGetRequest ddmStorageAdapterGetRequest)
throws StorageException;
public DDMStorageAdapterSaveResponse save(
DDMStorageAdapterSaveRequest ddmStorageAdapterSaveRequest)
throws StorageException;
Each method must return a DDMStorageAdapter[Save/Get/Delete]Response object, constructed using a static inner Builder
class’s newBuilder
method.
All methods are passed a DDMStorageAdapter[Save/Delete/Get]Request
. The request objects contain getter methods that return useful contextual information.
Implement File System Storage
The example already overrides the necessary methods. You’ll create private utility methods for your functionality and then call them from the overridden methods.
Declare the Service Dependencies
This code relies on two services deployed to an OSGi container. Add these declarations at the end of the class using Declarative Services @Reference
annotations, provided by org.osgi.service.component.annotations.Reference
.
@Reference
private DDMContentLocalService _ddmContentLocalService;
@Reference
private DDMFormValuesSerializerTracker _ddmFormValuesSerializerTracker;
Import com.liferay.dynamic.data.mapping.service.DDMContentLocalService
and com.liferay.dynamic.data.mapping.io.DDMFormValuesSerializerTracker
.
Create a Logger
Create a logger for the class and set it in a _log
variable:
private static final Log _log = LogFactoryUtil.getLog(
R2F1DDMStorageAdapter.class);
This will be used to add some log messages each time one of the CRUD methods is invoked.
Implement File Deletion
-
Set a private variable
_PATHNAME
so you can control where the files are stored. The path here points to the Liferay install location in the Docker container.private static final String _PATHNAME = "/opt/liferay/form-records";
-
Create a
_deleteFile
utility method (import thejava.io.File
class):private void _deleteFile(long fileId) { File file = new File(_PATHNAME + "/" + fileId); file.delete(); if (_log.isWarnEnabled()) { _log.warn("Deleted file with the ID " + fileId); } }
-
Find the overridden
delete
method. Immediately before thereturn
statement, addlong fileId = ddmStorageAdapterDeleteRequest.getPrimaryKey(); _deleteFile(fileId);
Now the code first deletes the file from the file system before it deletes the copy in the database.
Implement File Retrieval
You’ll follow the same procedure for the get
method: create a private utility method and then call it.
-
Add the
_getFile
utility method:private void _getFile(long fileId) throws IOException { try { if (_log.isWarnEnabled()) { _log.warn( "Reading the file with the ID " + fileId + ": " + FileUtil.read(_PATHNAME + "/" + fileId)); } } catch (IOException e) { throw new IOException(e); } }
Import
com.liferay.portal.kernel.util.FileUtil
andjava.io.IOException
. -
In the overridden
get
method (inside thetry
block), insert the following immediately before thereturn
statement, setting thestorageId
(retrieved byddmStorageAdapterGetRequest.getPrimaryKey()
) as thefileId
and calling the_getFile
utility method which prints the retrieved content to the Liferay log.long fileId = ddmStorageAdapterGetRequest.getPrimaryKey(); _getFile(fileId);
Implement File Creation Logic
There are two types of save requests: 1) a new record is added or 2) an existing record is updated. At each save, you’ll have the update
method overwrite the existing file, using the current ddmFormValues
content.
-
Create a
_saveFile
utility method:private void _saveFile(long fileId, DDMFormValues formValues) throws IOException { try { String serializedDDMFormValues = _serialize(formValues); File abstractFile = new File(String.valueOf(fileId)); FileUtil.write( _PATHNAME, abstractFile.getName(), serializedDDMFormValues); if (_log.isWarnEnabled()) { _log.warn("Saved a file with the ID" + fileId); } } catch (IOException e) { throw new IOException(e); } }
Import
com.liferay.dynamic.data.mapping.storage.DDMFormValues
andjava.io.File
. -
Create a
_serialize
utility method to convert theDDMFormValues
object to JSON:private String _serialize(DDMFormValues ddmFormValues) { DDMFormValuesSerializer ddmFormValuesSerializer = _ddmFormValuesSerializerTracker.getDDMFormValuesSerializer("json"); DDMFormValuesSerializerSerializeRequest.Builder builder = DDMFormValuesSerializerSerializeRequest.Builder.newBuilder( ddmFormValues); DDMFormValuesSerializerSerializeResponse ddmFormValuesSerializerSerializeResponse = ddmFormValuesSerializer.serialize(builder.build()); return ddmFormValuesSerializerSerializeResponse.getContent(); }
Import
com.liferay.dynamic.data.mapping.io.DDMFormValuesSerializer
,com.liferay.dynamic.data.mapping.io.DDMFormValuesSerializerSerializeRequest
, andcom.liferay.dynamic.data.mapping.io.DDMFormValuesSerializerSerializeResponse
. -
Add this logic and the call to
_saveFile
to thesave
method by replacing the existingreturn
statement:DDMStorageAdapterSaveResponse defaultStorageAdapterSaveResponse = _defaultStorageAdapter.save(ddmStorageAdapterSaveRequest); long fileId = defaultStorageAdapterSaveResponse.getPrimaryKey(); _saveFile(fileId, ddmStorageAdapterSaveRequest.getDDMFormValues()); return defaultStorageAdapterSaveResponse;
The
_defaultStorageAdapter.save
call is made first, so that a Primary Key is created for a new form entry. This Primary Key is retrieved from theResponse
object to create thefielId
.
Deploy and Test the Storage Adapter
Use the same deploy
command as earlier to deploy the Storage Adapter. From the module root run
./gradlew deploy -Ddeploy.docker.container.id=$(docker ps -lq)
Now verify that it’s working:
-
Go to the Forms application in Site Menu → Content → Forms.
-
Click the Add button to open the Form Builder.
-
In the Form Builder view, click the Options button () and open the Settings window.
-
From the select list field called Select a Storage Type, choose the R2F1 Dynamic Data Mapping Storage Adapter type and click Done.
-
Add a Text Field to the form, publish the form, and submit it a few times.
-
To verify the form records were written to the container’s file system, check the log. You’ll see messages like:
WARN [http-nio-8080-exec-5][R2F1DDMStorageAdapter:82] Acme storage adapter's save method was invoked WARN [http-nio-8080-exec-5][R2F1DDMStorageAdapter:134] Saved a file with the ID42088 WARN [http-nio-8080-exec-5][R2F1DDMStorageAdapter:61] Acme storage adapter's get method was invoked WARN [http-nio-8080-exec-5][R2F1DDMStorageAdapter:112] Reading the file with the ID 42088: {"availableLanguageIds":["en_US"],"defaultLanguageId":"en_US","fieldValues":[{"instanceId":"EJ5UglA1","name":"Field51665758","value":{"en_US":"Stretched limousine"}}]}
Conclusion
By implementing a DDMStorageAdapter
, you can save forms records in any storage format you want.