Writing a Form Storage Adapter
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.

The default storage adapter saves form records in the Liferay DXP database as JSON content. Next, 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.132-ga132
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/examples/liferay-r2f1.zip -Ounzip 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/moduleson 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 Add (
) to open the Form Builder. -
In the Form Builder view, click Options (
) 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, 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. 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 is used to add some log messages each time one of the CRUD methods is invoked.
Implement File Deletion
-
Set a private variable
_PATHNAMEso 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
_deleteFileutility method (import thejava.io.Fileclass):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
deletemethod. Immediately before thereturnstatement, 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
Follow the same procedure for the get method: create a private utility method and then call it.
-
Add the
_getFileutility 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.FileUtilandjava.io.IOException. -
In the overridden
getmethod (inside thetryblock), insert the following immediately before thereturnstatement, setting thestorageId(retrieved byddmStorageAdapterGetRequest.getPrimaryKey()) as thefileIdand calling the_getFileutility 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, the update method overwrites the existing file, using the current ddmFormValues content.
-
Create a
_saveFileutility 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.DDMFormValuesandjava.io.File. -
Create a
_serializeutility method to convert theDDMFormValuesobject 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
_saveFileto thesavemethod by replacing the existingreturnstatement:DDMStorageAdapterSaveResponse defaultStorageAdapterSaveResponse = _defaultStorageAdapter.save(ddmStorageAdapterSaveRequest); long fileId = defaultStorageAdapterSaveResponse.getPrimaryKey(); _saveFile(fileId, ddmStorageAdapterSaveRequest.getDDMFormValues()); return defaultStorageAdapterSaveResponse;The
_defaultStorageAdapter.savecall is made first, so that a Primary Key is created for a new form entry. This Primary Key is retrieved from theResponseobject 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 Add
to open the Form Builder. -
In the Form Builder view, click Options (
) 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. The messages should look like this:
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.