Blogs

Liferay 7 Development, Part 6

Introduction

In part 5 we started the portlet code.  We added the configuration support, started the portlet and added the PanelApp implementation to get the portlet in the control panel.

In this part we will be adding the view layer into the portlet and add the action handlers.  To complete these parts we'll be layering in the use of our Filesystem Access DS component.

We're also going to take a quick look at Lexicon and what it means for us average portlet developers and implement our portlet using the new Liferay MVC framework.

MVC Implementation Details

The new Liferay MVC takes on a lot of the grunt work for portlet implementations, and in this new iteration we're actually building OSGi components that leverage annotations to get everything done.

So let's take a look at one of the ActionCommand components to see how they work.  The TouchFileFolderMVCActionCommand is one of the simpler action commands in our portlet so this one will allow us to look at the aspects of ActionCommands without getting bogged down in the implementation code.

/**
 * class TouchFileFolderMVCActionCommand: Action command that handles the 'touch' of the file/folder.
 * @author dnebinger
 */
@Component(
	configurationPid = "com.liferay.filesystemaccess.portlet.config.FilesystemAccessPortletInstanceConfiguration",
	immediate = true,
	property = {
		"javax.portlet.name=" + FilesystemAccessPortletKeys.FILESYSTEM_ACCESS,
		"mvc.command.name=/touch_file_folder"
	},
	service = MVCActionCommand.class
)
public class TouchFileFolderMVCActionCommand extends BaseMVCActionCommand {

So here is a standard ActionCommand declaration.  The annotation identifies our configuration pid for accessing portlet instance config, the properties indicate this is an ActionCommand for our filesystem access portlet and the MVC command path to get to this action class, and the service declaration indicates this is an MVCActionCommand implementation.

The class itself extends the BaseMVCActionCommand so we don't have to implement all of the plumbing, just our necessary business logic.

	/**
	 * doProcessAction: Called to handle the touch file/folder action.
	 * @param actionRequest Request instance.
	 * @param actionResponse Response instance.
	 * @throws Exception in case of error.
	 */
	@Override
	protected void doProcessAction(
		ActionRequest actionRequest, ActionResponse actionResponse)
		throws Exception {

And this is the method declaration that needs to be implemented as an extension of BaseMVCActionCommand.

		// get the config instance
		FilesystemAccessPortletInstanceConfiguration config = getConfiguration(
			actionRequest);

		if (config == null) {
			logger.warn("No config found.");

			SessionErrors.add(
				actionRequest, MissingConfigurationException.class.getName());

			return;
		}

		// Extract the target and current path from the action request params.
		String touchName = ParamUtil.getString(actionRequest, Constants.PARM_TARGET);
		String currentPath = ParamUtil.getString(actionRequest, Constants.PARM_CURRENT_PATH);

		// get the real target path to use for the service call
		String target = getLocalTargetPath(currentPath, touchName);

		// use the service to touch the item.
		_filesystemAccessService.touchFilesystemItem(config.rootPath(), target);
	}

This next method demonstrates how an action command can get access to the portlet instance configuration.

	/**
	 * getConfiguration: Returns the configuration instance given the action request.
	 * @param request The request to get the config object from.
	 * @return FilesystemAccessPortletInstanceConfiguration The config instance.
	 */
	private FilesystemAccessPortletInstanceConfiguration getConfiguration(
		ActionRequest request) {

		// Get the theme display object from the request attributes
		ThemeDisplay themeDisplay = (ThemeDisplay)request.getAttribute(
			WebKeys.THEME_DISPLAY);

		// get the current portlet instance
		PortletInstance instance = PortletInstance.fromPortletInstanceKey(
			FilesystemAccessPortletKeys.FILESYSTEM_ACCESS);

		FilesystemAccessPortletInstanceConfiguration config = null;

		// use the configuration provider to get the configuration instance
		try {
			config = _configurationProvider.getPortletInstanceConfiguration(
				FilesystemAccessPortletInstanceConfiguration.class,
				themeDisplay.getLayout(), instance);
		} catch (ConfigurationException e) {
			logger.error("Error getting instance config.", e);
		}

		return config;
	}

	/**
	 * getLocalTargetPath: Returns the local target path.
	 * @param localPath The local path.
	 * @param target The target filename.
	 * @return String The local target path.
	 */
	private String getLocalTargetPath(final String localPath, final String target) {
		if (Validator.isNull(target)) {
			return null;
		}

		if (Validator.isNull(localPath)) {
			return StringPool.SLASH + target;
		}

		if (localPath.trim().endsWith(StringPool.SLASH)) {
			return localPath.trim() + target.trim();
		}

		return localPath.trim() + StringPool.SLASH + target.trim();
	}

Just as with our previous OSGi components, we rely on OSGi to inject services needed to implement the component functionality.

	/**
	 * setConfigurationProvider: Sets the configuration provider for config access.
	 * @param configurationProvider The config provider to use.
	 */
	@Reference
	protected void setConfigurationProvider(
		ConfigurationProvider configurationProvider) {

		_configurationProvider = configurationProvider;
	}

	/**
	 * setFilesystemAccessService: Sets the filesystem access service instance to use.
	 * @param filesystemAccessService The filesystem access service instance.
	 */
	@Reference(unbind = "-")
	protected void setFilesystemAccessService(
		final FilesystemAccessService filesystemAccessService) {
		_filesystemAccessService = filesystemAccessService;
	}

	private FilesystemAccessService _filesystemAccessService;

	private ConfigurationProvider _configurationProvider;

	private static final Log logger = LogFactoryUtil.getLog(
		TouchFileFolderMVCActionCommand.class);
}

As I started flushing out the other ActionCommand implementations, I quickly found that I was copying and pasting the getConfiguration() and getLocalTargetPath() methods.  I refactored those into a base class, BaseActionCommand, and changed all of the ActionCommand implementations to extend this base class, so don't be alarmed when the source differs from the listing above.

Serving resources is handled in a similar fashion.  Below is the declaration for the FileDownloadMVCResourceComand, the component which will handle serving the file as a Serve Resource handler.

/**
 * class FileDownloadMVCResourceCommand: A resource command class for returning files.
 * @author dnebinger
 */
@Component(
	immediate = true,
	property = {
		"javax.portlet.name=" + FilesystemAccessPortletKeys.FILESYSTEM_ACCESS,
		"mvc.command.name=/download-file"
	},
	service = MVCResourceCommand.class
)
public class FileDownloadMVCResourceCommand extends BaseMVCResourceCommand {

As with all Liferay MVC implementations, the view (render phase) is handled with JSP files.  JSP files do not have access to the OSGi injection mechanisms, so we have to use a different mechanism to get the OSGi injected resources and make them available to the JSP files.  We change the portlet class to handle this injection and pass through:

/**
 * 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.name=" + FilesystemAccessPortletKeys.FILESYSTEM_ACCESS,
		"javax.portlet.display-name=Filesystem Access",
		"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 {

	/**
	 * render: Overrides the parent method to handle the injection of our
	 * service as a render request attribute so it is available to all of the
	 * jsp files.
	 *
	 * @param renderRequest The render request.
	 * @param renderResponse The render response.
	 * @throws IOException In case of error.
	 * @throws PortletException In case of error.
	 */
	@Override
	public void render(RenderRequest renderRequest, RenderResponse renderResponse) throws IOException, PortletException {

		// set the service as a render request attribute
		renderRequest.setAttribute(
			Constants.ATTRIB_FILESYSTEM_SERVICE, _filesystemAccessService);

		// invoke super class method to let the normal render operation run.
		super.render(renderRequest, renderResponse);
	}

	/**
	 * setFilesystemAccessService: Sets the filesystem access service instance to use.
	 * @param filesystemAccessService The filesystem access service instance.
	 */
	@Reference(unbind = "-")
	protected void setFilesystemAccessService(
		final FilesystemAccessService filesystemAccessService) {

		_filesystemAccessService = filesystemAccessService;
	}

	private FilesystemAccessService _filesystemAccessService;

	private static final Log logger = LogFactoryUtil.getLog(
		FilesystemAccessPortlet.class);
}

So here we let OSGi inject the references into the portlet instance class itself, and we override the render() method to pass our service references to the view layer as render request attributes.  In our init.jsp page, you'll find that the service reference instances are extracted from the render request attributes and turned into a variable that will be available to all JSP pages that include the init.jsp file.  In this way our JSPs have access to the injected services without having to go through the older Util classes to statically access the service reference.

So the only remarkable thing about the JSP files themselves is their location.  Instead of the old way of having the JSPs right next to the WEB-INF folder the way we used to build and deploy portlets, now they are actually built and shipped within the jar bundle by putting them into the resources/META-INF/resources directory; this is our new "root" path for all web assets.  So in this folder in our project we have all of our JSP files as well as a css folder with our css file.

Unfortunately I implemented the JSP files before Nate Cavinaugh's new blog entry, https://web.liferay.com/web/nathan.cavanaugh/blog/-/blogs/the-status-and-direction-of-the-frontend-infrastructure-in-liferay-7-dxp.  Had I waited, I might have known that my use of AUI may have been a bad decision.  But then I remembered that the bulk of the portal is written using AUI and that unless the UI is completely rewritten all the way down to the least significant portlet, AUI is going to remain for some time to come.

Oh yeah, Lexicon

I mentioned that I was going to talk about Lexicon in the portlet.  Lexicon (and Metal and Soy and ...) are really hot topics for pure front end developers.  I'm looking for more of the discussions for cross-functional developers, the average developers that are doing front-end and back-end and are looking for a suitable path to navigate between both worlds without falling down the rabbit holes both sides offer from time to time.  AUI has historically been that path, a tag library that cross-functional developers could use to leverage Liferay's use of YUI javascript without really having to learn all of the details in using the AUI/YUI javascript library.

So to start that conversation, i'm going to talk about one of the design choices I made and how Lexicon was necessary as a result.

My need was fairly simple - I wanted to show an "add file" button that, when clicked, would show a dialog to collect the filename.  The dialog would have an OK button (that would trigger the creation of the file) and a cancel button (that cancelled the add of the file).

So I needed a modal dialog, but how was I going to implement that?  The choices for modal dialog implementation, as we all know, are seemingly endless, but does Lexicon offer a solution?

The answer is Yes, there is a Lexicon solution.  The doco can be found here: http://liferay.github.io/lexicon/content/modals/

Now if you're like me, you read this whole page and can see how the front end guys are just eating this up.  Use some standard tags, decorate with some attributes and voila, you have yourself a modal dialog on a page.  But you're left wondering how you're going to drop this in your portlet jsp page, how you're going to wire it up to trigger calls to your backend, etc., and you think back wistfully remembering the AUI tag library and how you could do some JSP tags to get a similar outcome...  Ah, the good ole days...

Oh, sorry, back on topic.  So I was able to leverage the Lexicon modal dialog in my JSP page and, with some javascript help, got everything working the way I needed.  What I'm about to show is likely not the best way to have integrated Lexicon, I'm sure Nate and his team would be able to shoot holes all through this code, but like I said I'm looking for the discussion that covers how cross-functional developers will use Lexicon and if this starts that discussion, then it's worth sharing.

So here goes, these are the parts of view.jsp which handles the add file modal dialog:

<button class="btn btn-default" data-target="#<portlet:namespace/>AddFileModal" data-toggle="modal" id="<portlet:namespace/>showAddFileBtn"><liferay-ui:message key="add-file" /></button>
<div aria-labelledby="<portlet:namespace/>AddFileModalLabel" class="fade in lex-modal modal" id="<portlet:namespace/>AddFileModal" role="dialog" tabindex="-1">
	<div class="modal-dialog modal-lg">
		<div class="modal-content">
			<portlet:actionURL name="/add_file_folder" var="addFileActionURL" />

			<div id="<portlet:namespace/>fm3">
				<form action="<%= addFileActionURL %>" id="<portlet:namespace/>form3" method="post" name="<portlet:namespace/>form3">
					<aui:input name="<%= ActionRequest.ACTION_NAME %>" type="hidden" />
					<aui:input name="redirect" type="hidden" value="<%= currentURL %>" />
					<aui:input name="currentPath" type="hidden" value="<%= currentPath %>" />
					<aui:input name="addType" type="hidden" value="addFile" />

					<div class="modal-header">
						<button aria-labelledby="Close" class="btn btn-default close" data-dismiss="modal" role="button" type="button">
							<svg aria-hidden="true" class="lexicon-icon lexicon-icon-times">
								<use xlink:href="<%= themeDisplay.getPathThemeImages() + "/lexicon/icons.svg" %>#times" />
							</svg>
						</button>

						<button class="btn btn-default modal-primary-action-button visible-xs" type="button">
							<svg aria-hidden="true" class="lexicon-icon lexicon-icon-check">
								<use xlink:href="<%= themeDisplay.getPathThemeImages() + "/lexicon/icons.svg" %>#check" />
							</svg>
						</button>

						<h4 class="modal-title" id="<portlet:namespace/>AddFileModalLabel"><liferay-ui:message key="add-file" /></h4>
					</div>

					<div class="modal-body">
						<aui:fieldset>
							<aui:input autoFocus="true" helpMessage="add-file-help" id="addNameFile" label="add-file" name="addName" type="text" />
						</aui:fieldset>
					</div>

					<div class="modal-footer">
						<button class="btn btn-default close-modal" id="<portlet:namespace/>addFileBtn" name="<portlet:namespace/>addFileBtn" type="button"><liferay-ui:message key="add-file" /></button>
						<button class="btn btn-link close-modal" data-dismiss="modal" type="button"><liferay-ui:message key="cancel" /></button>
					</div>
				</form>
			</div>
		</div>
	</div>
</div>

So first I have the button that will trigger the display of the modal dialog.  The modal dialog is in the <div /> that follows.  The content div contains my AUI-based form but is decorated with appropriate Lexicon tags to add the dialog buttons.

There's also some javascript on the page that affects the dialog:

	var showAddFile = A.one('#showAddFileBtn');

	if (showAddFile) {
		showAddFile.after('click', function() {
			var addNameText = A.one('#addNameFile');
		
			if (addNameText) {
				addNameText.val('');
				addNameText.focus();
			}
		});
	}

	var addFileBtnVar = A.one('#addFileBtn');

	if (addFileBtnVar) {
		addFileBtnVar.after('click',function() {
			var fm = A.one('#form3');
		
			if (fm) {
				fm.submit();
			}
		});
	}

The first chunk is used to set focus on the name field after the dialog is displayed.  The second chunk triggers the submit of the form when the user clicks the okay button in the dialog.

The highlight of this code is that we get a modal dialog without really having to code any javascript, any JSP tags, etc.  We had to basically add necessary code to flush out the dialog content.

And I think that's really the essence of the Lexicon stuff; I think it's all going to work out to be a "standard" set of tags and attribute decorations that will render the UI, plus we'll have some code to put in at the JSP level to bind into our regular portlet code.

Conclusion

So here we are at the end of part 6 and I think I've covered it all...

We now have a finished project that satisfies all of our original requirements:

  • Must run on Liferay 7 CE GA2 (or newer).  Since we leveraged all Liferay 7 tools and are building module jars, we're definitely Liferay 7 compatible.
  • Must use Gradle for the build tool.  We set this up in part 2 of the blog series using the blade command line tool.
  • Must be full-on OSGi modules, no legacy stuff.  We also started this in part 2 and continued the module development through all other parts.
  • Must leverage Lexicon.  This was done in our modal dialog just introduced above.
  • Must leverage the new Configuration facilities.  The configuration facilities were added in part 5 as part of the initial portlet setup.
  • Must leverage the new Liferay MVC portlet framework.  The bulk of the Liferay MVC implementation was added in this blog part.
  • Must provide new Panel Application (side bar) support.  This was covered in part 5 of the blog series.

The original requirements have been satisfied, but how does it look?  Here's some screen shots to whet your appetite:

Main View

The main view lists files/folders that can be acted upon.  The path shows that I'm in /tomcat-8.0.32 but actually I'm off somewhere else in the filesystem.  Remember in a previous part I was using a "root path" to constrain filesystem access?  This shows that I am within a view sandbox that I cannot just sneak my way out of.  Even though we're exposing the underlying filesystem, we don't want to just throw out all semblances of security.

Add File Dialog

Our Lexicon-based modal dialog for adding a new file.

Upload File Dialog

The modal dialog even works for a file upload.

View File

The file view component is provided by the AUI ACE editor component.

Edit File

The edit file component is also provided by the ACE editor.

While not shown, the configuration panel allows the admin to set the "root path", enable/disable adds, uploads, downloads, deletes and edits.

That's pretty much it.  Hope you enjoyed the multi-part blog.  Feel free to comment below or, better yet, launch a discussion in the forums.

And remember, you can find the source code in github: https://github.com/dnebing/filesystem-access

Nice series with clear and helpful information for people (like me) that also need to start working on making LR7 stuff or converting existing stuff to LR7. A LR7 version of CRaSH is something I should look at...
Thanks, Jan. Yeah, I needed something like CRaSH to expose filesystem access to special users that could not have actual server access. I didn't really dig into CRaSH because I didn't want to risk plagiarizing the good things you had done.
Just finished the series myself. Great work David -- terrible that it took me so long to get to it. I suppose the silver lining is that I ran into a few issues along the way which were great learning opportunities for understanding how all this stuff comes together. Once again, great work. Thanks for taking the time to help out the community with this series.