Liferay 7 Development, Part 3

Introduction

In part 2 of the series we created our initial project modules for the Filesystem Access Portlet project.

In part 3, we're going to move on to flushing out the DS service that will provide a service layer for our portlet.

Admittedly this service layer is contrived; while building out the initial version of this portlet there was no separate service layer, no external modules, everything was implemented directly in the portlet itself.  The portlet worked just fine.

But that doesn't make a great example of how to build things for Liferay 7 so I refactored out a service tier.

The Service Tier

No, we're not building with ServiceBuilder (even though it is still supported under Liferay 7) as our service layer has no database component.

No, we're not building a service tier using the Fake Entity technique for ServiceBuilder, that's not really necessary anymore in the new Liferay 7 OSGi world.

We're building a straight DS service.  In OSGi, DS is an acronym for Declarative Services.  I don't want to do a deep dive into what DS is (especially since there are already some great ones out there like http://blog.vogella.com/2016/06/21/getting-started-with-osgi-declarative-services/), but suffice it to say we're building out a module to define a service (the filesystem-access-api module) along with an implementation module (the filesystem-access-svc module) and we're also going to have a service consumer (the filesystem-access-web module). DS is the runtime glue that will bring these three modules together.

With DS we get new capabilities in Liferay to define independent services and apis that are not database related.  So you get the chance to build out your own entities, build services to operate on those entities and modularize your code to separate these concerns.  And DS also supports wiring of components at runtime without having to implement tedious ServiceTrackers or other more complex constructs.

The Data Transfer Object

So our DTO is actually going to be quite simple.  We need a container for some filesystem data so we can pass complex data around.

The container poses a question - should we define our container as a class or as an interface.  Either works, but my own recommendation is:

  1. If the container can be a simple POJO w/o any business logic, then use a class.
  2. If the container includes business logic, then use an interface.

When you're just passing data around, a POJO class is a great container.  But as soon as you start building out some methods that have business logic code, you're better off using an interface if only to hide the implementation details from your service API consumers.

Because I've already implemented the code, I know that a simple POJO container is not going to work for what I'll be implementing, so we're going to use an interface for our container item.

In the filesystem-access-api module, add a com.liferay.filesystemaccess.api.FilesystemItem interface.  I'm not going to list all of the methods here, but basically it's going to look something like:

/**
 * class FilesystemItem: This is a container for an individual filesystem item.
 *
 * @author dnebinger
 */
public interface FilesystemItem extends Serializable {

	/**
	 * getAbsolutePath: Returns the filesystem absolute path to the item.
	 * @return String The absolute path.
	 */
	public String getAbsolutePath();

	/**
	 * getData: Returns the byte data from the file.
	 * @return byte array of file data.
	 * @throws IOException in case of read failure.
	 */
	public byte[] getData() throws IOException;

	/**
	 * getDataStream: Returns an InputStream for the file data.
	 * @return InputStream The data input stream.
	 * @throws FileNotFoundException in case of read error.
	 */
	public InputStream getDataStream() throws FileNotFoundException;

	...
}

Notice how we can return complex objects, unlike how we are limited in ServiceBuilder code.  Pretty cool, huh?

So remember how I want to use an interface if there is business logic in play?  The getDataStream() method will have business logic in it for returning an input stream for the file, so an interface is the best route.

The Service

The service is going to be a basic interface listing out all of our methods.  We'll add these to the com.liferay.filesystem.api.FilesystemAccessService class that we created in part 2.  The interface we'll be implementing is:

/**
 * class FilesystemAccessService: The service interface for the filesystem
 * access.
 *
 * NOTE: The methods below refer to the *root path*.  The portlet will allow
 * an admin to constrain access to a given, fixed path.  This is the root path.
 * Whatever the user attempts, they will always be constrained within the root
 * path.
 *
 * @author dnebinger
 */
public interface FilesystemAccessService {

	/**
	 * countFilesystemItems: Counts the items at the given path.
	 * @param rootPath The root path.
	 * @param localPath The local path to the directory.
	 * @return int The count of items at the given path.
	 */
	public int countFilesystemItems(
		final String rootPath, final String localPath);

	/**
	 * createFilesystemItem: Creates a new filesystem item at the given path.
	 * @param rootPath The root path.
	 * @param localPath The local path for the new item.
	 * @param name The name for the new item.
	 * @param directory Flag indicating create a directory or a file.
	 * @return FilesystemItem The new item.
	 * @throws PortalException in case of error.
	 */
	public FilesystemItem createFilesystemItem(
			final String rootPath, final String localPath, final String name,
			final boolean directory)
		throws PortalException;

	/**
	 * deleteFilesystemItem: Deletes the target item.
	 * @param rootPath String The root path.
	 * @param localPath The local path to the item to delete.
	 * @throws PortalException In case of delete error.
	 */
	public void deleteFilesystemItem(
			final String rootPath, final String localPath)
		throws PortalException;

	/**
	 * getFilesystemItem: Returns the FilesystemItem at the given path.
	 * @param rootPath The root path.
	 * @param localPath The local path to the item.
	 * @param timeZone The time zone for the access.
	 * @return FilesystemItem The found item.
	 */
	public FilesystemItem getFilesystemItem(
		final String rootPath, final String localPath,
		final TimeZone timeZone);

	/**
	 * getFilesystemItems: Retrieves the list of items at the given path,
	 * includes support for scrolling through the items.
	 * @param rootPath The root path.
	 * @param localPath The local path to the directory.
	 * @param startIdx The starting index of items to return.
	 * @param endIdx The ending index of items to return.
	 * @param timeZone The time zone for the access.
	 * @return List The list of FilesystemItems.
	 */
	public List getFilesystemItems(
		final String rootPath, final String localPath, final int startIdx,
		final int endIdx, final TimeZone timeZone);

	/**
	 * touchFilesystemItem: Touches the target item, updating the modified
	 * timestamp.
	 * @param rootPath The root path.
	 * @param localPath The local path to the item.
	 */
	public void touchFilesystemItem(
		final String rootPath, final String localPath);

	/**
	 * updateContent: Updates the file content.
	 * @param rootPath The root path.
	 * @param localPath The local path to the item to update.
	 * @param content String The new content to write.
	 * @throws PortalException In case of write error.
	 */
	public void updateContent(
			final String rootPath, final String localPath, final String content)
		throws PortalException;

	/**
	 * updateDownloadable: Updates the downloadable flag for the item.  For
	 * directories, they will be downloadable if the total bytes to zip is less
	 * than the given value and contains fiewer than the given number of files.
	 *
	 * These limits allow the administrator to prevent building zip files of
	 * downloads of a directory if building the zip file would overwhelm
	 * available resources.
	 * @param filesystemItem The item to update the downloadable flag on.
	 * @param maxUnzippedSize The max number of bytes to allow for download,
	 *                        for a directory it's the max total unzipped bytes.
	 * @param maxZipFiles The max number of files which can be zipped when
	 *                    checking download of a directory.
	 */
	public void updateDownloadable(
		final FilesystemItem filesystemItem, final long maxUnzippedSize,
		final int maxZipFiles);

	/**
	 * uploadFile: This method handles the upload and store of a file.
	 * @param rootPath The root path.
	 * @param localPath The local path where the file will go.
	 * @param filename The filename for the file.
	 * @param target The uploaded, temporary file.
	 * @param timeZone The user's time zone for modification date display.
	 * @return FilesystemItem The newly loaded filesystem item.
	 * @throws PortalException in case of error.
	 */
	public FilesystemItem uploadFile(
		final String rootPath, final String localPath, final String filename,
		final File target, final TimeZone timeZone) throws PortalException;
}

So this interface allows us to get file items, create and delete them, update the contents, ...  Pretty much everything we need to support for the filesystem access portlet.

Additional Sources

There are some additional files in the com.liferay.filesystemaccess.api package.  These include some exception classes and a constant class.

There is also a com.liferay.filesystemaccess.audit package that contains some classes used for integrating auditing into the portlet.  Since we're exposing the underlying filesystem, we're going to add audit support so we can track who does what.  The classes in this package will help facilitate the auditing.

Configure The Module

So we're not quite finished yet.  We have the code done, but we should configure our bundle so we get the right outcome.

Edit the bundle file so it contains the following:

Bundle-Name: Filesystem Access API
Bundle-SymbolicName: com.liferay.filesystem.access.api
Bundle-Version: 1.0.0
Export-Package:\
    com.liferay.filesystemaccess.api,\
    com.liferay.filesystemaccess.audit
-sources: true

The Liferay standard is to give a meaningful name for the bundle, but the symbolic name should be something akin to the project package.  You definitely want the symbolic name to be unique when it is deployed to the OSGi container.

Since we have created the API, we'll declare the export package(s) for classes that others may consume.

Conclusion

Well the modifications are all done.  At a command prompt, go to the modules/apps/filesystem-access-api directory and execute the following command:

$ ../../../gradlew build

You should end up with your new com.liferay.filesystem.access.api-1.0.0.jar bundle file in the build/libs directory.  If you have a Liferay 7 CE or Liferay DXP tomcat environment running, you can drop the jar into the Liferay deploy folder.

Drop into the Gogo Shell and you can even verify that the module has started:

Welcome to Apache Felix Gogo

g! lb | grep Filesystem
  486|Active     |   10|Filesystem Access API (1.0.0)

So we see that our module deployed and started correctly, so all is good.

In the next part of the blog, we'll go about implementing the service module.