Asset Display Contributors in Action

How to visualize your own asset types using new display pages

Display pages functionality in Liferay always was tightly coupled to the Web Content articles, we never had plans to support more or less the same technology for other types of assets even though we had many of these types: Documents & Media, Bookmarks, Wiki, etc... Even User is an asset and every user has corresponding AssetEntry in the database. But for Liferay 7.1 we decided to change this significantly, we introduced a new concept for the Display Pages, based on Fragments, very flexible, much more attractive than the old ones and...we still support only Web Content articles visualization :). Good news for the developers is that the framework is extensible now and it is easy to implement an AssetDisplayContributor and visualize any type of asset using our great display pages, based on fragments and in this article I want to show you how to do it with an example.

Let's imagine that we want to launch a recruitment site, typical one with tons of job-offers, candidates profiles, thematic blogs etc. One of the main functionalities must be a candidate profile page - some sort of landing page with the basic candidate information, photo, personal summary, and skills. And this task can be solved using new Display Pages.

As I mentioned before, User is an Asset in Liferay and there is a corresponding AssetEntry for each User, it is good since for now, we support visualization only for the Asset Entries. To achieve our goal we need two things, first - an AssetDisplayContributor implementation for the User, to know which fields are mappable and which values correspond to those fields, and second - custom friendly URL resolver to be able to get our users profile page by a friendly URL with the user's screen name in it.

 Let's implement the contributor first, it is very simple(some repeated code is skipped, the full class available on GitHub):

@Component(immediate = true, service = AssetDisplayContributor.class)
public class UserAssetDisplayContributor implements AssetDisplayContributor {

   @Override
   public Set<AssetDisplayField> getAssetDisplayFields(
         long classTypeId, Locale locale)
      throws PortalException {

      Set<AssetDisplayField> fields = new HashSet<>();

      fields.add(
         new AssetDisplayField(
            "fullName", LanguageUtil.get(locale, "full-name"), "text"));

      /* some fields skipped here, see project source for the full implementation */

      fields.add(
         new AssetDisplayField(
            "portrait", LanguageUtil.get(locale, "portrait"), "image"));

      return fields;
   }

   @Override
   public Map<String, Object> getAssetDisplayFieldsValues(
         AssetEntry assetEntry, Locale locale)
      throws PortalException {

      Map<String, Object> fieldValues = new HashMap<>();

      User user = _userLocalService.getUser(assetEntry.getClassPK());

      fieldValues.put("fullName", user.getFullName());

      /* some fields skipped here, see project source for the full implementation */

      ServiceContext serviceContext = ServiceContextThreadLocal.getServiceContext();

      fieldValues.put(
         "portrait", user.getPortraitURL(serviceContext.getThemeDisplay()));

      return fieldValues;
   }

   @Override
   public String getClassName() {
      return User.class.getName();
   }

   @Override
   public String getLabel(Locale locale) {
      return LanguageUtil.get(locale, "user");
   }

   @Reference
   private UserLocalService _userLocalService;

}

As you can see, there are two main methods - getAssetDisplayFields which defines the set of AssetDisplayField objects, with the field name, label and the type (for the moment we support two types - text and image trying to convert to text all non-text values, like numbers, booleans, dates, and lists of strings) and getAssetDisplayFieldsValues which defines the values for those fields using specific AssetEntry instance.  There is a possibility to provide different field sets for the different subtypes of entities like we do it for the different Web Content structures, using the classTypeId parameter.

The second task is to implement corresponding friendly URL resolver to be able to get our profiles by users screen name. Here I'll show only the implementation of the getActualURL method of FriendlyURLResolver interface because it is the method that matters, but the full code of this resolver is also available in GitHub.

@Override
public String getActualURL(
      long companyId, long groupId, boolean privateLayout,
      String mainPath, String friendlyURL, Map<String, String[]> params,
      Map<String, Object> requestContext)
   throws PortalException {

   String urlSeparator = getURLSeparator();

   String screenName = friendlyURL.substring(urlSeparator.length());

   User user = _userLocalService.getUserByScreenName(companyId, screenName);

   AssetEntry assetEntry = _assetEntryLocalService.getEntry(User.class.getName(), user.getUserId());

   HttpServletRequest request = (HttpServletRequest)requestContext.get("request");

   ServiceContext serviceContext = ServiceContextFactory.getInstance(request);

   AssetDisplayPageEntry assetDisplayPageEntry =
      _assetDisplayPageEntryLocalService.fetchAssetDisplayPageEntry(
         assetEntry.getGroupId(), assetEntry.getClassNameId(), assetEntry.getClassPK());

   if (assetDisplayPageEntry == null) {
      LayoutPageTemplateEntry layoutPageTemplateEntry =
         _layoutPageTemplateEntryService.
            fetchDefaultLayoutPageTemplateEntry(groupId, assetEntry.getClassNameId(), 0);

      _assetDisplayPageEntryLocalService.addAssetDisplayPageEntry(
         layoutPageTemplateEntry.getUserId(), assetEntry.getGroupId(), assetEntry.getClassNameId(), 
         assetEntry.getClassPK(), layoutPageTemplateEntry.getLayoutPageTemplateEntryId(),
         serviceContext);
   }

   String requestUri = request.getRequestURI();

   requestUri = StringUtil.replace(requestUri, getURLSeparator(), "/a/");

   return StringUtil.replace(
      requestUri, screenName, String.valueOf(assetEntry.getEntryId()));
}

The key part here is that we need to know which AssetDisplayPageEntry corresponds to the current user. For the Web Content articles, we have a corresponding UI to define Display Page during the content editing. In the case of User, it is also possible to create the UI and save the ID of the page in the DB but to make my example simple I prefer to fetch default display page for the User class and create corresponding AssetDisplayPageEntry if it doesn't exist. And at the end of the method, we just redirect the request to our Asset Display Layout Type Controller to render the page using corresponding page fragments.

That's it. There are more tasks left, but there is no need to deploy anything else. Now let's prepare the fragments, create a Display Page and try it out! For our Display Page, we need 3 fragments: Header, Summary, and Skills. You can create your own fragments with editable areas and map them as you like, but in case if you are still not familiar with the new Display Pages mapping concept I recommend you to download my fragments collection and import them to your site.

When you have your fragments ready you can create a Display Page, just go to Build -> Pages -> Display Pages, click plus button and put the fragments in the order you like. This is how it looks using my fragments: 

Clicking on any editable area(marked with the dashed background) you can map this area to any available field of available Asset Type(there should be 2 - Web Content Article and User). Choose User type and map all the fields you would like to show on the Display Page and click Publish button. After publishing it is necessary to mark our new Display Page as default for this Asset Type, this action is available in the kebab menu of the display page entry:

Now we can create a user and try our new Display Page. Make sure you specified all the fields you mapped, in my case the fields are - First name, Last name, Job title, Portrait, Birthday, Email, Comments(as a Summary), Tags(as Skills list) and Organization(as Company). Save the user and use it's screen name to get the final result:

It is possible to create a corresponding AssetDisplayContributor for any type of Asset and use it to visualize your assets in a brand new way using Fragment-based Display Pages.

Full code of this example is available here.

Hope it helps! If you need any help implementing contributors for your Assets feel free to ask in comments.

Blogs

I got your steps till you created display pages using fragment and marked it as default as well.

 

How you are creating a new user and assigning display pages ?

Using Webcontent ?