Recent Bloggers

Olaf Kock

Staff
78 Mensagens
26 de Agosto de 2014

Matti Tahvonen

3 Mensagens
26 de Agosto de 2014

Ronald Sarayudej

Staff
151 Mensagens
25 de Agosto de 2014

James Falkner

Staff
95 Mensagens
25 de Agosto de 2014

Paulo Fernandes

Staff
15 Mensagens
22 de Agosto de 2014

Scott Lee

Staff
3 Mensagens
21 de Agosto de 2014

Martin Yan

Staff
5 Mensagens
20 de Agosto de 2014

Miguel Ángel Pastor Olivar

Staff
15 Mensagens
19 de Agosto de 2014

Josiah "Duke" Harrist

Staff
1 Mensagens
13 de Agosto de 2014

James Min

Staff
30 Mensagens
12 de Agosto de 2014

Community Roundup

Company Blogs 15 de Novembro de 2011 Por James Falkner Staff

"Life would be so much easier if we only had the source code." - Author Unknown

Here we go again.  Once again I have failed to live up to my promises of a once-a-month roundup, and for that I apologize! It's been a very busy and exciting 6 weeks in our community, so I am here to give you a digest of the latest coolness that is the Liferay Community.  So please keep arms and legs inside the vehicle at all times, and sit back and enjoy the ride.  Let's do this thing!

  • I was fortunate enough to attend two of Liferay's symposiums last month, the Liferay Europe Symposium in Frankfurt, and the Liferay Spain Symposium in Madrid.  Met many awesome community members, attended a bunch of interesting sessions and activities, and generally had a great time learning from our European community contingent.  Slides from Europe and Slides from Spain are now available!
  • If you are near Rome this week, the final symposium of 2011 is taking place this Friday (November 18).  If you have a chance to attend the Liferay Italy Symposium, you will be pleasantly surprised at the highest ever quality content to date. 
  • We are coming up on the close of the Liferay 6.1 chapter in the epic book of Liferay.  On November 14 Liferay released its Beta-4 build, and it is now downloadable, but if you are participating in Community BugSquad, of course you have already reviewed the sneak-peek 6.1 builds, and several of your feedbacks/suggestions/fixes/improvements have already been incorporated, with more to come!  
  • You may have noticed a new celebrity blogger on liferay.com: Jan Gregor from Mimacom is kicking off the first of many Liferay Community Projects.  This project is implementing a Liferay/JRebel plugin to facilitate easy realtime, no-restart-needed deployment of apps on LIferay.  More notable though is that we at Liferay are going to elevate these kinds of community-driven "incubation" projects to a higher level of visibility on liferay.org, providing more collaboration tools (like user groups) and project status.  If you are interested in starting a new project (or housing an existing project) on liferay.org, contact community@liferay.com
  • Olaf has published several more Radio Liferay releases, the latest of which features several of our high profile community members discussing their contributions and thoughts on our community. Check out the Radio Liferay feed page for more details and links to the streams. Recent Episodes: [9] [8] [7] [6] [5] and [4]
  • Liferay's User Groups continue to expand, with new groups beginning in Portland, Portugal, Norway, and others.  Visit the User Groups page to see all the recent activity.
  • Speaking of User Groups, the Spain User Group had a recent LSUG Meeting in Seville and it was a very successful event (15 attendees).  You can read the minutes and if interested, there are upcoming meetings in Marid, Sevilla, Barcelona, and Alicante.
  • Liferay's Community Leadership Team held its quarterly meetup last month, with several interesting discussions regarding what's buzzing in the community.  Check out the meeting minutes and follow the action on the thread if interested!
  • Notable industry blogger Dana Blankenhorn has an interesting take on Liferay's business model and mission.  Dana gave a rousing talk about the history of open source at WCS, and has written several great articles on open source in general.
  • Bradley's Tips!  This is a great collection of tidbits of useful information. Recently I've seen a lot of these kinds of things coming up (like Ray's gists), I think we need a centralized place into which these are placed, for easy access and searching!  *adds idea to queue*
  • Liferay IDE 1.4 has been released, with support of the upcoming Liferay 6.1, and Tomcat 7, as well as a few other goodies. Check out Greg's blog for details.
  • Random website running Liferay: AXA IM Corporate Website!
  • Remember that "50 Liferay Interview Questions" post from a while back?  Well, now you have the answers!
  • As you look back on the Liferay Community, you can see many innovative contributions and outstanding achievements.  You can also see a large ball of abused and ignored bug reports!  The Liferay Community has deputized a team of experts to help with this: Community Verifiers!  And we need your help!  If you are interested, leave comments below!
  • Wasim shows us how to install Liferay on eApps cloud hosting.  Yet another example of Liferay's cloud-friendliness.  
  • If you missed Oracle OpenWorld 2011, Liferay demonstrated the new features of Liferay 6.1, and was mentioned as one of 12 "hot products to see".  Cool! Well.. Hot!
  • We continue to see Open Source displacing proprietary stacks worldwide.  TechRepublic Live in Louisville mentioned Liferay as one of those displacing forces.
  • Speaking of the spread of Open Source, Gartner's 2011 Magic Quadrant for Horizontal Portals has placed Liferay in the Leader's Quadrant for the second year.  The only open source leader!  More coverage from CMS Report.
  • Mika has posted an excellent set of slides describing Liferay's SAML 2.0 support.  
  • Integrating Liferay with JOSSO (an open source SSO solution) has never been easier with Liferay 6.1.  Check out Gianluca's blog for details.
  • Liferay coverage from PyCON.  A Python conference.  Talking about Java portals?  Nice!
  • We in the Liferay Community are working on a new structured program to more easily get new users over the initial learning hill of Liferay, and provide valuable educational content to all of our community, regardless of expertise level. We need your input to proceed.  How should we structure such a program?  Leave your comments in the thread and learn by teaching!
  • Several people have been playing with Liferay and Jelastic (yet another cloud service).  Some discussion has taken place on the forums, and Marina has taken the time to write down details of this deployment.  Have fun with this one.
  • A thought-provoking examination of portals, and when they make sense.  For this author, it did not, but it provides some interesting guidance about when it does.
  • Activa wrote up a nice piece on the 2011 Spain Symposium.  Activa has been very active in our community and is a valuable community parter for Liferay! 
  • Finally!  Some handy scripts that can be cut/pasted into Liferay's Control Panel scripting console.  Terry provides a nice way to clean up user.  Be careful, there's no going back :)
  • No idea who created this or why, but it sounds fantastic :)
  • I really like Liferay's support for Dynamic Data Lists.  It's been a long time coming and how it's here.  Check out Arvind's description of it, and try it out in the latest beta!
  • Often the bane of developers, writing tests is a crucial part of ensuring quality in your projects.  With Liferay, it's really easy to write selenium tests (we at Liferay use this extensively, along with Jenkins).  Steffen shows how to do it.
  • Your 15 minutes are here.  15 minutes to Liferay on Cloudbees!

Recent Blog Posts: Radio Liferay Episodes [9] [8] [7] [6] [5] [4], WYSIWYG Editors, Philosophy of Writing, Custom Tools for Script Engine, Monitoring through firewalls, Embedding Portlets in Themes, Community Excellence, Spring and OSGi, Liferay @ Gartner ITExpo, Git Tips from Igor, Community Verifier, Fireworks, and finally Ehcache Configuration.

Recent Wiki Updates: CMIS and Alfresco 4.0, Custom Fields, Pagination, Development Style, Mail Integration, SOAP Web Services, Application Adapters (always wondered what those were!), Translation Team, AlloyUI Forms, Faceted Search, Slimming Liferay, Google Analytics

That's all I have for now.  I will do a Holiday Edition next month, with some goodies for all.  Enjoy your day!

 

Liferay 2011 Community Excellence Awards

Company Blogs 20 de Outubro de 2011 Por James Falkner Staff

In addition to the annual "Partner of the Year" award, this year Liferay recognized its partners for their community achievements, awarding the Community Excellence Award.  We consider partners a valuable part of our community (just like we do with "regular" liferay.com denizens, Liferay's employees, and Liferay customers), and wanted to recognize their participation, independent of any revenue figures, deals closed, or any other other business factors.

What We Measured

The actual, super-secret formula has not been revealed in orer to keep things fair, but the formula involves several (over 20) contribution types, including the obvious ones like helpful forum posts (answers) and code contributions, but other non-obvious, yet highly valuable community metrics.  For the 2011 awards, we considered activity that occured between midnight, August 31st, 2010 and midnight, August 1st, 2011.

The Winners

The winning partners have a proven, well-rounded, and highly effective track record of being involved in the community and giving back.  They realize the benefits of open participation: 1) It ultimately helps their business operate more effeciently, because they are more knowledgable, 2) It improves their standing with other community members and clients, because they are seen as having the expertise enough to be able to help others, and 3) It moves the technology forward, by offering innovation that we alone cannot provide.

Here they are:

MIMACOM

MIMACOM is a leading provider in Europe for Enterprise Open Source solutions based on Java.  Headquartered in Bern, with offices in Zurich, Valencia, Stuttgart, and Rome, MIMACOM also makes regular contributions to projects other than Liferay, including ICEfaces, Spring, and Hibernate.  In 2010-2011, MIMACOM was particularly helpful with the 6.0 release (doing much pre-release testing), contributed plugins, and established the Austria and Slovakia user groups.

 

Savoir-faire Linux

Founded in 1999 with locations in Montreal, Quebec City, and Ottawa, Savoir-faire Linux is the reference for open-source software integration. They are the largest and most experienced Liferay integrator in Eastern Canada.  In 2010-2011, Savoir-faire contributed a huge number of open source plugins, and worked closely with the community and Liferay engineering to implement and document the faceted search feature in 6.1.  They also contributed many fixes for the 6.1 release, and we are delighted to have them be part of it all.

 

IBA CZ

IBA CZ is a fast growing IT service provider and development center of the IBA Group headquartered in the Czech Republic. They focus on up-to-date technologies including enterprise portals, enterprise Java, Business Intelligence, and Business Process Management to customers located in Central and Western Europe.  In 2010-2011, IBA CZ was very active in reporting and providing contributions for issues, participated in 100 papercuts, BugSquad, and Community Verifier, and established the Czech Republic User Group.

 

CIGNEX Datamatics

Based in Santa Clara, CIGNEX Datamatics is the world's leading provider of Open Source Enterprise Content Management and SOA & ESB consulting services and software solutions. Their customers include companies in the Media, Healthcare, Education, Government, Gaming and High Tech industry. In 2010-2011, the team had consistent community contributions in many different areas (forum answers, bug reports, plugin contributions, and more) owing to their Liferay Platform expertise.   They also hold 1 of only 4 coveted "Liferay Legend" status indicators.

 

Componence

With locations in Nederland, Ukraine and India, Componence has offered Internet, Social Media and Mobile solutions since 2001. With over 200 portal implementations, Componence is portal market leader in the Netherlands with technology focus on Java and portal.  In 2010-2011, Componence contributed plugins, established the Netherlands User Group, participated in BugSquad, 100 PaperCuts, Community Verifier, and holds 1 of only 4 coveted "Liferay Legend" status indicators. 

 

A big "Thank You!" and congratulations goes to all of our 2011 Community Excellence Award winners!

How Can We Win Next Year?

Think about how participation in Liferay's open source community can help you (see the above winners for some ideas).  Rather than taking an ad-hoc approach, build in community participation into your business model and resourcing plans.  Value will organically flow into the community and back to you in various tangible ways.  For example, you fix a bug, provide a translation, develop and contribute a new feature, or investigate and solve someone else's problem on the forums.  You have helped the community, and without knowing it, got trained in the process!  How cool is that?

 

Liferay 6.1 Beta Release

Company Blogs 14 de Outubro de 2011 Por James Falkner Staff

Liferay is proud to announce the Liferay 6.1 CE Beta release!  The Liferay product and engineering teams, in close concert with our awesome community, have spent many months getting the 6.1 release ready, and this is one of the final builds before it will be generally available.  We are making this release in advance of the 6.1 GA release, in order to ferret out last minute issues, and get the great work that the the teams have poured into this into your hands earlier.

New stuff since 6.0

The community has been hard at work.  Over 2500 individual issues have been resolved, and several new major and minor features make up 6.1.  Ed Chung gave a great overview of these 6.1 themes and features at the East Coast Symposium.  Here's a cheat sheet to some of the major ones (many in the community will undoubtedly be familiar with them, as you helped develop and/or review them!)

  • Multiple Repository Mounting - including CMIS 1.0 Support
  • Unification of the Document Library and Image Gallery
  • Native support for storing videos and other media types
  • Robust content metadata management
  • Establishing contextual relationships between content types (Related Assets)
  • Enhanced staging support (including Branching, Versioning, and Rollback)
  • Dynamic Site and Page Templates
  • User Customizable Pages
  • User Defined Lists (Dynamic Data Lists)
  • Workflow-enabled Forms
  • Mobile Device Detection
  • Unified User Management
  • RESTful Web Services
  • OpenSocial 1.1 Support
  • Enhancements for Liferay IDE 1.4
  • More social networking and collaboration feaures  (too many to list!)
  • Better Scalability
  • Better Management and Monitoring
  • Better Documentation
  • Better Quality
  • ..."And There's More!"

Downloads

The beta release will be available on our downloads page soon.  In the meantime, the usual files (portal bundles, sql scripts, CE plugins, and other supporting files) can be found on SourceForge: portal bundles and plugins.

6.1 Documentation

Our documentation continues to improve.  With the help of the community, our end user, administrator, and developer documentation have all improved tremendously.  The effort to document this release continues through this beta.  While the final documentation has not yet been published (it will be published with the GA release), its open source nature means you can take a look at the development, and even contribute to the "code".  If there is a specific new feature you are interested in learning about, you are encouraged to use any of the myriad avenues through which the community supports its constituents, or you can just give the Beta build a try today!

Bug reporting

If you experience issues with this beta release, and believe it to be a bug in the product, you are encouraged to file a report at issues.liferay.com.  There is a specific release to denote in the Affects Version/s field: 6.1.0 B3 (Beta-3).  The issue will be triaged, and if it has a big enough impact, will be fixed for the final GA release.

Upgrading to 6.1

As this is still a beta, you are should not use this unsupported release in a production environment.  However, it is a good way to test that your eventual upgrade will succeed with the minimum of fuss.  Several of our BugSquad members have already successfully upgraded their customized Liferay 6.0 environments to the early 6.1 builds, and this build gives you a chance to do the same.

Going Forward

As we approach the final GA release, there may be additional Release Candidate builds to fix any last minute issues.  As always, you are encouraged to participate and contribute to the project, and I hope you enjoy this sneak peek!

Community Meetup at the Europe Symposium

Company Blogs 10 de Outubro de 2011 Por James Falkner Staff

Update: The Meetup will take place at Berliner 109 on Monday, October 17, at 18:00!  This is a couple of minutes walk east, from the Symposium venue (Sheraton Offenbach Hotel).

If you are attending this year's European Symposium next week in Frankfurt, on Monday evening (after the pre-symposium training) Liferay is hosting a community meetup.  This will be a free event, starting around 18:00, at a Berliner 109.  Come meet your fellow community members, Liferay staff, and other interested parties with some free drinks, snacks, and interesting conversation!  It'll be a great way to start off your two-day symposium.

If you are interested in attending, let me know by leaving a comment on this blog post, so that we can gauge how many will be there!  See you in Frankfrurt!

Liferay Community Verifier Program

Company Blogs 5 de Outubro de 2011 Por James Falkner Staff

As you look back on the Liferay Community, you can see many innovative contributions and outstanding achievements by our wider community of enthusiasts, users, customers, partners, and employees.  You can also see a large ball of abused and ignored bug reports, connected to us by a big chain, limiting the utility of the reporting system and efficiency of software development on the project!

Today begins a new community program at Liferay: Community Verifier.  I mentioned this a long time back, and we now have the resources in place and I have the time to manage such an undertaking. I have sent instructions to those that have already volunteered (almost 25 initially, with more to come hopefully!)

The goal of this program is to address issues (not fix them per-se, just triage them) in our issue database, especially those issues filed against past releases that may still be valid, but have been ignored for some time. We will make sure the bug still exists in recent releases, and if they do, promote them to the latest release and assign to the appropriate staff to resolve, otherwise we will close them out as fixed or not reproducible. 

Of course, new issues are still coming in, however if you look at the community's performance over the last year, we have chopped that mountain roughly in half (from 1700 "unverified" last year to 867), which is great progress, but we've more to go.  With the community's help, I think we can easily get that down to 0 unverified/unattended bugs.  Wouldn't that be swell?  I think so.

The program begins now, and continues indefinitely, and I will be actively recruiting new members as we go along (if interested, leave comments below).  Note that this is not for the beginner on the Liferay Platform.  Good judgement, technical know-how, and some Liferay experience are all needed. I will also establish a landing page (much like BugSquad) along with a forum category so that the team can hold conversations there instead of email (and so everyone gets a chance to see what we're up to), and get to know each other.

Community Roundup

Company Blogs 27 de Setembro de 2011 Por James Falkner Staff

It's that six dot one bump, make you put yo hands up - Make you put yo hands up, put yo, put yo hands up

Welcome to another installment of Community Roundup!  Your source for a digest of the goings-on in the Liferay Community.  I apologize for the delay, I am targeting a one month period, and promise to do better next time!  It is taking about 6 hours per roundup to sift through the mountain of coolness in the Liferay community. We have lots of material to cover, so sit back, relax, and start your clicking or tapping.

  • Last week, the Liferay Community enjoyed a fantastic 48 hours in sunny Southern California for Liferay's 3rd-annual West Coast Symposium.  As emcee, I was able to attend the first and last 10 minutes of each Track A session.  I will do a wrap-up in a separate post, but wanted to give a special shout-out and congratulations to Cignex Datamatics and Savior-faire Linux, recipients of the first ever Liferay Community Excellence Award for their outstanding achievements and contributions to our community! More coverage from WCS here, here, here, here, here, here, here, and...... wait for it... here.
  • If you attended my Community Talk at WCS (slides available soon), you'll be aware of many of the upcoming features we are working on for you.  Jamie Sammons from CDS Global was kind enough to get up on stage with me and discuss his role in our community, and I described the upcoming features: Community Blogging, Incubation Projects, Community Verifier, a real Ideation site, Badges and Achievements, and new Developer On-Ramp videos.  Good times!
  • With WCS behind us, it is time to look forward to the "Final 3" -- Europe (Frankfurt), Spain (Madrid), and Italy (Rome).  If you have a chance to attend any of these, you will be pleasently surprised at the highest ever quality content to date.  These symposiums are a great opportunity to meet and engage the leaders and active participants in our community.  See you there!
  • We are coming up on the close of the Liferay 6.1 chapter in the epic book of Liferay.  At WCS, we announced availability of a Liferay 6.1 release candidate build on October 14, but if you are participating in Community BugSquad, of course you have already reviewed the sneak-peek 6.1 builds, and several of your feedbacks/suggestions/fixes/improvements have already been incorporated, with more to come!  We are in the final approach to the 6.1 landing, so get your vote in now to get your favorite issues resolved.
  • Speaking of epic books: Manning has finally begun printing copies (and making e-books available) of Rich Sezov's Liferay In Action - THE definitive guide to Liferay Portal, covering all of the latest features of the Liferay 6.1 platform.  I saw a couple of copies floating around at WCS, and Rich was kind enough to autograph mine, and you can get yours today! 
  • Looking to put your newfound skills from WCS and Liferay In Action to good use?  Liferay Hungary has initiated an awesome program for University students: Liferay Code Camp 2011!  Meet professionals and other students, Write some code, get some real-world experience on a large software engineering project, and win a trip to Los Angeles or Madrid.
  • More greatness from (and for) the community: Radio Liferay!  A podcast series from Olaf Kock, covering the world of Liferay in human terms - a conversation with interesting people from in and around Liferay. Check out and subscribe to the feed.  Radio Liferay Episodes [3] [2], and [1(me!!)] are available now.
  • Liferay's Community Leadership Team had its second meetup in July, with the next one in mid-October.  You can find the minutes here.  Attended by 11 of the community's leaders, we had discussions ranging from community programs, to user groups, and an interesting discussion around "What Motivates the Community?".  The follow-up discussion thread is very interesting.
  • Harpreet from Cloudbees shows us how to deploy Liferay onto a cloud in 15 minutes.  If you saw his presentation at WCS, then this is old news, but still awesome!  
  • Dynamic Data Lists is a marquee feature in 6.1 - Mark shows us why.
  • Going to Oracle OpenWorld this year?  Liferay will be there (Moscone South - Booth 110 - behind the MySQL Demo Grounds), and is proud to be a Gold Sponsor at the first Jenkins User Conference.  Liferay uses Jenkins extensively for it's Continuous Integration process, and we look forward to meeting others who want to find out more about Jenkins and Liferay! Read more about our use of Jenkins in Harpreet's blog entry.
  • If you've browsed liferay.com on your mobile, you may have noticed the layout is different.  In fact, in a desktop browser, if you squeeze the website thinner and thinner, the layout will dynamically adjust (and images are changed, and so on).  This is a mobile responsive layout, and now you can use it, by grabbing it from here!
  • I don't normally re-post job listings, but this one caught my attention.  You simply show up (walk in) between 9a and 6p, work on a Liferay project, and get paid.  That's like fast food for Liferay developers.  Only reversed...  Sort of.  Interesting concept nonetheless!  Nice to see a proliferation of Liferay jobs.  Heh.. get it?  Pro-life-eration -- Liferay.  *groan*
  • Liferay Developers who want to use Maven with the Plugins SDK - Over at DZone, Kamesh explains how to do it.
  • jQuery is a nice toolkit for javascript developers - see how to easily integrate it into your portlets by reading Alexey's blog post.
  • I've seen How to Change Company Logo Liferay Portal 6.x on Twitter about a zillion times.  Occasionally the profile picture of the person re-tweeting this is a famous inventor or some celebrity.  Perhaps it is a cry for help, or to get on Community Roundup.  So here it is.  Now we can all change our company logos!  I just changed mine.  There, I did it again!
  • Liferay Community User Group activity continues to rise.  We are up to 10 groups, with a few more in the pipeline.  New groups include Austin, Netherlands, and Italy.  Spain already held a first meetup (minutes here, en Español, and excelente tee shirt pic here).  Contact community@liferay.com if you wish to start your own!
  • Ever wanted to open your portlet up in a modal (or non-modal) dialog in Liferay?  It's pretty easy!  And it's extensively used in Liferay 6.1 so that you don't lose context when visiting the various administrative interfaces.  
  • If you need direct database access from your Liferay plugin, look no further.  Daniel shows us the (easy) way.
  • Easy integration of OpenDJ and Liferay by profiq. I particularly like Liferay's UI for configuring LDAP and several other integration options.  *cough* database configuration *cough*.
  • A lot of the administrative interfaces in Liferay are driven by the Search Container API - simply showing a list of something and allowing actions on the individual items or in bulk.  If you've created new users, sites, or virtually any other portal entity in Liferay, you've used a Search Container.  If you want to create one for your custom entities, look no further than Daniel's excellent blog, and this post in particular.
  • OpenXava 4.2.3 is now available!  OpenXava allows you to define business applications with simple POJOs, JPA and Java 5 annotations.  Here's a nice (though dated) wiki article on how to integrate OpenXava into Liferay.
  • Liferay working with Java 7!  Though Java 7 might not be ready for primetime, Liferay works well (with a few caveats).
  • Liferay only good for intranets?  Not so, says Szymon (and I happen to agree :) ).  This site runs Liferay.
  • Soft Shake 2011 will take place October 3-4, in Geneva.  This year, Liferay will get some coverage via Vaadin's Sami Ekblad, with a discussion on Rich Portlets in Liferay 6 with Vaadin.  Stop by and say Hi if you're there!
  • A Liferay event hosted by Smile in Paris a couple of weeks ago attracted a lot of community members to learn more, and Guillaume provides a nice wrap-up of the event (en français).
  • Uh oh.. here comes the Android crowd!  Aritz shows us how to easily talk to Liferay from your Android device.
  • Well, this is interesting.  A PortletBuilder tool (akin to ServiceBuilder) - building Portlets based on XML definitions!  THanks Mark Polly!
  • Liferay does not discriminate in its support for various UI frameworks.  Stack traces from all are equally long :-)  But GWT is a popular one (Vaadin in fact is built on it).  Here's a great tutorial on using GWT and making instanceable portlets using it.
  • Wow.. a great contribution by Holistic Security!  Calling Alfresco Webscripts from Liferay w/AJAX.  A complete solution, including downloadable binaries!

New Blog Posts: Ehcache Configuration, Spain Symposium Details, Radio Liferay Episodes [3] [2], and [1(me!!)], Spring Contexts, DL Record Plugin, Liferay & OSGi, Utilizing Instance Storage on EC2, Adding Workflow Portlets to a Page, Community Activity Map (me!!), Activity Map Explained, Themes Settings and Advanced Controls, Liferay WCS and Jenkins User Conference, The Myth of Free Open Source Portals, Theme JSP Overrides, JSP Include Buffer, Changing Context PathApoyando a Unicef en la campaña del Cuerno de África, Test JARs, Controlling Content, Changing Default Column Target for Drag 'n Drop, Community Stats (me again!!).  Dang.  Once we enable blogging for the entire community, I may have to stop this bit :)

Recent Wiki Updates: Introduction to Liferay, JSON Web Services, Non-Standard Timezones, JBPM, AlloyUI Forms, Websphere 6.1 Tips, Application Servers, Custom Queries in 5.2, Liferay IDE 1.4 Plan, Liferay Hooks, Translation Team, Solr, Web Content Velocity Variables, URL Parameters, Contributing, Database Configuration.

Looking forward to meeting many of you in Frankfurt and Madrid!  I will leave you with two teaser videos produced for this year's symposium.  First, a recap of where we are at Liferay:

Finally, a recap of day 1 from WCS:

Enjoy!

 

Community Activity Map Explained

Company Blogs 30 de Agosto de 2011 Por James Falkner Staff

By popular demand, below is a technical description and source code of the new Community Activity Map that I posted a couple of weeks ago.

Overview

I started on this back in December 2010 when we were refreshing the community landing page.  Having a list of recent issues, and announcements is all well and good, but there is so much more happening in our worldwide community than the static content I can create, so I wanted a good way to dynamically visualize all of the activity.  Liferay has a built-in activity stream framework, and indeed activities can already be seen in a listing (for example, on my profile page on liferay.com).  However, it is a static display and only relates to my activities.  Wouldn't it be great to see everyone's activities, and the global location at which these activities occur?  Liferay and Google Maps to the rescue!

Portlet or Web Content

Initially, I created a simple MVC-based portlet with a single JSP page.  On the server side, the JSP contained a scriptlet that fetched activities using Liferay's SocialActivity service, and created a giant javascript object in the resulting markup, containing around 20 activities.  The markup is then shipped to the client side, where some more javascript rendered the activities on a google map.  This was all well and good, but unless you refreshed the page, you'd always see the same 20 activities, in a loop, forever.  It also meant that render time was slower because all of the action occured on the server side, and the client didn't see anything until the activities were already fetched and processed.

The other problem was I didn't test it on IE, and when I showed it to Brian Chan, he tried it on IE and predictably it didn't work.  

I abandoned this work due to other priorities, but then about a month ago I found some spare time and resurrected the code.  This time, I decided to make it closer to realtime, and update the page dynamically, so I switched to an AJAX mechanism, which fetched activities using the portlet's serveResource API in an AJAX call (using AlloyUI's built-in AJAX IO library).  I made sure it worked on IE, and submitted the code for review to Peter Shin.  Peter, being the awesome developer he is, thought this would be simpler to maintain if it were purely web content using Liferay's powerful Web Content Management system.  He was able to convert my portlet into pure Velocity-based web content (similar to Ray's example from a few months ago).  This means that its much easier to make updates, and the liferay.com maintainers don't have to be bothered every time I wanted to re-deploy a new version.  Nice, and immense thanks goes to Peter for this!

Web Content Framework

To create apps using Liferay WCM, you create web content structures and templates and articles (this should be familiar to anyone that has used Liferay WCM).  In this case, our structure is simple, and contains a single configurable element called height.  This is then referenced in the template using $height.data.  You could add other configurable parameters (for example, configurable timeouts, or configurable Google Map options, etc).  The web content template has the following framework:

#if ($request.lifecycle == "RENDER_PHASE")
  ## Client-side initial render call will render the result of this code
#elseif ($request.lifecycle == "RESOURCE_PHASE")
  ## Client-side AJAX call will fetch the result of this code
#end
That's it!  The RENDER_PHASE code is akin to a portlet's "View Mode" code (typically a JSP called view.jsp), and the RESOURCE_PHASE is akin to a portlet's serveResource code.

In this case, the RENDER_PHASE contains the javascript for fetching activities via an AlloyUI AJAX call, and then rendering the Google Map, and placing "bubbles" on the map at varying intervals.

The RESOURCE_PHASE results in a JSON object, which is read by code in the RENDER_PHASE, which contains the activities.

When this is all put together, the client side gets the RENDER_PHASE javascript, and executes it, which causes the AJAX call back to Liferay, and returns the result of the RESOURCE_PHASE code.  It's important to NOT make the template cachable, such that every AJAX call results in a new execution of the RESOURCE_PHASE call (otherwise, the same activities would be returned, time and time again).

Fetching and Rendering Activities

The important part of the RESOURCE_PHASE code is here:

#set ($socialActivities = $socialActivityLocalService.getGroupActivities($scopeGroupId, 0, 50))
#foreach ($socialActivity in $socialActivities)
  #set ($socialActivityFeedEntry = $socialActivityInterpreterLocalService.interpret($socialActivity, $themeDisplay))
  ##  do stuff with it
#end
This code retrieves 50 activities from the current group (in our case, the "liferay.com" community on liferay.com), and creates a JSON array, which is returned as a result of this call.
 
Each activity is placed in the array and consists of the following items:
  • body - The rendered content for the activity (rendered via the socialActivityInterpreterLocalService.interpret() call)
  • description - The description of the time of the activity (e.g. "A few seconds ago", "Yesterday", or a date/time)
  • geocoderAddress - The location information for the user who did the activity (more on this below)
  • title - The title of the activity
  • userDisplayURL - The user's profile URL
  • userFullName - The user's name
  • userPortraitURL - The URL to the user's profile picture
The resulting JSON object output by the RESOURCE_PHASE looks like
{
  "jsonArray": [{"body":"....", "geocoderAddress", "France", ...}, {...more activities here...}]
}

 

Geocoder Details

This app uses Google's Geocoder functionality to turn a human-readable address into a Latitude/Longitude, for placing on the map.  The address is taken from your profile, either your full address (if you entered it), or your country (if you entered it), and failing that, defaults to Liferay's headquarters near Los Angeles :)  We do NOT take your full street address.  Don't want anyone visiting you and telling you your wiki edit was wrong :)
 
To fetch the user's address, we make a call to Liferay's AddressService and look for the user's "Primary" address:
#foreach ($address in $user.getAddresses())
  #if ($address.isPrimary())
    #set ($city = $address.getCity())
    #set ($region = $address.getRegion().getName())
    #set ($country = $address.getCountry().getName())
    ## more stuff 
  #end
#end
 
If the user has not entered their address, we then look at a custom User Attribute called country that we have on liferay.com:
#set ($country = $user.getExpandoBridge().getAttribute("country"))
Once the user's location is known, it is stored in the resulting JSON object and shipped to the client during the AJAX call, where we pass the address to Google's geocoder API (or fetch a cached result if the address has already been geocoded).
 

Time Description

A simple way to make a more human-readable timestamp, by looking at how long ago an activity occurred, and generating a nice looking string to represent it.
#set ($now = $dateUtil.newDate())
#set ($millisecondsBetween = $now.getTime() - $socialActivity.getCreateDate())
#if ($millisecondsBetween < 60000)
  #set ($description = $languageUtil.get($locale, "a-few-seconds-ago"))
#elseif ....
  ## more strings for different time differences
#end
 

Making the AJAX call

Using AlloyUI, a simple call is made back to the server, to fetch the results of the RESOURCE_PHASE code, using the below javascript:
function ${portletNamespace}getSocialActivities() {
  AUI().use(
    "aui-base", "aui-io-plugin", "aui-io-request",
    function(A) {
      A.io.request(
        "${request.resource-url}",
        {
          data: {
          },
          dataType: "json",
          on: {
            success: function(event, id, obj) {
              var responseData = this.get("responseData");
              ${portletNamespace}socialActivityCache = responseData.jsonArray || [];
              ${portletNamespace}renderSocialActivity();
            },
          failure: function(event, id, obj) {
          }
        }
      }
    );
  }
);

}
The resulting JSON object is stored, and the renderSocialActivity() function is called to loop through the activities.
 

Rendering the map

You can see the javascript that is used to render the map in the source code below.
var ${portletNamespace}googleMap = new google.maps.Map(
  document.getElementById("${portletNamespace}map"),
  {
    center: new google.maps.LatLng(35, 0),
    mapTypeControl: false,
    mapTypeId: google.maps.MapTypeId.ROADMAP,
    navigationControl: false,
    scaleControl: false,
    streetViewControl: false,
    zoom: 2
  }
The (35,0) is arbitrarily in Northern Algeria.  Don't ask where I came up with that :)  You can turn on additional map controls (e.g. the ability to switch to sattelite view vs. road vew), and configure the default zoom level, with the above options object (you could even make these part of your web content structure, to make it easier to tweak).
 
The javascript code that actually places the bubbles on the map:
function ${portletNamespace}openInfoWindow(position, content, zIndex) {
	var infoWindow = new google.maps.InfoWindow(
		{
			content: content,
			position: position
		}
	);

	infoWindow.setOptions(
		{
			disableAutoPan: false,
			zIndex: zIndex
		}
	);

	infoWindow.open(${portletNamespace}googleMap);

	setTimeout(
		function() {
			infoWindow.close();
		},
		15000
	);
}

Notice the hard-coded values for timeouts. This could be improved by making it part of the web content's structure (like height), so that it could be configured easier. Also notice the zIndex argument - this makes sure that newer bubbles appear on top of older bubbles.

The geocoderAddress returned as part of the JSON object (from the RESOURCE_PHASE) is geocoded using Google's geocoder API:

 

geocoder.geocode(
  {"address": geocoderAddress},
  function(results, status) {
    if (status == google.maps.GeocoderStatus.OK) {
      ${portletNamespace}openInfoWindow(results[0].geometry.location, content, ${portletNamespace}index);
      ${portletNamespace}geocoderAddressCache[geocoderAddress] = results[0].geometry.location;
    }
  }
);
Notice we place the resulting address in a cache object, so we don't have to geocode the same address again (Google places a limit on the number of geocoding calls a given IP address can make in a given day).
 

Timeouts

This app is not truly realtime.  For a truly realtime result (which would be rather boring, since only one or two activities occur every minute), one would need to install some kind of ModelListener hook into Liferay, listening for new activities, and maintain a long-running comet-style open connection (similar to how liferay.com's chat functionality works) to feed new activities to the client side.  Instead, there are various timeouts that occur in the javascript which place the most recent block of activities on the map at random intervals.  Each activity's Time To Live is hard-coded at 15 seconds (again, this could be configured via a web content structure element), and the period between events is random between 10 and 20 seconds using this call:
setTimeout("${portletNamespace}renderSocialActivity()", 10000 + (Math.floor(Math.random() * 10000)));
After all of the activities in a given block are rendered, a new set of activities is fetched using the same AJAX call as before, and the loop begins all over again.
 

Random Notes and Improvements

There is a lot of noisy #set directives in the Velocity code in the RESOURCE_PHASE, because of the nuances of Liferay's WCM (in particular, because you don't have access to a "live" ThemeDisplay object, we have to create one and populate it with necessary content from the "sparse" name/value theme-display property available from the request object, which is available to WCM templates).  Don't let it distract you!
 
I am hoping some of you may be interested in making improvements, such as:
  • Making more things configurable, like the timeouts, map options, etc
  • Making it truly real-time using a ModelListener Hook as described above
  • Fetching more kinds of activities (for example, right now, the map won't show blog entries, since these occur in the user's "group", not the liferay.com group)
  • Accessing activities from outside of liferay.com!  Perhaps integrating with your Facebook profile, the Liferay twitter stream, or some other activity feed
  • Others?

 

The full Source Code

If you want to use this for your own purposes, below is the source code.  Here are the steps to use this:
  1. Create a web content structure, and cut and paste the below structure code into it.
  2. Create a web content template, associate it with the above newly-created structure, and cut and paste the below Velocity code into your template.
  3. Create a new web content article, selecting the above structure and template.
  4. Add a "Web Content Display" portlet to a page, and configure it to show the newly created article created in step 3.
#set ($portletNamespace = $request.portlet-namespace)
#set ($scopeGroupId = $getterUtil.getLong($request.theme-display.scope-group-id))
#set ($timeZone = $timeZoneUtil.getTimeZone($request.theme-display.time-zone))
#set ($userId = $getterUtil.getLong($request.theme-display.user-id))

#set ($height = $getterUtil.getString($height.data, "300"))

#if ($request.lifecycle == "RENDER_PHASE")
  <link href="//code.google.com/apis/maps/documentation/javascript/examples/standard.css" rel="stylesheet" type="text/css" />

  <script type="text/javascript" src="//maps.google.com/maps/api/js?sensor=false"></script>

  <div id="${portletNamespace}map" style="height: ${height}px; margin-bottom: 1.5em; width: 100%;"><!-- --></div>

  <script type="text/javascript">
    var ${portletNamespace}geocoderAddressCache = new Object();

    var ${portletNamespace}googleMap = new google.maps.Map(
      document.getElementById("${portletNamespace}map"),
      {
        center: new google.maps.LatLng(35, 0),
        mapTypeControl: false,
        mapTypeId: google.maps.MapTypeId.ROADMAP,
        navigationControl: false,
        scaleControl: false,
        streetViewControl: false,
        zoom: 2
      }
    );

    google.maps.event.addDomListener(window, "load", ${portletNamespace}getSocialActivities);

    var ${portletNamespace}index = 0;
    var ${portletNamespace}socialActivityCache = [];

    function ${portletNamespace}getSocialActivities() {
      AUI().use(
        "aui-base", "aui-io-plugin", "aui-io-request",
        function(A) {
          A.io.request(
            "${request.resource-url}",
            {
              data: {
              },
              dataType: "json",
              on: {
                success: function(event, id, obj) {
                  var responseData = this.get("responseData");

                  ${portletNamespace}socialActivityCache = responseData.jsonArray || [];

                  ${portletNamespace}renderSocialActivity();
                },
                failure: function(event, id, obj) {
                }
              }
            }
          );
        }
      );
    }

    function ${portletNamespace}openInfoWindow(position, content, zIndex) {
      var infoWindow = new google.maps.InfoWindow(
        {
          content: content,
          position: position
        }
      );

      infoWindow.setOptions(
        {
          disableAutoPan: false,
          zIndex: zIndex
        }
      );

      infoWindow.open(${portletNamespace}googleMap);

      setTimeout(
        function() {
          infoWindow.close();
        },
        15000
      );
    }

    function ${portletNamespace}renderSocialActivity() {
      if (${portletNamespace}socialActivityCache.length <= 0) {
        ${portletNamespace}openInfoWindow(new google.maps.LatLng(35, 0), Liferay.Language.get("there-are-no-recent-activities"), 1);

        return;
      }

      if (${portletNamespace}index >= ${portletNamespace}socialActivityCache.length) {
        ${portletNamespace}index = 0;

        setTimeout("${portletNamespace}getSocialActivities()", 1000);

        return;
      }

      var content =  '<div>' +
              '  <a href="' + ${portletNamespace}socialActivityCache[${portletNamespace}index].userDisplayURL + '">' +
              '    <img alt="' + ${portletNamespace}socialActivityCache[${portletNamespace}index].userFullName + '" style="float: left;" height="44" hspace="4" vspace="4" src="' + ${portletNamespace}socialActivityCache[${portletNamespace}index].userPortraitURL + '" />' +
              '  </a>' +
              '  <div>' +
                  ${portletNamespace}socialActivityCache[${portletNamespace}index].description +
              '  </div>' +
              '  <div>' +
                  ${portletNamespace}socialActivityCache[${portletNamespace}index].title +
              '  </div>' +
              '  <div>' +
                  ${portletNamespace}socialActivityCache[${portletNamespace}index].body +
              '  </div>' +
              '</div>';

      var geocoderAddress = ${portletNamespace}geocoderAddressCache[${portletNamespace}socialActivityCache[${portletNamespace}index].geocoderAddress];

      if (geocoderAddress) {
        ${portletNamespace}openInfoWindow(geocoderAddress, content, ${portletNamespace}index);
      }
      else {
        var geocoder = new google.maps.Geocoder();

        geocoderAddress = ${portletNamespace}socialActivityCache[${portletNamespace}index].geocoderAddress;

        geocoder.geocode(
          {"address": geocoderAddress},
          function(results, status) {
            if (status == google.maps.GeocoderStatus.OK) {
              ${portletNamespace}openInfoWindow(results[0].geometry.location, content, ${portletNamespace}index);

              ${portletNamespace}geocoderAddressCache[geocoderAddress] = results[0].geometry.location;
            }
          }
        );
      }

      ${portletNamespace}index = ${portletNamespace}index + 1;

      setTimeout("${portletNamespace}renderSocialActivity()", 10000 + (Math.floor(Math.random() * 10000)));
    }
  </script>
#elseif ($request.lifecycle == "RESOURCE_PHASE")
  #set ($logFactory = $portal.getClass().forName("com.liferay.portal.kernel.log.LogFactoryUtil"))
  #set ($log = $logFactory.getLog("com.liferay.portal.util.PortalImpl"))
  #set ($portalBeanLocator = $portal.getClass().forName("com.liferay.portal.kernel.bean.PortalBeanLocatorUtil"))
  #set ($portletBeanLocator = $portal.getClass().forName("com.liferay.portal.kernel.bean.PortletBeanLocatorUtil"))

  #set ($fastDateFormatFactoryUtil = $portal.getClass().forName("com.liferay.portal.kernel.util.FastDateFormatFactoryUtil"))
  #set ($jsonFactory = $portalBeanLocator.locate("com.liferay.portal.kernel.json.JSONFactoryUtil"))
  #set ($permissionThreadLocal = $portal.getClass().forName("com.liferay.portal.security.permission.PermissionThreadLocal"))
  #set ($socialActivityInterpreterLocalService = $portalBeanLocator.locate("com.liferay.portlet.social.service.SocialActivityInterpreterLocalService.velocity"))
  #set ($socialActivityLocalService = $portalBeanLocator.locate("com.liferay.portlet.social.service.SocialActivityLocalService.velocity"))
  #set ($userLocalService = $portalBeanLocator.locate("com.liferay.portal.service.UserLocalService.velocity"))

  #set ($dateFormatDateTime = $fastDateFormatFactoryUtil.getDateTime(1, 3, $locale, $timeZone))
  #set ($portalURL = $httpUtil.getProtocol($request.attributes.CURRENT_URL) + "://" + $getterUtil.getString($request.theme-display.portal-url))

  #set ($themeDisplay = $portal.getClass().forName("com.liferay.portal.theme.ThemeDisplay").newInstance())

  #set ($V = $themeDisplay.setLocale($locale))
  #set ($V = $themeDisplay.setPathImage($getterUtil.getString($request.theme-display.path-image)))
  #set ($V = $themeDisplay.setPathMain($getterUtil.getString($request.theme-display.path-main)))
  #set ($V = $themeDisplay.setPermissionChecker($permissionThreadLocal.getPermissionChecker()))
  #set ($V = $themeDisplay.setPortalURL($portalURL))
  #set ($V = $themeDisplay.setScopeGroupId($scopeGroupId))
  #set ($V = $themeDisplay.setTimeZone($request.theme-display.time-zone))
  #set ($V = $themeDisplay.setUser($userLocalService.getUserById($userId)))

  #set ($socialActivities = $socialActivityLocalService.getGroupActivities($scopeGroupId, 0, 50))

  #set ($jsonArray = $jsonFactory.createJSONArray())

  #foreach ($socialActivity in $socialActivities)
    #set ($socialActivityFeedEntry = $socialActivityInterpreterLocalService.interpret($socialActivity, $themeDisplay))

    #if ($validator.isNotNull($socialActivityFeedEntry))
      #set ($user = $userLocalService.getUserById($socialActivity.getUserId()))

      #set ($geocoderAddress = "")

      #foreach ($address in $user.getAddresses())
        #if ($address.isPrimary())
          #set ($city = $address.getCity())
          #set ($region = $address.getRegion().getName())
          #set ($country = $address.getCountry().getName())

          #set ($s = "")

          #if ($validator.isNotNull($city))
            #set ($s = $s + $city + ",")
          #end

          #if ($validator.isNotNull($region))
            #set ($s = $s + $region + ",")
          #end

          #if ($validator.isNotNull($country))
            #set ($s = $s + $country)
          #end

          #if ($validator.isNotNull($s))
            #set ($geocoderAddress = $s)
          #end
        #end
      #end

      #if ($validator.isNull($geocoderAddress))
        #set ($country = $user.getExpandoBridge().getAttribute("country"))

        #if ($validator.isNotNull($country))
          #set ($geocoderAddress = $languageUtil.get($locale, $stringUtil.merge($country)))
        #end
      #end

      #if ($validator.isNull($geocoderAddress))
        #set ($geocoderAddress = "Walnut, CA, United States of America")
      #end

      #set ($now = $dateUtil.newDate())

      #set ($millisecondsBetween = $now.getTime() - $socialActivity.getCreateDate())
      #set ($description = $dateFormatDateTime.format($socialActivity.getCreateDate()))

      #if ($millisecondsBetween < 60000)
        #set ($description = $languageUtil.get($locale, "a-few-seconds-ago"))

        #if ($validator.equals($description, "a-few-seconds-ago"))
          #set ($description = "A few seconds ago.")
        #end
      #elseif ($millisecondsBetween < 3600000)
        #set ($minutes = $millisecondsBetween / 60000)

        #set ($description = $languageUtil.format($locale, "about-x-minutes-ago", $stringUtil.merge([$minutes]), false))

        #if ($validator.equals($description, "about-x-minutes-ago"))
          #set ($description = "About " + $minutes + " minute(s) ago.")
        #end
      #elseif ($millisecondsBetween < 86400000)
        #set ($hours = $millisecondsBetween / 3600000)

        #set ($description = $languageUtil.format($locale, "about-x-hours-ago", $stringUtil.merge([$hours]), false))

        #if ($validator.equals($description, "about-x-hours-ago"))
          #set ($description = "About " + $hours + " hour(s) ago.")
        #end
      #elseif ($millisecondsBetween < 604800000)
        #set ($days = $dateUtil.getDaysBetween($dateUtil.newDate($socialActivity.getCreateDate()), $now, $timeZone))

        #set ($description = $languageUtil.format($locale, "about-x-days-ago", $stringUtil.merge([$days]), false))

        #if ($validator.equals($description, "about-x-days-ago"))
          #set ($description = "About " + $days + " day(s) ago.")
        #end
      #end

      #set ($jsonObject = $jsonFactory.createJSONObject())

      #set ($V = $jsonObject.put("body", $socialActivityFeedEntry.getBody()))
      #set ($V = $jsonObject.put("description", $description))
      #set ($V = $jsonObject.put("geocoderAddress", $geocoderAddress))
      #set ($V = $jsonObject.put("title", $socialActivityFeedEntry.getTitle()))
      #set ($V = $jsonObject.put("userDisplayURL", $user.getDisplayURL($themeDisplay)))
      #set ($V = $jsonObject.put("userFullName", $htmlUtil.escape($user.getFullName())))
      #set ($V = $jsonObject.put("userPortraitURL", $user.getPortraitURL($themeDisplay)))

      #set ($V = $jsonArray.put($jsonObject))
    #end
  #end

  {
    "jsonArray": $jsonArray
  }
#end

The WCM structure is pretty darn simple:

<root>
  <dynamic-element name='height' type='text' index-type='' repeatable='false'/>
</root>

That's all folks!

New Community Activity Map

Company Blogs 17 de Agosto de 2011 Por James Falkner Staff

You may have noticed a new feature on our community homepage, http://liferay.org - the Recent Activity Map is a dynamic Google Map that displays activities from our community, as they happen.  The map itself is a standard Google Map - and therefore, you can scroll around (using the hand tool), zoom in and out, and use the standard keyboard shortcuts (e.g. arrow keys, +/- keys).

The location information is taken from your liferay.com profile - either your actual address (city, region, and country only, not your street address), or your country, if you entered it.  Your profile picture is also used, along with the clickable activity information in the bubbles that pop up.

Entering your Location

If you want your location on the map to be correct, you can either enter your country, or your full address.

To enter your country, go to "My Account", and select a country from the "Country" drop-down list.  Note that when only entering country information, then all of your activities will appear to emanate from the geographical center of your country, which may be in the middle of a lake or the top of a mountain :-)

To enter a more accurate address, click on the "Address" tab on the right, and enter a complete address.  Make sure to mark it as your "Primary" address, as this is the address used for the map.

From then on, your activitiy bubbles will be shown in their proper location.  

Technical Notes

This is the best part: the map is implemented entirely as a Web Content Template!  Thanks to the power of Liferay's WCM system, and Peter Shin, who noticed my portlet was basically doing an AJAX call to the portlet's serveResource facility, Peter converted this to a velocity-based web content template (similar to Ray's earlier example).  Much easier to maintain and make updates.

The client side part is very simple, it just uses javascript and the Google Maps API to put up a map, and loop through the list of SocialActivity objects returned from Liferay's SocialActivity service.  User addresses are similarly looked up using Liferay's Address service.  If a user does not enter a full address, they can also just enter their Country through a drop-down on the user profile, implemented as an Expando attribute on the User entity.  We then access this through the ExpandoBridge for the User entity.

Once we have a set of activities, and associated usernames, profile URL and picture, timestamp (formatted to read nicer, such as "a few seconds ago" or "yesterday", also localized), then we use simple javascript to loop through them and place each of them on the map using the Google Maps API.  Once all entries are shown, we re-fetch a new set of activities. 

I think this gives our community a more lively, vibrant feel, especially for newcomers who happen upon this page early in their Liferay experience.  And it's fun seeing your name and activities "up in lights" every so often.  

Community Stats: Part 1

Company Blogs 11 de Agosto de 2011 Por James Falkner Staff

We all know that Liferay is a great social content and collaboration platform, which is why we at Liferay use it to implement our liferay.com website.  There are tons of interesting potential statistics to glean from this, to introspect into our community a bit more than the standard "50,000 registered users, 170,000 forum posts" kinds of stats that often find their way into slide decks at conferences.  I'd like to start a series of posts for interesting information about our community, that goes a bit deeper than typical stats.  Also, I welcome ideas for other stats, and I encourage everyone to use the community tools to their fullest extent.  For example, marking forum posts as questions when appropriate, and circling back to mark answers, and perhaps adding tags as appropriate.  

As part of a recent podcast which you'll be seeing/hearing in the near future, I wanted to generate some statistics on our activity level.  We often claim number like the above, but are they really indicative of the quality and usefulness of our community?  I think diving a bit deeper is useful, so here is the first in a series of stats I'll be looking at.  We are also planning a larger-scale survey (thanks to the awesome input from some of our valued community members in this thread), so look for that soon.

Registered User by Geography

First, a quick survey of registered liferay.com users by geography:

Ok, that's interesting, but what about by region?

This is not surprising to me - I've seen a lot of activity from our friends where Liferay enjoys a lot of mindshare.  

Recent Activity

Now, these first two charts are "all time" - that is, since the beginning of time (around 2002).  I thought it might be interesting to look at the numbers from more recent history, looking at the 5 year, 2 year, 1 year, 6 month, 2 month, and 1 month time periods.  I guess not surprisingly I didn't see much variation in the above.  When you get down to the last 2 months, the US starts to ramp up, perhaps we need to take more summer breaks or something.

 

Active, Unique Logins

Lastly, I often qualify the "50,000 registered users" stat with "well, of course, not all of them are active" and I usually guesstimate that 10% are active.  A basic measure of activity is when a user logs in.  You can use liferay.com anonymously all you wish, but to contribute, you generally have to log in.  So let's look at the lastLoginDate field for our user database and graph it out for unique users.

I was pleasently surprised to see that over 10,000 unique users have actually logged in at least once since February alone (of course many of these were 1-timers but still).

Finally, I found this graph rather amusing given my recent bad luck with IE (courtesy of "William" and GraphJam):

Have an idea of the kinds of stats you may be interested in?  Leave comments here or in the thread mentioned above and what we can dig up.

Community Roundup

Company Blogs 4 de Agosto de 2011 Por James Falkner Staff

Hi all, it's time for another installment of your linky-linky Community Roundup!  It's been quite busy in the community and at Liferay.  Community activity is buzzing and we are gearing up for various Fall events and activities, starting off with our annual West Coast Symposium in California, where there will be several key announcements, including the unveiling of Liferay 6.1, availability of the Liferay Marketplace, and several new and innovative event features that I guarantee you'll love (more on this later), so put down your ice cream and get registered! You will not want to miss this one.

Now, on to the links.

Whew, I have learned my lesson about waiting too long to post roundups.  Hope everyone is having a safe and enjoyable summer (in the northern hemisphere) or winter (down undah), see you all soon in LA at the West Coast Symposium!

Community BugSquad Begins

Company Blogs 13 de Julho de 2011 Por James Falkner Staff

You may have seen a previous mention of the Community BugSquad program in my last Community Roundup.  I'm excited to announce that the program has officially kicked off today!  The amazing volunteers of this program are contributing their time an expertise to review features in the upcoming Liferay 6.1 release, identify any new or regressed bugs (of course, Liferay is 100% bug-free, but it never hurts to make sure wink ), and improve their Liferay knowledge in a fun and educational environment.  My goal is to do this for every release.  

The first phase of the program involves reviewing the new Liferay 6.1 CE Features (such as Staging w/Branching, Personalizable Pages, Dynamic Site Templates, and others), with the goal of identifying any usability gaps, or other issues that would be encountered if this were released as-is, and recording in the form:

The second phase of the program involves downloading and trying our upcoming Release Candidate builds for Liferay 6.1, and finding (but not necessarily fixing) bugs.  Again, not that there would be any, but if there are, and they are known about before the final release, they have a good chance of getting fixed!  A win-win situation all around, and the team gets some hands-on Liferay experience, and valuable networking with other community members and Liferay staff.  If you're interested in participating, just leave a note in the comments below, and you'll be signed up!

 

Community Roundup

Company Blogs 13 de Junho de 2011 Por James Falkner Staff

8 Days to the official start of summer in the northern hemisphere.  2 days until the France Symposium.  That sounds like a good reason for another community roundup!  Hope everyone has a safe and fun summer season.  I have a love/hate relationship with it.  I love the summer season, but it means that from now on, the days will grow increasingly shorter and colder.  Luckily the Liferay community keeps me energized and warm.  On to the links!

  • As you may know, Liferay is looking to release the next version of Liferay Portal, version 6.1, later this year. We have found that engaging the community early on in the release cycle makes for higher quality and less migration issues, so I am organizing two special programs: Liferay Community Verifiers, and The Liferay BugSquad. Check out this forum thread, and respond if interested in participating!
  • There has been a lot of interest recently around Liferay User Groups.  Liferay has maintained a list of community events for our user groups in the past, however we are going to take it a step further with the introduction of user group "homes" on liferay.org!  Groups will get a set of collaboration applications (forums, wikis, calendars, etc) and a presence on the site.  Watch for this in the next couple of weeks.
  • Have a SCORM-based LMS and wish to surface its learning content in Liferay?  Arcusys has just released their SCORM module for Liferay to the community under LGPL.  Very nice work!  More background here, and here.
  • The 100 PaperCuts team is taking a short breather in advance of the 6.1 release.  Many of the same volunteers have signed up for the Community Verifier and 6.1 BugSquad program (see above).  It's been great getting to know everyone, and all of your hard work is definitely appreciated by the wider community!
  • The Liferay France Symposium is only days away, in beautiful Paris.  Featuring a host of Liferay experts and partners, the symposium will be a full day of discussion, demo, and use case studies.  If you are in the area, onsite registration is available!
  • The Liferay Hungary Symposium was held last month, and also featured a great set of speakers.  Steve provides some detail in his blog. If you missed the event, slides are available, and pics are up! [set 1, set 2].
  • On June 21, at the University of Szeged, Szeged Tech Meetup will be held, featuring discussions about Liferay.
  • Interesting.  Liferay is very often used to.. well.. integrate stuff.  So Steffen's very useful blog is well-titled. 
  • Swisher International designed a great corporate site on Liferay - and won an ADDY!
  • Alexey and EmForge continue to give back to the community with the release of Liferay 6.0.6 AMI images for Amazon EC2.  An easy and cheap way to quickly get a manageable Liferay instance up and running!
  • Alexey also provided a nice overview to Model Listener hooks for Liferay.  I'll see if I can't find time to update the wiki page.
  • Speaking of giving back, it's always nice to see ideas pop up and be driven by the community.  Case in point, providing Liferay's Document Library with anti-virus scanning ability.  This idea is well on its way to completion by those involved!  Great work by all!
  • Liferay LIVE continues its excellent series on all things Liferay.  This week Neil Griffin presented on Developing JSF 2 Portlets with ICEfaces, AlloyFaces, and LiferayFaces [slides now, video coming soon].  In the coming weeks we plan on doing encore presentations of several of  the East Coast Symposium presentations, so watch for those!
  • In a previous life I used to work on Accessibility, so I know how much attention to detail is required to make a site fully accessible.  Liferay makes it easy, as these sites can testify.  It's way more than alt attributes :)
  • Ben provides a nice overview of Liferay Layout Templates.
  • translate.liferay.com continues to enjoy tremendous success, giving our community a much-needed refresh of many of the translations provided out of box.  120 users and 41 languages, and the list keeps growing!  Top translations last week included German, Croation, Basque, Japanese, and Portugese.  And now, the core Liferay Portal translations are being managed through translate.liferay.com.  Wow!
  • Wondering about the benefits of an Alfresco + Liferay solution?  An interesting discussion has broken out on LinkedIn about the relative merits.  Take a look!
  • Couple of JIRA updates: we turned on rich text editing last week, so now your descriptions and comments can include all sorts of cool features.  Also, you may notice two new ticket types: "Epics" and "Stories".  We are going to begin using these to track Roadmap items and finally get rid of the always-outdated-the-moment-it-is-edited Roadmap wiki page.  This way the community can see, at a glance, what's coming down the pipe!
  • Creating new services using Liferay's Service Builder framework is now possible, graphically, using Liferay IDE.  This is now part of the nightly update, and slated for inclusion into the upcoming 1.3 release.  Nice work Greg et al!  [Also, a nice excerpt from Rich Sezov's Liferay in Action discusses Service Builder in detail].
  • The Holy Grail of Liferay Services would be a giant book that had a chapter for each Liferay service, along with descriptions of the underlying data model used to link everything together, java, javascript, and web services APIs to access the content, descriptions of all those esoteric formal parameters, etc.  While this isn't that, it's a neat start and a nice contribution.
  • Kristof provides us a fully-fleshed out example of embedded Web Content in your Liferay theme.  This keeps your editors from ever having to mess with your finely-crafted theme files!
  • Our very own Juan Fernández has released his latest beast of a contribution, the Wordpress Importer!  Bring all your content from Wordpress into Liferay with one click.  Very useful!
  • Interesting comparison of Liferay vs. Drupal.  Some of the items are questionable in their accuracy and even relevancy (why do we get an X next to PHP?). 
  • Recent Liferay Blog Posts:  Yet Another Liferay JSON Service Example, Liferay Portal Translation Project, Igor on JSON web service improvements, Liferay Is Not A Sausage Factory, and finally Liferay.com Mobile Sites and Responsive Layouts.
  • Recent Wiki Updates: Translation Team, WebDAV, JBoss Tips, Custom Logging, Accessible Sites, 100 PaperCuts, Liferay Development Style, JSON Serialization, JSON Web Services, and finally CMS Template (Velocity).

Ok, I said last time I'd do this every 2 weeks.  It's been 3 weeks.  That's an improvement from 4 weeks.  Baby steps, y'know? Finally, I'll leave you with my new desktop background pic.

Yet Another Liferay JSON Service Example

Company Blogs 10 de Junho de 2011 Por James Falkner Staff

The past couple of years I've seen many examples of using Liferay's built-in JSON services in various ways.  The architecture and syntax of this feature has undergone several refinements in the past few versions, so the documentation/examples you can find via a website search are usually slightly wrong and misleading.  Recently I saw this thread come alive and decided to sit down and make a non-trival read/write example of using Liferay's JSON services work.

Bear in mind that this applies to Liferay 6.0.x (I am using 6.0.6 CE in the examples).  In Liferay 6.1 there are new interfaces coming like (such as RESTful interfaces), ability to do automatic serialization and improved method argument passing, and there are also existing "heavy lifting" web service interfaces like SOAP endpoints that one can use. So this is not the only way to do things.  But it is great for prototyping and getting things to work quickly without dragging a bunch of dependencies and debugging hard-to-understand wire protocols.  I hope this example is still relevant!

The only dependency I am using here is Apache Commons HTTPClient.  I also decided to write it in Java (as opposed to Ray's earlier example on 5.2 in PHP).

Couple of things to be aware of:

  • By default, access is through Liferay's tunnel-web web app.  So the proper full URL in a default Liferay 6.0.6 install is http://localhost:8080/tunnel-web/secure/json .  
  • Since it is a "secure" (authenticated) interface we need to provide a username and password.  This is done using HTTP Basic Authentication, which of course is not appropriate for a production environment, since the password is unencrypted (it is instead base64-encoded following HTTP Basic Authentication).  The default username/password is "test/test".
  • There's no error checking whatsoever here.  You should add it for a real world scenario.

First Example

So, here's the first example.  A simple "Hello World" that does the same thing as Ray's example, only using Liferay 6 and written in simple Java.  It simply access the "Country" service and returns a list of country entities known to Liferay.

import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.BasicHttpContext;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

public class TestLiferayJSON {

    public static void main(String[] args) throws Exception {

        HttpHost targetHost = new HttpHost("localhost", 8080, "http");
        DefaultHttpClient httpclient = new DefaultHttpClient();
        httpclient.getCredentialsProvider().setCredentials(
                new AuthScope(targetHost.getHostName(), targetHost.getPort()),
                new UsernamePasswordCredentials("test", "test"));

        // Create AuthCache instance
        AuthCache authCache = new BasicAuthCache();
        // Generate BASIC scheme object and add it to the local
        // auth cache
        BasicScheme basicAuth = new BasicScheme();
        authCache.put(targetHost, basicAuth);

        // Add AuthCache to the execution context
        BasicHttpContext ctx = new BasicHttpContext();
        ctx.setAttribute(ClientContext.AUTH_CACHE, authCache);

        HttpPost post = new HttpPost("/tunnel-web/secure/json");

        List<NameValuePair> params = new ArrayList<NameValuePair>();
        params.add(new BasicNameValuePair("serviceClassName", "com.liferay.portal.service.CountryServiceUtil"));
        params.add(new BasicNameValuePair("serviceMethodName", "getCountries"));
        UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, "UTF-8");
        post.setEntity(entity);

        HttpResponse resp = httpclient.execute(targetHost, post, ctx);
        resp.getEntity().writeTo(System.out);
        System.out.println();
        httpclient.getConnectionManager().shutdown();
    }
}

If you compile and run this (you'll have to download the Apache Commons HTTPClient libraries and put them on the classpath and/or add them as dependencies in your IDE), then you should see something like this on your output screen (this is the returned content from the HTTP POST):

[{"countryId":20,"idd":"093","name":"Afghanistan","active":true,"a2":"AF","number":"4","a3":"AFG"},{"countryId":21,"idd":"355","name":"Albania","active":true,"a2":"AL","number":"8","a3":"ALB"},

.....many more countries listed

{"countryId":227,"idd":"263","name":"Zimbabwe","active":true,"a2":"ZW","number":"716","a3":"ZWE"}]

Notice where I specify the username/password using Apache HTTPClient APIs.  This should be easily translatable to your favorite client (or if you are using curl or some other RESTful client test console such as rest-client and you want to specify the authentication header manually, use an Authorization header with a value of Basic dGVzdDp0ZXN0Cg== )

Also note the serviceClassName parameter.  This name (com.lifery.portal.service.CountryServiceUtil) specifies the service name (and maps to an actual class).  Not all services have remote service endpoints (for example, there's no com.liferay.portlet.social.service.SocialActivityServiceUtil for creating new activity stream items.  I wish there were).

The parameters are encoded into the body of the HTTP POST request using the Apache Commons HTTPClient's UrlEncodedFormEntity utility.  This is the same as Ray's $a->addPostData examples in PHP.

Second Example

Ok, now that the easy one works (it does work for you, right?), let's move on to something trickier using the Web Content system.  Notice that this system used to be called "Journal" so all the APIs refer to the Journal Service since the actual APIs were not changed in the interest of compatibility.  This second example calls the JournalArticle service to retrieve a sample article using the default install's groupId of 10156 and the articleId of 10455.  These numbers are automatically generated during initial startup the first time and may be different for you.  If they are you'll need to change them.  You can find them through the Control Panel by going to Communities -> Actions -> Edit to find the groupId, and pick any articleId from Web Content.

This example calls a specific method by identifying it using its formal parameters and passes the values for the parameters.

 

    public static void journal() throws Exception {
        HttpHost targetHost = new HttpHost("localhost", 8080, "http");
        DefaultHttpClient httpclient = new DefaultHttpClient();
        httpclient.getCredentialsProvider().setCredentials(
                new AuthScope(targetHost.getHostName(), targetHost.getPort()),
                new UsernamePasswordCredentials("test", "test"));

        // Create AuthCache instance
        AuthCache authCache = new BasicAuthCache();
        // Generate BASIC scheme object and add it to the local
        // auth cache
        BasicScheme basicAuth = new BasicScheme();
        authCache.put(targetHost, basicAuth);

        // Add AuthCache to the execution context
        BasicHttpContext ctx = new BasicHttpContext();
        ctx.setAttribute(ClientContext.AUTH_CACHE, authCache);

        HttpPost post = new HttpPost("/tunnel-web/secure/json");

        // create Liferay API parameters
        List<NameValuePair> params = new ArrayList<NameValuePair>();
        params.add(new BasicNameValuePair("serviceClassName", "com.liferay.portlet.journal.service.JournalArticleServiceUtil"));
        params.add(new BasicNameValuePair("serviceMethodName", "getArticle"));
        params.add(new BasicNameValuePair("serviceParameters", "[groupId,articleId]"));
        params.add(new BasicNameValuePair("groupId", "10156"));
        params.add(new BasicNameValuePair("articleId", "10455"));
        UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, "UTF-8");
        post.setEntity(entity);

        // make actual HTTP request and print results to System.out
        HttpResponse resp = httpclient.execute(targetHost, post, ctx);
        resp.getEntity().writeTo(System.out);
        httpclient.getConnectionManager().shutdown();

    }

Notice here that the "setup" is exactly the same as before, only the parameters are different.  Also note that the list of parameter names (serviceParameters) starts and ends with brackets.  It's an array of Strings!  So don't forget the brackets.

The getArticle method returns a JSON-encoded article from Liferay's web content system, so if this example works for you, you should get this on your output stream:

{"urlTitle":"welcome","indexable":true,"statusDate":"1287600093000","type":"general","smallImageId":10458,"articleId":"10455","version":1,"id":10456,"title":"Welcome","description":"","userId":10134,"userName":" ","smallImage":false,"createDate":"1287600093000","displayDate":"1201824000000","smallImageURL":"","expirationDate":"","status":0,"statusByUserName":" ","reviewDate":"","modifiedDate":"1287600093000","content":...

Third Example

Ok, now that you a trivial and almost-trivial example, let's do something more interesting.  Let's add (and remove) a Journal Article (this is what the initial thread asked about anyway).  

Here are two methods: addArticle and removeArticle.  They use a hard-coded 60000 for articleId to make removal easy.  There are a ton of parameters for addArticle (29 to be exact), and since there are multiple addArticle methods in the JournalArticleServiceUtil class we have to specify a serviceParameterTypes list to tell Liferay which service API we wish to invoke.  So the parameter list gets nasty and I didn't do a very good job of coding it to look nice.  You can do that though.

 

  public static void addArticle() throws Exception {
        HttpHost targetHost = new HttpHost("localhost", 8080, "http");
        DefaultHttpClient httpclient = new DefaultHttpClient();
        httpclient.getCredentialsProvider().setCredentials(
                new AuthScope(targetHost.getHostName(), targetHost.getPort()),
                new UsernamePasswordCredentials("test", "test"));

        // Create AuthCache instance
        AuthCache authCache = new BasicAuthCache();
        // Generate BASIC scheme object and add it to the local
        // auth cache
        BasicScheme basicAuth = new BasicScheme();
        authCache.put(targetHost, basicAuth);

        // Add AuthCache to the execution context
        BasicHttpContext ctx = new BasicHttpContext();
        ctx.setAttribute(ClientContext.AUTH_CACHE, authCache);

        HttpPost post = new HttpPost("/tunnel-web/secure/json");
        Calendar yesterday = Calendar.getInstance();
        yesterday.add(Calendar.DAY_OF_YEAR, -1);
        Calendar nextWeek = Calendar.getInstance();
        nextWeek.add(Calendar.WEEK_OF_YEAR, 1);
        List<NameValuePair> params = new ArrayList<NameValuePair>();
        params.add(new BasicNameValuePair("serviceClassName", "com.liferay.portlet.journal.service.JournalArticleServiceUtil"));
        params.add(new BasicNameValuePair("serviceMethodName", "addArticle"));
        params.add(new BasicNameValuePair("serviceParameters", "[groupId,articleId,autoArticleId,title,description,content,type,structureId,templateId,displayDateMonth,displayDateDay,displayDateYear,displayDateHour,displayDateMinute,expirationDateMonth,expirationDateDay,expirationDateYear,expirationDateHour,expirationDateMinute,neverExpire,reviewDateMonth,reviewDateDay,reviewDateYear,reviewDateHour,reviewDateMinute,neverReview,indexable,articleURL,serviceContext]"));
        params.add(new BasicNameValuePair("serviceParameterTypes", "[long,java.lang.String,boolean,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,int,int,int,int,int,int,int,int,int,int,boolean,int,int,int,int,int,boolean,boolean,java.lang.String,com.liferay.portal.service.ServiceContext]"));
        params.add(new BasicNameValuePair("groupId", "10156"));
        params.add(new BasicNameValuePair("articleId", "60000"));
        params.add(new BasicNameValuePair("autoArticleId", "false"));
        params.add(new BasicNameValuePair("title", "Test JSON Article"));
        params.add(new BasicNameValuePair("description", "Test JSON Description"));
        params.add(new BasicNameValuePair("content", "<?xml version='1.0' encoding='UTF-8'?><root available-locales=\"en_US\" default-locale=\"en_US\"><static-content language-id=\"en_US\"><![CDATA[<p>\n" +
                "\ttest content</p>]]></static-content></root>"));
        params.add(new BasicNameValuePair("type", "general"));
        params.add(new BasicNameValuePair("structureId", ""));
        params.add(new BasicNameValuePair("templateId", ""));
        params.add(new BasicNameValuePair("displayDateMonth", "" + (1 + yesterday.get(Calendar.MONTH))));
        params.add(new BasicNameValuePair("displayDateDay", "" + yesterday.get(Calendar.DAY_OF_MONTH)));
        params.add(new BasicNameValuePair("displayDateYear", "" + yesterday.get(Calendar.YEAR)));
        params.add(new BasicNameValuePair("displayDateHour", "" + yesterday.get(Calendar.HOUR_OF_DAY)));
        params.add(new BasicNameValuePair("displayDateMinute", "" + yesterday.get(Calendar.MINUTE)));
        params.add(new BasicNameValuePair("expirationDateMonth", "" + (1 + nextWeek.get(Calendar.MONTH))));
        params.add(new BasicNameValuePair("expirationDateDay", "" + nextWeek.get(Calendar.DAY_OF_MONTH)));
        params.add(new BasicNameValuePair("expirationDateYear", "" + nextWeek.get(Calendar.YEAR)));
        params.add(new BasicNameValuePair("expirationDateHour", "" + nextWeek.get(Calendar.HOUR_OF_DAY)));
        params.add(new BasicNameValuePair("expirationDateMinute", "" + nextWeek.get(Calendar.MINUTE)));
        params.add(new BasicNameValuePair("neverExpire", "false"));
        params.add(new BasicNameValuePair("reviewDateMonth", "" + (1 + nextWeek.get(Calendar.MONTH))));
        params.add(new BasicNameValuePair("reviewDateDay", "" + nextWeek.get(Calendar.DAY_OF_MONTH)));
        params.add(new BasicNameValuePair("reviewDateYear", "" + nextWeek.get(Calendar.YEAR)));
        params.add(new BasicNameValuePair("reviewDateHour", "" + nextWeek.get(Calendar.HOUR_OF_DAY)));
        params.add(new BasicNameValuePair("reviewDateMinute", "" + nextWeek.get(Calendar.MINUTE)));
        params.add(new BasicNameValuePair("neverReview", "false"));
        params.add(new BasicNameValuePair("indexable", "true"));
        params.add(new BasicNameValuePair("articleURL", "articleURL"));
        params.add(new BasicNameValuePair("serviceContext", "{}"));
        UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, "UTF-8");
        post.setEntity(entity);
        HttpResponse resp = httpclient.execute(targetHost, post, ctx);
        System.out.println(resp.getStatusLine());
        resp.getEntity().writeTo(System.out);
        httpclient.getConnectionManager().shutdown();

    }

    public static void removeArticle() throws Exception {
        HttpHost targetHost = new HttpHost("localhost", 8080, "http");
        DefaultHttpClient httpclient = new DefaultHttpClient();
        httpclient.getCredentialsProvider().setCredentials(
                new AuthScope(targetHost.getHostName(), targetHost.getPort()),
                new UsernamePasswordCredentials("test", "test"));

        // Create AuthCache instance
        AuthCache authCache = new BasicAuthCache();
        // Generate BASIC scheme object and add it to the local
        // auth cache
        BasicScheme basicAuth = new BasicScheme();
        authCache.put(targetHost, basicAuth);

        // Add AuthCache to the execution context
        BasicHttpContext ctx = new BasicHttpContext();
        ctx.setAttribute(ClientContext.AUTH_CACHE, authCache);

        HttpPost post = new HttpPost("/tunnel-web/secure/json");
        Calendar now = Calendar.getInstance();
        Calendar nextWeek = Calendar.getInstance();
        nextWeek.add(Calendar.WEEK_OF_YEAR, 1);
        List<NameValuePair> params = new ArrayList<NameValuePair>();
        params.add(new BasicNameValuePair("serviceClassName", "com.liferay.portlet.journal.service.JournalArticleServiceUtil"));
        params.add(new BasicNameValuePair("serviceMethodName", "deleteArticle"));
        params.add(new BasicNameValuePair("serviceParameterTypes", "[long,java.lang.String,java.lang.String,com.liferay.portal.service.ServiceContext]"));
        params.add(new BasicNameValuePair("serviceParameters", "[groupId,articleId,articleURL,serviceContext]"));
        params.add(new BasicNameValuePair("groupId", "10156"));
        params.add(new BasicNameValuePair("articleId", "60000"));
        params.add(new BasicNameValuePair("articleURL", "articleURL"));
        params.add(new BasicNameValuePair("serviceContext", "{}"));

        UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, "UTF-8");
        post.setEntity(entity);
        HttpResponse resp = httpclient.execute(targetHost, post, ctx);
        System.out.println(resp.getStatusLine());
        resp.getEntity().writeTo(System.out);
        httpclient.getConnectionManager().shutdown();

    }

More Notes:

  • I specified "" (blank) strings for the structureId and templateId.  This means that the article has no structure and no template, so it will just be interpreted as raw HTML (as though you had created a new Web Content Article and didn't specify a structure or template).
  • The content parameter is XML with the content inside of a CDATA block.  If there were an associated structure for this content, the content would look different.  That's for an advanced reader to explore.
  • The timestamps use Java's Calendar method to set the display date and other dates.
  • The articleId is hard-coded to 60000.  If you wish to auto-generate specify autoArticleId to be true.
  • The serviceContext parameter is special and tricky and not well documented.  The "content" of the parameter is a JSON-serialized instance of the com.liferay.portal.service.ServiceContext class.  For other services, it might require a more complex serialized instance of the ServiceContext class.  For example, instead of {} (which, which decoded, results in a simple new instance of the ServiceContext class with null/empty/blank/0 for all of its properties), one might have something like {scopeGroupId:themeDisplay.getScopeGroupId()} (I took this from this example). Anyway, for this example, an empty {} works.  

If addArticle works, you should get in return a serialized version of the new article:

{"urlTitle":"test-json-article","indexable":true,"statusDate":"","type":"general","smallImageId":14540,"articleId":"60000","version":1,"id":14538,"title":"Test JSON Article","description":"Test JSON Description","userId":10168,"userName":"Test Test","smallImage":false,"createDate":"1307729885333","displayDate":"1310221080000","smallImageURL":"","expirationDate":"1310912280000","status":2,"statusByUserName":"","reviewDate":"1310912280000","modifiedDate":"1307729885333","content":"<?xml version='1.0' encoding='UTF-8'?><root available-locales=\"en_US\" default-locale=\"en_US\"><static-content language-id=\"en_US\"><![CDATA[<p>\n\ttest content<\/p>]]><\/static-content><\/root>","templateId":"","groupId":10156,"resourcePrimKey":14539,"structureId":"","statusByUserId":0,"companyId":10131,"uuid":"d2a09ad8-43d5-476b-bcb9-3a1621409835"}

Since deleteArticle returns void, you won't get anything (But the article should be gone, which you can verify in the GUI, or via a database browser).  

Good luck!

Community Roundup

Company Blogs 19 de Maio de 2011 Por James Falkner Staff

Wow, has it really been one month since my last roundup?  Apologies!  As usual, there are many moving parts to the World of Liferay and I spend a lot of time trying to keep up with it all, and sometimes things drop off the radar.  But they always come back, thanks to my handy organizer (aka TextWrangler).  We've been very busy here in the Liferay Community, and it will be getting more action-packed as the year progresses.  So grab your coffee and start clickin':

That is all for now.  I'll try to do more regular roundups (like every 2 weeks).  I hope I don't have to break Twitter's storage policies :-)

Liferay on Amazon Elastic Beanstalk and EC2

Company Blogs 3 de Maio de 2011 Por James Falkner Staff

Recently I came across a forum post asking about deploying Liferay on Amazon's Elastic Beanstalk.  Elastic Beanstalk (EB) is basically a managed Amazon EC2 instance running Linux and a pre-installed Tomcat.  You can install (web) apps into that container, and with clever configuration (and your credit card information) it will automatically scale horizontally and vertically as usage demands rise or fall. Pretty sweet right?  This is a part of being in "The Cloud".  Since it runs Tomcat, it shouldn't be too hard to get Liferay running in this environment, should it?  Turns out, it's actually not that hard.  

For this example, I used a stock Liferay configuration, using embedded HSQLDB.  Clever folks can configure this to use Amazon's RDS service.

For the impatient/advanced

Here's a quick rundown of what you need to do:

  1. On your local system, create custom Liferay WAR file which includes missing dependencies and a custom portal-ext.properties file
  2. On EB, Create new EB Application, and upload your custom WAR file
  3. Configure JVM instance for higher Java memory settings
  4. (optional) Configure EC2 instance to use SSH
  5. Restart

Details:

First, as of this date, Elastic Beanstalk runs Tomcat 6.0.32.  Pretty close to the 6.0.29 that Liferay 6.0.6 ships with.  However, the version that Liferay Bundles ship with are slightly modified to configure things like default network ports, classloader behavior, filesystem paths, dependent libraries, and other various things.  So, to successfully deploy to EB, we will need to build a custom Liferay WAR file that includes the things that the EB Tomcat is missing, and configure ${liferay.home}.  So, to create this custom app:

  1. Download and un-jar the stock Liferay WAR file into some temporary directory (for example, download the .war file to /tmp and extract it to /tmp/lr-eb-war).  Save the original .war, you'll be updating it shortly.
  2. Download and un-jar the stock Liferay Tomcat Bundle into some other place (for example /tmp/lr-606-bundle). You'll be borrowing libraries from it shortly.
  3. Copy all of the global jar files from the Liferay Tomcat Bundle in lib/ext to your custom Liferay WAR file directory
    1. cp /tmp/lr-606-bundle/liferay-portal-6.0.6/tomcat-6.0.29/lib/ext/*.jar /tmp/lr-eb-war/WEB-INF/lib
  4. Create a text file /tmp/lr-eb-war/WEB-INF/classes/portal-ext.properties which includes a single line: liferay.home=/tmp/liferay-home-eb
  5. Add these files back into the stock Liferay WAR file using the jar utility (note these instructions will work on unix/Linux/Mac OS X  Windows users can use a utility like 7-Zip or InfoZip or something else to accomplish the same thing).
    1. cd /tmp/lr-eb-war
    2. jar uvf ../liferay-portal-6.0.6-20110225.war WEB-INF/classes/portal-ext.properties WEB-INF/lib/*.jar

Once this is done, you're ready to create an EB account and log in.  Go here and click "Begin Using AWS Elastic Beanstalk" to create a new account. After creating an account, giving Amazon your credit card info, confirming your account via telephone, go to the main AWS Console screen.

Create a new EB Application, selecting to upload a custom file, and select to create a new deployment environment.  Make sure to pick the t1.micro configuration, otherwise you'll probably blow right past the "free" limits and start accruing charges on your credit card.  Better to save your money for the Liferay EE license and beefier EC2 instance later in production ;-)

After clicking "Finish" it will take a while to upload the (~130MB) custom WAR file.  You'll stare at a spinner for a while, with no indication of progress. Once you get the "Your Application has successfully been created" message you'll be ready to proceed.  You'll be placed at the AWS (Amazon Web Services) Console.  The only two tabs you'll use here is the "Elastic Beanstalk" tab (to configure Tomcat and applications) and the "EC2" tab to configure the virtual machines on which Tomcat runs.

Memory Configuration

Once the upload is complete, Amazon will create a virtual machine running Linux, put a load balancer in front, create a DNS record, deploy a bunch of other junk (such as an Auto Scaling service to add more machines when needed), launch the virtual machine and Tomcat, deploying your custom Liferay web app, but it'll be broken because it's not configured correctly and probably ran out of memory.  Ignore any errors in any log file output you may be able to see.   Once you get Liferay up and running you can play with all this stuff to your heart's content.  For now, let's go configure things so that Liferay works.

You'll have to wait for the environment to be fully "up" (might take 5-10 minutes) before you can start tweaking configuration.  Once the spinner is done spinning and everything seems steady-state,  click on the "Edit Configuration" link:

If things aren't ready to be configured, it'll tell you.  Here, you can see all the wonderful things you can configure, but the only thing you need to do here is on the Container tab.  Set the memory settings as shown below:

Click "Apply Changes".

Your First Liferay On The Cloud

If all goes well, Liferay should eventually come up (After 5-10 minutes).  On the main AWS Console, if the status turns to "Green" (you can also watch the "Events" tab), then you're good to go!  Click on the "Overview" tab and click on "View Running Version" to see and log into your new Liferay Instance On The Cloud.  Welcome to Web five-dot-oh :)  (You can also get the hostname from the Environment Details screen, and it's the same hostname you initially configured when setting up things at the beginning).

Troubleshooting

If things don't go so well, and you want to do traditional administration from the command line (and really, who doesn't?), you need to log into your running OS using ssh.  To do that, you need to create a keypair, download the private key, apply the key pair to the configuration for your EC2 instance, and open up port 22 (the ssh port) on the EC2 instance, and finally use your favorite ssh utility to ssh (or scp, or sftp, or...) into the virtual machine.

SSH: Creating a keypair

Here is where you use the "EC2" tab at the very top of your AWS console.  Click on it, and click "Key Pairs" on the left.  It'll tell you you don't have any.  Click "Create Key Pair" to create a new one.

Give it any name, and click create.  It'll automatically download a .pem file that contains the "private" side of this key pair.  This probably violates several PKI best practices, but what are you gonna do?  Store this file somewhere convenient, you'll be using it in a bit when ssh'ing to your running virtual machine. You'll also need to chmod 400 the file if on Unix/Linux/Mac OS X.  Otherwise ssh will refuse to use it, citing security concerns.  Thanks ssh.

SSH: Apply the Key Pair to your EC2 instance

You need to associate the newly-created key pair to your machine (this effectively configures SSH on the linux box).  To do this, go back to the "Elastic Beanstalk" tab at the very top of the screen.  Under "Environment Details" click on "Edit Configuration".  On the "Server" tab of this dialog, enter the name of your newly created keypair in the "Existing Key Pair" field.  Note there is no auto-completion or selection here.  You have to type in the full name (bah!!). Click "Apply Changes" and agree to the little warning that comes up:

SSH: Configure the Security Group

When you first created your Elastic Beanstalk application, Amazon automatically allocated a virtual machine to you, and applied a default Security Group configuration to it.  The Security Group, among other things, defines a set of networking rules that allow or disallow network traffic to and from the EC2 instance running Tomcat/Liferay.  You need to add ssh as one of the "allowed" applications for which network traffic is permitted.  Go back to the "EC2" tab at the very top of your screen, and click on "Security Groups" on the left, then click on the "elasticbeanstalk-default" group.  At the bottom, click on the "Inbound" tab, and create a new rule, selecting SSH from the "Create a new rule" dropdown:

Then click "Apply Rule Change" at the bottom of the "Inbound" tab.  This opens up port 22 to your EC2 instance so you can ssh to your running Linux instance.

SSH: Using The Command Line

Now that port 22 is open, you can ssh.  Again, these instructions work on unix/Linux/Mac OS X.  Windows users can use things like PuTTY to ssh to the machine.  To ssh to your instance, you need to know the DNS name of the virtual machine (Amazon dynamically allocates these so there's no way to guess).  To figure this out, click on the "Instances" tab on the left.  There may be multiple instances listed here.  You want the one that has the associated key pair mentioned in the "Key Pair Name" column.  Right-click on this, and select "Connect".  It'll show you the appropriate command line (you'll need to change the path to the saved .pem private key file from earlier).  It also shows you the DNS hostname of your running instance:

Note that the sample cut-and-pastable command line they give uses the root user.  Amazon has since updated their policy to require you to ssh as the ec2-user user.  You'll get error if you attempt to ssh as the root user.  So don't.  Here's the example, correct command line I use:

Once in, it's like any other Linux shell.  You can see the Tomcat process and other interesting info using ps and other utiities:

Notice Liferay is already deployed into Tomcat, located at /usr/share/tomcat6/webapps/ROOT.  You can edit anything under here to tweak Liferay.  However, you can't alter the Tomcat configuration directly, as the application files and execution runtime is owned by root, and you are merely ec2-user.  You can look in /tmp to see the Liferay Home directory you specified earlier in your portal-ext.properties.

That's it for now, hope you find it useful.  If you expand on this (e.g. configure RDS, or do some other cool thing), leave comments below!  Some improvements could be:

  • Configuring the JVM so that you don't have to inject all of the Liferay dependency JARs into the main Liferay web app.  This would allow future deployed Liferay extensions to use the same libraries and avoid classloader issues.
  • Configure Liferay IDE to be able to deploy to EB instances automatically given your login info
  • Configure the Liferay Home directory to not be in /tmp.

Have fun!

Social Visualization and Analytics with Liferay

Company Blogs 20 de Abril de 2011 Por James Falkner Staff

Recently I did a Liferay LIVE presentation on driving community participation with Liferay.  I'll post links to the video and slides once they are available.  The main topic was how to encourage community participation on sites that are based on Liferay.  There are many features in Liferay that make it easy to create and maintain good community sites.  One of the most underrated features in Liferay 6 is Social Equity, which is a social value system, essentially assigning a number to each person or piece of content.  The number represents the quality of the thing it is assigned to.  So an individual's equity value is a measure of their value to the community, based on content they've created and feedback from other users.  The value assigned to a piece of content (such as a blog post) represents the value of the content itself, based on user feedback.  Higher valued content is "better" in some way than lesser-valued content.  Same for people.

There have been a couple of writeups made regarding Social Equity in Liferay, and make for a great pre-read before continuing on this blog post!

Once Social Equity is enabled by an Administrator, applications which enable user content generation in Liferay (such as Blogs, Wikis, Message Boards, and Web Content) will contribute equity to users and content (information)  when actions occur (such as authoring a blog post, rating a forum thread, or commenting on a wiki page).  These equity values can then be exposed via visualization widgets such as Liferay's Top Users portlet.

One of the things missing in today's Liferay are other interesting visualizations of Social Equity data.  There are a bunch of use cases that Social Equity is good for, and if we had visualizations for them it would really encourage community participation.  Here are several of them:

  • Show the overall contribution and participation equity across all communities for a user
  • Show all tags associated with content created by a user, ordered by Tag Equity
    • Objective: show expertise of a user
  • Show all contributions from a user ordered by information equity
    • Objective: show a user’s contributions with high value
  • Show the top xx communities
    • Objective: Motivate communities to improve ranking
  • Show the top xx contributors or countries of all or a specific community
    • Objective: Motivate people to get into a top ranking
  • Show the top xx content of all or a specific community
    • Objective: Show the most valued content in a community
  • Show the top xx experts for tag yy
    • Objective: Show top experts for a given subject

These are highly useful visualizations, and are possible using basic equity information available to Liferay applications.  Unfortuntely, they do not exist today.  This is where you and I come in!  Below are two examples of simple yet powerful visualizations that align with two of the above use cases.  Can you think of more use cases that involve personal, tag, or information equity?

1. Equity-based Tag Cloud

This example builds on the popular notion of a Tag Cloud (indeed, Liferay has a built-in Tag Cloud portlet, but this portlet ranks tags solely on number of mentions of a tag, regardless of social value).  Ultimately it would be nice if the built-in portlet had this option (to order by equity), but it doesn't yet, and for this demo, I found an nifty 3D tag cloud (which relies on jQuery).  To calculate equity of a tag, you simply add up all the information equities for content that has been tagged with a given tag.  So, the pseudocode for this algorithm is:

for each asset a in community {

  for each tag t on asset a {

    Add [t, equity(a)] to Map<Tag, EquitySum> m;

  }

}

display resulting map m in tag sphere;

In this example, each tag's equity value is normalized into a range [0, 100), so that tags with higher equity values have larger font sizes.  In addition, when you click on a tag, it uses Liferay's Shared Render Parameter support to cause any other portlets on that page that support the tag parameter to update their displays to only show content tagged with that specific tag.

One thing to keep in mind with the below code: it is not very performant!  Every time the portlet is rendered, the equity sums are re-calculated.  For lots of tags or assets, this does not scale.  A real solution would calculate and store tag equities as actions occur, to avoid having to re-calculate every time.  I leave that as an exercise to the reader.

This portlet is a very basic MVCPortlet-based portlet.  There is no java portlet class.  There are just a couple of JSPs.  It is the default portlet type when using the Liferay IDE.  So a very easy way to use these examples is to create a new Liferay Portlet project in Liferay IDE, cut and paste the below files on top of the boilerplate files that Liferay IDE gives you, and deploy to your configured Liferay instance.  I also didn't bother to factor out imports, includes, or CSS into separate files, just for simplicity.  So everything is inline (please don't kick me out of the developer club).  The portlet uses Liferay's Asset Framework APIs, Tag APIs, and Social Equity APIs.

Finally, there are some tunable parameters for this, which are exposed using Liferay's Portlet Configuration Framework.  This way, to tune a tunable, you simply visit the portlet's configuration page, edit the value, and click save. That is represented by entries in liferay-portlet.xml and the configuration.jsp source code found below.  This uses the AlloyUI component library to render configuration items.

  • view.jsp (main algorithm and html):
<%@page import="com.liferay.portal.util.PortalUtil"%>
<%@page import="java.util.Calendar"%>
<%@page import="java.util.Collections"%>
<%@page import="java.util.Comparator"%>
<%@page import="java.util.Date"%>
<%@page import="com.liferay.portlet.asset.model.AssetTag"%>
<%@page import="java.util.ArrayList"%>
<%@page import="java.util.HashMap"%>
<%@page import="java.util.Map"%>
<%@page import="com.liferay.portal.kernel.util.GetterUtil"%>
<%@page import="com.liferay.portlet.PortletPreferencesFactoryUtil"%>
<%@page import="com.liferay.portal.kernel.util.Validator"%>
<%@page import="com.liferay.portal.kernel.util.ParamUtil"%>
<%@page import="javax.portlet.PortletPreferences"%>

<%@page
        import="com.liferay.portlet.asset.service.AssetEntryLocalServiceUtil"%>
<%@page import="com.liferay.portlet.asset.model.AssetEntry"%>
<%@page import="java.util.List"%>
<%@page
        import="com.liferay.portlet.asset.service.persistence.AssetEntryQuery"%>
<%@ taglib uri="http://java.sun.com/portlet" prefix="portlet"%>

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

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

<style type="text/css">
.tagCloud ul,.tagCloud li {
        list-style: none;
        margin: 0;
        padding: 0;
}

.tagCloud .tagClass img {
        border: 0 none;
}

.tagCloud .tagClass a {
        font-family: Arial;
        font-weight: normal;
        padding: 3px;
        text-align: center;
        text-decoration: none;
        vertical-align: middle;
        color: yellow;
}

.tagCloud .tagClass a:hover {
        border: solid 1px #ffdb00;
        color: #ff00ff;
        text-decoration: none;
}

#tagCloud1 {
        -moz-border-radius: 15px;
        border-radius: 15px;
        background: grey;
}
</style>
<%

PortletPreferences preferences = renderRequest.getPreferences();

String portletResource = ParamUtil.getString(request,
                "portletResource");

if (Validator.isNotNull(portletResource)) {
        preferences = PortletPreferencesFactoryUtil.getPortletSetup(
                        request, portletResource);
}

int MAX_TAGCLOUD = GetterUtil.getInteger(preferences.getValue(
                "maxTags", "50"));
double ANIMATION_TIME = (GetterUtil.getInteger(preferences.getValue(
                "animationTime", "1"))) / 10.0;

int EQ_CUTOFF = GetterUtil.getInteger(preferences.getValue(
                "eqCutoff", "1"));

final int EQ_NORMALIZED_RANGE = 100;

        // get all assets in this group (community)
        AssetEntryQuery q = new AssetEntryQuery();
        long[] ids = new long[] { scopeGroupId };
        q.setGroupIds(ids);
        q.setVisible(true);
        List<AssetEntry> entries = AssetEntryLocalServiceUtil.getEntries(q);
        final Map<String, Double> eqValues = new HashMap<String, Double>();
        final Map<String, Integer> eqNormal = new HashMap<String, Integer>();
        double maxEq = 0;

        // iterate over all assets and their tags, and build equity map
        for (AssetEntry e : entries) {
                for (AssetTag tagA : e.getTags()) {
                        Double oldVal = eqValues.get(tagA.getName());
                        if (oldVal == null) {
                                oldVal = (double)0;
                        }
                        Double newVal = oldVal + e.getSocialInformationEquity();

                        eqValues.put(tagA.getName(), newVal);
                        if (newVal > maxEq) maxEq = newVal;
                }
        }

        // normalize equity values to range [0, EQ_NORMALIZED_RANGE]
        for (String tag : eqValues.keySet()) {
                eqNormal.put(tag, (int)Math.round( ((double)eqValues.get(tag) / maxEq) * (double)EQ_NORMALIZED_RANGE));
        }

        // generate sorted list of tag equity values, eliminating those under the cutoff value
        List<String> sortedEq = new ArrayList<String>();
        for (Map.Entry<String, Double> entry : eqValues.entrySet()) {
                if (entry.getValue() > EQ_CUTOFF) {
                        sortedEq.add(entry.getKey());
                }
        }

        // sort the resulting list
        Collections.sort(sortedEq, new Comparator<String>() {
                public int compare(String s1, String s2) {
                        double eq1 = eqValues.get(s1);
                        double eq2 = eqValues.get(s2);
                        if (eq1 == eq2) return 0;
                        else if (eq2 > eq1) return 1;
                        else return -1;
                }
        });
        
        // truncate so our tag sphere isn't too CPU-intensive
        if (sortedEq.size() > MAX_TAGCLOUD) {
                sortedEq.subList(MAX_TAGCLOUD, sortedEq.size() - 1).clear();
        }
%>

<div id="tagCloud1"></div>

<script
        src="<%= PortalUtil.getStaticResourceURL(request, request.getContextPath() + "/js/jquery-1.4.1.js", new Date().getTime()) %>"
        type="text/javascript"></script>
<script
        src="<%= PortalUtil.getStaticResourceURL(request, request.getContextPath() + "/js/js-cumulus.min.js", new Date().getTime()) %>"
        type="text/javascript"></script>

<script type="text/javascript">

var tagCloud1;

var tags = [
        <%
                for (String tag : sortedEq) {
                        Integer tagEqNormal = eqNormal.get(tag);
        %>
        
        new Tag("<%=tag%>",<%=tagEqNormal%>, "<portlet:renderURL><portlet:param name="tag" value="<%=tag%>"/></portlet:renderURL>"),
        <%
                }
        %>
        ];
        tagCloud1 = new TagCloud(document.getElementById("tagCloud1"), tags, 400, 400, {AnimationTime:<%=ANIMATION_TIME%>, HoverStop:false});
        
</script> 
  • configuration.jsp (used to configure tunables such as maximum # of tags to display):
<%@page import="com.liferay.portal.kernel.util.Constants"%>
<%@page import="com.liferay.portal.kernel.util.GetterUtil"%>
<%@page import="com.liferay.portlet.PortletPreferencesFactoryUtil"%>
<%@page import="com.liferay.portal.kernel.util.Validator"%>
<%@page import="com.liferay.portal.kernel.util.ParamUtil"%>
<%@page import="javax.portlet.PortletPreferences"%>
<%@ taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet"%>
<%@ taglib uri="http://liferay.com/tld/aui" prefix="aui"%>
<%@ taglib uri="http://liferay.com/tld/portlet" prefix="liferay-portlet"%>
<%@ taglib uri="http://liferay.com/tld/security"
        prefix="liferay-security"%>
<%@ taglib uri="http://liferay.com/tld/theme" prefix="liferay-theme"%>
<%@ taglib uri="http://liferay.com/tld/ui" prefix="liferay-ui"%>
<%@ taglib uri="http://liferay.com/tld/util" prefix="liferay-util"%>

<%@ page contentType="text/html; charset=UTF-8"%>

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

<%
        PortletPreferences preferences = renderRequest.getPreferences();

        String portletResource = ParamUtil.getString(request,
                        "portletResource");

        if (Validator.isNotNull(portletResource)) {
                preferences = PortletPreferencesFactoryUtil.getPortletSetup(
                                request, portletResource);
        }

        int maxTags = GetterUtil.getInteger(preferences.getValue(
                        "maxTags", "50"));
        int animationTime = GetterUtil.getInteger(preferences.getValue(
                        "animationTime", "1"));
        int eqCutoff = GetterUtil.getInteger(preferences.getValue(
                        "eqCutoff", "1"));
%>

<liferay-portlet:actionURL portletConfiguration="true"
        var="configurationURL" />

<aui:form action="<%= configurationURL %>" method="post" name="fm">
        <aui:input name="<%= Constants.CMD %>" type="hidden"
                value="<%= Constants.UPDATE %>" />
        <aui:fieldset>

                <aui:select label="Maximum Tags to Display" name="preferences--maxTags--">
                        <aui:option label="5" selected="<%= maxTags == 5 %>" />
                        <aui:option label="10" selected="<%= maxTags == 10 %>" />
                        <aui:option label="15" selected="<%= maxTags == 15 %>" />
                        <aui:option label="20" selected="<%= maxTags == 20 %>" />
                        <aui:option label="30" selected="<%= maxTags == 30 %>" />
                        <aui:option label="40" selected="<%= maxTags == 40 %>" />
                        <aui:option label="50" selected="<%= maxTags == 50 %>" />
                        <aui:option label="100" selected="<%= maxTags == 100 %>" />
                </aui:select>
                <aui:select label="Animation Time" name="preferences--cutoffDays--">
                        <aui:option label="1" selected="<%= animationTime == 1 %>" />
                        <aui:option label="2" selected="<%= animationTime == 2 %>" />
                        <aui:option label="3" selected="<%= animationTime == 3 %>" />
                        <aui:option label="4" selected="<%= animationTime == 4 %>" />
                        <aui:option label="5" selected="<%= animationTime == 5 %>" />
                        <aui:option label="6" selected="<%= animationTime == 6 %>" />
                        <aui:option label="7" selected="<%= animationTime == 7 %>" />
                        <aui:option label="8" selected="<%= animationTime == 8 %>" />
                        <aui:option label="9" selected="<%= animationTime == 9 %>" />
                        <aui:option label="10" selected="<%= animationTime == 10 %>" />
                </aui:select>
                <aui:select label="Equity Cutoff"
                        name="preferences--eqCutoff--">
                        <aui:option label="1" selected="<%= eqCutoff == 1 %>" />
                        <aui:option label="5" selected="<%= eqCutoff == 5 %>" />
                        <aui:option label="10" selected="<%= eqCutoff == 10 %>" />
                        <aui:option label="15" selected="<%= eqCutoff == 15 %>" />
                        <aui:option label="20" selected="<%= eqCutoff == 20 %>" />
                        <aui:option label="25" selected="<%= eqCutoff == 25 %>" />
                        <aui:option label="30" selected="<%= eqCutoff == 30 %>" />
                        <aui:option label="60" selected="<%= eqCutoff == 60 %>" />
                        <aui:option label="70" selected="<%= eqCutoff == 70 %>" />
                        <aui:option label="80" selected="<%= eqCutoff == 80 %>" />
                        <aui:option label="90" selected="<%= eqCutoff == 90 %>" />
                        <aui:option label="100" selected="<%= eqCutoff == 100 %>" />
                        <aui:option label="110" selected="<%= eqCutoff == 110 %>" />
                        <aui:option label="150" selected="<%= eqCutoff == 150 %>" />
                        <aui:option label="200" selected="<%= eqCutoff == 200 %>" />
                        <aui:option label="500" selected="<%= eqCutoff == 500 %>" />
                </aui:select>

        </aui:fieldset>
        <aui:button-row>
                <aui:button type="submit" />
        </aui:button-row>


</aui:form>
  • portlet.xml: You need to add the <supported-public-render-parameter> and <public-render-parameter> elements to your portlet.xml, or use the below code in its entirety.  If you use the below code as-is, note that your portlet name must be "EquitySphere" in your other files (it may be easiest to name your project and portlet EquitySphere if you intend to use the below code as-is).
<?xml version="1.0"?>

<portlet-app
        version="2.0"
        xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"
>
        <portlet>
                <portlet-name>EquitySphere</portlet-name>
                <display-name>EquitySphere</display-name>
                <portlet-class>com.liferay.util.bridges.mvc.MVCPortlet</portlet-class>
                <init-param>
                        <name>view-jsp</name>
                        <value>/view.jsp</value>
                </init-param>
                <expiration-cache>0</expiration-cache>
                <supports>
                        <mime-type>text/html</mime-type>
                </supports>
                <portlet-info>
                        <title>EquitySphere</title>
                        <short-title>EquitySphere</short-title>
                        <keywords>EquitySphere</keywords>
                </portlet-info>
                <security-role-ref>
                        <role-name>administrator</role-name>
                </security-role-ref>
                <security-role-ref>
                        <role-name>guest</role-name>
                </security-role-ref>
                <security-role-ref>
                        <role-name>power-user</role-name>
                </security-role-ref>
                <security-role-ref>
                        <role-name>user</role-name>
                </security-role-ref>
                <supported-public-render-parameter>                         tag                 </supported-public-render-parameter>                 
        </portlet>
        <public-render-parameter>                 <identifier>tag</identifier>                 <qname xmlns:x="http://www.liferay.com/public-render-parameters">x:tag</qname>         </public-render-parameter>         
</portlet-app>
  • liferay-portlet.xml (contains declaration of ConfigurationImpl class - using Liferay's default so I don't have to write my own ConfigurationImpl but instead follow the naming convention for preference names, such as preferences--cutoffDays--.
<?xml version="1.0"?>
<!DOCTYPE liferay-portlet-app PUBLIC "-//Liferay//DTD Portlet Application 6.0.0//EN" "http://www.liferay.com/dtd/liferay-portlet-app_6_0_0.dtd">

<liferay-portlet-app>
        <portlet>
                <portlet-name>EquitySphere</portlet-name>
                <icon>/icon.png</icon>
                <configuration-action-class>com.liferay.portal.kernel.portlet.DefaultConfigurationAction</configuration-action-class>
                <instanceable>false</instanceable>
                <header-portlet-css>/css/main.css</header-portlet-css>
                <footer-portlet-javascript>/js/main.js</footer-portlet-javascript>
                <css-class-wrapper>EquitySphere-portlet</css-class-wrapper>
        </portlet>
        <role-mapper>
                <role-name>administrator</role-name>
                <role-link>Administrator</role-link>
        </role-mapper>
        <role-mapper>
                <role-name>guest</role-name>
                <role-link>Guest</role-link>
        </role-mapper>
        <role-mapper>
                <role-name>power-user</role-name>
                <role-link>Power User</role-link>
        </role-mapper>
        <role-mapper>
                <role-name>user</role-name>
                <role-link>User</role-link>
        </role-mapper>
</liferay-portlet-app>

The rest of the files (e.g. web.xml, liferay-plugin-package.properties, etc) should not need any changes from their default values.  The only other thing you must do is place jQuery (jquery-1.4.1.js) and the JS-Cumulus (js-cumulus.min.js) javascript files into the js/ subdirectory of your project.  You can use the minified jquery if you wish.  The final structure of your project would look like the picture on the right.  You can download these files from here.

Deploying and understanding Tag Equity Visualization

Once you have sucessfully deployed this application, you can add the tag cloud to any page within a community.  It proably won't show anything because you haven't enabled Social Equity and you haven't created any content.  So first, go to Control Panel -> Social Equity and click "Enable Social Equity" to turn it on.  Once that is done, add the Blogs application to a page, and start creating some blogs and adding tags to the blog posts.   Once the blogs are published, you should start to see tags appearing in the tag sphere!  Any content you create (blogs, wikis, forums, bookmarks, images, etc) that can be tagged will cause their tags to show up.  To tweak the tunables, click the wrench icon and select 'Configuration' to change the values.  Click 'save' to apply your changes.  You should see something like the tag cloud below.  

 Note that as tags get more equity (by people creating new content using that tag, OR by an existing piece of tagged content getting rated, updated, viewed, etc), the size of the font should get bigger (relative to the other tags).  A great way to see, at a glance, what topics are most valuable. Feel free to change the inline CSS to make this thing prettier (I am not a designer!).

 

 

 

 

2. Trending Topics

You're probably familiar with Twitter and its concept of "trending topics".  It shows a list of "hot topics" for that particular point in time, scrolling across the screen in a ticker-tape-like display.  Clicking on the topic shows you a list of tweets associated with that topic.  A handy way to see what is on people's minds at any one point in the Twittersphere.

How can we achieve the same thing within a Liferay community?  For community members, it would be a great way for them to keep in touch on the hot topics of the day.

Trending Algorithm

I looked around the web for some decent trending algorithms, and there were lots of them, but they were complicated and relied on advanced math that I left far behind in grade school.  So I came up with my own algorithm.  Here it is:

In this digram, "today" is represented at the far right.  To determine whether a topic is trending, we look at the average number of "mentions" in two time periods: before the "pivot" and "after".  If the average number of mentions in the time period between pivot and today is higher than a certain threshold (compared to the average number of mentions in the time period between cutoff and pivot), then the topic is considered trending.  In the example above, there were 5 mentions over 15 days in the first time period (.3333/day average), and 3 mentions in 5 days in the second time period (.6/day average), which is an 80% improvement.  If 80% is higher than your selected threshold, then the topic will display as a trending topic.

This portlet is identical in structure to the previous one, so not a lot of explanation is needed.  Everything is inline in view.jsp and the shared render parameter behavior is the same.  The main difference in this portlet is the algorithm, # of tunables, and javascript used to display the result.  I used another FOSS jQuery plugin called TurboTicker, which displays tags in a smooth, non-CPU-intensive, vertical scroll.  You can style all you want using CSS.  

view.jsp

<%@page import="com.liferay.portal.kernel.util.GetterUtil"%>
<%@page import="com.liferay.portlet.PortletPreferencesFactoryUtil"%>
<%@page import="com.liferay.portal.kernel.util.Validator"%>
<%@page import="com.liferay.portal.kernel.util.ParamUtil"%>
<%@page import="javax.portlet.PortletPreferences"%>
<%@page import="java.util.Set"%>
<%@page import="java.util.HashSet"%>
<%@page import="com.liferay.portal.util.PortalUtil"%>
<%@page import="java.util.Calendar"%>
<%@page import="java.util.Collections"%>
<%@page import="java.util.Comparator"%>
<%@page import="java.util.Date"%>
<%@page import="com.liferay.portlet.asset.model.AssetTag"%>
<%@page import="java.util.ArrayList"%>
<%@page import="java.util.HashMap"%>
<%@page import="java.util.Map"%>
<%@page
        import="com.liferay.portlet.asset.service.AssetEntryLocalServiceUtil"%>
<%@page import="com.liferay.portlet.asset.model.AssetEntry"%>
<%@page import="java.util.List"%>
<%@page
        import="com.liferay.portlet.asset.service.persistence.AssetEntryQuery"%>
<%@ taglib uri="http://java.sun.com/portlet" prefix="portlet"%>

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

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

<style type="text/css">
                
                #trends {
                width: 200px;
                
                }
                
                #trends ul {
                margin: 0;
                padding: 0;
                list-style: none;
                }
                
                #trends div div ul li {
                font-family: "Arial Black", Arial, Helvetica, sans-serif;
                font-size: 1.0em;
                }
</style>
<%
        // get preferences for tunables
        PortletPreferences preferences = renderRequest.getPreferences();

        String portletResource = ParamUtil.getString(request, "portletResource");

        if (Validator.isNotNull(portletResource)) {
                preferences = PortletPreferencesFactoryUtil.getPortletSetup(request, portletResource);
        }
        final int MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;

        int PIVOT_DAYS = GetterUtil.getInteger(preferences.getValue(
                "pivotDays", "5"));
        int CUTOFF_DAYS = GetterUtil.getInteger(preferences.getValue(
                "cutoffDays", "20"));
        int TREND_PCT_THRESHOLD = GetterUtil.getInteger(preferences.getValue(
                "trendPctThreshold", "100"));
        int NORMALIZED_RANGE = GetterUtil.getInteger(preferences.getValue("maxSize",
                "3"));
        int NEW_THRESHOLD = GetterUtil.getInteger(preferences.getValue(
                "newThreshold", "3"));

        // query for all assets
        AssetEntryQuery q = new AssetEntryQuery();
        long[] ids = new long[] { scopeGroupId };
        q.setGroupIds(ids);
        q.setVisible(true);
        List<AssetEntry> entries = AssetEntryLocalServiceUtil.getEntries(q);

        final Set<String> tagSet = new HashSet<String>();
        final Map<String, Integer> beforeMentions = new HashMap<String, Integer>();
        final Map<String, Integer> afterMentions = new HashMap<String, Integer>();
        
        final Map<String, Integer> trends = new HashMap<String, Integer>();
        final Map<String, Integer> trendNormal = new HashMap<String, Integer>();

        Date now = new Date();
        Date pivot = new Date(now.getTime() - (PIVOT_DAYS * MILLISECONDS_PER_DAY));
        Date cutoff = new Date(now.getTime() - (CUTOFF_DAYS * MILLISECONDS_PER_DAY));

        //iterate over assets, storing mentions before and after the pivot in separate maps
        for (AssetEntry e : entries) {
                for (AssetTag tagA : e.getTags()) {

                        Date updated = e.getModifiedDate();
                        long tagAge = now.getTime() - updated.getTime();

                        if (updated.before(cutoff) || tagAge < 0) {
                                // mention occurred before the cutoff or in the future, so ignore
                                continue;
                        } 
                        
                        if (updated.before(pivot)) {
                                beforeMentions.put(tagA.getName(), beforeMentions.get(tagA.getName()) != null ? beforeMentions.get(tagA.getName()) + 1 : 1);
                        } else {
                                afterMentions.put(tagA.getName(), afterMentions.get(tagA.getName()) != null ? afterMentions.get(tagA.getName()) + 1 : 1);                               
                        }
                        tagSet.add(tagA.getName());
                }
        }
        
        Integer maxValue = 0;
        for (String tag : tagSet) {
                if (afterMentions.get(tag) == null) {
                        // tag never mentioned after pivot, so tag can't be trending
                        continue;
                }
                if (beforeMentions.get(tag) == null) {
                        // tag never mentioned before pivot, so only trending if breaks NEW_THRESHOLD
                        if (afterMentions.get(tag) > NEW_THRESHOLD) {
                                Integer newTrendVal = 100 * afterMentions.get(tag);
                                trends.put(tag, newTrendVal);
                                if (newTrendVal > maxValue) {
                                        maxValue = newTrendVal;
                                }
                        }
                } else {
                        double avgBeforeMentions = (double)beforeMentions.get(tag) / (double) (CUTOFF_DAYS - PIVOT_DAYS);
                        double avgAfterMentions = (double)afterMentions.get(tag) / (double)PIVOT_DAYS;
                        int pctImprovement = (int)((avgAfterMentions - avgBeforeMentions) * 100.0 / avgBeforeMentions);
                        if (pctImprovement > TREND_PCT_THRESHOLD) {
                                // tag is trending!
                                trends.put(tag, pctImprovement);
                                if (pctImprovement > maxValue) maxValue = pctImprovement;
                        }
                }
        }
        
        // normalize over specified range
        for (String tag : trends.keySet()) {
                trendNormal.put(tag, (int)Math.round( ((double)trends.get(tag) / (double)maxValue) * (double)NORMALIZED_RANGE));
        }
        
%>

<div id="trends">
<ul>

        <%
                // create <li> entries for each trending topic, with appropriate liferay link
                for (Map.Entry<String, Integer> trendEntry : trendNormal.entrySet()) {
                        String trendEntryKey = trendEntry.getKey();
                        Integer trendEntryVal = trendEntry.getValue();
        %>
        <li><font size="+<%=trendEntryVal%>"><a href="<portlet:renderURL><portlet:param name="tag" value="<%=trendEntryKey%>"/></portlet:renderURL>"><%=trendEntryKey%></a></font></li>
        <%
                }
        %>
</ul>
</div>

<script  src="<%= PortalUtil.getStaticResourceURL(request, request.getContextPath() + "/js/jquery-1.4.1.js", new Date().getTime()) %>" type="text/javascript"></script>
<script src="<%= PortalUtil.getStaticResourceURL(request, request.getContextPath() + "/js/turboTicker.JQuery.js", new Date().getTime()) %>" type="text/javascript"></script>
  
<script type="text/javascript">

        $("#trends").ticker(50, true, true);
        
</script>

configuration.jsp

<%@page import="com.liferay.portal.kernel.util.Constants"%>
<%@page import="com.liferay.portal.kernel.util.GetterUtil"%>
<%@page import="com.liferay.portlet.PortletPreferencesFactoryUtil"%>
<%@page import="com.liferay.portal.kernel.util.Validator"%>
<%@page import="com.liferay.portal.kernel.util.ParamUtil"%>
<%@page import="javax.portlet.PortletPreferences"%>
<%@ taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet"%>
<%@ taglib uri="http://liferay.com/tld/aui" prefix="aui"%>
<%@ taglib uri="http://liferay.com/tld/portlet" prefix="liferay-portlet"%>
<%@ taglib uri="http://liferay.com/tld/security"
        prefix="liferay-security"%>
<%@ taglib uri="http://liferay.com/tld/theme" prefix="liferay-theme"%>
<%@ taglib uri="http://liferay.com/tld/ui" prefix="liferay-ui"%>
<%@ taglib uri="http://liferay.com/tld/util" prefix="liferay-util"%>

<%@ page contentType="text/html; charset=UTF-8"%>

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

<%
        PortletPreferences preferences = renderRequest.getPreferences();

        String portletResource = ParamUtil.getString(request,
                        "portletResource");

        if (Validator.isNotNull(portletResource)) {
                preferences = PortletPreferencesFactoryUtil.getPortletSetup(
                                request, portletResource);
        }

        int pivotDays = GetterUtil.getInteger(preferences.getValue(
                        "pivotDays", "5"));
        int cutoffDays = GetterUtil.getInteger(preferences.getValue(
                        "cutoffDays", "20"));
        int trendPctThreshold = GetterUtil.getInteger(preferences.getValue(
                        "trendPctThreshold", "100"));
        int maxSize = GetterUtil.getInteger(preferences.getValue("maxSize",
                        "3"));
        int newThreshold = GetterUtil.getInteger(preferences.getValue(
                        "newThreshold", "3"));
%>

<liferay-portlet:actionURL portletConfiguration="true"
        var="configurationURL" />

<aui:form action="<%= configurationURL %>" method="post" name="fm">
        <aui:input name="<%= Constants.CMD %>" type="hidden"
                value="<%= Constants.UPDATE %>" />
        <aui:fieldset>

                <aui:select label="Pivot Days" name="preferences--pivotDays--">
                        <aui:option label="1" selected="<%= pivotDays == 1 %>" />
                        <aui:option label="2" selected="<%= pivotDays == 2 %>" />
                        <aui:option label="3" selected="<%= pivotDays == 3 %>" />
                        <aui:option label="4" selected="<%= pivotDays == 4 %>" />
                        <aui:option label="5" selected="<%= pivotDays == 5 %>" />
                </aui:select>
                <aui:select label="Cutoff Days" name="preferences--cutoffDays--">
                        <aui:option label="10" selected="<%= cutoffDays == 10 %>" />
                        <aui:option label="20" selected="<%= cutoffDays == 20 %>" />
                        <aui:option label="30" selected="<%= cutoffDays == 30 %>" />
                        <aui:option label="40" selected="<%= cutoffDays == 40 %>" />
                        <aui:option label="50" selected="<%= cutoffDays == 50 %>" />
                        <aui:option label="60" selected="<%= cutoffDays == 60 %>" />
                        <aui:option label="70" selected="<%= cutoffDays == 70 %>" />
                        <aui:option label="80" selected="<%= cutoffDays == 80 %>" />
                        <aui:option label="90" selected="<%= cutoffDays == 90 %>" />
                        <aui:option label="100" selected="<%= cutoffDays == 100 %>" />
                </aui:select>
                <aui:select label="Improvement Threshold (%)"
                        name="preferences--trendPctThreshold--">
                        <aui:option label="10" selected="<%= trendPctThreshold == 10 %>" />
                        <aui:option label="20" selected="<%= trendPctThreshold == 20 %>" />
                        <aui:option label="30" selected="<%= trendPctThreshold == 30 %>" />
                        <aui:option label="40" selected="<%= trendPctThreshold == 40 %>" />
                        <aui:option label="50" selected="<%= trendPctThreshold == 50 %>" />
                        <aui:option label="60" selected="<%= trendPctThreshold == 60 %>" />
                        <aui:option label="70" selected="<%= trendPctThreshold == 70 %>" />
                        <aui:option label="80" selected="<%= trendPctThreshold == 80 %>" />
                        <aui:option label="90" selected="<%= trendPctThreshold == 90 %>" />
                        <aui:option label="100" selected="<%= trendPctThreshold == 100 %>" />
                        <aui:option label="110" selected="<%= trendPctThreshold == 110 %>" />
                        <aui:option label="150" selected="<%= trendPctThreshold == 150 %>" />
                        <aui:option label="200" selected="<%= trendPctThreshold == 200 %>" />
                        <aui:option label="500" selected="<%= trendPctThreshold == 500 %>" />
                </aui:select>
                <aui:select label="Max Size" name="preferences--maxSize--">
                        <aui:option label="1" selected="<%= maxSize == 1 %>" />
                        <aui:option label="2" selected="<%= maxSize == 2 %>" />
                        <aui:option label="3" selected="<%= maxSize == 3 %>" />
                        <aui:option label="4" selected="<%= maxSize == 4 %>" />
                        <aui:option label="5" selected="<%= maxSize == 5 %>" />
                        <aui:option label="6" selected="<%= maxSize == 6 %>" />
                        <aui:option label="7" selected="<%= maxSize == 7 %>" />
                </aui:select>
                <aui:select label="New Threshold" name="preferences--newThreshold--">
                        <aui:option label="1" selected="<%= newThreshold == 1 %>" />
                        <aui:option label="2" selected="<%= newThreshold == 2 %>" />
                        <aui:option label="3" selected="<%= newThreshold == 3 %>" />
                        <aui:option label="4" selected="<%= newThreshold == 4 %>" />
                        <aui:option label="5" selected="<%= newThreshold == 5 %>" />
                        <aui:option label="6" selected="<%= newThreshold == 6 %>" />
                        <aui:option label="7" selected="<%= newThreshold == 7 %>" />
                </aui:select>
        </aui:fieldset>
        <aui:button-row>
                <aui:button type="submit" />
        </aui:button-row>


</aui:form>

portlet.xml

<?xml version="1.0"?>

<portlet-app
        version="2.0"
        xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"
>
        <portlet>
                <portlet-name>TrendingTopics</portlet-name>
                <display-name>TrendingTopics</display-name>
                <portlet-class>com.liferay.util.bridges.mvc.MVCPortlet</portlet-class>
                <init-param>
                        <name>view-jsp</name>
                        <value>/view.jsp</value>
                </init-param>
                <expiration-cache>0</expiration-cache>
                <supports>
                        <mime-type>text/html</mime-type>
                </supports>
                <portlet-info>
                        <title>TrendingTopics</title>
                        <short-title>TrendingTopics</short-title>
                        <keywords>TrendingTopics</keywords>
                </portlet-info>
                <security-role-ref>
                        <role-name>administrator</role-name>
                </security-role-ref>
                <security-role-ref>
                        <role-name>guest</role-name>
                </security-role-ref>
                <security-role-ref>
                        <role-name>power-user</role-name>
                </security-role-ref>
                <security-role-ref>
                        <role-name>user</role-name>
                </security-role-ref>
                <supported-public-render-parameter>                         tag                 </supported-public-render-parameter>         </portlet>
        <public-render-parameter>                 <identifier>tag</identifier>                 <qname xmlns:x="http://www.liferay.com/public-render-parameters">x:tag</qname>         </public-render-parameter>
</portlet-app>

liferay-portlet.xml

<?xml version="1.0"?>
<!DOCTYPE liferay-portlet-app PUBLIC "-//Liferay//DTD Portlet Application 6.0.0//EN" "http://www.liferay.com/dtd/liferay-portlet-app_6_0_0.dtd">

<liferay-portlet-app>
        <portlet>
                <portlet-name>TrendingTopics</portlet-name>
                <icon>/icon.png</icon>
        <configuration-action-class>com.liferay.portal.kernel.portlet.DefaultConfigurationAction</configuration-action-class>         <instanceable>false</instanceable>
                <header-portlet-css>/css/main.css</header-portlet-css>
                <footer-portlet-javascript>/js/main.js</footer-portlet-javascript>
                <css-class-wrapper>TrendingTopics-portlet</css-class-wrapper>
        </portlet>
        <role-mapper>
                <role-name>administrator</role-name>
                <role-link>Administrator</role-link>
        </role-mapper>
        <role-mapper>
                <role-name>guest</role-name>
                <role-link>Guest</role-link>
        </role-mapper>
        <role-mapper>
                <role-name>power-user</role-name>
                <role-link>Power User</role-link>
        </role-mapper>
        <role-mapper>
                <role-name>user</role-name>
                <role-link>User</role-link>
        </role-mapper>
</liferay-portlet-app>

 

 

 

 

 

Once you have that all in place (including the turboTicker.JQuery.js and jquery-1.4.1.js files, which you can download from here), the project structure should be identical to the previous one (see image to the right, I think I have a couple of superfluous js files in there as well, which you can ignore).

 

 

 

 

Deploying and understanding Trending Topics

Once you have deployed the application, you can add it to any page in a community.  The topics display are based on mentions.  To mention a particular topic, just create any user-generated content that participates in Liferay's Asset Framework and can be tagged.  This includes a blog post, a wiki page, an image, a forum post, etc.  Any time content is created with a tag (not a category, though that can also be visualized with a slight change to the code), that constitutes a mention.

The tunables are:

  • Pivot Days: The number of days prior to today to calculate an average mention count (see above image).
  • Cutoff Days: The number of days prior today to ignore mentions of tags.
  • Trend Percent Threshold: The percentage improvement in average mentions between the two time periods, above which a tag is considered trending.
  • Max Size: After an improvement percent is measured for each trending topic, the numbers are normalized into a range of [0, NORMALIZED_RANGE] for display.  The number here is the biggest font size that should be used for the best trending topic.  For example, if you put 7 here, then, the topic with the highest trend improvement will be rendered with <font size="+7"></font>.  I know you HTML purists are thinking that this should be done with CSS and that <font> tags are the spawn of the devil, but I slept through that class in college, and this is about community participation, not web coding :-)
  • New Threshold: For tags that have never been mentioned before, their "improvement" percentage would be infinity, if calculated as above, since their average in the earlier time period was 0.  So for newly-mentioned tags, this number allows you to specify how many 'new' mentions it takes for a tag to be considered trending.  A value of 5, for example, means that for a new tag never mentioned before, it would take 5 mentions to get it onto the trending topics list.

Once deployed, your list should look like this:

and it should be slowly scrolling.  Clicking on one of the tags should cause any other Liferay portlets on that page (e.g. blog, wiki, asset publisher, etc) to only show content with the tag you clicked on.  Enjoy! 

Community Roundup

Company Blogs 13 de Abril de 2011 Por James Falkner Staff

Miss me yet?   It's been a busy several weeks here at Liferay so I've been a little absent (not disconnected, just heads down).  We had a very successful showing at the Gartner Portals, Content, and Collaboration Summit in late March.  And we're gearing up for Liferay's annual East Coast Symposium near Washington, D.C. next month (well, really in about 3 weeks!).  I have a whole pocketful of excuses, but for now, onto the links!

  • Liferay's annual East Coast Symposium (ECS) in Washington, D.C. starts May 10th.  Along with key Liferay business and technical leaders, for the first time we'll also have community speakers!  Check out the speakers page, and the agenda for more detail.
  • The 100 PaperCuts program continues to make great strides in resolving paper cuts.  Sprint 4 began this week.   In sprints 1-3 we have fixed almost 30 papercuts.  If you're interested in joining the team, leave comments below.  It's a great motivator for digging into the Liferay project!
  • There's a new community poll on liferay.org - what do you consider key Liferay features?  Not necessarily features that are important to you, but key to Liferay's success.
  • The Liferay Community Leadership team had its kickoff meeting last week.  You can follow the team on its forum category. I created an outline and took some notes from the meeting.  The plan is to have quarterly meetings to keep the community informed of the latest developments.  We even got some press coverage!  Christopher of The VAR Guy wrote up some positive comments on this.  If you're interested in joining the team, leave comments below.
  • Check out the Liferay Hungary team on their new Facebook page!  I wonder if this will become a pattern for other Liferay offices..
  • Rich's Liferay In Action book now has all its chapters.  Great work Rich!  And a great read. 

  • eWeek Labs continues to attract reviews on Liferay Portal, with an average score of 8.4!  Many users and industry veterans are commenting on its collaboration, social, and content management features.  If you have some time today, why not add your review?
  • CMSWire recently wrote up a piece on enterprise open source, and mentions Liferay.  It's a good read and we'd like to think it contributes to the phenomenal growth Liferay is experiencing!  

Finally, look for a new look for the liferay.com website in the coming weeks.  We are in the final stages of upgrading the site to use Liferay 6 and refresh the style.  More importantly, from the community perspective, we'll enable Social Equity and many other features from Liferay 6 you've come to know and love.  Can't wait!

 

Community Roundup

Company Blogs 7 de Março de 2011 Por James Falkner Staff

Hi Gang, welcome back to another edition of Community Roundup.  We've had another exciting several weeks of activity in the community, culminating with the release of Liferay Portal 6 CE GA4 (also known as 6.0.6) [Download] [Release Notes].  This serves as a nice interim release between 6.0 and 6.1, but more importantly, the changes that went into GA4 were dominated by changes that YOU said you wanted to see, so kudos to you for making your voice heard!  Now, onto the links.

  • Liferay's annual East Coast Symposium (ECS) in Washington, D.C. in May.  The Call For Papers is still open through March 10 (3 more days), so keep 'em coming!
  • Sprint 2 of the 100 PaperCuts program is wrapping up this week, with 5 issues remaining.  If you're interested in joining the team, leave comments below.  It's a great motivator for digging into the Liferay project!
  • The Liferay Community Leadership program kicked off last week.  You can follow the team on its forum category. As part of Liferay's re-commitment to open source and its community in 2011, we've started this program to identify existing community leaders, foster new leaders, and give voice to the community's growing impact on Liferay's open source project and product. We'll begin face-to-face (virtual) meetings in April.  If you're interested in joining up, leave comments below!
  • Jorge posted a new revision of the Liferay Development Guide [HTML] [PDF], after a thorough community and internal review.  New content includes Understanding Portlet Phases, extended detail on EXT plugins and Liferay's JSR-286 support, Asset and other Framework coverage, Liferay IDE, and extra reference pointers.
  • Liferay will be submitting itself as a potential Mentoring Organization for the 2011 Google Summer of Code program.   The list of potential projects can be found on the wiki.  If you are a university student interested in participating, registration opens March 28.  This is a great way to get solid development and mentoring experience, and 5000 USD for each completed project over the 12-week project period.
  • eWeek Labs continues to attract reviews on Liferay Portal, with an average score of 8.4!  Many users and industry veterans are commenting on its collaboration, social, and content management features.  If you have some time today, why not add your review?
  • CMSWire recently wrote up a piece on enterprise open source, and mentions Liferay.  It's a good read and we'd like to think it contributes to the phenomenal growth Liferay is experiencing! 
  • Speaking of open source, Srimana Mitra sat down with Bryan Cheung, Liferay CEO, and asked him a range of questions regarding Liferay, from its open source roots, to product positioning, and Liferay's unique culture.  It's a fantasic read on the history of the company.
  • Interested in training on Liferay?  Throughout the months of March and April 2011, get $400 off any US-based training course just for visiting our Facebook Fans page!
  • en Español: Liberado Portlet Lists para Liferay.  A simple portlet for managing lists of items in Liferay.
  • Liferay IDE 1.2 was released last month.  Did you also know it now has native Vaadin support?
  • EmDev Limited announces new version of the Activiti Liferay plugin. This is a workflow plugin that uses BPMN and has a graphical workflow editor in Liferay.
  • Movistar has selected Liferay's portal technology to revamp its mobile web shop. Nice to see more mobile offerings based on Liferay!
  • If you missed the last few Liferay LIVE offerings, replays are available: Liferay and Maven, Liferay and iPad/iPhone with Roambi, OpenSocial Development.  And, we heard you and are planning upcoming events for Developer Tools, Workflow, Sun Migration, and Upgrading to Liferay 6.  Register and attend for free!
  • JumpBox recently upgraded their Liferay-based VM to 6.0.5.  JumpBox is a Ready to run Virtual Machine/Virtual Appliance for VMware, Parallels, VirtualBox and Amazon EC2.  Easy way to get up and running with Liferay!
  • The Liferay Greek Community is sporting a new logo.  If you are an organized community group and are interested in having your own, leave comments below!
  • Recent Wiki updates: WSRP, Resin 4, Logical Architecture, FAQ, Eclipse, Chat, Weblogic.
  • Recent blogs: Eclipse Helios and Liferay IDE, Mounting Multiple CMIS Repositories, People who use Liferay: UAE Government and Careers at Total, Secure RSS Feeds, AlloyUI and YUI3 Console.

That's it for now.  See you on the forums!

Liferay Issue Tracker (JIRA) Upgrade

Company Blogs 2 de Março de 2011 Por James Falkner Staff

Recently Liferay completely an upgrade of the software that powers issues.liferay.com.  We upgrade to JIRA 4.2.4, which is around 5 years newer than the one before!  As you can imagine, there have been a lot of improvements and new features, so I wanted to illustrate a few that I use and that you may find useful.  A full list of features is listed on the Atlassian site (some of which are only available to the site administrators), but the following should be accessible to all.

Labels

I've wanted these for a long time!  Labels are arbitrary tags associated with issues, which serve as handy filters and just-in-time categorization.  These are normally added when you are creating an issue, but for issues you own (or if you are an admin or Liferay staff), you can update these as needed.  While we don't have a taxonomy for these labels, I will be using it for the 100 PaperCuts program, and we may in the future use this as an additional way to categorize issues.

Autocomplete

Many fields in JIRA will now be auto-completed as you type.  For example, selecting Components for a new issue. This also works with Labels, Affected Versions, and others.  Whee!

Dashboards and OpenSocial Gadgets

A very cool new feature, this allows you to construct personal dashboards very similar to Liferay pages, dragging and dropping OpenSocial gadgets onto the page and arranging them as desired.  JIRA comes with a bunch of pre-defined gadgets, but you can also develop your own (and if its generally useful, let us know and we'll check it out and possibly deploy it on the main server!). You can generate lots of interesting graphs and quick links, as shown below:

The data/visualization graphs work against either a full project (like LPS), or against a pre-configured filter you or someone else defined.

Activity Streams

You can now construct customized activity streams, and subscribe to them via RSS.  Handy to keep abreast of the latest goings-on at issues.liferay.com.  Activity Streams are also shown on each project's default landing page:

Internationalization

JIRA supports many languages, so you can manage issues in whatever local language you desire.

Rich Text Comments

Now, your comments can come to life using wiki-like markup to make code snippets standout, quote text, or highlight various phrases.

Image Gallery

For issues that have attached images (screenshots, etc), a handy browser is included so you don't have to visit each image individually, or download them.

It even works for introspecting into .zip file attachments:

Remote RPC

Our JIRA instance can also be queried and interacted with through remote services (SOAP and XML-RPC).  While this was possible in the past, there were numerous bugs that prevented its use.  Seems to have been fixed!  The WSDL URL is http://issues.liferay.com/rpc/soap/jirasoapservice-v2?wsdl .

Greenhopper Plugin

Greenhopper is a plugin for JIRA that supports Agile Program Management practices.  We have been using this for a while but now it is open to the wider community.  It can be accessed via the "Agile" dropdown.

That's all for now. I'm sure there are many other new features just waiting to be discovered.  Have fun!

100 PaperCuts: Sprint 1 Review

Company Blogs 17 de Fevereiro de 2011 Por James Falkner Staff

Yesterday we wrapped up the first sprint of the Liferay Community's 100 Paper Cuts program, and at at 80% solution rate, I'm calling it a success!  A big round of applause goes to the volunteers of this program: Szymon, Rafal, Juan, Milan, Deb, Boubker, Corne, Tomas, Maarten, and our newest member, Hitesh!

Of the 10 papercuts identified, 8 have been resolved.  In the next sprint, we'll take 10 more and do it again.  We learned several things that we will use to improve the next round:

  • I initially identified the 10 papercuts and randomly assigned them to people.  This does not work as everyone has a different skill set, and so there was some shuffling around of issues at the beginning.  In the next sprint, we'll identify a set of issues (a backlog) and individuals can pick and choose which ones to tackle.
  • One of the issues (LPS-12988) turned out not to be a papercut at all, requiring some in-depth investigation and non-trivial changes.  We'll try to avoid that next time.
  • A couple of the issues were fixed by the team, but the original submitter was no longer around to Accept Contribution.  This is a hole in our JIRA workflow (to be sorted after the JIRA upgrade).  Rest assured, the issue is assumed to be in the Community Resolved state now and can be handled by Liferay Staff.
  • A couple of issues turned out to be already fixed.  Not a huge deal, and clearing/closing them is an added benefit of this program.

Sprint 2 will begin next week, and between now and then we'll be identifying papercuts to fix.  If you want to participate, leave a comment below.  This program is a great way to immerse yourself in the code, learn a lot about enterprise-class web development, and give a little back to the community, all at the same time.  Requires a couple of hours of work every 2 weeks, so if you've got the motivation and like a (relatively easy) challenge, join up!

Mostrando 61 - 80 de 95 resultados.
Items 20
de 5