There are several options you have for rendering an asset in Liferay DXP. Before setting up the rendering process for your asset, make sure it’s added to the asset framework by following the Adding, Updating, and Deleting Assets tutorial. Once you have your asset added to the framework, Liferay DXP can render the asset by default using the Asset Publisher application. The default rendering process Liferay DXP provides, however, only displays the asset’s title and description text. Any further rendering of your asset requires additional coding. For instance, you might want these additional things:
- An edit feature for modifying an asset.
- Viewing an asset in its original context (e.g., a blog in the Blogs application; a post in the Message Boards application).
- Embedding images, videos, and audio.
- Restricting access to users who do not have permissions to interact with the asset.
- Allowing users to comment on the asset.
Liferay lets you dictate your asset’s rendering capabilities by providing the Asset Renderer framework. Implementing an asset renderer for an existing asset is easy because Liferay offers interfaces and factories to help get your custom asset rendering implemented fast. There are two things you must do to get your asset renderer functioning properly for your asset:
- Create an asset renderer for your custom asset.
- Create an asset renderer factory to create an instance of the asset renderer for each asset entity.
You’ll learn how to create an asset renderer and an asset renderer factory by studying a Liferay asset that already uses both by default: Blogs. The Blogs application offers many different ways to access and render a blogs asset. You’ll learn how a blogs asset provides an edit feature, comment section, original context viewing (i.e., viewing an asset from the Blogs application), workflow, etc. You’ll also learn how it uses JSP templates to display various blog views. The Blogs application is an extensive example of how an asset renderer can be customized to fit your needs.
If you want to create an asset and make it do more than display its title and description, read on to learn more!
Prerequisites for Asset Enabling and Application
To asset-enable your application, you need two things:
-
The application must store asset data. Applications that store a data model meet this requirement.
-
The application must contain at least one non-instanceable portlet.
Edit
links for the asset cannot be generated without a non-instanceable portlet.
Some applications may consist of only one non-instanceable portlet, while others may consist of a both instanceable and non-instanceable portlets. If your application does not currently include a non-instanceable portlet, adding a configuration interface through a panel app will both enhance the usability of the application, and meet the requirement for adding a non-instanceable component to the application. See our tutorial on Adding Custom Panel Apps to learn how to add one.
Now that you have all that taken care of, you can move on to creating an Asset Renderer.
Creating an Asset Renderer
An asset renderer lets you provide your own HTML for your asset. The
AssetRenderer
interface requires that you choose a templating technology (JSP,
FreeMarker, Soy, etc.) to display an asset’s HTML. For this tutorial, you’ll use
JSP templates to render an asset’s HTML. You’ll learn how to associate your JSP
templates with an asset renderer, along with configuring several other options.
To learn how an asset renderer is created, you’ll create the pre-existing BlogsEntryAssetRenderer class, which configures the asset renderer framework for the Blogs application.
-
Create a new package in your existing project for your asset-related classes. For instance, the
BlogsEntryAssetRenderer
class resides in thecom.liferay.blogs.web
module’scom.liferay.blogs.web.asset
package. -
Create your
-AssetEntry
class for your application in the new-.asset
package and have it implement the AssetEntry interface. Consider theBlogsEntryAssetRenderer
class as an example:public class BlogsEntryAssetRenderer extends BaseJSPAssetRenderer<BlogsEntry> implements TrashRenderer {
The
BlogsEntryAssetRenderer
class extends the BaseJSPAssetRenderer, which is an extension class intended for those who plan on using JSP templates to generate their asset’s HTML. TheBaseJSPAssetRenderer
class implements theAssetRenderer
interface. You’ll notice the asset renderer is also implementing the TrashRenderer interface. This is a common practice for many Liferay DXP applications, which enables its assets to use Liferay DXP’s Recycle Bin. -
Define the asset renderer class’s constructor, which typically sets the asset object to use in the asset renderer class.
public BlogsEntryAssetRenderer( BlogsEntry entry, ResourceBundleLoader resourceBundleLoader) { _entry = entry; _resourceBundleLoader = resourceBundleLoader; }
The
BlogsEntryAssetRenderer
also sets the resource bundle loader, which loads the language keys for a module. You can learn more about the resource bundle loader in the Overriding Language Keys tutorial.Also, make sure to define the
_entry
and_resourceBundleLoader
fields in the class:private final BlogsEntry _entry; private final ResourceBundleLoader _resourceBundleLoader;
-
Now that your class declaration and constructor are defined for the blogs asset renderer, you must begin connecting your asset renderer to your asset. The following getter methods accomplish this:
@Override public BlogsEntry getAssetObject() { return _entry; } @Override public String getClassName() { return BlogsEntry.class.getName(); } @Override public long getClassPK() { return _entry.getEntryId(); } @Override public long getGroupId() { return _entry.getGroupId(); } @Override public String getType() { return BlogsEntryAssetRendererFactory.TYPE; } @Override public String getUuid() { return _entry.getUuid(); }
These methods are pretty self-explanatory, but there are a couple things to point out. The
getAssetObject()
method sets theBlogsEntry
that was set in the constructor as your asset to track. Likewise, thegetType()
method references the blogs asset renderer factory for the type of asset your asset renderer renders. Of course, the asset renderer type isblog
, which you’ll set in the factory later. -
Your asset renderer must link to the portlet that owns the entity. In the case of a blogs asset, its portlet ID should be linked to the Blogs application.
@Override public String getPortletId() { AssetRendererFactory<BlogsEntry> assetRendererFactory = getAssetRendererFactory(); return assetRendererFactory.getPortletId(); }
The
getPortletId()
method instantiates an asset renderer factory for aBlogsEntry
and retrieves the portlet ID for the portlet used to display blogs entries. -
If you’re interested in enabling workflow for your asset, add the following method similar to what was done for the Blogs application:
@Override public int getStatus() { return _entry.getStatus(); }
This method retrieves the workflow status for the asset.
-
Another popular feature many developers want for their asset is to comment on it. This is enabled for the Blogs application with the following method:
@Override public String getDiscussionPath() { if (PropsValues.BLOGS_ENTRY_COMMENTS_ENABLED) { return "edit_entry_discussion"; } else { return null; } }
A comments section is an available option if it returns a non-null value. A JSP template defining a comments section is not required. For the comments section to display for your asset, you must enable it in the Asset Publisher’s Options () → Configuration → Setup → Display Settings section.
-
At a minimum, you should create a title and summary for your asset. Here’s how the
BlogsEntryAssetRenderer
does it:@Override public String getSummary( PortletRequest portletRequest, PortletResponse portletResponse) { int abstractLength = AssetUtil.ASSET_ENTRY_ABSTRACT_LENGTH; if (portletRequest != null) { abstractLength = GetterUtil.getInteger( portletRequest.getAttribute( WebKeys.ASSET_ENTRY_ABSTRACT_LENGTH), AssetUtil.ASSET_ENTRY_ABSTRACT_LENGTH); } String summary = _entry.getDescription(); if (Validator.isNull(summary)) { summary = HtmlUtil.stripHtml( StringUtil.shorten(_entry.getContent(), abstractLength)); } return summary; } @Override public String getTitle(Locale locale) { ResourceBundle resourceBundle = _resourceBundleLoader.loadResourceBundle( LanguageUtil.getLanguageId(locale)); return BlogsEntryUtil.getDisplayTitle(resourceBundle, _entry); }
These two methods return information about your asset in a generic way, so the asset publisher can display it. Anything appropriate for your asset can be the title or the summary.
The
getSummary(...)
method for Blogs returns the abstract description for a blog asset. If the abstract description does not exist, the content of the blog is used as an abstract. You’ll learn more about abstracts and other content specifications later.The
getTitle(...)
method for Blogs uses the resource bundle loader you configured in the constructor to load your module’s resource bundle and return the display title for your asset. -
If you want to provide a unique URL for your asset, you can specify a URL title. A URL title is the URL used to access your asset directly (e.g., localhost:8080/-/this-is-my-blog-asset). You can do this by providing the following method:
@Override public String getUrlTitle() { return _entry.getUrlTitle(); }
-
Insert the
isPrintable()
method, which enables the Asset Publisher’s printing capability for your asset.@Override public boolean isPrintable() { return true; }
This displays a Print icon when your asset is displayed in the Asset Publisher. For the icon to appear, you must enable it in the Asset Publisher’s Options () → Configuration → Setup → Display Settings section.
-
If your asset is protected by permissions, you should set permissions for the asset. You can do this via the asset renderer as well. See the logic below for an example used in the
BlogsEntryAssetRenderer
class:@Override public long getUserId() { return _entry.getUserId(); } @Override public String getUserName() { return _entry.getUserName(); } public boolean hasDeletePermission(PermissionChecker permissionChecker) { return BlogsEntryPermission.contains( permissionChecker, _entry, ActionKeys.DELETE); } @Override public boolean hasEditPermission(PermissionChecker permissionChecker) { return BlogsEntryPermission.contains( permissionChecker, _entry, ActionKeys.UPDATE); } @Override public boolean hasViewPermission(PermissionChecker permissionChecker) { return BlogsEntryPermission.contains( permissionChecker, _entry, ActionKeys.VIEW); }
Before you can check if a user has permission to view your asset, you must distinguish the user. The
getUserId()
andgetUserName()
retrieves the entry’s user ID and username, respectively. Then there are three boolean permission methods, which check if the user can view, edit, or delete your blogs entry. These permissions are for specific entity instances. Global permissions for blog entries are implemented in the factory, which you’ll do later.
Awesome! You’ve learned how to set up the blogs asset renderer to
- connect to an asset
- connect to the asset’s portlet
- use workflow management
- use a comments section
- retrieve the asset’s title and summary
- generate the asset’s unique URL
- display a print icon
- set up permissioning for the asset
You may recall that a major cog in asset renderer development generating HTML
using a templating technology. The BlogsEntryAssetRenderer
is configured to
use JSP templates to generate HTML for the Asset Publisher. You’ll learn more
about how to do this next.
Configuring JSP Templates for an Asset Renderer
An asset can be displayed in several different ways in the Asset Publisher, by
default. There are three templates to implement provided by the AssetRenderer
interface:
abstract
full_content
preview
Besides these supported templates, you can also create JSPs for buttons you’d like to provide for direct access and manipulation of the asset. For example,
- Edit
- View
- View in Context
The BlogsEntryAssetRenderer
customizes the AssetRenderer
’s provided JSP
templates and adds a few other features using JSPs. You’ll inspect how the blogs
asset renderer is put together to satisfy JSP template development requirements.
-
Add the
getJspPath(...)
method to your asset renderer. This method should return the path to your JSP, which is rendered inside the Asset Publisher. This is how theBlogsEntryAssetRenderer
uses this method:@Override public String getJspPath(HttpServletRequest request, String template) { if (template.equals(TEMPLATE_ABSTRACT) || template.equals(TEMPLATE_FULL_CONTENT)) { return "/blogs/asset/" + template + ".jsp"; } else { return null; } }
Blogs assets provide
abstract.jsp
andfull_content.jsp
templates. This means that a blogs asset can render a blog’s abstract description or the blog’s full content in the Asset Publisher. Those templates are located in thecom.liferay.blogs.web
module’ssrc/main/resources/META-INF/resources/blogs/asset
folder. You could create a similar folder for your JSP templates used for this method. The other template provided by theAssetRenderer
interface,preview.jsp
, is not customized by the blogs asset renderer, so its default template is implemented.You must create a link to display the full content of the asset. You’ll do this later.
-
Now that you’ve added the path to your JSP, you must include that JSP. Since the
BlogsEntryAssetRenderer
class extends theBaseJSPAssetRenderer
, it already has aninclude(...)
method to render a specific JSP. You must override this method to set an attribute in the request to use in the blog’s views:@Override public boolean include( HttpServletRequest request, HttpServletResponse response, String template) throws Exception { request.setAttribute(WebKeys.BLOGS_ENTRY, _entry); return super.include(request, response, template); }
The attribute includes the blogs entry object. Adding the blog object this way is not mandatory; you could obtain the blog entry directly from the view. Using the
include(...)
method, however, follows the best practice for MVC portlets.
Terrific! You’ve learned how to apply JSPs supported by the Asset Publisher for your asset. That’s not all you can do with JSP templates, however! The asset renderer framework provides several other methods that let you render convenient buttons for your asset.
-
Blogs assets provide an Edit button () that lets you edit the asset. This is provided by adding the following method to the
BlogsEntryAssetRenderer
class:@Override public PortletURL getURLEdit( LiferayPortletRequest liferayPortletRequest, LiferayPortletResponse liferayPortletResponse) throws Exception { Group group = GroupLocalServiceUtil.fetchGroup(_entry.getGroupId()); PortletURL portletURL = PortalUtil.getControlPanelPortletURL( liferayPortletRequest, group, BlogsPortletKeys.BLOGS, 0, 0, PortletRequest.RENDER_PHASE); portletURL.setParameter("mvcRenderCommandName", "/blogs/edit_entry"); portletURL.setParameter("entryId", String.valueOf(_entry.getEntryId())); return portletURL; }
The Asset Publisher loads the blogs asset using the Blogs application. Then the
edit_entry.jsp
template generates the HTML for an editing UI. Once the necessary edits are made to the asset, it can be saved from the Asset Publisher. Pretty cool, right? -
You can specify how to view your asset by providing methods similar to the methods outlined below in the
BlogsEntryAssetRenderer
class:@Override public String getURLView( LiferayPortletResponse liferayPortletResponse, WindowState windowState) throws Exception { AssetRendererFactory<BlogsEntry> assetRendererFactory = getAssetRendererFactory(); PortletURL portletURL = assetRendererFactory.getURLView( liferayPortletResponse, windowState); portletURL.setParameter("mvcRenderCommandName", "/blogs/view_entry"); portletURL.setParameter("entryId", String.valueOf(_entry.getEntryId())); portletURL.setWindowState(windowState); return portletURL.toString(); } @Override public String getURLViewInContext( LiferayPortletRequest liferayPortletRequest, LiferayPortletResponse liferayPortletResponse, String noSuchEntryRedirect) { return getURLViewInContext( liferayPortletRequest, noSuchEntryRedirect, "/blogs/find_entry", "entryId", _entry.getEntryId()); }
The
getURLView(...)
method generates a URL that displays the full content of the asset in the Asset Publisher. This is assigned to the clickable asset name. ThegetURLViewInContext(...)
method provides a similar URL assigned to the asset name, but the URL redirects to the original context of the asset (e.g., viewing a blogs asset in the Blogs application). Deciding which view to render in Liferay DXP is configurable by navigating to the Asset Publisher’s Options () → Configuration → Setup → Display Settings section and choosing between Show Full Content and View in Context for the Asset Link Behavior drop-down menu.
The Blogs application provides abstract
and full_content
JSP templates that
override the ones provided by the AssetRenderer
interface. The third template,
preview
, could also be customized. You can view the default preview.jsp
template rendered in the Add ()
→ Content menu.
You’ve learned all about implementing the AssetRenderer
’s provided templates
and customizing them to fit your needs. Next, you’ll put your asset renderer
into action by creating a factory.
Creating a Factory for the Asset Renderer
You’ve successfully created an asset renderer, but you must create a factory
class to generate asset renderers for each asset instance. For example, the
blogs asset renderer factory instantiates BlogsEntryAssetRenderer
for each
blogs asset displayed in an Asset Publisher.
You’ll continue the blogs asset renderer example by creating the blogs asset renderer factory.
-
Create an
-AssetRenderFactory
class in the same folder as its asset renderer class. For blogs, the BlogsEntryAssetRendererFactory class resides in thecom.liferay.blogs.web
module’scom.liferay.blogs.web.asset
package. The factory class should extend theBaseAssetRendererFactory
class and the asset type should be specified as its parameter. You can see how this was done in theBlogsEntryAssetRendererFactory
class belowpublic class BlogsEntryAssetRendererFactory extends BaseAssetRendererFactory<BlogsEntry> {
-
Create an
@Component
annotation section above the class declaration. This annotation is responsible for registering the factory instance for the asset.@Component( immediate = true, property = {"javax.portlet.name=" + BlogsPortletKeys.BLOGS}, service = AssetRendererFactory.class ) public class BlogsEntryAssetRendererFactory extends BaseAssetRendererFactory<BlogsEntry> {
There are a few annotation elements you should set:
- The
immediate
element directs the factory to start in Liferay DXP when its module starts. - The
property
element sets the portlet that is associated with the asset. The Blogs portlet is specified, since this is the Blogs asset renderer factory. - The
service
element should point to theAssetRendererFactory.class
interface.
- The
-
Create a constructor for the factory class that presets private attributes of the factory.
public BlogsEntryAssetRendererFactory() { setClassName(BlogsEntry.class.getName()); setCategorizable(true); setLinkable(true); setPortletId(BlogsPortletKeys.BLOGS); setSearchable(true); setSelectable(true); }
- linkable: other assets can select blogs assets as their related assets.
- categorizable: blogs can be used to delimit the scope of a vocabulary from the Categories Administration.
- searchable: blogs can be found when searching for assets.
- selectable: blogs can be selected when choosing assets to display in the Asset Publisher.
Setting the class name and portlet ID links the asset renderer factory to the entity.
-
Create the asset renderer for your asset. This is done by calling its constructor.
@Override public AssetRenderer<BlogsEntry> getAssetRenderer(long classPK, int type) throws PortalException { BlogsEntry entry = _blogsEntryLocalService.getEntry(classPK); BlogsEntryAssetRenderer blogsEntryAssetRenderer = new BlogsEntryAssetRenderer(entry, _resourceBundleLoader); blogsEntryAssetRenderer.setAssetRendererType(type); blogsEntryAssetRenderer.setServletContext(_servletContext); return blogsEntryAssetRenderer; }
For blogs, the asset is retrieved by calling the Blogs application’s local service. Then the asset renderer is instantiated using the blogs asset and resource bundle loader. Next, the type and servlet context is set for the asset renderer. Finally, the configured asset renderer is returned.
There are a few variables in the
getAssetRenderer(...)
method you must create. You’ll set those variables and learn what they’re doing next.a. You must get the entry by calling the Blogs application’s local service. You can instantiate this service by creating a private field and setting it using a setter method:
@Reference(unbind = "-") protected void setBlogsEntryLocalService( BlogsEntryLocalService blogsEntryLocalService) { _blogsEntryLocalService = blogsEntryLocalService; } private BlogsEntryLocalService _blogsEntryLocalService;
The setter method is annotated with the
@Reference
tag. Visit the Invoking Liferay Services Locally section of the Finding and Invoking Liferay Services tutorial for more information.b. You must specify the resource bundle loader since it was specified in the
BlogsEntryAssetRenderer
’s constructor:@Reference( target = "(bundle.symbolic.name=com.liferay.blogs.web)", unbind = "-" ) public void setResourceBundleLoader( ResourceBundleLoader resourceBundleLoader) { _resourceBundleLoader = resourceBundleLoader; } private ResourceBundleLoader _resourceBundleLoader;
Make sure the
osgi.web.symbolicname
in thetarget
property of the@Reference
annotation is set to the same value as theBundle-SymbolicName
defined in thebnd.bnd
file of the module the factory resides in.c. The asset renderer
type
integer is set for the asset renderer, but why an integer? Liferay DXP needs to differentiate when it should display the latest approved version of the asset, or the latest version, even if it’s unapproved (e.g., unapproved versions would be displayed for reviewers of the asset in a workflow). For these situations, the asset renderer factory should receive either0
for the latest version of the asset1
for the latest approved version of the asset
d. Since the Blogs application provides its own JSPs, it must pass a reference of the servlet context to the asset renderer. This is always required when using custom JSPs in an asset renderer:
@Reference( target = "(osgi.web.symbolicname=com.liferay.blogs.web)", unbind = "-" ) public void setServletContext(ServletContext servletContext) { _servletContext = servletContext; } private ServletContext _servletContext;
-
Set the type of asset that the asset factory associates with and provide a getter method to retrieve that type. Also, provide another getter to retrieve the blogs entry class name, which is required:
public static final String TYPE = "blog"; @Override public String getType() { return TYPE; } @Override public String getClassName() { return BlogsEntry.class.getName(); }
-
Set the Lexicon icon for the asset:
@Override public String getIconCssClass() { return "blogs"; }
You can find a list of all available Lexicon icons at https://liferay.github.io/clay/content/icons-lexicon/.
-
Add methods that generate URLs to add and view the asset.
@Override public PortletURL getURLAdd( LiferayPortletRequest liferayPortletRequest, LiferayPortletResponse liferayPortletResponse, long classTypeId) { PortletURL portletURL = PortalUtil.getControlPanelPortletURL( liferayPortletRequest, getGroup(liferayPortletRequest), BlogsPortletKeys.BLOGS, 0, 0, PortletRequest.RENDER_PHASE); portletURL.setParameter("mvcRenderCommandName", "/blogs/edit_entry"); return portletURL; } @Override public PortletURL getURLView( LiferayPortletResponse liferayPortletResponse, WindowState windowState) { LiferayPortletURL liferayPortletURL = liferayPortletResponse.createLiferayPortletURL( BlogsPortletKeys.BLOGS, PortletRequest.RENDER_PHASE); try { liferayPortletURL.setWindowState(windowState); } catch (WindowStateException wse) { } return liferayPortletURL; }
If you’re paying close attention, you may have noticed the
getURLView(...)
method was also implemented in theBlogsEntryAssetRenderer
class. The asset renderer’sgetURLView(...)
method creates a URL for the specific asset instance, whereas the factory uses the method to create a generic URL that only points to the application managing the assets (e.g., Blogs application). -
Set the global permissions for all blogs assets:
@Override public boolean hasAddPermission( PermissionChecker permissionChecker, long groupId, long classTypeId) throws Exception { return BlogsPermission.contains( permissionChecker, groupId, ActionKeys.ADD_ENTRY); } @Override public boolean hasPermission( PermissionChecker permissionChecker, long classPK, String actionId) throws Exception { return BlogsEntryPermission.contains( permissionChecker, classPK, actionId); }
Great! You’ve finished creating the Blogs application’s asset renderer factory! Now you have the knowledge to implement an asset renderer and produce an asset renderer for each asset instance using a factory!