Professional Documents
Culture Documents
for the
LIFERAY PLATFORM II
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
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
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
Setup
5
COURSE TOPICS
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
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
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!
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
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.
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.
16
MYSQL INSTALL (VII) - COMPLETE
1. Click Finish.
! MySQL is now successfully
installed.
CHECKPOINT! (IV)
1. Open a command prompt and execute
mysql --version
17
TROUBLESHOOT MYSQL
If MySQL is not found, add it to your system path:
18
Notes:
19
1.3 Liferay Training Setup
LIFERAY TRAINING SETUP
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
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
38
CREATE SITE PAGES (II)
1. Click My Sites in the Dockbar, then click on the Moon Colony site
! A new, blank page appears.
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.
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.
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.
42
Notes:
43
Chapter 2
Introduction to AlloyUI
45
INTRODUCING ALLOYUI
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.
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.
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.
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
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.
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.
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.
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:
Liferay Developer Studio and Liferay IDE contain snippets to make it easy
to insert AlloyUI components in your page.
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.
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) {
});
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.
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>
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.
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.
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");
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 %>" />
new A.Button({
icon: 'icon-gear',
label: buttonLabel,
on: { click: function(event) {
location.href = "<%=permissionsURL %>";}
}
})
.render(buttonRow);
</aui:script>
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.
64
CHECKPOINT: PARTS BUTTONS
! Re-deploy the portlet (or save in Developer Studio) to see the new
buttons:
65
Notes:
66
2.3 AlloyUI Events and Dynamic Content
ALLOYUI EVENTS
AND DYNAMIC CONTENT
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);
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.
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.
button.fire('click');
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.
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.
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();}
} ...
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();
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
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
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.
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.
84
Notes:
85
Chapter 3
87
INTRODUCTION TO LIFERAY SOCIAL
APPLICATIONS
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.
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
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
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
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.
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.
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.
102
EXERCISE: PROFILE PAGE CONFIGURATION
Note: The Requests Portlet will be invisible until a social request has
been received.
103
EXERCISE: HOME PAGE CONFIGURATION
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.
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.
106
GETTING THE GROUP
We need to get the user who owns the current page so we can compare
it with the user who is browsing the page.
<c:choose>
<c:when test="<%= themeDisplay.isSignedIn() &&
((user.getUserId() == user2.getUserId()) ||
SocialRelationLocalServiceUtil.hasRelation(user.getUserId(),
user2.getUserId(),
SocialRelationConstants.TYPE_BI_FRIEND)) %>">
</when>
</choose>
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
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.
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.
socialActivityLocalService.addActivity(userId,
part.getGroupId(), Part.class.getName(), part.getPartId(),
PartActivityKeys.ADD_PART, StringPool.BLANK, 0);
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.
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);
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.
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.
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
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.
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.
122
BONUS EXERCISE
BONUS: Add a social activity interpreter to the Manufacturer portlet,
following all of the steps in this presentation.
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
125
INTRODUCTION TO COLLABORATIVE
APPLICATIONS IN LIFERAY
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.
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.
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
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
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">
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.
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.
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.
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.
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.
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) {
@Override
public boolean hasViewPermission(PermissionChecker permissionChecker) {
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.
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.
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.
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.
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.
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.
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
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.
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.
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?
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.
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.
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
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.
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.
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
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
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.
161
CATEGORIES IN THE CONTROL PANEL
162
RELATED ASSETS IN CONTENT 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.
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
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.
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.
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.
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
173
REMOTE SERVICES
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.
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
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"
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.
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.
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.
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 {
return manufacturerLocalService.addManufacturer(manufacturer,
serviceContext);
}
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.
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!
186
EXERCISE: CREATE INVENTORY CLIENT (II)
Visit http://localhost:8080/InventoryClientSample/
sampleManufacturerServiceSoapProxy/TestClient.jsp to access the
Inventory Client you created.
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.
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?
189
IMPLEMENT PERMISSION CHECKS (IV)
Notice that we are extending BaseServiceImpl.
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
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.
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.
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
};
alert(manufacturer.name);
</aui:script>
<%
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>";
%>
<aui:script use="aui-tooltip">
var parentSelector =
'table[data-searchcontainerid="<portlet:namespace />partsSearchContainer"]';
var serviceContext = '/parts-inventory-portlet.manufacturer/';
var method = 'get-manufacturer';
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>
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
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.
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.
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.
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.
203
TRADITIONAL APPROACH: CREATE DB FACADE (III)
The SQL is executed and the model is populated with the results.
<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.
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.
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:
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.
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.
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.
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.
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;
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.
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.
214
Notes:
215
5.3 Custom SQL: Using Finders
CUSTOM SQL: USING FINDERS
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.
Now that we have created the service layer for the PurchaseOrder
object, let’s devise a way to make some objects in the database.
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.
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.
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:
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.
224
CHECKPOINT: VIEW THE OPEN ORDERS
! Now you can see the number of orders per Part:
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.
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;
}
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.
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
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.
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:
// 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);
FinderCacheUtil.putResult(FINDER_PATH_COUNT_BY_PART,
finderArgs, count); ← Put in Cache
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
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.
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 = ?)
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(…).
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.
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!
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.
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.
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
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.
246
EXAMPLE: FLIGHT CRITERIA (III)
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.
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.
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.
1. Replace the contents within the try {...} block in the findByUser
method in PurchaseOrderFinderImpl with the 02-findByUser snippet.
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.
total = PurchaseOrderLocalServiceUtil.countByUser
(userId, companyId, scopeGroupId, false);
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
255
MESSAGE BUS AND SCHEDULING
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.
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());
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.
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.
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.
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>
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>
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.
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.
...
<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>
...
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>
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!
<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.
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.
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.
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
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 {
...
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.
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
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.
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>
<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>
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
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.
286
EXERCISE: CREATE INDEXER HOOK (VIII)
How does the startString depend on user input?
String startString = normalizeDate(lastLoginDate);
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
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)
<aui:field-wrapper name="lastLoginDate">
<%= selUser.getLastLoginDate() %>
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.
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.
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.
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;
292
EXERCISE: CUSTOMIZING LANGUAGE PROPERTIES (II)
1. In your new Language.properties file, add the following line
(provided in 11-Language.properties):
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
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
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
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 %>"
300
EXERCISE: PART URLS (I)
To make Liferay aware of our new Friendly URL routes, we need to
modify our deployment descriptor:
<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>
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:
302
FRIENDLY URL ROUTES (II)
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>
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
<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
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. 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.
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">
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.
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();
315
EXERCISE: CREATE THE DATA HANDLER (II)
By default, all entities get support for exporting Data, Permissions, and
Categories.
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;
}
if (portletDataContext.getBooleanParameter(NAMESPACE, "manufacturers")) {
ActionableDynamicQuery manufacturerActionableDynamicQuery = new
ManufacturerExportActionableDynamicQuery(portletDataContext);
manufacturerActionableDynamicQuery.performActions();
}
...
return getExportDataRootElementString(rootElement);
}
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);
This method parses the LAR’s XML elements into Manufacturer entities.
ActionableDynamicQuery manufacturerActionableDynamicQuery =
new ManufacturerExportActionableDynamicQuery(portletDataContext);
manufacturerActionableDynamicQuery.performCount();
ActionableDynamicQuery partActionableDynamicQuery =
new PartExportActionableDynamicQuery(portletDataContext);
partActionableDynamicQuery.performCount();
}
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.
319
EXERCISE: CREATING THE PART
STAGED MODEL DATA HANDLER (II)
1. In the class body, insert snippet 06-Part-Staged-Model.
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);
}
...
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.
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.
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
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(...);
Notes:
328
6.7 Using the Recycle Bin
USING THE RECYCLE BIN
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.
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" />
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
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>
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.
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.
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.
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.
return themeDisplay.getPortalURL() +
"/parts-inventory-portlet/part.png";
}
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);
}
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);
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
343
RAPID DEVELOPMENT IN LIFERAY CMS
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?
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.
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?
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!
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:
349
EXERCISE: CREATE A JOB POST (III)
! Click the Publish button to save the new Web Content article:
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
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.
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.
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.
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.
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
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
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
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
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")
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
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.
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.
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.
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.
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.
372
Notes:
373
7.4 Using the Service Locator
USING THE SERVICE LOCATOR
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"))
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=
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.
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.
379
EXERCISE: RETRIEVING JOB POSTS (I)
To make use of the Service Locator, we need to update our Job Listing
template:
380
CHECKPOINT:
Your Job Listing should now dynamically display any posts you have
made:
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))
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.
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.
385
Notes:
386
7.5 Expando Data Modeling
EXPANDO DATA MODELING
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
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()
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")
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.
#parse("$journalTemplatesPath/EXPANDO-CRUD-INCLUDE")
393
CHECKPOINT: VIEWING A JOB APPLICATION
Click the View Application link next to the newly added application to
view it.
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
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}")
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
...
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.
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))
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
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.
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.
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.
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!
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
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.
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
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.
413
ALLOYUI LAYOUT
So how does this look in code?
<div class="aui-layout aui-w100">
...
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:
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
8.1 Summary
417
SUMMARY
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!
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!
420