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

Java tutorial

Introduction

Here is the source code for ch.entwine.weblounge.common.impl.site.SiteImpl.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.image.ImageStyle;
import ch.entwine.weblounge.common.content.page.PageLayout;
import ch.entwine.weblounge.common.content.page.PageTemplate;
import ch.entwine.weblounge.common.impl.content.page.PageTemplateImpl;
import ch.entwine.weblounge.common.impl.language.LanguageUtils;
import ch.entwine.weblounge.common.impl.scheduler.QuartzJob;
import ch.entwine.weblounge.common.impl.scheduler.QuartzJobTrigger;
import ch.entwine.weblounge.common.impl.scheduler.QuartzJobWorker;
import ch.entwine.weblounge.common.impl.scheduler.QuartzTriggerListener;
import ch.entwine.weblounge.common.impl.security.SiteAdminImpl;
import ch.entwine.weblounge.common.impl.testing.IntegrationTestBase;
import ch.entwine.weblounge.common.impl.testing.IntegrationTestGroup;
import ch.entwine.weblounge.common.impl.testing.IntegrationTestParser;
import ch.entwine.weblounge.common.impl.util.classloader.BundleClassLoader;
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.ValidationErrorHandler;
import ch.entwine.weblounge.common.impl.util.xml.XPathHelper;
import ch.entwine.weblounge.common.impl.util.xml.XPathNamespaceContext;
import ch.entwine.weblounge.common.language.Language;
import ch.entwine.weblounge.common.language.UnknownLanguageException;
import ch.entwine.weblounge.common.repository.ContentRepository;
import ch.entwine.weblounge.common.repository.ContentRepositoryException;
import ch.entwine.weblounge.common.request.RequestListener;
import ch.entwine.weblounge.common.request.WebloungeRequest;
import ch.entwine.weblounge.common.request.WebloungeResponse;
import ch.entwine.weblounge.common.scheduler.Job;
import ch.entwine.weblounge.common.scheduler.JobTrigger;
import ch.entwine.weblounge.common.scheduler.JobWorker;
import ch.entwine.weblounge.common.security.DigestType;
import ch.entwine.weblounge.common.security.Security;
import ch.entwine.weblounge.common.security.User;
import ch.entwine.weblounge.common.security.UserListener;
import ch.entwine.weblounge.common.security.WebloungeUser;
import ch.entwine.weblounge.common.site.Action;
import ch.entwine.weblounge.common.site.Environment;
import ch.entwine.weblounge.common.site.I18nDictionary;
import ch.entwine.weblounge.common.site.Module;
import ch.entwine.weblounge.common.site.ModuleException;
import ch.entwine.weblounge.common.site.Site;
import ch.entwine.weblounge.common.site.SiteException;
import ch.entwine.weblounge.common.site.SiteListener;
import ch.entwine.weblounge.common.site.SiteURL;
import ch.entwine.weblounge.common.url.UrlUtils;
import ch.entwine.weblounge.testing.IntegrationTest;

import org.apache.commons.lang.StringUtils;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathFactory;

/**
 * Default implementation of a site.
 */
public class SiteImpl implements Site {

    /** Serial version uid */
    private static final long serialVersionUID = 5544198303137698222L;

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

    /** Bundle property name of the site identifier */
    public static final String PROP_IDENTIFIER = "site.identifier";

    /** Xml namespace for the site */
    public static final String SITE_XMLNS = "http://www.entwinemedia.com/weblounge/3.0/site";

    /** Regular expression to test the validity of a site identifier */
    private static final String SITE_IDENTIFIER_REGEX = "^[a-zA-Z0-9]+[a-zA-Z0-9-_.]*$";

    /** The local roles */
    protected Map<String, String> localRoles = null;

    /** The site identifier */
    protected String identifier = null;

    /** Site enabled state */
    protected boolean autoStart = true;

    /** Site running state */
    private boolean isOnline = false;

    /** Site description */
    protected String name = null;

    /** Site administrator */
    protected WebloungeUser administrator = null;

    /** Page languages */
    protected Map<String, Language> languages = null;

    /** The default language */
    protected Language defaultLanguage = null;

    /** Page templates */
    protected Map<String, PageTemplate> templates = null;

    /** The default page template */
    protected PageTemplate defaultTemplate = null;

    /** Page layouts */
    protected Map<String, PageLayout> layouts = null;

    /** The default page template */
    protected PageLayout defaultLayout = null;

    /** Modules */
    protected Map<String, Module> modules = null;

    /** The default hostname */
    protected SiteURL defaultURL = null;

    /** Ordered list of site urls */
    protected List<SiteURL> urls = null;

    /** Default urls by environment */
    protected Map<Environment, SiteURL> defaultURLByEnvironment = null;

    /** Jobs */
    protected Map<String, QuartzJob> jobs = null;

    /** The i18n dictionary */
    protected I18nDictionaryImpl i18n = null;

    /** The site's content repository */
    protected ContentRepository contentRepository = null;

    /** Option handling support */
    protected OptionsHelper options = null;

    /** URL to the security configuration */
    protected URL security = null;

    /** This site's digest policy */
    protected DigestType digestType = DigestType.md5;

    /** Request listeners */
    private List<RequestListener> requestListeners = null;

    /** Site listeners */
    private List<SiteListener> siteListeners = null;

    /** User listeners */
    private List<UserListener> userListeners = null;

    /** Scheduling service tracker */
    private SchedulingServiceTracker schedulingServiceTracker = null;

    /** Quartz scheduler */
    private Scheduler scheduler = null;

    /** Listener for the quartz scheduler */
    private TriggerListener quartzTriggerListener = null;

    /** Flag to tell whether we are currently shutting down */
    private boolean isShutdownInProgress = false;

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

    /** The current system environment */
    protected Environment environment = Environment.Production;

    /** The list of integration tests */
    private List<IntegrationTest> integrationTests = null;

    /** The registered integration tests */
    private final List<ServiceRegistration> integrationTestRegistrations = new ArrayList<ServiceRegistration>();

    /** Flag to indicate whether the site has been initialized */
    private boolean siteInitialized = false;

    /** Site bundle initialization properties */
    protected Map<String, String> serviceProperties = null;

    /**
     * Creates a new site that is initially disabled. Use {@link #setEnabled()} to
     * enable the site.
     */
    public SiteImpl() {
        languages = new HashMap<String, Language>();
        templates = new HashMap<String, PageTemplate>();
        layouts = new HashMap<String, PageLayout>();
        modules = new HashMap<String, Module>();
        urls = new ArrayList<SiteURL>();
        defaultURLByEnvironment = new HashMap<Environment, SiteURL>();
        jobs = new HashMap<String, QuartzJob>();
        localRoles = new HashMap<String, String>();
        i18n = new I18nDictionaryImpl();
        options = new OptionsHelper();
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#initialize(ch.entwine.weblounge.common.site.Environment)
     */
    public void initialize(Environment environment) throws Exception {
        this.environment = environment;

        // Don't inialize twice
        if (!siteInitialized) {
            initializeSiteComponents();
        }

        // Pass the initialization on to the templates
        for (PageTemplate template : templates.values()) {
            template.setEnvironment(environment);
        }

        // Initialize modules as well
        for (Module module : modules.values()) {
            module.initialize(environment);
        }

        // Switch the options to the new environment
        options.setEnvironment(environment);

    }

    /**
     * Initializes the site components like modules, templates, actions etc.
     * 
     * @throws Exception
     *           if initialization fails
     */
    private void initializeSiteComponents() throws Exception {

        logger.debug("Initializing site '{}'", this);

        final Bundle bundle = bundleContext.getBundle();

        // Load i18n dictionary
        Enumeration<URL> i18nEnum = bundle.findEntries("site/i18n", "*.xml", true);
        while (i18nEnum != null && i18nEnum.hasMoreElements()) {
            i18n.addDictionary(i18nEnum.nextElement());
        }

        // Prepare schema validator
        SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        URL schemaUrl = SiteImpl.class.getResource("/xsd/module.xsd");
        Schema moduleSchema = schemaFactory.newSchema(schemaUrl);

        // Set up the document builder
        DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
        docBuilderFactory.setSchema(moduleSchema);
        docBuilderFactory.setNamespaceAware(true);
        DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();

        // Load the modules
        final Enumeration<URL> e = bundle.findEntries("site", "module.xml", true);

        if (e != null) {
            while (e.hasMoreElements()) {
                URL moduleXmlUrl = e.nextElement();
                int endIndex = moduleXmlUrl.toExternalForm().lastIndexOf('/');
                URL moduleUrl = new URL(moduleXmlUrl.toExternalForm().substring(0, endIndex));
                logger.debug("Loading module '{}' for site '{}'", moduleXmlUrl, this);

                // Load and validate the module descriptor
                ValidationErrorHandler errorHandler = new ValidationErrorHandler(moduleXmlUrl);
                docBuilder.setErrorHandler(errorHandler);
                Document moduleXml = docBuilder.parse(moduleXmlUrl.openStream());
                if (errorHandler.hasErrors()) {
                    logger.error("Errors found while validating module descriptor {}. Site '{}' is not loaded",
                            moduleXml, this);
                    throw new IllegalStateException("Errors found while validating module descriptor " + moduleXml);
                }

                // We need the module id even if the module initialization fails to log
                // a proper error message
                Node moduleNode = moduleXml.getFirstChild();
                String moduleId = moduleNode.getAttributes().getNamedItem("id").getNodeValue();

                Module module;
                try {
                    module = ModuleImpl.fromXml(moduleNode);
                    logger.debug("Module '{}' loaded for site '{}'", module, this);
                } catch (Throwable t) {
                    logger.error("Error loading module '{}' of site {}", moduleId, identifier);
                    if (t instanceof Exception)
                        throw (Exception) t;
                    throw new Exception(t);
                }

                // If module is disabled, don't add it to the site
                if (!module.isEnabled()) {
                    logger.info("Found disabled module '{}' in site '{}'", module, this);
                    continue;
                }

                // Make sure there is only one module with this identifier
                if (modules.containsKey(module.getIdentifier())) {
                    logger.warn("A module with id '{}' is already registered in site '{}'", module.getIdentifier(),
                            identifier);
                    logger.error("Module '{}' is not registered due to conflicting identifier",
                            module.getIdentifier());
                    continue;
                }

                // Check inter-module compatibility
                for (Module m : modules.values()) {

                    // Check actions
                    for (Action a : m.getActions()) {
                        for (Action action : module.getActions()) {
                            if (action.getIdentifier().equals(a.getIdentifier())) {
                                logger.warn("Module '{}' of site '{}' already defines an action with id '{}'",
                                        new String[] { m.getIdentifier(), identifier, a.getIdentifier() });
                            } else if (action.getPath().equals(a.getPath())) {
                                logger.warn("Module '{}' of site '{}' already defines an action at '{}'",
                                        new String[] { m.getIdentifier(), identifier, a.getPath() });
                                logger.error(
                                        "Module '{}' of site '{}' is not registered due to conflicting mountpoints",
                                        m.getIdentifier(), identifier);
                                continue;
                            }
                        }
                    }

                    // Check image styles
                    for (ImageStyle s : m.getImageStyles()) {
                        for (ImageStyle style : module.getImageStyles()) {
                            if (style.getIdentifier().equals(s.getIdentifier())) {
                                logger.warn("Module '{}' of site '{}' already defines an image style with id '{}'",
                                        new String[] { m.getIdentifier(), identifier, s.getIdentifier() });
                            }
                        }
                    }

                    // Check jobs
                    for (Job j : m.getJobs()) {
                        for (Job job : module.getJobs()) {
                            if (job.getIdentifier().equals(j.getIdentifier())) {
                                logger.warn("Module '{}' of site '{}' already defines a job with id '{}'",
                                        new String[] { m.getIdentifier(), identifier, j.getIdentifier() });
                            }
                        }
                    }

                }

                addModule(module);

                // Do this as last step since we don't want to have i18n dictionaries of
                // an invalid or disabled module in the site
                String i18nPath = UrlUtils.concat(moduleUrl.getPath(), "i18n");
                i18nEnum = bundle.findEntries(i18nPath, "*.xml", true);
                while (i18nEnum != null && i18nEnum.hasMoreElements()) {
                    i18n.addDictionary(i18nEnum.nextElement());
                }
            }

        } else {
            logger.debug("Site '{}' has no modules", this);
        }

        // Look for a job scheduler
        logger.debug("Signing up for a job scheduling services");
        schedulingServiceTracker = new SchedulingServiceTracker(bundleContext, this);
        schedulingServiceTracker.open();

        // Load the tests
        if (!Environment.Production.equals(environment))
            integrationTests = loadIntegrationTests();
        else
            logger.info("Skipped loading of integration tests due to environment '{}'", environment);

        siteInitialized = true;
        logger.info("Site '{}' initialized", this);
    }

    /**
     * Returns the site's OSGi bundle context.
     * 
     * @return the bundle context
     */
    public BundleContext getBundleContext() {
        return bundleContext;
    }

    /**
     * Returns the properties of the site's OSGi service.
     * 
     * @return the site properties
     */
    public Map<String, String> getServiceProperties() {
        return serviceProperties;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#setIdentifier(java.lang.String)
     */
    public void setIdentifier(String identifier) {
        if (identifier == null)
            throw new IllegalArgumentException("Site identifier must not be null");
        else if (!Pattern.matches(SITE_IDENTIFIER_REGEX, identifier))
            throw new IllegalArgumentException("Site identifier '" + identifier + "' is malformed");
        this.identifier = identifier;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getIdentifier()
     */
    public String getIdentifier() {
        return identifier;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#setAutoStart(boolean)
     */
    public void setAutoStart(boolean enabled) {
        this.autoStart = enabled;
        if (isOnline)
            stop();
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#isStartedAutomatically()
     */
    public boolean isStartedAutomatically() {
        return autoStart;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#setName(java.lang.String)
     */
    public void setName(String description) {
        this.name = description;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getName()
     */
    public String getName() {
        return name;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#setAdministrator(ch.entwine.weblounge.common.security.WebloungeUser)
     */
    public void setAdministrator(WebloungeUser administrator) {
        if (administrator != null)
            logger.debug("Site administrator is {}", administrator);
        else
            logger.debug("Site administrator is now undefined");
        this.administrator = administrator;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getAdministrator()
     */
    public WebloungeUser getAdministrator() {
        return administrator;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getI18n()
     */
    public I18nDictionary getI18n() {
        return i18n;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#addTemplate(ch.entwine.weblounge.common.content.page.PageTemplate)
     */
    public void addTemplate(PageTemplate template) {
        template.setSite(this);
        templates.put(template.getIdentifier(), template);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#removeTemplate(ch.entwine.weblounge.common.content.page.PageTemplate)
     */
    public void removeTemplate(PageTemplate template) {
        if (template == null)
            throw new IllegalArgumentException("Template must not be null");
        logger.debug("Removing page template '{}'", template.getIdentifier());
        templates.remove(template.getIdentifier());
        if (template.equals(defaultTemplate)) {
            defaultTemplate = null;
            logger.debug("Default template is now undefined");
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getTemplate(java.lang.String)
     */
    public PageTemplate getTemplate(String template) {
        return templates.get(template);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getTemplates()
     */
    public PageTemplate[] getTemplates() {
        return templates.values().toArray(new PageTemplate[templates.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#setDefaultTemplate(ch.entwine.weblounge.common.content.page.PageTemplate)
     */
    public void setDefaultTemplate(PageTemplate template) {
        if (template != null) {
            template.setSite(this);
            templates.put(template.getIdentifier(), template);
            logger.debug("Default page template is '{}'", template.getIdentifier());
        } else
            logger.debug("Default template is now undefined");
        this.defaultTemplate = template;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getDefaultTemplate()
     */
    public PageTemplate getDefaultTemplate() {
        return defaultTemplate;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#addLanguage(ch.entwine.weblounge.common.language.Language)
     */
    public void addLanguage(Language language) {
        if (language != null)
            languages.put(language.getIdentifier(), language);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#removeLanguage(ch.entwine.weblounge.common.language.Language)
     */
    public void removeLanguage(Language language) {
        if (language != null) {
            languages.remove(language.getIdentifier());
            if (language.equals(defaultLanguage))
                defaultLanguage = null;
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getLanguage(java.lang.String)
     */
    public Language getLanguage(String languageId) {
        return languages.get(languageId);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getLanguages()
     */
    public Language[] getLanguages() {
        return languages.values().toArray(new Language[languages.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#supportsLanguage(ch.entwine.weblounge.common.language.Language)
     */
    public boolean supportsLanguage(Language language) {
        return languages.values().contains(language);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#setDefaultLanguage(ch.entwine.weblounge.common.language.Language)
     */
    public void setDefaultLanguage(Language language) {
        addLanguage(language);
        defaultLanguage = language;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getDefaultLanguage()
     */
    public Language getDefaultLanguage() {
        return defaultLanguage;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#addLayout(ch.entwine.weblounge.common.content.page.PageLayout)
     */
    public void addLayout(PageLayout layout) {
        if (layout == null)
            throw new IllegalStateException("Layout must not be null");
        layouts.put(layout.getIdentifier(), layout);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#removeLayout(java.lang.String)
     */
    public PageLayout removeLayout(String layout) {
        if (layout == null)
            throw new IllegalStateException("Layout must not be null");
        return layouts.remove(layout);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getLayout(java.lang.String)
     */
    public PageLayout getLayout(String layout) {
        return layouts.get(layout);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getLayouts()
     */
    public PageLayout[] getLayouts() {
        return layouts.values().toArray(new PageLayout[layouts.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#setDefaultHostname(URL)
     */
    public void setDefaultHostname(SiteURL url) {
        defaultURL = url;
        if (url != null)
            addHostname(url);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#setDefaultHostname(ch.entwine.weblounge.common.site.SiteURL,
     *      ch.entwine.weblounge.common.site.Environment)
     */
    public void setDefaultHostname(SiteURL url, Environment environment) {
        if (url == null)
            throw new IllegalArgumentException("Url must not be null");
        if (environment == null)
            throw new IllegalArgumentException("Environment must not be null");
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#addHostname(URL)
     */
    public void addHostname(SiteURL url) {
        if (url == null)
            throw new IllegalArgumentException("Url must not be null");
        urls.add(url);

        if (url.isDefault()) {
            defaultURLByEnvironment.put(url.getEnvironment(), url);
            if (Environment.Production.equals(url.getEnvironment()))
                defaultURL = url;
        }

        // Make sure we have a default URL
        if (defaultURL == null)
            defaultURL = url;

        // ... and a default
        if (defaultURLByEnvironment.get(url.getEnvironment()) == null)
            defaultURLByEnvironment.put(url.getEnvironment(), url);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#removeHostname(URL)
     */
    public boolean removeHostname(SiteURL url) {
        if (url == null)
            throw new IllegalArgumentException("Hostname must not be null");
        if (url.equals(defaultURL))
            defaultURL = null;
        return urls.remove(url);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getHostnames()
     */
    public SiteURL[] getHostnames() {
        return urls.toArray(new SiteURL[urls.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getHostname()
     */
    public SiteURL getHostname() {
        return defaultURL;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getHostname(ch.entwine.weblounge.common.site.Environment)
     */
    public SiteURL getHostname(Environment environment) {
        SiteURL url = defaultURLByEnvironment.get(environment);
        if (url != null)
            return url;
        return defaultURL;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#addModule(ch.entwine.weblounge.common.site.Module)
     */
    public void addModule(Module module) throws ModuleException {
        if (module == null)
            throw new IllegalArgumentException("Module must not be null");
        module.setSite(this);
        modules.put(module.getIdentifier(), module);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#removeModule(java.lang.String)
     */
    public Module removeModule(String module) throws ModuleException {
        if (module == null)
            throw new IllegalArgumentException("Module must not be null");
        Module m = modules.remove(module);
        if (m != null)
            m.destroy();
        return m;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getModule(java.lang.String)
     */
    public Module getModule(String module) {
        if (module == null)
            throw new IllegalArgumentException("Module must not be null");
        return modules.get(module);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getModules()
     */
    public Module[] getModules() {
        return modules.values().toArray(new Module[modules.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#setContentRepository(ch.entwine.weblounge.common.repository.ContentRepository)
     */
    public void setContentRepository(ContentRepository repository) {
        ContentRepository oldRepository = contentRepository;
        this.contentRepository = repository;
        if (repository != null) {
            logger.debug("Content repository {} connected to site '{}'", repository, this);
            fireRepositoryConnected(repository);
        } else {
            logger.debug("Content repository {} disconnected from site '{}'", oldRepository, this);
            fireRepositoryDisconnected(oldRepository);
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#setSecurity(java.net.URL)
     */
    @Override
    public void setSecurity(URL url) {
        this.security = url;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getSecurity()
     */
    @Override
    public URL getSecurity() {
        return security;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#setDigestType(ch.entwine.weblounge.common.security.DigestType)
     */
    @Override
    public void setDigestType(DigestType digest) {
        this.digestType = digest;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getDigestType()
     */
    @Override
    public DigestType getDigestType() {
        return digestType;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getContentRepository()
     */
    public ContentRepository getContentRepository() {
        return contentRepository;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#suggest(java.lang.String,
     *      java.lang.String, int)
     */
    public List<String> suggest(String dictionary, String seed, int count) throws ContentRepositoryException {
        if (contentRepository == null)
            throw new IllegalStateException("Cannot suggest while site without a content repository");
        return contentRepository.suggest(dictionary, seed, count);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#addRequestListener(ch.entwine.weblounge.common.request.RequestListener)
     */
    public void addRequestListener(RequestListener listener) {
        if (requestListeners == null)
            requestListeners = new ArrayList<RequestListener>();
        synchronized (requestListeners) {
            requestListeners.add(listener);
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#removeRequestListener(ch.entwine.weblounge.common.request.RequestListener)
     */
    public void removeRequestListener(RequestListener listener) {
        if (requestListeners != null) {
            synchronized (requestListeners) {
                requestListeners.remove(listener);
            }
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#addSiteListener(ch.entwine.weblounge.common.site.SiteListener)
     */
    public void addSiteListener(SiteListener listener) {
        if (siteListeners == null)
            siteListeners = new ArrayList<SiteListener>();
        synchronized (siteListeners) {
            siteListeners.add(listener);
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#removeSiteListener(ch.entwine.weblounge.common.site.SiteListener)
     */
    public void removeSiteListener(SiteListener listener) {
        if (siteListeners != null) {
            synchronized (siteListeners) {
                siteListeners.remove(listener);
            }
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#addUserListener(ch.entwine.weblounge.common.security.UserListener)
     */
    public void addUserListener(UserListener listener) {
        if (userListeners == null)
            userListeners = new ArrayList<UserListener>();
        synchronized (userListeners) {
            userListeners.add(listener);
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#removeUserListener(ch.entwine.weblounge.common.security.UserListener)
     */
    public void removeUserListener(UserListener listener) {
        if (userListeners != null) {
            synchronized (userListeners) {
                userListeners.remove(listener);
            }
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#addLocalRole(java.lang.String,
     *      java.lang.String)
     */
    public void addLocalRole(String systemRole, String localRole) {
        if (StringUtils.isBlank(systemRole))
            throw new IllegalArgumentException("System role name cannot be blank");
        if (StringUtils.isBlank(localRole))
            throw new IllegalArgumentException("Local role name cannot be blank");
        localRoles.put(systemRole, localRole);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#getLocalRole(java.lang.String)
     */
    public String getLocalRole(String role) {
        if (localRoles.containsKey(role))
            return localRoles.get(role);
        return null;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#start()
     */
    public synchronized void start() throws SiteException, IllegalStateException {
        logger.debug("Starting site {}", this);
        if (isOnline)
            throw new IllegalStateException("Site is already running");

        // Start the site modules
        synchronized (modules) {
            List<Module> started = new ArrayList<Module>(modules.size());
            for (Module module : modules.values()) {
                if (!module.isEnabled())
                    continue;

                try {
                    module.start();
                    started.add(module);

                    // start jobs
                    for (Job job : module.getJobs()) {
                        scheduleJob(job);
                    }

                    // actions are being registered automatically

                } catch (Throwable t) {
                    logger.error("Error starting module '{}'", module, t);
                }
            }
        }

        // Register the integration tests
        if (integrationTests != null && integrationTests.size() > 0) {
            for (IntegrationTest test : integrationTests) {
                logger.debug("Registering integration test {}", test.getName());
                integrationTestRegistrations
                        .add(getBundleContext().registerService(IntegrationTest.class.getName(), test, null));
            }
        }

        // Finally, mark this site as running
        isOnline = true;
        logger.info("Site '{}' started", this);

        // Tell listeners
        fireSiteStarted();
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#stop()
     */
    public synchronized void stop() throws IllegalStateException {
        logger.debug("Stopping site '{}'", this);
        if (!isOnline)
            throw new IllegalStateException("Site is not running");

        // Stop jobs
        synchronized (jobs) {
            for (QuartzJob job : jobs.values()) {
                unscheduleJob(job);
            }
        }

        // Shutdown all of the modules
        synchronized (modules) {
            for (Module module : modules.values()) {
                try {
                    logger.debug("Stopping module '{}'", module);
                    module.stop();
                } catch (Throwable t) {
                    logger.error("Error stopping module '{}'", module, t);
                }
            }
        }

        // Unregister integration tests
        logger.debug("Unregistering integration tests");
        for (ServiceRegistration registration : integrationTestRegistrations) {
            try {
                registration.unregister();
            } catch (IllegalStateException e) {
                // Never mind, the service has been unregistered already
            } catch (Throwable t) {
                logger.error("Unregistering integration test failed: {}", t.getMessage());
            }
        }

        // Finally, mark this site as stopped
        isOnline = false;
        logger.info("Site '{}' stopped", this);

        // Tell listeners
        fireSiteStopped();
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.site.Site#isOnline()
     */
    public boolean isOnline() {
        return isOnline && contentRepository != null;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.request.RequestListener#requestStarted(ch.entwine.weblounge.common.request.WebloungeRequest,
     *      ch.entwine.weblounge.common.request.WebloungeResponse)
     */
    public void requestStarted(WebloungeRequest request, WebloungeResponse response) {
        // TODO: Remove
        fireRequestStarted(request, response);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.request.RequestListener#requestDelivered(ch.entwine.weblounge.common.request.WebloungeRequest,
     *      ch.entwine.weblounge.common.request.WebloungeResponse)
     */
    public void requestDelivered(WebloungeRequest request, WebloungeResponse response) {
        // TODO: Remove
        fireRequestDelivered(request, response);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.request.RequestListener#requestFailed(ch.entwine.weblounge.common.request.WebloungeRequest,
     *      ch.entwine.weblounge.common.request.WebloungeResponse, int)
     */
    public void requestFailed(WebloungeRequest request, WebloungeResponse response, int reason) {
        // TODO: Remove
        fireRequestFailed(request, response, reason);
    }

    /**
     * Method to fire a <code>requestStarted()</code> message to all registered
     * <code>RequestListener</code>s.
     * 
     * @param request
     *          the started request
     * @param response
     *          the response
     */
    protected void fireRequestStarted(WebloungeRequest request, WebloungeResponse response) {
        if (requestListeners == null)
            return;
        synchronized (requestListeners) {
            for (RequestListener listener : requestListeners) {
                listener.requestStarted(request, response);
            }
        }
    }

    /**
     * Method to fire a <code>requestDelivered()</code> message to all registered
     * <code>RequestListener</code>s.
     * 
     * @param request
     *          the delivered request
     * @param response
     *          the response
     */
    protected void fireRequestDelivered(WebloungeRequest request, WebloungeResponse response) {
        if (requestListeners == null)
            return;
        synchronized (requestListeners) {
            for (RequestListener listener : requestListeners) {
                listener.requestDelivered(request, response);
            }
        }
    }

    /**
     * Method to fire a <code>requestFailed()</code> message to all registered
     * <code>RequestListener</code>s.
     * 
     * @param request
     *          the failed request
     * @param response
     *          the response
     * @param error
     *          the error code
     */
    protected void fireRequestFailed(WebloungeRequest request, WebloungeResponse response, int error) {
        if (requestListeners == null)
            return;
        synchronized (requestListeners) {
            for (RequestListener listener : requestListeners) {
                listener.requestFailed(request, response, error);
            }
        }
    }

    /**
     * This method is called if a user is logged in.
     * 
     * @param user
     *          the user that logged in
     */
    protected void fireUserLoggedIn(User user) {
        if (userListeners == null)
            return;
        synchronized (userListeners) {
            for (UserListener listener : userListeners) {
                listener.userLoggedIn(user);
            }
        }
    }

    /**
     * This method is called if a user is logged out.
     * 
     * @param user
     *          the user that logged out
     */
    protected void fireUserLoggedOut(User user) {
        if (userListeners == null)
            return;
        synchronized (userListeners) {
            for (UserListener listener : userListeners) {
                listener.userLoggedOut(user);
            }
        }
    }

    /**
     * Method to fire a <code>siteStarted()</code> message to all registered
     * <code>SiteListener</code>s.
     */
    protected void fireSiteStarted() {
        if (siteListeners == null)
            return;
        synchronized (siteListeners) {
            for (SiteListener listener : siteListeners) {
                listener.siteStarted(this);
            }
        }
    }

    /**
     * Method to fire a <code>siteStopped()</code> message to all registered
     * <code>SiteListener</code>s.
     */
    protected void fireSiteStopped() {
        if (siteListeners == null)
            return;
        synchronized (siteListeners) {
            for (SiteListener listener : siteListeners) {
                listener.siteStopped(this);
            }
        }
    }

    /**
     * Method to fire a <code>repositoryConnected()</code> message to all
     * registered <code>SiteListener</code>s.
     * 
     * @param repository
     *          the content repository
     */
    protected void fireRepositoryConnected(ContentRepository repository) {
        if (siteListeners == null)
            return;
        synchronized (siteListeners) {
            for (SiteListener listener : siteListeners) {
                listener.repositoryConnected(this, repository);
            }
        }
    }

    /**
     * Method to fire a <code>repositoryDisconnected()</code> message to all
     * registered <code>SiteListener</code>s.
     * 
     * @param repository
     *          the content repository
     */
    protected void fireRepositoryDisconnected(ContentRepository repository) {
        if (siteListeners == null)
            return;
        synchronized (siteListeners) {
            for (SiteListener listener : siteListeners) {
                listener.repositoryDisconnected(this, repository);
            }
        }
    }

    /* -------------------------------- OSGi -------------------------------- */

    /**
     * This method is a callback from the service tracker that is started when
     * this site is started. It is looking for an implementation of the Quartz
     * scheduler. The configuration is expected to be <code>0..1</code>, so there
     * should only be one scheduler instance at any given moment.
     * 
     * @param scheduler
     *          the quartz scheduler
     */
    synchronized void setScheduler(Scheduler scheduler) {
        this.scheduler = scheduler;
        this.quartzTriggerListener = new QuartzTriggerListener(this);
        try {
            this.scheduler.addTriggerListener(quartzTriggerListener);
            if (isOnline) {
                synchronized (jobs) {
                    for (QuartzJob job : jobs.values()) {
                        scheduleJob(job);
                    }
                }
            }
        } catch (SchedulerException e) {
            logger.error("Error adding trigger listener to quartz scheduler", e);
        }
    }

    /**
     * This method is a callback from the service tracker that is started when
     * this site is started, indicating that the scheduler service is no longer
     * available.
     */
    void removeScheduler() {
        if (!isShutdownInProgress)
            logger.info("Site '{}' can no longer execute jobs (scheduler was taken down)", this);
        this.quartzTriggerListener = null;
    }

    /**
     * Callback from the OSGi environment to activate the site. Subclasses should
     * make sure to call this super implementation as it will assist in correctly
     * setting up the site.
     * <p>
     * This method should be configured in the <tt>Dynamic Services</tt> section
     * of your bundle.
     * 
     * @param context
     *          the bundle context
     * @param properties
     *          the component properties
     * @throws Exception
     *           if the site activation fails
     */
    protected void activate(BundleContext ctx, Map<String, String> properties) throws Exception {

        bundleContext = ctx;
        bundleContext.getBundle();
        serviceProperties = properties;

        // Fix the site identifier
        if (getIdentifier() == null) {
            String identifier = properties.get(PROP_IDENTIFIER);
            if (identifier == null)
                throw new IllegalStateException("Property'" + PROP_IDENTIFIER + "' missing from site bundle");
            setIdentifier(identifier);
        }
    }

    /**
     * Callback from the OSGi environment to deactivate the site. Subclasses
     * should make sure to call this super implementation as it will assist in
     * correctly shutting down the site.
     * <p>
     * This method should be configured in the <tt>Dynamic Services</tt> section
     * of your bundle.
     * 
     * @param context
     *          the bundle context
     * @param properties
     *          the component properties
     * @throws Exception
     *           if the site deactivation fails
     */
    protected void deactivate(BundleContext context, Map<String, String> properties) throws Exception {
        try {
            isShutdownInProgress = true;
            logger.debug("Taking down site '{}'", this);
            logger.debug("Stopped looking for a job scheduling services");
            if (schedulingServiceTracker != null)
                schedulingServiceTracker.close();
            logger.info("Site '{}' deactivated", this);
        } finally {
            isShutdownInProgress = false;
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.Customizable#setOption(java.lang.String,
     *      java.lang.String)
     */
    public void setOption(String name, String value) {
        options.setOption(name, 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#removeOption(java.lang.String)
     */
    public void removeOption(String name) {
        options.removeOption(name);
    }

    /**
     * {@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#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#getOptions()
     */
    public Map<String, Map<Environment, List<String>>> getOptions() {
        return options.getOptions();
    }

    /**
     * Schedules the job with the Quartz job scheduler.
     * 
     * @param job
     *          the job
     */
    private void scheduleJob(Job job) {
        if (scheduler == null)
            return;

        // If this scheduling operation is due to a site restart, the job needs to
        // be reset, otherwise fire-once jobs won't work.
        job.getTrigger().reset();

        // Throw the job at quartz
        String groupName = "site " + this.getIdentifier();
        String jobIdentifier = job.getIdentifier();
        Class<? extends JobWorker> jobClass = job.getWorker();
        JobTrigger trigger = job.getTrigger();

        synchronized (jobs) {

            // Set up the job detail
            JobDataMap jobData = new JobDataMap();
            jobData.put(QuartzJobWorker.CLASS, jobClass);
            jobData.put(QuartzJobWorker.CLASS_LOADER, new BundleClassLoader(bundleContext.getBundle()));
            jobData.put(QuartzJobWorker.CONTEXT, job.getContext());
            job.getContext().put(Site.class.getName(), this);
            job.getContext().put(BundleContext.class.getName(), bundleContext);
            JobDetail jobDetail = new JobDetail(jobIdentifier, groupName, QuartzJobWorker.class);
            jobDetail.setJobDataMap(jobData);

            // Define the trigger
            Trigger quartzTrigger = new QuartzJobTrigger(jobIdentifier, groupName, trigger);
            quartzTrigger.addTriggerListener(quartzTriggerListener.getName());

            // Schedule
            try {
                Date date = scheduler.scheduleJob(jobDetail, quartzTrigger);
                jobs.put(jobIdentifier, new QuartzJob(jobIdentifier, jobClass, trigger));
                String repeat = trigger.getNextExecutionAfter(date) != null ? " first" : "";
                logger.info("Job '{}' scheduled,{} execution scheduled for {}",
                        new Object[] { jobIdentifier, repeat, date });
            } catch (SchedulerException e) {
                logger.error("Error trying to schedule job '{}': {}",
                        new Object[] { jobIdentifier, e.getMessage(), e });
            }
        }
    }

    /**
     * Removes the job from the Quartz job scheduler.
     * 
     * @param job
     *          the job
     */
    private void unscheduleJob(QuartzJob job) {
        try {
            if (scheduler == null || scheduler.isShutdown())
                return;
        } catch (SchedulerException e1) {
            // Ignore
        }

        String groupName = "site " + this.getIdentifier();
        String jobIdentifier = job.getIdentifier();
        try {
            if (scheduler.unscheduleJob(jobIdentifier, groupName))
                logger.info("Job '{}' unscheduled", jobIdentifier);
        } catch (SchedulerException e) {
            logger.error("Error trying to schedule job {}: {}", new Object[] { jobIdentifier, e.getMessage(), e });
        }
    }

    /**
     * Loads all integration tests.
     * 
     * @return the tests
     */
    private List<IntegrationTest> loadIntegrationTests() {
        BundleContext ctx = getBundleContext();

        logger.info("Loading integration tests for '{}'", this);

        // Load test classes
        List<IntegrationTest> tests = loadIntegrationTestClasses("/", ctx.getBundle());
        for (IntegrationTest test : tests) {
            logger.debug("Registering integration test " + test.getClass());
            integrationTestRegistrations.add(ctx.registerService(IntegrationTest.class.getName(), test, null));
        }

        // Find and register site-wide integration tests
        logger.debug("Looking for integration tests in site '{}'", this);
        Enumeration<URL> siteDirectories = bundleContext.getBundle().findEntries("site", "*", false);
        while (siteDirectories != null && siteDirectories.hasMoreElements()) {
            URL entry = siteDirectories.nextElement();
            if (entry.getPath().endsWith("/tests/")) {
                tests.addAll(loadIntegrationTestDefinitions(entry.getPath()));
                break;
            }
        }

        // Find and register module integration tests
        Enumeration<URL> modules = bundleContext.getBundle().findEntries("site/modules", "*", false);
        logger.debug("Looking for integration tests in site '{}' modules", this);
        while (modules != null && modules.hasMoreElements()) {
            URL module = modules.nextElement();
            Enumeration<URL> moduleDirectories = bundleContext.getBundle().findEntries(module.getPath(), "*",
                    false);
            while (moduleDirectories != null && moduleDirectories.hasMoreElements()) {
                URL entry = moduleDirectories.nextElement();
                if (entry.getPath().endsWith("/tests/")) {
                    tests.addAll(loadIntegrationTestDefinitions(entry.getPath()));
                    break;
                }
            }
        }

        if (tests.size() > 0)
            logger.info("Registering {} integration tests for site '{}'", tests.size(), this);

        return tests;
    }

    /**
     * Loads and registers the integration tests that are found in the bundle at
     * the given location.
     * 
     * @param dir
     *          the directory containing the test files
     */
    private List<IntegrationTest> loadIntegrationTestDefinitions(String dir) {
        Enumeration<?> entries = bundleContext.getBundle().findEntries(dir, "*.xml", true);

        // Schema validator setup
        SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        URL schemaUrl = SiteImpl.class.getResource("/xsd/test.xsd");
        Schema testSchema = null;
        try {
            testSchema = schemaFactory.newSchema(schemaUrl);
        } catch (SAXException e) {
            logger.error("Error loading XML schema for test definitions: {}", e.getMessage());
            return Collections.emptyList();
        }

        // Module.xml document builder setup
        DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
        docBuilderFactory.setSchema(testSchema);
        docBuilderFactory.setNamespaceAware(true);

        // The list of tests
        List<IntegrationTest> tests = new ArrayList<IntegrationTest>();

        while (entries != null && entries.hasMoreElements()) {
            URL entry = (URL) entries.nextElement();

            // Validate and read the module descriptor
            ValidationErrorHandler errorHandler = new ValidationErrorHandler(entry);
            DocumentBuilder docBuilder;

            try {
                docBuilder = docBuilderFactory.newDocumentBuilder();
                docBuilder.setErrorHandler(errorHandler);
                Document doc = docBuilder.parse(entry.openStream());
                if (errorHandler.hasErrors()) {
                    logger.warn("Error parsing integration test {}: XML validation failed", entry);
                    continue;
                }
                IntegrationTestGroup test = IntegrationTestParser.fromXml(doc.getFirstChild());
                test.setSite(this);
                test.setGroup(getName());
                tests.add(test);
            } catch (SAXException e) {
                throw new IllegalStateException(e);
            } catch (IOException e) {
                throw new IllegalStateException(e);
            } catch (ParserConfigurationException e) {
                throw new IllegalStateException(e);
            }
        }

        return tests;
    }

    /**
     * Loads the integration test classes from the class path and publishes them
     * to the OSGi registry.
     * 
     * @param path
     *          the bundle path to load classes from
     * @param bundle
     *          the bundle
     */
    private List<IntegrationTest> loadIntegrationTestClasses(String path, Bundle bundle) {
        List<IntegrationTest> tests = new ArrayList<IntegrationTest>();

        // Load the classes in question
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        Enumeration<?> entries = bundle.findEntries("/", "*.class", true);
        if (entries == null) {
            return tests;
        }

        // Look at the classes and instantiate those that implement the integration
        // test interface.
        while (entries.hasMoreElements()) {
            URL url = (URL) entries.nextElement();
            Class<?> c = null;
            String className = url.getPath();
            try {
                className = className.substring(1, className.indexOf(".class"));
                className = className.replace('/', '.');
                c = loader.loadClass(className);
                boolean implementsInterface = Arrays.asList(c.getInterfaces()).contains(IntegrationTest.class);
                boolean extendsBaseClass = false;
                if (c.getSuperclass() != null) {
                    extendsBaseClass = IntegrationTestBase.class.getName().equals(c.getSuperclass().getName());
                }
                if (!implementsInterface && !extendsBaseClass)
                    continue;
                IntegrationTest test = (IntegrationTest) c.newInstance();
                test.setSite(this);
                tests.add(test);
            } catch (ClassNotFoundException e) {
                throw new IllegalStateException("Implementation " + className + " for integration test of class '"
                        + identifier + "' not found", e);
            } catch (NoClassDefFoundError e) {
                // We are trying to load each and every class here, so we may as well
                // see classes that are not meant to be loaded
                logger.debug("The related class " + e.getMessage() + " for potential test case implementation "
                        + className + " could not be found");
            } catch (InstantiationException e) {
                throw new IllegalStateException("Error instantiating impelementation " + className
                        + " for integration test '" + identifier + "'", e);
            } catch (IllegalAccessException e) {
                throw new IllegalStateException("Access violation instantiating implementation " + className
                        + " for integration test '" + identifier + "'", e);
            } catch (Throwable t) {
                throw new IllegalStateException(
                        "Error loading implementation " + className + " for integration test '" + identifier + "'",
                        t);
            }

        }
        return tests;
    }

    /**
     * Initializes this site 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 site node
     * @throws IllegalStateException
     *           if the site cannot be parsed
     * @see #fromXml(Node, XPath)
     * @see #toXml()
     */
    public static Site fromXml(Node config) throws IllegalStateException {
        XPath xpath = XPathFactory.newInstance().newXPath();

        // Define the xml namespace
        XPathNamespaceContext nsCtx = new XPathNamespaceContext(false);
        nsCtx.defineNamespaceURI("ns", SITE_XMLNS);
        xpath.setNamespaceContext(nsCtx);

        return fromXml(config, xpath);
    }

    /**
     * Initializes this site from an XML node that was generated using
     * {@link #toXml()}.
     * 
     * @param config
     *          the site node
     * @param xpathProcessor
     *          xpath processor to use
     * @throws IllegalStateException
     *           if the site cannot be parsed
     * @see #toXml()
     */
    public static Site fromXml(Node config, XPath xpathProcessor) throws IllegalStateException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

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

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

        } else {
            site = new SiteImpl();
            site.setIdentifier(identifier);
        }

        // name
        String name = XPathHelper.valueOf(config, "ns:name", xpathProcessor);
        if (name == null)
            throw new IllegalStateException("Site '" + identifier + " has no name");
        site.setName(name);

        // domains
        NodeList urlNodes = XPathHelper.selectList(config, "ns:domains/ns:url", xpathProcessor);
        if (urlNodes.getLength() == 0)
            throw new IllegalStateException("Site '" + identifier + " has no hostname");
        String url = null;
        try {
            for (int i = 0; i < urlNodes.getLength(); i++) {
                Node urlNode = urlNodes.item(i);
                url = urlNode.getFirstChild().getNodeValue();
                boolean defaultUrl = ConfigurationUtils.isDefault(urlNode);
                Environment environment = Environment.Production;
                Node environmentAttribute = urlNode.getAttributes().getNamedItem("environment");
                if (environmentAttribute != null && environmentAttribute.getNodeValue() != null)
                    environment = Environment.valueOf(StringUtils.capitalize(environmentAttribute.getNodeValue()));

                SiteURLImpl siteURL = new SiteURLImpl(new URL(url));
                siteURL.setDefault(defaultUrl);
                siteURL.setEnvironment(environment);

                site.addHostname(siteURL);
            }
        } catch (MalformedURLException e) {
            throw new IllegalStateException("Site '" + identifier + "' defines malformed url: " + url);
        }

        // languages
        NodeList languageNodes = XPathHelper.selectList(config, "ns:languages/ns:language", xpathProcessor);
        if (languageNodes.getLength() == 0)
            throw new IllegalStateException("Site '" + identifier + " has no languages");
        for (int i = 0; i < languageNodes.getLength(); i++) {
            Node languageNode = languageNodes.item(i);
            Node defaultAttribute = languageNode.getAttributes().getNamedItem("default");
            String languageId = languageNode.getFirstChild().getNodeValue();
            try {
                Language language = LanguageUtils.getLanguage(languageId);
                if (ConfigurationUtils.isTrue(defaultAttribute))
                    site.setDefaultLanguage(language);
                else
                    site.addLanguage(language);
            } catch (UnknownLanguageException e) {
                throw new IllegalStateException(
                        "Site '" + identifier + "' defines unknown language: " + languageId);
            }
        }

        // templates
        NodeList templateNodes = XPathHelper.selectList(config, "ns:templates/ns:template", xpathProcessor);
        PageTemplate firstTemplate = null;
        for (int i = 0; i < templateNodes.getLength(); i++) {
            PageTemplate template = PageTemplateImpl.fromXml(templateNodes.item(i), xpathProcessor);
            boolean isDefault = ConfigurationUtils.isDefault(templateNodes.item(i));
            if (isDefault && site.getDefaultTemplate() != null) {
                logger.warn("Site '{}' defines more than one default templates", site.getIdentifier());
            } else if (isDefault) {
                site.setDefaultTemplate(template);
                logger.debug("Site '{}' uses default template '{}'", site.getIdentifier(),
                        template.getIdentifier());
            } else {
                site.addTemplate(template);
                logger.debug("Added template '{}' to site '{}'", template.getIdentifier(), site.getIdentifier());
            }
            if (firstTemplate == null)
                firstTemplate = template;
        }

        // Make sure we have a default template
        if (site.getDefaultTemplate() == null) {
            if (firstTemplate == null)
                throw new IllegalStateException(
                        "Site '" + site.getIdentifier() + "' does not specify any page templates");
            logger.warn("Site '{}' does not specify a default template. Using '{}'", site.getIdentifier(),
                    firstTemplate.getIdentifier());
            site.setDefaultTemplate(firstTemplate);
        }

        // security
        String securityConfiguration = XPathHelper.valueOf(config, "ns:security/ns:configuration", xpathProcessor);
        if (securityConfiguration != null) {
            URL securityConfig = null;

            // If converting the path into a URL fails, we are assuming that the
            // configuration is part of the bundle
            try {
                securityConfig = new URL(securityConfiguration);
            } catch (MalformedURLException e) {
                logger.debug("Security configuration {} is pointing to the bundle", securityConfiguration);
                securityConfig = SiteImpl.class.getResource(securityConfiguration);
                if (securityConfig == null) {
                    throw new IllegalStateException("Security configuration " + securityConfig + " of site '"
                            + site.getIdentifier() + "' cannot be located inside of bundle", e);
                }
            }
            site.setSecurity(securityConfig);
        }

        // administrator
        Node adminNode = XPathHelper.select(config, "ns:security/ns:administrator", xpathProcessor);
        if (adminNode != null) {
            site.setAdministrator(SiteAdminImpl.fromXml(adminNode, site, xpathProcessor));
        }

        // digest policy
        Node digestNode = XPathHelper.select(config, "ns:security/ns:digest", xpathProcessor);
        if (digestNode != null) {
            site.setDigestType(DigestType.valueOf(digestNode.getFirstChild().getNodeValue()));
        }

        // role definitions
        NodeList roleNodes = XPathHelper.selectList(config, "ns:security/ns:roles/ns:*", xpathProcessor);
        for (int i = 0; i < roleNodes.getLength(); i++) {
            Node roleNode = roleNodes.item(i);
            String roleName = roleNode.getLocalName();
            String localRoleName = roleNode.getFirstChild().getNodeValue();
            if (Security.SITE_ADMIN_ROLE.equals(roleName))
                site.addLocalRole(Security.SITE_ADMIN_ROLE, localRoleName);
            if (Security.PUBLISHER_ROLE.equals(roleName))
                site.addLocalRole(Security.PUBLISHER_ROLE, localRoleName);
            if (Security.EDITOR_ROLE.equals(roleName))
                site.addLocalRole(Security.EDITOR_ROLE, localRoleName);
            if (Security.GUEST_ROLE.equals(roleName))
                site.addLocalRole(Security.GUEST_ROLE, localRoleName);
        }

        // options
        Node optionsNode = XPathHelper.select(config, "ns:options", xpathProcessor);
        OptionsHelper.fromXml(optionsNode, site, xpathProcessor);

        return site;
    }

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

        // schema reference
        b.append(
                "xmlns=\"http://www.entwinemedia.com/weblounge/3.0/site\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.entwinemedia.com/weblounge/3.0/site http://www.entwinemedia.com/xsd/weblounge/3.0/site.xsd\"");
        b.append(">");

        // autostart
        b.append("<autostart>").append(autoStart).append("</autostart>");

        // name
        b.append("<name><![CDATA[").append(name).append("]]></name>");

        // class
        if (!this.getClass().equals(SiteImpl.class))
            b.append("<class>").append(this.getClass().getName()).append("</class>");

        // languages
        if (languages.size() > 0) {
            b.append("<languages>");
            for (Language language : languages.values()) {
                b.append("<language");
                if (language.equals(defaultLanguage))
                    b.append(" default=\"true\"");
                b.append(">");
                b.append(language.getIdentifier());
                b.append("</language>");
            }
            b.append("</languages>");
        }

        // hostnames
        if (urls.size() > 0) {
            b.append("<domains>");
            for (SiteURL url : urls) {
                b.append("<url");
                if (url.equals(defaultURL))
                    b.append(" default=\"true\"");
                b.append(" environment=\"").append(url.getEnvironment().toString().toLowerCase()).append("\"");
                b.append(">");
                b.append(url.toExternalForm());
                b.append("</url>");
            }
            b.append("</domains>");
        }

        // security
        if (administrator != null || localRoles.size() > 0) {
            b.append("<security>");
            if (security != null) {
                b.append("<configuration>").append(security.toExternalForm()).append("</configuration>");
            }

            b.append("<digest>").append(digestType.toString()).append("</digest>");

            if (administrator != null)
                b.append(administrator.toXml());

            if (localRoles.size() > 0) {
                b.append("<roles>");

                // Administrator role
                String administratorRole = localRoles.get(Security.SITE_ADMIN_ROLE);
                if (administratorRole != null)
                    b.append("<" + Security.SITE_ADMIN_ROLE + ">").append(administratorRole)
                            .append("</" + Security.SITE_ADMIN_ROLE + ">");

                // Publisher Role
                String publisherRole = localRoles.get(Security.PUBLISHER_ROLE);
                if (publisherRole != null)
                    b.append("<" + Security.PUBLISHER_ROLE + ">").append(publisherRole)
                            .append("</" + Security.PUBLISHER_ROLE + ">");

                // Editor Role
                String editorRole = localRoles.get(Security.EDITOR_ROLE);
                if (publisherRole != null)
                    b.append("<" + Security.EDITOR_ROLE + ">").append(editorRole)
                            .append("</" + Security.EDITOR_ROLE + ">");

                // Guest Role
                String guestRole = localRoles.get(Security.GUEST_ROLE);
                if (publisherRole != null)
                    b.append("<" + Security.GUEST_ROLE + ">").append(guestRole)
                            .append("</" + Security.GUEST_ROLE + ">");

                b.append("</roles>");
            }
            b.append("</security>");
        }

        // templates
        if (templates.size() > 0) {
            b.append("<templates>");
            for (PageTemplate template : templates.values()) {
                b.append(template.toXml());
            }
            b.append("</templates>");
        }

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

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

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

}