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.


