Sharing classes between Liferay plugins with Maven

>> Thursday, April 26, 2012

A Java portal technology such as Liferay poses several challenges with respect to class loading. With Liferay we can implement different bits of functionality or design such as layouts, themes, portlets, services and hooks and package them inside plugins to be deployed on a portal instance. Each plugin is packaged as a WAR file. Unfortunately, the original JEE specification for servlet containers did not identify the need for different war files to "see" each other, quite the contrary. In Java, this means that each war file has its own classloader. One solution to this problem is to package all interdependent components (services, portlets etc) inside the same war. This is certainly not desirable from a modularity perspective. To alleviate this problem, Liferay proposes the use of Class Loader Proxies (CLPs) by which Java beans and services in one war can be used in another one.  The main caveat however is that this approach requires the deployment of a library (jar) containing all interface classes to the common lib of the servlet container so they get loaded by the container class loader. This would not be such a problem but unfortunately, redeployment of the common jars generally requires a recycling of the servlet container.  This solution is described partly in the following Wiki article: Using Class Loader Proxy classes to share plugins services.

In this article, I will explain how I setup my Maven projects so as to facilitate the sharing of plugin services. The scenario is typically the following: we need to define a number of service builder services and deploy them to our portal inside a war. We also want to be able to implement and deploy a number of other plugins that use or depend on these services. So here is how we do it.

Configuring the Base Plugin

Our first plugin that we call "abc-common.war" contains the ABC services defined and generated by the LR service builder. It does not have any UI component per say but it must be deployed in order to provide ABC services. Our maven pom.xml file associated with this project generates two deployable artifacts: a WAR for the plugin and a JAR to be made available in the classpath of the container (ex: $TOMCAT_HOME/lib/ext). The jar is a secondary artifact with a "service" classifier (i.e. abc-common-service.jar).

To make sure that abc-common.war and abc-common-service.jar each contain the required Java constructs, we need to configure the war and jar plugins in our POM as presented here:

 <plugin>  
      <artifactId>maven-war-plugin</artifactId>  
      <configuration>  
           <webResources>  
                <resource>  
                     <directory>src/main/webapp/WEB-INF</directory>  
                     <filtering>true</filtering>  
                     <targetPath>WEB-INF</targetPath>  
                </resource>  
           </webResources>  
      </configuration>  
      <executions>  
           <execution>  
                <phase>package</phase>  
                <goals>  
                     <goal>war</goal>  
                </goals>  
                <configuration>  
                     <packagingExcludes>  
                          WEB-INF/classes/**/abc/*Exception.class,  
                          WEB-INF/classes/**/abc/model/*,  
                          WEB-INF/classes/**/abc/service/*.class,  
                          WEB-INF/classes/**/abc/service/persistence/*Persistence.class,  
                          WEB-INF/classes/**/abc/service/persistence/*Util.class  
                     </packagingExcludes>  
                </configuration>                                
           </execution>  
      </executions>  
 </plugin>  
 <plugin>  
      <groupId>org.apache.maven.plugins</groupId>  
      <artifactId>maven-jar-plugin</artifactId>  
      <executions>  
           <execution>  
                <phase>package</phase>  
                <goals>  
                     <goal>jar</goal>  
                </goals>  
                <configuration>  
                     <classifier>service</classifier>  
                     <includes>  
                          <include>**/abc/*Exception.class</include>  
                          <include>**/abc/model/*</include>  
                          <include>**/abc/service/*</include>  
                          <include>**/abc/service/persistence/*</include>  
                     </includes>  
                     <excludes>  
                          <exclude>**/abc/model/impl/**</exclude>  
                          <exclude>**/abc/service/base/**</exclude>  
                          <exclude>**/abc/service/impl/**</exclude>  
                          <exclude>**/abc/service/persistence/*Impl*</exclude>  
                     </excludes>  
                </configuration>  
           </execution>  
      </executions>  
 </plugin>  

In the above POM fragment, one should note the use of "exclude " and "inlude" patterns. These must be adjusted to the context. Also, note the use of a jar classifer to define the modules's secondary artifact.
The war file must be deployed as any other Liferay plugin. The jar file must be deployed to the common classpath of the container as noted above.

Depending Plugins

Other plugins depending upon the first one should declare their dependency in the liferay-plugin-package.properties as such (this is something new in LR 6.1):

required-deployment-contexts=\
abc-common

A plugin (abc-client.war) that depends on ABC Services must declare the following dependency in it's POM file:

 <dependency>  
      <groupId>com.opnworks.tavel</groupId>  
      <artifactId>abc-common</artifactId>  
      <classifier>service</classifier>  
      <version>$VERSION</version>  
      <scope>provided</scope>  
 </dependency>  

Et voila! All classes inside the abc-client project should be able to "see" ABC Common interfaces and services at compile time and should be able to use them at runtime providing the ABC services were properly deployed.

Have fun in the Liferay lane...

Read more...

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...

  © Blogger template Webnolia by Ourblogtemplates.com 2009

Back to TOP