« Voltar para Development

Portlet DataHandlers

PortletDataHandlers#

This article describes the implementation and use of the Liferay PortletDataHandler API.

The intent of this API is to provide Liferay portal and third party portlet developers with a useful API for importing and exporting application content to and from Liferay (ideally) in a database agnostic fashion.

RD: database/repository or agnostic of persistence layer in general (or are we shooting for database only?)

The traditional method used for performing this task is to export data directly from the database through some form of export (sql file, or some db specific function). During portal upgrades, there are also db upgrade scripts available for upgrading db the schema.

Neither of these methods address the many use cases which might require portal/portlet data to be imported and/or exported. It is reasonable, at this point, to list several use cases in order to understand the motivation behind the API. Keep in mind that this is not an exhaustive list and may contain numerous omissions which might not be immediately obvious.

  • Content Development Staging: this case involves developing content in a non production portal instance (staging), then somehow exporting that content to the production instance. It might be that both staging/production environments are hosted on the same portal instance.
  • Data Archiving: this case involves storing portal data (in a db agnostic means) as a safeguard against complete system failure, or purely for records keeping.
  • Versioning: this case involves keeping a version of the content after each change is made. This might be to satisfy some change management policy, or as a means of protecting against mistakes, or as a means of maintaining several different content scenarios without requiring complete duplication of staging environment (a.k.a. tagged/candidate versions).

RD: It might be helpful to list the class of data that this API will deal with. As a reader I am already unclear. The name and API suggest that we are talking about only portlet data -- but this seems somewhat inconsistent with the use cases above and I also know LAR files to contain more than just PortletData. I assume this means: Portlet Configuration and not Content (IE Journal) and certainly not Layout/Page configuration User Profile Permissions

API#

The first step in using the API involves creating a class which implements:

 com.liferay.portal.kernel.lar.PortletDataHandler

This interface defines the following methods:

  public PortletDataHandlerControl[] getExportControls()
    throws PortletDataException;

  public PortletDataHandlerControl[] getImportControls()
    throws PortletDataException;

  public String exportData(
      PortletDataContext context, String portletId,
      PortletPreferences prefs)
    throws PortletDataException;

  public PortletPreferences importData(
      PortletDataContext context, String portletId,
      PortletPreferences prefs, String data)
    throws PortletDataException;

The core functionality is drawn from the implementation of the importData()/exportData() methods. These are called when the import/export functions are executed by the user (or invoked by the portal, future feature?).

There are several objects which round out the remainder of the API.

 javax.portlet.PortletPreferences
 com.liferay.portal.kernel.lar.PortletDataContext;
 com.liferay.portal.kernel.lar.PortletDataHandlerControl;
 com.liferay.portal.kernel.zip.ZipReader;
 com.liferay.portal.kernel.zip.ZipWriter;

We'll discuss each below as we traverse the implementation.

Storing Data#

As discussed above, the purpose of this API is to provide a pluggable way to handle the use cases we mentioned above and others, which generally revolve around the concept of storing data outside of the portal permanently or temporarily.

Liferay does this by handling the creation and interpretation of LAR files (Liferay Archives). LAR files are zip archives which contain, by default, a single file called layouts.xml

This xml file contains a whole lot of information about the Community from which it was created. Some of which might be layout permissions, portlet permissions, portlet preferences, etc.

Through the PortletDataHandler API it is also possible to store arbitrary data into this file which allows for solving the use cases above. When the handler is called into action, for example, on export, the returned String data is appended, as a CDATA section, to a new element created for the portlet in question.

Example#

 <portlet-data portlet-id="15"><![CDATA[...]]></portlet-data>

Conversely, on import and while the layouts.xml file is being processed, these portlet-data elements are retrieved and, if a PortletDataHandler implementation is found for the portlet in question (e.g. 15), the data is passed to the importData() method as String data.

It is the responsibility of the handler to format and/or interpret this data as the framework imposes no data/format restriction beyond the fact that it must be of type java.lang.String (which might result from serializing an xml file, or encoding a binary file(s) in BASE64.)

PortletPreferences#

During import/export it may also be desirable to persist information about the process, for example; the date that the process took place.

To achieve this, it is possible to access the PortletPreferences object for the portlet in question. All of the PortletPreferences methods are available during import/export, including store().

Example#

  public String exportData(
        PortletDataContext context, String portletId,
        PortletPreferences prefs)
    ...
    Date now = new Date();
    prefs.setValue("last-export-date", now.toString());
    prefs.store();
    ...
  }
  public PortletPreferences importData(PortletDataContext context,
        String portletId, PortletPreferences prefs, String data)
      throws PortletDataException {
    ...
    _log.info("Exported: " + prefs.getValue("last-export-date", "never"));
    ...
  }

PortletDataHandlerControl: Defining controls for your handler#

It is often desirable to have runtime control over the import/export behavior for your handler. Therefore, the API provides a mechanism for defining controls which are first presented to the user, then passed to the handler at runtime, for use in determining its exact behavior.

The methods used the get the handler controls are getImportControls()/getExportControls()

You'll notice that these methods return an array of PortletDataHandlerControls. Controls are defined in a hierarchical fashion such that any number of related controls can be defined.

There are currently two types of controls provided, out of the box:

 com.liferay.portal.kernel.lar.PortletDataHandlerBoolean
 com.liferay.portal.kernel.lar.PortletDataHandlerChoice

All control objects extend PortletDataHandlerControl. Therefore, adding new control types is a matter of creating other classes which extend it.

'Note://' If you create new control types, you'll also have to implement the rendering logic in the UI. (portal-web/docroot/html/portlet/communities/edit pages.jsp:: renderControls())

PortletDataHandlerBoolean#

The first control, PortletDataHandlerBoolean, essentially defines a control which can be either 'SET' or 'NOT SET'. It can also specify a set of child options, which can come into play when the parent is enabled.

For example, you might define an export option "Enable Data Export" and, once this option is selected, you might have child options to control export format, scope, detail, etc.

The default state for the PortletDataHandlerBoolean control, if not defined, is NOT SET (or false).

sample: insert image

PortletDataHandlerChoice#

The second control, PortletDataHandlerChoice, provides a control which gives the user a choice among a group of pre-defined choices.

For example, you might want to offer several export choices to determine the format data will take in the LAR; e.g. CSV, XML, XLS.

The default selected choice for the PortletDataHandlerChoice control is the first in the list. The default choices are 'false' and 'true', in that order. Meaning that the default selected choice is 'false'.

sample: insert image

Example#

Using the two definitions above, we can provide fairly complex controls for how the import/export process will behave on a given run.

Following is an example from the sample-lar-portlet on how export options might be defined.

 ...
  public PortletDataHandlerControl[] getExportControls()
    throws PortletDataException {
      return new PortletDataHandlerControl[] {_enableExport};
  }
 ...

  private static final String _NAMESPACE="export-sample";
  
  private static final String _CREATE_README = "create-readme";
  
  private static final String _DATA_TYPE = "data-type";

  private static final String _EXPORT_SAMPLE_LAR_DATA =
      "export-sample-lar-portlet-data";

  private static final String _TYPE_CSV = "csv";

  private static final String _TYPE_XML = "xml";

  private static final PortletDataHandlerBoolean _createReadme =
      new PortletDataHandlerBoolean(_CREATE_README, true);

  private static final PortletDataHandlerChoice _dataType =
      new PortletDataHandlerChoice(
          _DATA_TYPE, 1, new String[] {_TYPE_CSV, _TYPE_XML});

  private static final PortletDataHandlerBoolean _enableExport =
      new PortletDataHandlerBoolean(
          _EXPORT_SAMPLE_LAR_DATA, true,
              new PortletDataHandlerControl[] {_createReadme, _dataType});

PortletDataContext#

Access to available resources is provided through a reference to the PortletDataContext which is passed to the handler at runtime.

The 'PortletDataContext' provides access to the following resources:

  • the company id of the current process
  • the group id of the current process
  • a parameter Map containing runtime parameters
  • a reference to the com.liferay.portal.kernel.zip.ZipReader object (null during export)
  • a reference to the com.liferay.portal.kernel.zip.ZipWriter object (null during import)
  • a primaryKey Set used to maintain state across multiple handler invocations

The first two resources are fairly self explanatory and can be used to constrain the scope of your import/export processes.

Accessing the User's Selected Options#

The mechanism to provide additional controls, discussed above, would be of little use if there were no way to retrieve the user's selections. The 'PortletDataContext' provides a 'getParameterMap()' method which returns the parameter map of all the user's selections.

As such, it is possible to obtain the value for any defined control by querying the map for the control name.

PortletDataHandlerBoolean#

In the example above, we had defined a 'PortletDataHandlerBoolean' control:

  private static final PortletDataHandlerBoolean _enableExport =
      new PortletDataHandlerBoolean(
          _EXPORT_SAMPLE_LAR_DATA, true,
          new PortletDataHandlerControl[] {...});

Determining the user's selected value for this control, while defaulting to the control's default value (if not set by the user), can be achieved with the following:

  boolean exportData = MapUtil.getBoolean(
      parameterMap, _EXPORT_SAMPLE_LAR_DATA,
      _enableExport.getDefaultState());
PortletDataHandlerChoice#

If the control type is 'PortletDataHandlerChoice':

  private static final PortletDataHandlerChoice _dataType =
      new PortletDataHandlerChoice(
          _DATA_TYPE, 1, new String[] {_TYPE_CSV, _TYPE_XML});

then determining the user's selected choice can be achieved by the following:

  String dataType = MapUtil.getString(
      parameterMap, _DATA_TYPE, _dataType.getDefaultChoice());

The MapUtil method takes three arguments namely the map, the key and the default value, consider our case the following value needs to be passed to the MapUtil.getX methods,

  • map - the paramaterMap which is fetched form the PortletDataContext
  • key - the NAMESPACE + CONTROL name e.g. for fetching the user selection for DATA_TYPE we it will be _NAMESPACE + _DATA_TYPE
  • defaultValue - the default value associated with the control

Accessing the LAR: Storing files directly in the Zip file#

It is often not a great solution to place binary data inside the LAR's layouts.xml file in String format. In order to provide a better mechanism for dealing with arbitrary data (a.k.a. any kind of file), access to the LAR zipWriter/zipReader itself is available through the 'PortletDataContext'.

Writting files to the LAR#

During export (and only during export) you can obtain access to the LAR ZipWriter by calling 'getZipWriter()' on the 'PortletDataContext' object. This will allow us to write arbitrary files into the LAR.

For example, storing any file into the LAR can be achieved as follows:

  ZipWriter zipWriter = context.getZipWriter();
  StringMaker csv = new StringMaker();
  csv.append("data 1," + new Date() + "\n");
  csv.append("data 2," + new Date() + "\n");
  String filePath = portletId + "/data.csv";
  data = "<data-file>" + filePath + "</data-file>";
  zipWriter.addEntry(filePath, csv.toString());

There are several notable points about this particular fragment, so let us break it down in detail.

Obtain the com.liferay.portal.kernel.zip.ZipWriter from the context:

  ZipWriter zipWriter = context.getZipWriter();

Next, we're going to dynamically create a file for our exported data:

  StringMaker csv = new StringMaker();

We'll add some dummy data (but this would probably come from some call to a service backend).

  csv.append("data 1," + new Date() + "\n");
  ...

Next, we set the path for the file in the LAR. This must be a complete path relative to the root of the zip. Also, we prepend the portletId to the path so that the data files don't accidentally collide with some other 'PortletDataHandlers data which might be using the same file/path names.

String filePath = portletId + "/data.csv"; }}} 

RD: THis is a convention/practice or an enforced rule? -- looks like a convention

Next, we add the path/file to the data returned such that it can be looked up by the handler during import. (If the file names are fixed then this step is not required).

  data = "<data-file>" + filePath + "</data-file>";

You are free to format the above in any way that you like. It would probably be much better to use some DOM Document implementation to dynamically maintain filenames, paths, arbitrary data to the data to be returned, then serialize the DOM document to String.

Finally, add the file to the LAR:

  zipWriter.addEntry(filePath, csv.toString());
Reading files from the LAR#

During import (and only during import) you can obtain access to the LAR ZipReader by calling 'getZipReader()' on the 'PortletDataContext' object. This will allow us to read arbitrary files from the LAR.

For example, reading a file can be achieved as follows:

  ZipReader zipReader = context.getZipReader();
  _log.info("From README file:\n\n" + zipReader.getEntryAsString(portletId + "/README.txt"));

Note: During import we have access to the 'data' we had previously stored in the LAR. So, it would be possible to retrieve the exact (dynamically set) pathnames from there in order to locate relevant files in the LAR.

Returning null from exportData()#

Returning null from a call to exportData() causes the portal's export logic to assume that this handler's export process is to be ignored at this time.

For example, it would be proper to code your exportData() method this:

  public String exportData(...) ... {
    ...
    Map parameterMap = context.getParameterMap();
    boolean exportData = MapUtil.getBoolean(parameterMap, _EXPORT_MY_DATA,_enableExport.getDefaultState());
    if (!exportData) {
      // the user decided not to invoke this handler during the export
      return null;
    }
    // ok, continue with export but store everything using the ZipWriter
    ...
    ...
    // simply return a non null value (e.g. portletId) indicating we actually performed an export action
    return portletId;
  }

The reason why this is required is that if the return value is null, the:

 <portlet-data portlet-id="XXX"><![CDATA[...]]></portlet-data>

will be omitted from the layouts.xml.

RD: I may not know enough about what is really happening here but something smells fishy if you must return an arbitrary value RD: to block a false negative. Perhaps the API is not expressive enough of the caller is making too many assumptions?

The import end of the handler is only called (and associated with a portletId) when a

 <portlet-data portlet-id="XXX"><![CDATA[...]]></portlet-data>

is found in the layouts.xml file.

Configuring the PortletDataHandler implementation#

In order for the portal to call your handler it must be informed of it's existence. This is done by defining the

 <portlet-data-handler-class>...</portlet-data-handler-class>

tag in your portlet definition, within the liferay-portlet.xml or liferay-portlet-ext.xml file.

Here is the configuration for the default Journal PortletDataHandler:

  <portlet>
    <portlet-name>15</portlet-name>
    <struts-path>journal</struts-path>
    ...
    <portlet-data-handler-class>com.liferay.portlet.journal.lar.JournalPortletDataHandlerImpl</portlet-data-handler-class>
    ...
  </portlet>

Once the handler is properly defined and configured it will perform its actions when the import/export LAR functions are executed from the Import/Export tab of a Community or on the Page Settings view.

Localizing#

The PortletDataHandler feature is fully localizable. The keys used for all control names and choice values should be valid property keys and the actually translations should be placed in the portlet's resource bundle (or the struts message bundle, whichever the case may be).

e.g. with the example above, these keys would be placed in the translation files:

  create-readme=Create README file.
  csv=CSV
  data-type=Select data type:
  export-sample-lar-portlet-data=Enable Sample LAR export.
  xml=XML

See Localization of Portlets Outside of Liferay for more details on Localization.

Future Consideration#

With the recent additions to the framework, such as reading/writing to the LAR zip directly, as well as complex PortletDataHandler controls, it is now conceivable to envision many new possibilities with regard to import/export of data.

I offer these ideas for future consideration:

  • Since we can now store files arbitrarily, it is now much easier to craft LAR files by hand and in a human readable fashion.

For example, it might now be possible to create handlers which can import/export formats native to third party applications without having to hack into the portal core. One significant example might be the SCORM packaging format. If we were to implement a SCORM Portlet, it might be possible to natively support SCORM packages with an associated PortletDataHandler.

  • It might be a good idea to develop handlers for certain standards formats like SiteMap, or perhaps even docbook.
  • In order to truly make this a successful feature, it must support some level of inter-version compatibility, such that LARs for older version can be imported into newer versions of the portal. Conversely, it would be a good idea to have certain fixed backward compatibility levels (a.k.a. Export for 3., Export for 4.).

::Rdanner|Rdanner: Excellent thought! is the lar descriptive enough in declaration or format to store variations, does the handler receive the version from the framework :::Rotty|Rotty: Good point. I'll have to add the portal version info to the PortletDataContext. ::Rdanner|Rdanner: to enable it to respond to this need -- or do you envision separate classes handling this need? :::Rotty|Rotty: I would imagine that this would require having some version specific code that would perform the appropriate mapping.

  • We now allow themes to be stored in the LAR. It might be useful to also store other 'Liferay plugin' enabled artifacts, like hot deployable portlets, layout templates, LARs (recursion?).
0 Anexos
31736 Visualizações
Média (3 Votos)
A média da avaliação é 3.33333333333333 estrelas de 5.
Comentários