Light up your life with Liferay and Philips Hue

General Blogs January 30, 2018 By Koen Olaerts

On October 6, 2017 my colleague Sebastiaan and I held a presentation at Liferay DevCon about a cool integration the COIN team made. We were looking for a fun and exciting Internet of Things integration in Liferay. After a fruitful brainstorm, we decided to use Philips Hue. Another couple of brainstorms later, we formed the actual idea.

ACA Light Travel

The setup was to create an imaginary travel agency, named ACA Light Travel, that wants to provide the ultimate personalized experience to its customers. Potential travelers can browse through all available destinations by visiting a kiosk tailor-made for this travel agency. The nice thing about this kiosk is that it's equipped with contextual lighting. While the customer browses the website, the bulbs and LEDs in the kiosk will recognize which kind of destinations he or she is mostly interested in, and adapt their lighting accordingly.

But what is Philips Hue?

Hue is a great product line of Philips that consists of several types of lights, light bulbs, LED strips, ... They call it "Personal Wireless Lighting" and it's a stepping stone towards smart lighting.

Philips Hue

The line is based around a central unit called the bridge, which you need to plug into your own network using an ethernet cable. The bridge will then communicate wirelessly with the light bulbs, LEDs and other components in your network. You can turn the lights on or off, dim them, change the colors, perform some effects, etc. By grouping bulbs together, you don't even need to direct them individually anymore. All this can be done programmatically based on timers and events for instance. There really are plenty of options.

Liferay meets Hue

Liferay DXP can communicate with the Philips Hue system through a REST API that the people from Philips Hue provide. If you want to create something yourself, you can just go to the Philips Hue Developer Program website, download this API in two jars and start your own development.

Liferay meets Hue

ACA Light Travel

So, for our travel agency we used the Audience Targeting module of Liferay DXP and defined 5 different user segments. One segment for every type of holiday destination: sunny holidays, adventurous trips, city trips, cruises and winter holidays.

ACA Light Travel

All destinations on the site, which are plain web content articles, are categorized as one or more of these segments. Each time a visitor takes a look at the details of a certain destination, the system increments a counter for the corresponding segment. This is a by-default available rule in Audience Targeting.

Counter

We also wrote a custom Audience Targeting rule. This rule determines that the user belongs to the segment that corresponds with the highest counter. On a first visit, the user randomly sees all of the articles. When this user clicks on an article to look at the details of a city trip destination, the Audience Targeting rule increments the counter corresponding to that segment. Through the API, Liferay DXP communicates to the Philips Hue bridge that the lights need to turn red, because the user now belongs to that user segment.

User Segmentation

To actually control the lights, we have written a page listener in Liferay. With each page load, it will identify the segment that the user is in and change the lights to the corresponding color by calling the Philips Hue API. Upon a next visit to the homepage, the user will first see the destinations that match the "city trip" user segment, which is the segment that the user belongs to. Of course, it's possible that a customer liked city trip destinations before, but is more interested in sunny vacations now. In this case, he or she will move from the "city trip" segment to the "sunny" segment after enough clicks.

Under the hood

We implemented this project by making use of version 1.20 of the Hue API. The code is subject to change according to updates in the API.

Now that you know what we created, lets take a look at the how. As mentioned before, we created two custom modules. One is a custom Audience Targeting rule that determines that the user belongs to the segment that corresponds with the highest counter. The other is a page listener that identifies the segment the user is in on each page load and changes the lights into the corresponding color. Next to these modules, we also implemented the usual plugins and components. There are several portlets to show the suggested content items and two service modules to minimize the duplication of code. 

OSGi Bundles

Hue Service

This OSGi bundle consists of just one service that contains the logic to control the Hue lights. This is the only implementation in the project that actually holds the specific Hue API calls. Upon activating the bundle, a connection is made to the Hue Bridge by using the fixed IP and UserName we had set up. The service provides the possibility to toggle the lights on/off, change the brightness, set the color and start or stop blinking the lights. When you deactivate the bundle, you terminate the connection to the Hue Bridge.

User Segment Service

This bundle is the connection of our portlets to the Audience Targeting functionality of Liferay DXP. It contains all business logic required for our portlets to provide the desired filtering and segmented content.

Hue Page Listener

This is the plugin that determines the segment that the user belongs to after each page load and communicates to the bridge to change the color of the lights. It will only do this when the user is part of just one user segment. The plugin retrieves the required color from the description of the segment that the user currently belongs to.

Score Points Maximum Rule

The custom Audience Targeting Rule determines to which segment the user currently belongs by calculating the highest counter. The counter is set by the default available Score Point rule in Liferay DXP.

Promotion Listener

When a JournalArticle is categorized as a promotion, the lights in the kiosk will start blinking. This visual effect informs the user that it's a special kind of destination.

Suggestions Portlet

This is a portlet bundle that contains three different portlets, one for each type of filtering.

  • YourSelectionSuggestionsPortlet: a portlet that shows all JournalArticles categorized by the segment that the user currently belongs to.
  • OtherSuggestionsPortlet: shows all JournalArticles that do not belong to the segment that the user currently belongs to.
  • PromotionsSuggestionsPortlet: this portlet first shows the promotions for segments that the user belongs to, supplemented with all other promotions available.

Conclusion

Audience targeting is one of the most powerful features in Liferay DXP. If implemented the right way, it can really learn what the user tries to accomplish and steer him or her in the right direction. It can even persuade the user to perform a transaction. Liferay, as an open platform, can really integrate with anything. Connecting the Audience Targeting module of Liferay to the Philips Hue system wasn't too hard thanks to the existing API. Just imagine which other IoT integrations are possible. The sky is the limit!

DXP and Hue

Resources

You can find the code for the ACA Light Travel website on our public Bitbucket.

The presentation was recorded at Liferay DevCon and is published on Youtube.

The slides that go with the presentation are also available through Liferay.

Liferay DXP: Adding Custom Document Actions

Technical Blogs November 21, 2016 By Koen Olaerts

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

Searching for JPA entities in Liferay

Technical Blogs July 11, 2016 By Koen Olaerts

So you want to search for your custom JPA entities in Liferay? Quite some documentation is already available but it is somewhat scattered over several blogposts and articles. With this blogpost I would like to give you a summary of everything you need to do, or more precisely everything we did, to make custom JPA entities searchable in the standard Liferay Search Portlet.

The requirement

One of our customers wanted the search results to not only contain the default Liferay objects like articles, documents, etc... But also some of his own custom domain objects. We had created a custom lightweight Support Ticket framework with a set of limited functionalities. It allowed a user to create a ticket and post comments to it. The additional requirement was that a user had to be able to also search for these tickets based on title, description and... oh yeah the comments too. In this blog I will use a very simplified version of this domain that shows all the steps necessary to make a JPA entity searchable. The domain only has 1 JPA entity, SupportTicket, together with a repository and service. There is also a simple portlet that can be used to create and show instances of this entity. So let's go on a journey, a quest you might even call it, to enable the search functionality for your custom JPA entities. Eventually you will get to the end point where you will use the Liferay Search Portlet to search and find your own JPA entities. You can also create your own custom search (taking customization to the max!) but that is outside the scope of this blog post. In the resources section however you can find links for more information on this matter. In a vanilla Liferay 6.2 the result should look like this:

Full Result

Enter the Indexer

Liferay’s search & indexing functionality is provided by Apache Lucene, a Java search framework. It converts searchable entities into documents. Documents are custom objects that correspond to searchable entities. Usually performing full text searches against an index is much faster than searching for entities in a database. You can find more information about Lucene on its own website. To start you will need to create an indexer class. This class will hold the information required to transform your JPA entity into a Lucene Document. Next you will need to register this Indexer in the portlet so that the Liferay framework knows about it and can use it when necessary.

Creating the Indexer

I already made clear that the Indexer class is responsible for creating the Lucene documents for your JPA entity. It is important to know that this is where you can decide what fields of your entity will get indexed in order to be searchable. You can also specify if this needs to be a term or a phrase. And of course there are some other settings you can implement such as the name of the associated portlet, permissions, ... Have a look at the Sources for blog posts with more information. Liferay provides you with a default implementation named BaseIndexer as an easy start. This abstract class should be extended and the required methods implemented. Enough of the theoretical stuff, let's get started. As soon as you extend the BaseIndexer, you will need to overwrite the following methods. I'm not going to go into the details of all the implementations, I refer you to the Github project. Mostly the implementation will depend on your own requirements and domain.

  • doDelete: delete the document that corresponds to the object parameter
  • doGetDocument: specify which fields the Lucene Index will contain
  • doGetSummary: used to show a summary of the document in the search results
  • doReindex (3 times): will be called when the index is updated
  • getPortletId (2 times): to be able to narrow searches for documents created in a certain portlet instance (hence it needs to be the full portlet Id)
  • getClassNames: the FQCN of the entities this indexer can be used for

Something important to notice in this class is the difference between document.addKeyword and document.addText in the doGetDocument method.

  • addKeyword: adds a single-valued field, with only one term
  • addText: adds a multi-valued field, with multiple terms separated by a white space

The doGetDocument method is the most important piece of code in the TicketIndexer. In this method an incoming Object is transformed into an outgoing (Lucene) Document. The object is of type Ticket as the Indexer has been registered to be used for objects of that type. This registration occurs in the getClassNames method. A new Document object holding the necessary searchable fields can be created from this Ticket. Notice that all the Keyword/Text fields are already predefined in Liferay so you can use these constants. This is quite handy but you can always provide your own names.

@Override protected Document doGetDocument(Object obj) throws Exception {
   Ticket ticket = (Ticket) obj;

   List<TicketComment> comments = BeanLocator.getBean(TicketCommentService.class).getComments(ticket);

   Document document = new DocumentImpl();

   document.addUID(PORTLET_ID, ticket.getId());
   document.addKeyword(Field.COMPANY_ID, PortalUtil.getDefaultCompanyId());
   document.addKeyword(Field.ENTRY_CLASS_NAME, TICKET_CLASS_NAME);
   document.addKeyword(Field.ENTRY_CLASS_PK, ticket.getId());
   document.addKeyword(Field.PORTLET_ID, PORTLET_ID);
   document.addText(Field.TITLE, ticket.getSubject());
   document.addText(Field.CONTENT, ticket.getDescription());
   document.addText(Field.COMMENTS, Collections2.transform(comments, getComments()).toArray(new String[] {}));

   return document}

Registering the Indexer

Once the Indexer is created, you will need to register it in the Liferay platform. This is a very simple action and it is completed by adding the following line into your portlet’s liferay-portlet.xml (check the DTD for the exact location).

<indexer-class>FQCN of your custom Indexer</indexer-class>

Once your portlet is redeployed Liferay will automatically register your indexer into its framework.

Tip
Just by registering your indexer, existing entities won’t be indexed. As a Liferay admin however, you can trigger a reindex from the Control Panel. Just go to Configuration > Server Administration > Resources (tab) and locate the Execute button next to the Reindex all search indexes entry. Be advised that if you already have a large index (due to web content, documents, …) this may take a while.
Info
You can use some index inspection tool like Luke to inspect and search through your index. If your system contains at least one instance of your custom JPA entity you should see those pop up in Luke after doing a reindex, which means your Indexer implementation did its job.
Info
If you are eager to know whether or not your current progress has had any impact, you can already update the Liferay Search Portlet as described in Executing the Search.

Using the indexer programmatically

No doubt it is great that your existing entities are now indexed. But you don’t want to trigger this indexing yourself manually, do you? By all means you don’t, you want it to happen automagically. Or at least programmatically. That is why you are reading this blog post anyway. So how about actually using the indexer yourself in the creation of a support ticket? Actually you want to use your newly created Indexer at the right time. But what is the right time? Well, right after a new entity is created. Or updated. Or even deleted. So you will need to use your Indexer at these moments, using the code below. We used the nullSafeGetIndexer as it returns a dummy indexer when no indexer can be found for the class, contrary to the default getIndexer() which returns null.

Indexer indexer = IndexerRegistryUtil.nullSafeGetIndexer(Ticket.class.getName());
indexer.reindex(ticket);

This code instructs your indexer, which you've registered with the FQCN of your entity, to reindex any added or updated entities. It delegates to the doReindex methods you have implemented in your own Indexer. When an entity is deleted its corresponding document should not show up in any search results anymore. You will need to call the code below when you are deleting your entity.

Indexer indexer = IndexerRegistryUtil.nullSafeGetIndexer(Ticket.class.getName());
indexer.delete(ticket);

Redeploying your portlet should do the trick of having each new entity being registered as a Document in the index as well. Go and try it out (if you have a create option of course).

Executing the Search

In this blog post Liferay's Search Portlet will be used to search and find your custom entities. You can always create a custom search portlet as well, check the Sources section for articles on how to achieve that. But for our customer we decided that integrating the custom JPA entity into Liferay's Search Portlet was the best solution as other content, e.g. Web Content, should also be searchable. By doing this the client had a nice integration with the default Liferay functionality. The Liferay Search Portlet will need to be updated to also take into account your entity by adding the portlet to a page and adjusting its configuration. But first check out the portlets normal behaviour. As an admin, add the portlet to a page of your choice. Search for the value of one of the earlier defined searchable fields in the Indexer and hit Search. Notice that there are no search results found and that this is clearly communicated.

No Results

Now go to the Configuration panel of the portlet, select Advanced and add the FQCN of your entity between quotes in the asset_entries list. Keep in mind the comma separation.

{
   "displayStyle": "asset_entries",
   "fieldName": "entryClassName",
   "static": false,
   "data": {
      "frequencyThreshold": 1,
      "values": [
         "com.liferay.portal.model.User",
         "com.liferay.portlet.bookmarks.model.BookmarksEntry",
         "com.liferay.portlet.bookmarks.model.BookmarksFolder",
         "com.liferay.portlet.blogs.model.BlogsEntry",
         "com.liferay.portlet.documentlibrary.model.DLFileEntry",
         "com.liferay.portlet.documentlibrary.model.DLFolder",
         "com.liferay.portlet.journal.model.JournalArticle",
         "com.liferay.portlet.journal.model.JournalFolder",
         "com.liferay.portlet.messageboards.model.MBMessage",
         "com.liferay.portlet.wiki.model.WikiPage",
         "be.olaertskoen.blog.search.Ticket"
      ]
   },
   "weight": 1.5,
   "className": "com.liferay.portal.kernel.search.facet.AssetEntriesFacet",
   "label": "asset-type",
   "order": "OrderHitsDesc"
},

Hit Save, close the panel and perform the same search again. Notice that the search result page has changed. It looks like the portlet found something but doesn't really know how to show it. Oh me oh my...

First Results

Probably this is not exactly what you wanted. Hell, it should be nothing you ever wanted! What use was all of this? Well, it was the preparation for what is to follow. If you followed the hints or tip above, you already know the indexer has done its work. Now it is time to actually use its results.

How about some usable search results?

You now know that your entities can be found but that the search results look nothing like you ever dreamed of. Next up you are going to get that party started and you should come to an end of this search functionality quest. As you probably know, or not if you are new to the game, but Liferay uses their Asset framework extensively. Not only in the Asset Publisher or for all of their web content, but also for their search results. Yes, that is right: the results in the search portlet are rendered using Assets. Therefor you will also need to create an Asset Renderer for your JPA entities. And more or less consistently you will need to register it as well. At last you will need to use it one way or another.

Creating an Asset Renderer

Similar as to the Indexer there is a base class you can easily extend, BaseAssetRenderer, to implement your own version. This class will give you a bunch of methods you will need to overwrite. One if them is to provide a summary for your entity, another is for the title. Two other important methods are render and getUrlViewInContext. The first one, render, will return a String with the url leading to the full content of your entity. It will use an Asset Publisher on a hidden page created for the Search Portlet. In our project we have an entire page reserved for this so we need to return the url of that detail page (here the page to edit the ticket).

@Override
public String render(RenderRequest renderRequest, RenderResponse renderResponse, String template) throws Exception {
   return TicketDetailFriendlyUrl.create(ticket.getId());
}

The second method, getUrlViewInContext, will be used by the Search Portlet to render the asset in context. This is an option you can activate in the Search Portlet. The result is that the Asset Entry is shown in its own context unlike the previous where the entry is shown in an Asset Publisher. This is actually the default setting and according to me the nicest solution.

@Override
public String getURLViewInContext(LiferayPortletRequest liferayPortletRequest, LiferayPortletResponse liferayPortletResponse, String noSuchEntryRedirect) throws Exception {
   ThemeDisplay themeDisplay = (ThemeDisplay) liferayPortletRequest.getAttribute(WebKeys.THEME_DISPLAY);
   Group group = themeDisplay.getScopeGroup();
   boolean isPrivate = themeDisplay.getLayout().isPrivateLayout();
   String groupUrl = PortalUtil.getGroupFriendlyURL(group, isPrivate, themeDisplay);

   return groupUrl + TicketDetailFriendlyUrl.create(ticket.getId());
}

Fabricating the Asset Renderer

For assets there is actually a required Factory to be used for the AssetRenderers. But again this is easy to achieve as Liferay provides a BaseAssetRendererFactory for you to extend. It contains three methods: one for the type, one for the class name (both methods for our entity class of course) and one method that returns an AssetRenderer. In that method you will need to create a new instance of your entity based on the incoming id and type. The latter was not required in our case as the renderer is only registered for Objects of type Ticket.
@Override
public AssetRenderer getAssetRenderer(long classPK, int type) throws PortalException, SystemException {
   TicketService ticketService = BeanLocator.getBean(TicketService.class);
   Ticket ticket = ticketService.getTicket(classPK);

   return new TicketAssetRenderer(ticket);
}
Registering the AssetRendererFactory

Unlike the Indexer you are not going to register the AssetRenderer but the AssetRendererFactory in your liferay-portlet.xml. Why else would you have created that class, right? It is easily performed by adding an asset-renderer-factory tag, containing the factory's FQCN, just before the instanceable tag.

<asset-renderer-factory>be.olaertskoen.blog.search.tickets.renderer.TicketAssetRendererFactory</asset-renderer-factory>
Warning
To actually get the portlet started, you will need to add a non-empty portlet.properties into the resources folder. We just added a property plugin.package.name.

If you redeploy your portlet, you will again see some changes in the search results.

Fault Asset Results

Wait, where is the title? Where is the description? You coded that in the AssetRenderer, didn't you! Why are you not seeing any actual result? Why is the portal footer (that Powered By Liferay statement) suddenly shown in the search results? Well, Liferay would like to render an asset, but actually there is no asset. So it just can't render an Asset. If you would open your servers log, you will see an error message like this:

NoSuchEntryException: No AssetEntry exists with the key {classNameId=10458, classPK=1}

While you already had existing entities there were never any AssetEntries created for them unless you already implemented the Asset framework for some other reason. Hence you will need to use that framework into your entity creation process as well.

Creating Assets

You will need to adjust the add, update and delete code of your custom entities. Whenever a new entity is created, an AssetEntry needs to be created as well. When the entity is updated, the corresponding AssetEntry should be updated as well. And when an entity is deleted the corresponding AssetEntry should be deleted as well. No need for orphaned database entries! As you know, Liferay provides you with lots of LocalServices to use their framework. This is also the case for the Asset framework. When you create an instance of your entity you can use the AssetEntryLocalServiceUtil to create the corresponding AssetEntry using the following code:

String ticketFriendlyUrl = TicketDetailFriendlyUrl.create(ticket.getId());

Long userId = Long.valueOf(getExternalContext().getUserPrincipal().getName());
ThemeDisplay themeDisplay = getThemeDisplay();
Long groupId = themeDisplay.getScopeGroupId();


AssetEntryLocalServiceUtil.updateEntry(userId, groupId, ticket.getCreateDate(),
                                       ticket.getCreateDate(), Ticket.class.getName(), ticket.getId(), String.valueOf(ticket.getId()), 0,
                                       new long[] {}, new String[] {}, true, null, null, null, "text/html", ticket.getSubject(),
                                       ticket.getDescription(), ticket.getDescription(), ticketFriendlyUrl, null, 0, 0, null, false);

It is usually a good idea to do this right before the code you added to create the index entry for your entity. Now after you redeploy, you will need to create a new instance of your custom entity. For the existing ones there still are no AssetEntries present in Liferay. But when you create a new instance an AssetEntry will be created automagically (well at least we magicians now know the programmatics behind it). So go ahead and create a new instance, you will probably already have a portlet for that.

Alert
As written earlier you will need to include similar code in your update and delete code for your entities. In this blog post and example portlet these have been left out.

Next try and search for it using one of the properties you declared in the Indexer class and… Tadaa!

Asset Result

Congratulations! You now have a good looking search result. Hm... Wait, it is not really 100%... Why is the FQCN shown as type of the entity? That does not look so nice! And it is even shown twice! Merde, will this journey never end?

Naming your entities

Be sure, our quest will soon come to an end. To provide your entities with a nice name you can again use Liferay's framework (how many rabbits are there in that hat?). All you need to do is add a translation for it, simple as that. If you look closely it is not really the FQCN of your custom entity that is shown in the Search Portlet, it is prepended with model.resource. You can add that entire output into a language hook as the key and provide it with a value (Superentity, Batentity, Spiderentity or whatever you want to name it).

model.resource.be.olaertskoen.blog.search.Ticket=Support Ticket

Deploy this language hook and reload your previous search.

Full Result

Finally, it’s over

Isn’t that nice! You are using Liferay's Search Portlet to search for your own custom entities! What a journey that has been. But our customer was very satisfied with this functionality and I hope yours will be too.

Done

With this you now know how to enable a full fledged search option for your custom JPA entities in Liferay. Just as a reminder, here is a short overview of the steps you took.

  • You started out with creating, registering and using an Indexer class that transforms your custom entities into Lucene Documents.
  • Next you configured the default Liferay Search Portlet to also take your custom JPA entities into account in the search queries. This was an easy configuration in the Portlet itself.
  • Last but not least you created, registered and used an AssetRenderer to nicely render the AssetEntry for your custom JPA entity in the Search Portlet. In this step you also added code to create and maintain that AssetEntry according to the lifecycle of your own entities.
Those three steps are all it takes to enable the search functionality for your custom JPA entities created in Liferay. Enjoy! You can find all the example code for this post in my Github project.

Sources

As mentioned at the start of this blog post, there are several articles and blog posts concerning search and what is required. But we needed to combine several of them to full enable all the features as described in this blog post. Below is a list of the Liferay sources we used. They are still worth reading if you want to know more on what lies behind the scenes or if you need to implement some other specific features.
 
Showing 3 results.