Liferay DXP: Adding Custom Document Actions

At Coin we have developed SimpleEdit, a new and improved way for editing your documents stored in Alfresco or Liferay.
Now that Liferay has moved on to DXP, we are in the process of upgrading SimpleEdit to this new platform.

One of the required modules for SimpleEdit was a JSP hook that provided the user with buttons in the Documents section of the Control Panel and the Documents and Media portlet.

Control Panel

 

 

Documents and Media Portlet

 

Looking at the DXP UI, it became clear that we needed to add our buttons in the following locations.

Control Panel

 

 

Documents and Media Portlet

 

We hoped to be lucky and just port our changes to the new versions of JSP's in Liferay DXP, as described in a tutorial. But after a deep dive into the source code it became clear that this would not be the way forward. These menu's weren't build anymore in a JSP, but through different classes.
After several deep dives into the source code, reading a handful tutorials (see the Resources section) and even a short Twitter conversation with one of the Liferay Document Library developers, we were able to add the four menu items mentioned above.

So here is an overview of what you would need to do if you ever want to achieve the same goals.

Control Panel

Document Overview screen

Lets start with the Control Panel. We used to override the file_entry_action.jsp file and add the SimpleEdit action to the icon-menu.
In the new Document Library module the main object used is DLViewFileVersionDisplayContext. The only JSP code here is just one line (all the other lines are scriptlets :-s):

<liferay-ui:menu menu="<%= dlViewFileVersionDisplayContext.getMenu() %>" />

So you'll need to provide the implementation for a Display Context object and have it return some kind of menu. If you don't know what a Display Context is, read the Liferaypedia article describing it (don't worry, I had to discover this new object myself).

A Display Context is a Java class that controls access to a portlet screen's UI elements

To register your Display Context object so that a dlDisplayContextProvider can return it, you should also implement a Display Context Factory. This factory class instantiates and returns your Display Context which provides all the necessities for your view. The factory class will be chained in the list of factory classes according to the service ranking.

The Factory Class

The factory is really easy. From the previous mentioned article you'll learn that for the Document Library several interfaces and abstract classes are available to extend.

@Component(
	immediate = true,
	service = DLDisplayContextFactory.class
)
public class SimpleEditDisplayContextFactory implements DLDisplayContextFactory {

	...

}

Just don't forget to define this factory as an @Component and register it as a DLDisplayContextFactory!

At the time of writing you are supposed to implement four constructors, each for a different use case. For the current requirement the most important constructor is getDLViewFileVersionDisplayContext.

public DLViewFileVersionDisplayContext getDLViewFileVersionDisplayContext(DLViewFileVersionDisplayContext parentDlViewFileVersionDisplayContext, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FileVersion fileVersion) {
	return new SimpleEditDisplayContext(parentDlViewFileVersionDisplayContext, httpServletRequest, httpServletResponse, fileVersion);
}

See how it instantiates and returns the custom SimpleEditDisplayContext? The other three constructors just return the parent Display Context they receive as an input parameter as you are not in the process of changing those.

The Display Context Class

In the file_entry_action.jsp you can see that getMenu() is the method you need to implement in your Display Context.

public class SimpleEditDisplayContext extends BaseDLViewFileVersionDisplayContext {

	private static final Log LOGGER = LogFactoryUtil.getLog(SimpleEditDisplayContext.class);
	private static final UUID SIMPLE_EDIT_UUID = UUID.fromString("7B61EA79-83AE-4FFD-A77A-1D47E06EBBE10");

	public SimpleEditDisplayContext(DLViewFileVersionDisplayContext parentDLDisplayContext, HttpServletRequest request, HttpServletResponse response, FileVersion fileVersion) {
		super(SIMPLE_EDIT_UUID, parentDLDisplayContext, request, response, fileVersion);
	}

	public Menu getMenu() throws PortalException {
		Menu menu = super.getMenu();

		URLMenuItem urlMenuItem = new URLMenuItem();
		urlMenuItem.setMethod(HttpMethods.GET);
		urlMenuItem.setKey(SimpleEditLabel.OPEN.getUIItemKey());
		urlMenuItem.setLabel("Open with SimpleEdit");
		urlMenuItem.setURL("simpleedit://...");

		menu.getMenuItems().add(urlMenuItem);

		return menu;
	}
}

Actually this isn't all that hard either. You just need to create a new URLMenuItem, provide it with the proper values and add it to the list of MenuItems you retrieve from the BaseDLViewFileVersionDisplayContext.

After putting these two classes in place and deploying the module, you'll see the following result.

A vertical ellipsis is added to the Document and upon hovering on it, your MenuItem is shown among the other actions.

Document Detail screen

Maybe your first hope is (as well as mine was): the getMenu() method will serve as the provider for the actions menu in the Document Detail screen... Bad luck, if you go to the Document Detail you'll see that the menu hasn't changed a bit.

Again after some deep dives into the code, I was rattled on how to achieve this. But incidentally, I stumbled across the section Configuring your apps action menu (a real coincidence as at that time I was gathering information concerning styling). However this seemed to be more what I was trying to do here. Too bad this important piece of information was so well hidden. But now I had a proper clue of what to look for in the source code.

Thus I learned that you'll need something called a Portlet Configuration Icon (what's  in a name, heh) and that you can extend the BasePortletConfigurationIcon class for ease of implementation.
The three most important methods however (or the three I implemented) are:

  • isShow - whether or not your action should be displayed (can be conditional)
  • getMessage - to provide a translation for your action
  • getUrl - the url your action should link to

Portlet Configuration Icon can also be declared as a Declarative Service. As you are actually extending the already defined list of Document Library Configuration Icons, you can reuse the properties from e.g. the DownloadFileEntryPortletConfigurationIcon to add yours to that list. For more information on the properties, please read the aforementioned section in the Lexicon tutorial.

@Component(
	immediate = true,
	property = {
		"javax.portlet.name=com_liferay_document_library_web_portlet_DLAdminPortlet",
		"path=/document_library/view_file_entry"
	},
	service = PortletConfigurationIcon.class
)
public class SimpleEditConfigurationIcon extends BasePortletConfigurationIcon {

	private static final Log LOGGER = LogFactoryUtil.getLog(SimpleEditConfigurationIcon.class);

	@Reference private Portal portal;
	@Reference DLAppService dlAppService;

	public boolean isShow(PortletRequest portletRequest) {
		return true;
	}

	public String getMessage(PortletRequest portletRequest) {
		return "Open with SimpleEdit";
	}

	public String getURL(PortletRequest portletRequest, PortletResponse portletResponse) {
		HttpServletRequest servletRequest = portal.getHttpServletRequest(portletRequest);
		FileEntry fileEntry = retrieveFile(servletRequest);

		return "simpleedit://...";
	}

	private FileEntry retrieveFile(HttpServletRequest request) {
		try {
			long fileEntryId = ParamUtil.getLong(request, "fileEntryId");

			FileEntry fileEntry = null;

			if (fileEntryId > 0) {
				fileEntry = dlAppService.getFileEntry(fileEntryId);
			}

			if (fileEntry == null) {
				return null;
			}

			String cmd = ParamUtil.getString(request, Constants.CMD);

			if (fileEntry.isInTrash() && !cmd.equals(Constants.MOVE_FROM_TRASH)) {
				LOGGER.info("File entry is not supposed to be opened.");
				return null;
			}

			return fileEntry;
		} catch (PortalException e) {
			LOGGER.error("An exception ocurred while retrieving Simple Edit Url.", e);

			return null;
		}
	}
}

If you update your module now, I think you are happy to see the action.

So that's it for the Control Panel. Let's move on to customizing the Documents and Media Portlet!

Documents and Media Portlet

For Liferay 6.2 it was sufficient to override two JSP files: the file_entry_action.jsp I spoke earlier about and the view_file_entry.jsp file as well. In that last one you could add a button to the already provided action buttons (Download, Edit, Move, ...).
In fact, all it took were those two JSP's to adjust both the Control Panel and the Documents and Media Portlet because those shared the same resources.

So maybe you have already opened the Documents and Media Portlet after the first section? Then you have seen that for Liferay DXP this is no longer the case. All the effort you have done so far, has had just one impact. Only in the Documents Overview your custom action is added to the Document.

If you go to the Documents Detail screen, you only see an "Info" button. So you'll need to add some more code to get your action in that list as well.

Overview screen

But wait a minute. Isn't it strange that there is just one sole action shown? Shouldn't there be more actions such as Download, Edit, Move, ... ?
That's because this Portlet can also be configured and one of these Configurations is "Show Actions". When you activate this you'll see the other actions as well.

You should take this configuration into account while adding your custom action to the menu. I had some trouble figuring this out, so here is where the short Twitter conversation with Sergio González comes in place (he's mentioned as one of the Document Library developers in the source code):

Here is the link to the source code he mentions: https://t.co/YEGooQgR6Y

If you follow that, you see that you can use a SettingsFactoryUtil class that can retrieve the settings for the portlet instance you are using. If you want to use that, you'll need to store the ThemeDisplay from the original request to retrieve the necessary parameters from it. In the code below you can see this addition, as well as a showAction() method where the decision logic is captured. Please note the fact that if it concerns the Document Library Admin Portlet, it always returns true. If this is not the case, you will never see the action in the Control Panel anymore (I had to find this out at hand and via the example code as well).

public class SimpleEditDisplayContext extends BaseDLViewFileVersionDisplayContext {

	private static final Log LOGGER = LogFactoryUtil.getLog(SimpleEditDisplayContext.class);
	
	private ThemeDisplay themeDisplay;

	public SimpleEditDisplayContext(UUID uuid, DLViewFileVersionDisplayContext parentDLDisplayContext, HttpServletRequest request, HttpServletResponse response, FileVersion fileVersion) {
		super(uuid, parentDLDisplayContext, request, response, fileVersion);

		this.themeDisplay = (ThemeDisplay) request.getAttribute(WebKeys.THEME_DISPLAY);;
	}

	public Menu getMenu() throws PortalException {
		Menu menu = super.getMenu();

		if (showAction()) {
			URLMenuItem urlMenuItem = new URLMenuItem();
			urlMenuItem.setMethod(HttpMethods.GET);
			urlMenuItem.setKey("open-with-simple-edit");
			urlMenuItem.setLabel("Open with SimpleEdit");
			urlMenuItem.setURL("simpleedit://...");

			menu.getMenuItems().add(urlMenuItem);
		}

		return menu;
	}

	private boolean showAction() throws SettingsException {
		PortletDisplay portletDisplay = themeDisplay.getPortletDisplay();

		String portletName = portletDisplay.getPortletName();

		if (portletName.equals(PortletKeys.DOCUMENT_LIBRARY_ADMIN)) {
			return true;
		}

		Settings settings = SettingsFactoryUtil.getSettings(new PortletInstanceSettingsLocator(themeDisplay.getLayout(), portletDisplay.getId()));
		TypedSettings typedSettings = new TypedSettings(settings);

		return typedSettings.getBooleanValue("showActions");
	}
}

Another update of the module leads to the result that no actions are shown when the "Show Actions" Configuration is not selected. And your custom action is still added to the list when the Configuration is selected.

So with that in place, let's focus on the Document Detail screen again.

Document Detail screen

Now that you have configured the Documents and Media with the "Show Actions" configuration to true, you see all the other buttons next to Info as well (Download, Edit, ...). So how to add your own button to that list?

Well I don't know if you noticed it, but in the DLViewFileVersionDisplayContext there is also a method getToolbarItems(). When I was on the endeavour of adding an action to the Control Panel, I fidgeted with that method but it didn't had any result for the Control Panel. Nor in the Document Overview nor in the Document Detail view.
But one more try while investigating this last issue and I learned that those buttons on the Document Detail screen are in fact "Toolbar Items". Actually I have no idea why the menuItems aren't reused for this row. That would seem a logical decision, but for some reason or another Liferay didn't do it.

Therefor you'll have to provide an implementation for the getToolbarItems() method as well in order to complete this entire functionality. Again it is rather easy (just as the getMenu() implementation). Create a Toolbar Item yourself and populate it with the correct properties.

@Override
public List<ToolbarItem> getToolbarItems() throws PortalException {
	List<ToolbarItem> toolbarItems = super.getToolbarItems();

	URLToolbarItem toolbarItem = new URLToolbarItem();
	toolbarItem.setKey("open-with-simple-edit");
	toolbarItem.setLabel("Open with SimpleEdit");
	toolbarItem.setURL("simpleedit://...");

	toolbarItems.add(toolbarItem);

	return toolbarItems;
}

Another strange thing here is that for the Toolbar Items you do not need to consider the "Show Actions" configuration yourself. Whereas for the Menu you did need to take that into account. It seems like the Liferay people are missing some sort of consistency mechanism. Maybe they have there good reasons for it, but it just seems strange to me at the moment. Calling the getToolbarItems() method is conditionally determined on a higher level apparently, and the getMenu() method is not. Just deal with it, I think.

Conclusion

It seems somewhat more complex than overriding two JSP files like you would've done in the past. And there are some minor drawbacks at the moment.

It's a little sad that you can't annotate the DisplayContext with an @Component. This would make it much easier to inject Liferays Declarative Services or even your own ones. The real SimpleEdit implementation is more complicated than the code shown in this blog post and thus we had to pull some tricks. Liferay does provide you with some mechanics to work around this, e.g. Service Trackers. It is just a side effect of the modularity environment and we will have to learn to work with this. Let's hope the Liferay developers will find a way around this.

Another peculiarity is the fact that in the Display Context object you have to provide the getMenu() and the getToolbarItems() methods for roughly the same functionality. And that you need to consider the "Show Actions" Configuration yourself in the getMenu() method but not in the getToolbarItems() method. Some consistency might be in place here. But as long as you know about it, you can deal with it.

Also I feel that the naming sometimes sounds strange. I am still trying to find out what the actions for a Document Entry have to do with an Icon for the Portlet Configuration.
All in all it is harder to understand the internal workings of the platform at the moment. There is a whole new syntax in Liferay DXP, the relations between the modules are not always clear and the documentation is far from complete.
That's what you get from dividing everything in small modules and reworking the entire back end.

On the plus side DXP provides you with more flexibility and dynamics. If you divide your classes neatly in different modules, you can easily swap implementations, changing the behaviour of the Portal as you like without any downtime.
And you are working with real Java classes (instead of those dreadful JSP's) which provide you with lots of advantages such as service injection, OO programming, unit testing, ...

By implementing just three classes, you can add your custom buttons to the Document and Media Portlet and the Control Panel.

  • Display Context - to provide a view all the UI elements and the decision to show these or not
  • Display Context Factory - to conditionally add your DisplayContext to the list of already existing DisplayContexts
  • Configuration Icon - to add actions to the Portlets action menu

I hope you have learned a lot from this blog post. You probably also understood that you'll need to search heavily in the tutorials and the Liferay source code to gather all the information you need to connect the dots for the adjustments you are trying to do. But that is a new challenge that makes our job interesting, day in day out!

Thanks for reading!

You can find the source code for this blog post in my Github repository.

Resources

Tutorials

Display Context
Configuring your apps actions menu
Service Trackers
Service Ranking
Overriding core JSP's

Source code

Document Library
Document Library Google Docs module
Frontend image editor - DisplayContextHelper

Blogs
How to do for custom portlet .I m developing custom portlet in liferay 7 using plugin sdk,Could you please let me know how to add and remove configuration icon form custom portlet

Thanks a lot for the blog.

Could you please share us the source code .

Hello Anji,

 

Thank for your interest in this blog post. At the moment I don't have the code online anymore. Also this blog post was written for DXP 7.0. So it is probably not up to date anymore for the latest version.

Are there any specific questions I can maybe answer?