divendres, 24 juny de 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!

Cap comentari:

Publica un comentari a l'entrada