Documentation

Writing a Custom Data Provider

Liferay Forms fields can be populated using a Data Provider. Out of the box, there’s a REST Data Provider that provides a flexible way to consume data from most REST endpoints. See Using the REST Data Provider to Populate Form Options to learn more.

If the REST Data Provider doesn’t serve your purpose, use the DDMDataProvider extension point to create your own.

Note

The example Data Provider demonstrated here consumes XML data from the GeoDataSource™ Location Search Web Service. The API key of a Liferay employee is hard-coded into this sample; please do not overuse the sample. Never use it in production environments.

Deploy a Custom Data Provider

  1. Start Liferay.

    docker run -it -m 8g -p 8080:8080 liferay/portal:7.4.3.22-ga22
    
  2. Download and unzip the Acme XML Data Provider.

    curl https://learn.liferay.com/dxp/latest/en/process-automation/forms/developer-guide/liferay-b4d8.zip -O
    
    unzip liferay-b4d8.zip
    
  3. From the module root, build and deploy.

    ./gradlew deploy -Ddeploy.docker.container.id=$(docker ps -lq)
    

    Tip

    This command is the same as copying the deployed jars to /opt/liferay/osgi/modules on the Docker container.

  4. Confirm the deployment of each module in the Liferay Docker container console.

    STARTED com.acme.n4g6.impl_1.0.0
    

Test the Data Provider

To use the data provider in a form,

  1. Add an instance of the Data Provider:

    1. In the Site Menu, go to Content and Data → Forms.

    2. Open the Data Providers tab and click the Add button.

      The custom data provider is ready for use in Liferay Forms.

    3. Configure it:

      • Name: Cites Near Diamond Bar, CA (USA)

      • Description: GeoDataSource Location Search–Fetch the cities within 20 km of Liferay headquarters.

      • Outputs

        • Label: City

        • Path: city

        • Type: List

      Configure the custom data provider by specifying its output.

    4. Click Save.

  2. Add a form that uses the Cities Near Diamond Bar data provider:

    1. In the Site Menu, go to Content and Data → Forms.

    2. In the Forms tab, click the Add button.

    3. Add a Select from List fields with these settings:

      1. Label: Choose a City Near Liferay

      2. Create List: From Data Provider

      3. Choose a Data Provider: Cities Near Diamond Bar, CA (USA)

      4. Choose an Output Parameter: City

    4. Publish the form and verify that the list is populated from the data provider:

    The Data Provider returns a list of cities within 20 km of Liferay.

This is a nice example, but it hard-codes the URL for the data provider. If you allow the URL to be configurable, you can use this same data provider for other cities, or any other URL that serves XML.

Understanding the B4D8 DDM Data Provider

The Acme B4D8 Implementation project contains a custom data provider for returning XML from a specific URL. It contains three classes: B4D8DDMDataProvider, B4D8DDMDataProviderSettingsProovider, and B4D8DDMDataProviderSettings.

Implementing a DDMDataProvider

The data provider class implements the com.liferay.dynamic.data.mapping.data.provider.DDMDataProvider interface, overriding two methods: getData and getSettings. These method names capture the essence of a data provider: it provides data based on settings (settings are optional though).

Implementing the interface’s methods and providing two @Component settings is enough to register the data provider with the Liferay Forms application, so it appears naturally in the forms UI alongside Liferay’s default data provider.

@Component(
	property = "ddm.data.provider.type=b4d8", service = DDMDataProvider.class
)
public class B4D8DDMDataProvider implements DDMDataProvider {
	@Override
	public DDMDataProviderResponse getData(
			DDMDataProviderRequest ddmDataProviderRequest)
		throws DDMDataProviderException {
	}

	@Override
	public Class<?> getSettings() {
	}

The getData method does most of the work. It must return a DDMDaProviderResponse that the Forms framework understands. For the B4D8 data provider, these are the highlights:

  1. The URL to our XML data source is constructed:

    String key = "LAOOBDZVQ5Z9HHYC4OCXHTGZGQLENMNA";
    
    String url =
    	"https://api.geodatasource.com/cities?key=" + key +
    		"&format=xml&lat=33.9977&lng=-117.8145";
    
  2. The _createDDMDataProviderResponse method is called. This is where the construction of the response object happens. To call this method, give it two parameters: the data provider settings and the XML document returned from the remote API. The logic for both is in separate private utility methods. Importantly, HttpUtil.URLtoString(url) is the call that executes the URL to retrieve the XML.

  3. Now the pieces are in place to (based on the output parameter settings of the data provider instance) build the response conditionally. The logic involves

    • Begin building the response using a static inner Builder class’s newBuilder method:

      DDMDataProviderResponse.Builder builder =
      	DDMDataProviderResponse.Builder.newBuilder();
      
    • Loop through the data provider’s output parameter settings. In Test the Data Provider you added only one set of outputs (with three nested fields); if you create a data provider with additional outputs, by clicking the plus button in the data provider settings form, this loop parses each one.

    • For each output, get the XML nodes from the returned XML document, the output parameter ID, and the type of output data requested (in the example above you chose List).

    • Check the output parameter type and call the response builder’s withOutput method. Each call provides the output parameter ID and the content of the matching node (or nodes, if a list is requested).

      for (DDMDataProviderOutputParametersSettings
      		ddmDataProviderOutputParametersSettings :
      			b4d8DDMDataProviderSettings.outputParameters()) {
      
      	NodeList nodeList = document.getElementsByTagName(
      		ddmDataProviderOutputParametersSettings.outputParameterPath());
      	String outputParameterId =
      		ddmDataProviderOutputParametersSettings.outputParameterId();
      	String outputParameterType =
      		ddmDataProviderOutputParametersSettings.outputParameterType();
      
      	if (Objects.equals(outputParameterType, "list")) {
      		List<KeyValuePair> keyValuePairs = new ArrayList<>();
      
      		for (int i = 0; i < nodeList.getLength(); i++) {
      			Node node = nodeList.item(i);
      
      			keyValuePairs.add(
      				new KeyValuePair(
      					node.getTextContent(), node.getTextContent()));
      		}
      
      		builder.withOutput(outputParameterId, keyValuePairs);
      	}
      	else if (Objects.equals(outputParameterType, "number")) {
      		Node node = nodeList.item(0);
      
      		NumberFormat numberFormat = NumberFormat.getInstance();
      
      		builder.withOutput(
      			outputParameterId,
      			numberFormat.parse(node.getTextContent()));
      	}
      	else if (Objects.equals(outputParameterType, "text")) {
      		Node node = nodeList.item(0);
      
      		builder.withOutput(outputParameterId, node.getTextContent());
      	}
      }
      
    • At the end of the method, return the response: return builder.build().

Defining the Settings with DDMDataProviderSettings

The data provider settings class defines the settings that this data provider needs, in two parts:

  1. The layout of the settings form itself is defined using @DDMForm* class-level annotations:

    @DDMForm
    @DDMFormLayout(
    	{
    		@DDMFormLayoutPage(
    			{
    				@DDMFormLayoutRow(
    					{
    						@DDMFormLayoutColumn(
    							size = 12, value = "outputParameters"
    						)
    					}
    				)
    			}
    		)
    	}
    )
    

    Any fields that configure your data provider must be added to the settings form in this @DDMForm. This snippet currently uses only the inherited outputParameters field which is accessible because the B4D8DDMDataProviderSettings class extends DDMDataProviderParameterSettings. See Add Data Provider Settings to learn about adding more settings to the form.

  2. The class declaration and body determines what fields are available. Currently no additional settings are needed, so the class body is blank.

    public interface B4D8DDMDataProviderSettings
    	extends DDMDataProviderParameterSettings {
    }
    

    Note

    In addition to the outputParameters field, an inputParameters field is also provided in DDMDataProviderParameterSettings.

The data provider settings form is ready for work.

The settings form currently contains some default fields needed by all data providers that appear in the Forms UI: Name, Description, and a section for defining its permissions. You get these by adding your settings with the _ddmDataProviderInstanceSettings.getSettings(...) call. The Outputs field is the inherited outputParameters field you added to the layout, which is really a nested field consisting of a Label, Path, and Type.

Implementing the DDMDataProviderSettingsProvider

The settings provider class contains one method, getSettings, which returns the DDMDataProviderSettings class for a given data provider. It’s used to instantiate a settings class in the data provider, so you can get the settings values and configure the data provider accordingly.

Get a reference to the B4D8DDMDataProviderSettingsProvider and then call its getSettings method from the data provider class’s identically named getSettings method:

@Override
public Class<?> getSettings() {
	return _ddmDataProviderSettingsProvider.getSettings();
}

@Reference(target = "(ddm.data.provider.type=b4d8)")
private DDMDataProviderSettingsProvider _ddmDataProviderSettingsProvider;

Add Data Provider Settings

To add a Data Provider Setting, add an annotated field to the DataProviderSettings interface and update the DataProvider class to react to the setting’s value.

Add a URL Field to the Settings

  1. First add the new URL field to the DataProviderSettings. In the class body, add this annotated method:

    @DDMFormField(
        label = "%url", required = true,
        validationErrorMessage = "%please-enter-a-valid-url",
        validationExpression = "isURL(url)"
    )
    public String url();
    

    It requires this import:

    import com.liferay.dynamic.data.mapping.annotations.DDMFormField;
    
  2. In the class-level annotation that creates the form layout, replace the @DDMFormLayoutColumn with

    @DDMFormLayoutColumn(
        size = 12, value = {"url", "outputParameters"}
    )
    

Now the settings are ready to be used in the DataProvider class.

Handle the Setting in the Data Provider’s getData Method

Now the B4D8DDMDataProvider#getData method must be updated:

  • Remove the hard-coded String url variable.

  • Refactor the method to instantiate B4D8DDMDataProviderSettings earlier and retrieve the URL setting.

  • Set the URL into the response.

If you’re making these edits locally, copy the complete try block provided below these descriptive steps:

  1. To make sure you get a valid URL, now that user input is allowed:

    Remove the line defining the key variable—this is now configurable in the URL setting field.

    String key = "LAOOBDZVQ5Z9HHYC4OCXHTGZGQLENMNA";
    
  2. Replace the String variable defining the URL with Http.Options populated by the data provider setting field.

    Http.Options options = new Http.Options();
    
    options.setLocation(b4d8DDMDataProviderSettings.url());
    
  3. Use the new options in place of url in the return statement’s call to _createdDDMDataProviderResponse. Replace the existing return statement.

    return _createDDMDataProviderResponse(
        b4d8DDMDataProviderSettings,
        _toDocument(HttpUtil.URLtoString(options)));
    

The above steps omit the refactoring of the method. To compile and test these steps, overwrite the entire try block in the getData method:

try {
    B4D8DDMDataProviderSettings b4d8DDMDataProviderSettings =
        _ddmDataProviderInstanceSettings.getSettings(
            _getDDMDataProviderInstance(
                ddmDataProviderRequest.getDDMDataProviderId()),
            B4D8DDMDataProviderSettings.class);

    Http.Options options = new Http.Options();

    options.setLocation(b4d8DDMDataProviderSettings.url());

    return _createDDMDataProviderResponse(
        b4d8DDMDataProviderSettings,
        _toDocument(HttpUtil.URLtoString(options)));
}

Import Liferay’s Http class.

import com.liferay.portal.kernel.util.Http;

Now you’re ready to test the update data provider.

Deploy and Test the Updated Data Provider

To use the updated data provider in a form,

  1. From the module root, rebuild and redeploy.

    ./gradlew deploy -Ddeploy.docker.container.id=$(docker ps -lq)
    
  2. Add an instance of the Data Provider:

    • Name: Cites Near Recife, Pernambuco (Brazil)

    • Description: GeoDataSource Location Search–Fetch the cities within 20 km of Liferay’s Brazil office.

    • URL:

      https://api.geodatasource.com/cities?key=LAOOBDZVQ5Z9HHYC4OCXHTGZGQLENMNA&format=xml&lat=-8.0342896&lng=-34.9239708
      
    • Outputs

      • Label: City

      • Path: city

      • Type: List

  3. Add a form that uses the Cities Near Recife data provider:

    1. In the Site Menu, go to Content and Data → Forms.

    2. In the Forms tab, click the Add button.

    3. Add a Select from List fields with these settings:

      1. Label: Choose a City Near Liferay, BR

      2. Create List: From Data Provider

      3. Choose a Data Provider: Cities Near Recife, Pernambuco, (Brazil)

      4. Choose an Output Parameter: City

    4. Publish the form and verify that the list is populated from the data provider:

    The Data Provider returns a list of cities within 20 km of Liferay, Brazil.