Extending the Liferay BackOffice: A Clean Solution

>> Saturday, April 23, 2011

Liferay is a sophisticated system covering a wide range of needs but, as with any solution, the customer (or business) always needs something that is not available out of the box. That's what keeps our industry going and helps us pay the mortgage. In such a situation, the reflex is usually to go out and rewrite the function to accommodate the additional needs. But there is possibly a better approach.

Basically, I am interested in using Liferay as an applications platform and in leveraging existing Liferay functionality. So the problem is, how do we extend the basic Liferay backoffice functions without having to implement a completely new UI component? Liferay has a few solutions for that, namely: extension and hook plugins. Extensions plugin allow you to extend and modify the core Liferay functionnality. It's very powerful but you do not really want to abuse of this approach since it introduces tight couplings and it is somewhat intrusive and more tricky to deploy and update than other types of Liferay plugins.

Hook plugins are a more elegant and easy to manage solution but they do not always allow you to do what you want. After some time playing around with the two techniques it struck me that the solution for what I was trying to accomplish consisted in combining the two approaches: First, use a very simple extension plugin to insert a hookable extension point in your LR system and then implement and deploy the functionality as a hook that exploits the inserted extension point.

As it turns out, this solution works really well and since nobody else has mentioned this before (as far as I can tell), I thought it would be cool to share this solution with the community. I won't get into all the technical details but will outline the procedure I used to achieve this.

Basically, what we want to do is add one or more sub-sections to the Liferay Organization editor. While you can achieve the same result using another toolset, the following assumes you are using the Liferay IDE with Eclipse.

First, create a new Liferay project of type "Ext" (i.e. Extension project) using the LR new project wizard. You should end-up with a new project in your Eclipse workspace containing the base files for an extension plugin. We need to modify the file called [PROJECT_NAME]-ext/docroot/WEB-INF/ext-impl/src/portal-ext.properties. An empty file with that name should have been created by the new project wizard. This file is used to override or add to the base portal.properties. Simply add the following lines to this file:

    #
    # Input a list of sections that will be included as part of the organization
    # form when updating an organization.
    #
    organizations.form.update.main=details,pages,categorization,main-extension

The above is a copy of what you should find in portal.properties. We just added the ",main-extension" term at the end. This tells Liferay to add a sub-section called "main-extension" in the Organization editor after the "categorization" sub-section.

Next, create a file at [PROJECT_NAME]-ext/docroot/WEB-INF/ext-impl/src/content/Language-ext.properties to contain the localized string for the sub-section name. For example:

  main-extension=Extension

Finally, we need to add a default view for this sub-section. Put this view in [PROJECT_NAME]-ext/docroot/WEB-INF/ext-web/docroot/html/portlet/enterprise_admin/organization/main_extension.jsp (note the naming convention, you must replace dashes with underscores). Since, this is just a placeholder view, it only contains the following:

  <%@ include file="/html/portlet/enterprise_admin/init.jsp" %>
  <h3><liferay-ui:message key="main-extension" /></h3>

Finally, run the Ant deploy target on your project and make sure everything is OK. You should restart your Liferay instance after deployment and check that your files are deployed in the ROOT folder of your LR server. For instance, your ROOT context should contain the following file: ROOT/html/portlet/enterprise_admin/organization/main_extension.jsp.

Navigate to your LR control panel and click on an organization in the "Organizations" view to launch the organization editor. In the right panel containing the list of sections and sub-sections, you should see a new sub-section named "Extension" and if you click on this item, you should have a view containing only the "Extension" title.

We now have a hookable extension point so we can proceed with the creation of our hook. If you have a complete business application packaged in a Liferay plugin, you may want to put your hook in this plugin especially if it needs access to your application services. We will just create a new hook plugin using the same approach as for the extension plugin. What we will want to do is hook the Organization service so we can intercept the following method that is called to update an organization:

  com.opnworks.portal.service.ExtOrganizationService.updateOrganization(...) 

We also need to hook the JSP for our extension point to provide our own form/view and finally, we hook the language.properties so we can use our application-specific labels.

I won't get into the details of the implementation but here is the contents of my liferay-hook.xml file:

  <hook>
   <language-properties>content/org-hook-language_en.properties</language-properties>
   <custom-jsp-dir>/custom_jsps</custom-jsp-dir>
   <service>
    <service-type>
   com.liferay.portal.service.OrganizationService
    </service-type>
    <service-impl>
   com.opnworks.portal.service.ExtOrganizationService
    </service-impl>
   </service>
  </hook>

The hook plugin also contains the following service, view and language files:

  • docroot/WEB-INF/src/com/opnworks/portal/service/ExtOrganizationService.java
  • docroot/custom_jsps/html/portlet/enterprise_admin/organization/main_extension.jsp
  • docroot/WEB-INF/src/content/org-hook-language_en.properties

The service class has the following definition:

  public class ExtOrganizationService extends OrganizationServiceWrapper

Et voila, that was easy wasn't it? The hard part is to implement the service and the view. But that is left as an exercise ;-).

Have fun in the Liferay lane...

Laurent

Read more...

Maven Trick to Split Liferay Services and Views

>> Tuesday, April 12, 2011

The Liferay Service-builder is a pretty cool development tool that allows you to easily generate a service and persistance layer for your Liferay portlets. Combined with Maven and the Liferay Maven Plugin (see my previous post), I can use the Service-builder with my favorite Java toys. There are some caveats though as the Service-builder pretty much forces you to have your service and model code in the same Maven/Eclipse project as your presentation code (the view/controller part of the equation). Or does it? The main problem is that Service-builder (and Liferay) expect resources such as service.xml and sql files to reside in the WEB-INF folder and not in the war's classpath.

Actually, the Maven WAR plugin has some features that make it possible and relatively easy to have the service and presentation resources reside in different Maven/Eclipse projects. When you build your portlet plugin, you simply tell the WAR plugin to merge or "overlay" the service module on top of the current WAR.

Here is how it works: Create a first Maven project with a packaging of type "war" to hold the presentation layer resources and classes (JSPs, Controllers, CSS, JS etc). This we will call the "Portlet Plugin". Create a second Maven project of type "war" to hold the service layer resources (the one generated by Service-builder and the ones you create otherwise). We will call this the "Service Module".

Now, in the Portlet Plugin POM file, specify a dependency on the Service Module war and lib files as in the fragment below.

  
<dependency>
 <!-- This dependency will get "overlayed"  -->
 <groupId>com.transcontinental.medias</groupId>
 <artifactId>transco-demo-liferay-service</artifactId>
 <version>${liferay-service-version}</version>
 <type>war</type>
 <scope>runtime</scope>
</dependency>

<dependency>
 <groupId>com.transcontinental.medias</groupId>
 <artifactId>transco-demo-liferay-service</artifactId>
 <version>${liferay-service-version}</version>
 <classifier>lib</classifier>
</dependency>


In the same POM, configure the WAR plugin so that it merges the depended upon war(s) with the current one as such:

<plugin>
 <artifactId>maven-war-plugin</artifactId>
 <configuration>
  <!-- Do not overlay undesired files -->
  <dependentWarExcludes>WEB-INF/web.xml,**/**.class</dependentWarExcludes>
  <webResources>
   <resource>
    <directory>src/main/webapp/WEB-INF</directory>
    <filtering>true</filtering>
    <targetPath>WEB-INF</targetPath>
   </resource>
  </webResources>
 </configuration>
</plugin>

Since the Portlet Plugin also depends on the jar file containing the service resources, the later gets added to the WEB-INF/lib folder. The trick to generate a jar file containing the services resources is to add the following fragment to the POM file of the service module.

<plugin>
 <!-- Build a JAR artifact containing the java classes and qualified -->
 <!-- by a 'lib' classifier so it gets installed alongside the war   -->
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-jar-plugin</artifactId>
 <executions>
  <execution>
   <phase>package</phase>
   <goals>
    <goal>jar</goal>
   </goals>
   <configuration>
    <classifier>lib</classifier>
   </configuration>
  </execution>
 </executions>
</plugin>


That's pretty much all there is to it. Build the projects in the right order or group them under a common parent POM project and you should be able to produce a deployable Liferay plugin (war) that is the result of overlaying the service module on the portlet module.

Have fun in the Maven lane...

Read more...

Half a Day Fixing the Liferay Maven Plugin

>> Tuesday, April 5, 2011

I love Maven and I am pretty fond of Liferay and since I am in the business these days of developing Liferay-based solutions, I need to be able to rely on the Maven plugin for Liferay.

Actually, the Liferay-released Maven plugin works generally well and I will someday take the time to document a neat Maven trick to merge two Maven projects into a single Liferay plugin (for example, one project containing the Service Builder interfaces and classes and one containing the presentation stuff). However, what I want to share with you in this article is a bit different.

The Maven Liferay plugin can be used to invoke the ServiceBuilder (i.e. mvn liferay:build-service) and that's great except for one nasty issue that we were running into: every time we regenerate the services using the ServiceBuilder, the portlet-model-hints.xml file gets replaced with a fresh one. Now for those who are familiar with Liferay's ServiceBuilder, this is really annoying since portlet-model-hints is how you tell the ServiceBuilder to use something else than default values for things like SQL types. For example, generated entities have default varchar lengths of 75. If you need, say, a varchar(200) you simply add a hint in the said file and ServiceBuilder will take that into account when generating SQL scripts for example. To my experience, this works well when ServiceBuilder is invoked through the ANT scripts. Not being able to rely on this is a pain you know where and can introduce severe problems when deploying new versions of a plugin including loss of data.

I will spare you the details, but troubleshooting this bug/problem was not so easy. Turns out that the ServiceBuilder uses a ModelHints class which tries to load the load the portlet-model-hints.xml as a resource using the builder's class loading context. Now, when this happens in the context of the Maven Liferay plugin, the path to this file (i.e. the projects compileClasspath) is not in the builder's classpath so the "hints" file is never found much less loaded.

I modified the Liferay Maven plugin (i.e. the com.liferay.maven.plugins.ServiceBuilderMojo class) so that it adds the project's CompileClasspath to the Mojo's classloader and voila! Everything started to work as expected.

Note that I had to use some nasty Java tricks to achieve this since modifying a classloader's classpath is protected but Java provides a backdoor to invoke protected methods. If anybody know a better and more elegant solution to add a project compile classpath to the Mojo's classpath, please let us know.

Interested folks can download the modified Maven plugin (opnworks-LR-service-builder-mojo.zip). This file contains the source and binary code for the patched plugin. Simply unzip it and run a mvn install in the root folder and you can start using this modified Maven plugin in-lieu of the released one.

Have fun in the Liferay lane!

Laurent

Read more...

  © Blogger template Webnolia by Ourblogtemplates.com 2009

Back to TOP