Error Handling with Spring Portlet MVC

>> Thursday, October 27, 2011


In Liferay Portal, when an exception is thrown in a portlet, the default user experience is to view a message that the "Portlet is no longer available" in lieu and place of the portlet content. This can be acceptable is some circumstances but let's face it, when a user reports a problem there is not much contextual info he can share with us.

I recently worked on a major portal application with a lot of backoffice (control panel) portlets and error reports were not easy to diagnose since we had to go digging for the event trace in the log files of the staging or production servers. Surely there was a better way to manage this. As it happens, all our portlets use the Spring Portlet MVC framework and with very little effort we can provide the user or tester with more info on what caused the error and more importantly, the user can include (read: paste) the stack trace in his error report. So here is how to do it for anybody with a similar need.

First, in the Spring portlet context file, add the following bean definition:

      <bean  
           class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">  
 <!--           <property name="exceptionMappings"> -->  
 <!--                <map> -->  
 <!--                     <entry key="DataAccessException" value="data-error" /> -->  
 <!--                     <entry key="com.stuff.MyAppRuntimeException" value="app-unchecked-error" /> -->  
 <!--                     <entry key="com.stuff.MyAppCheckedException" value="app-checked-error" /> -->  
 <!--                </map> -->  
 <!--           </property> -->  
           <property name="defaultErrorView" value="general-error" />  
      </bean>  

This tells Spring what view to display when a controller receives an exception. The commented code can be used as an example to follow if we want to display different views based on the type of exceptions. We just use a generic error view by setting the "defaultErrorView". The later is a JSP file called general-error.jsp that we include in our plugin and it is coded as shown below.

 <%@page import="com.liferay.portal.util.PortalUtil"%>  
 <%@page import="com.liferay.portal.kernel.language.LanguageUtil"%>  
 <%@page import="java.io.PrintWriter"%>  
 <%@ include file="/WEB-INF/jsp/include.jsp" %>  
 <portlet:defineObjects />  
 <%  
 String portletTitle = PortalUtil.getPortletTitle(renderResponse);  
 if (portletTitle == null) {  
      portletTitle = LanguageUtil.get(pageContext, "portlet");  
 }  
 Throwable t = (Throwable) request.getAttribute("exception");  
 %>  
 <div class="portlet-msg-error">  
      <%= LanguageUtil.format(pageContext, "is-temporarily-unavailable", portletTitle, false) %>  
 </div>  
 <p>  
 <liferay-ui:message key="error-cause" /> <%= t.getMessage() %>  
 </p>  
 <liferay-ui:toggle-area id="toggle_area" showMessage='<%=LanguageUtil.get(pageContext, "Show-stack-trace")%>'   
      hideMessage='<%=LanguageUtil.get(pageContext, "Hide-stack-trace")%>'   
      hideImage="true" defaultShowContent="false" >  
      <div class="toggle_area"><br/>  
           <textarea style="width: 700px; height: 200px;" readonly="yes" wrap="off">  
 <%   
 if (t != null) {  
      t.printStackTrace(new PrintWriter(out));   
 }  
 %>  
           </textarea>  
      </div>  
 </liferay-ui:toggle-area>  
 <br/>  
 <hr/>  

What this does is very simple and is shown in the image below. When an error occurs, the usual message is displayed but, in addition, we display the specific error message and the user can elect to display the stack trace in an expandable textarea. By default, the textarea is collapsed so as to not intimidate the user...


There is one final thing that needs to be done to attach all the strings. We need to add an annotated method in our PortletMVC controller so that Spring knows what to do when an exception occurs. Here is the (extremeley complex) method:


   @ExceptionHandler(Exception.class)  
   public String handleException (Exception ex, RenderRequest request) {  
    request.setAttribute("exception", ex);  
    return "general-error";  
   }  


Naturally, if you have several controllers in your Liferay plugin, the idea is to put this method in an abstract superclass and have all your controllers that use this error view extend the superclass.

To finalize everything, you will want to add the following to your resource bundle (*.properties file) and localize theses labels as needed.


error-cause=Cause:
Show-stack-trace=Show stack trace
Hide-stack-trace=Hide stack trace


Have fun with your Liferay / Spring portlets (and make sure your users/testers send you those stack traces...).


Read more...

Experimenting with Liferay Permissions

>> Monday, July 18, 2011

Liferay has a very rich permission and role based access system that goes beyond the portlet specification. However, as with a few other Liferay things, it is not always easy to find good documentation and examples. I recently had to work on roles and permissions for a client's project and after quite a bit of reading from different sources, of experimenting with the LR control panel, of perusing LR source code and of writing and testing code, I managed to get a fairly good understanding of how things work and how one can leverage the permission system.

In this post, I share some of my findings. Hopefully, it will save others some of the trial and error that I went through. I also wrote an example portlet based on the example available from the Liferay public SVN repository. The OpnWorks example plugin is available from our public Maven repository and is the basis for this article. I strongly recommend to download the plugin and it's sources if you want to really go through the details of this article. The plugin provides a working example as well as a way to experiment with roles and permissions since the portlet gives you feedback on which permissions you have depending on your role if and when logged in.

Configuring Permissions

First, you need to define a few things in the portlet.xml file. The following is taken from the aforementioned example:

<portlet>
<portlet-name>opnworks_permission_example</portlet-name>
<display-name>Opnworks Permission Example</display-name>
<portlet-class>com.liferay.util.bridges.mvc.MVCPortlet</portlet-class>
<init-param>
<name>view-jsp</name>
<value>/permission-view.jsp</value>
</init-param>
<init-param>
<name>help-jsp</name>
<value>/help.jsp</value>
</init-param>
<expiration-cache>0</expiration-cache>
<supports>
<mime-type>text/html</mime-type>
<portlet-mode>view</portlet-mode>
<portlet-mode>help</portlet-mode>
</supports>
<portlet-info>
<title>Opnworks Permission Example</title>
<short-title>Opnworks Permission Example</short-title>
<keywords>permission</keywords>
</portlet-info>
<security-role-ref>
<role-name>example-role</role-name>
</security-role-ref>
<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>
</portlet>

In the above declaration, the elements that pertain to permissions are the security-role-ref elements. These indicate which security roles the portlet refers to. The "example-role" is there as an example and is normally not needed. The other ones are standard Portlet role names.

The liferay-portlet.xml file contains the following information that tells the portal how standard portlet role names map to Liferay role names.
<role-mapper>
<role-name>example-role</role-name>
<role-link>Example Role</role-link>
</role-mapper>
<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>

The real interesting and useful stuff is in the resource-actions file. This is where we define default permissions for canonical roles and where we declare which action-keys our portlet exposes. It is important to note that the resource actions file must be referenced in the portlet.properties file. In the example, the portlet.properties file contains the following line:

 resource.actions.configs=resource-actions/example.xml  

The resource-actions/example.xml file must be in the classpath. It contains portlet-resource and model-resource elements that list actions associated with the resources. Our example portlet is declared as follows:

<portlet-resource>
<portlet-name>opnworks_permission_example</portlet-name>
<permissions>
<supports>
<action-key>ADD_SOMETHING</action-key>
<action-key>ADD_TO_PAGE</action-key>
<action-key>CONFIGURATION</action-key>
<action-key>VIEW</action-key>
</supports>
<community-defaults>
</community-defaults>
<guest-defaults>
<action-key>VIEW</action-key>
</guest-defaults>
<guest-unsupported>
<action-key>ADD_TO_PAGE</action-key>
</guest-unsupported>
</permissions>
</portlet-resource>

This simply says that the opnworks_permission_example supports four actions and that guests can view the portlet but cannot add the portlet to a page and that community members do not have any default permission. The ADD_TO_PAGE, CONFIGURATION and VIEW are action keys that correspond to normal or well known portlet actions. The ADD_SOMETHING key is specific to our portlet. Note that all child elements (even empty ones) are required.

Our example also contains model-resource elements such as the following:

<model-resource>
<model-name>com.opnworks.example.model.Something</model-name>
<portlet-ref>
<portlet-name>opnworks_permission_example</portlet-name>
</portlet-ref>
<permissions>
<supports>
<action-key>DELETE</action-key>
<action-key>PERMISSIONS</action-key>
<action-key>UPDATE</action-key>
<action-key>VIEW</action-key>
</supports>
<community-defaults>
<action-key>VIEW</action-key>
</community-defaults>
<guest-defaults>
<action-key>VIEW</action-key>
</guest-defaults>
<guest-unsupported>
<action-key>DELETE</action-key>
<action-key>PERMISSIONS</action-key>
<action-key>UPDATE</action-key>
<action-key>VIEW</action-key>
</guest-unsupported>
</permissions>
</model-resource>

This element declares what users can do with a specific object type (in this case instances of com.opnworks.example.model.Something). Sometimes, a portlet requires access to more than one model-resource and specifically to portal entities such as Users, Organizations, Image Galleries etc. To support this, add model-resource elements to the resource-action file. For example, the following fragment defines what the portlet can do with organizations:

<model-resource>
<model-name>com.liferay.portal.model.Organization</model-name>
<portlet-ref>
<portlet-name>opnworks_permission_example</portlet-name>
</portlet-ref>
<permissions>
<supports>
<action-key>DELETE</action-key>
<action-key>PERMISSIONS</action-key>
<action-key>UPDATE</action-key>
<action-key>VIEW</action-key>
</supports>
<community-defaults>
<action-key>VIEW</action-key>
</community-defaults>
<guest-defaults>
<action-key>VIEW</action-key>
</guest-defaults>
...etc...

With this configuration in place we can build and deploy our Liferay plugin. Simply place the portlet (you will find it in the "OpnWorks" section of the catalog) on a page and see what it says depending on whether you are a simple guest (anonymous) or a signed in user with privileges. If you have administrative privileges, you can create a role, set the permissions for this role (namely the permissions associated with the opnworks_permission_example portlet and model), assign this role to a user and sign in as this user to see what the portlet returns in terms of permissions. The following is the content of the help page displayed by the example portlet.

Help for Example Permissions Portlet

To experiment with this portlet, proceed as follows:
  1. Open a browser on your Liferay instance as Liferay administrator
  2. Create a new role named "Example Role"
  3. Create a new user named "Example User" and set the user's password
  4. Assign the "Example Role" to the newly created user
  5. Open a different browser on your Liferay instance so as to have a different session, log in as "Example User" and navigate to the page containing this portlet
  6. As the administrator, add and remove permissions related to "Opnworks Permission Example" to the "Example Role" and refresh the user's portlet to see the effect of the change in role.
  7. Experiment with the Organization permissions by associating the user with organizations and by changing his organizational role.

Checking Permissions in the Code

Obviously, you can check permissions in your JSP, controller or service code. Check out the permission-view.jsp file in the example plugin for an example. The portal also contains many examples and the Liferay practice is to create a utility or helper class such as com.opnworks.portal.example.permission.SomethingPermissionUtil to provide permission checking services for a resource type. If you need such a utility class then you will need to add a bean definition such as the following to the plugin's META-INF/ext-spring.xml file:

      <bean id="com.opnworks.portal.example.permission.SomethingPermissionUtil" class="com.opnworks.portal.example.permission.SomethingPermissionUtil">
<property name="somethingPermission">
<bean class="com.opnworks.portal.example.permission.impl.SomethingPermissionImpl" />
</property>
</bean>

Well, that was a mouthful wasn't it?

Have fun with permissions...

Read more...

Polling the Liferay Portal

>> Sunday, July 3, 2011

On today's web sites, it is often desirable to have a page refresh it's content through AJAX calls. How can we achieve this in a Liferay portlet? Well, it turns out that the Liferay folks have already solved this problem and provide all the plumbing required to do it with minimal effort and overhead. In fact, the Liferay Chat portlet relies on this PollerService extensively. In the following, I describe how to tap into this bit of infrastructure through a working (albeit simple) example. The source code for this example Liferay plugin is available in our Maven repository.

In a nutshell, here is what you need to do:

1) Specify a Poller Processor Class

First, we specify the following in our plugin's liferay-portlet.xml file:

      <portlet>
     <portlet-name>opnworks_poller_example</portlet-name>
     <icon>/icon.png</icon>
     <poller-processor-class>com.opnworks.portal.example.poller.ExamplePollerProcessor</poller-processor-class>
     <instanceable>false</instanceable> 
     <private-request-attributes>false</private-request-attributes>
     <private-session-attributes>false</private-session-attributes>
     <header-portlet-css>/css/main.css</header-portlet-css>
     <footer-portlet-javascript>/js/main.js</footer-portlet-javascript>
</portlet>

The ExamplePollerProcessor class must implement the com.liferay.portal.kernel.poller.PollerProcessor interface. For the example, we simply extend the following abstract class: com.liferay.portal.kernel.poller.BasePollerProcessor.

In the above, we set "instanceable" to false which makes our life easier and is generally preferable since we do not necessarily want to have n portlets in the same page polling our portal service.

2) Implement the PollerProcessor

Our PollerProcessor implementation is quite simple: each poll request writes to the log and returns the current time in a JSON object. This is what we do here:

 package com.opnworks.portal.example.poller;
import java.util.Date;
import com.liferay.portal.kernel.json.JSONFactoryUtil;
import com.liferay.portal.kernel.json.JSONObject;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.poller.BasePollerProcessor;
import com.liferay.portal.kernel.poller.PollerRequest;
import com.liferay.portal.kernel.poller.PollerResponse;
public class ExamplePollerProcessor extends BasePollerProcessor {
private static Log _log = LogFactoryUtil.getLog(ExamplePollerProcessor.class);
@Override
protected void doReceive(PollerRequest pollerRequest,
          PollerResponse pollerResponse) throws Exception {
     _log.info("doReceive invoked.\n\tRequest: "
               + pollerRequest +
               "\n\tResponse: " + pollerResponse);
     JSONObject responseJSON = JSONFactoryUtil.createJSONObject();
     responseJSON.put("time", (new Date()).toString());
     pollerResponse.setParameter("content", responseJSON);
}
@Override
protected void doSend(PollerRequest pollerRequest) throws Exception {
     String status = getString(pollerRequest, "status");
     _log.info("doSend invoked. \n\tRequest: " + pollerRequest +
               "\n\tStatus: " + status);
}
}

3) Provide some JavaScript Glue

The Javascript code included in the example portlet is the following:

 AUI().use(
   'aui-base',
   'aui-delayed-task',
   'liferay-poller',
   function(A) {
        Liferay.namespace('PollEx');
        Liferay.PollEx.Manager = {
                  init: function(delay, encryptedUserId, portletId, containerId) {
                       var instance = this;
                       instance._delay = delay;
                       instance._portletId = portletId;
                       instance._pollExContainer = A.one('#' + containerId);
                       //Liferay.Poller.setEncryptedUserId(encryptedUserId);
                       console.log("encryptedUserId: " + encryptedUserId);
                       Liferay.Poller.init({
                            encryptedUserId : encryptedUserId,
                            supportsComet : false
                       })
                       instance._startPolling();
                       //alert(instance._pollExContainer);
                  },
                  _startPolling: function() {
                       var instance = this;
                       var task = new A.DelayedTask(instance.send, instance);
                       console.log("_delay: " + instance._delay + ", _portletId:" + instance._portletId + ", _pollExContainer: " + instance._pollExContainer);
                       //Liferay.Poller.setEncryptedUserId('vWdO6/SXDNI='); Q8Nczjmq7zU=
                       task.delay(instance._delay, null, null, [{
                                 portletId: instance._portletId
                                 }]
                            );                        
                       Liferay.Poller.addListener(instance._portletId, instance._onPollerUpdate, instance);
                       Liferay.on(
                            'sessionExpired',
                            function(event) {
                                 Liferay.Poller.removeListener(instance._portletId);
                                 //instance._pollExContainer.hide();
                            }
                       );
                  },
                  send: function(options, id) {
                       var instance = this;
                       console.log("options:" + options + " id: " + id);
                       Liferay.Poller.submitRequest(instance._portletId, options, id);
                  },
                  _onPollerUpdate: function(response, chunkId) {
                       var instance = this;
                       instance._pollExContainer.text(response.content.time);
                       instance.send(
                                 {
                                      status: 'OK'
                                 }
                            );
                  },
             };    
        A.augment(Liferay.PollEx.Manager, A.Attribute, true);
   }
);    

The critical lines of code are where we create and start a "DelayedTask" (new A.DelayedTask) and where we register a listener (Liferay.Poller.addListener...).

Essentially, we are setting up a task that fires every n ms (in our case, 10000) and a listener that gets called back when the response comes in. In addition, every callback fires a "send" to the server with a status.

4) Put a View on Things

Everything is in place but we still need a view. The view.jsp file contains the following:

 <%@page import="com.liferay.portal.kernel.util.GetterUtil"%>
<%@page import="com.liferay.portal.model.User"%>
<%@page import="com.liferay.portal.service.UserLocalServiceUtil"%>
<%@ page import="com.liferay.util.Encryptor" %>
<%@ include file="/init.jsp" %>
<%
User theUser = user;
//if (user == null) {
// Get default user
theUser = UserLocalServiceUtil.getDefaultUser(themeDisplay.getCompanyId());
//}
String encryptedUserId = Encryptor.encrypt(company.getKeyObj(), "" + theUser.getUserId());
%>
<h2>This is the OpnWorks Poller Example</h2>
<p id="<portlet:namespace />pollExContainer">
<!-- PLACEHOLDER -->
</p>
<aui:script use="aui-base">
AUI().ready(function(A) {
          Liferay.PollEx.Manager.init(20000, '<%=encryptedUserId %>', '<%= portletDisplay.getRootPortletId() %>',
               '<portlet:namespace />pollExContainer');
     });
</aui:script>

Pretty simple no? We call the "setIds" method once with the appropriate parameters and we have a periodically refreshed timestamp displayed in our portlet in the element with id pollExContainer.

5) One Gotcha to Top it Off

Despite my attempts, I could not get the poller service to work for a Guest (unauthenticated) user. This is possibly by design and fits the need of the Chat portlet but it would be very useful to be able to invoke the poller service for guest users. If anybody has suggestions or a solution please feel free to comment.

Meanwhile, have fun with your portlets...

Read more...

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