Package ch.entwine.weblounge.common.impl.site

Source Code of ch.entwine.weblounge.common.impl.site.SiteImpl

/*
*  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.2/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.2/site\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.entwinemedia.com/weblounge/3.2/site http://www.entwinemedia.com/xsd/weblounge/3.2/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;
  }

}
TOP

Related Classes of ch.entwine.weblounge.common.impl.site.SiteImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.