A Content-Driven Approach To Building Powerful And Flexible Wizards

Technical Blogs January 30, 2017 By Javeed Chida

Let me begin by clarifying that this post has nothing to do with the Harry Potter universe.
 
But seriously. You know what I mean by wizards, don’t you? Those helpful series of screens that gather a set of choices from the user and then use the captured choices to do something for the user. Often times, one user selection can lead to a different outcome that the other choices on the same screen.
 
It turns out I am faced with a requirement to build such a wizard. And the requirements are a little bit amorphous. So I asked myself the same question I have come to ask before embarking on a multi-layered application development effort:
Can I accomplish this with the content management system?
 
Or more verbosely stated as: 
Are all the layers I need to accomplish this already there for me?
 
Well, the answer is yes. It can be done using the CMS. And it can be powerful and flexible. 
 
Here are my requirements for a contrived Mood Wizard:
  • Screen 1: Ask user to pick their position (you know: standing, sitting, etc). Picking an option on Screen 1 takes them to Screen 2.
  • Screen 2: Ask user how they feel (happy, sad, angry, etc.). Picking an option on Screen 2 takes them to Screen 3 unless they pick ‘angry’, in which case they are taken to Screen 4.
  • Screen 3: Ask user to choose an energy level. Picking an option on Screen 3 takes user to Screen 999 (the final screen).
  • Screen 4: User types up a rant and hits Continue to be taken to Screen 999 (the final screen).
  • Screen 999: This is the final screen. User can click the Go button to do something with their choices.
  • At any point during use of the wizard, the user can back up a screen.
  • At any point during use of the wizard, the user can start over.
  • At any point during use of the wizard, the user can see what choices they’ve made from screens that had choices (aka options) rather than custom markup.
 
Here are the moving parts of my solution:
  • A structure
  • A template
  • A Dynamic Data List definition (to house rants from angry visitors)
  • A servlet to call into the DynamicDataListRecord API (because it’s a great persistence mechanism with a clean API and is one of the goodies that Liferay comes with). 
Here is the structure I defined:
 
 
 
Some interesting things to note here:
  • The screens are repeatable.
  • The options within a screen are repeatable.
  • Every option has a Target Screen Identifier - the identifier of the screen to load when the option is selected. it also has a Target URL in case picking it needs to change the page location.
That simplicity to me is POWER.
 
You may note that I also have a Custom Markup for each screen and a Target Screen Identifier for it. This allows for the definition of custom markup and a way to configure the next screen to go to if Custom Markup is adopted for any given screen. In my contrived use case, Custom Markup is perfect to capture the user’s rant if they were to pick Angry.
 
And that gives me the FLEXIBILITY I need.
 
 
For screen 2, I capture some user information (an email address and a rant) and post it to my VisitorRantServlet that saves it to a DDL record-set. The user can setup an instance of the stock Dynamic Data List Display portlet portlet to view/edit the submitted records.
 
Here are some screenshots if the above link doesn't work. The Web Content Display portlet on top shows the content item I created using the previously define structure and template. The Dynamic Data List Display portlet below it shows any rants added.
 
 
 
 
There are lots of possibilities here. Here's a few:
  • Any screen can use custom markup to load any data from a servlet, REST service and such, in much the same way that I use it to capture a visitor’s rant.
  • The template is wide open to code in any extra customization. (You know you’ll need it.)
  • We have the robustness of content versioning.
  • I do wish we had the added robustness of template (and even structure) versioning :-(.
By the way, you don't need a servlet to call into the DDL Record Set API. JSONWS makes it a snap but there is the inherent complexity of having to deal with basic authentication. The raw API lets you run any trusted code on the server and was my preference for the purpose of this demo.
 
My thanks to Allen Ziegenfus for sharing this tidbit here that came to good use.
 

Crafting document download links in a velocity asset publisher ADT

Technical Blogs January 19, 2017 By Javeed Chida

This short post brings together the little pieces critical to crafting an ADT in velocity to provide direct download links to documents in your Documents and Media repository. You will find parts of this solution strewn across forums and blogs. 
 
In essence, you'll have to do two things:
1. Add the following property to your portal-ext.properties if you have not done so yet. If you've been using the serviceLocator to pull in various Liferay service APIs in your ADTs, then you've likely done this already.
velocity.engine.restricted.variables=
 
That just says no variables are restricted in your ADTs.
 
2. Craft the document's download URL. Your document download URL should looks like this:
/documents/<groupId>/<folderId>/<document-title>/<document-uuid>?version=<version>
For example:
/documents/10181/11405/College+Sample+Doc.docx/7a5680cf-8de5-4d65-b179-72ee5c9f5966?version=1.0
Here's the velocity markup to do the above. There are freemarker versions of this all over the place. 
 
In the above code, you may have noticed the call:
$fileEntry.getLatestFileVersion(false)
 
That boolearn parameter is named trusted, and basically, this is what the code looks like in the implementation (just in case you're curious).
 
public DLFileVersion getLatestFileVersion(boolean trusted) throws PortalException, SystemException {
  if (trusted) {
    return DLFileVersionLocalServiceUtil.getLatestFileVersion(getFileEntryId(),false);
  }
 else {
    return DLFileVersionServiceUtil.getLatestFileVersion(getFileEntryId());
  }
}
 
I think that means that if trusted is true, then the working copy (Draft) is heeded as the latest version. Otherwise it is not. (Call me out on that if I'm off the mark.)
 
Hope this saves someone out there a little time.

Stopping By Abou Shousha's On A Snowy Evening

General Blogs December 8, 2016 By Javeed Chida

(or) Why categories are so much more than nestable tags


"I LOVE tags," Jaffer managed despite a mouthful of spiced lamb ouzie.
 
Sergei was finding the younger guest a bit annoying. The kid had ambled in from the snow to get a bite. Sergei himself had come in to escape the noise that seasoned other cafeterias. He fancied Abou Shousha's Mediterranean Oasis for its laid back ambience, not to mention the unending supply of Maghrebi mint tea.
 
And now, this kid. 
 
"They're okay," he mumbled before returning to crafting an XPATH expression for his ADT.
 
Jaffer's jaw dropped open. 
 
"OKAY?!? You got to be kidding me. Tags are PHENOMENAL. I mean, what else do you need to organize and query all that content?"
 
Sergei ignored him, but it wasn't easy.
 
"Huh? Right? Am I right?" Jaffer persisted as shreds of ouzie drizzled out of his mouth onto the platter below.
 
Sergei broke away from his laptop and turned to face the young programmer. 
 
"Have you heard of categories?"
 
Jaffer nodded vigorously. "Yeah, but you don't need them. Tags and categories all get concatenated into a list of comma-separated values and assigned to the page's keywords meta tag. Okay, sure, categories can have child categories..."
 
The boy wonder picked up crumbs of fallen ouzie and tossed it into his mouth as he went on.
 
"...and maybe a hierarchy of categories is useful to some businesses, but hey, you don't need categories. They just make your site harder to maintain. Am I right?"
 
Sergei pursed his lips and raised his eyebrows just as the grocer began making his rounds dispensing complimentary tea. The veteran developer picked up a cup of the steaming liquid and nodded his thanks. The aroma rose into his nostrils and he inhaled, even as he entertained the thought of how best to get this kid out of his hair, or what was left of it. He took a long sip and set the cup aside.
 
"Take a look," he invited the younger man to look at his screen as he clicked a few times. Jaffer leaned over and stared at into the display.
 
"Here, that's a tag for you. Let's edit it."
 
Another click. 
 
 
"See, all you can do is change it. There's nothing else it gives you."
 
Jaffer threw his hands up in exaggerated frustration.
 
"That's my point, Uncle."
 
"Uncle?" Sergei wrinkled his brow.
 
Jaffer went on. "A tag is a tag and that's all it is - a tag. A simple powerful way to... to.... to TAG your content. Nothing else. That is its power."
 
Sergei nodded, "That is true. I'm not going to argue it. Let's take a look at a category now, shall we?"
 
A couple more clicks. 
 
 
"Here's a category. Look! An Edit button AND a Permissions button as well." 
 
Jaffer bounced back from the display and shook his head dismissively. "Why? Why would a category need permissions?"
 
Sergei leaned back, looking down at his keyboard as he spoke. 
 
"Let's say you create a bunch of categories and sub-categories in your vocabulary, and then you realize your administrative team, you know content contributors and such, they need to look at the content through a different lens, let's say department-wise, or something along those lines. So now you create a bunch of internal, as in private, categories in the same vocabulary or in a whole new vocabulary if you like, but... and this is key..."
 
Sergei looked up and was a bit glad to see he had Jaffer's undivided attention.
 
"... but then you grant the View permission on those internal categories to the Site Member role only. So guests don't see the internal categories when the content comes back, say, on a search. Your admin folks can now configure their Asset Publisher instances to query by any combination of public and internal categories to get the view they want. Make sense?" 
 
Jaffer nodded. "Yeah. Actually, I think I could use that to address a certain requirement. In fact, I..."
 
Sergei cut him off. "Hold on. There's more. Let's click the Edit button."
 
 
"See that Properties section down there?" he pointed with his hand as Jaffer leaned in again.
 
"That, my young friend, is where the power lies."
 
Sergei took a few more sips of the mint tea. Jaffer's eyes were wide with anticipation. He wore the look of a child about to become privy to a long-guarded secret.
 
"Go on," he said softly.
 
Sergei typed frantically. "Let me just show you."
 
 
"There, I created one arbitrary property: BackgroundColor. Remember this is just an arbitrary textual key-value pair."
 
Lots more clicking and typing. 
 
"Here's an ADT I was working on for use by an Asset Publisher. Let's write a snippet to read in the category matching a content item, and then check to see if it has a "BackgroundColor" property. If it does, we'll read its value, and we'll use that value to set the background color of the DIV that holds the result corresponding to that content item."
 
#set ($vocabularyLocalService = $serviceLocator.findService("com.liferay.portlet.asset.service.AssetVocabularyLocalService"))
#set ($vocabularies = $vocabularyLocalService.getGroupVocabularies($getterUtil.getLong($groupId)))
 
### we are interested in 2 specific vocabularies - Schools and StudyLevels
#foreach($voc in $vocabularies)
    #if ($voc.name == "poems")
        #set ($poemsVocabularyId = $voc.vocabularyId)
    #end
#end
...
...
#if (!$entries.isEmpty())
    <div>
    #foreach ($curEntry in $entries)
        #set( $journalArticle = $renderer.getArticle() )
        #set ($articlePrimKey = $journalArticle.getResourcePrimKey())
        #set ($catLocalService = $serviceLocator.findService("com.liferay.portlet.asset.service.AssetCategoryLocalService"))
        #set ($catPropertyLocalService = $serviceLocator.findService("com.liferay.portlet.asset.service.AssetCategoryPropertyLocalService"))
        #set ($articleCats = $catLocalService.getCategories("com.liferay.portlet.journal.model.JournalArticle", $articlePrimKey))
        #set($viewURL = $assetPublisherHelper.getAssetViewURL($renderRequest, $renderResponse, $curEntry))
        #set( $document = $saxReaderUtil.read($journalArticle.getContent()) )
        #set( $rootElement = $document.getRootElement() )
        
        #set( $pictureSelector = $saxReaderUtil.createXPath("dynamic-element[@name='imgPicture']") )
        #set( $picture = $pictureSelector.selectSingleNode($rootElement).getStringValue() )
 
        #set ($backgroundColorCode = "")
 
        ###### get the poem categories #######
        #foreach($cat in $articleCats)
            #if($cat.vocabularyId == $poemsVocabularyId)
                #set ($backgroundColorCode = $catPropertyLocalService.getCategoryProperty($cat.categoryId, "BackgroundColor"))
            #end
        #end
 
######
###### Display the poem in a card #######
######
 
        <div class="span4" style="background-color: #$backgroundColorCode.value; color:white; padding:10px; margin:10px; height:200px; border-radius:10px;">
...
...
 
Jaffer's eyes took in everything as Sergei typed in lines and lines of velocity code from experience, and then proceeded to assign a few content items the category he was testing out. When Jaffer saw the resulting list returned by the asset publisher instance with the one content item having a distinct background color, he let out a low whistle. 
 
Sergei drained the remainder of his tea. 
 
"Think about the possibilities," Jaffer mumbled to himself. His eyes turned to rows of canned hummus and olive jars but his mind was elsewhere.
 
"We could store relative URLs to a document or an image as the value of that property field, or even... even store little clever flags that can dictate all sorts of complex work that the templates could do for a given category."
 
"Exactly!" Sergei couldn't contain his own pleasure at having reached out and touched a soul that needed more. 
 
"And wait, can we add more than one property to a category?" Jaffer asked as he poked his head towards Sergei's display.
 
"Oh yeah," replied Sergei. And he showed him how.
 
 
Jaffer slapped his thigh in delight.
 
"Uncle, you just helped me address a huge chunk of requirements that crept into my plate recently. How can I ever repay you?"
 
Sergei allowed a little smile. "How about not calling me Uncle?"
 
Jaffer grinned, then returned his plate to the counter and flew out of Abou Shousha's like a tornado that was late for an appointment in another town.
 
Sergei took a deep breath, glad for the return of silence in his little haven. 
 
"More tea, my friend?" the smiling grocer asked as he eyed Sergei's empty cup.
 
Sergei held it up in gratitude.
 
"Yes, my friend. Shukron."

Content SEO Title - Putty In Your Hand

Technical Blogs November 11, 2016 By Javeed Chida

In my last technical post titled Content SEO - Hidden in Plain Sight, I exposed a caveat in the way the title of a content item is auto-crafted by Liferay. Here’s an excerpt from that article, which I hope highlights the problem. If not, I encourage you to give that post a read..

 

Note that the Title specified is Young Night. But if you look at what got into the page source above, you can see we had: Young Night - Browse Poems - Liferay. The page name and site name seem to get suffixed to the Title.

 

So, here is how I solved it:

Lunch!

 

Over at LSNA2016, I hunted down the Content Management Roadmap table and found Julio Camarero crowded by rabid developers bombarding him with all sorts of CMS questions. I took my seat across from him… and waited.

...and waited

...and waited

...AND WAITED!

 

I eventually spied the door open and shamelessly stuck my foot in. In a blast of what felt like 400 words, I explained the problem as alluded to in my previous post hyperlinked at the top of this article.

 

Julio nodded and responded with - I paraphrase - “Yeah, there’s a JSP in the asset publisher where we construct the content title in that way. Email me in a couple days.”

 

I did. He responded. And here it is.

 

The first order of business is to understand that the below JSP is where the title and subtitle are seeded.

ROOT/html/portlet/asset_publisher/asset_html_metadata.jsp

 
<%@ include file="/html/portlet/asset_publisher/init.jsp" %>
<%
AssetEntry assetEntry = (AssetEntry)request.getAttribute("view.jsp-assetEntry");
AssetRenderer assetRenderer = (AssetRenderer)request.getAttribute("view.jsp-assetRenderer");
String title = (String)request.getAttribute("view.jsp-title");

if (Validator.isNull(title)) {
    title = assetRenderer.getTitle(locale);
}

String summary = StringUtil.shorten(assetRenderer.getSummary(locale), abstractLength);
PortalUtil.setPageSubtitle(title, request);
PortalUtil.setPageDescription(summary, request);
PortalUtil.setPageKeywords(AssetUtil.getAssetKeywords(assetEntry.getClassName(), assetEntry.getClassPK()), request);
%>
 

Now, you can insert a line to set the page title to the empty string.

 
<%@ include file="/html/portlet/asset_publisher/init.jsp" %>
<%
AssetEntry assetEntry = (AssetEntry)request.getAttribute("view.jsp-assetEntry");
AssetRenderer assetRenderer = (AssetRenderer)request.getAttribute("view.jsp-assetRenderer");
String title = (String)request.getAttribute("view.jsp-title");

if (Validator.isNull(title)) {
    title = assetRenderer.getTitle(locale);
}

String summary = StringUtil.shorten(assetRenderer.getSummary(locale), abstractLength);
PortalUtil.setPageTitle("", request);
PortalUtil.setPageSubtitle(title, request);
PortalUtil.setPageDescription(summary, request);
PortalUtil.setPageKeywords(AssetUtil.getAssetKeywords(assetEntry.getClassName(), assetEntry.getClassPK()), request);
%>
 

The problem with the above is that your title eventually changes from:

Young Night - Browse Poems - Liferay

 

… to

Young Night - - Liferay

 

Ugh! So, now I needed to figure out where that concatenation happens. I just plain didn't know. After some unsuccessful grepping, I decided to reach out to Julio again, and he graciously shone a light on where this happens. Here are the relevant lines from init.vm of the _unstyled theme.

 
#if ($pageSubtitle)
    #set ($the_title = $pageSubtitle + " - " + $the_title)
#end
 

Assuming your theme is based on the _unstyled theme, this value can be overridden by defining an init_custom.vm file and setting $the_title to be the $pageSubtitle alone, i.e.

 
#set ($the_title = $pageSubtitle)
 

Now, your title looks like this:

Young Night - Liferay
 

There!

 

WHAT?! Not satisfied?

 

I suppose you don’t want the site name to appear either. You find that trailing - Liferay there undesirable?

 

Keep in mind that the site name (aka company name) may be a good thing to hang on to depending on how your SEO philosophy pans out. I don’t know what the best practice is with respect to that, but if you don’t want it, simply remove the - $company_name variable from the value of the <title/> element in the portal_normal.vm of your theme.

 
<head>
<title>$the_title - $company_name</title>

 
So, in conclusory verse:

Tweak $the_title of your content

Fa-la-la-la-la la-la-la-laaa

Find the $company_name, yank it

Fa-la-la-la-la la-la-la-laaa

Have a great November!

 

FYI a ticket has been opened for the Content SEO fields to be configurable.

https://issues.liferay.com/browse/LPS-68495

...as well as an issue for the problem at hand, here:

https://issues.liferay.com/browse/LPS-68493

My 27th of September at LSNA2016

General Blogs September 27, 2016 By Javeed Chida

After a thirty-minute train ride sitting amidst super-excited teenagers, even the energized atmosphere of a downtown Chicago Tuesday morning seemed like a peace cloud. Anyway, got to the Hilton in good time for a bagel and creamcheese breakfast and an aromatic Colombian brew before Sergio's session.

 

Create Amazing Web Services with Liferay DXP (Sergio Gonzalez)

  • APIs are first class citizens
  • Cons of current web service APIs.
  • Configure a REST end point in Control Panel to expose a web service suite.

A simple BasicJAXRSSampleApplication class definition.

  • This class is an OSGI component.
  • Has a getSingletons() method.
  • Pretty standard REST servlet container with the usual annotations, bells and whistles.
  • Classic JAXB for XML<->Object model conversion, with freedom to define your own MessageBodyReader and MessageBodyWriter.
  • Swagger for REST documentation
  • Standard JAX-RS annotation-driven marshalling (to XML or JSON) as well as pagination for each web service request.
  • Demo'ed a nifty cURL-initiated request that posted an image to the document repository using a web service. The cool part was when he tweaked the service to set assetTagNames via the ServiceContext taking a comma-separated set of tag names from the web service invocation’s query params.
  • Decouple the internal logic of the service from the exposed contract.

 

Taking Complete Control of Product Navigation in Liferay DXP (Julio Camarero)

“How many of you remember dockbar? How many remember Control Panel? Site Administration? Okay, all of that has been revamped in DXP.”

  • New Control menu is the one menu to rule them all.
  • New Product menu now includes the Control Panel, Site Administration and all configuration menus.
  • New User Personal Bar
  • New Simulation menu

New concept of Product Themes – themes for the product navigation. Traditional themes are now referred to as Site Themes.

Julio demonstrated the ease with which a new tool/widget can be coded that attaches itself to a particular location in an existing control menu.

“You can have this class in your application/module/anywhere. Liferay will find it and use it.”

 

Guest Keynote: Why Customer Experience! (Mark Grannan)

Mark made a pretty compelling case for investment in customer experience. This presentation is for you if you’re having trouble seeing why customer experience is even necessary. The experiential sailing analogy was super interesting. Customer experience has to be balanced with digital operational excellence to harness a fly-wheel effect and propel towards the end goal.

 

Pulse Awards

I was very pleased to be included in the awardee list for Individual Contributor of the Year for 2016.

Thanks, Liferay, for the recognition.

Thanks, Apollo, for the support and a fostering work environment.

Congratulations to all the other awardees. I'm honored to be among you.

 

Lunch

The big highlight at lunch was not the food (don't get me wrong - great food!). Rather, it was a 4-minute conversation I had over lunch with the very talented Julio Camarero.

I pretty much described a content SEO issue I was hoping to have a work-around for, and he, right there, between mouthfuls of baked trout and asparagus, described a hook style fix for me, only asking for some time to recall the actual artifact that would need to be tweaked.

Extreme programming minus keyboard! 

 

The Changing Face of SEO (Corbin Murakami)

Corbin began with a sombre reminder.

“Don’t forget about SEO”

  • Distinguished between strategic and technical SEO.
  • Google increasingly rewards good UX
  • Google Knowledge Graphs
  • Schema.org to find structure data
  • Optimize site structure from a search implications perspective

Corbin has got to be one of the (if not the) funniest speakers at the conference. Oh, and did leave us with a relevant reminder.

“Don’t forget about SEO”

 

Search Intelligently Using Elastic Search (Andre Oliveira)

I just knew it when Andre spoke in his dreamy low tones that there was something hot bubbling beneath the surface.

He began with a nice survey of the search landscape.

  • The Search-Centric Digital Experience
  • Multi-asset searching (already available), but each asset can lead to a different user journey
  • Go beyond grammar and spelling in search suggestions; think invisible search network of previously made decisions/choices

And then came the demo.

There is a whole bunch of features enabled by default in the new out-of-the-box Search portlet. Can be whittled down to meet requirements.

Search portlet is still instantiable, but you can have multiple search pages in a site.

Andre built a new search view by dragging and dropping a search bar, search result lists, a few facets, a map,…WHOA!

And then he searched for all potholes in Boston. (I didn’t even know you could geolocate those.)

Another awesome presentation!

Additional search features include autocomplete readiness

Best approach with DXP is to have an external search engine running alongside - that's where Elastic Search comes in. 

  • REST API extensible with OSGI
  • explore the best query for results relevance
  • visualization with Kibana
  • (just watch the presentation when it becomes available ...lot more here)

 

 

How Graph Theory Helps the Development of Liferay's Publication Tools (Máté Thurzó)

Mate provided a fantastic “got button, need shirt to sew it on” scenario when the design team found that at the heart of the publication process is a graph of publishable data. But they had to think afresh as to how the graph nature of the publication data lent itself to the actual publication process.

This landed up being a theoretical journey into graph theory with some cool insights into the topological ordering algorithms underlying the content publication process used by Liferay.

A beast called the Coffman-Graham algorithm fosters the possibility of parallel processing, validation before the publication process begins, some prediction (probably to optimize a publication plan before execution), etc.

Great presentation if you're up for a theoretical discussion, or want to get into the inner workings of the Staging/Publication process.

Oh, and Liferay has a Neo4j driver, available on github.

 

Closing Keynote

It was a sombre keynote (and I was glad for it) with Bryan interviewing the Founder of Rahab's Daughers, a local Chicago non-profit that "aims to spread awareness about the underrated problem of human trafficking within the United States".

Exemplars of courage. Amazing work! 

rahabsdaughters.org/

-- -- -- -- -- -- -- --

It's been a fun two days. Got some ideas swimming around my head, and a casual conversation with my boss seems to suggest I can install DXP on a dev node and start experiementing with some of the goodness I've had the chance to hear about over these last two days. 

My 26th of September at LSNA2016

General Blogs September 26, 2016 By Javeed Chida

Here's a running note dump of my day 1 at Liferay Symposium North America 2016, here in Chicago.

This is just to pique your curiosity if you couldn't make it out here AND your interests happen to match mine. Wait for the slide decks and presentations to be shared by Liferay so you get it all.

Keynote (Bryan Cheung)

Bryan did it again, making his point with the same efficiency he preaches. I recalled his keynote from 2015 where he began with a heart-warming picture of children interacting with technology at Disney World, and came full circle with a sobering and grounding message not to lose track of what really matters as we embark on our digital ventures.

This year, Bryan took us through a history lesson on the culture of design – one that cuts across all industries – whereby the average and median population has traditionally been targeted. The challenge he left us with was to heed the need to design to the edges, so that our designs, while scaling for the majority, do not neglect the minority and certainly not the “jagged” reality of the majority as well.

Lots to think about. Relevant keynote.

 

New Features in Audience Targeting 2.0 – Liferay DXP (Julio Camarero)

Prospects: Converting users who are not yet customers

  • Define segments (audience definitions)
  • Fully renovated UI/UX
  • Create/manage user segments and campaigns
  • Users compartmentalized using:
    • IP addresses
    • Language rules
  • Custom fields
  • Take audience to form to capture as a lead

Recent inactive clients

  • Reports by user segment
  • Excel export capabilities
  • Asset Publisher integration
    • Enable user segment filtering feature in the new Asset Publisher configuration
  • New Simulation experience for user segment matches

Engaging customers

  • Improved campaigns
  • Multiple user segments
  • Campaign-specific content display
  • Measuring via reporting

“I want to check:

How many people watched the banner on the home page

How many people clicked on the link on the home page

How many typed into the form

How many actually completed and submitted the form

Etc..”

Audience Targeting is a framework – fully extensible to meet specific company requirements. Rules tutorials that show how to segment users based on custom rules are available on the wiki, etc.

Experimental app demonstrating:

  • Segment users based on mail keywords (keywords from emails they receive)
  • Targeting users by mood

I was really impressed by the experimental mood-driven targeting app that Julio demonstrated. No better way to say the sky is the limit than show a great (albeit forced) example.

Good stuff!

 

The ROI of Modularity (Ray Auge)

Ray’s gentle introduction to the struggle behind “IT’s platform for diversification” started to get my attention when he said “coupling exists in the IT industry at many levels, not just between one piece of code and another piece of code…”.

I thought this was a really fresh perspective on thinking about ROI for everyone – from programmers (IT decision makers), all the way up (or out) to executive decision makers.

The real takeaway from Ray’s presentation was to understand the importance of decoupling at ALL levels of IT development through delivery. My paraphrased bullet points are below.

  • Virtualization (decoupling inter-system dependencies and gaining virtual )
  • Polyglot (decoupling developers affinity to implementation language preferences)
  • Microservices ( decoupling systems by adopting clearly documented and stringent contracts between the moving parts)
  • Continuous delivery (decoupling from the unpredictability/variances of time)
  • Modularity (taking different deliverables, mixing them up and being able to get a return on the summation, doing it smartly appears to be the challenge)

“Reinventing yourself every few years is bloody costly”

 

Leading Your Company’s Digital Transformation (Brian Laird)

I’ve got to say. My boss’s boss’s boss is a fantastic talker. That’s likely not going to impact my raise any, but hey, it IS true!

You should check out this presentation if you are an IT thinker/professional who may be accustomed to thinking about change inside out, with IT as your greatest influencer. Brian forces a broader thinking mindset, outlining the main drivers for digital transformation. Great talk for IT entrepreneurs and decision-makers alike.

 

Lunch

Glorious whitefish! What else is there to say!

<insert picture here I was too busy eating to snap a picture of my plate. By the time I realized that, the food was all gone./>

 

Building a Culture of Trust, with Slack (Andrew Gruhn, Jay Landro)

If there was one presentation for a developer to attend at LSNA16, I think it would have to be this one. I think…

I had heard of Slack from a developer friend and even used it a little bit in the last year with a closed group of friends, but man, the good stuff is beneath the surface.

Andrew introduced Slack features real quick in the light of how his team used slack channels as a step toward dispelling the absolute absence of phatic communication in globally distributed teams.

“Slack is a javascript web app wrapped in electron.”

  • Slack provides integrations with well-known application stacks out three. E.g. Github, giffy. Great use case is when continuous integration can notify an integrated Slack group.
  • Rise of integrations as a service: IFTTT (IF This Then That) and Zapier.
  • They demonstrated a channel named #designideation which showed pictures they snapshotted using the camera from IFTTT.
  • Use of web hooks to setup a simple integration: being able to type a command to slackbot and then have the web hook propagate to another integrated system. E.g. QA person type a command to slackbot which (via a web hook) triggered a build and deployment to the QA servers.
  • Use of Liferay IO – a microservices host – that can be used to expose select operations. These operations could service commands sent to slackbot.
  • A framework for bots: “Slackbot is the language parsing tool within slack.” Worth checking out the “When someone says” and “Slackbot responds” mapping tool feature of Slackbot.
  • A LiferayBot implementation that could be queried by commands via Slack. E.g. “@liferay-bot search for xyz” causes the LiferayBot to perform the search and return a URL to the Search Results on a Liferay DXP instance.

Fascinating!

 

Boost Your Productivity with Web Experience Manager in Liferay DXP

This session was mostly a look at the Control menu in Liferay DXP.

The dockbar has been overhauled. Repeat. The dockbar has been overhauled.

Lots of improvements to navigating around Control Panel and User Account settings. Much improved UX.

Content Authoring improvements include:

  • ease in comparing historic versions of content
  • image selector enhancements (pull an image from a URL and drag-and-drop, inline image editing)
  • a new priority field that can be set in Asset Publisher to bring certain content with higher priority at the top of results
  • new “Restrictions” feature that lets you define what structures can be used for content in a specified folder. (this is pretty cool)
  • new page type called “Page set”. If a page is of type Page Set, then it does not get rendered as a link and gets treated as a true container. Thus we don’t need a hack to make it unclickable. Page sets are not clickable. Sweet!
  • ADTs for breadcrumb and navigation now available (I’ve only ever used Asset Publisher, time I dug into the other ones)
  • A theme can now have a decorator set for it – out of box nifty displays
  • Publishing simplified
  • LAR properties – delete (LAR) on success/failure.

Questions I asked:

  • There is a Restrictions API, right?
    • Not sure.
  • Have you looked into versioning templates?
    • No. (I would love this feature. Note to self to request it via JIRA.)

Jose Dominguez was super-welcoming of emails. Nice, tight presentation.

Coffee break!

Decadent Ice cream brownie. WHAT!?!?

So good.

 

Protect Your Data! Securing Your Liferay Instance

“One does not simply walk into Mordor”

That is how you begin a discussion of security. Not that the evil lord Sauron is an ideal example of an IT Director persona (no offence meant).

That’s how Jeff Handa began his presentation as he walked through a breadth-first list of common Infrastructure precautions and best practices.

Liferay is just on top of whatever foundation platform we have chosen. So pick a supported platform that is familiar, patched, and monitored.

 

Upgrading and Redesigning Websites (Ryan Schuhler)

Ryan  talked through the madness of a UI/UX upgrade on top of an actual version upgrade of liferay.com.

“What have I done?”

You know, he mentioned that every upgrade and redesign effort often feels like an existential crisis. For my part, and maybe this says too much about myself, but I have experienced existential crises of earth-shattering proportions with something as simple as a cut-and-paste operation. Again, that’s just me.

He shared a nice checklist of questions to ask yourself before embarking on a redesign/upgrade effort. Lots of good common-sense tips and spectrum of UX options to consider. Great talk for UX designers and primarily front-end developers.

- - - - 

Got to catch the train out. Will post tomorrow's notes... tomorrow!

Content SEO - Hidden In Plain Sight

Technical Blogs September 15, 2016 By Javeed Chida

It cannot be that it was just forgotten,
This fundamental feature, thus I sought
Through thick of forum, blog, e'en post ill-gotten
Until my quest returned me to the spot
Where sat the creature with expression cursed
Inquiring why I didn't look here first.
 
This post is the result of a search for how to configure SEO metadata tags for content. We're all familiar with how to specify those for pages - simply go to the page in Control Panel, choose the SEO tab and enter the Title, Description and Keywords.
 
 
 
Well, it so happens that you can do this for content as well, but not in the same way as we do it for pages.
 
Let's back up a bit. WHY would we need to specify SEO for a content. Those meta tags (title, description and keywords) are relevant at a page level so search engines can determine if a given page has relevancy to a given search.
 
But, content, unlike pages, is just markup that is part of a whole! When we think of a content-driven website, we think of pages with each page having one (often more than one) Web Content Display portlet instances, and, don't forget, possibly one or more Asset Publisher portlet instances.
 
Ah! Asset Publisher, a clever device that brings back content items meeting specified criteria.
 
If you code your Asset Puiblisher's Application Display Template (ADT) mindfully, it will likely have a link to a detail view of that content item. And if you've gone all the way, then you know that the detail view you see is really hosted on a placeholder page that also contains an Asset Publisher portlet instance that is configured as its Default Asset Publisher, and that your content has its Display Page set to the placeholder page we just talked about. All of this setup helps the Asset Publisher engine apply your content template to your content XHTML and render it on the placeholder page. These are details you are quite familiar with if you have used Asset Publisher before. 
 
If the previous paragraph is all new to you, you are in for a treat! Read this short blog post and come back here if you're still interested in this one. 
Warning: you may not want to come back for a while because Asset Publisher might just blow you away and preoccupy you with several little project ideas.
 
Now, the first thing to do is go to a page that has your Asset Publisher portlet instance duly configured. Here's mine, with a fancy card view for each result.
 
Next, click on one of the content items so you get to the detail view of your content. This basically follows the link you coded in your ADT.
 
And view the page source for that selected content item. You should see something like the below.
 
 
Spot the three tags we care about - title, description and keywords and study their values.
Title: Young Night - Browse Poems - Liferay
Description: Hand in hand, we take it all in: The babble of the river, The whisper of the wind,
Keywords: free verse
 
Now, let's see where those values are coming from. Go into the Control Panel, then Web Content Repository, and edit the content item in question. 
 
The title is just the Title on the Content tab. 
 
 
Note that the Title specified is Young Night. But if you look at what got into the page source above, you can see we had: Young Night - Browse Poems - Liferay. The page name and site name seem to get suffixed to the Title. 
 
Next, click on the Abstract tab. The Abstract field sources your description meta tag.
 
 
Finally, click on the Categorization tab. The Categories and Tags, together, source the keywords meta tag. We have one category specified, free-verse
 
 
For kicks, let's add a few tags.
 
 
Now, let's go view the page source again. We should see the newly added tags show up for the keywords meta tag. Note that the tags and the categories are consolidated into a comma-separated list.
 
 
So, there we have it. For content items that are served up via the Asset Publisher, we can now control the SEO title, description and keywords via content metadata.
 
The interesting thing here is that we're making our content SEO-friendly without really thinking about it. But if an SEO specialist were to come by and ask us how they might effect changes in a specific content item's SEO tags, we now know where to point them.
 
Isn't life[ray] beautiful?

The Glorious Simplicity of Structure Inheritance

General Blogs August 13, 2016 By Javeed Chida

The Glorious Simplicity of Structure Inheritance

This field (highlighted yellow) on a structure editing screen is what I am talking about.

When I first saw the words Parent Structure there, the following thoughts came to mind, in increasing order of coolness

  1. All fields in the parent structure are inherited by the child structure

  2. The idea of polymorphic content, i.e. content of one type (i.e. structure) Y that is a child of type X, is also of type X. This is in essence the IS-A relationship in object oriented programming.

  3. Asset Publisher would respect above polymorphic behavior so I can simply query for content of the parent type and get back all content tied to that structure as well as all content tied to child structures.

  4. The idea that a child structure would automatically be serviced by the CMS template that serves its parent.

If you guessed that all of that is true, then you would be… wrong. The only point that holds true is the first: a structure simply inherits the fields of its parent structure (if one is specified).

That’s it! Done!

Now, this sort of obviates the need for the rest of this post. But if you’re like me, you want to try stuff out and push the limits a bit to see how stringent things really are, and what the actual payoff is, in practice, of using parent structures. So that is what follows.

Important: There is no IS-A relationship between a child structure and its parent. These are not classes - they are simply XML definitions complying with Liferay’s DDM structure schema. The child structure takes all fields that its parent structure offers, and then cuts the cord in all its individualistic glory, refusing to be identified as an extension of its parent for practical purposes. The only evidence of the relationship is in that parent-structure association observed in the child structure definition. I’ll demonstrate this from an Asset Publisher perspective in a bit.

Let’s have a parent

Okay. Time for something concrete.

I have my classic Poem structure - graphic shown below.

I use the above structure for all my classical poem forms. But now, I have a burning requirement to have a separate structure for my ecphrastic poems.

Ecphrasis

noun  ek·phra·sis \ˈek-frə-səs\

a literary description of or commentary on a visual work of art

My new structure needs all the fields that my existing Poem structure already has. In addition, it has two fields:

  • Picture (to capture an image of the painting or photograph that inspired the poem)

  • Citation (text related to the picture such as name of work, name of artist, etc.)

One option would be to redefine all the fields from the Poem structure in our new structure, which is actually very simple and quick to do. Simply go to the Source view of the structure, select all, copy and paste into the Source view of the new structure. But that is not the point here. We want to follow the DRY principle as much as possible so that we have common fields in one place, thus avoiding a proliferation of repetitive data that really ought to be maintained in one place. Remember, templates are going to be working with the fields of a structure, and templates are stubborn little creatures that fuss about field names and such, so following a DRY approach can get to be quite important from a maintainability standpoint.

Anyway, no TA-DA moment here. That Parent Structure field that every structure offers helps us out. This is what the new structure looks like now.

 

Note how the parent structure is selected as Poem. And that the new structure, Ecphrastic, has only two fields: Picture and Citation. So, when you go to create content of type Ecphrastic, you see something like this:

 

New structure needs new template

A template can be associated with one and only one structure. So a new template is needed by content that wishes to use your new structure. Of course, you could go to the template used for the parent structure and select the structure in the template definition to be the new one you created, but then it would cease to service content associated with the parent structure.

You will need a new template for your new structure. This is not a problem. Simply replicate your old template and add in the presentation parts to show the new fields. You may also consider import the existing template if the new fields just need to be tagged on to the end or beginning. Do whatever makes sense.

So, here’s my CMS template to service my new Ecphrastic structure.

#parse ("$templatesPath/$selectMood.getData()")
<h3>$txtPoemTitle.getData()</h3>
<div class="poem-body">
$htmlVerse.getData()
</div>
<div style="clear:both"></div>
<div class="about-poem">
$htmlAbout.getData()
<br/>
<img alt="Picture" src="$imgPicture.getData()" />
<br/>
<i>$htmlCitation.getData()</i>
</div>
 

The newly added presentation is highlighted. Basically we show the picture right-aligned and the citation text right below it. This is how our content looks now when the template renders it.

Ugly, I know; but in a beautiful sort of way, yes?

Asset Publisher is just being Asset Publisher

If you wanted to configure an Asset Publisher to bring your content back, you would literally have to configure it so as to bring back content matching both parent and child structure types. As far as consumers of content are concerned, each structure is a first class structure in its own right. The structure inheritance is just cleverness to keep from repeating common fields.

Here’s a snapshot of what an Asset Publisher configuration might look like.

Now, if you wanted to display a mix of content of types Poem and Ecphrastic, you would have to work that into your Application Display Template. The ADT does not really care about the relationships between structures - you are working with content within an ADT - the actual asset. In an ADT, you just use XPATH syntax to find the fields you want using their corresponding names. If the field is found in the content, you retrieve it, else you get null.

Here is the ADT I wrote to show the results of the above configured Asset Publisher instance.

#set ($vocabularyLocalService = $serviceLocator.findService("com.liferay.portlet.asset.service.AssetVocabularyLocalService"))
#set ($vocabularies = $vocabularyLocalService.getGroupVocabularies($getterUtil.getLong($groupId)))
### we are interested in 2 specific vocabularies - Schools and StudyLevels
#foreach($voc in $vocabularies)
   #if ($voc.name == "poems")
       #set ($poemsVocabularyId = $voc.vocabularyId)
   #end
#end

#if (!$entries.isEmpty())
   <div>
  #foreach ($curEntry in $entries)
  #set( $picture = "")
  #set( $renderer = $curEntry.getAssetRenderer() )
  #set( $link = $renderer.getURLViewInContext($renderRequest, $renderResponse, ''))
  #set( $journalArticle = $renderer.getArticle() )
  #set ($articlePrimKey = $journalArticle.getResourcePrimKey())
  #set ($catLocalService = $serviceLocator.findService("com.liferay.portlet.asset.service.AssetCategoryLocalService"))
  #set ($catPropertyLocalService = $serviceLocator.findService("com.liferay.portlet.asset.service.AssetCategoryPropertyLocalService"))
  #set ($articleCats = $catLocalService.getCategories("com.liferay.portlet.journal.model.JournalArticle", $articlePrimKey))
  #set($viewURL = $assetPublisherHelper.getAssetViewURL($renderRequest, $renderResponse, $curEntry))
  #set( $document = $saxReaderUtil.read($journalArticle.getContent()) )
  #set( $rootElement = $document.getRootElement() ) 
  #set( $pictureSelector = $saxReaderUtil.createXPath("dynamic-element[@name='imgPicture']") )
  #set( $picture = $pictureSelector.selectSingleNode($rootElement).getStringValue() )
  #set ($colorCode = "")

  ###### get the poem categories #######
  #foreach($cat in $articleCats)
    #if($cat.vocabularyId == $poemsVocabularyId)
      #set ($colorCode = $catPropertyLocalService.getCategoryProperty($cat.categoryId, "color_code"))
    #end
  #end
###### Display the poem as a card #######
  <a href="$viewURL" style="color:white;">
  <div class="span4" style="background-color: #$colorCode.value; color:white; padding:10px; margin:10px; height:200px; border-radius:10px;">
  <span style="font-weight:bold;color:white;font-size:20px;">${curEntry.getTitle($locale)}</span>
  <hr>
  #if ($picture != "")
    <img src="$picture" align="right" width="120px" style="padding:5px; margin:5px;">
  #end
  ${curEntry.getSummary($locale)}
  </div>
  </a>
#end
</div> 
#end
 
The only snippet I added for my new ecphrastic content was this:
#if ($picture != "")
  <img src="$picture" align="right" width="120px" style="padding:5px; margin:5px;">
#end
 
And here is what the Asset Publisher portlet response looks like when that ADT renders. You can see how my Ecphrastic content displays with a thumbnail of the insipirational artwork right-aligned. 
 

Okay, so why glorious?

Ah, yes!

The Glorious Simplicity of Structure Inheritance.

Two reasons why I chose that as title for this post.

  1. I was thinking about The Curious Case Of Benjamin Button - may have had something to do with it.

  2. The simplicity is glorious because IMO we don’t want any more cleverness than the sharing of common fields to come out of this relationship between structures. Could you imagine the sort of madness that might ensue if an Asset Publisher, for instance, were to display content of a structure that simply extended another structure. What if the extension were only for reuse purposes and that there was no semantic relationship between the structures apart from field names (rare, but possible). The Asset Publisher already does a pretty good job with its querying mechanism giving us various criteria by which to bring back content (multiple structures, tags, categories, fields). We don’t need polymorphic madness injected into the mix.

That concludes it.

Fun with Generic Content Templates

General Blogs July 1, 2016 By Javeed Chida

I came across the idea of a generic template recently, and put it to good use.  If you don't know what I mean by generic template, let me clear that up right away.
 
A generic template is really just a content template that is not tied to a structure. The point of it is that you can sparate your template code as you see fit, including the generic templates in your main one. All you need to do is add this line of (velocity markup) code in your template.
#parse ("$templatesPath/12345")
where 12345 is the template key of your generic template. That's it. All the code in your generic template gets pulled into your main template and treated as one.
 
There! You know what I mean by generic templates now. So, let's talk about the fun I had with them. I've come to be used to velocity, so the sample code below is all vm.
 
Here's my main template.
<h3>$txtPoemTitle.getData()</h3>

<div>
$htmlVerse.getData()
</div>

<div>
$htmlAbout.getData()
</div>
 
At a glance, you can tell I am displaying three fields:
  • a title
  • a poem (rich text)
  • a few comments about the poem (rich text)

 

Contrived requirement #1: Style it

After one minute of frenzied googling, I have this:
 
<style>
.poem-body {
    float: left;
    font-size:16px;
    color: #989898;
    background-color: #B7E2F0;
    border: solid 1px #B7E2F0;
    border-radius: 5px/5px;
    -webkit-box-shadow:0 23px 17px 0 #B7E2F0;
    -moz-box-shadow:0 23px 17px 0 #B7E2F0;
    box-shadow: 0 23px 17px 0 #B7E2F0;    
}
 
.about-poem {
    float: left;
    margin:15px;
    font-size: 16px;
    font-style:italic;
    background-color: #efefef;
    color: #555555;
}
</style>

<h3>$txtPoemTitle.getData()</h3> <div class="poem-body"> $htmlVerse.getData() </div> <div class="about-poem"> $htmlAbout.getData() </div>

Calm down! Ugly, inefficient, but as I said, contrived. I'm just trying to make a point here.
 
All good. Now, let's move the styling into its own template - a generic template - one without a structure association. Now the main template looks like this:
 
#parse ("$templatesPath/73906")
<h3>$txtPoemTitle.getData()</h3>

<div class="poem-body">
$htmlVerse.getData()
</div>

<div class="about-poem">
$htmlAbout.getData()
</div>
 
73906 is indeed the template key of my generic template, as shown below.
 
 
 

Contrived requirement #2: Create an alternate style for our poem template.

A fiery red sonnet style. It looks very similar to the cool blue verse style, just some different colors.
 
<style>
.poem-body {
    float: left;
    font-size:16px;
    color: #ffffff;
    background-color: #CC0033;
    border: solid 1px #CC0033;
    border-radius: 5px/5px;
    -webkit-box-shadow:0 23px 17px 0 #CC0033;
    -moz-box-shadow:0 23px 17px 0 #CC0033;
    box-shadow: 0 23px 17px 0 #CC0033;    
}
 
.about-poem {
    float: left;
    margin:15px;
    font-size: 16px;
    font-style:bold;
    background-color: #efefef;
    color: #555555;
}
</style>
 
Now, I can simply change my generic template include in my main template as below to reference the alternate template.
#parse ("$templatesPath/73945")

<h3>$txtPoemTitle.getData()</h3>

<div class="poem-body">
$htmlVerse.getData()
</div>

<div class="about-poem">
$htmlAbout.getData()
</div>
 

Contrived requirement #3: Let the user pick which style to apply

This is slightly more involved. I modify my poem structure to have, in addition to the original three fields, a select field named Mood with three options, as shown below. Take note of the values of those options.
 
Alright! Now over to my main template to use the value from this field.
#parse ("$templatesPath/$selectMood.getData()")

<h3>$txtPoemTitle.getData()</h3>

<div class="poem-body">
$htmlVerse.getData()
</div>

<div class="about-poem">
$htmlAbout.getData()
</div>
 
And we're done. Here's my content with Mood options. The screenshots that follow show the rendered results.
 
The Content Item:
 
Selecting Cool Blue Verse:
 
Selecting Fiery Red Sonnet:

 

In Conclusion

If you don't see this as useful here, you're probably thinking: why go to all this trouble instead of creating an alternate content template altogether and just use that? Well, let me conclude by highlighting what we accomplished.
  1. Separation of code. We separated the template code between a main template and one or more generic templates. Sure, we just did css in the above example, but these are first clas velcity templates; they could have anything a velocity template can have - css, html, javascript, server-side calls into Liferay's universe of APIs. Power!
  2. Reuse. DRY principle and all. Each generic template is now usable in other main templates.
  3. User-empowerment. By adding a select box to the structure, we've now given the user the ability to switch the generic template that gets used. This makes for some useful indirection. 

Quick FYI on template keys: they are maintained through a LAR export/import, just in case you were wondering. 

 


Happy Fourth, America! To the rest of the world, I hear Jeff Goldblum will be peddling USB drives on an alien mothership once again. If you don't get that reference, consider yourself blessed and enjoy your July anyhow.

Smart content using category properties

Technical Blogs May 18, 2016 By Javeed Chida

If you've been using the Liferay CMS, you are probably familiar with a journal article's Categorization tab wherein you can specify the tags and the categories for the journal article. If you aren't familiar with those terms, you should read up on Categorization. Tags and categories are indispensable to a well-defined content architecture, apart from being the most convenient way to organize your content. More here:
 
The rest of this article assumes you are familiar with content categories. One little feature of categories that you may not be aware of is that a category can have one or more properties tied to it. If you knew about that already, you can safely skip this post and save yourself the 15 minutes it will take to read. But if you didn't know that, or you did but never really dug into it, read on.
 
Here is a screenshot of my experimental Versified site that lets you browse some of my classical poems. For the purposes of this post, I originated the following requirement:
 
Present all poems in a card layout wherein each card displays the summary of the poem and is color-coded based on the category it belongs to.
 
You can get to the Categories screen by clicking Admin / Content / Categories. As you can see, I created a new vocabulary on there called poems. Then I added a few categories to that vocabulary to represent some forms of classical poetry.
 
But I didn't stop there. A category can have one or more properties, where a property is any arbitrary name-value pair that makes sense for your business needs. So if you were to click the sonnet category you would see that it has one property named color_code which takes a HEXA HTML color code, as below.
 
You can click Edit to see how that property was added, see below.
 
 
In order to let me code to this specific property name, I went ahead and defined that same property for the remaining categories in my poems vocabulary. In effect, Sonnets are to show up in Midnight Blue (our category of interest in this post), Silly Rhymes in Red, Free verse in Dark Gray, Hybrid metered poems in Orange, and so on.
 
I then created some content items and assigned each of them an appropriate category. Screenshots below are provided for those new to Liferay CMS - they simply show how I (1) viewed the list of content (Admin / Content), (2) selected a content item to edit, and then (3) clicked the Categorization tab to select a category in the poems vocabulary. Don't forget to click Save to publish your changes.
 
 
Now to tie it all together. Here's what I know:
  1. Since I have to present multiple content items, I am going to need the Asset Publisher Portlet.
  2. Also I need to display the information in a card layout. So I can't use a stock ADT (Application Display Template), such as Abstracts. Instead I will need to craft a custom ADT.
  3. Further, I need to be able to color code each card based on the category that the content item is tied to. 
 
In coding my custom ADT, I could use an inelegant algorithm whereby we check the category name and compare it with a known list of categories, and then use a color code associated with each category. But such a programmatic approach is inflexible and confusing to maintain. That is what makes the category properties I created above an attractive declarative option.
 
Here is my final custom ADT that addresses the above. Note: I just hacked in styles rather than define clean CSS classes because I honestly don't care - not the point of this post. (Don't judge me.)
 
In particular, this excerpt shows how the color_code property is extracted from a content item's category.
#foreach($cat in $articleCats)
    #if($cat.vocabularyId == $poemsVocabularyId)
        #set ($colorCode = $catPropertyLocalService.getCategoryProperty($cat.categoryId, "color_code"))
    #end
#end
 
And here is my asset publisher after it was configured to use that ADT.
Now, if I need to change my sonnets to be green instead of blue, I just have to modify the value of the color_code property I defined for the sonnet category.
 
 
And here's how the Asset Publisher renders after that change.
Furthermore, if I need to define a new category (say, limerick), I can create it with its own color_code property and unique HTML color code value, and then simply assign any content to have the new limerick category via the Categorization tab.
 
Wasn't that easy?

Liferay Portal CMS Concepts for Non-Technical Users

General Blogs May 12, 2016 By Javeed Chida

I recently presented this to a group of non-technical users. The feedback was positive, and made me think this may be useful to others.

Why did I make this presentation?

I put this little presentation together because a lot of our users, who were being trained to contribute content, had trouble distinguishing a web content item from an instance of the Web Content Display portlet. The solid red line between a Web Content Display portlet and the journal article that it is configured to point at is more blurred to end users than you think. Furthermore, some folks could not appreciate the need for structures and templates, often viewing them as unnecessary overhead. 

In less than fifteen minutes, I address the above at a level high enough to make a techie gasp for oxygen.

Take note that:

  • This presentation is intended for non-technical business users. It is not for developers.
  • Developers interested in learning about Liferay Portal CMS concepts would do better to start with the Liferay Developer Network
    • That being said, if you are a developer and are looking for ideas on how to present some of these CMS concepts to a non-technical team of (say) content editors, then you may find the content of this presentation useful.

Warning! Cheesy analogies abound.

Part 1 (10:00)

Part 2 (3:34)

Content-driven applications: A Marriage Between CMS Templates and RESTful services

General Blogs April 15, 2016 By Javeed Chida

I've been busy building web application content.

  • A simple form with three fields
  • Under the form, a table shows the previously added records
  • The data in the table can grow to several hundred
  • Blah blah blah

We've done this many times. And we've got enough tools from here to the mooon and back to get it all done pretty well.

But of late, I've been bitten by the CMS template bug. The sheer power of CMS templates and the vantage point they offer to being able to examine the entire technology landscape AND interact with it is not a thing to be ignored.

Parts of my web application

A CMS structure

Here is where I define all the fields I can use to configure my application. Example fields include:

  • Caption
  • Header text
  • Footer text
  • Preferred style for buttons
  • Default page size for table records
  • Payment gateway request URL
  • Payment gateway success URL
  • Payment gateway failure URL
  • etcetera

A CMS template coded in velocity

This has equal parts Javascript and HTML. I use AUI, jQuery and a cocktail of them both wherever I feel the best of both worlds can be had.

My CMS template basically has a bunch of javascript at the top that is pulling in relevant fields from the structure.

The javascript is followed by some very tight (and light) HTML markup peppered with bootstrap classes and some minute inline javascript I didn't care to get rid of.

A journal article using this structure and template

This is where the structure becomes concrete. I specify values for the fields in the backing structure. These are the values that will configure my application.

An auxiliary suite of servlets

This is a web application containing a suite of simple, focused HttpServlets that are wrapped by Liferay's magical PortalDelegateServlet. If you don't know what that is, definitely look it up.

These servlets let me sneak under the portal's authentication hood and examine various session data. I can test that the user is authenticated (via CAS). If my servlets decide that user is not authenticated (or authorized as the case may be), my CMS template can roll over and play dead.

A RESTful service layer

...powered by my preferred JAX-RS implementation, Jersey. Lots of API resources here for all the little (and big) operational units we need to execute. There are POJO's (for use by JAXB) and various utility classes.

Now, for the over-dramatized monologue.

Why use CMS when, really, this should be a portlet?

It IS a portlet. It is the Web Content Display portlet that Liferay gave us out of the box.

Yeah, yeah. But then you're tied to the configuration that the Web Content Display portlet comes with. Right?

Yes. Exactly. It's content. I like to call it - stares off into space thoughtfully, then snaps back in an epiphanic moment -  I like to call it application content.

Alright, wise guy, how do you configure YOUR Web Content Display portlet?

I TOLD YOU. The journal article I created above lets me populate all those fields in the backing structure. Those fields are what I rely on to configure my (not going to say 'portlet') application content. There's a meta-shift in thought here.

What about the JSR-286 portlet spec and the cool lifecycle of a portlet?

Dude. Liferay's Web Content Display portlet follows the JSR-286 spec. It lives that lifecycle. It's all that already. Except I don't care about those parts. What I've done is essentially coded up a Single Page Application into my CMS template instead of into the portlet JSP.

Okay. But why?

Because I wanted to use all that ajax goodness anyway. Why would I want to put that into portlet JSPs when I can talk to my REST layer directly from velocity-enriched HTML. It's done all over the place.

Besides, when we upgrade to Liferay 7, this application content won't need any special migration. It should (hopefully) be supported as part of the standard CMS upgrade. And then, if I want to avail of Senna.js to write a real portlet-aware SPA application, it will be a lot easier to port my REST-conversant javascript over to an SPA.

The end is met

The means are fair

Or so I think. I would love to hear from the larger community and have this approach taken apart. Only good can come from any constructive criticism.

In the meantime though, this is soooo liberating. Frees me up to divert all my Java into a rich, flexible REST layer.

And all my UI code is slimmed down to HTML5 + Javascript + CSS - exactly what it ought to be.


Like I said: I've been busy building web application content.

Oh How I Love Asset Publisher, And Making Peace With Content Friendly URLs

General Blogs February 19, 2016 By Javeed Chida

This is not really a technical post. In what follows, I share two little tips for Liferay 6.2 users. This may also be useful to developers, unless you prefer crafting SQL to solve all your data problems (which you probably do).

Let's get to them.

#1: Find journal articles that use a particular structure

Read the errata.

When I was starting out with Liferay CMS, one of the features I missed the most was the ability to say with ease what journal articles use a given structure. I explored various options on various screens in the Admin console. And when I couldn't find anything, as with all things in life, I sighed and crafted some joins.

A couple weeks ago, a front end developer posed the question to me. Now, it was even worded much the same way I had asked it of myself weeks before:

"How do you find content items that use a particular structure?"

I can only imagine that in the months in between visitations from this question, my brain had discovered the answer and stowed it away amongst some 3.7M neurons. But the synapses lit up just then (that's right, I paid attention during Biology class) and pushed the answer up past the boundaries of my lower subconscious. 

Three words: Asset Publisher Portlet

We use it all over the place to solve our business problems, but it just hadn't occurred to me earlier that I could use it for this meta-reason. 

When you go into the Configuration of an Asset Publisher portlet instance, you get to choose the Asset type (Web Content Article) and then the.... wait for it.... Web Content Article Subtype.

That's it. Remember: subtype = structure.

So my recommendation is this: when you are in the heat of development, create a page, which is hidden from navigation, with all permissions granted only to admin users. Drop an Asset Publisher on that page, and use it when you need it.

 

 

Errata

Thanks to Jader Francia who took a minute to and point out in his comment (and with admirable politeness) that there is a Browse by Structure option hidden in plain sight. I just missed it. I do like having an experimental asset publisher, but the central purpose for it is now duly obviated :-). 

This little gem came to me from the intelligentsia called the Liferay Incident Support Team. Bless their hearts. I am just sharing it here. 

 

At first glance, this section may seem superfluous because the steps to copy content are the same. However, one of the consequences of copying web content is that it automatically replicates and tweaks the content-friendly URL for the new copy.
Note: this is a known issue, and is being tracked here: https://issues.liferay.com/browse/LPS-51210.
 
Follow the below steps to have a relevant content-friendly URL for your journal article.
 

Wait a second! What, kind Sir, could you possibly mean by "a relevant content friendly URL"? 

There's no need to be condescending indecision. The thing is, not following the below steps will result in journal articles having... how shall I say it... strange content-friendly URLs. For example, consider the awkwardness of seeing http://site.com/-/my-favorite-item-to-copy-1 in the address bar for a journal article that is titled "How To Become A Renegade Hacker".
 
To Create a Journal Article Titled How To Become A Renegade Hacker from an existing journal article titled My Favorite Item To Copy:
  1. Edit the journal article you are copying from. Change the Title from My Favorite Item To Copy to How To Become A Renegade Hacker
  2. Publish the article.
  3. While viewing the content list, click the recently-edited content item's dropdown arrow. Choose Copy, then Copy. 
  4. You should now have a copy created titled, How To Become A Renegade Hacker, with the correct content-friendly URL. 
  5. Test it by visiting http://site.com/how-to-become-a-renegade-hacker.
 

Crafting Content Friendly URLs

General Blogs January 22, 2016 By Javeed Chida

Content Friendly URL?

This is one of those tidbits you get served on a platter if you're lucky enough to attend a Liferay training session. Anyway, content friendly url may not be the legit term, but what I'm referring to is a URL crafted using a Journal Article's urlTitle attribute, which is stored in every journalarticle record btw. 
 
The standard format of such a URL is;
http(s)://<sitename>/-/<article-urlTitle>

For example:

http://renegadehacker.com/-/about-the-hacker

The Setup

So how do we get such a URL to become available to us? Here are the steps. For convenience, consider we have a content structure named XYZ.
  1. Create a page XYZ Detail. Check the box to hide from navigation. You do this because this page is going to serve as a sort of placeholder to pickup and serve the requested content automagically (calm down: no magic, all science).
  2. Add an Asset Publisher portlet to that XYZ Detail page.
  3. Configure that Asset Publisher portlet instance to bring back Web Content Article having subtype XYZ (this corresponds to the structure for your content).
  4. On the Display Settings tab of the Asset Publisher portlet, click the checkbox to Set as the default Asset Publisher for this page.
  5. (IMPORTANT and easy to miss) Edit each course content item, click the Display Page option on the right-hand-side. Select the XYZ Detail page as the display page. 
 
Now, if you have a web content item (aka journalarticle for newbies) with a title: The Rain in Spain stays mainly in the plain, you will be able to get to it using:
http://renegadehacker.com/-/the-rain-in-spain-stays-mainly-in-the-plain/
 

It Doesn't Work!

One thing to try out (apart from ensuring you carried out the above steps) is to check the urlTitle for that journal article in the journalarticle table of your database. Good chance it is quite different than what you expected it to be. Well, that is what you have to use, and that is what the urlTitle reserved variable in velocity brings back. But why is it different?
 
Here is what I noticed:
The very first value you give to the title gets copied into the urltitle for your article and it sticks no matter what you change the title to after that.
 

And that my friends, is your friendly report on content friendly URLs, if that be the friendly term for it.

Search-backed and content-driven Megamenus

General Blogs December 28, 2015 By Javeed Chida

Okay, that is a mouthful. To rephrase my compound title, we'll be talking about an approach that 

  1. uses search to
  2. update a content item that
  3. follows a structure tailored to a
  4. template that renders a megamenu.

There! Thats the flow in a nutshell. If that already has the gears turning in your head, then you're welcome. Ta-ta! But if you need more, read on.

Note that I'm discussing a design here, resorting to low-level implementations only where needed. Undertanding the moving parts in this design/approach is the real value of this post.

What is a megamenu?

You've seen them. Megamenus are everywhere. A megamenu is basically a beautiful and useful kink in your navigation. Most navigation HTML provides a hierarchical menu reflecting your sitemap. But every so often, a business may want to break away from that mundaneness and give the user a rich navigational experience.

For a quick example, visit amazon.com and hover over any item under Shop by DepartmentEach of those is a megamenu.

Our Simple Course Megamenu

Consider the below megamenu which we will use as a case study for the purposes of this post.

It features 4 tabs. Each tab shows a list of courses arranged alphabetically. Clicking a course takes you to the course content page. All courses are Liferay journal articles of a custom subtype called Course.

The Megamenu Content Structure

More often than not, the information we display in a megamenu has a well-defined structure. For the above example, we can quickly define a content structure for our megamenu that would look something like this:

Tab (Text/Repeats)
   Course Letter (Text/Repeats)
        Course Title (Text)
            Course URL (Text)

Using the above structure, you could manually create all the content necessary for our courses megamenu. Sounds crazy! Right. And we don't want to do anything crazy! So hang in there.

The Megamenu Content Template

Here is the stripped-down megamenu template that uses the structure above to render the megamenu.

<div class="mega-dropdown-nav-container">
  <ul class="nav nav-pills nav-justified block-light" role="tablist">
    #foreach ($studyLevel in $txtQualificationLevel.getSiblings())
      <li role="presentation">
        <a data-target="#$studyLevel.txtIdentifier.getData()" aria-controls="#
           $studyLevel.txtIdentifier.getData()" role="tab" data-toggle="pill"><strong>
              $studyLevel.getData()</strong></a>
      </li>
    #end
  </ul>
</div>

<div class="mega-dropdown-tab-container">
  <div class="tab-content">
    #foreach ($studyLevel in $txtQualificationLevel.getSiblings()) 
      <div role="tabpanel" class="tab-pane $active" id="$studyLevel.txtIdentifier.getData()">
        <div class="mega-dropdown-menu-inner">
          #foreach ($letter in $studyLevel.txtLetter.getSiblings())
            <ul>
              <li class="dropdown-header">$letter.getData()</li>
                #foreach ($course in $letter.txtCourseTitle.getSiblings())
                  <li><a href="/-/$course.txtFriendlyUrl.data">$course.getData()</a></li>
                #end
            </ul>
          #end
        </div>
      </div>
    #end
  </div>
</div>
 

Theme Navigation Trigger

Now, hook up your megamenu content to your theme navigation.
 

navigation.vm

...

#foreach ($nav_item in $nav_items)
#if ($velocityCount == 3) ### drop the COURSES megamenu item here
#parse ("$full_templates_path/megamenu.vm")

#end
...

 

megamenu.vm

#set ($journalContentUtil = $utiLocator.findUtil("com.liferay.portlet.journalcontent.util.JournalContentUtil"))
#set ($content = $journalContentUtil.getContent( $themeDisplay.getSiteGroupId(), $propsUtil.get("megamenu.article.id"), null, $locale.toString(), $themeDisplay ) )
 
<li class="navlink-courses dropdown mega-dropdown visible-md visible-lg visible-xl" id="layout_megamenu" $nav_item_attr_selected role="presentation">
        <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">Courses <i class="icon icon-chevron-down icon-sm"></i></a>
        <div class="dropdown-menu mega-dropdown-menu" role="tabpanel">
                $content
        </div>
</li>
<li class="navlink-courses hidden-md hidden-lg hidden-xl"><a href="#">Courses</a></li>
 

What we have so far

  • A megamenu content structure
  • A megamenu content template
  • A megamenu content item (journal article) tied to that structure and template
What is still worrisome is the craziness involved in manually updating our megamenu content item. This is where the Liferay Search API comes to the rescue. But first, a quick note on the megamenu XSD.
 

The Megamenu XSD 

The Megamenu Content XML

If you examine the content in the database of your megamenu journal article, you will find something similar to the below.
 
<?xml version="1.0"?>
<root available-locales="en_US" default-locale="en_US">
    <dynamic-element name="txtQualificationLevel" type="text" index-type="keyword" index="0">
        <dynamic-element name="txtIdentifier" index="0" type="text" index-type="keyword">
            <dynamic-content language-id="en_US"><![CDATA[all-courses]]></dynamic-content>
        </dynamic-element>
        <dynamic-element name="txtLetter" index="0" type="text" index-type="keyword">
            <dynamic-element name="txtCourseTitle" index="0" type="text" index-type="keyword">
                <dynamic-element name="txtFriendlyUrl" index="0" type="text" index-type="keyword">
                    <dynamic-content language-id="en_US"><![CDATA[advanced-certificate-in-management]]></dynamic-content>
                </dynamic-element>
                <dynamic-content language-id="en_US"><![CDATA[Advanced Certificate in Management]]></dynamic-content>
            </dynamic-element>
            <dynamic-element name="txtCourseTitle" index="1" type="text" index-type="keyword">
                <dynamic-element name="txtFriendlyUrl" index="1" type="text" index-type="keyword">
                    <dynamic-content language-id="en_US"><![CDATA[advanced-certificate-in-financial-planning]]></dynamic-content>
                </dynamic-element>
                <dynamic-content language-id="en_US"><![CDATA[Advanced Certificate In Financial Planning]]></dynamic-content>
            </dynamic-element>
            <dynamic-content language-id="en_US"><![CDATA[A]]></dynamic-content>
        </dynamic-element>
        <dynamic-element name="txtLetter" index="1" type="text" index-type="keyword">
            <dynamic-element name="txtCourseTitle" index="2" type="text" index-type="keyword">
                <dynamic-element name="txtFriendlyUrl" index="2" type="text" index-type="keyword">
                    <dynamic-content language-id="en_US"><![CDATA[bachelor-of-commerce-bcomm-]]></dynamic-content>
                </dynamic-element>
                <dynamic-content language-id="en_US"><![CDATA[Bachelor of Commerce (BCOM)]]></dynamic-content>
            </dynamic-element>
            <dynamic-content language-id="en_US"><![CDATA[B]]></dynamic-content>
        </dynamic-element>
        <dynamic-content language-id="en_US"><![CDATA[All Courses]]></dynamic-content>
    </dynamic-element>
    <dynamic-element name="txtQualificationLevel" type="text" index-type="keyword" index="1">
        <dynamic-element name="txtIdentifier" index="1" type="text" index-type="keyword">
            <dynamic-content language-id="en_US"><![CDATA[undergraduates]]></dynamic-content>
        </dynamic-element>
        <dynamic-element name="txtLetter" index="2" type="text" index-type="keyword">
            <dynamic-element name="txtCourseTitle" index="3" type="text" index-type="keyword">
                <dynamic-element name="txtFriendlyUrl" index="3" type="text" index-type="keyword">
                    <dynamic-content language-id="en_US"><![CDATA[advanced-certificate-in-management]]></dynamic-content>
                </dynamic-element>
                <dynamic-content language-id="en_US"><![CDATA[Advanced Certificate in Management]]></dynamic-content>
            </dynamic-element>
            <dynamic-content language-id="en_US"><![CDATA[A]]></dynamic-content>
        </dynamic-element>
        <dynamic-content language-id="en_US"><![CDATA[Undergraduates]]></dynamic-content>
    </dynamic-element>
    <dynamic-element name="txtQualificationLevel" type="text" index-type="keyword" index="2">
        <dynamic-element name="txtIdentifier" index="2" type="text" index-type="keyword">
            <dynamic-content language-id="en_US"><![CDATA[postgraduates]]></dynamic-content>
        </dynamic-element>
        <dynamic-element name="txtLetter" index="3" type="text" index-type="keyword">
            <dynamic-element name="txtCourseTitle" index="4" type="text" index-type="keyword">
                <dynamic-element name="txtFriendlyUrl" index="4" type="text" index-type="keyword">
                    <dynamic-content language-id="en_US"><![CDATA[higher-certificate-in-management]]></dynamic-content>
                </dynamic-element>
                <dynamic-content language-id="en_US"><![CDATA[Higher Certificate In Management]]></dynamic-content>
            </dynamic-element>
            <dynamic-content language-id="en_US"><![CDATA[H]]></dynamic-content>
        </dynamic-element>
        <dynamic-element name="txtLetter" index="4" type="text" index-type="keyword">
            <dynamic-element name="txtCourseTitle" index="5" type="text" index-type="keyword">
                <dynamic-element name="txtFriendlyUrl" index="5" type="text" index-type="keyword">
                    <dynamic-content language-id="en_US"><![CDATA[master-of-business-administration-mba-]]></dynamic-content>
                </dynamic-element>
                <dynamic-content language-id="en_US"><![CDATA[Master of Business Administration (MBA)]]></dynamic-content>
            </dynamic-element>
            <dynamic-content language-id="en_US"><![CDATA[M]]></dynamic-content>
        </dynamic-element>
        <dynamic-content language-id="en_US"><![CDATA[Postgraduates]]></dynamic-content>
    </dynamic-element>
    <dynamic-element name="txtQualificationLevel" type="text" index-type="keyword" index="3">
        <dynamic-element name="txtIdentifier" index="3" type="text" index-type="keyword">
            <dynamic-content language-id="en_US"><![CDATA[ce]]></dynamic-content>
        </dynamic-element>
        <dynamic-element name="txtLetter" index="5" type="text" index-type="keyword">
            <dynamic-element name="txtCourseTitle" index="6" type="text" index-type="keyword">
                <dynamic-element name="txtFriendlyUrl" index="6" type="text" index-type="keyword">
                    <dynamic-content language-id="en_US"><![CDATA[postgraduate-diploma-in-banking]]></dynamic-content>
                </dynamic-element>
                <dynamic-content language-id="en_US"><![CDATA[Postgraduate Diploma in Banking]]></dynamic-content>
            </dynamic-element>
            <dynamic-content language-id="en_US"><![CDATA[P]]></dynamic-content>
        </dynamic-element>
        <dynamic-content language-id="en_US"><![CDATA[Continuing education]]></dynamic-content>
    </dynamic-element>
</root>
 
This content may look overwhelming at first, but it is sickeningly simplistic, and hence very flexible. This is all you need to know.
  1. The root element has one or more dynamic-element elements.
  2. Each dynamic-element can have one dynamic-content element and one or more dyamic-element elements.
  3. And recursion. That's it.
 

The XML Schema Definition

Use any of numerous tools to create an XML Schema Definition for this XML structure. Once you have an XSD, you can then use any XML Schema binding framework (XML Beans, JAXB, etc) to convert between an object model and an XML document that conforms to said schema. This is powerful now.
 

Putting Search to work

There are many ways to do this, but we chose to implement a simple admin portlet with a single button on it. Clicking the button does the following work.
  1. Fire a search to bring back all content of type course
  2. Sort the courses
  3. Partition the courses into four buckets corresponding to the four tabs on the megamenu. We use a custom field called studyLevel for each course that serves as the discriminator for the tabs
  4. Organize the courses into the object model that is convertible to our Megamenu content XML document.
  5. Convert the object model to XML
  6. Retrieve the megamenu content item
  7. Update it with the XML
  8. Done.

So why even do it this way?

  1. The content that populates the megamenu is web content, so updating the megamenu is as simple as updating a content item. Happens on the fly.
  2. We wanted our megamenu to be extremely performant, so we preferred to just load a content item and thus reuse any Liferay caching that already services existing content
  3. We wanted a way to refresh the content on demand. E.g. fire a search and refresh the content item after adding one or more courses.
  4. We wanted to leverage Liferay's cotnent versioning so we can examine the content evolution of our megamenu and use as needed.

Some useful code snippets follow. I hope someone finds this approach helpful.

Useful Code Snippets

Faceted Search Configuration

{"facets": [
    {
        "displayStyle": "asset_entries",
        "static": true,
        "weight": 0.25,
        "order": "OrderHitsDesc",
        "data": {
            "values": ["com.liferay.portlet.journal.model.JournalArticle"],
            "frequencyThreshold": 0
        },
        "className": "com.liferay.portal.kernel.search.facet.AssetEntriesFacet",
        "label": "asset-type",
        "fieldName": "entryClassName"
    },
    {
        "displayStyle": "asset_tags",
        "weight": 0.25,
        "static": true,
        "order": "OrderHitsDesc",
        "data": {
            "values": ["course"],
            "frequencyThreshold": 0
        },
        "label": "asset-type",
        "className": "com.liferay.portal.kernel.search.facet.MultiValueFacet",
        "fieldName": "type"
    }
]}
 

Calling into the Search API

String searchConfiguration = portletPreferences.getValue("jsonFacetedSearchConfigurationString", StringPool.BLANK);
 
import com.liferay.portal.kernel.search.Document
import com.liferay.portal.kernel.search.SearchContext
import com.liferay.portal.kernel.search.QueryConfig
import com.liferay.portal.kernel.search.FacetedSearcher
import com.liferay.portal.kernel.search.facet.AssetEntriesFacet
import com.liferay.portal.kernel.search.facet.Facet
import com.liferay.portal.kernel.search.facet.ScopeFacet
import com.liferay.portal.kernel.search.facet.config.FacetConfiguration
import com.liferay.portal.kernel.search.facet.config.FacetConfigurationUtil
import com.liferay.portal.kernel.search.facet.util.FacetFactoryUtil
import com.liferay.portal.kernel.search.Field
import com.liferay.portal.kernel.search.Hits
import com.liferay.portal.kernel.search.Indexer
import com.liferay.portal.kernel.search.IndexerRegistryUtil
import com.liferay.portal.kernel.search.QueryConfig
import com.liferay.portal.kernel.search.SearchContext
import com.liferay.portal.kernel.search.SearchContextFactory
import com.liferay.portal.kernel.search.SearchResultUtil
 
List<Document> documents = null;
SearchContext searchContext = SearchContextFactory.getInstance(request);
searchContext.setAttribute("paginationType", "none");
 
QueryConfig queryConfig = new QueryConfig();
searchContext.setQueryConfig(queryConfig);
 
searchContext.setStart(1);
//searchContext.setEnd(); // I am hoping NOT setting this would work as paginationType is set to "none"
 
Facet assetEntriesFacet = new AssetEntriesFacet(searchContext);
assetEntriesFacet.setStatic(true);
searchContext.addFacet(assetEntriesFacet);
Facet scopeFacet = new ScopeFacet(searchContext);
scopeFacet.setStatic(true);
searchContext.addFacet(scopeFacet);
 
List<FacetConfiguration> facetConfigurations = FacetConfigurationUtil.load(undergraduateSearchConfiguration);
for (FacetConfiguration facetConfiguration : facetConfigurations) {
    Facet facet = FacetFactoryUtil.create(searchContext, facetConfiguration);
    searchContext.addFacet(facet);
}
Indexer indexer = FacetedSearcher.getInstance();
Hits hits = indexer.search(searchContext);
documents = hits.toList()
 

Customizing Liferay search results with a JSP hook & a content template

General Blogs November 30, 2015 By Javeed Chida

I'd like to point out at the outset that the content of this post may be mundane for the seasoned Liferay developer. What I really want to demonstrate is the ease with which the problem (defined below) can be solved quickly and easily through use of the portal's architectural mechanisms and APIs.

The Problem

Customize the out-of-the-box Search portlet's presentation of results to show each row as a course card; note that our searches are restricted to course content only.

A duly smudged-out page specification is shown below.

 

Each course card in the above is one search resultWhen I first came across this requirement, I hopped onto my local portal instance and dropped the Liferay out-of-the-box Search portlet onto a page. Here's what you would see (taken from the Liferay Developer Network).

 

Two major gaps that needed bridging jumped out at me:

  • The search criteria had to be moved to the right-hand-side.
  • The list of results was row-wise, but had to be transformed into a course card view, with each course item corresponding to a row.

A quick examination of the portlet configuration did not seem helpful. So I made the foregone conclusion that we would need to write a custom search portlet. We had the HTML from the designers, we had the necessary APIs at our disposal. We even had the codebase of the search portlet as a reference for the faceted search implementation as well as the search criteria presentation. All set to get started.

But our estimates were far less than ideal. We needed this thing implemented fast!

Hooked on a Solution

And then I read up on hooks, in particular the JSP hook. Using a JSP hook, I could alter any code in the actively deployed search portlet, running here: [liferay]/webapps/ROOT/html/portlet/search

So I created the hook and added search.jsp to my META-INF (more on hooks here). After about 30 minutes of studying search.jsp and one simple tweak, the search criteria was on the right-hand-side. Great! 

So, the only challenge left now was the resultset presentation.

A Template called Course Card

I had to disengage from the search portlet and focus on the content architecture of the site. An increased familiary with structures and templates ensued. Among these, I had a course structure and a course template.

Pretty soon, I found myself coding up an Application Display Template (ADT) for use with an Asset Publisher portlet instance. The goal was to show courses that matched certain critera. In this template, I iterate through the assets (essentially course content items) and for each asset, I make xpath calls to acquire the various attributes of the course into a bunch of variables. And then, using an HTML fragment I was handed by the designers, I replaced the data placeholders with the corresponding variables. I called the ADT Course Card. It looked something like the below.

The ADT

<div class="row-fluid">
<div class="span12">
<h2 class="heading-secondary">Courses</h2>
</div>
</div>

#set ($rowcounter = 0)
#if (!$entries.isEmpty())
    <div class="row-fluid">
#foreach ($curEntry in $entries)
   #set ($rowcounter = $rowcounter + 1)
   #set( $renderer = $curEntry.getAssetRenderer() )
   #set( $link = $renderer.getURLViewInContext($renderRequest, $renderResponse, ''))
   #set( $journalArticle = $renderer.getArticle() )
   #set ($articlePrimKey = $journalArticle.getResourcePrimKey())
   #set ($catLocalService = $serviceLocator.findService("com.liferay.portlet.asset.service.AssetCategoryLocalService"))
   #set ($catPropertyLocalService = $serviceLocator.findService("com.liferay.portlet.asset.service.AssetCategoryPropertyLocalService"))
   #set ($articleCats = $catLocalService.getCategories("com.liferay.portlet.journal.model.JournalArticle", $articlePrimKey))
   #set ($qualificationLevelAttribute = $journalArticle.getExpandoBridge().getAttribute("qualificationLevel"))
   #set ($studyModeAttribute = $journalArticle.getExpandoBridge().getAttribute("studyMode"))
   #set ($locationAttribute = $journalArticle.getExpandoBridge().getAttribute("location"))
   #set ($primaryFaculty = $journalArticle.getExpandoBridge().getAttribute("primaryFaculty"))
   #set( $document = $saxReaderUtil.read($journalArticle.getContent()) )
   #set( $rootElement = $document.getRootElement() ) 

   ### get any content fields you need using XPath
   #set( $courseNameSelector =  $saxReaderUtil.createXPath("dynamic-element[@name='txtCourseName']") )
   #set( $courseName =  $courseNameSelector.selectSingleNode($rootElement).getStringValue() )

   ### get any custom fields as below
   #set ($durationMax = $journalArticle.getExpandoBridge().getAttribute("durationMax"))
   #set ($durationMin = $journalArticle.getExpandoBridge().getAttribute("durationMin"))

####### skipping a bunch of irrelevant code that extracts several other fields and custom fields into various variables. ########

################# Display the course data in a card format ###########################
<div class="span4 block-vspace">
<div class="card-container $cardCss.value">
<a href="$link" class="card">
      <div class="card-content-container">
       <p class="card-category-title">$faculty</p>
       <div class="card-content">
      <h4 class="card-title fade in">$courseName</h4>
      <p><strong>$qualificationLevels</strong></p>
      <p class="small">NQF Level $nqfLevel, SAQA No. $saqaNumber</p>
      <p>$curEntry.summary</p>
      <hr>
      <p class="small">Offered as: <strong>$studyModes</strong></p>
      <p class="small">Minimum duration: <strong>$durationMin</strong><br>
      Maximum duration: <strong>$durationMax</strong></p>
      <p class="small"><strong>$locations</strong></p>
     </div>
     <hr class="card-horizontal-rule">
     <button class="card-btn-link btn btn-link btn-block"><strong>View course <i class="fa fa-chevron-right fa-sm"></i></strong></button>
   </div>
    </a>
  </div>
</div>
    #if ($rowcounter % 3 == 0)
       </div>
        <div class="row-fluid"> &nbsp; </div>
        <div class="row-fluid">
    #end
#end
</div> 
#end

Hmm. Now, that was what I wanted each of my search results to look like. The difference was I wouldn't be working within the Asset Publisher's query framework, but rather within the search portlet's.

I already had web content items (aka JournalArticles) for the courses. And I already had a Course Detail template (a web content template, not to be confused with ADT) that showed ALL of the course details in one presentation. I could now craft a new Course Card content template that shadowed my Course Card ADT. You can see how this was a bit backwards in that the ADT was more complex to code up. The content template to display the course card was more straightforward because I had easy acces to the fields in the course type structure. This is what my Course Card content template looked like. Notice the similarity barring the XPATH syntax in the ADT to retrieve content fields.

The content template

############### get vocabulary ID's ####################
#set ($vocabularyLocalService = $serviceLocator.findService("com.liferay.portlet.asset.service.AssetVocabularyLocalService"))
#set ($vocabularies = $vocabularyLocalService.getGroupVocabularies($getterUtil.getLong($groupId)))

### we are interested in 2 specific vocabularies - Schools and StudyLevels
#foreach($voc in $vocabularies)
    #if ($voc.name == "Faculty")
        #set ($facultyVocabularyId = $voc.vocabularyId)
    #end
#end

#set ($journalArticleLocalService = $serviceLocator.findService("com.liferay.portlet.journal.service.JournalArticleLocalService"))
#set ($journalArticle = $journalArticleLocalService.getArticle($getterUtil.getLong($groupId), $reserved-article-id.data))
#set ($articlePrimKey = $journalArticle.getResourcePrimKey())
#set ($catLocalService = $serviceLocator.findService("com.liferay.portlet.asset.service.AssetCategoryLocalService"))
#set ($catPropertyLocalService = $serviceLocator.findService("com.liferay.portlet.asset.service.AssetCategoryPropertyLocalService"))
#set ($articleCats = $catLocalService.getCategories("com.liferay.portlet.journal.model.JournalArticle", $articlePrimKey))
#set ($qualificationLevelAttribute = $journalArticle.getExpandoBridge().getAttribute("qualificationLevel"))
#set ($studyModeAttribute = $journalArticle.getExpandoBridge().getAttribute("studyMode"))
#set ($locationAttribute = $journalArticle.getExpandoBridge().getAttribute("location"))
#set ($primaryFaculty = $journalArticle.getExpandoBridge().getAttribute("primaryFaculty"))
#set ($durationMin = $journalArticle.getExpandoBridge().getAttribute("durationMin"))
#set ($durationMax = $journalArticle.getExpandoBridge().getAttribute("durationMax"))

####### skipping a bunch of irrelevant code that extracts several other fields and custom fields into various variables. ########

################# Display the course card data ###########################

<div class="span4 block-vspace">
<div class="card-container $cardCss.value">
<a href="/-/$reserved-article-url-title.data" class="card">
      <div class="card-content-container">
       <p class="card-category-title">$faculty</p>
       <div class="card-content">
      <h4 class="card-title fade in">$txtCourseName.getData()</h4>
      <p><strong>$qualificationLevels</strong></p>
      <p class="small">NQF Level $txtNqfLevel.getData(), SAQA No. $txtSaqaNumber.getData()</p>
      <p>Designed to offer well-balanced exposure to the knowledge, skills and attitudes required to operate effectively in a general business environment.  This qualification offers three majors, namely, Business Management, Marketing and Human Resources.</p>
      <hr>
      <p class="small">Offered as: <strong>$studyModes</strong></p>
      <p class="small">Minimum duration: <strong>$durationMin</strong><br>
      Maximum duration: <strong>$durationMax</strong></p>
      <p class="small"><strong>$locations</strong></p>
     </div>
     <hr class="card-horizontal-rule">
   <button class="card-btn-link btn btn-link btn-block" onclick="window.location.href='/-/$reserved-article-url-title.data;'"><strong>View course <i class="icon icon-chevron-right fa-sm"></i></strong></button>
   </div>
    </a>
  </div>
</div>

With that in place, we were left with the task of using it, i.e retrieving the content (easy) and rendering it using this template from within the search portlet.

Back to the Search JSP hook, and Making it all Work

The only file I needed to touch was main_search.jspf. These were the tweaks.

  1. Fixed the search container delta to 9 so each page had no more than 9 results, after which default pagination would kick in.
  2. Added a scriptlet into the row container to load the JournalArticle for each course in the iteration.
  3. Retrieved the content string after transforming the course JournalArticle using the Course Card content template. The API method that does this for you is JournalArticleLocalServiceUtil.getArticleContent.

  4. Write out the content to the response.
 
These tweaks are highlighted in the below excerpt from main_search.jspf.
 
<%
SearchContainer mainSearchSearchContainer = new SearchContainer(renderRequest, null, null, SearchContainer.DEFAULT_CUR_PARAM, 9, portletURL, null, null);

SearchContext searchContext = SearchContextFactory.getInstance(request);

mainSearchSearchContainer.setEmptyResultsMessage(LanguageUtil.format(pageContext, "no-results-were-found-that-matched-the-keywords-x", "<strong>" + HtmlUtil.escape(searchContext.getKeywords()) + "</strong>"));
...
...
...
<aui:col cssClass="result" first="<%= !showMenu %>" span="10">
<%@ include file="/html/portlet/search/main_search_suggest.jspf" %>

<liferay-ui:search-container
searchContainer="<%= mainSearchSearchContainer %>"
total="<%= hits.getLength() %>"
>

<liferay-ui:search-container-results
results="<%= hits.toList() %>"
/>

<liferay-ui:search-container-row
className="com.liferay.portal.kernel.search.Document"
escapedModel="<%= false %>"
keyProperty="UID"
modelVar="document"
stringKey="<%= true %>"
>

</liferay-ui:search-container-row>
<div class="row-fluid">
           <c:forEach items="${hitsVar}" var="doc"  varStatus="counter">
               <%
                       int counter = 0;
                       Document docItem = (Document) pageContext.getAttribute("doc");
                       String idString = docItem.getField(com.liferay.portal.kernel.search.Field.ROOT_ENTRY_CLASS_PK).getValues()[0];
                       long articleId = new Long(idString).longValue();
                       com.liferay.portlet.journal.model.JournalArticle article = com.liferay.portlet.journal.service.JournalArticleLocalServiceUtil.getLatestArticle(articleId);
                       String ddmTemplateKey = com.liferay.portal.kernel.util.PropsUtil.get("search.coursecard.templatekey");
                       String content = null;
                       try{
                               content = com.liferay.portlet.journal.service.JournalArticleLocalServiceUtil.getArticleContent(article, ddmTemplateKey, "view", "en_US", themeDisplay);
                       }catch(com.liferay.portlet.journal.NoSuchArticleException e){
                               System.out.println("---> no article found with id " + idString);
                       }
               %>
               <%= content %>
               <c:if test="${counter.count mod 3 == 0}">
                       </div>
                       <div class="row-fluid"> &nbsp; </div>
                       <div class="row-fluid">
               </c:if>
           </c:forEach>
       </div>
<br style="clear:both"/>
<liferay-ui:search-iterator type="more" />
...
...
</liferay-ui:search-container>
</aui:col>

That's it!

In Conclusion

The bulk of the work done here was to:

  • code a content template for each result (corresponding to a row in the search results)
  • modify main_search.jspf (through a hook) to retrieve the content markup from the JournalArticle, apply the new content template to it and include the result in the response

What you need to know

This is a quick (very quick) solution and should be good for the most part. But you need to know that Liferay 7 will not support JSP hooks. There is talk of (hopefully) a Migration Assistant that will transform the hook into its own OSGI module. However, my impression is that we should plan ahead and design/implement a custom portlet that incorporates the functionality of our hook(s). That way, when an upgrade to Liferay 7 knocks on our door, we'll actually be ready to invite it in.

Hope this was useful.

LRNAS2015 left me cautiously craving a 7.0 upgrade

General Blogs November 25, 2015 By Javeed Chida

LRNAS2015 was everything I hoped it would be. Every session was packed with demos of upcoming 7.0 features that left anyone using 6.2 very grateful (I was) that they weren't on an even older version. Upgrading for 6.2 won't go unaided, but will require some careful planning and testing.

Here is a small slice of new features I am looking forward to with equal parts interest and caution.

  • Senna.js as Liferay's in-portlet SPA framework. New York University extended Senna for their entire portal experience.
  • Switch to osgi bundles. Expect JSP hooks to break on an upgrade. There is the promise of a Liferay 7 Upgrade Assistant that will ease the conversion of such hooks into OSGI modules. There will be support for migration of velocity templates as well. Still recommended to manually rewrite any JSP hooks as portlet modules to prepare for an upgrade to 7.0
  • Benefits of upgrading portlets to OSGI modules: true hot deploy, true sharing of the same classloader
  • Liferay IDE 3 will make a shift from plugins to modules
  • Liferay IDE 3 deploys modules to OSGI container directly. LARs will be replaced by WABs (Web Application Bundles).
  • Brand new Forms content type with support for multi-page forms. There are also plans in the future to incorporate workflows into the forms.
  • New Alloy Editor is impressive, to say the least, with WYSIWYG editing that delivers.
  • Elastic search replacing lucene, faster search performance, recommended to run in its own JVM.
  • Search API has changed. This tells me I need to encapsulate the calls we make into the 6.2 Search API in a facade to insulate our portlets from this change.
  • Faceted search will support the selection of multiple values for a given facet. (Facets in 6.2 only allow the selection of one value within a facet.)

I'll be looking out for any 7.0 Upgrade Checklists that emerge in the coming months. 

The other vendor products that caught my fancy were Valamis for VLE and Dynatrace for performance monitoring. The Vaadin 7 demo was eye-popping and would be my framework choice if we ever need to do a dashboard style application.

Showing 17 results.
Items 20
of 1