Form Tee Storage Types

Send that form data wherever you need to...

Introduction

So recently a couple of times I've seen folks ask how to send form data somewhere. Some want to trigger remote web service calls, some want to store the data in a different entity or database, etc.

The common answer I give is to use a Tee storage type to persist in Liferay while using as you need, and this is sometimes followed by "um, what was that?" ;-)

So I figured it was about time to just explain what the heck I mean here, that way you can find it and use it in your own solutions.

So before diving in, let's explain what happens with the normal form...

Unless you change it, new forms will use the only storage type that ships out of the box with Liferay, the JSON storage type:

In 7.4+, this will be called the Default type, but it will still be the JSON storage type.

Using this storage type, when you save a form entry in the forms UI, this default storage type will encode it as JSON and store it in a clob column in the database (somewhere that you shouldn't be looking ;-) ).

This works out great for forms. The data is persisted, it can be retrieved and used when necessary, ...

But it doesn't work out so well for you if you need to, say, invoke a web service with the form field details.

A Drink with Jam and Bread

I always start by recommending the use of a Tee. And no, it's not a beverage best served hot (or cold), nor was it the same Ti from that song from Sound of Music. A Tee is an old-timey computer command used to duplicate input, sending it to two or more output locations. You can read more about Tees here: https://en.wikipedia.org/wiki/Tee_(command)

I recommend using a Tee with your form data because even though you need to send the data somewhere, you probably also want Liferay to be able to show the table of received values and show a retrieved form sometime later. While it is possible to handle this with your own implementation, do you really want to?

In some cases you might, in which case you'll need to build a complete storage type for your form data. But if you only need to use the form data to trigger a web service call, a Tee implementation will give you the benefits of the Liferay persistence and avoid having to write and maintain your own code.

Writing a Tee Storage Type

If you're still here, then you're interested in seeing my Tee idea come to life, so here it is, a Tee storage type for 7.3:

import com.liferay.dynamic.data.mapping.exception.StorageException;
import com.liferay.dynamic.data.mapping.storage.DDMFormFieldValue;
import com.liferay.dynamic.data.mapping.storage.DDMStorageAdapter;
import com.liferay.dynamic.data.mapping.storage.DDMStorageAdapterDeleteRequest;
import com.liferay.dynamic.data.mapping.storage.DDMStorageAdapterDeleteResponse;
import com.liferay.dynamic.data.mapping.storage.DDMStorageAdapterGetRequest;
import com.liferay.dynamic.data.mapping.storage.DDMStorageAdapterGetResponse;
import com.liferay.dynamic.data.mapping.storage.DDMStorageAdapterSaveRequest;
import com.liferay.dynamic.data.mapping.storage.DDMStorageAdapterSaveResponse;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * class MyDDMStorageAdapter: Custom storage adapter for 7.3+
 *
 * @author dnebinger
 */
@Component(
        immediate = true,
        property = {
                "ddm.storage.adapter.type=custom"
        },
        service = DDMStorageAdapter.class
)
public class MyDDMStorageAdapter implements DDMStorageAdapter {

    @Override
    public DDMStorageAdapterDeleteResponse delete(
            DDMStorageAdapterDeleteRequest ddmStorageAdapterDeleteRequest)
            throws StorageException {
        // perhaps look up the form and do something before the actual form
        // gets deleted out of Liferay.
        
        return _jsonDDMStorageAdapter.delete(ddmStorageAdapterDeleteRequest);
    }

    @Override
    public DDMStorageAdapterGetResponse get(
            DDMStorageAdapterGetRequest ddmStorageAdapterGetRequest)
            throws StorageException {
        DDMStorageAdapterGetResponse response = 
            _jsonDDMStorageAdapter.get(ddmStorageAdapterGetRequest);
        
        // lookup and change the form field values here
        
        return response;
    }

    @Override
    public DDMStorageAdapterSaveResponse save(
            DDMStorageAdapterSaveRequest ddmStorageAdapterSaveRequest)
            throws StorageException {

        Map<String, List<DDMFormFieldValue>> values =
                ddmStorageAdapterSaveRequest.getDDMFormValues()
                        .getDDMFormFieldValuesMap(true);
        Locale locale = 
                ddmStorageAdapterSaveRequest.getDDMFormValues()
                        .getDefaultLocale();

        if (ddmStorageAdapterSaveRequest.isInsert()) {
            // this is an insert of a new form

            // bad implementation to get a field value, but you get the idea
            String fieldValue = values.get("fieldName").get(0).getValue().getString(locale);
            ...
            
            // invoke a web service or whatever with our field values.
            
            // maybe change field values, adding new values or changing existing values
        } else {
            // this is an update to an existing form
            
            // maybe change field values, adding new values or changing existing values
        }

        // fall through to let Liferay do its thing
        return _jsonDDMStorageAdapter.save(ddmStorageAdapterSaveRequest);
    }

    @Reference(unbind = "-", target = "(ddm.storage.adapter.type=json)")
    public void setJSONDDMStorageAdapter(final DDMStorageAdapter jsonDDMStorageAdapter) {
        _jsonDDMStorageAdapter = jsonDDMStorageAdapter;
    }

    private DDMStorageAdapter _jsonDDMStorageAdapter;

    private static final Logger _log = LoggerFactory.getLogger(MyDDMStorageAdapter.class);
}

Now if you're building a solution for an earlier or later version of Liferay, obviously you'll need to make some changes to match up with your version of Liferay, but hopefully this gives you an idea of how to proceed.

So we have a reference to the current JSON storage adapter, the target string will exclude other possible adapters that are available. With this in place, we pass gets and deletes off to it directly for handling.

For saves, we can distinguish between inserts and updates so we can take different courses of action if we wanted, all before passing the save through to the JSON adapter for storage.

Note that it is possible for our custom code here to actually change the form values. We could, for example, set a form field value with a value returned from a web service call, for example if we're given a ticket ID we could set the form field and keep it with the persisted form.

This really opens up options for us, too. If we needed to, during the delete we could retrieve the form, extract out our ticket id, and then perhaps make a web service call to cancel the ticket. For the get method, we could call a web service to retrieve the current ticket status and set that into another form field value.

Really the possibilities here are almost endless.

Conclusion

By leveraging a custom storage type using an output Tee, we get the opportunity to process raw form data before it is persisted. We can invoke web services, we can add or change field values, and we could even throw a StorageException if we didn't want the form to be saved successfully.

We get an opportunity to inject logic around the save, get and delete operations on specific form data, and this custom logic is free to do whatever is necessary.

We also leverage Liferay's existing JSON storage so we can let Liferay do all of the heavy lifting with respect to persistence, leveraging the tested, validated and debugged code Liferay normally uses out of the box.

So wait, is this then the first case of actually being able to have your cake and eating it too?

You tell me...

Blogs

Interesting blog post.Question: Would it be possible to skip the JSON storage adapter completely? I mean would it be possible to just send a mail or what ever with the data and do not save anything in the database? 

I'm asking because we often use the form builder to create simple contact forms. In terms of data privacy it is not required to save information from a contact form to the database. Sending a mail is enough.

Oh, no, the persistence is not necessary at all. Just don't @Reference in the JSON guy and throw away the request objects after you've sent the form.

Me, I like persisting even in this case because I can go to the control panel as an admin and see all of the form submissions, whether I've received the contact emails or not, so it just makes sense to me to Tee them where I need them to go and yet still persist them as Liferay normally would.

But that's really my own preference. If you don't share it, I'm certainly not going to hold it against you ;-)

David, thank you for discovering to us this important function.

With the use of Liferay Forms and Liferay Objects there is any further flexibility to manage the data entered in a Form?

In the tab "Actions" of a Liferay Objects item we can trigger a Webhook with a payload.

What about to use this Webhook instead a  new Tee Storage Type?

Thank you in advance for some light!