ch.entwine.weblounge.common.impl.site.ActionSupport.java Source code

Java tutorial

Introduction

Here is the source code for ch.entwine.weblounge.common.impl.site.ActionSupport.java

Source

/*
 *  Weblounge: Web Content Management System
 *  Copyright (c) 2003 - 2011 The Weblounge Team
 *  http://entwinemedia.com/weblounge
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public License
 *  as published by the Free Software Foundation; either version 2
 *  of the License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program; if not, write to the Free Software Foundation
 *  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

package ch.entwine.weblounge.common.impl.site;

import ch.entwine.weblounge.common.content.page.HTMLHeadElement;
import ch.entwine.weblounge.common.content.page.HTMLInclude;
import ch.entwine.weblounge.common.content.page.Link;
import ch.entwine.weblounge.common.content.page.PageTemplate;
import ch.entwine.weblounge.common.content.page.Pagelet;
import ch.entwine.weblounge.common.content.page.PageletRenderer;
import ch.entwine.weblounge.common.content.page.Script;
import ch.entwine.weblounge.common.impl.content.GeneralComposeable;
import ch.entwine.weblounge.common.impl.content.page.LinkImpl;
import ch.entwine.weblounge.common.impl.content.page.ScriptImpl;
import ch.entwine.weblounge.common.impl.request.RequestUtils;
import ch.entwine.weblounge.common.impl.url.WebUrlImpl;
import ch.entwine.weblounge.common.impl.util.config.ConfigurationUtils;
import ch.entwine.weblounge.common.impl.util.config.OptionsHelper;
import ch.entwine.weblounge.common.impl.util.xml.XPathHelper;
import ch.entwine.weblounge.common.request.RequestFlavor;
import ch.entwine.weblounge.common.request.WebloungeRequest;
import ch.entwine.weblounge.common.request.WebloungeResponse;
import ch.entwine.weblounge.common.site.Action;
import ch.entwine.weblounge.common.site.ActionException;
import ch.entwine.weblounge.common.site.Environment;
import ch.entwine.weblounge.common.site.Module;
import ch.entwine.weblounge.common.site.Site;
import ch.entwine.weblounge.common.url.UrlUtils;
import ch.entwine.weblounge.common.url.WebUrl;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.lang.StringUtils;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathFactory;

/**
 * This class is the default implementation for an <code>Action</code>. Its main
 * two methods
 * {@link #configure(WebloungeRequest, WebloungeResponse, RequestFlavor)} and
 * {@link #startResponse(WebloungeRequest, WebloungeResponse)}
 * <p>
 * <b>Note:</b> Be aware of the fact that actions are pooled, so make sure to
 * implement the <code>activate()</code> and <code>passivate()</code> method
 * accordingly and of course to include the respective super implementations.
 */
public abstract class ActionSupport extends GeneralComposeable implements Action {

    /** Logging facility */
    private static final Logger logger = LoggerFactory.getLogger(ActionSupport.class);

    /** The action mountpoint */
    protected String mountpoint = null;

    /** The list of flavors */
    protected Set<RequestFlavor> flavors = new HashSet<RequestFlavor>();

    /** Options support */
    protected OptionsHelper options = new OptionsHelper();

    /** The requested output flavor */
    protected RequestFlavor flavor = null;

    /** The parent site */
    protected Site site = null;

    /** The parent module */
    protected Module module = null;

    /** List of supported methods */
    private Set<String> verbs = null;

    /** Map containing uploaded files */
    protected List<FileItem> files = null;

    /** The number of includes */
    protected int includeCount = 0;

    /** The current request object */
    protected WebloungeRequest request = null;

    /** The current response object */
    protected WebloungeResponse response = null;

    /** The site's bundle context */
    protected BundleContext bundleContext = null;

    /**
     * Default constructor.
     */
    public ActionSupport() {
        verbs = new HashSet<String>();
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Action#startResponse(ch.entwine.weblounge.common.request.WebloungeRequest,
     *      ch.entwine.weblounge.common.request.WebloungeResponse)
     */
    public abstract int startResponse(WebloungeRequest request, WebloungeResponse response) throws ActionException;

    /**
     * Enables support for the given HTTP verb.
     * 
     * @param method
     *          the method
     * @throws IllegalArgumentException
     *           if <code>method</code> is <code>null</code> or empty
     * @see Action#supportsMethod(String)
     */
    protected void enableMethod(String method) {
        if (StringUtils.isBlank(method))
            throw new IllegalArgumentException("Method must not be blank");
        if (verbs == null)
            verbs = new HashSet<String>();
        verbs.add(method.toUpperCase());
    }

    /**
     * Enables support for the given HTTP verbs.
     * 
     * @param methods
     *          the HTTP verbs to support
     * @throws IllegalArgumentException
     *           if <code>methods</code> is <code>null</code>
     * @see Action#supportsMethod(String)
     */
    protected void enableMethods(String... methods) {
        if (methods == null)
            throw new IllegalArgumentException("Methods must not be blank");
        for (String method : methods) {
            enableMethod(method);
        }
    }

    /**
     * Disables support for the given HTTP verb.
     * 
     * @param method
     *          the method
     * @throws IllegalArgumentException
     *           if <code>method</code> is <code>null</code> or empty
     * @see Action#supportsMethod(String)
     */
    protected void disableMethod(String method) {
        if (StringUtils.isBlank(method))
            throw new IllegalArgumentException("Method must not be blank");
        if (verbs != null)
            verbs.remove(method.toUpperCase());
    }

    /**
     * Disables support for the given HTTP verbs.
     * 
     * @param methods
     *          the methods
     * @throws IllegalArgumentException
     *           if <code>methods</code> is <code>null</code>
     * @see Action#supportsMethod(String)
     */
    protected void disableMethods(String... methods) {
        if (methods == null)
            throw new IllegalArgumentException("Methods must not be blank");
        for (String method : methods) {
            disableMethod(method);
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * Note that this default implementation enables support for <code>GET</code>
     * requests only.
     * 
     * @throws IllegalArgumentException
     *           if <code>method</code> is <code>null</code>
     * @see ch.entwine.weblounge.common.site.Action#supportsMethod(java.lang.String)
     */
    public boolean supportsMethod(String method) {
        if (StringUtils.isBlank(method))
            throw new IllegalArgumentException("Method must not be blank");
        method = method.toUpperCase();
        // TODO Remove after all actions provide their own verbs
        if (verbs == null || verbs.size() == 0)
            return "GET".equals(method) || "POST".equals(method) || "PUT".equals(method);
        return verbs.contains(method.toUpperCase());
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Action#getMethods()
     */
    @Override
    public String[] getMethods() {
        if (verbs == null || verbs.size() == 0) {
            return new String[] { "GET", "POST", "PUT" };
        } else {
            return verbs.toArray(new String[verbs.size()]);
        }
    }

    /**
     * Returns the site's OSGi bundle context if available, <code>null</code>
     * otherwise.
     * 
     * @return the bundle context
     */
    protected BundleContext getBundleContext() {
        return bundleContext;
    }

    /**
     * Sets the parent module.
     * 
     * @param module
     *          the parent module
     */
    public void setModule(Module module) {
        this.module = module;
        for (HTMLHeadElement headElement : headers) {
            headElement.setModule(module);
        }
    }

    /**
     * Returns the parent module.
     * 
     * @return the module
     */
    public Module getModule() {
        return module;
    }

    /**
     * Sets the associated site.
     * 
     * @param site
     *          the associated site
     */
    public void setSite(Site site) {
        this.site = site;
        for (HTMLHeadElement headElement : headers) {
            headElement.setSite(site);
        }

        // Store the site's bundle context
        if (site != null && site instanceof SiteImpl) {
            bundleContext = ((SiteImpl) site).getBundleContext();
        }
    }

    /**
     * Returns the associated site.
     * 
     * @return the site
     */
    public Site getSite() {
        return site;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.impl.content.GeneralComposeable#setEnvironment(ch.entwine.weblounge.common.site.Environment)
     */
    @Override
    public void setEnvironment(Environment environment) {
        if (environment == null)
            throw new IllegalArgumentException("Environment must not be null");

        if (!this.environment.equals(environment) && module != null) {
            processURLTemplates(environment);
            options.setEnvironment(environment);
        }

        super.setEnvironment(environment);
    }

    /**
     * Processes both renderer and editor url by replacing templates in their
     * paths with real values from the actual module.
     * 
     * @param environment
     *          the environment
     * 
     * @return <code>false</code> if the paths don't end up being real urls,
     *         <code>true</code> otherwise
     */
    private boolean processURLTemplates(Environment environment) {
        if (site == null)
            throw new IllegalStateException("Site cannot be null");
        if (module == null)
            throw new IllegalStateException("Module cannot be null");

        // Process the head elements (scripts and style sheet includes)
        for (HTMLHeadElement headElement : headers) {
            headElement.setEnvironment(environment);
        }

        return true;
    }

    /**
     * Returns the absolute link pointing to this action.
     * 
     * @return the action's link
     */
    public WebUrl getUrl() {
        return new WebUrlImpl(site, mountpoint);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Action#setPath(java.lang.String)
     */
    public void setPath(String path) {
        if (StringUtils.isBlank(path))
            throw new IllegalArgumentException("Path cannot be blank");
        if (!path.startsWith("/"))
            throw new IllegalArgumentException("Action mountpoint '" + path + "' must be absolute");
        this.mountpoint = UrlUtils.trim(path);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Action#getPath()
     */
    public String getPath() {
        return mountpoint;
    }

    /**
     * Returns the requested output flavor.
     * 
     * @return the output flavor
     */
    protected RequestFlavor getFlavor() {
        return flavor;
    }

    /**
     * Returns <code>true</code> if <code>composer</code> equals the stage of the
     * current renderer.
     * 
     * @param composer
     *          the composer to test
     * @param request
     *          the request
     * @return <code>true</code> if <code>composer</code> is the main stage
     */
    protected boolean isStage(String composer, WebloungeRequest request) {
        if (composer == null)
            throw new IllegalArgumentException("Composer may not be null!");

        String stage = PageTemplate.DEFAULT_STAGE;
        PageTemplate template = (PageTemplate) request.getAttribute(WebloungeRequest.TEMPLATE);
        if (template != null)
            stage = template.getStage();
        return composer.equalsIgnoreCase(stage);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Action#addFlavor(ch.entwine.weblounge.common.request.RequestFlavor)
     */
    public void addFlavor(RequestFlavor flavor) {
        flavors.add(flavor);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Action#removeFlavor(ch.entwine.weblounge.common.request.RequestFlavor)
     */
    public void removeFlavor(RequestFlavor flavor) {
        flavors.remove(flavor);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Action#getFlavors()
     */
    public RequestFlavor[] getFlavors() {
        return flavors.toArray(new RequestFlavor[flavors.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Action#supportsFlavor(java.lang.String)
     */
    public boolean supportsFlavor(RequestFlavor flavor) {
        return flavors.contains(flavor);
    }

    /**
     * Removes all flavors.
     */
    protected void clearFlavors() {
        flavors.clear();
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Action#setOption(java.lang.String,
     *      java.lang.String)
     */
    public void setOption(String key, String value) {
        options.setOption(key, value);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.Customizable#setOption(java.lang.String,
     *      java.lang.String, ch.entwine.weblounge.common.site.Environment)
     */
    public void setOption(String name, String value, Environment environment) {
        options.setOption(name, value, environment);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.Customizable#getOptionValue(java.lang.String)
     */
    public String getOptionValue(String name) {
        return options.getOptionValue(name);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.Customizable#getOptionValue(java.lang.String,
     *      java.lang.String)
     */
    public String getOptionValue(String name, String defaultValue) {
        return options.getOptionValue(name, defaultValue);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.Customizable#getOptionValues(java.lang.String)
     */
    public String[] getOptionValues(String name) {
        return options.getOptionValues(name);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.Customizable#getOptions()
     */
    public Map<String, Map<Environment, List<String>>> getOptions() {
        return options.getOptions();
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.Customizable#hasOption(java.lang.String)
     */
    public boolean hasOption(String name) {
        return options.hasOption(name);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.Customizable#getOptionNames()
     */
    public String[] getOptionNames() {
        return options.getOptionNames();
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.Customizable#removeOption(java.lang.String)
     */
    public void removeOption(String name) {
        options.removeOption(name);
    }

    /**
     * @see ch.entwine.weblounge.common.site.Action.module.ActionHandler#configure(ch.entwine.weblounge.api.request.WebloungeRequest,
     *      ch.entwine.weblounge.api.request.WebloungeResponse, java.lang.String)
     */
    public void configure(WebloungeRequest request, WebloungeResponse response, RequestFlavor flavor)
            throws ActionException {

        this.includeCount = 0;
        this.request = request;
        this.response = response;
        this.flavor = flavor;

        // Check if we have a file upload request
        if (ServletFileUpload.isMultipartContent(request)) {

            // Create a factory for disk-based file items
            DiskFileItemFactory factory = new DiskFileItemFactory();
            // TODO: Configure factory
            // factory.setSizeThreshold(yourMaxMemorySize);
            // factory.setRepository(yourTempDirectory);

            // Create a new file upload handler
            ServletFileUpload upload = new ServletFileUpload(factory);

            // Set overall request size constraint
            // TODO: Configure uploader
            // upload.setSizeMax(yourMaxRequestSize);

            // Parse the request
            try {
                files = upload.parseRequest(request);
            } catch (FileUploadException e) {
                logger.error("Error parsing uploads: {}", e.getMessage(), e);
            }
        }

    }

    /**
     * Returns an iteration of the files that have been uploaded in the current
     * step. Note that this iterator may be empty if no files are present, since
     * the files collection is cleared if the wizard moves on. <br>
     * The iterator returns elements of type <code>UploadedFile</code>.
     * 
     * @return an iteration of uploaded files
     */
    protected Iterator<FileItem> files() {
        if (files != null)
            return files.iterator();
        return (new ArrayList<FileItem>()).iterator();
    }

    /**
     * includes the given renderer with the request.
     * 
     * @param request
     *          the request
     * @param response
     *          the response
     * @param renderer
     *          the renderer to include
     * @param data
     *          is passed to the renderer
     * @throws ActionException
     *           if the passed renderer is <code>null</code>
     */
    protected void include(WebloungeRequest request, WebloungeResponse response, PageletRenderer renderer,
            Pagelet data) throws ActionException {
        if (renderer == null) {
            String msg = "The renderer passed to include in action '" + this + "' was <null>!";
            throw new ActionException(new IllegalArgumentException(msg));
        }

        if (data != null)
            request.setAttribute(WebloungeRequest.PAGELET, data);

        // Include renderer in response
        try {
            renderer.render(request, response);
        } catch (Throwable t) {
            String params = RequestUtils.dumpParameters(request);
            String msg = "Error including '" + renderer + "' in action '" + this + "' on " + request.getUrl() + " "
                    + params;
            Throwable o = t.getCause();
            if (o != null) {
                msg += ": " + o.getMessage();
                logger.error(msg, o);
            } else {
                logger.error(msg, t);
            }
            response.invalidate();
        }

        request.removeAttribute(WebloungeRequest.PAGELET);
        includeCount++;
    }

    /**
     * Requests the renderer with the given id from the current module and
     * includes it in the request.
     * 
     * @param request
     *          the request
     * @param response
     *          the response
     * @param renderer
     *          the renderer to include
     * @throws ActionException
     *           if the passed renderer cannot be found.
     */
    protected void include(WebloungeRequest request, WebloungeResponse response, String renderer)
            throws ActionException {
        include(request, response, getModule(), renderer, null);
    }

    /**
     * Requests the renderer with the given id from the current module and
     * includes it in the request.
     * 
     * @param request
     *          the request
     * @param response
     *          the response
     * @param renderer
     *          the renderer to include
     * @param data
     *          is passed to the renderer
     * @throws ActionException
     *           if the passed renderer cannot be found.
     */
    protected void include(WebloungeRequest request, WebloungeResponse response, String renderer, Pagelet data)
            throws ActionException {
        include(request, response, getModule(), renderer, data);
    }

    /**
     * Requests the renderer with the given id from the current module and
     * includes it in the request.
     * 
     * @param request
     *          the request
     * @param response
     *          the response
     * @param renderer
     *          the renderer to include
     * @throws ActionException
     *           if the passed renderer cannot be found.
     */
    protected void include(WebloungeRequest request, WebloungeResponse response, PageletRenderer renderer)
            throws ActionException {
        include(request, response, renderer, null);
    }

    /**
     * Requests the renderer with the given id from module <code>module</code> and
     * includes it in the request.
     * 
     * @param request
     *          the request
     * @param response
     *          the response
     * @param module
     *          the module identifier
     * @param renderer
     *          the renderer to include
     * @param data
     *          is passed to the renderer
     * @throws ActionException
     *           if the passed renderer cannot be found.
     */
    protected void include(WebloungeRequest request, WebloungeResponse response, String module, String renderer,
            Pagelet data) throws ActionException {
        if (module == null)
            throw new ActionException(new IllegalArgumentException("Module is null!"));
        if (renderer == null)
            throw new ActionException(new IllegalArgumentException("Renderer is null!"));
        Module m = getSite().getModule(module);
        if (m == null) {
            String msg = "Trying to include renderer from unknown module '" + module + "'";
            throw new ActionException(new IllegalArgumentException(msg));
        }
        include(request, response, m, renderer, data);
    }

    /**
     * Requests the renderer with the given id from module <code>module</code> and
     * includes it in the request.
     * 
     * @param request
     *          the request
     * @param response
     *          the response
     * @param module
     *          the module
     * @param renderer
     *          the renderer to include
     * @param data
     *          is passed to the renderer
     * @throws ActionException
     *           if the passed renderer cannot be found.
     */
    protected void include(WebloungeRequest request, WebloungeResponse response, Module module, String renderer,
            Pagelet data) throws ActionException {
        if (module == null)
            throw new ActionException(new IllegalArgumentException("Module is null!"));
        if (renderer == null)
            throw new ActionException(new IllegalArgumentException("Renderer is null!"));
        PageletRenderer r = module.getRenderer(renderer);
        if (r == null) {
            String msg = "Trying to include unknown renderer '" + renderer + "'";
            throw new ActionException(new IllegalArgumentException(msg));
        }
        logger.debug("Including renderer {}", renderer);

        // Add the pagelet's header elements to the response
        for (HTMLHeadElement header : r.getHTMLHeaders()) {
            if (!HTMLInclude.Use.Editor.equals(header.getUse()))
                response.addHTMLHeader(header);
        }

        include(request, response, r, data);
    }

    /**
     * Finds the first service in the service registry and returns it. If not such
     * service is available, <code>null</code> is returned.
     * 
     * @param c
     *          the class of the service to look for
     * @return the service
     */
    @SuppressWarnings("unchecked")
    protected <S extends Object> S getService(Class<S> c) {
        String className = c.getName();
        BundleContext ctx = getBundleContext();
        if (ctx == null)
            return null;
        try {
            ServiceReference serviceRef = ctx.getServiceReference(className);
            if (serviceRef == null) {
                logger.debug("No service for class {} found", className);
                return null;
            }
            S service = (S) ctx.getService(serviceRef);
            return service;
        } catch (IllegalStateException e) {
            logger.debug("Service of type {} cannot be received through deactivating bundle {}", className, ctx);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * When overwriting this method, please make sure to call
     * <code>super.activate()</code> as well.
     * 
     * @see ch.entwine.weblounge.common.site.Action#activate()
     */
    public void activate() {
        logger.trace("Activating action {}", this);
    }

    /**
     * {@inheritDoc}
     * <p>
     * When overwriting this method, please make sure to call
     * <code>super.passivate()</code> as well.
     * 
     * @see ch.entwine.weblounge.common.site.Action#passivate()
     */
    public void passivate() {
        logger.trace("Passivating action {}", this);
        files = null;
        includeCount = 0;
        request = null;
        response = null;
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        return identifier.hashCode();
    }

    /**
     * Returns <code>true</code> if <code>o</code> equals this action handler.
     * 
     * @param o
     *          the object to test for equality
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object o) {
        if (o != null && o instanceof ActionSupport) {
            ActionSupport h = (ActionSupport) o;
            if (module == null && h.getModule() != null)
                return false;
            if (module != null && !module.equals(h.getModule()))
                return false;
            return identifier.equals(h.identifier);
        }
        return false;
    }

    /**
     * Initializes this action from an XML node that was generated using
     * {@link #toXml()}.
     * <p>
     * To speed things up, you might consider using the second signature that uses
     * an existing <code>XPath</code> instance instead of creating a new one.
     * 
     * @param config
     *          the action node
     * @throws IllegalStateException
     *           if the configuration cannot be parsed
     * @see #fromXml(Node, XPath)
     * @see #toXml()
     */
    public static Action fromXml(Node config) throws IllegalStateException {
        XPath xpath = XPathFactory.newInstance().newXPath();
        return fromXml(config, xpath);
    }

    /**
     * Initializes this action from an XML node that was generated using
     * {@link #toXml()}.
     * 
     * @param config
     *          the action node
     * @param xpath
     *          xpath processor to use
     * @throws IllegalStateException
     *           if the configuration cannot be parsed
     * @see #toXml()
     */
    @SuppressWarnings("unchecked")
    public static Action fromXml(Node config, XPath xpath) throws IllegalStateException {

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

        // identifier
        String identifier = XPathHelper.valueOf(config, "@id", xpath);
        if (identifier == null)
            throw new IllegalStateException("Unable to create actions without identifier");

        // class
        Action action = null;
        String className = XPathHelper.valueOf(config, "m:class", xpath);
        if (className != null) {
            try {
                Class<? extends Action> c = (Class<? extends Action>) classLoader.loadClass(className);
                action = c.newInstance();
                action.setIdentifier(identifier);
            } catch (ClassNotFoundException e) {
                throw new IllegalStateException(
                        "Implementation " + className + " for action handler '" + identifier + "' not found", e);
            } catch (InstantiationException e) {
                throw new IllegalStateException("Error instantiating impelementation " + className
                        + " for action handler '" + identifier + "'", e);
            } catch (IllegalAccessException e) {
                throw new IllegalStateException("Access violation instantiating implementation " + className
                        + " for action handler '" + identifier + "'", e);
            } catch (Throwable t) {
                throw new IllegalStateException(
                        "Error loading implementation " + className + " for action handler '" + identifier + "'",
                        t);
            }
        } else {
            action = new HTMLActionSupport();
            action.setIdentifier(identifier);
        }

        // mountpoint
        String mountpoint = XPathHelper.valueOf(config, "m:mountpoint", xpath);
        if (mountpoint == null)
            throw new IllegalStateException("Action '" + identifier + " has no mountpoint");
        action.setPath(mountpoint);
        // TODO: handle /, /*

        // content url
        String targetUrl = XPathHelper.valueOf(config, "m:page", xpath);
        if (StringUtils.isNotBlank(targetUrl)) {
            if (!(action instanceof HTMLActionSupport))
                throw new IllegalStateException("Target page configuration for '" + action.getIdentifier()
                        + "' requires subclassing HTMLActionSupport");
            ((HTMLActionSupport) action).setPageURI(targetUrl);
        }

        // template
        String targetTemplate = XPathHelper.valueOf(config, "m:template", xpath);
        if (StringUtils.isNotBlank(targetTemplate)) {
            if (!(action instanceof HTMLActionSupport))
                throw new IllegalStateException("Target template configuration for '" + action.getIdentifier()
                        + "' requires subclassing HTMLActionSupport");
            ((HTMLActionSupport) action).setDefaultTemplate(targetTemplate);
        }

        // client revalidation time
        String recheck = XPathHelper.valueOf(config, "m:recheck", xpath);
        if (recheck != null) {
            try {
                action.setClientRevalidationTime(ConfigurationUtils.parseDuration(recheck));
            } catch (NumberFormatException e) {
                throw new IllegalStateException("The action revalidation time is malformed: '" + recheck + "'");
            } catch (IllegalArgumentException e) {
                throw new IllegalStateException("The action revalidation time is malformed: '" + recheck + "'");
            }
        }

        // cache expiration time
        String valid = XPathHelper.valueOf(config, "m:valid", xpath);
        if (valid != null) {
            try {
                action.setCacheExpirationTime(ConfigurationUtils.parseDuration(valid));
            } catch (NumberFormatException e) {
                throw new IllegalStateException("The action valid time is malformed: '" + valid + "'", e);
            } catch (IllegalArgumentException e) {
                throw new IllegalStateException("The action valid time is malformed: '" + valid + "'", e);
            }
        }

        // scripts
        NodeList scripts = XPathHelper.selectList(config, "m:includes/m:script", xpath);
        for (int i = 0; i < scripts.getLength(); i++) {
            action.addHTMLHeader(ScriptImpl.fromXml(scripts.item(i)));
        }

        // links
        NodeList includes = XPathHelper.selectList(config, "m:includes/m:link", xpath);
        for (int i = 0; i < includes.getLength(); i++) {
            action.addHTMLHeader(LinkImpl.fromXml(includes.item(i)));
        }

        // name
        String name = XPathHelper.valueOf(config, "m:name", xpath);
        action.setName(name);

        // options
        Node optionsNode = XPathHelper.select(config, "m:options", xpath);
        OptionsHelper.fromXml(optionsNode, action, xpath);

        return action;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Action#toXml()
     */
    public String toXml() {
        StringBuffer b = new StringBuffer();
        b.append("<action id=\"");
        b.append(identifier);
        b.append("\">");

        // class
        b.append("<class>").append(getClass().getName()).append("</class>");

        // mountpoint
        b.append("<mountpoint>").append(mountpoint).append("</mountpoint>");

        // Recheck time
        if (clientRevalidationTime >= 0) {
            b.append("<recheck>");
            b.append(ConfigurationUtils.toDuration(clientRevalidationTime));
            b.append("</recheck>");
        }

        // Valid time
        if (cacheExpirationTime >= 0) {
            b.append("<valid>");
            b.append(ConfigurationUtils.toDuration(cacheExpirationTime));
            b.append("</valid>");
        }

        // Name
        if (StringUtils.isNotBlank(name)) {
            b.append("<name><![CDATA[");
            b.append(name);
            b.append("]]></name>");
        }

        // Includes
        if (headers.size() > 0) {
            b.append("<includes>");
            for (HTMLHeadElement header : getHTMLHeaders()) {
                if (header instanceof Link)
                    b.append(header.toXml());
            }
            for (HTMLHeadElement header : getHTMLHeaders()) {
                if (header instanceof Script)
                    b.append(header.toXml());
            }
            b.append("</includes>");
        }

        // Options
        b.append(options.toXml());

        b.append("</action>");
        return b.toString();
    }

    /**
     * Returns a string representation of this action, which consists of the
     * action identifier and the configured method.
     * 
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        StringBuffer buf = new StringBuffer();
        if (module != null)
            buf.append(module.getIdentifier()).append("/");
        buf.append(identifier);
        return buf.toString();
    }

}