Liferay 7 Development, Part 5

Introduction

In the first four parts we have introduced our project, laid out the Liferay workspace to create our modules, defined our DS service API and have just completed our DS service implementation.

It's now time to move on to starting our Filesystem Access Portlet.  With everything I want to do in this portlet, it's going to span multiple parts.  In this part we're going to start the portlet by tackling some key parts.

Note that this portlet is going to be a standard OSGi module, so we're not building a portlet war file or anything like that.  We're building an OSGi portlet module jar.

Configuration

So configuration is probably an odd place to start but it is a key for portlet design.  This is basically going to define the configurable parts of our portlet.  We're defining fields we'll allow an administrator to set and use that to drive the rest of the portlet.  Personally I've always found it easier to build in the necessary flexibility up front rather than getting down the road on the portlet development and try to retrofit it in later on.

For example, one of our configurable items is what I'm calling the "root path".  The root path is a fixed filesystem path that constrains where the users of the portlet can access.  And this constraint is enforced at all levels, it forms a layer of protection to ensure folks are not creating/editing files outside of this root path.  By starting with this as a configuration point, the rest of the development has to take this into account.  And we've seen this already in the DS API and service presented in the previous parts - every method in the API has the rootPath as the first argument (yes I had my configation parts figured out before I started any of the development).

So let's review our configuration elements:

Item Description
rootPath This is the root path that constrains all filesystem access.
showPermissions This is a flag whether to show permissions or not.  On windows systems, permissions don't really work so this flag can remove the non-functional permissions column.
deletesAllowed This is a flag that determines whether files/folders can be deleted or not.
uploadsAllowed This is a flag that determines whether file uploads are allowed.
downloadsAllowed This is a flag that determines whether file downloads are allowed.
editsAllowed This is a flag that determines whether inline editing is allowed.
addsAllowed This is a flag that determines whether file/folder additions are allowed.
viewSizeLimit This is a size limit that determines whether a file can be viewed in the browser.  This can impose an upper limit on generated HTML fragment size.
downloadableFolderSizeLimit This defines the size limit for downloading folders.  Since folders will be zipped live out of the filesystem, this can be used to ensure server resources are not overwhelmed creating a large zip stream in memory.
downloadableFolderItemLimit This defines the file count limit for downloadable folders.  This too is a mechanism to define an upper limit for server resource consumption.

Seeing this list and understanding how it will affect the interface, it should be pretty clear it's going to be much easier building that into the UI from the start rather than trying to retrofit it in later.

In previous versions of Liferay we would likely be using portlet preferences for these options, but since we're building for Liferay 7 we're going to take advantage of the new Configuration support.

We're going to start by creating a new package in our portlet, com.liferay.filesystemaccess.portlet.config (current Liferay practice is to put the configuration classes into a config package in your portlet project).

There are a bunch of classes that will be used for configuration, let's start with the central one, the configuration definition class FilesystemAccessPortletInstanceConfiguration:

/**
 * class FilesystemAccessPortletInstanceConfiguration: Instance configuration for
 * the portlet configuration.
 * @author dnebinger
 */
@ExtendedObjectClassDefinition(
	category = "platform",
	scope = ExtendedObjectClassDefinition.Scope.PORTLET_INSTANCE
)
@Meta.OCD(
	localization = "content/Language",
	name = "FilesystemAccessPortlet.portlet.instance.configuration.name",
	id = "com.liferay.filesystemaccess.portlet.config.FilesystemAccessPortletInstanceConfiguration"
)
public interface FilesystemAccessPortletInstanceConfiguration {

	/**
	 * rootPath: This is the root path that constrains all filesystem access.
	 */
	@Meta.AD(deflt = "${LIFERAY_HOME}", required = false)
	public String rootPath();

	// snip
}

There's a lot of stuff here, so let's dig in...

The @Meta annotations are from BND and define meta info on the class and the members.  The OCD annotation on the class defines the name of the configuration (using the portlet language bundle) and the ID for the configuration.  The ID is critical and is referenced elsewhere and must be unique across the portal, so the full class name is the current standard.  The AD annotation is used to define information about the individual fields.  We're defining the default values for the parameters and indicating that they are not required (since we have a default).

The @ExtendedObjectClassDefinition is used to define the section of the System Settings configuration panel.  The category (language bundle key) defines the major category the settings will be set from, and the scope defines whether the config is per portlet instance, per group, per company or system-wide.  We're going to use portlet instance scope so different instances can have their own configuration.

The next class is the FilesystemAccessPortletInstanceConfigurationAction class, the class that handles submits when the configuration is changed.  Instead of showing the whole class, I'm only going to show parts of the file that need some discussion.  The whole class is in the project in Github.

/**
 * class FilesystemAccessConfigurationAction: Configuration action for the filesystem access portlet.
 * @author dnebinger
 */
@Component(
	immediate = true,
	property = {
		"javax.portlet.name=" + FilesystemAccessPortletKeys.FILESYSTEM_ACCESS
	},
	service = ConfigurationAction.class
)
public class FilesystemAccessPortletInstanceConfigurationAction
	extends DefaultConfigurationAction {

	/**
	 * getJspPath: Return the path to our configuration jsp file.
	 * @param request The servlet request.
	 * @return String The path
	 */
	@Override
	public String getJspPath(HttpServletRequest request) {
		return "/configuration.jsp";
	}

	/**
	 * processAction: This is used to process the configuration form submission.
	 * @param portletConfig The portlet configuration.
	 * @param actionRequest The action request.
	 * @param actionResponse The action response.
	 * @throws Exception in case of error.
	 */
	@Override
	public void processAction(
		PortletConfig portletConfig, ActionRequest actionRequest,
		ActionResponse actionResponse)
		throws Exception {

		// snip
	}

	/**
	 * setServletContext: Sets the servlet context, use your portlet's bnd.bnd Bundle-SymbolicName value.
	 * @param servletContext The servlet context to use.
	 */
	@Override
	@Reference(
		target = "(osgi.web.symbolicname=com.liferay.filesystemaccess.web)", unbind = "-"
	)
	public void setServletContext(ServletContext servletContext) {
		super.setServletContext(servletContext);
	}
}

So the configuration action handler is actually a DS service.  It's using the @Component annotation and is implementing the ConfigurationAction service.  The parameter is the portlet name (so portlets map the correct configuration action handler).

The class returns it's own path to the JSP file used to show the configuration options.  The path returned is relative to the portlet's web root.

The processAction() method is used to process the values from the configuration form submit.  When you review the code you'll see it is extracting parameter values and saving preference values.

The class uses an OSGi injection using @Reference to inject the servlet context for the portlet.  The important part to note here is that the value must match the Bundle-SymbolicName value from the project's bnd.bnd file.

There are three other source files in this package that I'll describe briefly...

The FilesystemAccessDisplayContext class is a wrapper class to provide access to the configuration instance object in different portlet phases (i.e. Action or Render phases).  In some phases the regular PortletDisplay instance (a new object availble from the ThemeDisplay) can be used to get the instance config object, but in the Action phase the ThemeDisplay is not fully populated so this access fails.  The FilesystemAccessDisplayContext class provides access in all phases.

The FilesystemAccessPortletInstanceConfigurationBeanDeclaration class is a simple component to return the FilesystemAccessPortletInstanceConfiguration class so a configuration instance can be created on demand for new instances.

The FilesystemAccessPortletInstanceConfigurationPidMapping class maps the configuration class (FilesystemAccessPortletInstanceConfiguration) with the portlet id to again support dynamic creation and tracking of configuration instances.

The Portlet Class

Portlet classes are much smaller than what they used to be under Liferay MVC.  Here is the complete portlet class:

/**
 * class FilesystemAccessPortlet: This portlet is used to provide filesystem
 * access.  Allows an administrator to grant access to users to access local
 * filesystem resources, useful in those cases where the user does not have
 * direct OS access.
 *
 * This portlet will provide access to download, upload, view, 'touch' and
 * edit files.
 * @author dnebinger
 */
@Component(
	immediate = true,
	property = {
		"com.liferay.portlet.display-category=category.system.admin",
		"com.liferay.portlet.header-portlet-css=/css/main.css",
		"com.liferay.portlet.instanceable=false",
		"javax.portlet.display-name=Filesystem Access",
		"javax.portlet.init-param.config-template=/configuration.jsp",
		"javax.portlet.init-param.template-path=/",
		"javax.portlet.init-param.view-template=/view.jsp",
		"javax.portlet.resource-bundle=content.Language",
		"javax.portlet.security-role-ref=power-user,user"
	},
	service = Portlet.class
)
public class FilesystemAccessPortlet extends MVCPortlet {
}

It has no body at all.  Can't get any simpler than that...

There is no longer a portlet.xml file or a liferay-portlet.xml file.  Instead, these values are all provided through the properties on the DS component for the portlet.

The Panel App

Our portlet is a regular portlet that admins will be able to drop on any page they want.  However, we're also going to install the portlet as a Panel App, the new way to create a control panel.  We'll do this using the FilesystemAccessPanelApp class:

/**
 * class FilesystemAccessPanelApp: Component which exposes our portlet as a control panel app.
 * @author dnebinger
 */
@Component(
	immediate = true,
	property = {
		"panel.app.order:Integer=750",
		"panel.category.key=" + PanelCategoryKeys.CONTROL_PANEL_CONFIGURATION
	},
	service = PanelApp.class
)
public class FilesystemAccessPanelApp extends BasePanelApp {

	/**
	 * getPortletId: Returns the portlet id that will be in the control panel.
	 * @return String The portlet id.
	 */
	@Override
	public String getPortletId() {
		return FilesystemAccessPortletKeys.FILESYSTEM_ACCESS;
	}

	/**
	 * setPortlet: Injects the portlet into the base class, uses the actual portlet name for the lookup which
	 * also matches the javax.portlet.name value set in the portlet class annotation properties.
	 * @param portlet
	 */
	@Override
	@Reference(
		target = "(javax.portlet.name=" + FilesystemAccessPortletKeys.FILESYSTEM_ACCESS + ")",
		unbind = "-"
	)
	public void setPortlet(Portlet portlet) {
		super.setPortlet(portlet);
	}

}

The @Component annotation shows this is yet another DS service that implements the PanelApp class.  The panel.category.key value will put our portlet under the configuration section of the control panel and the high panel.app.order property will put our portlet near the bottom of the list.

The methods specified will ensure the base class has the Filesystem Access portlet references for the panel app to work.

The JSPs

We will update the init.jsp and add the configuration.jsp files.  Not much to see, pretty generic jsp implementations.  The init.jsp file pulls in all of the includes used in the other jsp files and copies the config into local member fields.  The configuration jsp file has the AUI form for all of the configuration elements.

Configure The Module

Our portlet is still in the process of being flushed out, but we'll wrap up this part of the blog by configuring and deploying the module in it's current form.

Edit the bundle file so it contains the following:

Bundle-Name: Filesystem Access Portlet
Bundle-SymbolicName: com.liferay.filesystemaccess.web

Bundle-Version: 1.0.0

Import-Package:\
	javax.portlet;version='[2.0,3)',\
	javax.servlet;version='[2.5,4)',\
	*

Private-Package: com.liferay.filesystemaccess.portlet
Web-ContextPath: /filesystem-access

-metatype: *

So here we're forcing the import of the portlet and servlet APIs, these ensure that dependencies of our dependencies are included.

We also declare that our portlet classes are all private.  This means that other folks will not be able to use us as a dependency and include our classes.

An important addition is the Web-ContextPath key.  This value is used while the portlet is running to define the context path to portlet resources.  Given the value above, the portal will make our resources available as /o/filesystem-access/..., so for example you could go to /o/filesystem-access/css/main.css to pull the static css file if necessary.

Deployment

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

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

You should end up with your new com.liferay.filesystem.access.web-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
  487|Active     |   10|Filesystem Access Service (1.0.0)
  488|Active     |   10|Filesystem Access API (1.0.0)
  489|Active     |   10|Filesystem Access Portlet (1.0.0)

If they are all active, you're in good shape.

Viewing in the Portal

For the first time in this blog series, we actually have something we can add in the portal.  Log in as an administrator to your portal instance and go to the add panel.  Under the Applications section you should find the System Administration group and in there is our new Filesystem Access portlet.  Grab it and drop it on the page.  You should see it render in the page.

So we haven't really done anything to the main view, but let's test what we did add.  First go to the ... menu and choose the Configuration element.  Although it probably isn't pretty, you should see your configuration panel there.  You can change values, click save, exit and come back in to see that they stick, ...  Basically the configuration should be working.

Next pull up the left panel and go to the Control Panel to the Configuration section.  You should see the Filesystem Access portlet at the bottom of the list (well, position depends upon whatever else you have installed, but in a clean bundle it will be at the bottom).  You can click on the option and you'll get your portlet again, but just the welcome message.  Not very impressive, but we'll get there.

You can also go to the System Settings control panel and you'll see a Platform tab at the top.  When you click on Platform, you should see Filesystem Access.  Click on it for the default configuration settings (used as defaults for new portlet instances).  This configuration will look different than your configuration.jsp because it's not using your configuration, it's a version of the form generated using just the FilesystemAccessPortletInstanceConfiguration class and the information in the Meta annotations to create the form.

Another cool thing you can try, create a com.liferay.filesystemaccess.portlet.config.FilesystemAccessPortletInstanceConfiguration.cfg file in the $LIFERAY_HOME/osgi/modules directory and you can define the default configuration values there.  The values in this file override the defaults from the @Meta annotations and allow you to use a deployable config you can use.

Conclusion

Well, our portlet is not done yet.  We have a good start on it, but we'll have yet more to add in the next part.  Remember the code for the project is up on Github and the code for this part is in the dev-part-5 branch.

In the next part we'll pick up on the portlet development and start building the real views.

6
Blogs
Are OSGi based portlets are compatible with the portlet standard?
And if we want to stay with the standard approach (no osgi api and packaging), how do we access the various services?

Thank you
So OSGi portlet modules do not have a portlet.xml file, instead the values are set using the OSGi annotation on the portlet class so, technically, they do not adhere to the letter of the specification. But in all other ways, they do adhere to the spec.

Liferay still has the Util classes that provide static access to service instances. Even when you do your own LR7 SB modules, you still end up with an API jar that has the static Util classes. So in legacy mode, your service access methods are the same.

Note that when your in this legacy mode, your war files get dropped into the Liferay deploy folder and Liferay will actually rebundle these as WABs and deploy them within the OSGi framework as if you have a "pure" OSGi portlet.

While this is supported, I would encourage you to keep this method only for your existing projects; if you are starting a new project, you will be much better served by picking up the new ways of developing portlets.

I mean, I have no inside information, but would you want to guess if legacy wars will be supported under LR 7.1 or LR 8 or 9 or ...? Maybe they will, I really don't know. I can't see Liferay moving backwards off of OSGi, however.
I understand that OSGi offers certain advantages, especially for the developers of the platform. However from the view of the developer of applications, OSGi brings in added complexity and some important disadvantages I might say.

Firstly, it creates a separate container from the application server. That, makes it an alien environment with respect to the application server and as such stands in the way of the builtin provided JEE services. For example. Can we use the application server CDI services? or the EJB services? Can we use the application server provided transaction management? the application server AOP functionalities? the application server ORM functionalities? I haven't delved much into LR 7 yet, but I guess that that the answer is no.

Even from the perspective of configuration and packaging, I believe that being compliant with the standards is something crucially important for some organizations. The idea that a future Liferay version could drop the support for standards based portlet development is very worrisome at least.
The problem, though, is that the java specifications say nothing about JEE; they don't even require an application container for that matter. The portlet spec talks about it's own requests and responses, it's own session management, etc. The fact that Liferay and others have built their portlet standard compliant container on a regular JEE server is an implementation detail left to the implementors, but it is not a requirement from the portlet specifications themselves.

So the joining of the portlet spec and JEE services is not a requirement, and I don't believe you'll find any commitment from Liferay to ever support or sanction or leverage JEE services in any documentation in the last 5 years or so.

The last time JEE was supported was back under versions 4 (and possibly 5) where the Liferay EE lines were leveraging some J2EE facilities, but that was before the explosion of Hibernate, Spring, Spring Security, etc. that offered similar functionality w/o the J2EE application containers. From 6.0 on, Liferay has been dependent upon only a container implementing the Java Servlet and Java Server Pages specifications.

All of those things said, the OSGi container is running within the Liferay web application that can be deployed to a JEE container. So all of the OSGi code is still under the Liferay web application. Can it therefore leverage the JEE facilities mentioned? Possibly, it make take some research and some glue to make it happen, but I can't say it can't be done.
Thanks for providing these tutorials. As a complete newbie not just to liferay but the whole portal world there are many technologies I need to learn simultaneously. This tutorial has helped make some of the things mentioned in the official LR documentation concrete and this will help bootstrap into the rest of the stack.