dissabte, 23 de juliol del 2011

Zeep o'Tron for Liferay 6

Hi back!

Since my last post I've received some requests about a working sample for Liferay 6. Ok then...here it goes!
First there are some things I have to tell you....

Since some libraries and packages have changed from Liferay 5.2.3 to Liferay 6, I had to do some refactoring, so, taking advantage of this, I've made some modifications to the Zeep o'Tron version for Liferay 5.2.3.

As I told in some other posts, one of the features of Zeep o'Tron was the possibility to override/add your own Spring beans to Liferay core. Since Liferay already provides the possibility to add your own services through the service tag in hook's definition files, I've eliminated this feature from Zeep o'Tron. Maybe I missed something when taking that decision, so, if you think of any situation where adding your own Spring beans could be an advantage, just let me know, and I'll put it back.

Another thing that has been changed is the way Struts and Tiles configurations are loaded. In previous version, Zeep o'Tron just scanned any deployed hook for classes and libs and added them to Liferay's classloader, then, looked for a "struts-config.xml" file under hook's WEB-INF directory, scanned it, and added those configs to Liferay's struts configs. Finally, searched for a "tiles-defs.xml" file, under the same directory, and added those Tiles configurations to Liferay's Tiles configurations.
Thinking about it, I find it too much aggressive, I mean Zeep o'Tron  was applied to each and every deployed hook, and, sometimes, it makes no sense. That's why I've decided to modify the version for Liferay 6 to make it work different, so that not only you can choose which hooks will be parsed by Zeep o'Tron, but which kind of configurations you want to be loaded, and which files contain those configs.
How does it work now? From now on, when a hook is deployed, Zeep o'Tron will look for a file called zeep-o-tron.properties under hook's WEB-INF directory. If it is found, it will check for two properties: 'struts.file.path' and 'tiles.file.path'. If there is a filled 'struts.file.path' property, Struts configurations found in that file (the one represented in the property value) will be loaded into Liferay's Struts configurations, overriding and adding whatever is needed. Besides, all classes and libs found in the hook will be added to Liferay's classloader, so that we can make sure that all classes needed by those configurations are available to Liferay.
At last, something similar is done for Tiles configurations: if a value is found for the property 'tiles.file.path', that file is parsed and all Tiles configurations found in there, added to Liferay's Tiles definitions.
You can take a look at the same hook for the details.

So, the sample hook you'll find in this entry, will work exactly as that shown in Liferay Portal Server: avoiding use of extension. JAR and sample for Liferay 5.2.3, except by the Spring part:

  1. Install Zeep o'Tron for Liferay 6 the same way as explained for Liferay 5.2.3
  2. Start Liferay, and deploy the sample hook provided later on in this entry 
  3. Create an event in the Calendar portlet and have a look at the logs (remember the sample hook outputs to the logs just by using System.out) to check Zeep o'Tron's Struts functionality
  4. Try to see the details of an event, to check Zeep o'Tron's Tiles functionallity
For more details about the testing the sample hook, check out the blog entry related to Zeep o'Tron for Liferay 5.2.3 (and remember to avoid the Spring part, of course!).

Finally, here you can download Zeep o'Tron for Liferay 6, and a sample hook. Sorry for uploading the artifacts in such a place, but found nothing better..

Enjoy!!


Have any idea on how to improve it? Just let me know!

dimecres, 13 de juliol del 2011

Liferay Portal Server: avoiding use of extension. JAR and sample for Liferay 5.2.3

Hi!

Since I posted my set of entries called "Liferay Portal Server: avoiding use of extension" (1, 2 and 3), I've received a rush of visits so, it looks like there is some interest in Zeep 'oTron. So I decided to package it and upload some samples.

My first sample is for Liferay 5.2.3 (bundled with Tomcat 6). So, if you want to try it, just download the zeep-o-tron-liferay_5_2_3.jar, deploy it, start your Liferay Portal Server, and test the sample (zeep-o-tron_demo_hook.war).

I've already explained how to install Zeep O'Tron in another post, but I'll repeat so that you don't need to look for it:
  1. Copy zeep-o-tron-liferay_5_2_3.jar in LIFERAY_HOME/tomcat-6.0.18/webapps/ROOT/WEB-INF/lib
  2. Edit web.xml and locate the servlet named 'Main Servlet'. Then, in that servlet definition:
    • comment out the servlet-class tag
    • add you own servlet-class tag with 'com.blogspot.aigloss.hook.hotdeploy.servlet.HotdeployMainServlet' as value
    • add an init-param with 'configFactory' as param-name, and 'com.blogspot.aigloss.hook.hotdeploy.config.ModuleConfigFactory' as param value
    • now, the Main Servlet from your web.xml, shoul look like this:
      <servlet>
         <servlet-name>Main Servlet</servlet-name>
         <!--servlet-class>com.liferay.portal.servlet.MainServlet</servlet-class-->
         <servlet-class>com.blogspot.aigloss.hook.hotdeploy.servlet.HotdeployMainServlet</servlet-class>
         <init-param>
            <param-name>config</param-name>
            <param-value>/WEB-INF/struts-config.xml</param-value>
         </init-param>
         <!--zeep o'tron -->
         <init-param>
            <param-name>configFactory</param-name>
            <param-value>com.blogspot.aigloss.hook.hotdeploy.config.ModuleConfigFactory</param-value>
         </init-param>
         <!--   -->
         <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
         </init-param>
         <init-param>
            <param-name>detail</param-name>
            <param-value>0</param-value>
         </init-param>
         <load-on-startup>1</load-on-startup>
      </servlet>
  3. Edit your portal-ext.properties and add 'com.blogspot.aigloss.hook.hotdeploy.portal.HookHotdeployListener' as a new hot.deploy.listener. If you have'nt defined any, you can just add the following:
  4. hot.deploy.listeners=\ com.liferay.portal.deploy.hot.PluginPackageHotDeployListener,\ com.liferay.portal.deploy.hot.HookHotDeployListener,\ com.liferay.portal.deploy.hot.LayoutTemplateHotDeployListener,\ com.liferay.portal.deploy.hot.PortletHotDeployListener,\ com.liferay.portal.deploy.hot.ThemeHotDeployListener,\ com.liferay.portal.deploy.hot.ThemeLoaderHotDeployListener,\ com.liferay.portal.deploy.hot.MessagingHotDeployListener,\ com.blogspot.aigloss.hook.hotdeploy.portal.HookHotdeployListener
Ok! Zeep O'Tron is already installed. Now, let's test the sample.
Open your browser, login, and go to a blank page. Add the Calendar portlet.
Now create an event and look at the server logs; back to the browser, click the recently created event and check out its details. Back to the calendar main page, and delete the event. Take a look at the logs again...
Nothing special, uh?!

Ok, let's deploy the sample. Just leave it in LIFERAY_HOME/deploy and let Liferay do its work...
Now repeat what you did again:

  1. Create an event and take a look at the logs....you find a message like this: 'THE CONFIGURATION FOR THIS ACTION HAS BEEN MODIFIED BY ZEEP O'TRON!!!!!' . Of course the sample overrides the action 'com.liferay.portlet.calendar.action.EditEventAction' to output that log to the console...
  2. Now let's take a look at the event's details...can't see the details? That's it! The sample overrides the Tiles configuration called 'portlet.calendar.view_event' to output that message on the screen instead of the event details...
  3. Finally, go back, and delete the event. Check out the logs...see that message?! Yes! As you can read in the message, the hook has overriden the spring bean called 'com.liferay.portlet.calendar.service.CalEventLocalService.impl'
Of course, later versions of Liferay already allow you to override spring services, just remember I developed this for Liferay 5.1.2. By the way, you can take advantage of overriding Struts and Tiles configurations. Of course, if you can override, you can add! 

Last but not least, since all this has been made from a hook, there is no need to demonstrate that all the classes used to override Struts actions, ans Spring beans have been added to Liferay's classloader...so you can take advantage of this too.

Hope you like it!

You can find a version for Liferay 6 (tested in LF 6.0.6) here!

dissabte, 9 de juliol del 2011

Injecting Spring beans in web service handlers

Whenever you have a handler defined for a web service (follow this link for an example) you'll propably find out that any Spring bean you try to inject in your handler results in being null...
It is just beacuse the responsible for instantiating the handler is the underlying web service technology (p.e.: JAX-WS, Axis,...), and that technology is not aware of your Spring context.

Of course there are some workarounds for this problem. 
One approach, could be to:
  1. define the handler class as a singleton Spring bean
  2. inject all the services you need
  3. make sure all those services are thread-safe
  4. make the variables on the handler class, that will store those, services static
This way, when the application starts, the Spring context will be loaded, the "handler bean" created and the service injected. Whenever the handler needs to be instantiated by, for example, JAX-WS, it will have the service injected already (since the variable is static), so you'll be able to use it...

However, keep in minf the cons for this approach: you'll probably end up with a phantom singleton bean in your heap that will never be used.

divendres, 8 de juliol del 2011

Adding handlers to Spring web services

In my previous related entry I showed how easy it was to create a web service using Spring-WS and annotations. Now, we're discussing how to add a handler to the web service. In particular, we will add a handler that will extract the user requesting the service from a SAML token attached to it.

To add a handler to oour existing web service, we just need to files:
  • the handler implementation
  • an XML file (aka bingings file) defining the handler chain for the web service
Finally, we will just need to wire them.

Creating the handler implementation

To implement the hanlder, we just need to create a class that implements the interface javax.xml.ws.handler. This interface will force us to implement the following methods:

  • handleMessage
  • handleFault
  • close
  • getHeaders
Since in this sample we just want to validate the user requesting (extracting it from a SAML token) our service, we're only implementing the handleMessage method:

package com.blogspot.aigloss.springws.handler;

import java.util.Set;

import javax.xml.namespace.QName;
import javax.xml.soap.SOAPHeader;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;
import javax.xml.ws.soap.SOAPFaultException;

import org.opensaml.common.xml.SAMLConstants;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class SAMLHandler implements SOAPHandler {
 private static String WS_SECURITY_URI =
   "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";

 public boolean handleMessage(SOAPMessageContext smc) {
   SOAPFaultException exception = null;
   Boolean outboundProperty = (Boolean) smc.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
   if (!outboundProperty.booleanValue()) {
     Element assertionElement;
     try {
       // get SOAP Header
       SOAPHeader sh = smc.getMessage().getSOAPHeader();
       if (sh == null) {
         // throw SOAPFaultException
       }
       // check for wsse:security element under SOAP Header
       Node wsse = sh.getFirstChild();
       if (wsse == null || !"Security".equals(wsse.getLocalName())
           || !WS_SECURITY_URI.equals(wsse.getNamespaceURI())) {
         //throw SOAPFaultException exception
       }

       // check for SAML assertion under wsse:security element
       assertionElement = (Element) wsse.getFirstChild();
       if (assertionElement == null
           || !"Assertion".equals(assertionElement.getLocalName())
           || !SAMLConstants.SAML20_NS.equals(assertionElement.getNamespaceURI())) {
         //throw SOAPFaultException exception
       }
       String user = null;
       //DON'T USE DOM! USE XPATH INSTEAD!!
       NodeList nodeList = assertionElement.getElementsByTagName("saml:NameID");
       if(nodeList.getLength() == 1) {
         user = nodeList.item(0).getTextContent();
       } else {
         //NameID not found under assertion token --> throw SOAPFaultException
       }
       if(exception == null) {
         //validate user
       }
     } catch (Exception e) {
       //throw SOAPFaultException exception
     }
     if(exception != null) {
       throw exception;
     }
   }
   return true;
 }

 public boolean handleFault(SOAPMessageContext context) {
   return false;
 }

 public void close(MessageContext context) {
 }

 public Set getHeaders() {
   return null;
 }
}


Note I've retrieved the NameId element content form the SAML token using DOM (code in red). I've done it just to tell you this is not the best way to retrieve it. Instead, use Xpath to get it.

Creating the bindings file


To inform the web service that there is one or more handler who want to manage the request, we need to create a handler chain. A handler chain is just a sequence of handlers that need to be applied in an specific order. It is defined in an XML file like this:


<?xml version="1.0" encoding="UTF-8"?>
<handler-chains xmlns="http://java.sun.com/xml/ns/javaee">
   <handler-chain>
      <handler>
       <handler-name>SAMLHandler</handler-name>
         <handler-class>com.blogspot.aigloss.ws.handler.SAML2Handler</handler-class>
      </handler>
   </handler-chain>
</handler-chains>


Linking the bindings file with the handler implementation

Right now we have the implementation of our handler, and the definition on our handler chain.  So, finally, we just have to wire one with the other so that it all makes sense (and, of course, get it to work!). To get that wire, we're annotating the service endpoint class with javax.jws.HandlerChain:

@HandlerChain(file="myservice_bindings.xml")

Since we haven't specified any path for the file, we'll put it in the same directory as the service endpoint. For example, if you're working with Maven, you could put it into the same path as your service endpoint implementation but, instead of having it under /src/main/java, under /src/main/resources.

dimarts, 5 de juliol del 2011

Creating Spring web services using annotations

In this entry, I'm about to show how easy it is to create a web service using Spring-WS using annotations. Spring-WS makes it very easy to publish webservices so that you can publish your already existing services. It is based on JAX-WS, so it is great for application servers like Weblogic, since it uses the same web service technology stack.

To create a Spring-WS based web service, we just need to implement one class: the endpoint. By the way, since we are using Spring, usually it'd be better to implement the logic for the web service in its own service class, so that's the way we're going to take...

To create the enpoint class we just need to create a class extending org.springframework.web.context.support.SpringBeanAutowiringSupport and annotated by javax.jws.WebService. Once we have this class, any method representing an operation in the service, will have to be annotated by javax.jws.WebMethod.

Supose we want to publish a web service called GreetingsService with an operation sayHi. In this case, we'd create the following endpoint class:

import javax.jws.WebMethod;
import javax.jws.WebService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;

@WebService(serviceName="greetingsService")
public class GreetingsServiceEndpoint extends SpringBeanAutowiringSupport {

   @Autowired
   private GreetingsService greetingsService;

   @WebMethod
   public String sayHi() {
      return greetingsService.sayHi();
   }
}
Of course, we're not going into details about the GreetingService definition nor into how to configura my app to use Spring.

If we package and deploy this service, the url http://localhost:7001/springws-sample/greetingsService?wsdl (I'm currently using Weblogic), will return the following WSDL:
<!-- Published by JAX-WS RI at http://jax-ws.dev.java.net. RI's version is
   Oracle JAX-WS 2.1.5. -->
<!-- Generated by JAX-WS RI at http://jax-ws.dev.java.net. RI's version is
   Oracle JAX-WS 2.1.5. -->
<definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
   xmlns:tns="http://endpoint.springws.aigloss.blogspot.com/" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
   xmlns="http://schemas.xmlsoap.org/wsdl/" targetNamespace="http://endpoint.springws.aigloss.blogspot.com/"
   name="greetingsService">
   <types>
      <xsd:schema>
         <xsd:import namespace="http://endpoint.springws.aigloss.blogspot.com/"
            schemaLocation="http://localhost:7001/springws-sample/greetingsService?xsd=1" />
      </xsd:schema>
   </types>
   <message name="sayHi">
      <part name="parameters" element="tns:sayHi" />
   </message>
   <message name="sayHiResponse">
      <part name="parameters" element="tns:sayHiResponse" />
   </message>
   <portType name="GreetingsServiceEndpoint">
      <operation name="sayHi">
         <input message="tns:sayHi" />
         <output message="tns:sayHiResponse" />
      </operation>
   </portType>
   <binding name="GreetingsServiceEndpointPortBinding" type="tns:GreetingsServiceEndpoint">
      <soap:binding transport="http://schemas.xmlsoap.org/soap/http"
         style="document" />
      <operation name="sayHi">
         <soap:operation soapAction="" />
         <input>
            <soap:body use="literal" />
         </input>
         <output>
            <soap:body use="literal" />
         </output>
      </operation>
   </binding>
   <service name="greetingsService">
      <port name="GreetingsServiceEndpointPort" binding="tns:GreetingsServiceEndpointPortBinding">
         <soap:address
            location="http://localhost:7001/springws-sample/greetingsService" />
      </port>
   </service>
</definitions>

In the following entry, I'll explain how add a handler to the web service so that we can, for example, extract the user from a SAML token sent within the request to the web service,

divendres, 24 de juny del 2011

Liferay Portal Server: avoiding use of extension (Part 3)

Putting all together


Sumarizing what I explained in Part 2, we need to create:

  1. A Main servlet extending Liferay's Main Servlet
  2. A Struts ModuleConfig, and a ModuleConfigFactory
  3. A TilesDefinitionFactory
  4. A HotDeployListener
If you've read previous entries you already know why we need all of these and what are the objectives of each class, so I'm just gonna provide you with some code so that you don't need to write it down. Just copy/paste that code into the correnponding classes, package it, deploy it, and take advantage of this!

Overrinding MainServlet


package com.blogspot.aigloss.hook.hotdeploy.servlet;


import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;


import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


import org.apache.commons.digester.Digester;
import org.apache.struts.config.ModuleConfig;
import org.apache.struts.tiles.DefinitionsFactory;
import org.apache.struts.tiles.xmlDefinition.XmlDefinitionsSet;
import org.apache.struts.tiles.xmlDefinition.XmlParser;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.ResourceEntityResolver;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.FileSystemResource;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;


import com.blogspot.aigloss.hook.hotdeploy.struts.tiles.TilesDefinitionsFactory;
import com.liferay.portal.servlet.MainServlet;
import com.liferay.portal.spring.context.PortalApplicationContext;
import com.liferay.portal.struts.PortletRequestProcessor;
import com.liferay.portal.util.WebKeys;


public class HookHotdeployMainServlet extends MainServlet {

private static boolean addConfigs = false;
private static boolean addStrutsConfigs = false;
private static boolean addTilesDefs = false;
private static boolean addSpringFiles = false;


private static List<String> xmlStrutsConfigs = null;
private static List<String> xmlTilesDefs = null;
private static List<String> springFiles = null;


public static void addSpringFile(String filePath) {
if(filePath != null) {
if(springFiles == null) {
springFiles = new ArrayList<String>();
}
springFiles.add(filePath);
HookHotdeployMainServlet.addSpringFiles = true;
HookHotdeployMainServlet.addConfigs = true;
}
}


public static void addStrutsConfigFile(String xmlConfig) {
if(xmlConfig != null) {
if(xmlStrutsConfigs == null) {
xmlStrutsConfigs = new ArrayList<String>();
}
xmlStrutsConfigs.add(xmlConfig);
HookHotdeployMainServlet.addStrutsConfigs = true;
HookHotdeployMainServlet.addConfigs = true;
}
}


public static void addTilesDefinitions(String xmlConfig) {
if(xmlConfig != null) {
if(xmlTilesDefs == null) {
xmlTilesDefs = new ArrayList<String>();
}
xmlTilesDefs.add(xmlConfig);
HookHotdeployMainServlet.addTilesDefs = true;
HookHotdeployMainServlet.addConfigs = true;
}
}


protected synchronized void parseSpringFiles() throws ServletException {
try {
if(HookHotdeployMainServlet.addSpringFiles) {
WebApplicationContext ac = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
for(String springFile : springFiles) {
DefaultListableBeanFactory bf = (DefaultListableBeanFactory) ((PortalApplicationContext)getServletContext().getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)).getBeanFactory();
XmlBeanFactory beanFactory = new XmlBeanFactory(new FileSystemResource(springFile));


XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);


beanDefinitionReader.setResourceLoader(ac);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(ac));


beanDefinitionReader.loadBeanDefinitions("file:///" + springFile);


//Copy loaded fields
String[] beans = beanFactory.getBeanDefinitionNames();
for(String beanName : beans) {
try {
bf.getBean(beanName);

if(bf.isSingleton(beanName)) {
bf.destroySingleton(beanName);
} else {
bf.destroyScopedBean(beanName);
}
} catch(NoSuchBeanDefinitionException nsbd) {
}
bf.registerBeanDefinition(beanName, beanFactory.getBeanDefinition(beanName));
}
}
}
} finally {
/*
* Whatever happens these tiles config files don't have to be parsed ever again,
* since a parsing error would imply not cleaning xmlTilesDefs and that parsing error
* would happen again and again (every request would return that parsing error)
*/
springFiles = null;
HookHotdeployMainServlet.addSpringFiles = false;
}
}


protected synchronized void parseTilesDefinitions() throws ServletException {
try {
if(HookHotdeployMainServlet.addTilesDefs) {
XmlDefinitionsSet xmlDefinitions = new XmlDefinitionsSet();
for(String file : xmlTilesDefs)
try
{
XmlParser xmlParser = new XmlParser();
xmlParser.setValidating(true);
xmlParser.parse(new ByteArrayInputStream(file.getBytes()), xmlDefinitions);
}
catch(Exception ex)
{
throw new ServletException("Error while parsing file\n " + file + "\n" + ex.getMessage(), ex);
}


DefinitionsFactory definitionsFactory = (DefinitionsFactory) getServletContext().getAttribute("org.apache.struts.tiles.DEFINITIONS_FACTORY");
if(definitionsFactory instanceof TilesDefinitionsFactory) {
((TilesDefinitionsFactory) definitionsFactory).addXmlDefinitions(xmlDefinitions);
} else {
TilesDefinitionsFactory factory = new TilesDefinitionsFactory(definitionsFactory);
factory.addXmlDefinitions(xmlDefinitions);
getServletContext().setAttribute("org.apache.struts.tiles.DEFINITIONS_FACTORY", factory);
}
((PortletRequestProcessor)getServletContext().getAttribute(WebKeys.PORTLET_STRUTS_PROCESSOR)).init(this, getModuleConfig());
}
} finally {
/*
* Whatever happens these tiles config files don't have to be parsed ever again,
* since a parsing error would imply not cleaning xmlTilesDefs and that parsing error
* would happen again and again (every request would return that parsing error)
*/
xmlTilesDefs = null;
HookHotdeployMainServlet.addTilesDefs = false;
}
}


protected synchronized void parseStrutsConfigs() throws ServletException {
try {
if(HookHotdeployMainServlet.addStrutsConfigs) {
Digester digester = initConfigDigester();
ModuleConfig moduleConfig = getModuleConfig();
digester.push(moduleConfig);
for(String is : xmlStrutsConfigs) {
try {
digester.parse(new ByteArrayInputStream(is.getBytes()));
} catch (Exception e) { 
throw new ServletException("Error while parsing file /WEB-INF/struts-config.xml", e);
}
digester.push(moduleConfig);
}
getServletContext().setAttribute("org.apache.struts.action.MODULE" + moduleConfig.getPrefix(), moduleConfig);


PortletRequestProcessor portletReqProcessor = (PortletRequestProcessor) getServletContext().getAttribute(WebKeys.PORTLET_STRUTS_PROCESSOR); 
if (portletReqProcessor == null) {
portletReqProcessor =
PortletRequestProcessor.getInstance(this, moduleConfig);


getServletContext().setAttribute(
WebKeys.PORTLET_STRUTS_PROCESSOR, portletReqProcessor);
}
}
} finally {
/*
* Whatever happens these struts config files don't have to be parsed ever again,
* since a parsing error would imply not cleaning xmlStrutsConfigs and that parsing error
* would happen again and again (every request would return that parsing error)
*/
xmlStrutsConfigs = null;
HookHotdeployMainServlet.addStrutsConfigs = false;
}
}


protected ModuleConfig getModuleConfig() {
return (ModuleConfig) getServletContext().getAttribute("org.apache.struts.action.MODULE");
}


public void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
if(HookHotdeployMainServlet.addConfigs) {
parseStrutsConfigs();
parseTilesDefinitions();
parseSpringFiles();
HookHotdeployMainServlet.addConfigs = false;
}
super.service(request, response);
}
}

Creating Struts ModuleConfigFactory and ModuleConfig



package com.blogspot.aigloss.hook.hotdeploy.config;


import java.io.Serializable;
import com.blogspot.aigloss.hook.hotdeploy.config.impl.ModuleConfig;


public class ModuleConfigFactory extends org.apache.struts.config.ModuleConfigFactory implements Serializable {
public ModuleConfigFactory(){}
public ModuleConfig createModuleConfig(String prefix)
{
return new ModuleConfig(prefix);
}
}



package com.blogspot.aigloss.hook.hotdeploy.config.impl;


import org.apache.struts.config.ActionConfig;
import org.apache.struts.config.ActionConfigMatcher;
import org.apache.struts.config.impl.ModuleConfigImpl;


public class ModuleConfig extends ModuleConfigImpl {


private static final long serialVersionUID = 1L;


public ModuleConfig(String prefix) {
super(prefix);
}


@Override
public void freeze() {
        ActionConfig aconfigs[] = findActionConfigs();
        matcher = new ActionConfigMatcher(aconfigs);
}
}

Creating TilesDefinitionsFactory



package com.blogspot.aigloss.hook.hotdeploy.struts.tiles;


import java.util.Iterator;


public class TilesDefinitionsFactory extends ComponentDefinitionsFactoryWrapper {


private DefinitionsFactory instance = null; private org.apache.struts.tiles.xmlDefinition.DefinitionsFactory definitionsFactory = null;

public TilesDefinitionsFactory(DefinitionsFactory factory) {
this.instance = (ComponentDefinitionsFactoryWrapper) factory;
}

@SuppressWarnings("rawtypes")
public synchronized void addXmlDefinitions(XmlDefinitionsSet xmlDefinitions) throws ServletException {
if(xmlDefinitions != null) {
try {
if(this.definitionsFactory == null) {
this.definitionsFactory = new org.apache.struts.tiles.xmlDefinition.DefinitionsFactory(xmlDefinitions);
}
xmlDefinitions.resolveInheritances();
XmlDefinition xmlDefinition;
for(Iterator i = xmlDefinitions.getDefinitions().values().iterator(); i.hasNext(); this.definitionsFactory.putDefinition(new ComponentDefinition(xmlDefinition)))
xmlDefinition = (XmlDefinition)i.next();
} catch (NoSuchDefinitionException e) {
throw new ServletException("Error while loading Tiles Definitions");
}
}
}

@Override
public void destroy() {
this.instance.destroy();
this.definitionsFactory = null;
this.instance = null;
}

@Override
public DefinitionsFactoryConfig getConfig() {
return this.instance.getConfig();
}

@Override
public ComponentDefinition getDefinition(String name,
ServletRequest request, ServletContext servletContext)
throws NoSuchDefinitionException, DefinitionsFactoryException {
ComponentDefinition definition = this.instance.getDefinition(name, request, servletContext);
if(definition == null && this.definitionsFactory != null) {
definition = definitionsFactory.getDefinition(name, request, servletContext);
}
return definition;
}

@Override
public void init(DefinitionsFactoryConfig config,
ServletContext servletContext) throws DefinitionsFactoryException {
this.instance.init(config, servletContext);
}

@Override
public void setConfig(DefinitionsFactoryConfig config,
ServletContext servletContext) throws DefinitionsFactoryException {
this.instance.setConfig(config, servletContext);
}

@Override
public String toString() {
return this.instance.toString();
}
}

Creating the HotdeployListener


package com.blogspot.aigloss.hook.hotdeploy.portal;


import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;


import javax.servlet.ServletContext;


import org.apache.catalina.loader.WebappClassLoader;
import org.xml.sax.InputSource;


import com.blogspot.aigloss.hook.hotdeploy.servlet.HookHotdeployMainServlet;
import com.liferay.portal.deploy.hot.BaseHotDeployListener;
import com.liferay.portal.kernel.deploy.hot.HotDeployEvent;
import com.liferay.portal.kernel.deploy.hot.HotDeployException;
import com.liferay.portal.kernel.util.HttpUtil;
import com.liferay.portal.kernel.util.PortalClassLoaderUtil;




public class HookHotdeployListener extends BaseHotDeployListener {


public void invokeDeploy(HotDeployEvent event) throws HotDeployException {
ServletContext servletContext = event.getServletContext();


if(!isHookPlugin(servletContext)) {
return;
}
loadStrutsConfigs(event);
loadTilesConfigs(event);
addClassLoader(event.getServletContext());
loadSpringConfigs(event);
}


private void loadSpringConfigs(HotDeployEvent event) throws HotDeployException {
ServletContext servletContext = event.getServletContext();
try {
if(servletContext.getResource("/WEB-INF/applicationContext.xml") != null) {
HookHotdeployMainServlet.addSpringFile(servletContext.getRealPath("/WEB-INF/applicationContext.xml"));
}
} catch (MalformedURLException e) {
throwHotDeployException(event, "Error loading applicationContext.xml while registering hook for ", e);
}
}


protected void addClassLoader(ServletContext servletContext) {
WebappClassLoader webappClassLoader = (WebappClassLoader)PortalClassLoaderUtil.getClassLoader();
String classesUrl = servletContext.getRealPath("/WEB-INF/classes/")+"/";
String[] files = new File(classesUrl).list(new FilenameFilter() {
public boolean accept(File dir, String name) {
boolean accept = (name != null && name.endsWith(".class"));
if(!accept) {
File d = new File(dir, name);
if(d.isDirectory()) {
for(String file : d.list()) {
if(accept(d, file)) {
return true;
}
}
}
}
return accept;
}
});

if(files != null && files.length > 0) {
webappClassLoader.addRepository("file:///" + classesUrl);
}
String libUrl = servletContext.getRealPath("/WEB-INF/lib/")+"/";
files = new File(libUrl).list(new FilenameFilter() {
public boolean accept(File dir, String name) {
return (name != null && name.endsWith(".jar"));
}
});
if(files != null && files.length > 0) {
for(String file : files) {
webappClassLoader.addRepository("file:///" + libUrl + file);
}
}
}


protected void loadTilesConfigs(HotDeployEvent event) throws HotDeployException {
try {
String content = getFileContent("/WEB-INF/tiles-defs.xml", event.getServletContext());
if(content != null && content.length() > 0) {
HookHotdeployMainServlet.addTilesDefinitions(content);
}
} catch (IOException e) {
throwHotDeployException(event, "Error loading tiles-defs.xml while registering hook for ", e);
}
}


protected void loadStrutsConfigs(HotDeployEvent event) throws HotDeployException {
try {
String content = getFileContent("/WEB-INF/struts-config.xml", event.getServletContext());
if(content != null && content.length() > 0) {
HookHotdeployMainServlet.addStrutsConfigFile(content);
}
} catch (IOException e) {
throwHotDeployException(event, "Error loading struts-config.xml while registering hook for ", e);
}
}


private String getFileContent(String path, ServletContext servletContext) throws IOException {
String content = null;
URL strutsConfigFile = servletContext.getResource(path);
if(strutsConfigFile != null) {
InputStream input = null;
InputSource is = new InputSource(strutsConfigFile.toExternalForm());
input = strutsConfigFile.openStream();
is.setByteStream(input);
content = read(input);
}
return content;
}


private boolean isHookPlugin(ServletContext servletContext) {
String xml = null;
try {
xml = HttpUtil.URLtoString(servletContext.getResource("/WEB-INF/liferay-hook.xml"));
} catch (Exception e) {
//do nothing
}


return (xml != null);
}


private String read(InputStream is) {
StringBuilder res = new StringBuilder();
int c;
try {
while((c = is.read()) != -1) {
res.append((char)c);
}
} catch (IOException e) {
e.printStackTrace();
}
return res.toString();
}


public void invokeUndeploy(HotDeployEvent arg0) throws HotDeployException {
}
}
Deployment


Once you've package that code, just :
  1. copy the resulting JAR file in Liferay's ROOT/WEB-INF/lib directory
  2. in ROOT app's web.xml:
    • modify Main Servlet's class to be your new Main Servlet class
    • add/modify Main Servlet's initParam configFactory to have your new ModuleConfigFactory class as value
  3. add your new HotDeployListener to Liferay's portal-ext.properties
That's all. After following those steps, any struts-config.xml file located inside WEB-INF's directory of a hook will be parsed and loaded by Struts engine (overriding existing ones if necessary), at runtime, so that those configurations can be used from then on. Same for tiles configurations found in tiles-defs.xml file in the same directory, and Spring config files.

Thing to keep in mind
  • Be careful on using this features since other struts/tiles/spring definitions can be overridden accidentally.
  • Struts configurations (mappings) lookup methods are not synchronized. This means that if you hot deploy a hook and new struts configurations are loaded, there may be some running threads trying to use one of these configurations before their struts configuration information has been synchronized with changes.
Get the packaged JAR file and a sample hook here!