Journal VM Template meets SAXReaderUtil

Recently my friend Journal VM Template was feeling a little down in the dumps because of his lack of ability when it comes to handling XML content. He was feeling inferior to Journal XSL Template because of this one's superlative ability in dealing with any type of XML content, local and remote.

Not liking to see my friend down in the dumps, because he really has many other likeable features, I decided that it was time for an intervention. I got together with a new friend I'd recently started working with, SAXReaderUtil. SAXReaderUtil is a new and upcomming character around the Liferay codebase, and has tremendous skill when if comes to dealing with XML content.

He can easily get XML from Strings, Files, java.net.URLs, java.io.Readers, InputStreams, and most recently from a plain old String URLs. Not only that, but he can get you Documents with one line, can handle XPATH, do Node sorting, etc.. all in very little code.

Not only that, but all his buddies, Element, Node, and Branch (because he's a relative of dom4j) are all equally great to work with and very skilled.

Well, I'll show some code in a minute, but what I really wanted to tell you was that, I introduced SAXReaderUtil to Journal VM Template and they are getting along very well. So well, in fact, that I'm itching to show you some of the cool stuff these two can do now that they are working together.

Ok, so let's make a new Document from scratch:

#set ($document = $saxReaderUtil.read("<friends/>"))
#set ($root = $document.getRootElement())
#set ($friends = ["Journal VM Template", "SAXReaderUtil", "Document", "Element", "Branch", "Node"])
#foreach ($friend in $friends)
  #set ($friendEl = $root.addElement("friend"))
  #set ($friendEl = $friendEl.addText($friend))
#end

<pre>$htmlUtil.escape($document.asXML())</pre>

The output should look something like this (but not pretty printed, I did that):

<?xml version="1.0" encoding="UTF-8"?>
<friends>
  <friend>Journal VM Template</friend>
  <friend>SAXReaderUtil</friend>
  <friend>Document</friend>
  <friend>Element</friend>
  <friend>Branch</friend>
  <friend>Node</friend>
</friends>

Cool eh?

Ok, now suppose we mocked up a whole doc full of content and we want to provide it to some external client, an RSS reader, as a response to an AJAX call, etc.

First off, make sure that the article using the template is on a public page (so it's not gonna give you permission problems to start with). Next, lets add a little bit more code to make this content available as a regular old feed.

#set ($document = $saxReaderUtil.read("<friends/>"))
#set ($root = $document.getRootElement())
#set ($friends = ["Journal VM Template", "SAXReaderUtil", "Document", "Element", "Branch", "Node"])
#foreach ($friend in $friends)
  #set ($friendEl = $root.addElement("friend"))
  #set ($friendEl = $friendEl.addText($friend))
#end

#if ($request.window-state != "exclusive")
  <pre>$htmlUtil.escape($document.asXML())</pre>
  <a href="${request.render-url-exclusive}">FEED ME</a>
#else
$document.asXML()
#end

Now, click the "FEED ME" link and see what you get... (view source...)

That's right.. Whoop! Whoop!! Who's your Daddy???

That effectively turns your regular old templates into a relatively simple service end point. Tie that together with the ability to call Liferay's underlying services and you can publish any kind of feed you like... could even provide reports for some remote system which process some of your data blah blah... you get the idea.

But what if you want the template to be a consumer of some data in XML and produce a view of that data. A common scenario is a custom Journal Article list. Let's write some code to do that.

#set ($document = $saxReaderUtil.readURL("http://localhost:8080/lportal/c/journal/get_articles?groupId=14&delta=1"))

<textarea style="height: 800px; width: 500px;">
$document.asXML()
</textarea>

So, I'm making an API call to the backend Journal API to get the most recent article (delta=1) updated in the community with groupId=14. So first off, it's worth noting that it was easy to make the call, we just used the readURL method. Next, we're just dumping the contents into a textarea. This is the first step to see if we have good data to work with. It should look something like this.

Ok, so we were talking about our new friend SAXReaderUtil. It seems he's doing a fine job so far. Let's see what other tricks him and his buddies provide.

Let's get the list of articles using XPATH (of course we have one... but you'd likely have more right?).

#set ($document = $saxReaderUtil.readURL("http://localhost:8080/lportal/c/journal/get_articles?groupId=14&delta=1"))
#set ($root = $document.getRootElement())
#set ($articles = $root.selectNodes("/result-set/result/root"))

<textarea style="height: 800px; width: 500px;">
$articles.get(0).asXML()
</textarea>

Now we know our list is right, we can iterate through each article and get some details about each one.

#set ($document = $saxReaderUtil.readURL("http://localhost:8080/lportal/c/journal/get_articles?groupId=14&delta=1"))
#set ($root = $document.getRootElement())
#set ($articles = $root.selectNodes("/result-set/result/root"))

<ul>
#foreach ($article IN $articles)
#set ($articleId = $article.selectSingleNode("dynamic-element[@name='reserved-article-id']/dynamic-content"))
#set ($articleTitle = $article.selectSingleNode("dynamic-element[@name='reserved-article-title']/dynamic-content"))
#set ($articleModifiedDate = $article.selectSingleNode("dynamic-element[@name='reserved-article-modified-date']/dynamic-content"))
#set ($articleAuthorName = $article.selectSingleNode("dynamic-element[@name='reserved-article-author-name']/dynamic-content"))
<li>
<a href="${request.render-url-maximized}${request.portlet-namespace}articleId=${articleId.text}">${articleTitle.text}</a>
<br/>
<span style="font-size: smaller;">${articleAuthorName.text}, ${articleModifiedDate.text}</span>
#end
</ul>

What we get is this:

Ok, we're only showing one... how does it look with more. All we do is modify the API query by setting the delta parameter to something higher. Let's pick 4.

Now notice that the URL's actually work, placing the given article in Maximized mode. You can choose to not go maximized if you like (use ${request.render-url} instead of ${request.render-url-maximized}. And if you use a friendly URL you can target another portlet on the page completely, using the traditional "narrow-side-nav-links vs. wide-side-view-port" model.

Anyway, It's pretty impressive what you can accomplish when friends work together. I'd suggest visiting with Journal VM Template and SAXReaderUtil & Co. as soon as you can take Liferay 5.1.2 for a spin (5.1.x until the official release). All my friends are enjoying their new acquaintances. I hope you enjoy them too.

Blogs
Oh Ray, that's really cool! I always enjoy your blogs but this was one was great!
What did your friends get you into...There's a world of possibility with this. Nice Ray.
Ray, that's really fine. may be you could help me with the following questions:

<a href='http://www.liferay.com/web/guest/community/forums/-/message_boards/message/1386734'>http://www.liferay.com/web/guest/community/forums/-/message_boards/message/1386734</a>

Thanks in advance.
Oh Ray, that's a beautifull job ... its nice to share this with us!
Hey Ray - I am implementing this on 5.2, and I'm seeing some strange behavior with the way the links are handled. Put simply, they're not loading the articles with the code above...just refreshing the journal content in maximized view. Has something changed in the way these are handled?
FYI, I tried this out at 5.1.2 and found the same behavior with render-url. I was able to get it working through other means, though. Another question, though: is it possible to pull journal articles by tag ID in this manner? I'm trying to create a stripped-down version of the Asset Publisher using this mechanism that will allow me to style the content in any way that I want. Should be cool if I can figure this last part out!
Is there any way to easily reverse the list of returned nodes? I know you can add a sort-XPath to the selectNodes function, like this:
#set ($articles = $root.selectNodes("/result-set/result/root", "dynamic-element[@name='date']/dynamic-content"))

This list returns me a sorted list of articles indeed, but in ascending order, while I want them in descending order. Is this possible in the sort-xpath parameter, or with any list util? (I don't find any reverse-like feature in the Liferay utils)

Sorting is one of those things that remains the easiest in XSL :-)
Dears, issue with Liferay 6.0.5 + Jboss Bundle.

below is the code snippet:

#set ($document = $saxReaderUtil.readURL("http://@portal_url@/c/journal/get_articles?groupId=@group_id@&type=general&delta=1&approved=true&expired=false&orderBy=display-date"))
#set ($root = $document.getRootElement())
##$root has the xml document object, if printed using $root.asXML(), it displays xml data

##from below code it does not work properly
#set ($articles = $root.selectNodes("/result-set/result/root"))

$articles is coming empty, how to proceed further. Appreciate if anyone can help with code snippet along with opening the article in maximized mode.
I've been trying to get back to you on this. To me it looks right! I have to setup a small test example.

I'll try by early next at the latest!
hi ray,

i already opened up new thread here
http://www.liferay.com/community/forums/-/message_boards/message/7681234?_19_preview=false

thanks
Hi Ray, thanks for respoding, any update on this? if this works it would save me creating new custom porlet for News emoticon
hi,

instead of clicking the link to display article, how do i display content?

#set ($contentC = $article.selectSingleNode("static-content[@language-id='en_US']/static-content"))

i try this but doesnt work, pls help.
sorry
this is the correct one
#set ($contentC = $article.selectSingleNode("static-content")­)

but the content does change according to languageId.
Thank you for this piece of code and those rich ideas ! Thanks for sharing this.
hi,
does anyone applied to 6.1 version?

mines not working.
hi, is it work on 6.1?

or

how I get articles in template on different way?

tnx
Well...if I do this:
#set ($document = $saxReaderUtil.readURL("http://@portal_url@/$!{request.locale}/c/journal/get_articles?groupId=@group_id@&type=news&templateId=FICHA_NOTICIA&delta=6&approved=true&expired=false&orderBy=display-date"))
#set ($root = $document.getRootElement())
#set ($articles = $root.selectNodes("/result-set/result/root"))

Fails!
But if I do:
#set ($document = $saxReaderUtil.readURL("http://localhost:8080/$!{request.locale}/c/journal/get_articles?groupId=@group_id@&type=news&templateId=FICHA_NOTICIA&delta=6&approved=true&expired=false&orderBy=display-date"))
#set ($root = $document.getRootElement())
#set ($articles = $root.selectNodes("/result-set/result/root"))

Works!
Note that the only change I made is change @portal_url@ constant for hardcoded URL 'localhost:8080'. I don't understand but works...
I forgot to say that in version 6.1.0 don't work. Version 6.0.6 works with @portal_url@.
Yes, unfortunately this is a known bug.

http://issues.liferay.com/browse/LPS-29202

Fortunately the fix is very simple: https://github.com/liferay/liferay-portal/commit/c54b27bfb8 (I believe this is also available in a post EE-GA2 fixpack).
[...] Liferay has an RSS reader portlet that is minimally configurable. You are not limited to this portlet, however. Using the SAXReaderUtil and Velocity you can fairly easily create your own. Check... [...] Read More
Excellent post! Thanks Ray!

I was parsing the $xmlRequest variable available in the templates in Liferay 6.2 first...but then I found that using the ${request.theme-display} call instead, I was able to retrieve the theme display so easily!

So my question here would be...what is the difference between using the ${request} variable rather than the $xmlRequest one? are they the same object? From my point of view (and sorry if I sound confused) using the ${request} variable is much easier than having to use the SaxReaderUtil...so why using an $xmlRequest variable instead?

A little clarification would help, but either way my code is working in my template. Anyways, I am glad that such a "classic" functionality from back in 2008 can still provide such an interesting value in the 6.2 version.

Regards
The $xmlRequest is a xml string consumable by the XSL template engine. You probably don't need it for template languages where you can use the $request hash.

HTH
Thanks for the quick answer. I will stick to the $request hash then. I was curious since the $xmlRequest appears as one of the variables available to get with a click on the left panel from the templates editor (Liferay 6.2 ga2 CE.).. So I imagined that I would have to use it to get variables from the request. But your post and comment clarified it, thanks!
Hi, I've had good luck using method this to read some external rss feeds with rss\channel\item in the xml but having problems with others that use feed\entry in the xml. Maybe it is the ATOM type\dom vs whatever that is a problem? An example of one I can't parse\read is http://weather.gc.ca/rss/city/on-69_e.xml. Any help how to read\parse that xml greater appreciated -- working only in velocity script. Thanks!
Seems like it should be parseable! Are you getting an error? Might just be that your parser logic is not tuned for this particular format? There are several RSS formats and versions of each format out there.
Hey Ray - thanks for fast reply (and awesome blogging in general); No errors, just don't seem to get anything back with selectNodes - tried a dozen things instead of feed\entry (inc nothing and //entry). Here is a snippet of code (it's a tab based RSS reader, was also gonna add paging). can send you whole script if there is a way)...
<code>
#foreach ($item in $feed.getSiblings())
<div id="tabs-$velocityCount">
#set ($feedDoc = $saxReaderUtil.readURL("$item.feedURL.getData()"))
#if ($item.feedNode.data == '1')
<p>got a 1 $item.feedURL.getData()</p>
#set ($feedItems = $feedDoc.selectNodes("/feed/entry"))
#else
#set ($feedItems = $feedDoc.selectNodes("/rss/channel/item"))
#end
<ul style="list-style-position: inside; white-space: nowrap; overflow: hidden;">
#if ($feedItems.isEmpty())
<p><li>Nothing to report.</li></p>
#end
#foreach ($feedItem in $feedItems)
<\code>
Ah, it's the schema! You need to do it like so:

/*[local-name() = feed]/*[local-name() = entry]

etc.

Or you have to use the XPath API to pass in namespace URI which would let you do something like:

x = http://www.w3.org/2005/Atom
/x:feed/x:entry

The Java code looks like this:

XPath xPath = SAXReaderUtil.createXPath("/x:feed/x:entry", "x", "http://www.w3.org/2005/Atom");
List<Node> nodes = xPath.selectNodes(rootElement);
Getting closer! - this statement is now finding the nodes and the foreach walks through them properly (pretty sure, count is right);
#set ($feedItems = $feedDoc.selectNodes("/*[local-name()='feed']/*[local-name()='entry']"))

To fetch the attributes (singlenode) out of it, I was using the statement below for regular xml but it does not work for this one (I tried putting the /*[ stuff in there where title is but no go;
(within foreach)
#set($entryTitle = $feedItem.selectSingleNode('title').getData())
Ideas?
The attributes are just as namespaced as the elements. Welcome to the joy that is xml parsing! emoticon
Tried something like this (but doesn't work; sorry to be so clueless);
#set($feedItemNode = $feedItem.selectSingleNode("/*[local-name()='title']"))
#set($entryTitle = $feedItemNode.getData())
attributes you handle like this:

@*[local-name()='name'

Also, you don't use the selectSingleNode for attributes. You use the attribute getters.