You are on page 1of 420

DEVELOPING

for the
LIFERAY PLATFORM II

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.
6.2 R10
Contents

1 Setup 5
1.1 Course Topics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.2 Tools Installation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3 Liferay Training Setup . . . . . . . . . . . . . . . . . . . . . . . . . . 20
1.4 Setting Up The Space Program Portal . . . . . . . . . . . . . . . . . . 33

2 Introduction to AlloyUI 45
2.1 Introducing AlloyUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
2.2 Using AlloyUI Components . . . . . . . . . . . . . . . . . . . . . . . . 51
2.3 AlloyUI Events and Dynamic Content . . . . . . . . . . . . . . . . . . 67
2.4 AlloyUI Best Practices . . . . . . . . . . . . . . . . . . . . . . . . . . 80

3 Liferay’s Social API 87


3.1 Introduction to Liferay’s Social Applications . . . . . . . . . . . . . . . 87
3.2 Using Liferay’s Implementation of the Social API . . . . . . . . . . . . 98
3.3 Publishing Social Activities . . . . . . . . . . . . . . . . . . . . . . . . 109

4 Collaboration 125
4.1 Introduction to Collaborative Applications in Liferay . . . . . . . . . . 125
4.2 Assets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
4.3 Workflow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
4.4 Tags, Categories, and Related Assets . . . . . . . . . . . . . . . . . . 159
4.5 Discussions and Ratings . . . . . . . . . . . . . . . . . . . . . . . . . 166

5 Advanced Service Builder 173


5.1 Remote Services . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
5.2 External Databases . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198
5.3 Custom SQL: Using Finders . . . . . . . . . . . . . . . . . . . . . . . 216
5.4 Custom SQL: Joins . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234
5.5 Dynamic Query API . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244

6 Liferay APIs 255


6.1 Message Bus and Scheduling . . . . . . . . . . . . . . . . . . . . . . 255
6.2 Search and Indexing . . . . . . . . . . . . . . . . . . . . . . . . . . . 274
6.3 Indexer Hooks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281
6.4 Using Friendly URLs . . . . . . . . . . . . . . . . . . . . . . . . . . . 296
6.5 Portlet Data Handlers . . . . . . . . . . . . . . . . . . . . . . . . . . 306
6.6 Search Engine Optimization . . . . . . . . . . . . . . . . . . . . . . . 325
6.7 Using the Recycle Bin . . . . . . . . . . . . . . . . . . . . . . . . . . 329

3
7 RAD with CMS 343
7.1 Rapid Development in Liferay CMS . . . . . . . . . . . . . . . . . . . 343
7.2 Using CMS Structures . . . . . . . . . . . . . . . . . . . . . . . . . . 357
7.3 Understanding Velocity Templates . . . . . . . . . . . . . . . . . . . . 363
7.4 Using the Service Locator . . . . . . . . . . . . . . . . . . . . . . . . 374
7.5 Expando Data Modeling . . . . . . . . . . . . . . . . . . . . . . . . . 387
7.6 Using Custom Variables in Velocity . . . . . . . . . . . . . . . . . . . 401
7.7 Integrating AlloyUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . 409

8 Advanced Topics and Summary 417


8.1 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417
Chapter 1

Setup

1.1 Course Topics

5
COURSE TOPICS

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

DAY 1
Setup
Tools Installation
Liferay Training Setup
Setting Up the Space Program Portal
Alloy UI
Introduction to Alloy UI
Using AlloyUI Components
AlloyUI Events and Dynamic Content
AlloyUI Best Practices

6
DAY 1 (cont.)
Liferay’s Social API
Introduction to Liferay’s Social Applications
Using Liferay’s Implementation of the Social APIs
Publishing Social Activities
Collaboration
Introduction to Collaborative Applications in Liferay
Assets
Workflow
Tags, Categories, and Related Assets
Discussions and Ratings

DAY 2
Advanced Service Builder
Remote Services
External Databases
Custom SQL: Using Finders
Custom SQL: Joins
Dynamic Query API
Liferay APIs
Message Bus and Scheduling
Search and Indexing
Indexer Hooks

7
DAY 3
Liferay APIs (cont.)
Using Friendly URLs
Portlet Data Handlers
Search Engine Optimization
Recycle Bin
RAD with CMS
Rapid Development in Liferay CMS
Using CMS Structures and Templates
Using the Service Locator
Expando Data Modeling
Using Custom Variables in Velocity
Integrating AlloyUI

PROVIDED SOFTWARE
The materials you have been provided should include:
Liferay Developer Studio
The Snippet plugin
The Solutions SDK
MySQL
Java
Exercise Files

8
1.2 Tools Installation
TOOLS INSTALLATION

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

OVERVIEW
This presentation describes the tools necessary for development on a
Liferay Portal platform.
First, we’ll install the JDK and set the JAVA_HOME environment variable.
Next, we’ll set up our database: MySQL.

10
INSTALL JAVA
You should already have the Java JDK installed.
If not, you can download the latest JDK from Oracle:
http://www.oracle.com/technetwork/java/javase/downloads/index.html
JRE vs JDK:
JRE = Java Runtime Environment
Required to run Java applications
JDK = Java Development Kit
Required to develop Java applications

SETUP ENVIRONMENT VARIABLES


When the JDK finishes installation, go to Windows → Control Panel →
System and Security → System.

1. Click on the Advanced system settings link in the left panel.


2. In the System Properties dialog box, click on the Advanced tab.
3. Click the Environment Variables button, then click New under System
Variable.
3.1 Variable name: JAVA_HOME
3.2 Variable value: the install path for the JDK. (e.g. C:\java\jdk6)
4. Click OK.
5. Under System Variables, find the variable Path, click Edit, and add
;%JAVA_HOME%\bin to the end of Path.
6. Click OK → OK → OK.

11
CHECKPOINT! (I)
Verify that JAVA_HOME is correct.

CHECKPOINT! (II)
You must open a new command prompt for the Environment Variables to
take effect!

1. Click Start → Run...


2. Type cmd and press Enter.
3. Type path.
! Make sure there is only one JDK in the Path!

12
CHECKPOINT! (III)
1. Click Start → Run...
2. Type cmd.
3. Type java -version.
! The following message should be displayed:

MYSQL
MySQL is a leading open source database.
It is widely used to power many web sites.
It is small and fast.
Its small footprint makes it ideal for a developer’s machine.

13
MYSQL INSTALL (I) – RUN INSTALLER
A copy of MySQL can be found within the provided material.
If necessary, you can also download MySQL from
http://dev.mysql.com/downloads/mysql/5.5.html#downloads

1. Run the installer.


2. Select Install MySQL Products.

MYSQL INSTALL (II) – INSTALLATION OUTLINE


License Information – agree to license.
Find latest products – download
additional products and updates.
Setup Type – select an existing or
custom setup.
Feature Selection – select from MySQL
server, applications, connectors, and
documentation.
Check Requirements – confirm feature
selections.
Installation – install features.
Configuration – configure MySQL.
Complete – confirm completion.

14
MYSQL INSTALL (III) – LICENSE, PRODUCTS, SETUP TYPE
License Information – Agree to the
license and click Next.
Find latest products – Select an
option to update or skip latest
product updates, and click Next.
Setup Type
1. Select Custom.
2. Click Next, accepting the
default Installation Path and
Data Path.
Note that the installer puts the
MySQL executable in your system
path automatically.

MYSQL INSTALL (IV) – FEATURE SELECTION


1. At a minimum, select the
following features required
for this course:
MySQL Server.
MySQL Workbench (from
Applications).
Connector/J (from
Connectors).
2. Click Next.

15
MYSQL INSTALL (V) – CHECK REQS AND INSTALL
1. Click Next through the Check Requirements phase.
2. For the Installation phase, click Execute to install the products. Then
click Next.

MYSQL INSTALL (VI) – CONFIGURATION


1. Screen 1 of 3: Click Next,
accepting the defaults.
2. Screen 2 of 3:
2.1 Leave the Current Root
Password blank (if prompted).
2.2 Enter root as MySQL Root
Password.
2.3 Click Next.
3. Screen 3 of 3: Click Next,
accepting the default MySQL
server details.

16
MYSQL INSTALL (VII) - COMPLETE
1. Click Finish.
! MySQL is now successfully
installed.

CHECKPOINT! (IV)
1. Open a command prompt and execute
mysql --version

! The version of your MySQL installation is displayed:


mysql Ver 14.14 Distrib 5.5.16, for Win32 (x86)

17
TROUBLESHOOT MYSQL
If MySQL is not found, add it to your system path:

1. In your file explorer, navigate to your MySQL Workbench directory.


C:\Program Files (x86)\MySQL\MySQL Workbench CE 5.2.44

2. Copy the MySQL Workbench directory path to your buffer.


3. Go to your Start menu and right click Computer → Properties →
Advanced System Settings → Environment Variables.
4. Under System Variables, select Path, then click Edit…
5. Append the Path variable value with a semicolon and the MySQL
Workbench directory path wrapped in quotes.
;"C:\Program Files (x86)\MySQL\MySQL Workbench CE 5.2.44"

6. Click OK → OK → OK until all the windows are closed.


! Open a new command prompt for the new setting to take effect.

CREATE MYSQL DATABASE


1. From a command prompt, type:
mysql -u root -p

2. Enter the password and type:


create database lportal character set utf8;

We will use this database in future exercises.

18
Notes:

19
1.3 Liferay Training Setup
LIFERAY TRAINING SETUP

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

DEVELOPER SETUP
We have provided Liferay Developer Studio (LDS), an IDE based on Eclipse
and optimized for Liferay development, with your training materials.
This training course is built around using Liferay Developer Studio (LDS)
as the primary development tool.
Instructions on how to set up other development environments are
provided in your books, so if you don’t plan on using Studio, we
encourage you to look at them on your own.

21
LIFERAY DEVELOPER STUDIO DIAGRAM

LIFERAY DEVELOPER STUDIO


Liferay Developer Studio is the Integrated Development Environment for
writing applications for the Liferay platform.
It provides many conveniences which make it easy for you to get started
implementing your site on Liferay.
The version we are providing to you as a bonus for attending training is
exactly the same as the commercial version.

22
LIFERAY IS TOOL AGNOSTIC
Though we are using Liferay Developer Studio in training, it is important
to note that Liferay is tool agnostic.
You can use anything from a command prompt and text editor to a full
blown IDE to develop for Liferay’s platform.
We use Liferay Developer Studio in training to streamline the setup
process and to help the exercises function more smoothly.

23
SETTING UP LIFERAY DEVELOPER STUDIO
Now that you have Java installed, it is easy to install Liferay Developer
Studio.

1. Find the Liferay Developer Studio zip for your platform. It will be clearly
labeled by OS and platform. Bitsize (32-bit or 64-bit) is very important –
using the wrong bitsize for your platform can cause unexpected issues.
2. Create a directory called C:\Liferay (or ~/liferay on Unix based
systems) and unzip the contents of the archive there.
3. Don’t start Liferay Developer Studio yet! We have some more
configuration to do, and if you start it now, you’ll have to re-install it.

INSTALLATION TROUBLESHOOTING
If you install Liferay Developer Studio into a directory different than the
one specified, you may run into problems.
Windows 8/7/Vista: You cannot install in Program Files - the package
contains files which will need to be edited, and Windows has built in
protection of subfolders of Program Files which can interfere with Liferay
Developer Studio operation.
Linux: You cannot install to any encrypted folders, as encrypted folders
have a 256 character limit on file names. Files in Liferay Developer
Studio will exceed this because of the folder structure.

24
INSTALLING PATCHES AND PLUGINS
Your class materials should include a 00-setup folder. In that folder is
another folder called plugins-patches.
In addition, there are two custom plugins developed for this training,
located in the course-plugins folder of your class materials.

1. Copy the contents (not the folder itself, just the contents) of the
plugins-patches folder into the
liferay-developer-studio/DeveloperStudio/dropins folder.
2. Copy the contents of the course-plugins folder into the dropins
folder as well.
3. Don’t start Liferay Developer Studio yet! We have some more
configuration to do, and if you start it now, you’ll have to re-install it.

INSTALLING THE SNIPPETS PLUGIN


Before you run Liferay Developer Studio for the first time, you should
copy the provided snippets plugin to the drop-ins folder so the snippets
will be loaded upon startup.

1. Copy the provided snippets JAR file to your


liferay-developer-studio/DeveloperStudio/dropins folder.
! The plugin loads when you start Liferay Developer Studio.
If the snippet plugin import fails, you can add the snippets manually.
We’ll show how to do this later, if necessary.

25
LAUNCHING LIFERAY DEVELOPER STUDIO
1. Navigate to liferay-developer-studio and run the executable file.
2. Liferay Developer Studio is based on Eclipse, so it uses workspaces.
Place your workspace in the C:\Liferay\training-workspace
folder, as shown below.

LIFERAY DEVELOPER STUDIO FIRST RUN


Liferay Developer Studio runs a
wizard to help you get started.

1. On the first screen, choose the


default option to use the
embedded Liferay bundle.
2. When prompted for a activation
key, select Browse, and locate the
activation key key provided with
your training materials.

Click Next.

26
CONFIGURE MYSQL DATABASE CONNECTION
1. Select Use internal connection
pool.
2. Choose MySQL as the database.
3. Check the URL to be sure it has
the correct database name.
4. Provide the MySQL user name and
password you selected earlier.
5. Click Next.

IMPORT LIFERAY JAVADOC


1. Click Browse zip... and select the provided
liferay-portal-doc-[version].zip
! Click Next, then Finish.

27
REGISTER A PLUGINS SDK WITH LIFERAY DEVELOPER STUDIO
We have grouped all the solutions to the exercises in this course into a
separate Plugins SDK which we’ve called the Solutions SDK.
Let’s register this SDK with our IDE so that we can import its projects.

REGISTERING THE SOLUTIONS SDK


1. Copy the solutions-sdk from
the provided materials to the
Liferay Developer Studio
home directory.
2. In Liferay Developer Studio,
click Window → Preferences
→ Liferay → Installed Plugins
SDKs.
3. Click Add.
4. Browse to your
solutions-sdk folder in the
LDS installation.
5. Select it and click OK.
! Click OK again.

28
IF YOU GET STUCK
In case you get stuck during the
course of the training, you can
now import the training solutions
into your workspace to examine
them.

UPDATING THE DEFAULT PLUGINS SDK


Liferay was updated with all the fix-packs we added to the dropins
folder; let’s update the default Plugins SDK to the same level.

1. Unzip the provided liferay-plugins-sdk-6.2-ee-[version].zip


file into the liferay-developer-studio directory.
2. In Liferay Developer Studio, click Window → Preferences → Liferay →
Installed Plugins SDKs → Add.
3. Browse to the unzipped SDK.
4. Select it and click OK→ OK.
! Now make the new SDK the default by selecting the checkbox next to it.

29
INSTALLING MySQL SUPPORT
The MySQL driver is not distributed with Liferay Portal EE.
However, Liferay automatically downloads and installs the MySQL driver
when it detects that it should run MySQL.
Since this automatic installation only works if you’re online, we’ve
provided copies of the MySQL driver on your thumb drives.
You need to install this driver before starting Liferay.

1. Copy the MySQL JAR file (mysql-connector-[version].jar) from


your thumb drive to
liferay-portal-[version]/tomcat-[version]/lib/ext.
! Liferay can now connect to MySQL.

MANUALLY IMPORTING SNIPPETS


If the snippet plugin import failed, you can add the snippets manually.

1. Right click in the Snippets view, and click Customize.

30
SELECTING SNIPPETS TO IMPORT
1. Click Import, navigate to the
directory containing the provided
materials for the class and import
the XML files that correspond to
the relevant snippets.

USEFUL ECLIPSE SHORTCUTS


We’ll be editing many files and inserting many snippets throughout this
course.
Consider using the following Eclipse shortcuts:
Ctrl-Shift-F formats a selection or the entire contents of a file if there is no
selection. Ctrl-Shift-F often comes in handy immediately after inserting a
snippet since the whitespace that’s included in the snippet may be
inconsistent with the whitespace that’s already included in the file.
Ctrl-Shift-T opens a search box where you can begin typing a Java class
name and can select the class you’d like to open.
Ctrl-Shift-R opens a search box where you can begin typing a resource
name (any file) and can select the file you’d like to open.

31
LIFERAY PLUGIN NAMING CONVENTIONS
When creating Liferay plugins, you should append the name of the
plugin type to the plugin name.
Failing to follow the Liferay plugin naming convention can cause
deployment issues.
For example, you should append
-portlet to a portlet plugin’s name
-hook to a hook plugin’s name
-theme to a theme plugin’s name
-layouttpl to a layout template plugin’s name
-ext to an Ext plugin’s name
-web to an web plugin’s name
If you omit the suffix from the name of a plugin you’re creating via
Liferay Developer Studio, Developer Studio automatically appends the
correct suffix to the plugin name.

Notes:

32
1.4 Setting Up The Space Program Portal
SETTING UP THE
SPACE PROGRAM PORTAL

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

PORTAL SETUP
We will continue in this course from where we left off in the Mastering
Liferay Fundamentals and Developing for the Liferay Platform 1 courses.
During the Developer Studio setup, we imported data based on content
created in those courses.
Now we need to set up our portal so we can continue its development!
We’ll configure the portal, add portlets to some site pages, and assign
ourselves to the sites so we can navigate there easily throughout the
course.

34
SETUP WIZARD (I)
1. Start your Liferay server by
clicking on the green arrow
button in the bottom left
window of Liferay Developer
Studio.
2. Navigate to localhost:8080 in
your web browser and the
Basic Configuration page
appears.
3. Change the Portal Name to
The Space Program.

SETUP WIZARD (II)


1. Set up a default
administrative user with your
name and email address.
2. Uncheck the Add Sample Data
box.
3. Click Finish Configuration.

35
TROUBLESHOOTING
Your MySQL driver must match the version of the MySQL server that you
have installed.
If you let Liferay install the MySQL driver automatically and Liferay
complains about an inability to access the database during startup,
check to see if Liferay installed a MySQL driver that’s incompatible with
your MySQL server.
Visit http://dev.mysql.com/downloads/connector/ to download a
different driver from MySQL.

SETUP WIZARD (III)


! Once your configuration has been saved, click on Go to My Portal.
You’re not quite done yet: you also need to agree to the Terms and
Conditions, set your password, and password reminder question.

36
SELECTING THE SPACE PROGRAM THEME
1. In your portal, navigate to Admin → Pages and confirm that you’re in
the Public Pages section of the Site Pages area.
2. Under the Look and Feel section, select the Liferay Space theme we
installed in the last chapter.
3. Click Save and click on the Back icon at the top left corner of the page to
return to the site’s home page.
! The Liferay Space theme has been applied to the site’s public pages.

IMPORTING SPACE PROGRAM CONTENT


1. Go to Admin → Site Administration and click Import.
2. Click Select File, select liferay-space.lar and click Continue.
3. Accept the default, click Continue again and then Import.
! Check the Site Administration page to see the imported pages:

37
IMPORTED USERS AND SITES
1. Navigate to Admin → Control Panel → Users and Organizations. You
should see several users and one organization, which includes two
suborganizations.
2. Click on the Colonies organization.
3. You should see the two suborganizations. Click the Actions button for
the Moon Colony and select Assign Users.
4. Click the Available tab, assign yourself to the organization, and click
Update Associations.
5. Repeat the same steps to assign yourself to the Mars Colony
organization.

CREATE SITE PAGES (I)


1. Go back to Site Administration →
Pages by clicking Sites → The
Space Program.
2. In the site selector, click on Moon
Colony.
3. In the Private Pages tab, click Add
Page.
4. Name the page Inventory, and
then click Add Page.
5. Create another private Inventory
page in the Mars Colony site.

38
CREATE SITE PAGES (II)
1. Click My Sites in the Dockbar, then click on the Moon Colony site
! A new, blank page appears.

PLUGINS SDK SETUP


We will be making major enhancements to the Parts portlet that was
developed in the Liferay Platform 1 course.
In order to do this, we need a copy of that portlet as it stood at the end
of that course.
A Plugins SDK containing the Parts portlet has been provided to you in
your course materials.

39
REGISTERING THE dev2-sdk
1. Copy the dev2-sdk from your
provided materials to your Liferay
Developer Studio home folder.
2. In Liferay Developer Studio, click
Window → Preferences.
3. In the dialog that comes up,
select Installed Plugin SDKs from
the Liferay category.
4. Click Add, browse to the location
of your dev2-sdk, and enter the
name dev2-sdk.
5. Click OK.
! Select the dev2-sdk to make it
the default, and click OK.

IMPORTING THE PROJECT


1. Click File → Import.
2. Select Liferay → Liferay Projects
from Plugins SDK and click Next.
3. Select the dev2-sdk, your Liferay
runtime, and the
parts-inventory-portlet
project.
! Click Finish.

40
ECLIPSE CAN BECOME CONFUSED
Sometimes, Eclipse can become confused when Service Builder
generates new classes that it’s watching.
Since we will be building services a lot in this course, we will update the
parts-inventory-portlet project’s configuration to help Eclipse find the
classes that Service Builder modifies.
Specifically, we’ll add the service folder as a source folder to the
project’s Java build path in Eclipse.

ADDING THE SERVICE FOLDER AS A SOURCE FOLDER


1. Right-click on the
parts-inventory-portlet
and select Properties.
2. Select Java Build Path,
and then select the
Source tab.
3. Click Add Folder, and
select the
WEB-INF/service
folder.
4. Click OK, and OK again.
! Confirm that you now
have two source folders
in Eclipse.

41
START LIFERAY AND DEPLOY THE PROJECT
1. If you haven’t already, start Liferay (by clicking the green Start button in
the server section of your workspace at the bottom left).
2. Once Liferay has started, drag the Parts Inventory portlet from the
Package Explorer and drop it onto the Liferay server.
3. Once the application has deployed, go back to your browser, which
should still be on the Inventory page of the Moon Colony site that you
created earlier.

ADD THE PORTLETS


1. Add the Parts portlet to the Moon
Colony Inventory page.
2. Go to Admin → Site Administration,
and click Content. The Manufacturer
portlet should be at the top of the list
here.
3. Navigate to the Mars Colony site, and
add a Parts portlet to the Inventory
Page. If you look in the site’s Site
Administration → Content section,
you’ll see a Manufacturer portlet for
this site as well.
! You’re now ready to begin!

42
Notes:

43
Chapter 2

Introduction to AlloyUI

2.1 Introducing AlloyUI

45
INTRODUCING ALLOYUI

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

MODULE GOALS
To understand AlloyUI and its features
To implement Alloy Components
To use Alloy Events
To understand the Alloy API

46
WHAT IS ALLOYUI? (I)
All platforms need a consistent user interface.
Web applications leverage HTML, CSS, JavaScript, and images to build a
UI.
UI frameworks were born out of a need to unify these technologies in a
consistent API:
jQuery provides an easy-to-use JavaScript library with UI elements (jQuery
UI) and CSS styles.
YUI is another JavaScript library and set of styles to ease development.
Vaadin is a Java-based UI framework to accomplish this task.
UI frameworks are useful, but none provide consistent integration with
Liferay.

WHAT IS ALLOYUI? (II)


AlloyUI is a framework that unifies HTML, CSS, and JavaScript
development in one library.
AlloyUI was designed with Liferay in mind, and integrates seamlessly
with the portal.
Alloy was designed to be:

Consistent
Simple
Maintainable
Customizable

47
ALLOYUI CORE TECHNOLOGIES
AlloyUI brings together presentation technologies for a beautiful UI:

HTML: Alloy provides formulas for building consistent objects with HTML
tags, encapsulated in Java Taglibs.

CSS: Alloy provides hundreds of styles for ease of layout, design, and
customization.

JavaScript: Alloy contains a JavaScript library built on top of YUI3,


making a powerful library even richer.

CORE TECHNOLOGIES: HTML


AlloyUI uses the HTML5 standard.
AlloyUI is still compatible with all major browsers and systems, elegantly
degrading the rendering on older systems.
HTML components and predefined layout formulas are contained in
Taglibs.
Taglibs make it easy for a Java developer to build rich UIs with less code.
All AlloyUI taglibs are available in Liferay Portal out-of-the-box.

48
CORE TECHNOLOGIES: CSS
AlloyUI is fully CSS3 compatible.
AlloyUI provides a full set of CSS styles for layout, design, and event
markup.
CSS styles are progressive, so you can start with the base styles and add
the components you need.
AlloyUI contains tag libraries that apply numerous CSS classes to
pre-defined components.
Any Alloy styles used in taglibs and elsewhere can be modified at the
theme level.

CORE TECHNOLOGIES: JAVASCRIPT


AlloyUI provides a powerful, rich library based on YUI3.
YUI3 provides UI components and events-driven plugins for easily
creating complex applications.
AlloyUI builds upon YUI with more extensions and components.
AlloyUI integrates with Liferay by providing taglibs that use AUI’s
JavaScript components.
AlloyUI works in tandem with Liferay to provide Portal and
portlet-specific functionality.
The base library is only 6kb and loads instantly.
AlloyUI loads plugins and modules asynchronously, using only what you
need.

49
WHY USE ALLOYUI?
With so many frameworks out there, why add another one?
AlloyUI provides a consistent interface to the common UI technologies.
AlloyUI uses a simple way to build consistent interfaces.
Built-in integration with Liferay means your portlets look, feel, and
behave like native Liferay portlets.
AlloyUI was built to gracefully degrade, so older browsers and systems
still look good.
AlloyUI loads quickly and on demand, so you can avoid design overhead.
AlloyUI provides Taglibs and easy-to-use functionality that empowers
Java developers to provide a consistent UI experience with little effort.

Notes:

50
2.2 Using AlloyUI Components
USING ALLOYUI COMPONENTS

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

GOALS
To see the range of AlloyUI components.
To understand how to use Taglib components.
To understand how to use JavaScript components.
To use AlloyUI components in a portlet.
The snippets for this presentation are in the category 01.1-Alloy
Components.

52
ALLOYUI COMPONENTS
AlloyUI comes with more than 60 different components.
Components range from layout and design to animation and interaction.
AlloyUI components provide a consistent look-and-feel across the portal.
Many components are available in the AlloyUI tag library.
Interactive components can be generated on-the-fly with Alloy’s
JavaScript library.
Many components are designed to still function when a user has older
technology, even with JavaScript turned off.

EXAMPLE COMPONENTS: LAYOUT


Alloy provides CSS styles and HTML formulas for predictable layouts.
Developers can take advantage of the <aui:layout> and
<aui:column> tags for rapid results.

53
EXAMPLE COMPONENTS: PANEL
Alloy Panels are effective for grouping and organizing content.
Java developers can build panels with the <aui:panel> tag or by using
the JavaScript object Panel for dynamic, customizable controls.

EXAMPLE COMPONENTS: BUTTONS


Alloy contains many button styles and types, accessible through Java
tags like <aui:button-row> and <aui:button>, and JavaScript
objects like Button and Toolbar.

54
EXAMPLE COMPONENTS: PROGRESS BAR AND TOOLTIPS
Alloy provides many dynamic interface components, including progress
bars and tooltips.
JavaScript objects Tooltip and ProgressBar can create, render,
manipulate, and animate new instances on-the-fly.

HOW TO USE ALLOYUI COMPONENTS


AlloyUI provides two major methods for using components:
HTML markup
JavaScript objects
Shortcuts for HTML markup are provided for Java developers in tag
libraries.
Using the JavaScript objects requires external .js files, or the <script>
tag.

55
HOW TO USE COMPONENTS: TAGLIB
AlloyUI contains many tags for creating HTML structures, using the aui
prefix.
To use them, use the following declaration:

<%@ taglib uri="http://liferay.com/tld/aui" prefix="aui"%>

Liferay Developer Studio and Liferay IDE contain snippets to make it easy
to insert AlloyUI components in your page.

THE JAVASCRIPT SANDBOX


Due to the nature of JavaScript, any code you include on your page,
whether in <script> tags or in an external .js file, is globally available.
This simplistic approach works well for small amounts of code, but
creates problems when more robust libraries and code are included.
Since all code is global, it is easy for separate developers to write
variables and functions with the same names, causing clashes and
hard-to-trace bugs.
There are two common solutions to this issue:
Guaranteed unique names
Locally-scoped code

56
THE JAVASCRIPT SANDBOX
JavaScript, like many programming and scripting languages, has both a
global and a local scope.
Any code placed inside JavaScript functions is locally scoped: nothing
inside the function can be seen outside the function.
By placing a large portion of your application’s code inside functions and
callbacks, your code is said to be sandboxed: it is protected from
outside interference.
The JavaScript Sandbox pattern is used in frameworks everywhere,
including YUI3 and AlloyUI.
By using the AlloyUI approach, all of your JavaScript code can be
sandboxed, preventing possible conflicts from other portlets, the portal,
and even instances of the same portlet.

HOW TO USE COMPONENTS: JAVASCRIPT (I)


AlloyUI is invoked from an external JavaScript file or inline tags.
When using an external file, AlloyUI is invoked with the AUI() function.
All component objects are accessible through this method.
Use AUI().use() to load a component library, declare dependencies,
and sandbox your code.
AUI().use() asynchronously loads any components that were not
loaded by default, making them available to you instantly.

57
HOW TO USE COMPONENTS: JAVASCRIPT (II)
As an example, let’s say we wanted to create a progress bar for dynamic
content we need to load.
In an external file, we first declare our intent to use the progress bar:
AUI().use('aui-progressbar', function(A) {

Then we instantiate a new ProgressBar object:


new A.ProgressBar({
boundingBox: "#dynamicDiv"
}).render();

});

All of this takes place inside a callback function, keeping all your code
local.
Sandboxing the JavaScript code prevents most cases of naming conflicts
and dependency conflicts.

HOW TO USE COMPONENTS: JAVASCRIPT (III)


Instead of placing all of your JavaScript in an external file, you can use
the <script> tag.
However, the <script> tag has no way of knowing when it is loaded,
and whether AlloyUI is available.
AlloyUI provides a custom tag called <aui:script> to make it easier to
incorporate JavaScript with your JSPs.
With <aui:script>, your JavaScript executes when Alloy is loaded, and
is lazy-loaded for better performance.

58
HOW TO USE COMPONENTS: JAVASCRIPT (IV)
Alloy’s <aui:script> tag also makes it easier to declare dependencies.
The tag provides the use attribute to perform the same function as
AUI().use().
The previous example can be rewritten with the new tag as:
<aui:script use="aui-progressbar">
new A.ProgressBar({
boundingBox: '#dynamicDiv'
}).render();
<aui:script>

Though functionally equivalent, the <aui:script> tag results in more


readable code.

ALLOYUI API
AlloyUI provides a rich, deep, and complete JavaScript API.
Components use the same patterns, making it easy to adopt new UI
elements and manipulate existing ones.
More information on AlloyUI at:
http://alloyui.com
http://www.liferay.com/community/liferay-projects/alloy-ui/overview
The JavaScript API is available at:
http://alloyui.com/api/
Comparisons between frameworks:
http://www.jsrosettastone.com (jQuery and YUI)
http://alloyui.com/rosetta-stone (jQuery, YUI, and AUI)

59
USING ALLOYUI COMPONENTS: PARTS INVENTORY
Our Parts Inventory portlet is functional, but basic and plain.
We’ve already seen the power of some AlloyUI components in
<aui:form> and <aui:button>.
Let’s improve the look and functionality of our portlet with AlloyUI
components.
We’ll implement both new tags and JavaScript objects.

EXERCISE: PARTS INVENTORY


Currently, the buttons in our Parts Inventory portlet use the
<aui:button> tag.
This is perfectly functional, but we have little control over the buttons.
We can use the Button component to create new, custom buttons for a
better look and feel.

60
EXERCISE: MANUFACTURER HOUSEKEEPING
First, we need to ready our portlet to allow adding JavaScript
components:

1. Open /html/manufacturer/view.jsp.
2. Replace the opening <aui:button-row> tag with the snippet 01 Button
Row class:

<aui:button-row cssClass="manufacturer-buttons">

This assigns a CSS class to the button row, which makes it easier to
select this area of our view.

EXERCISE: MANUFACTURER BUTTONS (I)


In order to preserve the permission checking, we need to keep the
<c:if> statements intact.
We will implement new components to directly replace the
<aui:button> tags:

1. Replace the first <aui:button> tag (under the


</portlet:renderURL> tag) with the snippet 02 Add Manufacturer
Button.

61
EXERCISE: MANUFACTURER BUTTONS (II)
You may have noticed the JavaScript is surrounded by the new
<aui:script> tag:
<aui:script use="aui-button">
var buttonRow = A.one("#p_p_id<portlet:namespace/>
.manufacturer-buttons");

var buttonLabel = "<liferay-ui:message key="add-manufacturer" />";

var button = new A.Button({


icon: 'icon-plus',
label: buttonLabel,
on: { click: function(event) {
location.href = "<%=addManufacturerURL.toString()%>";}
}
})
.render(buttonRow);
</aui:script>

Alloy loads the aui-button component dynamically, making it available


to our script.

EXERCISE: MANUFACTURER BUTTONS (III)


First, we grab the container object for the buttons, and get the localized
button label:
var buttonRow = A.one("#p_p_id<portlet:namespace/>.manufacturer-buttons");

var buttonLabel = "<liferay-ui:message key="add-manufacturer"/>";

Next, we instantiate the component object. All options are passed in a


JavaScript object map:
var button = new A.Button({
icon: 'icon-plus',
label: buttonLabel,
on: { click: function(event) {
location.href = "<%=addManufacturerURL.toString()%>";}
}
})

Lastly, we render the new component on the page (in our container):
.render(buttonRow);

62
EXERCISE: MANUFACTURER BUTTONS (IV)
1. Find the next <aui:button>, for the permissions button:
<aui:button value="permissions" onClick="<%= permissionsURL %>" />

2. Replace the above with snippet 03 Manufacturer Permissions Button:


<aui:script use="aui-button">
var buttonRow = A.one("#p_p_id<portlet:namespace/>
.manufacturer-buttons");

var buttonLabel = "<liferay-ui:message key="Permissions" />";

new A.Button({
icon: 'icon-gear',
label: buttonLabel,
on: { click: function(event) {
location.href = "<%=permissionsURL %>";}
}
})
.render(buttonRow);
</aui:script>

EXERCISE: MANUFACTURER BUTTONS (V)


The code for the permissions button is almost identical to the code for
the Add Manufacturer button, but with a new label, URL, and icon.
Notice that both buttons use the <portlet:namespace/> tag.
This ensures that we’re using a unique name to refer our portlet
instance.
Notice also that both buttons use <liferay-ui:message> to retrieve
the localized string from Language.properties.
There are many more options for this and other components.
Explore the AlloyUI API to use the complete set of options and
components.

63
CHECKPOINT: MANUFACTURER BUTTONS
! Deploy the portlet (saving your changes in Developer Studio invokes a
re-deploy), and you should see new buttons:

Since we are using JavaScript components, they render after the page
and portlet render, creating a small delay.

EXERCISE: PARTS BUTTONS


Do the same thing to the Parts portlet:

1. Open /html/parts/view.jsp, and replace the opening


<aui:button-row> tag with snippet 04 Parts Button Row class.
2. Replace the first <aui:button> tag with snippet 05 Add Part Button.
3. Replace the second <aui:button> tag with snippet 06 Part Permissions
Button.

64
CHECKPOINT: PARTS BUTTONS
! Re-deploy the portlet (or save in Developer Studio) to see the new
buttons:

REVIEW: ALLOYUI COMPONENTS


Similar procedures can be used to implement the full range of
components in your own portlets.
Some tags are available through the AUI taglib for easy building of HTML
structures.
Alloy’s rich JavaScript API provides an object-based environment for
using components.
View the examples and review the AlloyUI API for a complete reference
of the API and HTML markup.

65
Notes:

66
2.3 AlloyUI Events and Dynamic Content
ALLOYUI EVENTS
AND DYNAMIC CONTENT

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

GOALS
To understand how to use AlloyUI to handle events
To understand how to use AlloyUI to handle asynchronous loading
To use event handling and dynamic content in our portlet
The snippets for this presentation are in the category 01.2-Alloy Events

68
EVENTS
All interface-driven applications need to deal with user input and
feedback.
In addition to user activity, it can be useful to know when the page is
loaded, an element has moved, or a package is finished loading.
Built on top of YUI3, Alloy inherits a rich events framework you can
leverage.
AlloyUI deals with these activities as events: an event is fired when the
user performs an action or on a DOM event, such as loading the page.
In addition to these DOM-related events, Liferay provides events relating
directly to the portal and portlet.
For example, when the portlet has been rendered to the page, Liferay
fires an event.

EVENTS
Events can be categorized in the following ways:
DOM Events: These are normal user interactions like clicks and mouse
overs, or changes to the structure of the Document.
AlloyUI Events: These are events that Alloy fires, such as when a module
is loaded or a component receives interaction from the user.
Liferay Events: These are specific to the portal, such as when a portlet
loads, when all the portlets have loaded, etc.
In JavaScript, these are represented by:
Alloy: DOM Events and AlloyUI events are contained in the Alloy library.
Liferay: Liferay Events are created and handled by the object Liferay.

69
ALLOY EVENTS
When do you use events in Alloy?
User interaction
Click
Double-Click
Mouse over
Drag
DOM Events
Dynamic content loads
Nodes added/removed
Alloy Events
Component is clicked on, dragged, etc.

ALLOY EVENTS
When an event happens (such as when the user clicks), the event is
fired.
Once an event is fired, Alloy calls all objects and methods that are
listening for that event.
Methods and objects that listen for and react to these events are called
handlers.
Attaching an event handler in Alloy is incredibly simple:

on(event, handler);

All objects in Alloy have this method, which means you can listen for
events on any object.
event is a string with the event name, and handler is a method (or
pointer to a method) that is called when the event is fired.

70
CSS SELECTORS
CSS provides a convenient way to reference any part of the HTML
document (referred to as the DOM) through selectors.
A selector is a pattern that you can match elements against, using a
special syntax.
Using selectors, you can refer to elements in the page by name, class,
ID, attribute and more:
Elements are simply the lowercase name of the element: p, div
Classes are matched with a ”.” and Ids with a ”#”: .my-class,
#object-id
Attributes are matched inside brackets []: [name="myDiv"]
Special selectors called pseudo-classes with a ”:” : div:first-child
We can use selectors with Alloy methods (inherited from YUI3) to select
one or more elements on the page.

ALLOY EVENTS
Events can be handled on individual nodes:
A.one("#my-div).on("click", myClickHandler);

Or on a collection of nodes:
A.all("div").on("mouseenter", myMouseHandler);

Or even Alloy itself:


A.on("domready", myReadyHandler);

More selectors can be found at:


http://www.w3.org/TR/css3-selectors/

71
EXERCISE: HANDLING USER EVENTS (I)
To see how we can handle events easily, we’ll modify part of our Parts
Inventory Portlet to make use of keyboard shortcuts.
Just like mouse click, a keyboard press is an event.
Let’s modify our Manufacturer Portlet to make it easier to add a new
manufacturer.

1. Open the view.jsp for the Manufacturer Portlet


(/html/manufacturer/view.jsp), and inside the <aui:script> tag
for the Add Manufacturer button, insert the snippet 01 Manufacturer
Key Shortcut at the end:
A.getDoc().on('key', function() {
button.fire('click');
},'down:77+alt+shift');

EXERCISE: HANDLING USER EVENTS (II)


getDoc() is a shortcut for getting the Document object, similar to:
A.one('document')

Since we are creating a global keyboard shortcut, we attach the event


handler to the HTML Document object.

1. Let’s attach a similar event to the Parts Portlet. Open


/html/parts/view.jsp and at the bottom of the <aui:script> tag
for the Add Part button, insert the snippet 02 Part Key Shortcut:

A.getDoc().on('key', function() {
button.fire('click');
},'down:80+alt+shift');

72
CHECKPOINT: HANDLING USER EVENTS
1. Redeploy the portlet and go to the page with the Parts portlets.
2. Press Alt+Shift+P to open the Add Parts form.
3. Navigate to the Manufacturer portlet in Site Administration → Content.
4. Press Alt+Shift+M to open the Add Manufacturer form.

We first attach a handler to an event called key, which identifies a key


press in the browser.
We pass in a function that acts as the event handler, which fires an
event on the button, simulating a mouse click:

button.fire('click');

After the event, we limit the scope by specifying a key combination to


react to.

KEY COMBINATIONS AND KEY CODES


The key combinations used for capturing events on the keyboard consist
of a key code, followed by optional modifiers.
Modifiers are one of the control (Ctrl), shift (Shift), alt (Alt) or
enter/return (Enter) keys.
Key codes are a numerical representation of the character pressed on
the keyboard.
ASCII represents an old character encoding that is still preserved in the
Unicode Standard.
The first 128 characters of the Unicode Character Set consist of the ASCII
encoding, and can be used as key codes for matching key presses.
The entire character set may be referenced at:
http://www.unicode.org/charts/PDF/U0000.pdf

73
REVIEW: EVENTS
User actions and DOM changes can be represented as Events.
All events can be subscribed to using event handlers.
Alloy provides an easy mechanism through on() to attach handlers to
events.
Rich, interactive experiences can be created through well-designed event
handlers.
Using events and event handlers helps us separate the definition of an
action from the implementation of that action.

ALLOY PLUGINS (I)


AlloyUI provides a wealth of functionality in its JavaScript library, with
both Components and Plugins.
We have already seen some of the power of components, both as
markup patterns and JavaScript objects.
Alloy also provides plugins, JavaScript objects that provide additional
functionality on a component, document, HTML element (node), or
almost anything else.
All plugins can be used by plugging them into the object you want to
use them on:

A.one("#my-plain-div).plug(A.Plugin.NameOfPlugin,
{ key: "value" });

74
ALLOY PLUGINS (II)
Once an Alloy plugin has been plugged in, it is available on the object:
A.one("#my-plain-div").nameOfPlugin

Alloy provides a great number of plugins, many inherited from YUI, with
some additional plugins written for Alloy.
Plugins are a part of the AlloyUI API, and can be found documented at:
http://alloyui.com/api/

DYNAMIC CONTENT
In addition to reacting to user events, document changes, and portal
events, Alloy provides ways to change the content on the page
dynamically.
Rich, dynamic applications can be developed by loading new information
and content without ever leaving the page.
Alloy handles this through the use of the IO plugin, which allows for
AJAX-like requests and other dynamic input/output requests.
We will use the IO Plugin to make a simple modification that enhances
the user experience.

75
EXERCISE: DYNAMIC CONTENT (I)
Let’s modify the Add Parts and Add Manufacturer buttons so that the
form appears in place of the Search Container.

EXERCISE: DYNAMIC CONTENT (II)


Since we’re loading the edit pages dynamically, we want to use the same
renderURL that our button uses.
Before loading the content dynamically, we need a place into which to
load it.
Let’s wrap the Search Container with an <aui:layout> and ID, so we
have an easy way to modify the DOM.

1. Open the Manufacturer view.jsp.


2. Before the opening <liferay-ui:search-container> tag, insert
snippet 03 Manufacturer Layout Before Search Container.
3. After the closing </liferay-ui:search-container> tag, insert
snippet 04 Manufacturer Layout After Search Container.

76
EXERCISE: DYNAMIC CONTENT (III)
The AUI layout tags provide a shortcut for well-formed HTML divs with
consistent styles. We are using it to help control a content area of our
portlet that we can easily modify:
1. In the Manufacturer view.jsp, replace the <aui:script>
</aui:script> section in the hasAddPermission check with the
snippet 05 Manufacturer Button Handler:
<aui:script use="aui-button,aui-io">
...
var contentPane =
A.one("#<portlet:namespace/>manufacturer-dynamic-content");

contentPane.plug(A.Plugin.IO, {
uri: '<%=addManufacturerURL.toString()%>',
selector: '#p_p_id<portlet:namespace /> .portlet-body',
autoLoad: false
});
... on: { click: function(event) {
contentPane.io.start();}
} ...

RETRIEVING DOM OBJECTS


To make loading the content easier, we designated the area around the
Search Container our content-loading area, which Alloy namespaces:
<aui:layout>
<aui:column id="manufacturer-dynamic-content">

Using our CSS Selectors, we can retrieve this area of the page, using the
namespace tag from the portlet taglib:
var contentPane =
A.one("#<portlet:namespace/>manufacturer-dynamic-content");

This same pattern can be used anywhere else in the page to reference
specific divs, spans, and more:
<aui:button-row cssClass="manufacturer-buttons">
...
var buttonRow = A.one(".manufacturer-buttons");

77
USING THE IO PLUGIN
Any plugin can be plugged into a Node:
contentPane.plug()
After specifying the plugin, we provide the configuration options for the
plugin:
contentPane.plug(A.Plugin.IO, {
uri: '<%=addManufacturerURL.toString()%>',
selector: '#p_p_id<portlet:namespace /> .portlet-body',
autoLoad: false
});
Once plugged in, this plugin can be accessed through:
contentPane.io
By default, IOPlugin starts the request behind the scenes and loads
the result in the content div.
By setting autoLoad to false, we can start the dynamic loading anytime
we want:
contentPane.io.start();

EXERCISE: DYNAMIC CONTENT (IV)


Do the same thing for the Parts Portlet:

1. In the Parts view.jsp, locate the <liferay-ui:search-container>


and place the snippet 06 Parts Layout Before Search Container before it,
and place the snippet 07 Parts Layout After Search Container after the
closing </liferay-ui:search-container>.
2. Locate the <aui:script></aui:script> section in the
hasAddPermission check, and replace it with the snippet 08 Parts
Button Handler.

78
CHECKPOINT: DYNAMIC CONTENT
! Deploy the portlet, and view the changes.
Click each of the buttons, and notice how the page loads dynamically in
the main body of the portlet.
You can cancel out, and try using the keyboard shortcuts Shift+Alt+M
and Shift+Alt+P from before, and see that they still work.

Notes:

79
2.4 AlloyUI Best Practices
ALLOYUI BEST PRACTICES

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

WHERE TO USE ALLOY


AlloyUI is a combination of three major technologies: HTML, CSS, and
JavaScript.
AlloyUI can be used in a JSP page, or a separate JavaScript and CSS file.
JSP provides the advantages of using Tag Libraries: shortcuts to formulas
of prebuilt HTML elements with CSS styles.
External CSS files are ideal for customizations that need to be made to
the portlet or theme.
Using Alloy CSS styles on elements, where possible, provides consistency
and trouble-free code across the portlet, portal, and theme.

81
WHERE TO PUT JAVASCRIPT
AlloyUI provides two major mechanisms for injecting JavaScript: the
<aui:script> tag and external JavaScript files.
Use the tag for:
Short, lightweight scripts
Isolated segments of code
Code that needs to be placed in permission checks
Use a separate file for:
Large, complex applications
Easier debugging and error-locating
Control of dependencies and namespacing

ELEMENT STRUCTURE AND NAMING


Use HTML and XML elements for their intended purposes:
divs encapsulate larger blocks of text, images, and code
spans encapsulate segments of text inline
Use unique IDs on elements when needed, IDs are to be not repeated
anywhere.
Alloy’s JavaScript library automatically generates a unique ID on each
DOM Element, prefixed by aui-.

82
JAVASCRIPT AS A CHOICE
JavaScript is a useful and exciting technology to develop rich web
applications for your end users.
Sometimes, JavaScript may be turned off for security reasons or personal
preference.
It has been a long-standing best practice to account for this possibility
when designing such rich applications.
To maximize absolute compatibility with or without JavaScript, make
good use of Alloy’s HTML formulas for components.
You can then use Alloy JavaScript events and components to replace or
enhance HTML elements and content.
By providing a fully functional page without JavaScript, and then using
JavaScript to enhance and add functionality, your web application
gracefully degrades on older systems.

JAVASCRIPT BEST PRACTICES


Be careful to namespace or sandbox code to prevent unwanted collisions.
Make use of AUI().use() in external JavaScript files, to automatically
sandbox code and lazy-load dependencies.
Use Alloy’s JavaScript API to perform actions on the DOM, react to Events
and load Dynamic Content to ensure compatibility and performance
across platforms.
Limit use of JavaScript-only components that load after the page has
loaded.
Since event handlers and animation can take large amounts of memory
and processing time:
Manipulate as few Nodes as possible
Leverage the parent-child relationship
Use plugins and methods that have been tested before writing your own.

83
NAMESPACING FOR INSTANCEABLE PORTLETS
If your portlets are instanceable, then it is likely to have multiple sets of
JavaScript code sections executing on the same page.
If you use selectors to manipulate content, you need to namespace IDs
and methods in order to prevent unwanted collisions and side effects.
When using JavaScript inside a JSP, use the <portlet:namespace />
tag to ensure variables, IDs, and methods are namespaced when needed.

ALLOYUI AND OTHER WEB APPS


AlloyUI is based on the popular Yahoo! UI (YUI) framework.
This framework is well-supported, documented, and tested.
Much of the knowledge gained from AlloyUI can be used in YUI as well.
Anyone familiar with YUI (YUI3 in particular) can already effectively use
AlloyUI.
AlloyUI was designed as an independent framework, not just for
integrating into Liferay.
Any web application in any language (PHP, Ruby, ASP.NET) can use Alloy
as an effective framework.
Keep in mind that only Java web applications can make use of the Taglib
resources included in AlloyUI.

84
Notes:

85
Chapter 3

Liferay’s Social API

3.1 Introduction to Liferay’s Social Applications

87
INTRODUCTION TO LIFERAY SOCIAL
APPLICATIONS

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

MODULE GOALS
To understand social networking and why it’s important
To explore features of Liferay’s Social API
To examine Liferay’s implementation of the Social API
To use user profile pages for social networking
To manage social relations
To implement social activities for the Parts Inventory application

88
SOCIAL NETWORKING TODAY
Social networking has become very popular.
Many web sites, taking their cues from Facebook, Twitter, and LinkedIn,
are building social features into their user experience.
Social web sites have user-generated content as their main reason for
existence.
Users make use of social web sites to connect with each other and share
content.
Users participate because of the relationships they have with each other.

AGGREGATION IS KEY
Aggregation is the central theme of social web sites.
Facebook tries to keep all of your casual, social communication in one
place.
LinkedIn tries to keep all of your professional business networking in one
place.
Twitter gives you a forum for all of your public statements.
We tend to visit fewer individual sites because the sites we do visit
aggregate content so we can more easily find it.
Sounds like what a portal does, doesn’t it?

89
REASONS TO CONSIDER SOCIAL NETWORKING FOR YOUR SITE
Your site begins to become
popular when users can
participate in the ownership of
some content.
People naturally connect with one
another when they can interact
with your site’s content.
Users who have ownership and a
connection with each other are
far more likely to participate.
If people are participating, they
will keep coming back.

BENEFITS OF SOCIAL NETWORKING IN LIFERAY


Social networking creates a more positive user experience by integrating
and aggregating access to content and interaction about content.
Liferay Portal excels at aggregating many different kinds of content.
Liferay’s platform makes it easy for users to upload content, share links,
and communicate.
The industry has shown that users want this instead of separate
applications like email, static web sites, or single-purpose web
applications.
Liferay is well positioned to combine all of these features into one
dynamic experience.

90
FEATURES OF LIFERAY’S SOCIAL API
Liferay’s Social API contains three basic features that you can use to
power your social applications:
Relating to others
Sending social requests
Publishing activities

RELATING TO OTHERS
Relationships are connections between people.
In the API, Liferay calls these social relations.
Currently, the API has two types of social relations: bidirectional and
unidirectional.

91
BIDIRECTIONAL SOCIAL RELATIONS
Co-worker
Friend
Romantic Partner
Sibling
Spouse

UNIDIRECTIONAL SOCIAL RELATIONS


Parent
Child
Supervisor
Subordinate
Enemy
Note: Realistically, an enemy
relation is bidirectional;
however, to keep users from
knowing they have been set as
enemies, it is implemented as
a unidirectional social relation.

92
EXTENDING SOCIAL RELATIONS
If you wish, you can use a hook
to provide an extension of TYPE_BI_CONNECTION= 12;
Liferay’s Social Relations. TYPE_BI_COWORKER = 1;
TYPE_BI_FRIEND = 2;
Each Liferay social relation is TYPE_BI_ROMANTIC_PARTNER = 3;
TYPE_BI_SIBLING = 4;
defined as a public static TYPE_BI_SPOUSE = 5;
final int in TYPE_UNI_CHILD = 6;
SocialRelationConstants.java TYPE_UNI_ENEMY = 9;
TYPE_UNI_FOLLOWER = 8;
in the Liferay source. TYPE_UNI_PARENT = 7;
TYPE_UNI_SUBORDINATE = 10;
Create a new class that extends TYPE_UNI_SUPERVISOR = 11;
this one and use it to define your
own social relationships.

SOCIAL REQUESTS
Social requests are one implementation of a pattern found in Liferay
called a feed pattern.
Requests go into a feed; the feed is read and then interpreted by an
interpreter object.
The interpreter converts the data from its generic form as a feed entry to
its real form as a Java object representing a persisted entity (and back).
In this way, one user can submit a request for a social relation with a
user.
The Requests portlet, which ships in the Liferay core, can read the
request feed and display it to the other user.
The other user can then take action (approve or deny the request), and
the request is turned by the interpreter back into an object that can be
manipulated and persisted.

93
LIFERAY’S FEED PATTERN

TWO FEED PATTERNS IN THE SOCIAL API


There are two implementations of the feed pattern in Liferay’s Social API:
Requests
Activities
Since we have a ready implementation of requests in Liferay’s Social
Networking portlet, we’ll make use of it in our project.
To show you the pattern in more detail, we’ll use it to publish activities
to the activities feed.
Later, we’ll see more instances of the feed pattern in the Collaboration
API.

94
SOCIAL ACTIVITIES
Users perform activities on your
web site.
Blog posts
Forum posts
Wiki articles
Adding content from your
applications
Any of these can be published as
a social activity.

FACEBOOK INTEGRATION
In addition to using Liferay as a platform for a social web site, you can
also use Liferay as a platform for Facebook applications.
Liferay makes it very easy to serve your applications on Facebook and
take advantage of Facebook’s API.
To add a Liferay portlet as an application on Facebook, you must first get
a developer key. A link for doing this is provided to you in the Facebook
tab in any portlet’s Configuration screen.
Follow the link to create the application on Facebook and get the key and
canvas page URL. Once you’ve done this, you can copy and paste their
values into the Facebook tab. Your portlet is now available on Facebook.
This integration enables you to make things like Message Boards,
Calendars, Wikis, and other content on your portal available to a much
larger audience (unless you already have a billion users on your site, in
which case, kudos to you).

95
EASY FACEBOOK INTEGRATION

SUMMARY
Liferay’s Social API contains everything you need to build social features
into your web site.
Additionally, you can use Liferay as a platform to build social
applications for Facebook.
All the building blocks are already there; all you need to do is assemble
them.
As we’ll see next, Liferay provides default implementations of many of
these features to help you get started.

96
Notes:

97
3.2 Using Liferay’s Implementation of the Social API
USING LIFERAY’S IMPLEMENTATION
OF THE SOCIAL API

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

GOALS
To understand how to use Liferay’s out-of-the-box social networking
portlets
To configure user profiles for optimal use of social networking
To prepare our site for future social applications

99
LIFERAY’S SOCIAL NETWORKING PORTLETS
Liferay has a default implementation of many of the social networking
features that its API provides.
These are in the Social Networking portlets, which you’ll find in Liferay’s
repository.
Because we’ll be looking at the feed pattern many times in this course,
we’ll take advantage of Liferay’s implementation in this case.

INSTALLING LIFERAY’S SOCIAL NETWORKING PORTLETS


You already installed the Social Networking portlets at the beginning of
this course, when you set up Developer Studio.
The social-networking-portlet-[version] plugin is included in
the Social Networking EE app which is available from Liferay Marketplace.

1. Start Liferay and look for the following console message to indicate a
successful installation:
21:40:05,936 INFO
[pool-2-thread-5][HookHotDeployListener:690] Hook for
social-networking-portlet is available for use

100
SOCIAL PORTLETS
1. Log in to Liferay with your default
administrator account.

! When the social networking


plugin has been installed, its
portlets appear in the Social
category of the Dockbar’s Add →
Applications menu.

SOCIAL APPLICATIONS ON USER PROFILE PAGES


User profile pages are an ideal place to make use of social applications
that have to do with connecting users in social relations.
We’ll configure Liferay so that user profile pages automatically contain
the social portlets that we want to use.
We can use user group sites to apply our configurations to the public
and private pages of users’ personal sites.
We’ll create two user group site pages: one for users’ public pages and
one for their private pages.
Since we want to update the personal sites of all users, we’ll add our
user group to the default user associations.

101
EXERCISE: CREATING A USER GROUP FOR ALL USERS
1. Navigate to the Control Panel and click on User Groups.
2. Click Add, enter the name All Users and a description, and click Save.
3. Click on the Actions button next to the newly created All Users user
group and select Manage Site Pages.
4. With the Public Pages tab selected, click Add Page. Enter the name
Profile and click Add Page again to create the page.
5. Next, click on the Private Pages tab and add a private page named Home.

EXERCISE: SETTING UP THE PROFILE PAGE


1. Navigate back to the User Groups
page of the Control Panel, either
by clicking the User Groups link
or the back arrow icon .
2. Click on the Actions button
corresponding to the All Users
group, and select Go to the Site’s
Public Pages to open the Profile
page in a new browser tab.
3. Add the Summary, Search,
Activities, and Wall portlets to this
page and arrange them according
to the scheme on the next slide.

102
EXERCISE: PROFILE PAGE CONFIGURATION

EXERCISE: SETTING UP THE HOME PAGE


1. Navigate back to the User Groups page of the Control Panel, click on the
Actions button corresponding to the All Users group, and select Go to the
Site’s Private Pages to open the Home page in a new browser tab.
2. Add the Requests, Dictionary, My Sites Directory, Friend’s Activities, My
Sites, and Calendar portlets to this page and arrange them according to
the scheme on the next slide.

Note: The Requests Portlet will be invisible until a social request has
been received.

103
EXERCISE: HOME PAGE CONFIGURATION

EXERCISE: ADDING OUR USER GROUP TO THE DEFAULT USER


ASSOCIATIONS
1. Click on Portal Settings in the
Control Panel, then on the Users
link to the right, and finally on
the Default User Associations tab.
2. Type the name of the user group
you created, All Users, into the
User Groups text field, check the
Apply to existing users box, then
click Save.

104
EXERCISE: ASSIGNING EXECUTIVE USERS TO THE COLONIES
We need our Executive users to have access to the Parts Inventory
Portlet in the Moon Colony and Mars Colony sites; let’s assign them to
the organizations now.

1. Navigate to the Users and Organizations section of the Control Panel.


2. Click the Colonies organization.
3. You should see two suborganizations. Click the Actions button for the
Moon Colony and select Assign Users.
4. Click the Available tab and assign both One Executive and Two Executive
to the organization.
5. Click Update Associations to confirm your selections.
6. Repeat the same steps to assign One Executive and Two Executive to the
Mars Colony organization.

EXERCISE: MAKING RELATIONSHIPS (I)


1. Log in to Liferay as executive1@spaceprogram.liferay.com (the password
for the users imported by the user import hook is liferay).
2. Navigate to your public Profile page: click on your name at the top right
corner of the Dockbar, click on My Profile, then navigate to your Profile
page (that was imported from the All Users user group).
3. Note the URL of the page from the user group:
http://localhost:8080/web/executive1/~/[ID]/profile
4. Log out and log in as executive2@spaceprogram.liferay.com with the
password liferay.
5. Navigate to your public Profile page: click on your name at the top right
corner of the Dockbar, click on My Profile, then navigate to your Profile
page.
6. Note the URL of the Profile page:
http://localhost:8080/web/executive2/~/[ID]/profile

105
EXERCISE: MAKING RELATIONSHIPS (II)
1. Change the URL to http://localhost:8080/web/executive1/~/[ID]/profile to
visit the public Profile page of executive1@spaceprogram.liferay.com.
2. Note that the Wall portlet states that you have to be his friend to write
on his wall.
3. Click the Ask One to be your friend link in the Wall portlet.
4. Log out and log back in as executive1@spaceprogram.liferay.com.
5. Navigate to your private Home page: click on your name at the top right
corner of the Dockbar, click on My Dashboard, then navigate to your
Home page.
6. Confirm the friend request in the Requests portlet so that the two
executives become friends.

BENEFITS OF SOCIAL RELATIONSHIPS


Now if executive1 navigates to the profile page of executive2, executive1
can write on executive2’s wall, and vice versa.
This is because the wall portlet has been coded for relationships.
Rather than checking permissions to see if a user can see the wall, it
instead checks to see if the user viewing the wall has a relationship with
the user who owns the wall.
Let’s examine the Wall portlet to see how to perform these checks.

106
GETTING THE GROUP

Group group = GroupLocalServiceUtil.getGroup(scopeGroupId);


Organization organization = null;
User user2 = null;
if (group.isOrganization()) {
Organization =
OrganizationLocalServiceUtil.getOrganization(group.getClassPK());
}
else if (group.isUser()) {
user2 = UserLocalServiceUtil.getUserById(group.getClassPK());
}

We need to get the user who owns the current page so we can compare
it with the user who is browsing the page.

CHECKING FOR RELATIONSHIPS

<c:choose>
<c:when test="<%= themeDisplay.isSignedIn() &&
((user.getUserId() == user2.getUserId()) ||
SocialRelationLocalServiceUtil.hasRelation(user.getUserId(),
user2.getUserId(),
SocialRelationConstants.TYPE_BI_FRIEND)) %>">

[logic for displaying the wall goes here]

</when>
</choose>

1. If the user is browsing his/her own profile, or


2. If the user has the Friend relation with the page owner,
! Show the wall.

107
DIFFERENT RELATIONSHIP CHECKS
You could use this to enable all the different relationship types Liferay
supports and show different content for each one of them.
You can also extend SocialRelationConstants.java with a hook
and provide your own relationship types.

Notes:

108
3.3 Publishing Social Activities
PUBLISHING SOCIAL ACTIVITIES

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

GOALS
To implement our first example of Liferay’s feed pattern
To publish our own custom activities to Liferay’s Activities portlets
The snippets for this presentation are in the category 02-Social Apps

110
ACTIVITIES AS THE CORE OF THE SOCIAL EXPERIENCE
Once relationships have been formed, social activities become central to
the user experience.
Users will want to see what their friends are doing and then respond.
Any application you write can publish social activities.
We’ll configure the Parts Inventory application to publish the addition of
new parts as social activities.
When a new part is created, a corresponding social activity will also be
created.
When the part is deleted, the corresponding social activity will be
deleted.

EXERCISE: BUILDING THE SERVICE LAYER (I)


Social activities follow the feed pattern found throughout Liferay.
To implement social activities, we need to start at the service layer.

1. In Liferay Developer Studio’s package explorer, open the Parts Inventory


project that you imported earlier from the dev2-sdk.
2. Then open the service.xml file in the
/parts-inventory-portlet/docroot/WEB-INF directory.
3. Add the contents of snippet 01-service.xml to the bottom of both entities.
4. Save the file and re-run Service Builder.
5. Create a new Java package called
com.liferay.training.parts.social.
6. Create a new Java class in this package called PartActivityKeys.
7. Add the contents of snippet 02-PartActivityKeys to the body of this class.

111
EXERCISE: BUILDING THE SERVICE LAYER (II)
1. Open the PartLocalServiceImpl class and replace the addPart()
method with the contents of snippet 03-addPart.
2. Also, replace the deletePart(Part part) method with the contents
of snippet 04-deletePart.
3. Hit Ctrl-Shift-O to organize imports.
4. Open the PartsPortlet class and replace the call to
PartLocalServiceUtil.addPart...() in the addPart() method with
the contents of the 05-addPartWithServiceContext snippet.
5. Remove the userId variable declaration since we’re using a
serviceContext parameter instead of userId in the call to
addPart(...).
6. Hit Ctrl-Shift-O to organize imports.
! Save all files and re-run Service Builder.

SERVICE REFERENCES
The first thing we just did was add two references to other services to
our entity.
This allows us to call that entity’s services from within ours.
The other entity is injected by Spring.

112
ACTIVITY KEYS
Next, we created a class to hold constants for our activity keys.
This class defines every activity that our application might want to
publish.
In our example, we provided a key for adding a part and a key for
deleting a part.
This class is nothing but a simple Java class that holds the constants.

CALL THE SOCIAL ACTIVITY SERVICE


Next, with our references and keys in place, we modified the service
method so that when a Part is saved, an activity gets published as well:

socialActivityLocalService.addActivity(userId,
part.getGroupId(), Part.class.getName(), part.getPartId(),
PartActivityKeys.ADD_PART, StringPool.BLANK, 0);

We also changed the signature of this method to use a special object


called ServiceContext.

113
WHAT IS ServiceContext?
ServiceContext is an object which contains common context
information about the request that you are likely to need.
It contains information such as the current user ID, the current company
ID, and the current URL.
We’re beginning to use it now, and we’ll find it to be useful throughout
the rest of this course.

A NOTE ON CROSS SITE SCRIPTING


You’ll notice as you look through the code that in places where we pass
data through a URL, we’ve wrapped that data in the
HtmlUtil.escape() class.
When creating a web application, it is very important that all data being
passed through a URL is escaped, otherwise a cleverly written script
could be inserted through a form field and execute malicious code.
Throughout the course, you’ll notice more places where we escape data
using HtmlUtil or use Alloy UI’s escapedModel="true" attribute for
the search container.

114
MODIFY THE PORTLET CLASS
Finally, since we changed the method signature of the addPart()
method in our service layer, we needed to change the call to that
method in the portlet class.
The new version of the method passes only two objects: the Part to be
added and the ServiceContext object, which contains the rest of the
information we need:

ServiceContext serviceContext =
ServiceContextFactory.getInstance(Part.class.getName(), request);
PartLocalServiceUtil.addPart(part, serviceContext);

After this, we re-ran Service Builder to propagate the method signature


change up to the interface.

SO WHAT DOES ALL THIS CODE DO?


When our entity is saved, we now also add a social activity.
This is a very similar concept to adding resources, which we did in the
Developing for the Liferay Platform 1 course to enable us to use Liferay
permissions.
But we’re only halfway there: now that we can add activities, we need
to be able to publish them to the Activities portlets.

115
THE ACTIVITIES PORTLET
The activities portlet changes
based on the kind of page it has
been placed upon.
There are two scenarios that
change its display:
Whether the portlet resides in
the same site in which the
activity occurred
Whether it’s in a different site
than the one in which the
activity occurred

DIFFERENT MESSAGES
If it’s in the same site:
Some dude did this thing.
If it’s in a different site:
Some dude did this thing in Site X.
Because of this, we’ll need to provide two messages per published
activity.

116
MESSAGES AND CLASSLOADERS
Messages from plugins can’t display in the Activities portlet, which runs
in Liferay’s class loader. This is a Java EE spec limitation.

HOOKS FIX THE ISSUE


Liferay hooks can get your messages into Liferay’s class loader,
bypassing the Java EE limitation.

117
EXERCISE: ADDING A HOOK TO OUR PROJECT
1. Choose File → New → Liferay Hook Configuration in Liferay Developer
Studio and select the parts-inventory-project for the hook plugin project.
2. Check the Language properties box and click Next.
3. Change the content folder to the value below:
/parts-inventory-portlet/docroot/WEB-INF/src/content-portal

4. Click Add next to Language property files and enter the filename
Language.properties.
5. Click Finish and open the docroot/WEB-INF/liferay-hook.xml file
that Liferay Developer Studio created.
6. Add the snippet 06-liferay-hook.xml inside the <hook></hook> section
of the liferay-hook.xml file.
7. Save and close the file.

EXERCISE: ADDING PORTAL-WIDE LANGUAGE PROPERTIES


1. Add the contents of snippet 07-Language Properties to the
docroot/WEB-INF/src/content-portal/Language.properties
file that Liferay Developer Studio created.
2. Open the project’s build.xml file.
3. Beneath the import statement, add the contents of snippet 08-Ant Target
to the file.
! Save and close both files.

118
EXERCISE: RUNNING THE ANT BUILD-LANG TARGET
1. From Liferay Developer Studio’s Liferay perspective, drag the build.xml
file from your Package Explorer window to the Ant tab at the bottom
right corner of the screen.
2. Run the Ant build-portal-lang target to test that your build setup works –
this also creates translations of the language properties that you’ll need
later.

HOOK LIMITATIONS
Use multiple hooks with caution!
Deployment order is not guaranteed.
Liferay happily deploys two hooks that customize the same feature.

119
SOCIAL ACTIVITY INTERPRETERS
The final step in enabling our portlet to publish social activities is to
create a social activity interpreter.
This is a class that translates data from your entity into a more generic
form that can be published to an activity feed.
The generic form has several fields you can populate with your data:
link—a URL link to your application
key—the language key to be displayed
title—a human readable label for your entity
body—a text field which displays information from your entity

EXERCISE: ADDING A SOCIAL ACTIVITY INTERPRETER


1. Right-click on the package com.liferay.training.parts.social and select
New → Class.
2. Create a class called PartsSocialActivityInterpreter that
extends BaseSocialActivityInterpreter.
3. Remove the body of the class and then replace it with snippet 09-Social
Interpreter. Hit Ctrl-Shift-O to organize imports.
4. Open liferay-portlet.xml from the WEB-INF folder.
5. In the Parts portlet, just below the template handler definition, add
snippet 10-Social Interpreter XML.
! Save both files.

120
TWO PIECES
As you can see, the social activity
interpreter takes two pieces to
work:
Declaration in
liferay-portlet.xml
Interpreter class
implementation
When Liferay initializes the
portlet, it instantiates the class
defined in the configuration file.

HOW DOES IT WORK?


When the Activities portlet is processing the feed, it passes the activity
to your interpreter.
The activity has the primary key of your entity in it.
It’s your job when writing the interpreter to get your entity and translate
it to a SocialActivityFeedEntry.
This is then passed back to the Activities portlet for display.

121
INTERPRETING
You must implement a doInterpret() method which returns a
SocialActivityFeedEntry, as well as an array which contains the
names of classes this interpreter knows how to interpret.
This method can do all the work or it can delegate some of the work to
other methods.
In our example, doInterpret() does most of the work, and it
delegates the title to a separate method.
This method selects the correct language key for the language the user
has configured.
This ensures that the activities feed appears in the correct language for
the user.
We did not implement links because they rely on Friendly URLs, which
we’ll cover later in the course.

EXERCISE: ACTIVITY FEEDS IN ACTION


1. To test your activity feeds, grant the User role permission to add Parts
(you might need to log in as an Administrator and create a Manufacturer
first).
2. Log in as executive1@spaceprogram.liferay.com, add a Part, and check to
see the activity on executive1’s public page.
3. Log in as executive2@spaceprogram.liferay.com, browse to his private
page, and look for the activity in the Friends’ Activities portlet.

122
BONUS EXERCISE
BONUS: Add a social activity interpreter to the Manufacturer portlet,
following all of the steps in this presentation.

CONNECTING WITH SOCIAL MEDIA (I)


Liferay also provides a simple method of sharing content from your
portal with popular social networks.
Using the following taglib in your JSP, you can add share buttons for
Facebook, Twitter, and Google+:
<liferay-ui:social-bookmarks
displayStyle="horizontal"
target="_blank"
title="<%= part.getName() %>"
url="<%= PortalUtil.getCanonicalURL(
(PortalUtil.getCurrentURL(request)),
themeDisplay, layout) %>" />

Note: This shares the specific URL on your site.

123
CONNECTING WITH SOCIAL MEDIA (II)
Since our app appears on a private page, this doesn’t make a lot of
sense for our site but this is how it would look:

Notes:

124
Chapter 4

Collaboration

4.1 Introduction to Collaborative Applications in Liferay

125
INTRODUCTION TO COLLABORATIVE
APPLICATIONS IN LIFERAY

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

MODULE GOALS
To understand Liferay’s asset system and how it powers many of
Liferay’s features
To use the asset system to publish custom data
To learn how to use Liferay’s workflow API with Kaleo workflow
To enable users to tag and categorize your content
To enable users to add discussions and ratings to your content

126
LIFERAY’S ASSET SYSTEM
The Asset system is very similar to social activities.
Entities are converted to assets, which can appear not in feeds, but in
special queries in the Asset Publisher portlet.
Additionally, assets are used in the back end by several collaborative
features of Liferay, such as ratings, tags, discussions, and workflow.
For this reason, we need to enable our application to create assets that
Liferay can access before we can use any of these features.

THE ASSET PUBLISHER


In the Mastering Liferay Fundamentals
course, we used the Asset Publisher
to display different kinds of content
from the Space Program’s web site.
As you can see, the Asset Publisher
brings together many different types
of content into one place.
Clicking one of the links will show the
content in the Asset Publisher.
By adding assets to our application,
we can publish our data in the Asset
Publisher as well.

127
LIFERAY WORKFLOW
Liferay’s workflow is an API on top of a pluggable mechanism that allows
for different workflow engine implementations.
As a developer, you won’t need to worry about the details of the
particular workflow engine a user has installed.
Instead, you can enable workflow for your application, and it works with
every workflow engine Liferay supports.
The API is straightforward and simple to use.
Liferay’s default workflow engine is called Kaleo, which means ”called”
in Greek.
Other supported workflow engines include jBPM and Activiti.
For training, we will use Kaleo.

TAGS AND CATEGORIES


When you have content going into your portal, it helps users to find
content if you tag or categorize it.
Tags are metadata that users attach to content either during its creation
or after it has been created.
For example, an article talking about humpback whales in the northern
Pacific Ocean might have the tags whale, humpback, and Pacific Ocean.
Categories are a hierarchical structure of metadata that’s predefined by
administrators as ”buckets” for content.
If an alien race contacted Earth searching for whales, it might help if
your humpback whales article was categorized under Mammal → Large
→ Whale → Humpback Whale.
Think of categories as a table of contents and tags as an index.

128
DISCUSSIONS AND RATINGS
Liferay allows you to add
discussions about your content
very easily.
These work very much like
message boards.
Ratings are just as easy to add,
and enable users to give content
a score.
You can use thumbs up / thumbs
down or stars to score your
content.

SUMMARY
Liferay Portal gives you all the collaborative tools you need to empower
your users.
Users who can interact with content on your site will use these features
and keep coming back.
Collaboration works hand in hand with social networking to help you
build communities around your content.
Next, we’ll see how you can use these features in your applications.

129
Notes:

130
4.2 Assets
ASSETS

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

GOALS
To implement Liferay’s Asset system in a portlet
To use Liferay’s Asset Renderer
The snippets for this presentation are in the category 03-Collaboration-1.

132
ASSETS POWER COLLABORATION
Assets underlie Liferay’s collaborative features.
They are a way of referring to your entities in a generic way that can be
published across the portal.
Assets use the feed pattern which we’ve already seen in the Social API.
To asset enable your application, you will do almost the exact same
things you did to add social features to your application:
Add some references and fields to service.xml
Add assets in your service layer
Create an AssetRenderer which can translate your entity to an asset
and back

PREPARING OUR ENTITIES FOR ASSETS AND WORKFLOW (I)


A number of collaborative features of Liferay depend on assets.
Workflow is one of these collaborative features, and in addition to
assets, it requires some extra fields to be added to our entities.

1. Open the service.xml file and for both entities, add the attribute
uuid="true". They should read like this:
<entity name="Manufacturer" uuid="true" local-service="true"
remote-service="false">
<entity name="Part" uuid="true" local-service="true" remote-service="false">

1. Add the contents of snippet 01-Workflow Fields to the bottom of the


Other Columns section of the Manufacturer and Part entity; if there is
already a userName column, delete one of them.
2. Add the contents of snippet 02-Asset References to the Manufacturer and
Part entities, after the last finder.

133
PREPARING OUR ENTITIES FOR ASSETS AND WORKFLOW (II)
1. Add the contents of snippet 03-Status Finder to the Finder Methods
section of the Manufacturer entity.
2. Save the file and run Service Builder.

To publish only workflow approved Manufacturers in the Asset Publisher,


we need a finder that returns only approved Manufacturers.

3. Open ManufacturerLocalServiceImpl and add the contents of the


04-Get Mfg By Status snippet as the last method in the class.

ADDING UUIDs TO OUR SERVICE CLASSES


We should also include UUIDs in the addManufacturer and addPart
methods in our service classes.

1. Open ManufacturerLocalServiceImpl and add the contents of the


05-set-manufacturer-uuid snippet in the addManufacturer method,
just above the line manufacturer.setCompanyId(...)
2. Open PartLocalServiceImpl and add the contents of the
06-set-part-uuid snippet in the addPart method, just above the line
part.setCompanyId(...)

134
WHAT IS A UUID?
A UUID is a Universally Unique Identifier.
It is used by Liferay to make sure that an entity has a unique ID,
regardless of the system in which it exists.
Some of the calls we’ll be using require a UUID, so we needed to make
our entities UUID-aware.

WORKFLOW FIELDS
We also added fields to track the workflow of this entity as it progresses
through a user-defined workflow.
By default, Liferay ships with its own Kaleo workflow engine.
Engines for jBPM and Activiti are also available.
Regardless of which workflow engine is installed, for the developer,
adding workflow to your application is the same.
We’ll come to workflow later in this course module, but since we’re
editing service.xml, we added the fields that workflow requires here.

135
THE FEED PATTERN
We’re coming to another instance of Liferay’s feed pattern.
When we gave our application social features, we used this pattern and
created a Social Activity Interpreter to translate our entities to and from
Social Activities.
Assets do almost the same thing and follow the same exact pattern.
So our next step is to add the asset with our entity at the same time we
save the entity, the resources, and the social activity.
To help you remember the pattern, we’re not going to give you all the
snippets this time.

HANDLING ASSET ENTRIES AND ASSET LINKS (I)


1. Open the ManufacturerLocalServiceImpl class.
2. Change the method signature of addManufacturer() so that instead
of taking a Manufacturer and a long userId, it takes a
Manufacturer and a ServiceContext object.
3. Add code that gets a long userId variable out of the ServiceContext
object.
4. Add code that populates the userName field with
user.getFullName().
5. Modify the setGroupId() call to get the ScopeGroupId from the
serviceContext.
6. Under the code that adds resources, add the contents of snippet 07-Add
Asset Entry.

136
HANDLING ASSET ENTRIES AND ASSET LINKS (II)
1. Add the contents of snippet 08-Delete Asset Entry to the
deleteManufacturer(Manufacturer manufacturer) method
before the return statement.
2. Add the contents of snippet 09-Update Manufacturer as a new method.
3. Organize imports, save the file and re-run Service Builder.
4. Fix the call to
ManufacturerLocalServiceUtil.addManufacturer() in the
ManufacturerPortlet class so that it makes the call with a
Manufacturer and a ServiceContext.
5. Do the same for updateManufacturer().
Hint: Make sure you’ve initialized serviceContext variables in both
methods.
! Save the file.

ASSET ENTRIES AND ASSET LINKS EXPLANATION


In the snippets for addManufacturer and deleteManufacturer, you
called assetEntryLocalService.updateEntry.
This method checks to see if there’s an existing asset entry
corresponding to the manufacturer that’s being added or updated.
If an asset entry already exists, it’s updated. If no asset entry exists, a
new one is created.
assetEntryLocalService.updateEntry is overloaded. Using the
longer method signature allows you to specify a title for the asset entry.
Soon, we’ll discuss related assets, a feature of Liferay’s asset framework.
Related assets are called asset links in Liferay’s back-end.
In the snippets for addManufacturer and deleteManufacturer, you
also called assetLinkLocalService.updateLinks.
This method updates the related assets associated with the
manufacturer asset entry.

137
ASSET RENDERERS
The asset renderer is slightly different from the social activity interpreter
in that it uses a factory pattern.
For this reason, you’ll need to create both the factory and the asset
renderer.

CREATING AN ASSET RENDERER FACTORY


1. Create a new package in your source folder called
com.liferay.training.parts.asset.
2. Create a new class in this folder called
ManufacturerAssetRendererFactory. This class should extend
com.liferay.portlet.asset.model.BaseAssetRendererFactory.
3. Replace the body of the class with the contents of snippet
10-Manufacturer Asset Renderer Factory.
! Fix the imports and save the file. There will still be one error in the file
regarding the ManufacturerAssetRenderer class, because you
haven’t created it yet.

138
WHAT DID WE DO?
The factory pattern is one of the original Gang of Four (GoF) design
patterns.
Rather than instantiating a class yourself, the instantiation of a class is
delegated to a Factory class which is responsible for creating the type of
class you want.
In this case, we want an AssetRenderer that can render
Manufacturers.
We haven’t created that AssetRenderer yet, but we have created the
factory that can give us one.
Next, we’ll create the ManufacturerAssetRenderer.

CREATING AN ASSET RENDERER (I)


1. Create a new class in the same package called
ManufacturerAssetRenderer. It needs to extend
com.liferay.portlet.asset.model.BaseAssetRenderer.
Notice the methods that need implementation: they’re all generic pieces
of Liferay information, like userId, groupId, etc. with the addition of a
title and a summary.
2. Create an uninitialized private Manufacturer instance variable called
_mfg, then create a constructor method using snippet 11-Manufacturer
Constructor.
3. Make all the methods return values from the Manufacturer instance
variable _mfg. For getTitle(), return the name of the Manufacturer.
When done, you should have implementations for every method except
getSummary() and render().
! Organize imports and save the file. HINT: The PK in ClassPK stands for
Primary Key.

139
CREATING AN ASSET RENDERER (II)
1. Add snippet 12-Permission Logic right after the
ManufacturerAssetRenderer constructor method you created above.
! Organize imports and save the file.
@Override
public boolean hasEditPermission(PermissionChecker permissionChecker) {

return ManufacturerPermission.contains(permissionChecker, _mfg,


ActionKeys.UPDATE);
}

@Override
public boolean hasViewPermission(PermissionChecker permissionChecker) {

return ManufacturerPermission.contains(permissionChecker, _mfg,


ActionKeys.VIEW);
}

WHY DID WE PROVIDE PERMISSION IMPLEMENTATIONS?


ManufacturerAssetRenderer extends BaseAssetRenderer from
Liferay.
BaseAssetRenderer has implementations of permissions already;
however they are not usable implementations.
The default implementation of hasEditPermission() doesn’t actually
check for permissions; it just returns false.
The default implementation of hasViewPermission() is similar; it
returns true.
This has the effect of allowing anyone to view data published in Asset
Publisher without actually checking for permissions.
For this reason, it is important to always add your own permission
checking logic to your asset renderers.

140
TITLE, SUMMARY, AND RENDER
Items in an asset feed appear with a Title and a Summary.
Depending on the type of content, you should populate these fields with
the appropriate data.
For example, the Wiki portlet uses the title of the article for the Title field
and a 200 character abstract of the article for the summary.
The Render method needs to return the path to a JSP in our project
which can display (or render) the entity to the user.
We’ll implement the JSP once we finish with our AssetRenderer.

IMPLEMENTING ASSET ENTRY FIELDS


1. For the getSummary() method, replace the return statement with
snippet 13-Summary.
2. For the render() method, replace the entire method with snippet
14-Render.
! Save the file.

141
SUMMARY AND RENDER EXPLANATION
The summary method returns a String concatenation of several fields
from the Manufacturer entity.
The render method sets the Manufacturer object we’re working with
as a request attribute called MANUFACTURER ENTRY.
It then returns the name of a JSP based on a variable from the super
class (BaseAssetRenderer).
If you were to look at BaseAssetRenderer, you would see that this
variable equates to the string ”full_content”, which makes our JSP
file full_content.jsp.
When a user clicks on a link to an asset in the Asset Publisher portlet,
the Asset Publisher sends a request for the full content template.

CREATING PART ASSET CLASSES


Since we went through the details for Manufacturers, we’ll make it
easier on you to create the asset classes we need for Parts.

1. In com.liferay.training.parts.asset, create two classes:


PartAssetRendererFactory (Superclass:
BaseAssetRendererFactory).
PartAssetRenderer (Superclass: BaseAssetRenderer).
2. Replace the PartAssetRendererFactory class body with snippet
15-PartAssetRendererFactory.
3. Replace the PartAssetRenderer class body with snippet
16-PartAssetRenderer.
4. Organize imports and save both files.

142
UPDATING ASSETS WITH PARTS
1. Open PartLocalServiceImpl.
2. Add snippet 17-Update Asset Entry before the return statement of
addPart().
AssetEntry assetEntry = assetEntryLocalService.updateEntry(userId,
part.getGroupId(), part.getCreateDate(),
part.getModifiedDate(), Part.class.getName(), part.getPartId(),
part.getUuid(), 0, serviceContext.getAssetCategoryIds(),
serviceContext.getAssetTagNames(), true, null, null, null,
ContentTypes.TEXT_HTML, part.getName(), null, null, null, null,
0, 0, null, false);

assetLinkLocalService.updateLinks(userId, assetEntry.getEntryId(),
serviceContext.getAssetLinkEntryIds(),
AssetLinkConstants.TYPE_RELATED);

3. Add snippet 18-Delete Asset Entry to the deletePart() method, before the
return statement.

IMPLEMENTING A JSP TO RENDER CONTENT


Now it’s time to turn back to the Manufacturer portlet.
You need to create a JSP for displaying a manufacturer asset.
This is a very simple JSP; all it does is display the values of the fields
from the Manufacturer entity.

1. Create a new file called full_content.jsp in the


docroot/html/manufacturer folder.
2. Add the contents of snippet 19-Full Content JSP to this file.
! Save the file.

143
DECLARING OUR RENDERERS IN liferay-portlet.xml
The final step to getting this working is to declare the Asset Renderer
Factory in liferay-portlet.xml.

1. Open liferay-portlet.xml from the docroot/WEB-INF folder of the


project.
2. In the Manufacturer portlet configuration, add the contents of the
20-liferay-portlet.xml snippet just above the <header-portlet-css>
tag.
3. In the Parts portlet configuration, follow the example of the last snippet,
and declare your PartAssetRendererFactory.
! Save the file.

ADDING A MANUFACTURER INDEXER


In order for our manufacturers to appear in the Asset Publisher portlet,
they need to be indexed so that the Asset Publisher can find them.
We’ll add a custom indexer to index manufacturers now, but we’ll cover
the topic of search and indexing in more detail later.

1. Create a new class called ManufacturerIndexer in a new package


called com.liferay.training.parts.search.
2. Choose com.liferay.portal.kernel.search.BaseIndexer as the
superclass of ManufacturerIndexer.
3. Replace the newly created ManufacturerIndexer class with the
contents of the 21-ManufacturerIndexer snippet and save the file.

144
REGISTERING AND CALLING OUR MANUFACTURER INDEXER
Next, we need to register our custom indexer with the portlet and
update ManufacturerLocalServiceImpl so that our indexer is called
whenever manufacturers are added, updated, or deleted.

1. Open liferay-portlet.xml and add the contents of the


22-indexer-class snippet to liferay-portlet.xml in the Manufacturer
section, just below the <icon> element.
2. Open ManufacturerLocalServiceImpl and add the contents of
23-add-manufacturer-indexer to the addManufacturer method just
before the return statement.
3. Add the contents of the same snippet, 23-add-manufacturer-indexer, to
the updateManufacturer method just before the return statement.
4. Add the contents of the 24-delete-manufacturer-indexer snippet to the
deleteManufacturer(Manufacturer manufacturer) method just
before the return statement.

INDEXING EXISTING MANUFACTURERS


1. Organize imports, then run Service Builder.
2. Once your project has been redeployed, navigate to Control Panel →
Server Administration and click the Execute button next to Reindex all
search indexes.

This step is necessary since existing manufacturers must be indexed


before the Asset Publisher can find them.

145
TEST
1. Log in to the portal with your administrator
account and add an Asset Publisher portlet to the
Moon Colony’s Inventory page.
2. The Asset Publisher’s default configuration is to
dynamically display assets of any kind from the
current site.
3. After the Parts Inventory project redeploys, add a
new manufacturer, go back to the Inventory page,
examine the Asset Publisher, and find the entry
for the manufacturer that you added.
! When you click on either link, check that the
full_content.jsp displays the fields from the
Manufacturer.

EXERCISE: IMPROVING PORTLET ICONS (I)


We can make our applications look nicer if we provide icons for different
entities.
These icons will appear in the Manufacturer and Parts portlets, and in
other portlets where our Parts Inventory entities appear, such as the
Activities portlet.
We just need to add the icons to our project and change the path for
each of our portlet’s icons in liferay-portlet.xml:

1. Copy manufacturer.png and part.png from the 02-social folder of


your exercises bundle to the docroot folder of the
parts-inventory-project.
2. Open liferay-portlet.xml, replace the contents of the Manufacturer
portlet’s <icon> tag with /manufacturer.png, and replace the
contents of the Parts portlet’s <icon> tag with /part.png.

146
EXERCISE: IMPROVING PORTLET ICONS (II)
To make our new icons appear in the
Asset Publisher, we need to add a
getIconPath() method to our
AssetRenderer class.

1. Open
ManufacturerAssetRenderer.java
and add the contents of the
25-getIconPath snippet as a new method
after the last method in the class.
2. Hit Ctrl-Shift-O to organize imports and
then save the file.
! View the Asset Publisher portlet and
confirm that the new icons are displayed.

Notes:

147
4.3 Workflow
WORKFLOW

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

GOALS
To configure our portlet to use Liferay’s Workflow
To set up Workflow for Liferay Portal
The snippets for this presentation are in the category 03-Collaboration-2.

149
LIFERAY WORKFLOW
Liferay has a workflow service that can be used throughout the product.
The API that developers use is a layer of abstraction on top of multiple
workflow implementations.
We will use Kaleo for this course because it includes default workflows
that we can use.
Enabling workflow for our applications is the same regardless of the
workflow plugin being used.

INSTALLING KALEO WORKFLOW


Kaleo workflow is installed by default on Liferay 6.2.
To check that Kaleo workflow is installed, navigate to your [Liferay
Home]/tomcat-[version]/webapps folder and look for the
kaleo-web plugin.
Since we’re using Liferay EE, the kaleo-designer, and kaleo-forms
portlets have also been installed.
These plugins provide graphical tools that allow you to create, edit, and
publish custom workflows.
You can create and edit custom workflows by accessing your portal via a
browser or you can use Liferay Developer Studio and publish your
workflows to your Liferay server.
All of these plugins are included in the Kaleo Forms EE app, which you
can install from Liferay Marketplace; if you just want the Kaleo workflow
plugin itself, download the Kaleo Workflow EE app.

150
WORKFLOW OVERVIEW
The workflow process is comprised of several pieces: some you have
seen before and some follow patterns which you’ve now seen twice.
(Can you guess?)
First, we’ll change our service so that workflow is handled there.
Next, we’ll create a WorkflowHandler class that can translate data
between our entities and the information that workflow needs.
The WorkflowHandler updates the status of our entities using the
fields we added to them previously.
Finally, we re-save our entities so that the current status becomes active.
The diagram on the next slide helps to illustrate what happens.

WORKFLOW DIAGRAM

151
SUPPORTING WORKFLOW IN THE SERVICE LAYER
Presumably, The Space Program wants to keep its list of approved
vendors down to a manageable level.
Adding a workflow step before a Manufacturer is added allows
supervisors to approve or deny new vendors.
It is easy to add support for workflow to the service layer.
At the beginning of our discussion of collaboration, we added several
fields to the Manufacturer entity to support workflow.
Now we’ll manually set the status field to draft.
Then we’ll call
WorkflowHandlerRegistryUtil.startWorkflowInstance().
If workflow is enabled in the UI for our entity, this instantiates our
WorkflowHandler to update the status.
If workflow is not enabled, the status is changed back to approved.

MODIFYING THE SERVICE LAYER


1. Open ManufacturerLocalServiceImpl.
2. Under the last setter method call in addManufacturer(), add the
contents of snippet 01-Set Status.
3. Just before the return statement, add the contents of snippet 02-Start
Instance.
! Organize imports and save the file.

152
UPDATING THE STATUS
We’ve just completed items #1 and #2 from the workflow diagram a few
slides ago.
You’ll notice that #3 and #4 both refer to an updateStatus() method.
The first one calls the second one. Does anyone know why?

WORKFLOW AND ENTITY PERSISTENCE


The WorkflowHandler gets its status from whatever happened in the
workflow engine.
If it were to set that status in the handler, the handler would then have
to persist the change to the database.
Since the handler has no idea how to talk to a database, it’s much better
to delegate that to our service layer.
So all the handler does is pass the status back down to the service layer,
where all the fields can be updated and then the entity can be persisted.
For simplicity’s sake, we’ll add the updateStatus() method which
does the actual persistence first, since we’re already working on the
service layer.

153
SERVICE UPDATE STATUS
1. Add the contents of snippet 03-Service Update Status to the bottom of
ManufacturerLocalServiceImpl, just before the ending brace of the
class.
2. Organize imports, save the file and run Service Builder.

All this method does is set all the values of the workflow fields that were
passed to it by the WorkflowHandler.
Once these values have been updated, the entity is persisted.
After this, a visible flag is set on the underlying asset which belongs to
the entity.
This tells the Asset Publisher not to display the asset if it has not been
approved, and to display it if it has been approved.

THE WORKFLOW HANDLER


Next, we’ll implement the Workflow Handler.

1. Create a new package in your source folder called


com.liferay.training.parts.workflow.
2. Create a new class in this package called
ManufacturerWorkflowHandler. The class should extend
com.liferay.portal.kernel.workflow.BaseWorkflowHandler.
3. Replace the body of the class with snippet 04-Workflow Handler.
4. Organize imports and save the file.
! Run Service Builder to generate the necessary services.

154
REGISTERING THE WORKFLOW HANDLER
Now that the Workflow Handler is done, we need to register it with
Liferay, so that WorkflowHandlerRegistryUtil can find it and
instantiate it.

1. Open liferay-portlet.xml from the docroot/WEB-INF folder.


2. In the configuration for the Manufacturer portlet, underneath the
declaration you have for your asset renderer factory, add the contents of
snippet 05-liferay-portlet.xml Workflow.
! Save the file.

TEST WORKFLOW
1. After the Parts portlet redeploys, navigate to the Control Panel and
enable workflow for Manufacturer entities.
2. Test adding manufacturers and making sure that:
Workflow operates correctly
Manufacturers that aren’t approved don’t appear in the Asset Publisher

Note: The site context is important. To be consistent, test this by using


the Moon Colony’s Manufacturer portlet and the Asset Publisher on the
Moon Colony’s Inventory page.

155
SERVICE LAYER
Manufacturers not showing up in the Asset Publisher are one thing, but
what about the portlet itself?
We don’t want non-approved manufacturers showing up until they’re
approved.
To fix this, we need to modify our service layer slightly.

ADD A NEW FINDER


The first thing we need to do is have Service Builder generate us a finder
that takes the workflow status into account.
1. Open service.xml and in the Manufacturer entity, just below the
existing finders, insert snippet 06-Finder By Status.
2. Save the file and run Service Builder.
3. Open ManufacturerLocalServiceImpl and in the
getManufacturersByGroupId(long groupId) method, replace the
return statement with snippet 07-Get Manufacturers.
4. In the getManufacturersByGroupId(long groupId, int start,
int end) method, replace the return statement with snippet 08-Get
Manufacturers StartEnd.
5. In the getManufacturersCountByGroupId(long groupId) method,
replace the return statement with snippet 09-Count Manufacturers.
! Save the file and run Service Builder.

156
TEST WORKFLOW
1. Refresh your browser and check that the unapproved Manufacturer you
added previously now doesn’t appear in either the portlet or the Asset
Publisher.
! Approve the Manufacturer, and check that it appears in both.

EXERCISE: ENABLE WORKFLOW FOR PARTS


Now we’ll enable workflow for our Part entity, but you’ll be on your own
for this one.
Follow the pattern we just used for the Manufacturer entity and use
these snippets:

PartLocalServiceImpl PartWorkflowHandler
10-Part Set Status 16-PartWorkflowHandler
11-Part Start Instance liferay-portlet.xml
12-Part Service Update Status
13-Get Parts 17-Part liferay-portlet.xml
14-Get Parts StartEnd Workflow
15-Count Parts service.xml
18-Part Finder By Status

! Remember to save files, run Service Builder, and organize imports as


necessary.

157
WHEN DO YOU NEED TO BUILD SERVICES?
As you go through these exercises, it’s easy to get into the habit of
building services pretty much any time you save, or when you would
want to redeploy – ”just to be safe.”
In fact, during some of these exercises, we told you to ”build services”
more often than necessary – to make sure that you indeed ran it when it
was needed.
Based on what you know about Service Builder, which of these cases
actually require you to build services?
You updated service.xml.
You added a method to an *Impl class.
You changed a method signature in an *Impl class.
You changed a method’s implementation in an *Impl class without
changing the signature.
You added another class in the WEB-INF/src folder.

Notes:

158
4.4 Tags, Categories, and Related Assets
TAGS, CATEGORIES,
AND RELATED ASSETS

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

GOALS
To understand the concepts behind Liferay’s categorization system
To implement the tagging and categorization features in a portlet
The snippets for this presentation are in the category 03-Collaboration-2.

160
WHY TAGS AND CATEGORIES?
Tags and categories are ways of adding metadata to your content.
They help users search, browse, and otherwise sort through your content
to find what they are looking for.
For this reason, it increases the usability of your site if your content is
properly tagged and categorized.
Liferay gives you an administrative interface in the Control Panel for both
categories and tags.
Of course, it helps if you can expose the ability to tag and categorize
content to users.
Developers can expose this functionality for custom applications, and it
is very easy to do.

WHY RELATED ASSETS?


Related Assets are different from tags and categories, but also allow you
to link assets together.
By tagging something or putting it in a category, you create a
relationship between that particular asset and any number of assets that
share that tag or category.
With related assets, you can relate one specific asset to one other
specific asset, without adding any additional associations.

161
CATEGORIES IN THE CONTROL PANEL

TAGS IN THE CONTROL PANEL

162
RELATED ASSETS IN CONTENT CREATION

DIFFERENCE BETWEEN TAGS AND CATEGORIES


Content is tagged by users. Tags are metadata that can be applied to
your content either at its creation or after the fact.
There is no structure to tags; a group of tags is simply a flat list.
Content is categorized by administrators. They are a hierarchical
structure for your content.
Categories are generally created before the content is created, and they
are assigned to content at its creation.

163
TAGS AND CATEGORIES ARE TIED TO ASSETS
Tags, categories, and related assets use Liferay’s asset system to refer to
entities.
In the code, they are referred to as assetTags, assetCategories,
and assetLinks and they have matching tables in the database where
they are stored.
For this reason, by asset-enabling our application, we’ve already done
most of the work needed to support these features.
The only thing we have left to do is the UI piece.

ADDING TAGS AND CATEGORIES TO MANUFACTURERS


1. Open the docroot/html/manufacturer/edit_manufacturer.jsp
file.
2. Under the last field on the form (the phone number field), add snippet
19-Tags and Categories.
! Save the file.
The snippet includes code for the tags for Categories, Tags, and Related
Assets, as well as error handling.

164
USERS CAN NOW TAG AND CATEGORIZE YOUR CONTENT
Adding those three tags to your JSP caused
the interfaces for tags and categories to be
generated for you automatically.
There’s nothing further to be done to
support this feature: because you asset
enabled the application earlier, they’re
automatically supported.
Note: You need to add a vocabulary with at
least one category for the Categories
button to appear.
Bonus: If you completed the earlier bonus
and asset enabled the Parts portlet, add
tags, categories, and related assets to
edit_part.jsp.

Notes:

165
4.5 Discussions and Ratings
DISCUSSIONS AND RATINGS

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

GOALS
To implement Liferay’s discussions and ratings
The snippets for this presentation are in the category 03-Collaboration-2.

167
DISCUSSIONS
Discussions are prevalent everywhere on the web.
If there’s a piece of content posted, users expect to be able to discuss it.
The best sites provide a facility for this on the same page as the content.
This is great for site owners, because it keeps the users and the traffic
on the site.
It’s also great for users, because they don’t have to use one site for the
content and another for the discussion.
Liferay provides a discussion facility which you can attach to any content
on the site, including your own.

LIFERAY DISCUSSIONS

168
RATINGS
Ratings allow users to give your content a score.
There are two types of ratings:
Stars
Thumbs
Stars show a score by a definable number of stars.
Thumbs show a simpler thumbs-up or thumbs-down interface.

IMPLEMENTING DISCUSSIONS AND RATINGS


Discussions and ratings are as easy to implement as tags and categories
with one caveat: you have to create a JSP for viewing entities.
Most of the time, you’ll want users to discuss and rate content in a
different interface than the editing interface for one of two reasons:
You’re cutting down on confusion by separating collaboration from
content edits.
You don’t want users editing content because it’s owned by somebody
else.

169
ADDING A JSP FOR VIEWING
1. In the docroot/html/manufacturer folder, create a file called
view_manufacturer.jsp.
2. Add the contents of snippet 20-view_manufacturer.jsp to the file and
save view_manufacturer.jsp.
3. Open docroot/html/init.jsp, add the required import statements
using snippet 21-init.jsp, and save init.jsp.
4. Open docroot/css/main.css, add the contents of snippet
22-main.css, and save the main.css.
! This styles the manufacturer views in both view_manufacturer.jsp
and full_content.jsp.

TAGS GENERATE THE INTERFACE


Note that in a similar fashion to tags and categories, the interface is
generated for you by two simple JSP tags:

<liferay-ui:ratings className="<%= Manufacturer.class.getName() %>"


classPK="<%= mfg.getManufacturerId() %>" type="stars" />
...

<portlet:actionURL name="invokeTaglibDiscussion" var="discussionURL" />

<liferay-ui:discussion className="<%= Manufacturer.class.getName() %>"


classPK="<%= mfg.getManufacturerId() %>"
formAction="<%= discussionURL %>" formName="fm2"
ratingsEnabled="<%= true %>" redirect="<%= currentURL %>"
subject="<%= mfg.getName() %>"
userId="<%= mfg.getUserId() %>" />

170
WE NEED A VIEW
Right now, our view_manufacturer.jsp is orphaned: there’s no URL
that points to it.
So we need to add a URL to the Search Container in view.jsp so that
users can click to view manufacturers.

MODIFY VIEW TO USE NEW JSP


1. Open docroot/html/manufacturer/view.jsp.
2. Find the Search Container.
3. Just under the <liferay-ui:search-container-row> tag, add the
contents of snippet 23-Row URL.
4. In the first Search Container column, add an attribute which has as its
value the Row URL, like this:
href="<%=rowURL %>"
! Save the file.
Bonus: Add the Row URL to the values in the other columns.

171
DEPLOY
1. After the portlet deploys, refresh the page so that the view.jsp on the
Manufacturer portlet reloads.
2. Click the manufacturer name in the first column of the search container.
! Your view_manufacturer.jsp should load, complete with discussions
and ratings:

Notes:

172
Chapter 5

Advanced Service Builder

5.1 Remote Services

173
REMOTE SERVICES

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

GOALS
To understand how Liferay Service Builder is used to generate remote
services
To generate Remote Services
To consume Remote Services
Consume SOAP Based Web Services
Consume JSON Based Web Services
The snippets for the exercises in this presentation are in the category
04.1-Remote-Entities.

174
LIFERAY SERVICE BUILDER REVIEW
Leveraging Liferay’s Service Builder has allowed us to maintain a clean
separation of concerns.
We’ve created a Manufacturer and Parts service layer which contains all
of our persistence and business logic, and can be consumed by our
client layer.
Up to this point, the client layer has been our portlets running inside the
portal, but that doesn’t have to be the case.
Many of Liferay’s services are also available as remote services.

WHY REMOTE SERVICES?


Remote services allow us to offer these services up to other non-portlet
based clients.
In fact, these clients don’t even need to be Java-based.
This opens up many exciting possibilities:
External systems can perform real-time synchronization of data.
Data from Liferay based applications is easily available throughout the
enterprise.
Mobile devices can now make use of Liferay based services.
All of this can be accomplished through the use of Liferay Service Builder.

175
WHAT ARE REMOTE SERVICES?
Liferay’s remote services are web services.
Web Services are resources which may be called over the HTTP protocol
to return data.
They are platform-independent, allowing communication between
applications on different operating systems and application servers.
Liferay’s web services support several protocols including SOAP and JSON
over HTTP.
Java clients may be generated from Liferay’s WSDL using any number of
tools (Axis, Xfire, JAX-RPC, etc.).

LIFERAY SERVICE

176
WEB SERVICES AND SECURITY
To access a service remotely, the
host must be allowed via the
portal-ext.properties file.
Each call to a Liferay portal web
service must be accompanied by
a user authentication token. (If
you’re logged in to Liferay, your
sessionId is used automatically.)
If no Liferay user matches the
authentication token, the web
service invocation is aborted.
After that, the user must have
permission to access the portal
resources.

portal-ext.properties

##
## Axis Servlet
##

#
# Servlets can be protected by com.liferay.filters.secure.SecureFilter.
#
# Input a list of comma delimited IPs that can access this servlet. Input a
# blank list to allow any IP to access this servlet. SERVER_IP will be
# replaced with the IP of the host server.

axis.servlet.hosts.allowed=127.0.0.1,SERVER_IP
axis.servlet.https.required=false

177
LIFERAY PERMISSIONS
The user must already have permission (using the GUI) to access
whatever resources will be accessed via the web service.
For example, if uploading via a web service to a Document Library folder,
the user should already have permission to upload documents to that
folder using a browser.

CREDENTIALS
Your credentials must be passed on the URL with the portal’s user
authentication method set either to screen name or user ID:
http://[user ID]:[password]@[server name]:[port number]/api/axis

For example, to get Organization data:


http://10198:test@localhost:8080/api/axis/Portal_OrganizationService

178
CHECKPOINT
As we’ve seen in the Parts Portlet example, Service Builder generated all
the interfaces and classes needed to implement our service layer.
However, when we originally ran Service Builder, we had the local
attribute set to true and remote set to false.
In this example, we’ll go a step farther and implement the Remote
Services for our application.

APPROACH
Generate remote service classes and interfaces.
Implement methods we want to expose without security checks.
Publish and test our remote services.
Implement security checks on our remote methods.
Republish and test our remote services.

179
EXERCISE: GENERATE REMOTE SERVICES
1. Open parts-inventory-portlet/docroot/WEB-INF/service.xml.
2. Locate the Manufacturer’s <entity> element and change the
remote-service attribute to true.
3. Enable remote services for the Part entity.
remote-service="true"

4. Save the file.


5. Rebuild services.

Service Builder has created all of the classes and interfaces necessary to
support Remote Services, so let’s take a look at what Service Builder
created for us.

MANUFACTURER SERVICE
com.liferay.training.parts.service.ManufacturerService
This class defines the interface for the manufacturer service.
You should never reference this interface directly, but rather use
ManufacturerServiceUtil to access the manufacturer service.
You should never modify this interface directly, but rather add custom
service methods to ManufacturerServiceImpl and rerun Service
Builder to regenerate the interface.
This is a remote service, so methods of this service are expected to have
security checks.

180
MANUFACTURER SERVICE BASE IMPL
com.liferay.training.parts.service.base.
ManufacturerServiceBaseImpl
This abstract class implements IdentifiableBean and implements the
ManufacturerService interface.
This is the base implementation of the manufacturer remote service and is
a container for the default service methods generated by Service Builder.
Never modify or reference this class directly.
Put custom service methods in ManufacturerServiceImpl.
Use ManufacturerServiceUtil to access the manufacturer service.

MANUFACTURER SERVICE IMPL


com.liferay.training.parts.service.impl.
ManufacturerServiceImpl
This class extends ManufacturerServiceBaseImpl and provides the
implementation for any customized methods in the manufacturer service.
All custom service methods should be put in this class.
Whenever methods are added, rerun Service Builder to copy their
definitions into the ManufacturerService interface.
Never instantiate this class directly. Always use
ManufacturerServiceUtil to access the manufacturer service.
This is a remote service. Methods of this service are expected to have
security checks.

181
MANUFACTURER SERVICE SOAP
com.liferay.training.parts.service.http.
ManufacturerServiceSoap
This is a SOAP utility class made available by the
ManufacturerServiceUtil service utility.
The static methods of this class call the corresponding methods of the
remote service.
However, the signatures are different because it is difficult for SOAP to
support certain types.

MANUFACTURER JSON SERIALIZER


In previous versions of Liferay, Service Builder would have generated a
ManufacterJSONSerializer, but that doesn’t happen any more.
We now use loose serialization to examine and automatically serialize
objects using the JSONFactoryUtil class.
If there are properties that you do not want serialized, you can use the
exclude() method.
You can also use the @JSON annotation to include or exclude properties.
For more information see:
https://www.liferay.com/documentation/liferay-portal/6.2/development/
-/ai/json-web-services-liferay-portal-6-2-dev-guide-05-en

182
IMPLEMENTING REMOTE METHODS
For remote services, Service Builder has only generated the methods
needed to locate existing services; it doesn’t generate any of the CRUD
methods.
It’s up to you to determine the methods that are exposed as remote
methods.
For training, we’ll only implement a few methods for the manufacturer
service.
Initially, we won’t include any permission checking. Once we’ve
confirmed that our services are available remotely, we’ll add the
necessary permission checks.

EXERCISE: IMPLEMENT UNSECURED METHODS


1. Open ManufacturerServiceImpl.
2. Insert the contents of 01-ManufacturerServiceImpl into the body of the
class.
3. Organize imports to resolve errors (press Ctrl-Shift-O):
com.liferay.portal.kernel.exception.SystemException

com.liferay.portal.kernel.exception.PortalException

java.util.List

com.liferay.portal.service.ServiceContext

4. Save ManufacturerServiceImpl.
5. Rebuild services and, if necessary, refresh the project in Liferay
Developer Studio.

183
ADD MANUFACTURER
public Manufacturer addManufacturer(long companyId, long groupId,
long userId, String name, String emailAddress, String phoneNumber,
String website) throws SystemException, PortalException {

/* Permission Check Placeholder */


Manufacturer manufacturer = new ManufacturerImpl();
manufacturer.setCompanyId(companyId);
manufacturer.setGroupId(groupId);
manufacturer.setUserId(userId);
manufacturer.setName(name);
manufacturer.setEmailAddress(emailAddress);
manufacturer.setPhoneNumber(phoneNumber);
manufacturer.setWebsite(website);

ServiceContext serviceContext = new ServiceContext();


serviceContext.setCompanyId(companyId);
serviceContext.setUserId(userId);
serviceContext.setScopeGroupId(groupId);

return manufacturerLocalService.addManufacturer(manufacturer,
serviceContext);
}

DELETE & GET METHODS


public void deleteManufacturer(long manufacturerId) throws PortalException,
SystemException {

/* Permission Check Placeholder */


manufacturerLocalService.deleteManufacturer(manufacturerId);
}

public Manufacturer getManufacturer(long groupId, long manufacturerId)


throws PortalException, SystemException{

/* Permission Check Placeholder */


return manufacturerLocalService.getManufacturer(manufacturerId);
}

public List<Manufacturer> getManufacturersByGroupId(long groupId)


throws SystemException {

return manufacturerLocalService.getManufacturersByGroupId(groupId);
}

184
PUBLISHING REMOTE SERVICES
Now that we’ve implemented our remote methods and regenerated our
services, we’ll need to publish our services.
Liferay uses Axis to generate Web Services and Axis requires a Web
Services Deployment Descriptor (.wsdd) file to be generated.
Liferay Developer Studio (LDS) makes it easy to generate this file, but
even if you’re not using LDS, you can still use the build-wsdd Ant target
provided in the build.xml.
Once the WSDD has been generated, a Web Services Definition Language
(WSDL) will be available for your web service.

EXERCISE: BUILD WSDD


1. Open docroot/WEB-INF/service.xml.
2. Click the Build WSDD button in the upper
right hand corner of the Overview view.
! After Liferay Developer Studio finishes
building the WSDD and refreshes the
workspace, the WSDL is available at the
following URL:
http://localhost:8080/parts-inventory-portlet/api/axis/Plugin_Inventory_
ManufacturerService?wsdl
! The above URL can be copied from 01-wsdl-url.txt:
exercises/04-liferay-dev-2/04-adv-service-builder/01-remote-services/
01-wsdl-url.txt

Note: The portlet must be deployed to access the above URL. If the
portlet isn’t already deployed, deploy it now.

185
TESTING REMOTE SERVICES
Now that we’ve exposed our services as a SOAP service, we’ll want to
test it to make sure we’re able to connect.
We’ll generate a Java-based test client using Liferay Developer Studio.
Note that since you have access to the WSDL, you could generate a client
in any language you like!

EXERCISE: CREATE INVENTORY CLIENT (I)


1. Select File → New → Project → Java → Java Project → Next.
2. Name the project InventoryClient.
3. Click Finish.
4. Right-click the new project and select New → Other.
5. Select Web Services → Web Service Client, and click Next.
6. Paste the WSDL URL into the Service Definition field.

7. Drag the slider to the top, as


shown.
! Click Finish.
Note: On a Mac, you may have to
drag the slider to the bottom.

186
EXERCISE: CREATE INVENTORY CLIENT (II)
Visit http://localhost:8080/InventoryClientSample/
sampleManufacturerServiceSoapProxy/TestClient.jsp to access the
Inventory Client you created.

Note: Sometimes an HTTP Error


500 appears. If this happens to
you, restart your server.

EXERCISE: TEST INVENTORY CLIENT (I)


1. Try to add or delete a manufacturer using the inventory client.

HINT: You can query the Inventory_Manufacturer table in your


database to find the necessary fields.
An even easier way to find the necessary fields is to use Firebug or
Developer tools on the Moon Colony site’s Inventory page to access the
Liferay JavaScript object.
For example, you can issue the following console commands:
Liferay.ThemeDisplay.getCompanyId();
Liferay.ThemeDisplay.getScopeGroupId();
Liferay.ThemeDisplay.getUserId();

187
EXERCISE: TEST INVENTORY CLIENT (II)
When you try to add or delete a manufacturer using the inventory client,
you’ll get an Authenticated Access Required error since all Liferay remote
service URLs are secured, by default.
We need to provide valid credentials in order to invoke remote service
URLs via the inventory client.
We also need to implement permission checking for our remote services.

IMPLEMENT PERMISSION CHECKS (I)


We’ve verified that our remote services are reachable but we need to
revisit our service layer and implement permission checking where
applicable.
When we used local services, there was no permission checking at the
service layer.
It was up to our client code to enforce permission checking.
Because we have less control over remote clients, the methods in our
remote services are expected to include permission checks.

188
IMPLEMENT PERMISSION CHECKS (II)
Let’s start by looking back at the permission checking we implemented
in our original ManufacturerPortlet.
How did we check permissions at the UI layer?
How did we check permissions in our actions?
Can we follow the same approach for our remote methods?

IMPLEMENT PERMISSION CHECKS (III)


Without passing a PortletRequest into our remote methods, we won’t
have access to the ThemeDisplay object.
We need another way to get a reference to the PermissionChecker.
In Liferay Developer Studio, open ManufacturerServiceBaseImpl
and examine the Class declaration.

189
IMPLEMENT PERMISSION CHECKS (IV)
Notice that we are extending BaseServiceImpl.

BaseServiceImpl contains a getPermissionChecker() method


which we can use.

EXERCISE: IMPLEMENT PERMISSION CHECKING


1. Open ManufacturerServiceImpl.
2. Replace the Permission Check Placeholder in the addManufacturer
method with 02-Add-Manufacturer-Permission-Check.
3. Replace the Permission Check Placeholder in the deleteManufacturer
method with 03-Delete-Manufacturer-Permission-Check.
4. Replace the Permission Check Placeholder in the getManufacturer
method with 04-Get-Manufacturer-Permission-Check.
5. Organize imports.
com.liferay.portal.security.auth.PrincipalException

Note: Because we haven’t changed any of our public method signatures,


we don’t have to re-build services or re-build WSDD.

190
EXERCISE: UPDATE CLIENT TO USE SECURE URL
Next, we need to update our client code to use valid credentials:
http://userid:password@127.0.0.1:8080/parts-inventory-portlet/api/axis/
Plugin_Inventory_ManufacturerService

1. Open ManufacturerServiceSoapServiceLocator in the


InventoryClient project.
2. Replace the URL in the variable
Plugin_Inventory_ManufacturerService_address with
05-Credentials-URL.
3. Ensure that the screen name and password are the same as your
administrative ID; if they aren’t, change them. In the snippet, the default
screen name is test and the password is test.
4. Save the file.

EXERCISE: TEST INVENTORY CLIENT (III)


In order to authenticate remotely, we need to set the portal’s user
authentication method to screenName or userId.
We’ll set the portal’s authentication method to screenName since we
specified a screen name in our remote service URL.

1. Navigate to the Control Panel and click on Portal Settings →


Authentication, then select By Screen Name under How do users
authenticate?
2. Save your changes and restart your Liferay server.

191
EXERCISE: TEST INVENTORY CLIENT (IV)
Having added permission checking and updated the remote service URL
with our credentials, let’s test our inventory client again.

1. Run the InventoryClient again and try to add or delete a


Manufacturer via http://localhost:8080/InventoryClientSample/
sampleManufacturerServiceSoapProxy/TestClient.jsp.

Remember that you can query the Inventory_Manufacturer table in


your database to find the necessary fields or you can use the Liferay
JavaScript object from the Moon Colony site’s Inventory page.
! Invoking the remote web services should work now.

ACCESSING REMOTE SERVICES THROUGH JSON (I)


In addition to the SOAP based web services we’ve seen, Service Builder
also provides an easy to use JSON interface.
JSON stands for JavaScript Object Notation.
The JSON interface relies on the underlying SOAP based services, but can
be accessed directly from the UI layer:
A portlet running in Liferay.
Another web page running outside of the portal.
Anywhere else you can make an HTTP request to the JSON URL.

192
APPROACH
To test the JSON interface, we’ll update the Parts portlet to display
manufacturer information when we hover over the manufacturer’s name
in the Parts portlet’s search container.
Note that we’ll implement this with permission checking in place, so
you’ll need to be signed in to test the changes.
We’ll use the Alloy tool tip component to make this intuitive.

ACCESSING REMOTE SERVICES THROUGH JSON (II)


Because we will be making the call from JavaScript running inside of a
portlet, the actual command is remarkably simple:
Liferay.Service(
'/namespace.entity/method',
params,
serviceCallback
);

namespace – The namespace we defined in the service.xml.


entity – The name of the entity as defined in the service.xml.
method – The method we created in our entityServiceImpl,
hyphenated.
params – An object that contains any method arguments.

193
ACCESSING REMOTE SERVICES THROUGH JSON (III)
So, as a simple example, to call the getManufacturer method in our
service and display it in an alert box, we’d use the following:

<aui:script use="aui-base">

var params = {
groupId: Liferay.ThemeDisplay.getScopeGroupId(),
manufacturerId: 401
};

var manufacturer = Liferay.Service(


'/parts-inventory-portlet.manufacturer/get-manufacturer',
params
);

alert(manufacturer.name);

</aui:script>

EXERCISE: TEST JSON (I)


1. Open
/parts-inventory-portlet/docroot/html/parts/view.jsp and
find the scriptlet that defines the manufacturerName.

<%
String manufacturerName = "";
try {
manufacturerName = HtmlUtil.escape(ManufacturerLocalServiceUtil
.getManufacturer(part.getManufacturerId()).getName());
} catch (PortalException pe) {
System.err.println(pe.getLocalizedMessage());
} catch (SystemException se) {
System.err.println(se.getLocalizedMessage());
}
%>

194
EXERCISE: TEST JSON (II)
1. Replace that scriptlet with 06-Manufacturer-Name-Column.

<%
String manufacturerName = "<span class=\"manufacturerId\"
data-manufacturerId=\"" + part.getManufacturerId() + "\">";
try {
manufacturerName = manufacturerName + ManufacturerLocalServiceUtil
.getManufacturer(part.getManufacturerId())
.getName();
} catch (PortalException pe) {
System.err.println(pe.getLocalizedMessage());
} catch (SystemException se) {
System.err.println(se.getLocalizedMessage());
}
manufacturerName = manufacturerName + "</span>";
%>

EXERCISE: TEST JSON (III)


1. Add 07-AUI-Tool-Tip to the very bottom of the file.

<aui:script use="aui-tooltip">
var parentSelector =
'table[data-searchcontainerid="<portlet:namespace />partsSearchContainer"]';
var serviceContext = '/parts-inventory-portlet.manufacturer/';
var method = 'get-manufacturer';

var node = {};

var tooltips =
new A.TooltipDelegate({
trigger: parentSelector + ' span.manufacturerId',
width: 200,
height: 100,
zIndex: 999
});
...

195
EXERCISE: TEST JSON (IV)
var serviceCallback = function(json) {
var manufacturerDetails = 'Name: ' + json.name +
'<br/> Email Address: ' + json.emailAddress +
'<br/> Phone Number: ' + json.phoneNumber +
'<br/> Website: ' + json.website;
tooltips.getTooltip().set('bodyContent', manufacturerDetails);
};
A.one(parentSelector).delegate('mouseenter',
function(event){
node = event.currentTarget;
var manufacturerId = node.attr('data-manufacturerId');
var params = {
groupId: Liferay.ThemeDisplay.getScopeGroupId(),
manufacturerId: manufacturerId
};
Liferay.Service(
serviceContext + method,
params,
serviceCallback
);
},'.manufacturerId');
</aui:script>

EXERCISE: TEST JSON (V)


1. Save your changes.

196
SUMMARY
Service Builder can generate both Local and Remote services.
When generating remote services, it’s up to the developer to determine
which methods to expose, and to implement the proper permission
checks on those methods.
Implement methods in the EntityServiceImpl class, then rebuild
services and build the WSDD.
By using the generated WSDL, Clients can be created using any language
that supports SOAP based web services.
A JSON interface is also exposed, which can be easily used from the UI
layer of your portlets or from any other web page.

Notes:

197
5.2 External Databases
EXTERNAL DATABASES

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

GOALS
To show how to work with external databases
To learn what is involved in using external databases without Service
Builder
To leverage Service Builder with external databases

199
WORKING WITH EXTERNAL DATABASES (I)
Working with Service Builder provides many compelling features, but
there may be times when the data you want to access already exists in
an external database.
For example, you might need to access data from a legacy database that
contains flight information for various shuttles in the Space Program.
To access data from an external database, there are two general
approaches you can take:
Access the database using more traditional web application techniques.
Use Service Builder to generate services against the external database.

WORKING WITH EXTERNAL DATABASES (II)


We’ll start by explaining how to access the database directly from a
portlet, without the use of Service Builder.
This approach may be useful if you are doing simple queries.
This would also be necessary if you need to use stored procedures.

200
TRADITIONAL APPROACH: OVERVIEW
The steps to this approach are the following:
1. Create the model.
2. Specify and configure the data source.
3. Access the data – Create classes that use JDBC to access the database,
execute SQL to retrieve the data, and populate the model with that data.
This is a DB Facade.
4. Populate the view in a JSP by making calls to the DB Facade interface.

TRADITIONAL APPROACH: CREATE THE MODEL


ShuttleModel holds the retrieved data and provides setter and getter
methods.

private long _shuttleId;


private String _shuttleName;
...

public long get_shuttleId() { return _shuttleId; }


public void setShuttleId(long shuttleId) { this._shuttleId = shuttleId; }
public String getShuttleName() { return _shuttleName; }
public void setShuttleName(String shuttleName) { this._shuttleName = shuttleName; }
...

201
TRADITIONAL APPROACH: SPECIFY DATA SOURCE (I)
You can specify a data source by creating a
docroot/META-INF/context.xml file in your project and adding a
<Resource> element inside of a <Context> element in this file.
If you were to create a context.xml file for your parts-inventory-portlet
project, it would be copied to your
[Tomcat Home]/webapps/parts-inventory-portlet/META-INF
folder at deploy time.
Note that Tomcat must be restarted for this change to take effect.

<?xml version="1.0" encoding="UTF-8"?>


<Context>
<Resource name="jdbc/legacyDB" auth="Container" type="javax.sql.DataSource"
maxActive="100" maxIdle="30" maxWait="10000"
username="root" password="password" driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/legacy"/>
</Context>

TRADITIONAL APPROACH: SPECIFY DATA SOURCE (II)


On Tomcat, several files are involved with data source specification and
configuration. Other application servers differ.
For more information on Tomcat’s context container, please refer to its
documentation.
E.g., http://tomcat.apache.org/tomcat-7.0-doc/config/context.html
specifies multiple ways to define a context.
After specifying the data source in context.xml, the JDBC context must
be specified in the portlet.properties file.
portlet.shuttlestats.jdbc.context=java:comp/env/jdbc/legacyDB

Lastly, the JDBC data source context must be made available in Java.
For example, a Java constant can hold this context.
public static final String DATASOURCE_CONTEXT =
"portlet.shuttlestats.jdbc.context"

202
TRADITIONAL APPROACH: CREATE DB FACADE (I)
The next slides contain key excerpts from DB Facade classes.
The SQL is defined.

private static String _SELECT_ALL_SHUTTLES_SQL = "select " +


"shuttle.shuttle_id as shuttleid, " +
"shuttle.shuttle_name as shuttlename, " +
"shuttle_stats.num_flights as numflights, " +
"shuttle_stats.num_days as numdays, " +
"shuttle_stats.num_orbits as numorbits, " +
"shuttle_stats.long_flight as longflight, " +
"shuttle_stats.first_flight as firstflight " +
"from shuttle, shuttle_stats " +
"where shuttle.shuttle_id = shuttle_stats.shuttle_id";

TRADITIONAL APPROACH: CREATE DB FACADE (II)


The data source is initialized.
Context initialContext = new InitialContext();
if (initialContext == null) {
_log.error("JNDI problem. Cannot get InitialContext.");
}
_datasource = (DataSource) initialContext.lookup(datasourceContext);

The connection is made to the database.


_connection = _datasource.getConnection();

203
TRADITIONAL APPROACH: CREATE DB FACADE (III)
The SQL is executed and the model is populated with the results.

public List<ShuttleModel> getShuttles() throws SQLException {


try {
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(_SELECT_ALL_SHUTTLES_SQL);
while (rs.next()) {
ShuttleModel model = new ShuttleModel();
model.setShuttleId(rs.getLong("shuttleid"));
// … Populate the model
result.add(model);
}
rs.close();
connection.close();
} ...

TRADITIONAL APPROACH: POPULATE THE VIEW (I)


Lastly, the DB Facade interface provides access to the model for
displaying data in a JSP.

<portlet:defineObjects />
<liferay-theme:defineObjects />

<liferay-ui:search-container emptyResultsMessage="empty-results-message">
<liferay-ui:search-container-results
results="<%= DbFacadeUtil.getShuttles(searchContainer.getStart(),
searchContainer.getEnd()) %>"
total="<%= DbFacadeUtil.getShuttlesCount() %>"/>

<liferay-ui:search-container-row
className="com.liferay.training.shuttle.model.ShuttleModel"
modelVar="aShuttleModel">
...

204
TRADITIONAL APPROACH: POPULATE THE VIEW (II)
… continued.

<liferay-ui:search-container-column-text property="shuttleName" />

<liferay-ui:search-container-column-text property="numFlights" />

<liferay-ui:search-container-column-text property="numOrbits" />


...

CHECKPOINT: TRADITIONAL APPROACH


The model needed to be created manually.
Several files (a context XML file, a properties file, and a Java class) were
required to specify and configure Java access to the external database.
Implementation of SQL queries and JDBC was required.

205
USING SERVICE BUILDER WITH EXTERNAL DATABASES
Pros:
Service and persistence code is generated for you.
Automatically generated transaction-aware methods.
Simple to write!
Cons:
Not portable (cannot be run on a non-Liferay portal).
Any query that is beyond Hibernate (PL/SQL, etc.) may require more work
than using JDBC.

EXERCISE: CREATING THE EXTERNAL DATABASE


1. From the command line, run mysql -u root -p
2. Enter your MySQL password.
3. Run (from the MySQL prompt): create database legacy;
4. Edit the script
.../04-adv-service-builder/02-external-databases
/createShuttleTables.cmd (or createShuttleTables.sh) and
replace the current MySQL password with your own, then save.
5. If you’re on Linux or Mac, make createShuttleTables.sh executable
(chmod 775 createShuttleTables.sh).
6. Run createShuttleTables.cmd (or ./createShuttleTables.sh)
to create the tables and add sample data.

206
CHECKPOINT: CREATING THE EXTERNAL DATABASE
The createShuttleTables.cmd script executed the SQL found in
shuttle-stats.sql to create the external database tables and add
sample data.
The external database schema is as follows:

CREATE TABLE shuttle(


shuttle_id INT NOT NULL PRIMARY KEY,
shuttle_name VARCHAR(50));

CREATE TABLE shuttle_stats(


shuttle_id INT NOT NULL PRIMARY KEY,
num_flights INT,
num_days VARCHAR(25),
num_orbits INT,
long_flight VARCHAR(25),
first_flight DATE);

EXERCISE: USING SERVICE BUILDER WITH EXTERNAL


DATABASES (I)
1. Create a new Liferay Plugin Project.
Project name: shuttle-stats
Display name: Shuttle Stats Portlet
2. Select Portlet for the Plugin Type.
3. Uncheck the Include sample code box and check the Launch New Portlet
Wizard after project is created box.
4. Click Finish to accept the remaining default values for the project.

207
EXERCISE: USING SERVICE BUILDER WITH EXTERNAL
DATABASES (II)
1. When the New Liferay Portlet wizard opens, make sure
shuttle-stats-portlet is selected and click Create New Portlet.
2. Fill in the fields as follows:
Portlet class: ShuttleStatsPortlet
Java package: com.liferay.training.shuttle.portlet
Superclass: com.liferay.util.bridges.mvc.MVCPortlet
3. Click Next, then fill out the following fields:
Name: shuttle-stats
Display name: Shuttle Stats Portlet
Title: Shuttle Stats Portlet
4. Click Finish to accept the remaining default values for the portlet.

EXERCISE: USING SERVICE BUILDER WITH EXTERNAL


DATABASES (III)
1. Right-click on the shuttle-stats portlet and select New → Liferay Service
Builder.
2. Enter the following field values and click Finish.
Package Path: com.liferay.training.shuttlestats
Namespace: ShuttleStats
3. In the Source view of the service.xml for the ShuttleStats service,
replace the default entity named Foo with the 01-service.xml snippet
from the 04.2 External DB with SB category.
Note that the db-name property allows us to map the database column
name to a more user-readable name.
4. Save the file and click Build Services.

208
EXERCISE: USING SERVICE BUILDER WITH EXTERNAL
DATABASES (IV)
1. Create a file called ext-spring.xml in
docroot/WEB-INF/src/META-INF/.
2. Insert the contents of the snippet 02-ext-spring.xml into the file.
3. Update the username, password, and URL (in the
trainingDataSourceTarget bean) if necessary and save.
This file overrides existing spring beans and adds some of our own.
This file is referenced by default in service.properties, and is not
changed or rebuilt by Service Builder.

EXERCISE: USING SERVICE BUILDER WITH EXTERNAL


DATABASES (V)
1. Open file
docroot/WEB-INF/liferay-plugin-package.properties.
2. Click Add... from within the Portal Dependency Jars section.
3. Select the commons-dbcp and commons-pool JARs.
4. Click OK and then save the file.

The JARs give us the ability to create our own data source with Spring.

209
EXERCISE: USING SERVICE BUILDER WITH EXTERNAL
DATABASES (VI)
1. Open docroot/html/shuttlestats/view.jsp and replace its
contents with 03–view.jsp into the file.
2. Click Save.

This is a Search Container UI object that simplifies the display of model


objects.
! Deploy the portlet.

CHECKPOINT: USING SERVICE BUILDER WITH EXTERNAL


DATABASES
1. Create a new private page called Shuttle Stats and add the Shuttle Stats
portlet to it.
! Notice that the shuttle table is now displaying data!
However, we need to find a way to map the quantity data in
shuttle_stats to the rest of our data.

210
MAPPING RELATED TABLES WITH SERVICE BUILDER
Ideally, we would design a data model that mitigates this need.
However, external database schemas that require additional mapping of
related tables may need to be used.
This can be done by using the *ServiceImpl class to populate
non-persistent fields.

EXERCISE: RETRIEVE DATA USING SERVICE LAYER (I)


1. Re-open service.xml and paste the 04-add to service.xml snippet after
the first entity.
2. Click Save and build the services.
3. Open up ShuttleImpl and insert the 05-ShuttleImpl snippet after the
default constructor.
This class creates non-persistent fields that we will use in our display
layer and business method.
Organize imports to resolve errors (Ctrl-Shft-O).
import java.util.Date;

4. Rebuild the services.

211
EXERCISE: RETRIEVE DATA USING SERVICE LAYER (II)
1. Open ShuttleLocalServiceImpl and insert the
06-ShuttleLocalServiceImpl snippet within the class.
This populates the extra fields by using the ShuttleStats service layer.
This method can be costly! (look at getShuttles(...)).
Organize imports to resolve errors (Ctrl-Shft-O).
import com.liferay.portal.kernel.exception.SystemException;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.exception.PortalException;
import java.util.List;

2. Rebuild the services.

EXERCISE: RETRIEVE DATA USING SERVICE LAYER (III)


1. Re-open the view.jsp.
2. Add the 07-add-to-view.jsp snippet below the last column.
3. Delete the column for shuttleId.
4. Add the page import:
<%@ page import="java.text.SimpleDateFormat" %>

5. Re-deploy the portlet.

212
CHECKPOINT: RETRIEVE DATA USING SERVICE LAYER
! Refresh the page and notice the quantities displayed.
Note: you may need to shut down the server and delete the Tomcat
/work directory to see a change.
Note that this method only works one-way – to retrieve data.
Doing an update to a Shuttle’s ShuttleStats requires two service calls.

TRADITIONAL VS. SERVICE BUILDER APPROACH


Traditional Service Builder
Implement connection handling.
Use ServiceLayer: Populate model
Define SQL.
with data from the data model
Implement SQL execution. objects.
Implement data extraction from result
sets to populate the model.

Data source specification required


several files. Data source specified in
ext-spring.xml.
Application server restart required after
initial specification.

Specify model in service.xml.


Implement model manually.
Service Builder builds the model.

213
EXTERNAL DATABASE INTEGRATION SIMPLIFIED WITH SERVICE
BUILDER
Service Builder generated the model from our service.xml.
The external data source connection was much more consolidated.
Service Builder approach required only one configuration file
(ext-spring.xml).
The traditional approach required one XML file, one properties file, and
one Java source file.
Service Builder took care of SQL definitions, connection handling, and SQL
execution under the hood.
! Lastly, the next slide provides a sneak peak at even more that is
available with Service Builder to address your persistence needs.

GET MORE WITH SERVICE BUILDER


Persistence Needs Service Builder Infrastructure
Query Methods Finder and counter methods generated for each entity
– findAll, findByPrimaryKey, countAll, … etc.
Query features Generates classes to support DynamicQuery and Cus-
tomSQL features. DynamicQuery allows query con-
struction and invocation via API. Custom SQL feature
facilitates best practices and convenience methods to
use with your SQL.
Caching Caching is done for results of the finders methods
generated by Service Builder.
DB dialect handling Liferay’s Dialect interface is used for handling DB di-
alects.

214
Notes:

215
5.3 Custom SQL: Using Finders
CUSTOM SQL: USING FINDERS

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

INTRODUCTION
Service Builder offers a robust suite of methods to perform CRUD
operations on entities.
However, some applications require more specific or custom queries to
fulfill their requirements.
To do this, we will use Custom SQL, a Service Builder-supported method
to perform complex and custom queries against the database.
Custom SQL enables us to join multiple tables, count rows, and even call
stored procedures.
Liferay uses the Hibernate SQLQuery system to perform Custom SQL
queries, so for more info on capabilities/limitations, see the Hibernate
documentation.
The snippet category for this presentation is 04.3-Custom SQL Finders.
Note: Stored Procedures must be called using SQL92 syntax, and must
return a ResultSet.

217
PURCHASE ORDERS
To provide a simple example to query against, we will create a new
Entity in the Parts application: PurchaseOrder.
PurchaseOrder represents an example fulfillment order for a Part.
It has the following fields:
orderId - long: primary key.
companyId - long: foreign key.
groupId - long: foreign key.
partId - long: part that is ordered.
userID - long: user that ordered the part.
orderDate - date: when the order was created.
closed - boolean: whether the order was filled.

EXERCISE: CREATING THE PURCHASE ORDER CODE (I)


1. Add 01-service.xml to your parts-inventory-portlet project’s
service.xml, inside the <service-builder> element.
2. Save the file and rebuild the services.

Now that we have created the service layer for the PurchaseOrder
object, let’s devise a way to make some objects in the database.

3. Add the 02-order-method snippet to


PurchaseOrderLocalServiceImpl, organize imports to resolve errors
(Ctrl-Shft-O). Save the file.
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.exception.SystemException;
import com.liferay.training.parts.model.PurchaseOrder;
import com.liferay.training.parts.model.impl.PurchaseOrderImpl;
import com.liferay.training.parts.service.base.PurchaseOrderLocalServiceBaseImpl
import java.util.Date;

218
EXERCISE: CREATING THE PURCHASE ORDER CODE (II)
Next, we need to add a method to update the inventory of parts.

1. Open PartLocalServiceImpl.java.
2. Add the snippet 03-updateInventory as the last method in the class and
organize imports (Ctrl-Shft-O).
3. Save the file and rebuild the services.
4. Add the 04-portlet-method snippet to PartsPortlet and organize
imports. This adds an action method to the portlet class that calls the
service method. Save the file.
5. Add the 05-order-button snippet in the parts/part_actions.jsp,
after the permissions button, but in the icon-menu element. Save the
file.
! Re-deploy the portlet.

CHECKPOINT: CREATING THE PURCHASE ORDERS (I)


Click Actions → Order a few times for some parts to put some data in
the database.
The portlet should display the message Your request completed
successfully.

219
CHECKPOINT: CREATING THE PURCHASE ORDERS (II)
Why does our Quantity field decrease when you submit an order?
When you place an order against the inventory (represented by the
Quantity field), the quantity of that part decreases. One of the parts you
ordered is simply taken out of the current inventory. Consider this
scenario:
An inventory manager at the Ganymede Moon Colony maintains a
warehouse with parts that might be used to fix various common
components found in the colony.
Maybe flux capacitors fail like light bulbs, or turbo encabulators need to be
replaced regularly.
Colonists can order parts from the inventory manager, but then, of course,
the inventory count decreases.
Once a part runs out, the inventory manager needs to reorder from Earth
so that he can continue to fulfill part orders sent to him by the colonists.
We’ll implement part reordering functionality in a later exercise.

QUERY
Now that we have some data in the database, let’s step back and
construct some queries:
Let’s say we wanted to show how many PurchaseOrders there were for
each Part.
To do this, we count each PurchaseOrder row that has the partId as a
foreign key.
The SQL:
SELECT
COUNT(*) AS COUNT_VALUE
FROM
Inventory_PurchaseOrder
WHERE
(Inventory_PurchaseOrder.partId = ?) AND
(Inventory_PurchaseOrder.closed = false)

220
DEFAULT.XML
Liferay stores the Custom SQL queries in an XML file.
This file must be named default.xml and placed in the classpath in
the folder custom-sql.
This allows it to be picked up by the Liferay CustomSQLUtil class.
The format is:

<custom-sql>
<sql id="{fully qualified classname + method}">
SQL query wrapped in <![CDATA[...]]>
No terminating semi-colon
</sql>
</custom-sql>

DEFAULT.XML - EXAMPLE

221
EXERCISE: ADDING SQL QUERY
1. Create a folder in the main source folder
(/docroot/WEB-INF/src) called
custom-sql.
2. Create a file called default.xml in the
custom-sql folder and insert the
contents of the 06-default.xml snippet
into the file.
3. Save the file.

This snippet contains the basic


default.xml format plus the query for
countByPart.

EXERCISE: FINDER IMPL (I)


Custom SQL is called through a Finder class.
The implementation class name takes the format
[entityname]FinderImpl.
Just like the [entityname]LocalServiceImpl class, the interface and
static util will be generated by Service Builder.

1. Create a class PurchaseOrderFinderImpl.


Package: com.liferay.training.parts.service.persistence
Class Name: PurchaseOrderFinderImpl

222
EXERCISE: FINDER IMPL (II)
2. Replace the entire contents of the PurchaseOrderFinderImpl.java
file with the 07-finder-impl snippet.
In addition to the class itself, the snippet contains the method
countByPart.
Even though we are using Liferay classes for Session, Query, etc., the
method for making the query and returning the data is similar to how it
would be done using Hibernate.
Each ”?” in the query is replaced by a QueryPos.add, in respective
order:

EXERCISE: FINDER IMPL (III)


1. Save the newly created PurchaseOrderFinderImpl and rebuild the
services.
Service Builder generates the interface and static util class as well as
adds the finder as a dependency in our LocalService layer.
Bonus: How does Service Builder expose the dependency?
[java] Building Manufacturer
[java] Writing /.../WEB-INF/src/com/liferay/training/parts/service/base/
ManufacturerLocalServiceBaseImpl.java
[java] Building Part
[java] Writing /.../WEB-INF/src/com/liferay/training/parts/service/base/
PartLocalServiceBaseImpl.java
[java] Building PurchaseOrder
[java] Writing /.../WEB-INF/service/com/liferay/training/parts/service/persistence/
PurchaseOrderFinder.java
[java] Writing /.../WEB-INF/service/com/liferay/training/parts/service/persistence/
PurchaseOrderFinderUtil.java
[java] Writing /.../WEB-INF/src/com/liferay/training/parts/service/base/
PurchaseOrderLocalServiceBaseImpl.java

223
EXERCISE: LOCAL SERVICE IMPL
Now we need to create some service methods to give our display code
access to the Finder.

1. Open PurchaseOrderLocalServiceImpl.
2. Paste the 08-countByPart snippet below the last method in the class.
3. Save the file and rebuild the services.

Note: We don’t need to import the finder class, as we are getting the
dependency from the PurchaseOrderLocalServiceBaseImpl parent
class.

EXERCISE: MODIFYING THE VIEW LAYER


Now that we have a service method, we can finally display the data to
the user. Let’s modify the Parts view.

1. Open the view.jsp in the parts folder.


2. Add the 09-numOrders snippet to the view.jsp just after the
manufacturer column.
<%
int numOrders = PurchaseOrderLocalServiceUtil.countByPart(part.getPartId(),
true);
%>
<liferay-ui:search-container-column-text name="open-orders"
value="<%= String.valueOf(numOrders) %>"/>

3. Add the following import to init.jsp and redeploy the portlet.

<%@ page import="com.liferay.training.parts.service.


PurchaseOrderLocalServiceUtil"%>

224
CHECKPOINT: VIEW THE OPEN ORDERS
! Now you can see the number of orders per Part:

EXERCISE: HANDLING OUTSTANDING ORDERS (I)


What would happen if you deleted a part for which there were
outstanding orders?
This would break the portlet since it would not be able to retrieve the
information required to display all its orders.
We’ll update the deletePart() method of the Parts portlet to check for
outstanding orders before deleting a part.
If there are outstanding orders, we won’t allow the part to be deleted
and we’ll display a custom error message.

1. Open the PartsPortlet class.


2. Replace the existing deletePart() method with the contents of the
10-deletePart snippet.
! Save the file.

225
EXERCISE: HANDLING OUTSTANDING ORDERS (II)
1. Open the Parts portlet’s view.jsp and insert the contents of the
11-custom-error snippet after the existing <liferay-ui:success/>
tags.
2. Open the content/Language.properties file and insert the contents
of the 12-Language.properties snippet to the end of the file.
3. Redeploy and then try to delete a part for which there are outstanding
orders.
! You’ll see the error message that appears on the next slide.

EXERCISE: HANDLING OUTSTANDING ORDERS (III)

226
CACHING QUERY RESULTS
Currently, we are retrieving the count of open-orders from the database
each time we generate contents of the view.
Loading data from memory (e.g., from cache) is always faster than
loading it from a database or from disk.
Good news! Liferay makes caching easy by providing caching utilities
and generating code that uses caching.
Let’s look at the caching code Liferay generates for you!

CACHING IN PersistenceImpl
Wherever you find EntityCacheUtil and FinderCacheUtil, caching
is being used.
Each PersistenceImpl class generated by Service Builder for an entity
has methods that use the cache utilities.
Here are just some of the methods in
PurchaseOrderPersistenceImpl that leverage cache:
cacheResult() - Puts results into cache.
fetchByPrimaryKey() - Gets the result from cache.
remove() - Removes a result from cache.
clearCache() - Removes all results from cache.
updateImpl() - Updates associated cache with the new result.

227
CACHING IN PersistenceImpl - EXAMPLE
@Override
public PurchaseOrder fetchByPrimaryKey(Serializable primaryKey)
throws SystemException {
PurchaseOrder purchaseOrder = (PurchaseOrder)
EntityCacheUtil.getResult(PurchaseOrderModelImpl.ENTITY_CACHE_ENABLED,
PurchaseOrderImpl.class, primaryKey);

if (purchaseOrder == _nullPurchaseOrder) {
return null;
}

if (purchaseOrder == null) {
...
(Try to retrieve purchaseOrder from the database and cache the
result)
...
}

return purchaseOrder;
}

BACKGROUND: TWO TIERS OF CACHING


Hibernate Tier (Entity Caching)
Caches results whenever Hibernate is used.
Transparent.
Custom Tier (Finder Caching)
Background: Liferay introduced support for custom SQL queries but found
that Hibernate doesn’t know to invalidate result sets for entities.
To solve this problem, Liferay created a custom tier that extends and
enhances the Hibernate tier.
Liferay’s custom tier is not transparent since it sits above Hibernate’s
entity cache in order to interact with it.

228
ENTITY CACHE VS. FINDER CACHE - FUNCTIONAL DIFFERENCES
Entity Cache – Handles individual entities that are not null and puts
them into cache.
Finder Cache
Caches completed result sets.
Is implemented in pure SQL.
Wraps around Hibernate.
Generalizes the behaviors of the Hibernate entities persistence tier,
instead of going through that tier.

EXERCISE: CACHE ORDER COUNTS


1. Open PurchaseOrderFinderImpl.
2. Replace the entire file with the contents of the 13-cache snippet.
3. Save the file.
4. Rebuild the services.

229
CHECKPOINT: CACHE ORDER COUNTS
! When you first view your Parts portlet, console messages should indicate
an initial count of null from cache for a part, followed by a message
indicating the count retrieved from the database query for the part.
countByPart: count for partId:1 from cache is null
countByPart: count for partId:1 from query is 4

! On refreshing the view of the portlet or revisiting the page, console


messages should indicate a non-null count value from cache for the part.
countByPart: count for partId:1 from cache is 4

BREAKDOWN: FINDER PATH (I)


Let’s take a look at the Finder Path we defined at the top of
PurchaseOrderFinderImpl:
public static final FinderPath FINDER_PATH_COUNT_BY_PART =
new FinderPath(
PurchaseOrderModelImpl.ENTITY_CACHE_ENABLED,
PurchaseOrderModelImpl.FINDER_CACHE_ENABLED, Long.class,
PurchaseOrderPersistenceImpl.
FINDER_CLASS_NAME_LIST_WITHOUT_PAGINATION,
"countByPart",
new String[] {
Long.class.getName()
);

230
BREAKDOWN: FINDER PATH (II)
The FinderPath constructor consists of:
entityCacheEnabled - Flag as to whether entity cache is enabled for
the model.
finderCacheEnabled - Flag as to whether finder cache is enabled for
the model.
resultClass - The class or type on which the finder cache operates.
cacheName - The name of the cache.
methodName - The name of the method in which the finder path is to be
used.
params - The list and types of arguments to use in the key to the finder
cache.

FINDER PATH AND FINDER ARGS


The Finder Path and Finder Args make up the path to the cache we want
to use and the key for that cache, respectively.
Finder Path – Specifies how we will work with the cache.
Finder Args – The argument values to use with the Finder Path as a key
to the cache.
Now, let’s look at how we used our Finder Path and Finder Args together
in accessing our Finder Cache!

231
BREAKDOWN: GET RESULT FROM CACHE…
We used the partId as our value for our Finder Args and then passed
the Finder Args along with our Finder Path to get our result from
FinderCacheUtil:

public int countByPart(long partId) throws SystemException {

// Build the key values to interface with the finder cache

Object[] finderArgs = new Object[] {


partId
};

// Get the finder result from finder cache using finder path and finder args

Long count =
(Long)FinderCacheUtil.getResult(FINDER_PATH_COUNT_BY_PART,
finderArgs, this);

BREAKDOWN: …OR GET RESULT FROM DB, PUT IN CACHE


If the count result from cache is null, we query the database (using our
Custom SQL code) and put the query results into Finder Cache.
session = openSession(); ← Open DB Session

String sql = CustomSQLUtil.get(COUNT_BY_PART); ← Custom


Query

FinderCacheUtil.putResult(FINDER_PATH_COUNT_BY_PART,
finderArgs, count); ← Put in Cache

closeSession(session); ← Close DB Session

232
CONCLUSION
We’ve made use of Liferay CustomSQL to retrieve order counts for each
part in our inventory.
We’ve implemented our query logic in a FinderImpl for our
PurchaseOrder entity.
We’ve used Liferay’s Finder Cache for quick access to our count of
purchase orders.

Notes:

233
5.4 Custom SQL: Joins
CUSTOM SQL: JOINS

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

INTRODUCTION
In the last presentation, we learned how to use Finder classes to call
native SQL queries.
Now we will use joins, as well as more advanced methods, to retrieve
and map data from the database.
The snippets for this presentation are in the 04.4-Custom SQL Joins
category.
Review:
SQL queries are placed in default.xml.
Queries are called in a *FinderImpl.
Finder class interfaces are generated by Service Builder.
To help illustrate the concepts, we created a PurchaseOrder object to
represent sample fulfillment orders for our Parts Inventory portlet.

235
NEW QUERIES (I)
Let us now create an example portlet that presents a view of all the
open orders for the user.
For this we will need two queries:
1. All open orders for the user.
2. A count of the open orders (for the Search Container).
As the PurchaseOrder table doesn’t contain any data about the Parts it
references, we will also need to display some data from the Part table.
To do this, we will query the part data directly and store the data in
non-persistent fields in the PurchaseOrder object.
This will prevent unnecessary round trips to the database.

NEW QUERIES (II)


Get PurchaseOrders by User:

SELECT
Inventory_PurchaseOrder.orderId as orderId,
Inventory_PurchaseOrder.orderDate as orderDate,
Inventory_PurchaseOrder.partId as partId,
Inventory_Part.partNumber as partNumber,
Inventory_Part.name as partName,
Inventory_Part.manufacturerId as manufacturerId
FROM
Inventory_Part, Inventory_PurchaseOrder
WHERE
Inventory_PurchaseOrder.closed = false AND
(Inventory_PurchaseOrder.partId = Inventory_Part.partId) AND
(Inventory_Part.companyId = ?) AND
(Inventory_Part.groupId = ?) AND
(Inventory_PurchaseOrder.userId = ?)

236
NEW QUERIES (III)
Count PurchaseOrders by User:

SELECT
COUNT(*) AS COUNT_VALUE
FROM
Inventory_PurchaseOrder
INNER JOIN Inventory_Part ON
(Inventory_Part.partId = Inventory_PurchaseOrder.partId)

WHERE
(Inventory_PurchaseOrder.closed = false) AND
(Inventory_Part.companyId = ?) AND
(Inventory_Part.groupId = ?) AND
(Inventory_PurchaseOrder.userId = ?)

EXERCISE: CREATE DEFAULT.XML & FINDER


1. Add the 01-listByUser snippet inside the <custom-sql> elements of the
default.xml to add our new queries.
2. Save the file.
3. Add the 02-finder-methods snippet into PurchaseOrderFinderImpl to
add the methods that call the new queries.
4. Organize imports to resolve errors.
! Save the file.
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.training.parts.model.Part;
import java.util.Date;
import java.util.List;

237
BREAKDOWN: FIND BY USER (I)
Let’s look at how we use our Custom SQL in the findByUser method in
PurchaseOrderFinderImpl:
We use addScalar(…) to add columns to the result.
The columns are returned from QueryUtil as a List of Object arrays,
which we convert into a list of PurchaseOrder objects using our
assembleOrders(…) method.
Your IDE will note that there is no method to set a Part name, number, or
manufacturer for the PurchaseOrder object.
Let’s also look at how we are using the cache in the findByUser
method.
To get our result from the cache, we build our Finder Args and pass them
along with our Finder Path FINDER_PATH_FIND_BY_USERID into
FinderCacheUtil.getResult(…).

BREAKDOWN: FIND BY USER (II)


If the result (i.e., the list of orders) from the cache is null, we get the
result from the database query.
If the result from the database query is good:
Cache the results in the relevant Entity Cache.
Cache the results in our Finder Cache.
Otherwise, remove any existing result from our Finder Cache.
Otherwise, the result from the cache is good.
To populate the non-persistent fields (i.e., fields in
PurchaseOrderImpl) of each PurchaseOrder object , we invoke the
method populateOrders*.
Note: The retrieval of Part information involves calling method
PartPersistenceImpl.fetchByPrimaryKey that retrieves the Part from Entity
Cache.

238
EXERCISE: CREATE PURCHASE ORDER IMPL
In our query, we are selecting some additional relevant data that is not
part of the PurchaseOrder object.
To help display this data, we will add some non-persistent fields to the
PurchaseOrderImpl (transfer object).
This way we can simply iterate through the list of results in the display
layer and avoid additional unnecessary queries.

1. Paste the 03-order-impl snippet inside the PurchaseOrderImpl class.


2. Organize imports to resolve errors.
import com.liferay.portal.kernel.util.LocalizationUtil;

3. Rebuild the services.


Service Builder adds the new fields to the PurchaseOrder interface.

EXERCISE: CREATE LOCAL SERVICE (I)


Our final step in creating the new methods is to expose them to the
display layer via the PurchaseOrderLocalService.

239
EXERCISE: CREATE LOCAL SERVICE (II)
1. Add the 04-local-service-impl snippet after the last method in
PurchaseOrderLocalServiceImpl.
Finder methods findByUser and countByUser are now added.
2. Organize imports.
import java.util.List;
3. Save the file.
4. Rebuild the services.
! Now all we have to do is create the portlet to display the orders!

EXERCISE: CREATE PORTLET (I)


1. Create the Order Portlet in the parts-inventory-portlet project:
Portlet Class: OrderPortlet
Java Package: com.liferay.training.parts.portlet
Superclass: com.liferay.util.bridges.mvc.MVCPortlet
2. Click Next.

240
EXERCISE: CREATE PORTLET (II)
1. Set the Name to orderPortlet, the Display Name to My Orders Portlet, and
the Title to My Orders.
2. Set the JSP directory to /html/orders.
3. Click Finish.

EXERCISE: CREATE PORTLET VIEW


1. Replace the contents of orders/view.jsp with the 05-view-jsp
snippet.
Note how all the display layer has to do is call the service methods we
provided and show the joined data of two tables!
2. Add the 06-language-property snippet to the end of the
content/Language.properties file to give the open-orders
column the display title Open Orders.
open-orders=Open Orders
3. Add the 07-training-category snippet within the category.training
<category> element in liferay-display.xml. Remove the sample
category and its contents.
<category name="category.training">
<portlet id="parts-portlet"></portlet>
<portlet id="orderPortlet"></portlet>
</category>

241
EXERCISE: UPDATE INIT.JSP
1. Finally, confirm that the contents of snippet 08-init-import have been
added to init.jsp:
<%@ page import=
"com.liferay.training.parts.service.PurchaseOrderLocalServiceUtil" %>

Note: You should have already added this import to init.jsp during the
previous section on custom SQL finders.

! Rebuild the services and re-deploy the plugin.

CHECKPOINT: PORTLET VIEW


! Add the My Orders portlet to the Moon Colony site’s Inventory page to
see your purchase orders!

242
CONCLUSION
We joined data from two entities, Part and PurchaseOrder, to display
purchase order details to the user.
We leveraged Liferay’s Custom SQL framework to query the necessary
data.
We cached our purchase order data.
We assembled Part and Purchase order data from the queries into a
list of helpful purchase order information for the user.

Notes:

243
5.5 Dynamic Query API
DYNAMIC QUERY API

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

INTRODUCTION
The Dynamic Query API is an interface for performing detached queries.
Differences from Custom SQL:
The query can be defined in business logic.
No string-based SQL manipulation
No XML files
No SQL
The Liferay Dynamic Query API is based on Hibernate’s Detached Query
API.
Even though some classes and methods are different, the general
concept is the same.
The snippets for this presentation are in the category 04.5-Dynamic
Query.

245
EXAMPLE: FLIGHT CRITERIA (I)
Let us consider a flight criteria dialog as a use case for building a
Dynamic Query.

! Note that we will need to be able to handle the option of multiple origins
and destinations dynamically.

EXAMPLE: FLIGHT CRITERIA (II)

246
EXAMPLE: FLIGHT CRITERIA (III)

EXAMPLE: FLIGHT CRITERIA (IV)

247
EXERCISE: USE DYNAMIC QUERY (I)
Now that we’ve considered a sample use case, let’s use the Dynamic
Query API in our Parts Inventory project.

1. Replace the contents within the try {...} block in the countByPart
method in PurchaseOrderFinderImpl with the 01-countByPart
snippet.
If the parameter useCustomSQL is true, our Custom SQL implementation
is invoked.
Otherwise, our Dynamic Query implementation is invoked.
2. Organize imports.
! Let’s take a look at the code we’ve added.

BREAKDOWN: DYNAMIC QUERY (I)


Dynamic Query:
DynamicQuery dq =
DynamicQueryFactoryUtil.forClass(PurchaseOrder.class)
.add(RestrictionsFactoryUtil.eq("partId", partId))
.add(RestrictionsFactoryUtil.eq("closed", Boolean.FALSE))
.setProjection(ProjectionFactoryUtil.rowCount());
count = countWithDynamicQuery(dq);

DynamicQueryFactoryUtil is used to initialize a new DynamicQuery


Object.
Passing the class to forClass(...) specifies the Hibernate entity to
query.

248
BREAKDOWN: DYNAMIC QUERY (II)
RestrictionsFactoryUtil.eq(String propertyName, Object
value)
This adds a {propertyName} = {value} restriction.
ProjectionFactoryUtil.rowCount()
Adds a projection, which returns the row count.
Upon execution of the query, Hibernate generates and executes the SQL
for us.
Note: It was not strictly necessary for us to add a row count projection
to the dynamic query since the row count projection is used by default
in the countWithDynamicQuery(DynamicQuery, Projection)
method of BasePeristenceImpl (PurcaseOrderFinderImpl extends
PurchaseOrderPersistenceImpl which extends
BasePersistenceImpl).
! Next, let’s compare the Custom SQL to the DynamicQuery.

COMPARE: CUSTOM SQL AND DYNAMIC QUERY CODE


No SQL was required for the DynamicQuery.
The dynamic query was invoked from within the business logic class
PurchaseOrderLocalServiceImpl.
Service Builder makes helper dynamicQuery* methods (e.g.
dynamicQueryCount) available in the local service base.
Custom SQL DynamicQuery
SELECT COUNT(*) AS COUNT_VALUE DynamicQuery dq =
FROM DynamicQueryFactoryUtil.forClass(
Inventory_PurchaseOrder PurchaseOrder.class)
WHERE .add(RestrictionsFactoryUtil.eq(
(Inventory_PurchaseOrder.partId "partId", partId))
= ?) AND .add(RestrictionsFactoryUtil.eq(
(Inventory_PurchaseOrder.closed "closed", Boolean.FALSE))
= false) .setProjection
(ProjectionFactoryUtil.rowCount());

249
REFERENCE (I)
Next, let’s take a look at some Dynamic Query references:

DynamicQuery
add(Criterion) – adds a criterion (Restriction, Disjunction,
etc.).
addOrder(Order) – adds an ordering.
setProjection(Projection) – adds a projection (rowCount, avg,
max, etc.).
These methods return the DynamicQuery object so you can chain the
method calls.
OrderFactoryUtil
addOrderByComparator(DynamicQuery, OrderByComparator) –
ordering by Comparator.
asc(String) – ascending order by the property.
desc(String) – descending order by the property.

REFERENCE (II)
RestrictionsFactoryUtil
eq(String,Object) – equal
gt(String,Object) – greater than
lt(String,Object) – less than
in(String,Object[]) – found in the array
like(String,Object) – is like
All these methods return a Criterion type, meaning they can be used
together with methods like and(…), not(…), and or(…).

250
REFERENCE (III)
ProjectionFactoryUtil
avg(String)
count(String)
distinct(String)
max(String)
min(String)
rowCount()
sum(String)
alias(Projection, String) allows aliasing a Projection so that it
can be used elsewhere in the query.

EXERCISE: USE DYNAMIC QUERY (II)


Let’s continue where we left off by implementing a version of
findByUser that uses DynamicQuery.

1. Replace the contents within the try {...} block in the findByUser
method in PurchaseOrderFinderImpl with the 02-findByUser snippet.

If the parameter useCustomSQL is true, our Custom SQL implementation


is invoked.
Otherwise, our Dynamic Query implementation is invoked.
As the Dynamic Query only allows us to return one class per query, we
invoke the method populateOrders to populate the non-persistent
fields in each PurchaseOrder object.
2. Rebuild the services and redeploy the portlet.

251
EXERCISE: USE DYNAMIC QUERY (III)
In order to have our Dynamic Query methods invoked, we’ll need to pass
in a useCustomSQL parameter value of false from our views to the
countByPart and findByUser methods.

1. Set the useCustomSQL parameter value to false in parts/view.jsp.


int numOrders =
PurchaseOrderLocalServiceUtil.countByPart(part.getPartId(), false);

2. Set the useCustomSQL parameter values to false in orders/view.jsp.


results = PurchaseOrderLocalServiceUtil.findByUser
(userId, companyId, scopeGroupId, false, searchContainer.getStart(),
searchContainer.getEnd());

total = PurchaseOrderLocalServiceUtil.countByUser
(userId, companyId, scopeGroupId, false);

3. Redeploy the portlet.

CHECKPOINT: USE DYNAMIC QUERY


When you log in, you should see log messages indicating that the
Dynamic Query methods were invoked…

countByPart: using Dynamic Query


...
findByUser: using Dynamic Query

Bonus: change the implementation of countByUser to use Dynamic


Query.

252
COMPARE: CUSTOM SQL AND DYNAMIC QUERY
Database Topic Dynamic Query Custom SQL
SQL req’s None Must specify all SQL
Config Files None default.xml for SQL
DB specific SQL None, DB agnostic Not limited, can be DB specific
DB dialects Handled in service Must call getDialect() ex-
BasePersistenceImpl plicitly
SQL complexity Does not support complex Not limited
joins or string-based manipu-
lation
Dynamic query Easy and clean to add criteria Less clean to add in business
assembly in business logic logic; error prone (e.g. string
concatenation), especially in
team development.
Optimization Harder to optimize; query SQL is externalized in
logic is in Java and may be in default.xml making it
multiple source files easier to optimize

CONCLUSION
You have now implemented and exercised Custom SQL and Dynamic
Query APIs to return Purchase Order information in your Parts Inventory.
You have compared Custom SQL and Dynamic Query to learn their
strengths and limitations so that you can decide how best to use either
or both of these features in your portlets.

253
Notes:

254
Chapter 6

Liferay APIs

6.1 Message Bus and Scheduling

255
MESSAGE BUS AND SCHEDULING

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

GOALS
To understand Liferay’s Message Bus
To set up listeners for the Message Bus that receive notification of parts
added to the Parts Inventory portlet
To understand Liferay’s Scheduler
To set up a schedule to reorder parts when quantities are low and due
for reordering
The snippets for the exercises in this presentation are in the category
05.1-Messaging and Sched.

256
MESSAGE BUS - OVERVIEW
The Message Bus is a service level API used to exchange messages
within Liferay Portal.
Messages can be exchanged between plugins and within plugins.
The Message Bus is similar to Java Message Service (JMS), but supports a
smaller and easier-to-use feature set.
Remote messaging is not supported.
However, messages can be sent across a cluster using ClusterLink.

COMMON USES
Sending search index write events
Sending subscription emails
Handling messages at scheduler endpoints
Running asynchronous processes (See liferay/async_service)

257
MESSAGE BUS SYSTEM
The Message Bus System is comprised of the following:
Message Bus – Manages transfer of messages from message senders to
message listeners.
Destinations – Contain addresses or endpoints to which listeners subscribe
for messages.
Listeners – Consume messages sent to destinations to which they are
registered. They receive all messages sent to the destination.
Senders – Produce messages and invoke the Message Bus to send the
messages to destinations.
The Message Bus knows nothing of the plugins but simply transfers
messages received at the destinations to registered listeners.
Note: A plugin can listen to multiple destinations and/or send messages
to multiple destinations.

MESSAGE BUS ARCHITECTURE


Plugins can communicate with each other via the Message Bus.

258
MESSAGE LISTENERS
A plugin can listen for messages at one or more destinations.

MESSAGE SENDERS
A plugin can send messages to one or more destinations.

259
WHAT’S IN A MESSAGE?
The Message class can do the following:
Hold mappings of name/value pairs:
message.put("userId", userId);
message.put("partName", part.getName(Locale.US));
message.put("partNumber", part.getPartNumber());
message.put("orderDate", part.getOrderDate());

Carry a payload (Object)


Carry a message response
Carry a reference to a response destination (for callback to the sender)

SYNCHRONOUS MESSAGING
Liferay’s Message Bus supports synchronous and asynchronous
communication.
In synchronous messaging with Liferay’s Message Bus, the message
sender sends a message to the destination and blocks waiting for a
response.
The code below demonstrates sending a synchronous message.

Object responseObj = MessageBusUtil.sendSynchronousMessage(


"destinationName", "myMessage");

260
SYNCHRONOUS MESSAGING TIMEOUT
You can also specify a response timeout in sending synchronous
messages.
A timeout value of -1 may be specified in order to wait indefinitely until
there is a response from sending the message. It is recommended not to
use -1 as it can lock up your thread.
The code below demonstrates sending a synchronous message with a
timeout.

Object responseObj = MessageBusUtil.sendSynchronousMessage(


"destinationName", "myMessage", timeoutMilliseconds);

SYNCHRONOUS MESSAGING

261
ASYNCHRONOUS MESSAGING
In asynchronous messaging, the sender does not block after sending its
message. The sender simply continues on with its processing.
More specifically, the Message Bus processes the message in a different
thread allowing the sender to continue with its processing. This
demonstrates a behavior that may be referred to as send and forget.

Message message = new Message();


message.put("userId", userId);
message.put("partName", part.getName(Locale.US));
message.put("partNumber", part.getPartNumber());
message.put("orderDate", part.getOrderDate());
MessageBusUtil.sendMessage("liferay/parts", message);
// Continue with other things
// ...

ASYNCHRONOUS MESSAGING (SEND AND FORGET)

262
SERIAL DESTINATION
Use a serial destination when you want messages to be sent in a series.
The message is sent to each message listener one at a time.
The SerialDestination class uses a ThreadPool of size 1.
To configure, specify the SerialDestination for the particular
destination bean in the spring configuration file
messaging-spring.xml.

<bean id="destination.part"
class="com.liferay.portal.kernel.messaging.SerialDestination">
<property name="name" value="liferay/parts" />
</bean>

ASYNCHRONOUS SERIAL MESSAGING

263
PARALLEL DESTINATION
Use a parallel destination when you want to send messages in parallel
to listeners.
The message is dispatched on separate threads to each message listener.
With the ParallelDestination class, you can set the initial and
maximum sizes of the ThreadPool used by the Message Bus.
To configure, specify the ParallelDestination for the particular
destination bean in the spring configuration file
messaging-spring.xml.

<bean id="destination.part"
class="com.liferay.portal.kernel.messaging.ParallelDestination">
<property name="name" value="liferay/parts" />
</bean>

Note, parallel destinations are only available with asynchronous


messaging.

ASYNCHRONOUS PARALLEL MESSAGING

264
CONFIGURATION OVERVIEW
Liferay’s Message Bus is configured with Spring:
The Listener class must implement the Listener interface.
The destination is represented by a bean that implements the
Destination interface.
The destination name is a property.
A MessageConfigurator bean is configured to map the destinations
and listeners together.

CHECKPOINT
Now that we understand the architecture and components of the
Message Bus system, let’s use the Message Bus to send messages
within our Parts Inventory System.

265
APPROACH
Create a listener that will receive a message when a new part is added
to the inventory.
Specify a destination as an endpoint for the messages.
Register the listener with the destination.
Implement creation and sending of messages to the destination.
Note: The messaging will be asynchronous send and forget, as no
response will be sent back to the sender and the sender will not block to
wait for a response.

EXERCISE: CREATE A MESSAGE LISTENER


1. Create a new package in your source folder called
com.liferay.training.parts.messaging.
2. Right-click on this new package and select New → Class.
3. Enter the name PartListener.
4. Click the Add button next to the Interfaces window and select
com.liferay.portal.kernel.messaging.MessageListener.
5. Select Finish.
6. Replace the body of the newly created PartListener class with the
snippet 01-PartListener and organize imports (Ctrl + Shift + O).

266
EXERCISE: SPECIFY A DESTINATION
1. Create a new file in src/META-INF (the location of spring files)
directory named messaging-spring.xml.
2. Add snippet 02-messaging-spring.xml to the newly created file
messaging-spring.xml.
3. Save the file.

Note: The value of the destination’s name


<!-- Destinations -->
<bean id="destination.part"
class="com.liferay.portal.kernel.messaging.ParallelDestination">
<property name="name" value="liferay/parts" />
</bean>

Note: The listener’s ID


<bean id="messageListener.part_listener"
class="com.liferay.training.parts.messaging.PartListener"/>

EXERCISE: REGISTER THE LISTENER


A MessagingConfigurator bean is now configured in
messaging-spring.xml mapping the destination and listener together.

...
<entry key="liferay/parts">
<list value-type="com.liferay.portal.kernel.messaging.MessageListener">
<ref bean="messageListener.part_listener" />
</list>
</entry>
</map>
</property>
<property name="destinations">
<list><ref bean="destination.part"/></list>
</property>
...

<entry key="liferay/parts" and <ref


bean="destination.part"/> refer to the destination and <ref
bean="messageListener.part_listener" /> refers to the listener.

267
EXERCISE: ADD A CONTEXT CONFIG LOCATION
1. Paste snippet 03-web.xml before the closing </web-app> tag in
web.xml in order to specify messaging-spring.xml as a context
configuration location.

<context-param>
<param-name>portalContextConfigLocation</param-name>
<param-value>/WEB-INF/classes/META-INF/messaging-spring.xml</param-value>
</context-param>

EXERCISE: CREATE AND SEND THE MESSAGE


1. Add snippet 04-sendMessage after the last method in
PartLocalServiceImpl.
2. Organize imports to resolve errors (Ctrl-Shft-O).
com.liferay.portal.kernel.messaging.Message
com.liferay.portal.kernel.messaging.MessageBusUtil

3. Add a call to sendMessage(...) within PartLocalServiceImpl’s


addPart(...) method, right before the return statement.
sendMessage(part, userId);

268
CHECKPOINT
1. Re-deploy the portlet.
! Upon adding a new part, you should see a message with the part’s name
and part number printed to standard output!

Part Added : Cryogenic Oxidizer Tank CTZ-45B

USING THE SCHEDULER (I)


Liferay includes a built-in scheduling system using Quartz.
This scheduler is controlled via the liferay-portlet.xml descriptor:

<portlet>
...
<scheduler-entry>
<scheduler-event-listener-class>
com.liferay.portlet.calendar.messaging.CheckEventMessageListener
</scheduler-event-listener-class>
<trigger>
<simple>
<property-key>calendar.event.check.interval</property-key>
<time-unit>minute</time-unit>
</simple>
</trigger>
</scheduler-entry>
...
</portlet>

269
USING THE SCHEDULER (II)
Scheduler configuration is composed of two parts:
Event Listener Class: This is a Message Bus Listener class that will be sent
a message at a specified interval.
Trigger: This can be either a Simple (once every time interval) or Cron
(using cron-style intervals).
The simple interval value or cron text can also be specified in a property
for maximum configurability.
Use portal(-ext).properties to configure schedulers for the core
portal.
Use portlet.properties to configure schedulers for a plugin.

USING THE SCHEDULER (III)


Consider Liferay’s Calendar application, for example.
Liferay’s Calendar belonged to the core portal in Liferay 6.1 and prior
versions.
In Liferay 6.2, it was redesigned and extracted into a plugin.
So you’d use portal-ext.properties to configure the Calendar event
scheduler for Liferay 6.1, but you’d use portlet.properties to
configure the Calendar event scheduler for Liferay 6.2.
Note: Since Liferay will throw an exception if we configure our Inventory
application to accept a scheduler message before creating the referenced
class, we’ll implement the class first and the configuration last.

270
EXERCISE: CREATE A SCHEDULER EVENT LISTENER
Using our Parts portlet, let’s add a simple scheduler to reorder a part
once its quantity is less than 1 and its order date has expired.
Let’s create a class to listen for the events from the scheduler.

1. Create a new class called PartReorderMessageListener in the


package com.liferay.training.parts.messaging.
2. Replace the contents of newly created class
PartReorderMessageListener with the snippet 05-Scheduler-Listener
and save.
protected void doReceive(Message message) throws Exception {
PartLocalServiceUtil.reorderParts();
}

Note: This listener simply delegates responsibility of reordering of parts


to PartLocalServiceUtil.

EXERCISE: ADD THE RE-ORDER LOGIC


Let’s implement the logic for reordering the parts. If a part’s quantity is
less than 1 and if its order date has expired, then reorder the part!

1. Add the method reorderParts() in the snippet 06-reorderParts after


the last method in PartLocalServiceImpl.java.
2. Organize the imports to include:
com.liferay.portal.kernel.dao.orm.QueryUtil
java.util.Date

3. Re-build the services.


4. Re-deploy the portlet.

271
EXERCISE: CONFIGURE A SCHEDULER
Now that we’ve implemented the logic for our scheduler task, let’s make
Liferay aware of our new scheduler event.

1. Insert snippet 07-liferay-portlet.xml in your liferay-portlet.xml, in


the section for the Parts Portlet, just after the
<configuration-action-class> tag and save.
<scheduler-entry>
<scheduler-event-listener-class>com.liferay.training.parts.
PartReorderMessageListener</scheduler-event-listener-class>
<trigger>
<simple><simple-trigger-value>1</simple-trigger-value>
<time-unit>minute</time-unit></simple>
</trigger>
</scheduler-entry>

Note: The above specifies that every minute, a message will be sent to
event listener
com.liferay.training.parts.PartReorderMessageListener.

CHECKPOINT
! The portlet should redeploy automatically.
1. Use the Parts portlet to set the order date on a part to yesterday and set
the quantity to zero.
! After a minute, you should see a message similar to the following:
Reordering Part no: CTZ-456

272
NOTES
To use a property instead of hard coding the interval:
Replace the simple-trigger-value element with a property-key element.
Insert a property key in portlet.properties that matches the one in
the property-key element.
BONUS: Set up a cron-based trigger. Hints:
You will need to add a key to liferay-portlet.xml
You will also need to add a new property to portlet.properties
Here’s a good resource for help with setting up Cron with Quartz:
http://www.quartz-scheduler.org/documentation/quartz-1.x/tutorials/
crontrigger

Notes:

273
6.2 Search and Indexing
SEARCH AND INDEXING

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

SEARCH IN LIFERAY
Liferay includes a built-in search system based on Lucene.
Index-based searching allows users to find data without expensive
database queries.
We will add a search feature to the Parts portlet, to make finding a part
simpler.
To do this, we will create:
An Indexer to create index entries for the Parts
A search JSP to call the search engine and display the results
A search text box on the main view
The snippets for this presentation can be found in the category
05.2-Search.

275
SEARCH: ANOTHER INSTANCE OF THE FEED PATTERN
Search works very much like Social Activities, Assets, and Workflow.
Your entities are converted to Document objects, which are then
indexed.
Search queries return Hit objects, which are pointers to documents.
Documents are converted back to entities when users click on hits.

EXERCISE (I)
1. Create a class named PartIndexer in the following package:
com.liferay.training.parts.search
2. Replace the contents of the class with the snippet 01-PartIndexer.
3. Add the snippet 02-indexer-class to liferay-portlet.xml in the Parts
Portlet section, right after the <configuration-action-class>
element.
The indexer class takes in a Part object and creates a Document, which
is then indexed by Lucene.
Part part = (Part) obj;
long groupId = getSiteGroupId(part.getGroupId());
long scopeGroupId = part.getGroupId();
String description = part.getName();
Document document = getBaseModelDocument(PORTLET_ID, part);
document.addKeyword(Field.GROUP_ID, groupId);
document.addKeyword(Field.SCOPE_GROUP_ID, scopeGroupId);
document.addText(Field.DESCRIPTION, description);
return document;

276
EXERCISE (II)
The indexer should be called upon creation, update, or deletion of a Part.
1. Add snippet 03-add-part-indexer in PartLocalServiceImpl, in the
addPart(...) method, before the return statement.
2. Also, add 04-update-part, which adds a new method to
PartLocalServiceImpl:
public Part updatePart(Part part) throws SystemException, SearchException {
...

Indexer indexer = IndexerRegistryUtil.getIndexer(Part.class);

try {
indexer.reindex(part);
} catch (SearchException se) {
System.out.println("Search Exception: " + se.getMessage());
}

return part;
}

EXERCISE (III)
1. Finally, add 05-delete-part-indexer to the deletePart(Part part)
method, before the call to super.
2. Add a SearchException to the throws clause in reorderParts().
3. Organize imports.
4. Re-build the services.
! Now our indexer will be called upon any change to the model object.

277
ADDING A USER INTERFACE
We will now add a UI to allow users to search the indexed parts.

1. Create a file called search.jsp in docroot/html/parts and add


snippet 06-search.jsp.
2. Then, in parts/view.jsp, add snippet 07-search-box right below the
<aui:button-row>.
3. Add the snippet 08-init.jsp imports to init.jsp.
! Save all files.

REINDEX
Since we have data but have just implemented our search indexer, none
of our parts can be found.
To solve this problem, we need to reindex the portal.
! Go to the Control Panel → Server Administration, and click the Execute
button next to Reindex all search indexes.

278
SEARCH.JSP
What’s going on in search.jsp?
We call the default implementation of Indexer.search(...)
We iterate through the results (stored as a Hits class), retrieve the
parts corresponding the the hits’ documents and add the parts to a list.
We then display the list of parts in a SearchContainer.
The search keywords entered by the user are added to the portlet
breadcrumb entry and are displayed on the page.
Note how you can declare and use a custom log for the search.jsp file.

SEARCH RESULTS
! Try searching for some parts!

279
Notes:

280
6.3 Indexer Hooks
INDEXER HOOKS

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

INDEXER HOOKS
Indexer Hooks build a post processing system on top of an existing
indexer.
Indexer Hooks can be used to modify search summaries, indexes, and
queries.
For our exercise, we will build an indexer post processor hook to add the
lastLoginDate field to the User Indexer.
This allows administrators to search for users according to the last time
they logged in to the portal.
The snippets for this presentation can be found in the category 05.3-User
Indexer.

282
INDEXER HOOK EXERCISE OVERVIEW
Our exercise involves four steps:
First, we’ll create a new hook plugin project and create a custom indexer
post processor hook to add the lastLoginDate field to the User
Indexer.
Next, we’ll make the lastLoginDate visible and searchable from
Liferay’s Control Panel by overriding several of Liferay’s JSPs.
After that, we’ll create a custom login action hook that calls the
UserIndexer as a post-login action.
Finally, we’ll customize Liferay’s language properties so that ”Last Login
Date” appears in our portal instead of ”last-login-date” and ”Last Month”
appears instead of ”last-month”, etc. We’ll also learn how to provide
translations of ”Last Login Date”, ”Last Month”, etc. in other languages.

EXERCISE: CREATE INDEXER HOOK (I)


1. In Liferay Developer Studio, click File → New → Liferay Plugin Project
and enter the following information into the wizard:
Project Name: user-indexer-post-processor-hook
Display Name: User Indexer Post Processor
2. Select the Liferay Plugins SDK and Liferay Portal Runtime that you have
configured.
3. For Plugin Type, choose Hook, then click Finish.

283
EXERCISE: CREATE INDEXER HOOK (II)
1. Go to the user-indexer-post-processor-hook project in Liferay Developer
Studio, open the docroot/WEB-INF/liferay-hook.xml file that was
created by the wizard, replace its contents with the contents of the
01-liferay-hook.xml snippet, and save the file.

<?xml version="1.0"?>
<!DOCTYPE hook PUBLIC "-//Liferay//DTD Hook 6.2.0//EN"
"http://www.liferay.com/dtd/liferay-hook_6_2_0.dtd">
<hook>
<indexer-post-processor>
<indexer-class-name>com.liferay.portal.model.User</indexer-class-name>
<indexer-post-processor-impl>
com.training.hook.indexer.CustomUserIndexerPostProcessor
</indexer-post-processor-impl>
</indexer-post-processor>
</hook>

EXERCISE: CREATE INDEXER HOOK (III)

<indexer-post-processor>
<indexer-class-name>com.liferay.portal.model.User</indexer-class-name>
<indexer-post-processor-impl>
com.liferay.training.hook.indexer.CustomUserIndexerPostProcessor
</indexer-post-processor-impl>
</indexer-post-processor>

<indexer-class-name> specifies the model entity for indexer.


Here we use com.liferay.portal.model.User; you can use any
other model entities, such as
com.liferay.portal.model.JournalArticle,
com.liferay.portal.model.DLFileEntry, etc.
<indexer-post-processor-impl> specifies the implementation of
the interface
com.liferay.portal.kernel.search.IndexerPostProcessor.

284
EXERCISE: CREATE INDEXER HOOK (IV)
1. Right-click on your user-indexer-post-processor-hook project, choose New
→ Class, and enter the following information:
Package: com.liferay.training.hook.indexer
Name: CustomUserIndexerPostProcessor
Interface: IndexerPostProcessor
2. Click Finish.
3. Replace the postProcessDocument method with the contents of
02-postProcessDocument and hit Ctrl-Shift-O to add the following
imports:
com.liferay.portal.model.User
java.util.Date

In the postProcessDocument method, we get the lastLoginDate


from the user and add it to the index.

EXERCISE: CREATE INDEXER HOOK (V)

public class CustomUserIndexerPostProcessor implements IndexerPostProcessor


{
...
public void postProcessDocument(Document document, Object obj)
throws Exception {

User user = (User) object;

Date lastLoginDate = user.getLastLoginDate();

document.addDate("lastLoginDate", lastLoginDate);
}
}

285
EXERCISE: CREATE INDEXER HOOK (VI)
1. Next, replace the postProcessContextQuery method with the
contents of 03-postProcessContextQuery and add the following imports
(Ctrl-Shift-O):
import com.liferay.portal.kernel.search.BooleanClauseOccur;
import com.liferay.portal.kernel.search.TermRangeQuery;
import com.liferay.portal.kernel.search.TermRangeQueryFactoryUtil;
import com.liferay.portal.kernel.util.DateFormatFactoryUtil;
import java.text.DateFormat;
import java.util.Calendar;
import java.util.LinkedHashMap;
In the postProcessContextQuery method, we get the
lastLoginDate from the searchContext.
Then we set up a startString and an endString to use in a
TermRangeQuery so that we can search for users who’ve logged in
during a specified period of time: during the last month, last day, etc.
Note that the endString is always the current time while the
startString depends on user input.

EXERCISE: CREATE INDEXER HOOK (VII)

public void postProcessContextQuery(BooleanQuery contextQuery,


SearchContext searchContext) throws Exception {

LinkedHashMap<String, Object> params = (LinkedHashMap<String, Object>)


searchContext.getAttribute("params");
if (params != null) {
String lastLoginDate = (String) params.get("lastLoginDate");
Calendar now = Calendar.getInstance();
now.set(Calendar.SECOND, 0);
DateFormat dateFormat =
DateFormatFactoryUtil.getSimpleDateFormat("yyyyMMddHHmmss");
String endString = dateFormat.format(now.getTime());
String startString = normalizeDate(lastLoginDate);
TermRangeQuery facetTermRangeQuery =
TermRangeQueryFactoryUtil.create(searchContext,
"lastLoginDate", startString,endString, true, true);
contextQuery.add(facetTermRangeQuery,
BooleanClauseOccur.MUST.getName());
}
}

286
EXERCISE: CREATE INDEXER HOOK (VIII)
How does the startString depend on user input?
String startString = normalizeDate(lastLoginDate);

The normalizeDate method is a helper method that returns a properly


formatted date String based on the user’s search selection, which
could be the last month, last day, etc.

1. Add the normalizeDate method to the end of the class using


04-normalizeDate.
Let’s also add a log to our CustomUserIndexerPostProcessor class.
2. Add the contents of 05-log to the end of the class, after the
normalizeDate method, and add the following import:
com.liferay.portal.kernel.log.Log

EXERCISE: OVERRIDE LIFERAY’S JSPs (I)


Good work! We now have a complete user indexer post processor.
However, we don’t yet have a way to view users’ last login dates or
search for users according to their last login dates.
Let’s override some of the JSPs from Liferay’s Control Panel to add this
functionality.

1. Right-click on your user-indexer-post-processor-hook project and select


New → Liferay Hook Configuration.
2. Check the Custom JSPs box and click Next.

287
EXERCISE: OVERRIDE LIFERAY’S JSPs (II)
1. Accept the default Custom JSP
folder and click Add from Liferay
next to the JSP files to override.
2. Add the following JSPs and click
Finish:
html/portlet/users_admin
/user/details.jsp

html/portlet/users_admin
/user_search.jsp

html/portlet/users_admin
/user_search_results_index.jspf

EXERCISE: OVERRIDE LIFERAY’S JSPs (III)


1. Reopen liferay-hook.xml and find the
<custom-jsp-dir>/custom_jsps</custom-jsp-dir> tag that
Liferay Developer Studio added.
2. Make sure that the <custom-jsp-dir> tag appears before the
<indexer-post-processor> tag.

For our first JSP customization, we’ll make the last login date visible from
the page of the Control Panel that displays a user’s details.

3. Open your
/user-indexer-post-processor-hook/docroot/custom_jsps
/html/portlet/users_admin/user/details.jsp file and insert
the contents of 06-details.jsp just above the first </aui:fieldset> tag.

288
EXERCISE: OVERRIDE LIFERAY’S JSPs (IV)

<c:if test="<%= selUser != null %>">


<liferay-ui:error exception="<%= DuplicateUserIdException.class %>"
message="the-user-id-you-requested-is-already-taken" />
<liferay-ui:error exception="<%= ReservedUserIdException.class %>"
message="the-user-id-you-requested-is-reserved" />
<liferay-ui:error exception="<%= UserIdException.class %>"
message="please-enter-a-valid-user-id" />

<aui:field-wrapper name="lastLoginDate">
<%= selUser.getLastLoginDate() %>

<aui:input name="lastLoginDate" type="hidden"


value="<%= selUser.getLastLoginDate() %>" />
</aui:field-wrapper>
</c:if>

EXERCISE: OVERRIDE LIFERAY’S JSPs (V)


Next, we’ll add a last login date field to the Advanced Search page for
users to allow administrators to search for users who’ve logged in during
the following time periods:
Last Month
Last Day
Last Hour
Last 30 Minutes
Last Minute

1. Add the contents of 07-user_search.jsp-1 to your user_search.jsp


after the line UserDisplayTerms displayTerms =
(UserDisplayTerms)searchContainer.getDisplayTerms();
2. Then add the contents of 08-user_search.jsp-2 before the closing
</aui:fieldset> tag.

289
EXERCISE: OVERRIDE LIFERAY’S JSPs (VI)
The last JSP we’ll override is user_search_results_index.jspf.
We need to add the time period selected by the administrator (last
month, last day, etc.) as a user parameter to be included in the search.

1. Add the contents of 09-user_search_results_index.jspf to your


user_search_results_index.jspf after the line
userParams.put("expandoAttributes",
searchTerms.getKeywords()); but above the line
Sort sort = SortFactoryUtil.getSort(...)

String tempLastLoginDate = ParamUtil.getString(request, "lastLoginDate");


userParams.put("lastLoginDate", tempLastLoginDate);

2. Save all files.

EXERCISE: CREATE A POST LOGIN ACTION (I)


Great! Our hook plugin is now fully functional.
However, we’d have to manually reindex our search indexes each time a
user logs in if we wanted to keep users’ last login dates up to date.
We’ll create a post login action to run Liferay’s user indexer after each
user logs in so that we can keep users’ last login dates up to date.
Remember that our user indexer post processor is called after the user
indexer runs.

1. Right-click on your user-indexer-post-processor-hook project and select


New → Liferay Hook Configuration.
2. Check the Portal properties box and click Next.

290
EXERCISE: CREATE A POST LOGIN ACTION (II)
1. Click Add next to Define actions to be executed on portal events: and
select the login.events.post event.
2. For the Class, click New and enter the following information:
Classname: CustomLoginAction
Java package: com.liferay.training.hook.action
Superclass: com.liferay.portal.kernel.events.Action
3. Click Create, OK, then Finish.

EXERCISE: CREATE A POST LOGIN ACTION (III)


We’ll create a new User indexer to index users after they log in to the
portal.
First, we’ll use the httpServletRequest to get a serviceContext
and then we’ll use the serviceContext to get the current user’s ID so
we know which user to reindex.

1. In your new CustomLoginAction class, replace the run(...) method


with the contents of 10-run.
...
long userId = serviceContext.getUserId();
Indexer indexer = IndexerRegistryUtil.nullSafeGetIndexer(User.class);
try {
indexer.reindex(userId);
} catch (SearchException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

291
EXERCISE: CREATE A POST LOGIN ACTION (IV)
It’s not possible to create a new UserIndexer or LuceneIndexer
directly since these classes belong to portal-impl, not to
portal-service.
The IndexerRegistryUtil service class allows us to create a new
indexer to index users as they log in to the portal.

1. After you’ve replaced the CustomLoginAction’s run method, add the


following imports (Ctrl-Shift-O):

import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.exception.SystemException;
import com.liferay.portal.kernel.search.Indexer;
import com.liferay.portal.kernel.search.IndexerRegistryUtil;
import com.liferay.portal.kernel.search.SearchException;
import com.liferay.portal.model.User;
import com.liferay.portal.service.ServiceContext;
import com.liferay.portal.service.ServiceContextFactory;

EXERCISE: CUSTOMIZING LANGUAGE PROPERTIES (I)


Now we’re almost done.
However, if you deploy your user-indexer-post-processor-hook plugin,
navigate to Control Panel → Users and Organizations, and edit a user,
you’ll find that ”last-login-date” is displayed as the title of the last login
date.
Let’s customize Liferay’s language properties to provide a more suitable
title like ”Last Login Date”.
1. Right-click on your user-indexer-post-processor-hook plugin and select
New → Liferay Hook Configuration.
2. Check the Language properties box and click Next.
3. Accept the default Content folder and click Add next to the Language
property files: field.
4. Enter Language.properties for the name of your Language property file,
click OK, and then Finish.

292
EXERCISE: CUSTOMIZING LANGUAGE PROPERTIES (II)
1. In your new Language.properties file, add the following line
(provided in 11-Language.properties):

last-login-date=Last Login Date

To create translations for other languages, simply create


Language.properties files with two-character language keys
appended to the word ”Language” in the filename.
For example, to create a Spanish translation, create a file called
Language_es.properties in the same directory as your
Language.properties file and enter the following line (provided in
12-Language_es.properties):

last-login-date=Última Fecha de inicio de Sesión

EXERCISE: CUSTOMIZING LANGUAGE PROPERTIES (III)


Next, we should add a few more language properties for the selectable
date ranges (Last Month, Last Day, etc.).
1. In your Language.properties file, add the following lines (provided in
13-Language.properties):
last-minute=Last Minute
last-30-minutes=Last 30 Minutes
last-hour=Last Hour
last-day=Last Day
last-month=Last Month

If you’d like a Spanish translation, add the contents of


14-Language_es.properties to your Language_es.properties file.
That’s it! We’re done!
! Deploy your user-indexer-post-processor-hook plugin!
! Before testing your hook, go to the Control Panel → Server
Administration and click Execute next to Reindex all search indexes.

293
CHECKPOINT (I)
1. Log in as an administrator and go to
Control Panel → Users and Organizations
and edit a user.
2. Make sure that the Last Login Date field
appears at the bottom of the page.
Note that the Last Login Date represents
the ”last” time a user logged in, not the
time they logged in to their current
session.
For example, if you view your user
account, the Last Login Date field shows
the last time you logged in, not the time
you logged in to your current session.

CHECKPOINT (II)
1. Go back to Control Panel → Users and
Organizations and click on All Users.
2. Then click on the Search icon next to the
Search box and check that the Last Login
Date select field appears among the
searchable fields.

294
CHECKPOINT (III)
Test the User Indexer Post Processor:

1. Create a few new user accounts, log out of your administrator account
and log in with your new accounts.
Our custom login action triggers the indexer post processor.
2. Log back in with your administrator account and search for the users you
created and used to log in to the portal.
3. Make sure that the select fields (Last Month, Last Day, etc.) work as
expected.

Notes:

295
6.4 Using Friendly URLs
USING FRIENDLY URLS

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

GOALS
To understand Friendly URLs
To implement Friendly URL routes
To build Friendly URLs
The snippets for this presentation are in the category 05.4-Friendly URL.

297
WHAT ARE FRIENDLY URLS?
URLs generated by the portal can be complex:
http://www.liferay.com/web/nathan.cavanaugh/blog?
p_p_id=33&p_p_lifecycle=0&p_p_state=normal&p_p_mode=view
&_33_struts_action=%2Fblogs%2Fview

In most cases, the user is oblivious to these URLs, so the added


complexity is no issue.
Human-readable URLs are necessary, however, if you want memorable
pages or references to important items (like products and office
information).
Additionally, making pages SEO-friendly requires descriptive URLs.

WHAT ARE FRIENDLY URLS?


Friendly URLs let you route important information to the portal while
making URLs readable, creating a URL like this:
.../web/nathan.cavanaugh/blog/-/blogs/
In this case, the extra-long URL was obfuscating what was actually being
done.
Liferay’s friendly URL mapper can remove the extra cruft from a URL and
provide the user with the relevant information in the address.

298
HOW DO FRIENDLY URLS WORK?
Friendly URLs work based on routes, which define a pattern in the URL to
match:
/[urlTitle]/[p_p_state]
The developer then maps this route to a set of parameters, both explicit
and implicit.
The resulting mapped URL is then used by the portal to process the
request.
All of the traditional parameters in a Portlet URL are available:
p_p_id
p_p_lifecyle
p_p_state
p_p_mode

FRIENDLY URL MAPPING


Prior to Liferay 6.0, Friendly URLs could only be used by implementing
your own Friendly URL Mapper object.
Since Liferay 6.0, we can use the DefaultFriendlyURLMapper object
to handle our routing.
All that the developer must do is define the routes for the Friendly URL
mapping object, and declare its intentions in liferay-portlet.xml.
We’ll define a custom view for viewing individual parts just like we did
for viewing individual manufacturers.
Then we’ll update our Parts portlet to use Friendly URLs so that we’ll
have an easy-to-read path to our custom view.

299
EXERCISE: ADDING A VIEW FOR INDIVIDUAL PARTS (I)
1. Create a new file in the docroot/html/parts folder called
view_part.jsp.
2. Add the contents of the 01-view_part.jsp snippet to the view_part.jsp
file.
3. Next, open the docroot/html/parts/view.jsp and add the contents
of the 02-view.jsp snippet just above the first
<liferay-ui:search-container-column-text/> tag.
4. Add the following attribute to the first
<liferay-ui:search-container-column-text/> tag:
href="<%= rowURL %>"

EXERCISE: ADDING A VIEW FOR INDIVIDUAL PARTS (II)


1. Open your project’s content/Language.properties file and add the
contents of the 03-Language.properties snippet to the end of the file.
2. Check that you can click on the name of a part to view that part’s
detailed information.
3. Note the URL of the page displaying the part’s detailed information.
4. Also check that the back link takes you back to the default view with the
search container.

300
EXERCISE: PART URLS (I)
To make Liferay aware of our new Friendly URL routes, we need to
modify our deployment descriptor:

1. In Liferay Developer Studio, open the Parts Inventory Portlet project.


2. Open liferay-portlet.xml located in the project’s
docroot/WEB-INF folder.
3. Inside the Parts Portlet, just after the </scheduler-entry> tag, insert
the snippet 04-Liferay Portlet XML and save the file:

<friendly-url-mapper-class>
com.liferay.portal.kernel.portlet.DefaultFriendlyURLMapper
</friendly-url-mapper-class>
<friendly-url-mapping>
parts
</friendly-url-mapping>
<friendly-url-routes>
/com/liferay/training/parts/parts-friendly-url-routes.xml
</friendly-url-routes>

LIFERAY PORTLET DECLARATION


In order for Liferay to pay any attention to our Friendly URLs, we need to
define three nodes:
friendly-url-mapper-class: specifies the object that will process our
Friendly URLs – we’ll just use the default handler
friendly-url-mapping: specifies the namespace that our Friendly URLs will
exist in:

friendly-url-routes: the location of the XML descriptor for the Friendly URL
routes.

301
EXERCISE: PART URLS (II)
To use the Friendly URLs, we need to define a set of routes:

4. Inside the package com.liferay.training.parts, create a new file


named parts-friendly-url-routes.xml.
5. Insert the snippet 05-Routes XML into the file
parts-friendly-url-routes.xml and save the file.
...
<route>
<pattern>/{partId:\d+}/{p_p_state}</pattern>
<generated-parameter name="mvcPath">
/html/parts/view_part.jsp
</generated-parameter>
</route>
<route>
<pattern>/{partId:\d+}</pattern>
<generated-parameter name="mvcPath">
/html/parts/view_part.jsp
</generated-parameter>
</route>
...

FRIENDLY URL ROUTES (I)


Once you’ve declared your intention to use Friendly URLs, you simply
need to provide a set of routes for Liferay to process.
Routes are a concept used in many web application platforms, including
Ruby on Rails, Cake PHP, and more.
A route is a URL pattern to match, followed by a set of mappings to
parameters, both generated and implicit.
Implicit parameters cover portal and portlet variables that are always
present in Portlet URLs.
Generated parameters are custom variables you want added to the
request: in our case, things like mvcPath and resourcePrimKey.

302
FRIENDLY URL ROUTES (II)

Accessing a Friendly URL is simple once the routes are defined.


All Friendly URLs on a page (Layout) are set off by a /-/ after the Layout
name.
The mapping specified in liferay-portlet.xml follows next.
Finally, one of the routes from your Friendly URL Routes XML file can be
used.

ROUTES DECLARATION
Friendly URL Routes are declared in a simple XML file.
Each Route is composed of a pattern and parameters:
The pattern is a string that shows the desired URL path:
<pattern>/view-story/{articleId: \d+}</pattern>

Parts of the URL that will be processed can be assigned to a variable


denoted by {}.
Each variable in the pattern can be constrained with regular expressions
(following a : after the variable):
<pattern>/view-story/{articleId: \d+}</pattern>

Parameters define Portlet URL parameters using the values of these


variables, or static values.

303
CHECKPOINT: TESTING FRIENDLY URLS
On the page with the Parts portlet, click the link to view any of the parts.
The URL at the top of the page should look something like this:

http://localhost:8080/group/moon/inventory/-/parts/11302/maximized

BONUS EXERCISE: ADDITIONAL ROUTES


Add additional routes for the edit functionality of the portlet:
<route>
<pattern>/edit/{partId:\d+}/{p_p_state}</pattern>
<generated-parameter name="jspPage">
/html/parts/edit_part.jsp
</generated-parameter>
</route>

<route>
<pattern>/edit/{partId:\d+}</pattern>
<generated-parameter name="jspPage">
/html/parts/edit_part.jsp
</generated-parameter>
</route>

304
USING FRIENDLY URLS
Though our implementation is simple, there is much customization you
can do on Friendly URLs:
Setting portlet window states.
Creating finder methods for your entity’s other fields, so they can be used
in the route instead of ugly primary keys.
Bonus: Implement human-readable routes for parts, such as the
following:

http:// .../group/moon/inventory/-/parts/turbo-encabulator-1

Notes:

305
6.5 Portlet Data Handlers
PORTLET DATA HANDLERS

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

OBJECTIVES
To understand how Liferay Portlet Data Handlers can be teamed up with
Staged Model Data Handlers, and used to import and export portlet data.
To review the Portlet Data Handler API.
To implement a Portlet Data Handler and Staged Model Data Handlers.
The snippets for the exercises in this presentation are in the category
05.5-Portlet Data Handler.

307
BACKGROUND
A common requirement for many data driven applications is the ability
to import and export data.
This is often accomplished by accessing the database directly and
running SQL queries to import or export data.
Accessing the database directly has several drawbacks:
Working with different database vendors might require customized SQL
scripts.
Access to the database may be tightly controlled and you may not be able
to import and export on demand.
DBAs at your organization may not give you access to the Liferay database.
Liferay provides the Liferay Archive feature to address the need to import
and export data in a database agnostic manner.

LIFERAY ARCHIVE
A Liferay Archive or LAR file is a
compressed file (ZIP archive) that can be
used to export or import data from
Liferay.
LAR files can be created for a single
portlet, a page (layout), or a set of public
or private pages (layout set).
Portlets that support importing and
exporting LARs provide an interface to let
users control how portlet data is exported
and/or imported.
Custom portlets can support the export
and import of LAR files by implementing
the Portlet Data Handler API.

308
WHEN TO USE A LAR
Backing up and restoring portlet specific data, without requiring a full
database backup
Cloning sites
Specifying a template to be used for users’ private or public pages
As a basis for more advanced features, such as the Local Live or Remote
Staging
Implementing the API for importing and exporting data enables custom
portlets to support all of these uses

LAR LIMITATIONS
<com.liferay.training.parts.model.impl.ManufacturerImpl>
LAR files are version <__cachedModel>false</__cachedModel>
<__new>false</__new>
specific (this includes <__uuid>333caabb-a468-4047-8547-ad293900115c</__uuid>
<__originalUuid>333caabb-a468-4047-8547-ad293900115c</__originalUuid>
Service Pack levels). <__manufacturerId>1</__manufacturerId>
<__companyId>10154</__companyId>
A LAR file may contain <__originalCompanyId>10154</__originalCompanyId>
<__setOriginalCompanyId>false</__setOriginalCompanyId>
user IDs to identify <__groupId>10706</__groupId>
<__originalGroupId>10706</__originalGroupId>
the creator or <__setOriginalGroupId>false</__setOriginalGroupId>
<__userId>10198</__userId>
modifier of data; <__userUuid>3806b0c8-55e6-4030-8291-c37e553ec466</__userUuid>
<__name>ShuttleCraft, Ltd.</__name>
however, the LAR file <__emailAddress>shuttlecraft@liferayspace.com</__emailAddress>
<__website>shuttlecraft.liferayspace.com</__website>
does not contain the <__phoneNumber>111-111-1111</__phoneNumber>
<__status>0</__status>
actual user data. <__originalStatus>0</__originalStatus>
<__setOriginalStatus>false</__setOriginalStatus>
<__statusByUserId>10198</__statusByUserId>
<__statusByUserName>Test Test</__statusByUserName>
<__userName>Test Test</__userName>
<__columnBitmask>0</__columnBitmask>
</com.liferay.training.parts.model.impl.ManufacturerImpl>

309
EXERCISE: CREATE SAMPLE CONTENT
Let’s create a sample web content article.
We’ll use this web content article to demonstrate the export/import
process.

1. Log in to the portal with your administrator account.


2. Add a Web Content Display portlet to the Welcome page.
3. Use the Web Content Display portlet to create and display a new web
content article entitled Space Program History.
! Use the space-program-history.txt and
space-program-history.jpg exercise files from the
05-liferay-apis folder to create the article.

EXERCISE: EXPORT LAR


Before we dive into the code, let’s learn how the export/import process
works for a single portlet.

1. Click on the Gear icon and select Export/Import on the Web Content
Display portlet on the Welcome page.
2. Click the Export button to accept the default options.
! Download the LAR file to your system.

310
EXPORT CONTROLS
Portlets implementing the Portlet Data
Handler API can provide controls to the
end user during the export process.
The options selected are passed to the
handler and can be used to modify the
behavior of the data handler.
Different Liferay assets have different
export options.
When exporting Wiki data, for example,
you can select a date range of Wiki data
to export, whether or not to export
comments and ratings, whether or not to
export the Wiki permissions, and more.

EXERCISE: IMPORT LAR


1. Navigate to the Moon Colony site.
2. Create a public Welcome page for the Moon Colony site.
3. Navigate to the Moon Colony site’s public Welcome page.
4. Add a Web Content Display portlet to the page.
5. Click on the gear icon and select Export/Import.
6. Click on the Import tab.
7. Click Select File, select the LAR file you downloaded, and click Continue
until you see the Import button.
! Click on the Import button, accepting the default options.

311
IMPORT CONTROLS
Portlets implementing the Portlet
Data Handler API can also provide
controls to the end user during
the import process.

STAGED MODELS
To support staging or import/export, a portlet’s
entities must be Staged Models.
Staged Models are marked by the portal as being
able to handle staging.
Quite simply, a Staged Model is a database entity
with a UUID and the Audit Fields: User Name,
Group Id, Company Id, Create Date, and Modified
Date.
<entity name="Manufacturer" uuid="true" local-service=
"true" remote-service="true" trash-enabled="true">

Create Date and Modified Date are used for


tracking what’s been published previously, and
what’s changed since the last publication.
UUID is used for finding already published or
imported entities.

312
CONSIDERATIONS
Before implementing a data handler for our Parts Inventory portlet, we’ll
have to make a few design decisions.
We have several portlets and three entities in our portlet. How many
data handlers will we implement?
We have established a relationship between parts and manufacturers.
How will that affect our export?
Will we allow manufacturers to be exported without parts?
Will we allow parts to be exported without manufacturers?

APPROACH
For training purposes, we’ll be working with a single data handler.
We’ll allow manufacturers to be exported or imported with or without
the parts.
We won’t, however, allow parts to be exported or imported without
manufacturers.
We’ll begin by creating our data handler class
Once we’ve successfully implemented the portlet data handler, we’ll
focus on implementing staged model data handlers.
Note: In a stroke of forward-thinking genius, we already made our
entities Staged Models. Check the
parts-inventory-portlet/docroot/WEB-INF/service.xml if you
want to verify.

313
STAGED MODEL CLASSES
Making entities Staged Models means that Service Builder has done
some of the work already.
[Entity]ExportActionableDynamicQuery classes were generated
for both our Staged Models.
These are convenience classes used in querying the entities during export.
We’ll be writing three classes:
InventoryDataHandler:
PartStagedModelDataHandler:
ManufacturerStagedModelDataHandler:
Other than writing our classes, we simply need to declare them in our
liferay-portlet.xml
When finished, custom portlet data can be staged, exported, and
imported, just like Liferay’s core portlets.

PORTLET DATA HANDLER


In order for our portlet to support the LAR export and import, we will
implement a portlet data handler.
Our data handler will extend the BasePortletDataHandler class.
BasePortletDataHandler is an abstract class which provides a base
implementation of the PortletDataHandler interface.
The data handler queries entities and calls the
[Entity]StagedModelDataHandler classes we’ll create.
Even though BasePortletDataHandler is an abstract class, it
contains implementations for all its methods, so we only have to
override the methods that we need.

314
STAGED MODEL DATA HANDLERS
In an export scenario, the portlet data handler makes the proper call to
query and serialize the exportable database entities.
manufacturerActionableDynamicQuery.performActions();

We’ll need to implement another class to handle entity-specific export


logic to be run on each entity in the returned list.
Our staged model data handlers will extend the
BaseStagedModelDataHandler class.
We’ll create a staged model data handler for parts and a separate one
for manufacturers.
These classes will implement XML handling logic that gets each entity
into a separate XML file in the LAR we’ll export.
On import, the same classes will contain logic to parse the XML elements
into objects, and either create them or update them in the database, if
they already exist.

EXERCISE: CREATE THE DATA HANDLER (I)


1. Right-click on the parts-inventory-portlet project and select New →
Class.
2. Enter com.liferay.training.parts.lar for the package.
3. Enter InventoryDataHandler for the Name.
4. Click the Browse button next to the Superclass.
5. Begin typing BasePortletDataHandler (Choose the one that’s in the
com.liferay.portal.kernel.lar package).
! Click Finish.

315
EXERCISE: CREATE THE DATA HANDLER (II)
By default, all entities get support for exporting Data, Permissions, and
Categories.

1. Insert snippet 01-Get-Controls into the InventoryDataHandler class body.


2. Press Ctrl-Shift-O to organize imports.
import com.liferay.portal.kernel.lar.PortletDataHandlerBoolean;
import com.liferay.portal.kernel.lar.StagedModelType;
import com.liferay.training.parts.model.Manufacturer;
import com.liferay.training.parts.model.Part;

EXERCISE: CREATE THE DATA HANDLER (III)


Now we’ve created the class constructor, which instantiates the import
and export controls, and the StagedModelType variables for Parts
and Manufacturers.
There’s one important difference in the PortletDataHandlerBoolean
instantiation for Manufacturers. The disabled parameter is set to
true, whereas it’s false for Parts.
new PortletDataHandlerBoolean(NAMESPACE, "manufacturers", true,
true, null, Manufacturer.class.getName()),
new PortletDataHandlerBoolean(NAMESPACE, "parts", true, false,
null, Part.class.getName()));

With this boolean parameter, we’re disabling the selection check-box


that allows Manufacturers to be selected or deselected during import
or export; Manufacturers should always be imported and exported in
our use case.

316
EXERCISE: CREATE THE DATA HANDLER (IV)
1. After the class constructor, insert snippet 02-doDeleteData.
The static utility class’s delete method is called if the Delete Portlet Data
Before Importing option is selected in the portal.
protected PortletPreferences doDeleteData(
PortletDataContext portletDataContext, String portletId,
PortletPreferences portletPreferences) throws Exception {

PartLocalServiceUtil.deletePart(portletDataContext.getScopeGroupId());

ManufacturerLocalServiceUtil.deleteManufacturer(portletDataContext.
getScopeGroupId());

return portletPreferences;
}

2. Organize imports (Ctrl-Shift-O).


import com.liferay.portal.kernel.lar.PortletDataContext
import javax.portlet.PortletPreferences

EXERCISE: CREATE THE DATA HANDLER (V)


1. After the doDeleteData() method, insert snippet 03-doExportData.
protected String doExportData(PortletDataContext portletDataContext,
String portletId, PortletPreferences portletPreferences)
throws Exception {

Element rootElement = addExportDataRootElement(portletDataContext);

if (portletDataContext.getBooleanParameter(NAMESPACE, "manufacturers")) {
ActionableDynamicQuery manufacturerActionableDynamicQuery = new
ManufacturerExportActionableDynamicQuery(portletDataContext);

manufacturerActionableDynamicQuery.performActions();
}
...
return getExportDataRootElementString(rootElement);
}

The XML root element <InventoryDataHandler> is returned and the


generated code is called that references our staged model data handler
class to determine how we want to populate our entities into the LAR.

317
EXERCISE: CREATE THE DATA HANDLER (VI)
1. Insert snippet 04-doImportData after the doExportData() method.
@Override
protected PortletPreferences doImportData(
PortletDataContext portletDataContext, String portletId,
PortletPreferences portletPreferences, String data)
throws Exception {

if (portletDataContext.getBooleanParameter(NAMESPACE, "manufacturers")) {
Element manufacturersElement =
portletDataContext.getImportDataGroupElement(Manufacturer.class);

List<Element> manufacturerElements = manufacturersElement.elements();

for (Element manufacturerElement : manufacturerElements) {


StagedModelDataHandlerUtil.importStagedModel(
portletDataContext, manufacturerElement);
}
}

This method parses the LAR’s XML elements into Manufacturer entities.

EXERCISE: CREATE THE DATA HANDLER (VII)


1. Insert snippet 05-doPrepareManifestSummary after the
doImportData() method.
@Override
protected void doPrepareManifestSummary(
PortletDataContext portletDataContext,
PortletPreferences portletPreferences)
throws Exception {

ActionableDynamicQuery manufacturerActionableDynamicQuery =
new ManufacturerExportActionableDynamicQuery(portletDataContext);

manufacturerActionableDynamicQuery.performCount();

ActionableDynamicQuery partActionableDynamicQuery =
new PartExportActionableDynamicQuery(portletDataContext);

partActionableDynamicQuery.performCount();
}

2. Organize imports and save the file.

318
STAGED MODEL DATA HANDLERS
Our data handler holds the code for executing the import and export of
data and controls what can be exported or imported.
We still need to add logic so we can exert further control over the
specific entities we’re importing and exporting.
For instance, we stated that we want parts to only be imported or
exported with their manufacturers. Well, we need our portlet data
handler to reference staged model classes that define the import and
export behavior of our entities.

EXERCISE: CREATING THE PART


STAGED MODEL DATA HANDLER (I)
1. Right-click on the parts-inventory-portlet project and select New →
Class.
2. Enter com.liferay.training.parts.lar for the package.
3. Enter PartStagedModelDataHandler for the Name.
4. Click the Browse button next to the Superclass and select
BaseStagedModelDataHandler.
5. Click Finish.
6. In the class, specify <Part> as the type <T>.

319
EXERCISE: CREATING THE PART
STAGED MODEL DATA HANDLER (II)
1. In the class body, insert snippet 06-Part-Staged-Model.

Several housekeeping methods are declared here:


A method for deleting Part entities if the user selects that option in the UI.
A method for returning the class name, used in introspecting this class.
A method for returning the name field of this entity.
And finally, a method that returns the status of these entities, in case
they’re going through workflow.

EXERCISE: CREATING THE PART


STAGED MODEL DATA HANDLER (III)
1. After the getExportableStatuses() method, insert snippet
07-doExportStagedModel.
! This method does three notable things:
It creates an Element from each Part object in the database
Element partElement = portletDataContext.getExportDataElement(part);
It calls the StagedModelDataHandlerUtil to export parts with their
manufacturers.
StagedModelDataHandlerUtil.exportReferenceStagedModel(
portletDataContext, part, manufacturer,
PortletDataContext.REFERENCE_TYPE_DEPENDENCY);
Additionally, we’re calling addClassedModel, which will serialize the
entities into a LAR file, each as its own XML file, with database fields
populated as XML elements.
portletDataContext.addClassedModel(partElement,
ExportImportPathUtil.getModelPath(part), part);

320
EXERCISE: CREATING THE PART
STAGED MODEL DATA HANDLER (IV)
1. Following the end the doExportStagedModel() method, insert snippet
08-doImportStagedModel.
...
if (portletDataContext.isDataStrategyMirror()) {
Part existingPart = PartLocalServiceUtil.fetchPartByUuidAndGroupId(
part.getUuid(), portletDataContext.getScopeGroupId());

if (existingPart == null) {
serviceContext.setUuid(part.getUuid());

importedPart = PartLocalServiceUtil.addPart(part,
serviceContext);
} else {
importedPart = PartLocalServiceUtil.updatePart(part,
serviceContext);
}
} else {
importedPart = PartLocalServiceUtil.addPart(part, serviceContext);
}
...

EXERCISE: CREATING THE MANUFACTURER


STAGED MODEL DATA HANDLER (I)
1. Right-click on the parts-inventory-portlet project and select New →
Class.
2. Enter com.liferay.training.parts.lar for the package.
3. Enter ManufacturerStagedModelDataHandler for the Name.
4. Click the Browse button next to the Superclass.
5. Begin typing BaseStagedModelDataHandler
6. Click Finish.
7. In the class, specify <Manufacturer> as the type <T>.

321
EXERCISE: CREATING THE MANUFACTURER
STAGED MODEL DATA HANDLER (II)
1. In the class body, insert snippet 09-Manufacturer-Staged-Model.
This snippet populates the class, following the pattern of the snippets we
added to the PartStagedModelDataHandler. The biggest difference is
that we are not ensuring that parts are associated with manufacturers on
import or export.
2. Reorganize the imports.

EXERCISE: REGISTER DATA HANDLER


Before we can use our newly created functionality, we need to tell
Liferay about our new data handling classes.

1. Open docroot/WEB-INF/liferay-portlet.xml
2. Insert snippet 10-liferay-portlet.xml after the <indexer-class>
element in the Manufacturer portlet section.
3. In the Parts portlet, after the <friendly-url-routes> element,
declare the data handler again.

<portlet-data-handler-class>com.liferay.training.parts.lar.InventoryDataHandler
</portlet-data-handler-class>

322
EXERCISE: TEST EXPORT
1. Log in to the portal with your administrator account, and navigate to the
Moon Colony site’s Manufacturer Portlet (accessible via Admin →
Content).
2. Add a new manufacturer and a new part.
3. Click on the gear icon of the Manufacturer portlet and select
Export/Import.
4. Accept the default export settings and click the Export button.
5. Download the Manufacturer LAR file.
! After you download the LAR, navigate to the Mars Colony site’s
Manufacturer Portlet.

EXERCISE: TEST IMPORT


1. Click on the gear icon in the Manufacturer portlet and select
Export/Import.
2. Click on the Import tab.
3. Click Select File and select the LAR file you downloaded.
4. Click Continue until you see the Import button.
! Click on the Import button to accept the default options.

323
REVIEW: PORTLET DATA HANDLERS
The Liferay Archive (LAR) process allows you to export and import portlet
data in a database agnostic way.
Liferay provides an API that developers can use to enable LAR
functionality in their custom portlets.
The developer must create classes that implement
com.liferay.portal.kernel.lar.PortletDataHandler, and
com.liferay.portal.kernel.lar.StagedModelDataHandler.
Portlets can provide custom data handler controls to provide runtime
export and import options.
Since much of the code can be reused, regardless of the entities being
handled, it takes little time to enable export/import and staging
functionality for your custom portlets.

Notes:

324
6.6 Search Engine Optimization
SEARCH ENGINE OPTIMIZATION

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

SEO FOR YOUR PORTLET


Chances are, if you’re a web developer, we don’t need to tell you about
the importance of Search Engine Optimization.
You can make your portlet’s data available as standard HTML page
elements so they can be crawled successfully by search engines.
Liferay provides an API for improving the way custom asset types get
indexed.

326
SEO RELATED METHODS
Each of our Manufacturers and Parts has a dynamically generated page.
We can use the following methods in the JSP that generates those pages
to make sure that all the proper values are available.
addPageSubtitle: adds a subtitle to the page’s metadata.
setPageTitle: sets the HTML page title to what you specify.
setPageDescription: sets the HTML page description to what you
specify.
setPageKeywords: sets specified keywords for the page.
addPortletBreadcrumbEntry: adds an entry for Liferay’s breadcrumb
system to use.

EXERCISE: VIEW_MANUFACTURER.JSP
1. Open view_manufacturer.jsp and add snippet 01-Add SEO Methods
at the bottom of the section where the mfg variable is set. (This section
is near the top of the file.)
2. Add the required imports from the 02-init.jsp snippet to init.jsp.
3. Add snippet 03-Add SEO Attribute to the Manufacturer portlet section in
liferay-portlet.xml, after the <workflow-handler> tag.
4. Save all edited files.
5. Navigate to a manufacturer and note that the manufacturer’s name
appears in the browser’s title bar.
6. Edit a manufacturer and add one or more tags to it.
! Navigate to the manufacturer that you edited and use Firebug or Chrome
Developer tools to examine the page and confirm that tags are now
declared as meta keywords in the page header.

327
BONUS EXERCISE: VIEW_PART.JSP
Make the same improvements to view_part.jsp that you made to
view_manufacturer.jsp.
In view.jsp, invoke the following methods with the correct arguments:

PortalUtil.addPortletBreadcrumbEntry(...);
PortalUtil.setPageSubtitle(...);
PortalUtil.setPageDescription(...);
PortalUtil.setPageKeywords(...);

If you aren’t sure about the correct arguments, refer to


view_manufacturer.jsp.
Watch out for any differences between manufacturers and parts. Hint:
Remember that we localized the part name but not the manufacturer
name.

Notes:

328
6.7 Using the Recycle Bin
USING THE RECYCLE BIN

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

OBJECTIVES
To understand how using the Recycle Bin in Liferay portlets improves
end user experience
To review the Trash Handler API
To implement recycle bin functionality in the Parts Inventory Portlet
The snippets for the exercises in this presentation are in the category
05.7 Trash.

330
BACKGROUND
A common requirement for many applications is the ability to delete
data.
It is often convenient if applications can restore ”deleted” data.
Liferay provides the ability to configure your portlet to send deleted
items to the recycle bin, rather than deleting them from the database.
Items sent to the recycle bin can be restored for a (configurable) period
of time before they are permanently deleted. Or they can be explicitly
deleted from the recycle bin itself.

331
RECYCLE BIN FRAMEWORK
We’ll demonstrate four capabilities your portlet can leverage when you
implement asset recycling in your portlets:
Moving assets to the recycle bin
Restoring assets from the recycle bin
Enabling the Undo action for a recycled asset
Resolving conflicts between items currently in your portlet and those
being restored from the recycle bin

RECYCLING PARTS
The Moon Colony Inventory Manager needs the ability to delete parts
from the Parts Inventory portlet if they are no longer in production.
However, it’d be a real pain if he or she accidentally deleted the Turbo
Encabulator from the database, when the Flux Capacitor was actually
supposed to be deleted.
We’ll make it easy for the inventory manager to restore parts from the
recycle bin, or even undo the action without navigating to the recycle
bin UI at all.
We’ll take the following steps to implement the recycle bin functionality
for Parts entities:
Enable trash for service entities
Implement a Trash Handler for Parts
Create a service method to move Parts to the recycle bin
Create a portlet action to initiate moving Parts to the recycle bin
Implement a Trash Renderer for Parts

332
EXERCISE: ENABLING THE TRASH FEATURE (I)
1. Open docroot/WEB-INF/service.xml, and choose the Source view.
2. Inside the <entity> tag for the Parts and Manufacturer portlets, enable
trash by specifying trash-enabled="true".
<entity name="Part" uuid="true" local-service="true" remote-service="false"
trash-enabled="true">

We’ll enable the recycle bin for both entities, but we’ll only implement it
for the Parts portlet.

EXERCISE: ENABLING THE TRASH FEATURE (II)


Because of a method we’ll be adding to our PartLocalServiceImpl
later, we’ll need to specify the proper reference classes so Service
Builder generates the code we need in PartLocalServiceBaseImpl.

1. Open service.xml.
2. In the References section of the Part entity, add snippet 01-Reference
Classes.
<reference package-path="com.liferay.portlet.trash" entity="TrashEntry" />
<reference package-path="com.liferay.portlet.trash" entity="TrashVersion" />

3. Save the file and rebuild services.

333
TRASH HANDLING
We’ll need to create a trash handler class so we can handle necessary
trash-related tasks.
We’ll implement the following methods in our trash handler, which we’ll
extend from BaseTrashHandler:
deleteTrashEntry(): deletes the entity
getClassName(): returns the class name handled by the trash handler
getRestoreMessage(): returns the message with the location of the
restored entity
hasTrashPermission(): checks that a user has the required permissions for
performing trash actions on an entity
isInTrash(): uses primary key to check whether an entity is in the recycle
bin
restoreTrashEntry(): restores the entity

EXERCISE: CREATING A TRASH HANDLER


We’ll need to implement a trash handler for any entity we want to
recycle.
For our purposes, we’ll give recycle bin functionality to Parts.

1. Create a new package in the parts-inventory-portlet plugin


project called com.liferay.training.parts.trash.
2. Inside the new package, add a new class PartTrashHandler that
extends BaseTrashHandler.
3. Replace the contents of the class body with snippet 02-PartTrashHandler.
4. Organize the imports and save the file.

Ignore the errors that are still present. We’ll resolve these later.

334
EXERCISE: DECLARING THE TRASH HANDLER
We need to declare the Trash Handler we just created in our project’s
docroot/WEB-INF/liferay-portlet.xml.

1. Open docroot/WEB-INF/liferay-portlet.xml.
2. In the Parts Portlet section, find the <asset-renderer-factory>
element and add snippet 03-Handler Declaration immediately after it.

<trash-handler>com.liferay.training.parts.trash.PartTrashHandler</trash-handler>

EXERCISE: ADDING SERVICE METHODS


So far, we’ve implemented trash handling functionality for Parts in the
recycle bin and declared our new class in liferay-portlet.xml.
However, our entities currently have no way to get to the recycle bin. We
need a service method for that.

1. Open PartLocalServiceImpl.
2. After the last method of the class, add snippet 04-movePartToTrash.
3. It’s pretty obvious that we’ll also want to restore parts from the recycle
bin as well, so add snippet 05-restorePartFromTrash to
PartLocalServiceImpl, after your new movePartToTrash method.
4. Organize imports and Build services.

You’ll notice there’s still one error in PartTrashHandler. It will be


resolved when PartAssetRenderer implements TrashRenderer.

335
EXERCISE: PORTLET ACTION FOR MOVING PARTS TO THE
RECYCLE BIN
Now we’ll modify our PartsPortlet so the deletePart method
invokes the newly created movePartToTrash service method.
We’ll provide a condition here that only invokes movePartToTrash if
the recycle bin is enabled for the site inside Liferay where our portlet is
being used. If recycling is disabled, we can tell our portlet to simply
invoke the deletePart service method.

1. Open the PartsPortlet class.


2. Replace the deletePart method with snippet 06-deletePart.

EXERCISE: PORTLET ACTION FOR RESTORING PARTS


We’ll make a restorePart() method that will call our service method
to restore parts from the trash.

1. In the PartsPortlet class, right after the new deletePart method,


add snippet 07-restorePart.
2. Organize your imports and save the file.

336
EXERCISE: INVOKING PORTLET ACTION IN A JSP
part_actions.jsp already calls our delete action, but the icon for
deleting and recycling looks the same at this point.
Let’s modify the icon in our JSP to use a different image and text to
differentiate between deleting and recycling.

1. Find the current code in the part_actions.jsp that controls part


deletion:
<c:if test="<%= PartPermission.contains(permissionChecker, partId,
ActionKeys.DELETE %>">
...
</c:if>

2. Replace it entirely with snippet 08-Part Actions.


3. We need to add an import to our JSP, so open
docroot/html/init.jsp and insert:
<%@ page import="com.liferay.portlet.trash.util.TrashUtil" %>

EXERCISE: RENDERING PARTS IN THE RECYCLE BIN (I)


Now we can move Parts to the recycle bin, but we still need to render
them in the recycle bin.
Since Parts are already enabled as assets, we can use the
PartAssetRenderer to implement our trash rendering functionality.

1. Open the PartAssetRenderer class and edit it so it implements


TrashRenderer.
The PartTrashHandler error should be gone when you save the file.
2. Add snippet 09-TrashRenderer Methods after the last method of the
class. These methods add implementations for getType() and
getPortletId().
3. Replace the current getTitle() method with snippet 10-getTitle. This
modifies the getTitle() method so it calls getOriginalTitle() if
the part is in the trash.

337
EXERCISE: RENDERING PARTS IN THE RECYCLE BIN (II)
Let’s give our parts a thumbnail image when they’re rendered in the
recycle bin. We already have an image that we used as a portlet icon in
the Assets module.

1. In PartAssetRenderer, add snippet 11-getThumbnailPath.


@Override
public String getThumbnailPath(PortletRequest portletRequest)
throws Exception {

ThemeDisplay themeDisplay = (ThemeDisplay)portletRequest.getAttribute(


WebKeys.THEME_DISPLAY);

return themeDisplay.getPortalURL() +
"/parts-inventory-portlet/part.png";
}

CHECKPOINT: MOVING PARTS TO THE RECYCLE BIN


1. Navigate to the Moon Colony site and make sure there is an existing part
in the Parts portlet.
2. Click Actions → Move to Recycle Bin for a part without open orders.
3. Navigate to Admin → Site Administration → Content → Recycle Bin, and
your part should be listed.

338
RESTORING PARTS FROM TRASH
Now we can get parts into the recycle bin, and restore them.
In PartTrashHandler, we created restoreTrashEntry to restore
trash entries to their original location.
If you remember, we already added a restorePartFromTrash service
method to PartLocalServiceImpl that will let us restore parts from
the trash.
In our method, we call assetEntryLocalService.updateVisible,
provide the class name, the part ID, and set the boolean parameter to
true. This restores the part’s visibility in the Parts Portlet.
@Override
public void restoreTrashEntry(long userId, long classPK)
throws PortalException, SystemException {

PartLocalServiceUtil.restorePartFromTrash(userId, classPK);
}

EXERCISE: DELETING PARTS FROM TRASH


Now that we have trash entries, we need to make sure the trash entry is
deleted when we choose to delete a part from the recycle bin.
We’ll do this in our deletePart service method.

1. Open the PartLocalServiceImpl class and locate the


deletePart(Part part) method.
2. Right after the code that deletes the asset entry, add the
12-deleteTrashEntry snippet to delete the trash entry.

339
CHECKPOINT: RESTORING PARTS
1. Now go back to the Recycle Bin and click Actions → Restore for the part
you deleted in the previous checkpoint.
! You should see a message indicating it was restored; if you return to the
page with the Parts Portlet on it, you’ll see the Part in its original
location.

UNDO FUNTIONALITY
Recycling and restoring Parts is convenient. We can make it even more
convenient by adding an option to undo the last recycle action. This is
useful if you accidentally recycled the wrong part, for instance.
Once we add the undo functionality, you won’t need to go to the recycle
bin to restore a part that you just deleted.
We can get this done in three steps:
Add the undo tag to our JSP
Create a restorePart action
Provide the trashed part’s information to the
<liferay-ui:trash-undo> taglib

340
EXERCISE: IMPLEMENTING UNDO FUNCTIONALITY
1. Open docroot/html/parts/view.jsp, and add snippet 13-view.jsp
right after the last <liferay-ui:error /> tag.
2. Open the PartsPortlet class, find the deletePart(...) method,
and add snippet 14-SessionMessages to the end of its
if(moveToTrash) statement.
SessionMessages
.add(request,
PortalUtil.getPortletId(request)
+ SessionMessages.KEY_SUFFIX_HIDE_DEFAULT_SUCCESS_MESSAGE);

RESTORING STAGED MODELS FROM THE TRASH


StagedModelDataHandler classes were implemented for Parts and
Manufacturers in the slide deck on Portlet Data Handlers.
Now that we’ve implemented trash handling, we need to add
doRestoreStagedModel() methods in our
StagedModelDataHandler classes, so that parts that are in the trash
can be restored, if we try to import a LAR with those recycled parts in it.

1. Open the PartStagedModelDataHandler class in the


com.liferay.training.parts.lar package.
2. Insert snippet 15-doRestoreStagedModel-Part as the last method in the
class.
3. Do the same in the ManufacturerStagedModelDataHandler, using
snippet 16-doRestoreStagedModelDataHandler-Manufacturer.
4. Rebuild your services.

341
CHECKPOINT: UNDOING A RECYCLE ACTION
1. Move a part to the recycle bin by choosing Actions → Move to Recycle
Bin.
2. You should see the option to Undo the action in the top of the Parts
Portlet. Click Undo.
! The part is restored to its original location, without having to navigate to
the recycle bin.

Notes:

342
Chapter 7

RAD with CMS

7.1 Rapid Development in Liferay CMS

343
RAPID DEVELOPMENT IN LIFERAY CMS

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

MODULE GOALS
To understand RAD
To understand how CMS is implemented in Liferay
To review the basics of web content creation
To explore templates as applications
To use Expando models
To integrate Alloy features
To build a simple application
The exercise files for this slide deck are in the
06-rad-with-cms/00-rad-cms-overview folder.

344
DEVELOPING APPLICATIONS
Implementing custom business logic and functionality usually requires
creating a custom portlet.
Building portlets requires large amounts of time, expertise, testing, and
deployment.
Implementing custom entities (models) in portlets requires building
services to generate a service layer.
Even simple portlets present a fair amount of complexity in the
numerous configuration files, JSPs, portlet classes, and the deployment
process.
Is there a simpler approach?

RAPID APPLICATION DEVELOPMENT


The solution to a complex development process is to streamline it in a
process called rapid application development (RAD).
The RAD development paradigm requires that writing new applications
be as simple as possible.
It focuses attention directly on the needs of the application.
This means that complex setup and configuration are handled
behind-the-scenes or highly simplified.
RAD also implies that deployment of new applications and updates be as
easy as possible.
In the often complex world of web application programming, RAD has
high value.

345
CURRENT DEVELOPMENT IN LIFERAY
What if we want to build a Job Listing portlet for our Space Program?
A sample list of requirements could have been given to us:
Allow job postings to be added, edited, or deleted
Allow users to apply for jobs
Allow comments to be posted regarding the job and/or applications
Allow user data to be tracked for auditing purposes
Allow a workflow to be used to manage the posting and editing of jobs
Allow permissions to be used with roles
Take a moment to list just some of the things you would need to start
building this portlet.

CURRENT DEVELOPMENT IN LIFERAY


To build this portlet, we could approach it by:
Creating a Service Builder portlet
Create entities for Jobs, Programs, and Posts
Integrating a commenting system
Creating a portlet class to manage the controller logic
Creating permissions
Creating workflow rules
Creating audit information
Training users in using the portlet
Building services, compiling, and deploying
Further debugging, re-factoring, compiling, and deploying, as needed

346
ACCELERATING DEVELOPMENT IN LIFERAY
With all that planning, coding, and testing, what if we could:
Rapidly prototype a functioning jobs listing
Iterate over the UI, tweaking it for performance and usability
Use built-in permissions, indexing, and audit trails
Skip the need to deploy, and have changes available on save
Planning time would decrease, development time would decrease, and
our application server wouldn’t need to worry about deployment.
Is this a realistic goal?

CONTENT MANAGEMENT IN LIFERAY


Liferay’s Web Content portlets come with a wide range of built-in
features.
Easy-to-use forms and rich text editors simplify the writing process.
Structures provide the backbone to Web Content articles, and can be
easily created in the UI or via an editor.
Templates provide rich functionality for formatting and styling the
content, with advanced features we’ll explore later.

347
CONTENT MANAGEMENT: ADDITIONAL FEATURES
In addition to these powerful features, Web Content:
Integrates seamlessly with Workflow
The use of permissions is fine-grained
Audit data is tracked in the portlet
Import/Export of Web Content is well-supported (LAR)
Easy to use and setup
Web Content provides powerful mechanisms for creating articles, pages,
and static text.
These powerful mechanisms can be used to create complex, dynamic
applications without the need to deploy a new portlet!

BASIC WEB CONTENT MANAGEMENT


To understand how to use Web Content, let’s create some basic content
and display it on our site.
We’ll make a sample job post, much like what we’ll want to display in
our application.
You’ll see how easy it is to enter static content, set it in a structure, and
manipulate it in a template.

348
EXERCISE: CREATE A JOB POST (I)
1. Log in with your administrator account, and navigate to the About Us →
Careers page in the Space Program site.
2. Add the Web Content Display portlet to the page:

3. Click the Add button on the bottom of the portlet.

EXERCISE: CREATE A JOB POST (II)


1. Enter Space Program Astronaut for the title.
2. Click on the Source button in the rich text editor (CK Editor):
3. Paste in the contents of
exercises/06-advanced-developer/06-rad-with-cms/
00-rad-cms-overview/01-job-post.txt:

349
EXERCISE: CREATE A JOB POST (III)
! Click the Publish button to save the new Web Content article:

JOB POST IMPROVEMENTS


Now that we’ve created our job post, we could duplicate the effort for as
many posts as we need.
Using this method, however, we have to rely on the writer to correctly
format and enter the information.
What if we could help guide the data entry?
What if we could automate the formatting?
We can use Liferay Structures and Templates to accomplish these tasks.
Both Structures and Templates play an important role in creating Web
Content applications.

350
INFORMATION IN A JOB POST STRUCTURE
We will use a structure to specify some required pieces of information.
This will make it easier to enter data and will encourage quality job post
content.
Some of the job post information we need includes:
Job Title
Job Location
Space Program Mission associated with the Job
Who can apply for the Job
When can you apply for the Job
Any required qualifications for the Job
Any additional information about Job duties

CREATING A JOB POST STRUCTURE


To make data entry easier and guarantee we have all the information
required, we’ll create a Web Content Structure.
Structures are simple XML schemas that define forms and inputs that a
writer, or end-user, will use to create Web Content.
We will use Structures as schemas for our own data models.
For now, let’s describe our basic job listing in a structure.
<root>
<dynamic-element name="JobTitle" type="text" index-type="" repeatable="false">
...</dynamic-element>
<dynamic-element name="Department" type="text" index-type="" repeatable="false">
...</dynamic-element>
...<dynamic-element name="JobSummary"
type="text\_area"index-type="" repeatable="false">
...
<dynamic-element name="KeyRequirements" type="text" index-type="" repeatable="true">
... </dynamic-element>
</dynamic-element>
</root>

351
EXERCISE: JOB POST STRUCTURE
1. Go to Admin → Site Administration → Content. With The Space Program
selected in the context menu selector, go to Web Content and select
Manage → Structures.
2. Click on the Add button.
3. In the Name field for the Structure, enter the name Job Post Structure.
4. Enter a description for the Structure.
5. Click the Source tab.
6. Replace the default root tags with in the contents of
exercises/06-advanced-developer/06-rad-with-cms/
00-rad-cms-overview/02-job-post-structure.xml.
! Click the Save button to save the XML schema.

CHECKPOINT: JOB STRUCTURE


You should now have a brand new Structure listed:

352
CREATING A JOB POST TEMPLATE
Now that we have the Structure to help direct the input of information,
we need a Template to pair it with.
Templates are scripted documents that can style and direct the flow of
Web Content, based on the Structure.
Templates support multiple languages:
FreeMarker
Velocity
XSLT
We will focus on Velocity, but concepts carry over into the other two
options.

STYLING THE JOB POST


Our Template will accomplish a few, much-needed enhancements:
Style the Job Post to match a predetermined look and feel for each post.
Retain the elements of the Structure while styling them.
Be completely transparent to users, reinforcing the separation of content
creation versus styling.
Templates can do much more, but their basic function is to collect the
content from a Structure and present it in an appealing fashion.

353
EXERCISE: JOB POST TEMPLATE (I)
1. Go to Admin → Site Administration → Content. With The Space Program
selected in the context menu selector, go to Web Content then Manage
→ Templates.
2. Click on the Add button.
3. In the Name field for the Template, enter the name Job Post Template.
4. Enter a description for the Template.
5. Choose Velocity as the Template Language.
6. Under Structure, click Select and choose the Job Post Structure you made
earlier.
7. Underneath the Script heading, copy the contents of the
.../00-rad-cms-overview/03-job-post-template.vm file and
paste it into the editor.
8. Click the Save button to save the Velocity template.

EXERCISE: JOB POST TEMPLATE (II)


1. Go back to the Careers page.
2. Click on the Add button in the Web
Content Display portlet.
3. Click on the Select button next to the
Structure heading and choose the Job
Post Structure we just made:

354
EXERCISE: JOB POST TEMPLATE (III)
1. For the Title, enter Job Post. Then enter the rest of the required
information for your job post, guided by the fields and explanations from
the structure.
2. For requirements and locations (repeatable content), click the Add icon
to add more items to the list:
! Click Publish and view the new content in the portlet.

JOB POST WITH STRUCTURE AND TEMPLATE


With a Structure and Template, we were able to direct input to describe a
proper job listing, and format the output to guarantee a look and feel:

355
GOING FURTHER
We have already seen how Structures can be used to describe a data
model, but we will explore how to make it even more valuable.
Templates provide a great way to format plain input, guaranteeing a look
and feel, but scripting provides even more powerful tools.
Using Templates, Structures, and more, we will take this sample and
expand it, building a robust job entry system entirely in Web Content.

Notes:

356
7.2 Using CMS Structures
USING CMS STRUCTURES

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

GOALS
To understand Web Content Structures
To use Structures as data objects
To build the Jobs Posting portlet with Structures

358
WEB CONTENT STRUCTURES
Structures, as we have seen, can be used to help guide writer input on a
piece of Web Content.
Using a Structure, the designer can control what information is gathered,
and then turn to a Template to properly format and style that
information.
In our Job Post example, we used the Structure to help guide what parts
of a job listing we needed.
This Structure can also be used to describe a data model:
Fields are attributes
Built-in editing and creation mechanism
Versioning
Auditing

STRUCTURES AS DATA MODELS (I)


Using our job post Structure, we have described a data model that can
be used to store future Job Posts with little effort.
If we were writing this in a portlet, we would need to:
Create an entity in Service Builder (likely called JobPost)
Add fields to that entity that lined up with the fields we have in our
Structure
Provide business logic to store, retrieve, and update the model
Provide controller logic to direct the user through those processes
Provide front-end pages (JSPs) to enter data, display data, and delete data

359
STRUCTURES AS DATA MODELS (II)
By using a Web Content Structure, all of that functionality is provided for
us.
To create a new ”entry,” all we need to do is provide a link to the user to
add a piece of Web Content that uses our Structure.
In addition to basic CRUD operations, using Web Content gives us:
Built-in permissioning
Versioning
Localization
Indexing in Search

MODIFYING OUR JOB POST STRUCTURE


Our current Job Post Structure gives us a good foundation for data entry.
For example, by default, all of our fields will be indexable.
Indexing for a Structure field allows the Search Indexer to store values
for our models, giving us an easy way to access them from the Search
Engine API.
If you do not want certain fields to be indexed, you can set them to Not
Indexable in the structure editor.

360
SEARCH: KEYWORD OR TEXT?
You can choose fields to be indexed by keyword or text.
Choosing keyword indexes the text exactly as it is entered.
Choosing text tokenizes the text first before it is indexed.
Tokenized text is missing common words, such as ”the,” ”and,” ”but,”
”for,” ”to,” etc.
Use keyword for text fields containing terms that need to be indexed
verbatim.
Use text for longer content fields, where longer pieces of text are stored.

STRUCTURES REVIEW
We now have a Structure representing our data model, Job Post.
Each of the Structure fields represents a data field we would normally
need a database table for.
By enabling Indexable on some of the fields, we have an easy way to
find collections of related posts, or specific posts without resorting to
low-level database queries.
Use Structures to store data models when you need:
Versioning
Built-in CRUD and data entry
Workflow integration
Built-in audit information

361
Notes:

362
7.3 Understanding Velocity Templates
UNDERSTANDING VELOCITY TEMPLATES

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

GOALS
To understand Velocity basics
To use Velocity for formatting
To use Velocity for program flow
To build a simple Velocity portlet
The exercise files for this slide deck are in the
06-rad-with-cms/02-velocity-templates folder.

364
WHAT IS VELOCITY?
Velocity is a templating engine provided by the Apache Foundation. It
includes the Velocity Templating Language (VTL).
The term Velocity is used in Liferay to specifically refer to Templates
written in VTL, also referred to as VM files, or Velocity Markup.
Velocity provides an API that exposes data from the underlying article
structure, Liferay’s themeDisplay object, as well as the
renderRequest object and many more.

VELOCITY BASICS
Velocity provides access to a set of directives and variables.
Directives are preceded by a #, and variables with a $.
New variables are created with the #set directive:
#set ($myVariable = "Test")

Variable access works the same as Java objects (variables can be


surrounded with {}):
$myVariable or ${themeDisplay.getURLCurrent()}

Common directives:
#set: sets the value of a variable
#if: starts an if block
#foreach: starts a for loop over a List.
#end: ends a block, such as #foreach or #if
#set ($myVariable = "Test")

$myVariable or ${themeDisplay.getURLCurrent()}

365
WEB CONTENT VARIABLES
Liferay provides a large amount of objects to Web Content Templates.
A few common objects are:
paramUtil: simplifies dealing with URL parameters
portalUtil: contains helper methods for getting portal information
dateUtil: simplifies handling dates and date formats
renderRequest & request: contains information about the request,
not identical to the Java request object
themeDisplay: contains information about the theme, current page, and
layout
user: contains the current user object for the logged-in user

PUTTING VELOCITY TO WORK


Using a powerful set of directives and a wide variety of objects and
variables, Velocity gives a broad palette to use for creating applications.
Simple applications could revolve around showing content based on a
request variable:
Show a standard display, with a link or input that sets a variable value.
At the beginning of the template, check for the variable.
If the value is set to a particular value, show a different display.
While this pattern is simple, it can be iterated over to produce a rich
application with complex behavior.

366
JOB LISTING APPLICATION
Our Space Program needs an easy way to show open jobs in the program.
We want our application to:
Show a list of available positions
Allow applicants to view and apply to positions
Allow administrators to add new job posts
Provide standard data fields for jobs, and format the display
Based on these requirements, we’ve decided to build our application in
Web Content with Structures and Templates.
We already have a way for administrators to create new job posts using
our friendly Structure. We can also display individual job posts using our
formatted Template.
Next, we need to display a list of job posts.

APPLICATION BUSINESS LOGIC


We’ll use a Velocity Template to host all of the logic for our application.
In addition to simple control statements and request variable checking,
we will need to use some additional objects:
JournalArticleLocalServiceUtil
SearchEngineUtil
Since JournalArticleLocalServiceUtil and SearchEngineUtil
need to be loaded into our Template, and are not available by default,
we will implement this feature later.
To simplify development, we’ll focus on the core logic, and manually
enter the article information in Velocity.
To start meeting our application criteria, we will build a template that
displays a list of job posts in summary form with links to each full job
post.

367
EXERCISE: JOB LISTING TEMPLATE (I)
1. Go to Site Administration → Content → Web Content → Manage →
Structures.
2. Click on Add.
3. Name the Structure Job Listing Structure.
4. Under XML Schema Defintion, drag a Text element into the pane.
5. Click on the wrench icon for the element and change the Name to
itemDelta and change the Field Label to Item Delta.
! Then click Save.
We always need to start with a Structure to back our Template.
In this case, we provide a very simple Structure with only one field:
itemDelta.
We will use this field to configure how many job posts to display at once.

EXERCISE: JOB LISTING TEMPLATE (II)


1. Go to Site Administration → Content → Web Content → Manage →
Templates.
2. Click on Add.
3. Name the Template Job Listing Template.
4. Under Structure, click Select and choose the Job Listing Structure we just
created.
5. Under Language, choose Velocity.
6. Under Script, copy the contents of the file
01-job-listing-template-1.vm, paste it into the editor, then click
Save.

368
EXERCISE: JOB LISTING TEMPLATE (III)
1. Navigate to the Careers page you created before.
2. Use the Web Content Display portlet on the page to create a new web
content article, using the Job Listing Structure we just created.
3. Enter Jobs at The Space Program for the title.
! Enter a value of 3 for itemDelta (an arbitrary choice – we’ll use
itemDelta later to specify how many job listings to display), and click
Publish.

CHECKPOINT: JOB LISTING TEMPLATE


We should now have a basic Template in place showing a Job Post listing:
Since we haven’t used any services, we just manually inserted a row in
the results table.
In order for the link to work, we need to insert the correct article ID into
the template.
Keep in mind that this will be hard-wired for now, but we will fix this
later.

369
EXERCISE: JOB LISTING TEMPLATE (IV)
We’ll insert an article ID into our template, representing the sample Job
Post we made.

1. Open the Configuration dialog box of the Web Content Display portlet.
2. In the list of displayed web content, copy the ID of the article that
contains our sample Job Post:
3. Close the Configuration dialog box and click the Edit Template icon.

EXERCISE: JOB LISTING TEMPLATE (V)


1. Underneath the Script heading, find the variable that contains the article
ID.
2. Paste the article ID in place of the current value and click Save at the
bottom of the page.

370
CHECKPOINT: TEMPLATE NAVIGATION
You should now be able to click on the listed Job Post, and it will
correctly display your sample Job Post:

IMPROVING NAVIGATION
One problem you will quickly find: we can’t navigate back to the Job
Listing!
This is easily solved by adding a Back link in our Job Post Template.
In order to navigate back to the Job Listing, we need the article ID of the
Jobs at The Space Program web content article.
In the Job Listing template, we’ve already taken care of providing this to
the job post.
This places the current article ID in the request as the parameter
listingId, which we need to retrieve in the Job Post.

371
EXERCISE: UPDATING THE JOB POST TEMPLATE
1. Now we’ll update the Job Post Template so that it includes a Back link.
Go to the Site Administration → Content → Web Content → Manage →
Templates and click on the Job Post Template.
2. Under Script, replace the existing script with the contents of the file
src/06-advanced-developer/06-rad-with-cms/
02-velocity-templates/02-job-post-template-2.vm.
3. Look at how the parameter is used to link to the Job Listing, then click
Save.

CHECKPOINT: JOB POST NAVIGATION


After clicking Save, you should be able to navigate to your Job Listing,
click on the Job Post, and navigate back:

372
Notes:

373
7.4 Using the Service Locator
USING THE SERVICE LOCATOR

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

GOALS
To understand the Service Locator
To learn how to make the Service Locator available
To see practical Service Locator uses
To understand security concerns and best practices
The exercise files for this slide deck are in the
06-rad-with-cms/03-service-locator folder.

375
TEMPLATES REVIEW
Liferay CMS provides powerful Templates that enable you to develop
complex articles and applications.
By default, Liferay exposes some commonly-used objects and utilities to
the templates:
Request
DateUtil
ThemeDisplay
JournalContentUtil
These should be enough for many applications, but the advanced
developer might like to use Liferay’s service layer.
With access to the service layer, a rapid application developer could get
a list of articles or users, and display the results in the template.

SERVICE LOCATOR
The simple answer to this dilemma is the Service Locator.
Service Locator is an object (serviceLocator) that provides methods
(findService) to return a reference to that service.
For instance, if we wanted to use the User service and find a list of
Users, we would retrieve the User service:
#set ($userService =
$serviceLocator.findService("com.liferay.portal.service.UserLocalService"))

Then we could use any of the methods as usual:


$userService.getUsers(-1, -1)

Though our examples are in Velocity, you could use FreeMarker or XSLT
to accomplish the same task.

376
WHAT DOES SERVICE LOCATOR DO?
Though it seems rather simple, the Service Locator performs some
important functions:
Loads the stated object
Wraps the object in a container that handles exceptions
Sends the container to the template
Velocity, and other templating languages, cannot handle exceptions that
occur.
Exceptions can frequently occur in service calls.
Without the Service Locator, exceptions would print out in the Web
Content.
For a customer-facing site, this can be ugly and embarrassing.

RESTRICTED VARIABLES
By default, serviceLocator is listed as a restricted variable in
portal.properties:
# Input a comma delimited list of variables which are restricted from the
# context in Velocity based Journal templates.

velocity.engine.restricted.classes=
velocity.engine.restricted.variables=serviceLocator
velocity.engine.restricted.packages=

In order to enable Template developers to use serviceLocator, it


must be removed from this list.
Though this example says velocity, it can be applied to FreeMarker by
substituting freemarker for velocity.
Conversely, some of the other utility objects that are available in
Templates can be removed or restricted by adding their names:
velocity.engine.restricted.variables=

377
USING SERVICE LOCATOR
In our Jobs Listing application, we would like to get a list of Job Posts to
display in our table.
Each Job Post is a Web Content piece (JournalArticle) that was
attached to a specific Structure.
Using serviceLocator, we can access the Journal Article service to
retrieve lists of Articles.
We can iterate over the list to display data from the object in the table,
and build links to view the posts.
Using the Service Locator, we can build dynamic, powerful applications
based on a simple structure.

HOUSEKEEPING: ENABLING THE SERVICE LOCATOR


We must first enable the Service Locator to be used from our templates:

1. In your Liferay Home directory, locate your portal-ext.properties


file.
2. In portal-ext.properties, paste the contents of
01-portal-ext.properties:
! Restart your application server.
#
# Input a comma delimited list of variables which are restricted from the
# context in Velocity based Journal templates.
#
velocity.engine.restricted.classes=
velocity.engine.restricted.variables=

378
EXERCISE: RETRIEVING THE STRUCTURE KEY (I)
In order to dynamically reference content based on a structure, we need
to use the structure’s structureKey.
Although a stucture’s structureId can be viewed through the UI, the
structureKey cannot.
You need to find the correct entry in the DDMStructure table of Liferay’s
database to get it.

1. First you need the Job Post structure’s ID: Go to Site Administration →
Content → Web Content → Manage → Structures and copy it down.

EXERCISE: RETRIEVING THE STRUCTURE KEY (II)


1. Open a command prompt or terminal.
2. Run the command: mysql -u root -p
3. Enter your password (it should be root).
4. Open the mysql-templatekey-snippet.txt and replace the
job-posting-structure-id text with the Job Post structure ID that you
copied down.
5. Now copy and paste the contents of
mysql-templatekey-snippet.txt into the MySQL prompt. You
should get a result that looks like this:
| structureKey |
+--------------+
| 10542 |

! Copy this structureKey.

379
EXERCISE: RETRIEVING JOB POSTS (I)
To make use of the Service Locator, we need to update our Job Listing
template:

1. Go to Admin → Site Administration → Content → Web Content →


Manage → Templates.
2. Click on the Job Listing Template.
3. Under the Script section replace the existing script with the contents of
the file 02-job-listing-template-2.vm.

EXERCISE: RETRIEVING JOB POSTS (II)


1. Find the following line in the script you inserted: #set ($structureId
= "JOB-POST-STRUCTURE")
2. Replace the value of the StructureId with the value of the
structureKey that you wrested from the database earlier.
3. Click Save.

380
CHECKPOINT:
Your Job Listing should now dynamically display any posts you have
made:

HOW DOES IT WORK?


Our new, service-based dynamic script for listing jobs centers on:
Loading services
Gathering required information
Finding all articles with the Job Post Structure
Iterating through the Jobs, and listing some basic information from them
in a list
Providing a link to view the relevant information
A basic pattern we can take from this template is:
Declare dependencies
Gather information (from the request or a service)
Iterate over information and format

381
LOADING AND WORKING WITH SERVICES
Using our new friend, ServiceLocator, we are able to obtain a
reference to our much-needed service:
#set ($journalArticleService =
$serviceLocator.findService
('com.liferay.portlet.journal.service.JournalArticleLocalService'))

Now that we have the reference to our service, we can retrieve all the
articles with a certain Structure Key (in this case, our Jobs Post
Structure):
#set ($articles =
$journalArticleService.getStructureArticles($groupId,$structureId))

WEB CONTENT VERSIONS


Liferay stores all of its Web Content as versioned XML files.
While the benefits of versioning are obvious, they can be a headache for
developers if they are unaware.
In order to ensure the list of Web Content pieces (JournalArticle) is
correct, and only includes the latest version, we need to perform a check
when iterating through the article list:
#if ($journalArticleService.isLatestVersion($groupId,
$article.articleId, $article.version, $article.status))

By encapsulating our results table in this check, we can be sure that the
returned articles are the current version, with no duplicates.
If you want the entire list of all Web Content versions, simply leave this
check out.

382
PARSING WEB CONTENT
Since we are dealing with JournalArticle, we need to know a bit
about how Liferay stores Web Content.
All Web Content is stored as XML, the structure of which is determined
by the Structure the Web Content is based on.
In order to pull any meaningful information out of our Job Post, which is
Web Content, we will need to parse the XML contained inside the
JournalArticle.
Fortunately, Liferay provides a utility we can use right from a Velocity
Template: SaxReaderUtil.

SaxReaderUtil
SaxReaderUtil is an object that makes dealing with XML easy.
In our case, we retrieve the XML content from the Web Content:
#set ($document = $saxReaderUtil.read($article.content))

Then, using the root node as a starting point, we can select information
quickly using an XPath:
#set ($root = $document.getRootElement())
#set ($jobTitle = $root.selectSingleNode(
"dynamic-element[@name='JobTitle']/dynamic-content"))

383
NOTES ON PARSING XML
Use SaxReaderUtil when you need to retrieve information from an
XML file, or when you need to create XML.
Since Web Content Templates always have a reference as
$saxReaderUtil, there is no need to use ServiceLocator to
reference it.
Use selectSingleNode or selectNodes to retrieve one or many
instances.
Use XPath to accurately describe where the information that you need
is located, using your Structure as a guide.

WEB CONTENT TEMPLATES SECURITY


Using Web Content Templates provides you with great flexibility, and the
ability to use a scripting language of your choice.
With this flexibility and power, comes the added complexity of security.
Anyone who writes Web Content Templates is able to, with a little Java
magic, retrieve services without using ServiceLocator:
#set ($journalArticleService = $portal.getClass().forName
('com.liferay.portlet.journal.service.JournalArticleLocalServiceUtil'))

Since the Web Content portlets reside in the Portal Class Loader, any
service or object can be retrieved or instantiated.
While this flexibility is great for the developer, it can cause some security
concerns.

384
BEST PRACTICES FOR TEMPLATES (I)
Taking any security concerns into account, Web Content Templates can
be a safe and effective development strategy.
Always treat Templates and Structures as Development, and not as Web
Content creation.
Grant only privileged users (such as Portal Developers) the ability to
create, edit, and update Templates.
Automatically restrict permissions on standard Portal users, so they are
unaware of Structures and Templates.

BEST PRACTICES FOR TEMPLATES (II)


Disable or inhibit the power of Users to maintain their own pages in the
portal.
Use ServiceLocator to encapsulate your objects for
Exception-catching.
By taking a few, simple steps, most security concerns are easily taken
care of.
Best of all, enjoy the flexibility and power of using Templates for rapid
development!

385
Notes:

386
7.5 Expando Data Modeling
EXPANDO DATA MODELING

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

GOALS
To review Expandos
To understand using Expandos for entities
To implement Expando entities in our Jobs Portlet
The exercise files for this slide deck are in the
06-rad-with-cms/04-expando-modeling folder.

388
WHAT ARE EXPANDOS?
Expandos are Liferay’s way of extending objects.
With Expandos, we can add custom data fields to existing models, such
as User, Site, and Layout.
Expandos can be manipulated through the Control Panel UI, or
programmatically.
Just like adding Expandos to other Portal objects, we can add Expandos
to Web Content articles.
We have two uses for Expandos in Web Content:
Standalone Entities
Useful add-on values that need to be modified easily

EXPANDOS IN WEB CONTENT: STANDALONE MODELS


Using an Expando as a standalone entity means:
Creating a new Expando table that is attached to no existing model
Creating the Expando table if it doesn’t exist
Adding Expando values when needed
Modifying Expando values when needed
Deleting Expando values when needed
Handling validation and input methods

389
USING EXPANDO SERVICES
Using an Expando in a Template is simple, since the required services
have already been included.
Expandos can be represented in the following structure:

EXPANDO STRUCTURE
Expandos consist of Expando Tables, which represent a database table
attached to a particular model (or an imaginary model).
Expando Tables contain a set of Expando Columns, which define the
fields we are adding to our Table (and, by extension, to the model).
After we have Columns, we can then add Expando Values to the Table.
A Row in Expando would consist of an Expando Value for each Column.

390
CREATING EXPANDOS
Simple methods are provided for each stage of the process:
Expando Tables: creating and retrieving tables:
$expandoTableLocalService.addTable()
and
$expandoTableLocalService.getTable()

Expando Columns: creating and retrieving columns:


$expandoColumnLocalService.addColumns()
and
$expandoColumnLocalService.getColumn()

Expando Values: creating values:


$expandoValueLocalService.addValue()

Expando Rows: retrieving rows:


$expandoRowLocalService.getRows()

USING EXPANDOS IN THE JOB POSTS


We would like a simple way to apply for a Job Post without having to use
too many services.
An easy way to store applications is to store their values in Expandos,
attached to the piece of Web Content that represents a Job Post.
By using Expandos, we can easily attach a new model that references
JournalArticle.
This will enable us to track information about applicants.

391
CRUD METHODS
Instead of using Service Builder to create our models, we will use
Expandos.
When using Expandos, we will perform some basic setup and CRUD
operations that can be tedious and mundane.
To help separate some of this code out, and make our Job Post template
more manageable, we’ll place some of this common code in a different
Template.
Velocity allows us to include external templates within our template,
using a variable Liferay provides us:
#parse("$journalTemplatesPath/TEMPLATE-ID")

EXERCISE: CREATING JOB APPLICATIONS (I)


1. Go to the Site Administration → Web Content → Manage → Templates.
2. Click on Add, enter the name Expando CRUD Include, and enter a
description.
3. Select Velocity as the Language.
4. Under the Script section, copy the contents of the file
01-expando-crud.vm into the editor window, then click Save.
5. Record the template’s Template Key. We’ll use it on the next slide.

NOTE: The Template Key is not the Template ID. The Template Key is a
separate entry which can be found when viewing the template.
The Expando CRUD Include template contains many of the common
operations we will use in the Job Application.

392
EXERCISE: CREATING JOB APPLICATIONS (II)
1. Click on the Job Post Template and under the Script section, and replace
the contents of the editor window with the contents of the file
02-job-post-template-3.vm.
2. Replace the value EXPANDO-CRUD-INCLUDE with the Job Listing
Template Key that you recorded. Click Save.

This Template includes the Expando Model Template we just created,


using a useful Velocity directive:
## Include Expando utility

#parse("$journalTemplatesPath/EXPANDO-CRUD-INCLUDE")

The reserved variable $journalTemplatesPath references Liferay’s


Template directory, so you don’t need to figure it out.
Later, we’ll take a closer look at how these Templates work together.

CHECKPOINT: APPLYING FOR A JOB


To check that the application works, go to a Job Post, click Apply, and
add a new Application to the Posting:

393
CHECKPOINT: VIEWING A JOB APPLICATION
Click the View Application link next to the newly added application to
view it.

HOW IT WORKS: EXPANDO INITIALIZATION


The Expando Include Template provides some basic setup including:
initializing Expando Tables and initializing Expando Columns.
An Expando Table for our custom model is retrieved; if it doesn’t exist, it
is created:
#set ($expandoModelTable =
$expandoTableLocalService.addTable($companyId,
$className, $expandoModelTableName))

className refers to the fully qualified name of the class we are


extending; with a custom model, the class we are extending is our own.
Likewise, if our Expando Table has not been created before, then the
fields for our model (stored in Expando Columns) are created:
#set($VOID =
$expandoColumnLocalService.addColumn($expandoModelTableId, $column, 15))

394
HOW IT WORKS: EXPANDO INITIALIZATION
The parameter represented by the variable $column represents the
name of the column (field) as a String.
The last parameter is a numerical representation of a column type:
Type Constant (Single) Constant (Array)
boolean 1 2
Date 3 4
double 5 6
float 7 8
integer 9 10
long 11 12
short 13 14
String 15 16

HOW IT WORKS: ADD EXPANDO MODEL


Adding a new entry in our Expando Table (for Job Applications) is simple:
retrieve form data, and add the set of values.
Retrieving form data is made easier by using $getterUtil to parse the
parameters by type:
#set ($name = $getterUtil.getString($request.parameters.name, ""))

Similarly, we use $dateTool to retrieve the current timestamp to use as


our primary key:
#set ($date = $dateTool.getDate())
#set ($classPk = $date.getTime())

Adding a new Row to our Expando Table is a simple service call:


$expandoValueLocalService.addValues($expandoModelClassNameId,
$expandoModelTableId,
$columns, $classPk, {'name': $name,
'email': $email,
'comment': $comment})

395
ASSOCIATING APPLICATIONS WITH JOBS
In a normal ServiceBuilder-created portlet, we would provide a foreign
key in our model to refer to the Job Post we want to apply for.
We can have a field for Job Post ID in our Expando Model that acts as a
primary key, but it will be difficult to retrieve multiple applications based
on the Job Post ID.
If we use the Job Post Article ID as our primary key instead, then we can
only have one application per job – that won’t work!
If we use the User ID as our primary key, then only one application per
user can be created, which might be OK, but we still have a difficult time
retrieving multiple applications for each Job Post.
An easy solution to this problem is to make the primary key random (in
this case, a timestamp), and use the Job Post Article ID as the Class the
Expando Table extends:
#set ($className = "JournalArticle-${currentJob}")

HOW IT WORKS: RETRIEVING EXPANDO MODELS


Retrieving a list of Expando Rows to display for a Jobs Post is a simple
service call:
#set ($jobApplications =
$expandoRowLocalService.getRows($expandoModelTableId, -1, -1))

Since we are storing all applications for each post as a separate Expando
Table, we simply grab all of the applications for this Job Post.
The last two parameters are start and end, denoting how many of the
result models to retrieve.
Using -1 for either or both values is a special case, meaning all of the
models.

396
HOW IT WORKS: RETRIEVING VALUES
As we iterate over each Expando Row, we need to retrieve and display
data from the row.
Retrieving all of the values from a row is simple:
Since we are looking only for name, we compare the values to the list of
columns we have, and pull out the matching value:
#set ($values =
$expandoValueLocalService.getRowValues($application.getRowId()))
## Extract the name of the applicant
#foreach ($value in $values)
#foreach ($column in $columns)
#if ($column.name =="name")

#if($column.columnId == $value.columnId)
#set ($name = $value.string)

#end
...

HOW IT WORKS: PROGRAM FLOW AND LOGIC


Our Jobs Application can have three views: view, add, and list.
We track what state we are in with a request parameter:
## Check the request for an action
#set ($applyAction = $request.parameters.myAction)

Based on the value of this parameter, we can modify the view:


#if ($applyAction == "apply")
...
#end

We can show an application form in this statement, and if the form is


submitted:
#if ($request.parameters.submit)
... add application ...
#end

We can perform our service call to add the new values.

397
SANITIZING OUR FORM DATA
At this point, we have a very straightforward data entry form.
One security hole is that we don’t sanitize or vet the data we collect in
the form, leaving it open to simple XSS attacks.
One simple way to sanitize the data is by using the HtmlUtil object
from Liferay’s API.
In a Velocity template, this is made available to us as $htmlUtil.
With a simple method, escape(), we can sanitize any String passed
through the POST data.

EXERCISE: SANITIZING APPLICATION DATA


1. You’ll need the Template Key of the Expando CRUD Include Template:
navigate to Site Administration → Web Content → Manage →
Templates, and click on the Expando CRUD Include Template.

NOTE: The Template ID is not the same as the Template Key, so you must
click on the template (or click Actions → Edit) to find the Template Key.

2. Click on the Job Post Template and under the Script section, copy the
contents of the file 03-job-post-template-4.vm into the editor
window, replace EXPANDO-CRUD-INCLUDE with the Key of the Expando
CRUD Include template, then click Save.

This Template contains new code that sanitizes input from the form.

398
HOW IT WORKS: DATA SANITIZATION
Before storing the retrieved parameters, we pass all data through
Liferay’s utility object:
#set ($name = $htmlUtil.escape($name))
#set ($email = $htmlUtil.escape($email))
#set ($comment = $htmlUtil.escape($comment))

$htmlUtil returns a String with all code elements escaped, so they


display properly as text, and cannot be executed as code.
While not perfect, this is an easy way to start protecting against the
most common types of attacks.

SUMMARY
Using Expandos, we can create new models that:
Exist on their own, or extend an existing model
Can be created, added, upated, and deleted from Web Content Templates
Can be dynamically associated and retrieved with Web Content pieces
Additionally, using additional render parameters in the template allows
us to simulate portlet phases such as Action and View.
Remember that when using Expando-based models, you may need to:
Sanitize input, such as with $htmlUtil
Validate input
Handle localized input
Provide ways to create, update, and delete Expando data

399
Notes:

400
7.6 Using Custom Variables in Velocity
USING CUSTOM VARIABLES IN VELOCITY

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

GOALS
To implement custom objects
To create a custom Velocity plugin
To use custom objects in Velocity
The Liferay Developer Studio snippets for this presentation are in the
category 06-RAD with CMS.
The exercise files for this slide deck are in the
06-rad-with-cms/05-custom-velocity-variables folder.

402
VELOCITY VARIABLES OVERVIEW
Liferay provides access to many convenient variables in Velocity
templates.
Liferay manages the Velocity context, making it possible to inject
variables into Velocity templates for use in Web Content and Themes.
Just like $dateUtil and $getterUtil, any object can be exposed to Velocity
templates.
Objects made available to Velocity in templates are referred to as Tools.
New objects that have been written and added are referred to as Custom
Tools.
Liferay’s custom tools are automatically made available to Velocity
templates.
With very little work, we can make our own custom tools available to
Velocity templates.

VELOCITY CONTEXT
To make an object available to Velocity templates, Liferay needs to know
about it in its Velocity Context.
Leveraging Spring provides a convenient way to provide custom objects
to the context.
All tools follow a Dependency Injection pattern that requires we define a
bean to represent the Tools API (Interface), and we then provide an
implementation bean (Impl-class).
Once the beans are defined, a Context Class Loader in Liferay can pick up
the configuration and make it available to Velocity.

403
REFINING THE JOB POST APPLICATION
Our Job Listing application implements a custom model to store Job
Applications for Job Posts.
While it is very easy to implement all of the Expando code in Velocity
Templates, we want to clean up the code and separate that functionality.
We’ll create a custom Velocity Tool that performs the common logic for
Job Applications.
This will clean up the Template code, as well as hide some of the
complexity from developers that use the Tool.

EXERCISE: JOB APPLICATION TOOL (I)


1. Create a new Liferay Plugin Project in Liferay Developer Studio.
2. Type job-application-tool for the project name.
3. Select the Liferay Plugins SDK and Liferay Portal Runtime you have
configured.
4. Choose Hook for the plugin type.
5. Click Finish.

We will use a Hook to inject a Tool using a simple implementation and


bean definition.

404
EXERCISE: JOB APPLICATION TOOL (II)
1. Create a new Java Interface: right-click on your job-application-tool-hook
project and select New → Interface
2. Set the package name as com.liferay.training.hook.jobapplication.
3. Use the interface name JobApplicationTool.
4. Click Finish.

EXERCISE: JOB APPLICATION TOOL (III)


1. Open the new interface source and replace its contents with the snippet
01 JobApplicationTool.
This represents the API of the Tool we will implement.
2. Create a new Java Class in the same package as the interface.
3. Use the class name JobApplicationToolImpl.
4. Open the new source file, and replace its contents with the snippet 02
JobApplicationToolImpl.

This is the complete implementation of our Expando utility methods.


All that is left to do is declare the beans for Spring to inject, and make
Liferay aware of the beans.

405
EXERCISE: JOB APPLICATION TOOL (IV)
Liferay provides a quick way to define new beans to be instantiated
through Spring, via a plugin’s application context.
By default, Liferay automatically inserts a context listener for Portlet
Plugins, but not Hook Plugins.
To make Liferay aware of the new application context, we need to use a
context listener to locate it.

1. Create a new file named applicationContext.xml in the project’s


/docroot/WEB-INF folder.
2. Insert the contents of the snippet 03 applicationContext.xml into
the file.

EXERCISE: JOB APPLICATION TOOL (V)


1. Open the web.xml file in the project’s /docroot/WEB-INF folder.
2. Replace the contents of this file with the contents of the snippet 04
Portlet Context Listener web.xml.
! Save any unsaved files, and deploy the hook by dragging the plugin onto
the Tomcat server in Developer Studio.

406
CHECKPOINT: JOB APPLICATION TOOL
Once the hook has deployed, Liferay will use the Portlet Context Listener
to pick up the new beans declared in applicationContext.xml.
<bean
id="com.liferay.training.hook.jobapplication.JobApplicationTool"
class="com.liferay.training.hook.jobapplication.JobApplicationToolImpl"
/>

The new Job Application Tool is now available in Liferay to any Velocity
Template.
All we need to do is modify our Job Post template to use this new tool!

EXERCISE: USING THE JOB APPLICATION TOOL


1. Start the Tomcat server if it isn’t already running, and log in with your
administrator account.
2. Go to Admin → Site Administration → Web Content → Manage →
Templates.
3. Find the Job Post Template and click on it.
4. Under the Script heading, replace the contents of the editor with the
contents of the file 05-job-post-template-5.vm.
! Click Save.

407
HOW IT WORKS
Once we have implemented the desired logic and provided it to the
portal, the first thing we have to do is load the new tool:
#set ($applicationTool =
$utilLocator.findUtil("job-application-tool-hook",
"com.liferay.training.hook.jobapplication.JobApplicationTool"))
#set ($jobApplications =
$applicationTool.getApplications())

Providing the application context (the name of the plugin) and the name
of the tool (the interface we declared) is enough to provide the object to
our template.
Once we have the tool loaded, we simply use it as the tool’s API
describes:
With custom Velocity Tools, common pieces of logic or complex model
methods can be encapsulated in an easy-to-use object.

Notes:

408
7.7 Integrating AlloyUI
INTEGRATING ALLOYUI

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

GOALS
To understand UI and presentation needs
To leverage AlloyUI to streamline the UI in our Jobs Portlet
To understand the limitations of Velocity versus FreeMarker
The exercise files for this slide deck are in the
06-rad-with-cms/06-integrating-alloy folder.

410
USING ALLOYUI IN TEMPLATES
AlloyUI is not just for plugins. It can be used in Templates to unify the
user experience.
AlloyUI provides the developer with:
HTML markup patterns
Taglibs
JavaScript libraries
CSS styles
In developing rapid applications, Taglibs are the only part of Alloy that
cannot be accessed from Velocity Templates.
Instead of using Taglibs, the developer will need to reference HTML
markup patterns to implement the same features.

LOCATING MARKUP PATTERNS: WEBSITE


There are two fundamental locations for Alloy markup patterns:
Alloy Examples Site
Alloy Bundle Examples
A number of components are made available as examples on the
website:
http://alloyui.com/examples/
The HTML source of these pages can be referred to for the correct HTML
element and CSS class combinations.
Components and patterns not found on the site may be found in the
source bundle for Alloy.

411
LOCATING MARKUP PATTERNS: ALLOY SOURCE
Alloy’s source may be obtained as a release package from Alloy’s
website:
http://alloyui.com/
Alternatively, the latest code may be downloaded from the public
repository:
https://github.com/liferay/alloy-ui/
These more complete examples are located in SOURCEFOLDER/demos.
In addition, Alloy is included as a third-party package in Liferay, which
can be found in Liferay’s source:
LIFERAYSOURCE/portal-web/third-party/alloy-VERSION.zip

EXERCISE: TWEAKING JOB APPLICATIONS


We’ll make some modifications to the form for adding new applications:

1. Go to Site Administratrion → Web Content → Manage → Templates.


2. Click on the Job Post Template.
3. Scroll down to the Script heading, and replace the contents of the editor
with the contents of the file 01-job-post-template-6.vm.
4. Click Save.
! Navigate back to the Job Posts page, click on a Post and click on the
Apply button.

412
CHECKPOINT: NEW APPLICATION FORM LAYOUT
You should now see a new form layout with inline labels:

Next, let’s look at the markup patterns we used and learn how to use
Alloy patterns in other components.

USING MARKUP PATTERNS: AUI LAYOUT


You may recall using the taglib <aui:layout> and <aui:column> to build a
predictable layout.
Without the convenience of taglibs, we simply need to follow a
predictable pattern:

Most components can be distilled into this basic container-content


pattern.

413
ALLOYUI LAYOUT
So how does this look in code?
<div class="aui-layout aui-w100">
...

<div class="aui-layout aui-column aui-column-first aui-w90">


...
<form class="aui-layout-content aui-column-content
aui-column-content-first ..." ... >
</div>
</div>

Notice the outer div tags contain the parent classes aui-layout and
aui-column, while the inner or content sections contain
aui-layout-content and aui-column-content.

ALLOYUI FORM
The same concepts carry over into the form itself, where we set it up for
inline labels:
<form class=" ... aui-form aui-field-labels-inline"... >
<fieldset class="aui-fieldset aui-fieldset-content">
<label ... class="aui-field-label">
...
<input class="aui-field-element" ... />
...
</fieldset>
</form>

Here, we see the container-content concept used again. The form is the
container and the fieldset is content for the form.
Using this basic knowledge, you can pick out the markup patterns in
examples to use in your templates.

414
EXERCISE: JOB LISTING BUTTON
To show we can use Alloy’s JavaScript components in much the same
way as in a portlet, we’ll use a Button in our Job Listing:

1. Go to Site Administration → Web Content → Manage → Templates and


open the Job Listing Template.
2. In the Script section, find the Job Post Structure ID, and copy it.
3. Next, replace the editor’s contents with the contents of the file
02-job-listing-template-3.vm.
4. Now paste the Structure ID you copied earlier into the correct place.
! Click Save, and navigate back to the Careers page.

CHECKPOINT: JOB LISTING BUTTONS


You should now see the same list, but with attractive buttons instead of
plain links:

Once again, use JavaScript components sparingly, since they have to


dynamically load after the page has loaded.

415
JAVASCRIPT IN TEMPLATES
If you look at the new template source, the JavaScript looks almost
identical to what we would write in a JSP:
<script>
AUI().use("aui-button",
function(A) {
/* Instantiate a new Button
*/
var buttonRow =
A.one(".${namespace}button-row-${currentJob}");
var button = new A.Button({
icon: 'icon-search',
label: "View",
on: { click: function(event) {
location.href = "${viewURL}";}
}

})
.render(buttonRow);
});
</script>

Notes:

416
Chapter 8

Advanced Topics and Summary

8.1 Summary

417
SUMMARY

Copyright ©2015 Liferay, Inc.


All Rights Reserved.
No material may be reproduced electronically or in print,
distributed, copied, sold, resold, or otherwise exploited
for any commercial purpose without express written
consent of Liferay, Inc.

YOU ARE NOW AN ADVANCED LIFERAY DEVELOPER!


Over the course of the past few days, you’ve been exposed to many
powerful features of Liferay’s development platform.
The information in this course equips you to build your site on Liferay’s
platform, taking advantage of the features and conveniences the
platform has to offer.
You’ll soon receive badges on your liferay.com profile pages recognizing
the training you’ve completed. Here are a couple of example badges:

418
WHAT’S NEXT?
Liferay offers both public and private trainings on a variety of courses.
To see all of Liferay’s training offerings, including course descriptions,
please visit http://www.liferay.com/services/training/topics.
Further information is also provided in Liferay in Action, Liferay’s official
guide to development, which is published by Manning Publications. You
can find more information about the book here:
http://manning.com/sezov.
Please keep in touch on Liferay’s forums and if you can, help us out by
contributing plugins, core code, or wiki articles. We love collaborating
with our community!

LIFERAY CERTIFICATION PROGRAM


Liferay also offers a certification program that allows members of the
Liferay community to demonstrate their expertise and to differentiate
themselves as certified Liferay professionals.
The Liferay Certification Program also gives companies adopting Liferay
another way to assess a candidate’s Liferay knowledge.
For more information about the Liferay Certification program, please visit
http://www.liferay.com/services/certification.

419
GET INVOLVED
Liferay relies on its community for continued innovation through
contributions, adding to its global knowledge base, and increasing
awareness through collaboration and social networking.
By participating, you can continue your Liferay experience by working on
leading edge technology, working with and connecting to other
professionals, and influencing product direction.
By contributing, you can give back a little of what you have learned here
today. Contribution and the teaching of others benefits the entire
community, as well as your own career development.
Forums, Blogs, Social Media, IRC, Liferay LIVE, whitepapers, discussions,
reviews, and documentation are just some of the possibilities.
Visit http://liferay.org/ to learn more about our community!

We have very much enjoyed working with you, and wish you much
success in your Liferay projects!

We’ll distribute a form for you to provide feedback on the training.


Please provide feedback!

420

You might also like