dissabte, 18 de juny del 2011

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

Creating the skeleton for Zeep o'Tron
As told in my previous entry our objective relies on creating "HookHotDeployer that feeds our new methods in Liferay's Main Servlet so that this can load all the necessary stuff". So, first of all, we'll need to:

  1. Extend Liferay's Main Servlet class (com.liferay.portal.servlet.MainServlet), wich we will call HotdeployMainServlet from now on
  2. Create a HookHotDeployListener, extending com.liferay.portal.kernel.deploy.hot.BaseHotDeployListener


Both of them will have three main methods: one to load struts configurations, one to load tiles configurations, and one to load hook's classes and libraries into Liferay's classloader. Three of them will work the same way:

  1. The HookHotDeployer will check if there are any of these three elements to loaaddClassLoaderRepositoryd
  2. If so, it will retrieve the necessary information (the content of the corresponding files) and inform HotdeployMainServlet through some static methods
  3. In each request, HotdeployMainServlet will check if there are any configurations/classes to load, if so, it will do it before calling its parent class service method.

Adding classes to Liferay's classloader
To add hook's classes and libraries to Liferay's classloader we will create a method in HookHotDeployListener, wich I will call "addClassLoader", receiving the hook's servlet context, wich we can get from the event passad to HookHotDeployListener's invokeDeploy method.
In addClassloader, we will need to check if there are some class files under the WEB-INF/classes directory, and/or some jar files under the WEB-INF/lib directory. If so, we will add the classes dir as a new repository to the corresponding classloader (Liferay's one), and add those jar files, individually, as new repositories to that classloader too.
And that's it!
Let's put some code on those words...

/**
* Checks if there are classes and/or JARs within the Hook's classloader and adds them as new
* repositories to the current application classloader
*
* @param servletContext Hook's servlet context
*/
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) {
      addClassLoaderRepository(webappClassLoader, 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) {
         addClassLoaderRepository(webappClassLoader, libUrl + file);
      }
   }
}


/**
* Add a new repositry to Liferay's classloader if not already added
*
* @param webappClassLoader Liferay's classloader
* @param pathToRepository Path to the repository to be added
*/
protected void addClassLoaderRepository(WebappClassLoader webappClassLoader, String pathToRepository) {
   if(!classloaderRepositories.contains(pathToRepository)) {
      webappClassLoader.addRepository("file:///" + pathToRepository);
      classloaderRepositories.add(pathToRepository);
   }
}

You can put that code in the HookHotDeployer and call it whenever a hook is deployed.
As you've seen, we have'nt, already, extended Liferay's Main Servlet, it is beacuse that is only needed for the Struts and Tiles part of this module.

Adding and overwriting Struts configurations
To load and/or overwrite Liferays Struts configurations, we will let the new hotdeployer read those new configurations found in the deployed hook, and tell Main Servlet to apply them.
So the sequence is:

  1. A hook is hotdeployed and our HookHotdeployer reads that hook's struts-config file
  2. The HookHotdeployer extracts those configurations from the file, and sends them to our Main Servlet
  3. The new main servlet stores thos configurations and writes down that there are configurations to be loaded, through a static class variable called addStrutsConfigs, and what are those configurations, through a static variable called xmlStrutsConfigs.
  4. On the next request to the Main Servlet, it will check wheter there are new configurations to be loaded, and, if so, loads them using a method called addStrutsConfigFile


Before showing the code, I'll explain one important thing you should know:
When Liferay ends loading its struts configurations, those are freezed, so that they can not be changed later. To avoid this, we will implement our own Struts module config factory, and our own ModuleConfig. The new factory will only have to overwrite the method "createModuleConfig(String prefix)"
so that this returns our ModuleConfig. On the other hand, the new ModuleConfig will just override the method freeze so that it does just nothing.

Here is the code that reads the Hook's Struts configurations:


/**
* Loads struts configurations found in hook's WEB-INF/struts-config.xml file
* @param event
* @throws HotDeployException
*/
protected void loadStrutsConfigs(HotDeployEvent event) throws HotDeployException {
try {
String content = getFileContent("/WEB-INF/struts-config.xml", event.getServletContext());
if(content != null && content.length() > 0) {
HotdeployMainServlet.addStrutsConfigFile(content);
}
} catch (IOException e) {
throwHotDeployException(event, "Error loading struts-config.xml while registering hook for ", e);
}
}

As you can see, a static method addStrutsConfig is called on HotdeployMainServlet (our extension of Liferay's Main Servlet), this method is, actually, the responsible for loading those configs on Liferay's Struts contex:

protected synchronized void parseStrutsConfigs() throws ServletException {

try {
if(HotdeployMainServlet.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;
HotdeployMainServlet.addStrutsConfigs = false;
}
}

The condition "HotdeployMainServlet.addStrutsConfigs" wil be explained further on. By the moment, just let you know that it is read from a properties file that let's you tell the module wich configurations should be loaded or not, so that we can keep some control over zeep o'Tron.

Adding/overriding Tiles configurations
Following the same strategy as with Struts configurations the Hook Hotdeployer will send to our Main Servlet what is to be loaded.
So, in our hotdeployer we can add:


public static void addTilesDefinitions(String xmlConfig) {
if(xmlConfig != null) {
if(xmlTilesDefs == null) {
xmlTilesDefs = new ArrayList<String>();
}
xmlTilesDefs.add(xmlConfig);
HotdeployMainServlet.addTilesDefs = true;
HotdeployMainServlet.addConfigs = true;
}
}
And in our Main Servlet:

protected synchronized void parseTilesDefinitions() throws ServletException {
try {
if(HotdeployMainServlet.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;
HotdeployMainServlet.addTilesDefs = false;
}
}


And that's it! Following those headlines you will be able to modify/add classes, struts and spring configurations, and tiles definitions to Liferay Portal Server just by using hook, that can be hot deployed!

If you don't feel like getting those clues and pieces, and building up the code yourself, in the next and final entry for these series, you'll find the full code and deployment instructions.

Keep reading about this in Part 3!

Cap comentari:

Publica un comentari a l'entrada