Package ch.entwine.weblounge.kernel.site

Source Code of ch.entwine.weblounge.kernel.site.SiteManager

/*
*  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.kernel.site;

import ch.entwine.weblounge.common.repository.ContentRepository;
import ch.entwine.weblounge.common.repository.ContentRepositoryException;
import ch.entwine.weblounge.common.site.Environment;
import ch.entwine.weblounge.common.site.Site;
import ch.entwine.weblounge.common.site.SiteException;
import ch.entwine.weblounge.common.site.SiteURL;

import org.apache.commons.lang.StringUtils;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.component.ComponentContext;
import org.osgi.util.tracker.ServiceTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.PatternSyntaxException;

/**
* The site manager watches site services coming and going and makes them
* available by id, server name etc.
*/
public class SiteManager {

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

  /** The configuration admin service */
  private ConfigurationAdmin configurationAdmin = null;

  /** The site tracker */
  private SiteTracker siteTracker = null;

  /** The content repository tracker */
  private ContentRepositoryTracker repositoryTracker = null;

  /** The sites */
  private List<Site> sites = new ArrayList<Site>();

  /** Maps server names to sites */
  private Map<String, Site> sitesByServerName = new HashMap<String, Site>();

  /** Maps sites to osgi bundles */
  private Map<Site, Bundle> siteBundles = new HashMap<Site, Bundle>();

  /** The environment */
  private Environment environment = Environment.Production;

  /** Maps content repositories to site identifier */
  private Map<String, ContentRepository> repositoriesBySite = new HashMap<String, ContentRepository>();

  /** Maps content repository configurations to site identifier */
  private Map<String, Configuration> repositoryConfigurations = new HashMap<String, Configuration>();

  /** Registered site listeners */
  private List<SiteServiceListener> listeners = new ArrayList<SiteServiceListener>();

  /**
   * Adds <code>listener</code> to the list of site listeners.
   *
   * @param listener
   *          the site listener
   */
  public void addSiteListener(SiteServiceListener listener) {
    synchronized (listeners) {
      listeners.add(listener);
    }
  }

  /**
   * Removes <code>listener</code> from the list of site listeners.
   *
   * @param listener
   *          the site listener
   */
  public void removeSiteListener(SiteServiceListener listener) {
    synchronized (listeners) {
      listeners.remove(listener);
    }
  }

  /**
   * Callback for OSGi's declarative services component activation.
   *
   * @param context
   *          the component context
   * @throws Exception
   *           if component activation fails
   */
  void activate(ComponentContext context) throws Exception {
    logger.debug("Starting site dispatcher");

    BundleContext bundleContext = context.getBundleContext();
    siteTracker = new SiteTracker(this, bundleContext);
    siteTracker.open();

    repositoryTracker = new ContentRepositoryTracker(this, bundleContext);
    repositoryTracker.open();

    logger.debug("Site manager activated");
  }

  /**
   * Callback for OSGi's declarative services component dactivation.
   *
   * @param context
   *          the component context
   * @throws Exception
   *           if component inactivation fails
   */
  void deactivate(ComponentContext context) {
    logger.debug("Deactivating site manager");

    siteTracker.close();
    siteTracker = null;

    repositoryTracker.open();
    repositoryTracker = null;

    logger.info("Site manager stopped");
  }

  /**
   * Returns the site with the given site identifier or <code>null</code> if no
   * such site is currently registered.
   *
   * @param identifier
   *          the site identifier
   * @return the site
   */
  public Site findSiteByIdentifier(String identifier) {
    synchronized (sites) {
      for (Site site : sites) {
        if (site.getIdentifier().equals(identifier)) {
          return site;
        }
      }
    }
    return null;
  }

  /**
   * Returns the site associated with the given server name.
   * <p>
   * Note that the server name is expected to not end with a trailing slash, so
   * please pass in <code>www.entwinemedia.com</code> instead of
   * <code>www.entwinemedia.com/</code>.
   *
   * @param url
   *          the site url, e.g. <code>http://www.entwinemedia.com</code>
   * @return the site
   */
  public Site findSiteByURL(URL url) {
    String hostName = url.getHost();
    Site site = sitesByServerName.get(hostName);
    if (site != null)
      return site;

    // There is obviously no direct match. Therefore, try to find a
    // wildcard match
    synchronized (sites) {
      for (Map.Entry<String, Site> e : sitesByServerName.entrySet()) {
        String siteUrl = e.getKey();

        try {
          // convert the host wildcard (ex. *.domain.tld) to a valid regex (ex.
          // .*\.domain\.tld)
          String alias = siteUrl.replace(".", "\\.");
          alias = alias.replace("*", ".*");
          if (hostName.matches(alias)) {
            site = e.getValue();
            logger.info("Registering {} to site '{}', matching url {}", new Object[] {
                url,
                site.getIdentifier(),
                siteUrl });
            sitesByServerName.put(hostName, site);
            return site;
          }
        } catch (PatternSyntaxException ex) {
          logger.warn("Error while trying to find a host wildcard match: ".concat(ex.getMessage()));
        }
      }
    }

    logger.debug("Lookup for {} did not match any site", url);
    return null;
  }

  /**
   * Returns the site that is defined by the given OSGi bundle or
   * <code>null</code> if the bundle is not known to have registered a site.
   *
   * @param bundle
   *          the bundle
   * @return the site
   */
  public Site findSiteByBundle(Bundle bundle) {
    synchronized (sites) {
      for (Map.Entry<Site, Bundle> entry : siteBundles.entrySet()) {
        if (bundle.equals(entry.getValue()))
          return entry.getKey();
      }
    }
    return null;
  }

  /**
   * Returns the OSGi bundle that contains the given site or <code>null</code>
   * if no such site has been registered.
   *
   * @param site
   *          the site
   * @return the site's OSGi bundle
   */
  public Bundle getSiteBundle(Site site) {
    if (site == null)
      throw new IllegalArgumentException("Parameter 'site' must not be null");
    return siteBundles.get(site);
  }

  /**
   * Returns an iteration of all currently registered sites.
   *
   * @return the sites
   */
  public Iterator<Site> sites() {
    List<Site> site = new ArrayList<Site>();
    site.addAll(sites);
    return site.iterator();
  }

  /**
   * {@inheritDoc}
   *
   * @see ch.entwine.weblounge.dispatcher.SiteDispatcherService#addSite(ch.entwine.weblounge.common.site.Site,
   *      org.osgi.framework.ServiceReference)
   */
  void addSite(Site site, ServiceReference reference) {

    synchronized (sites) {
      sites.add(site);
      siteBundles.put(site, reference.getBundle());

      // Make sure we have an environment
      Environment env = environment;
      if (env == null) {
        logger.warn("No environment has been defined. Assuming '{}'", Environment.Production.toString().toLowerCase());
        env = Environment.Production;
      }

      // Register the site urls and make sure we don't double book
      try {
        site.initialize(env);
      } catch (Throwable t) {
        logger.error("Error loading site '{}': {}", site.getIdentifier(), t.getMessage());
        return;
      }

      for (SiteURL url : site.getHostnames()) {
        if (!env.equals(url.getEnvironment()))
          continue;
        String hostName = url.getURL().getHost();
        Site registeredFirst = sitesByServerName.get(hostName);
        if (registeredFirst != null && !site.equals(registeredFirst)) {

          // Maybe a wildcard site has taken too many urls. Make sure concrete
          // sites are able to take over
          boolean replace = false;
          for (SiteURL siteUrl : registeredFirst.getHostnames()) {
            if (siteUrl.toExternalForm().contains("*")) {
              try {
                // convert the host wildcard (ex. *.domain.tld) to a valid regex
                // (ex.
                // .*\.domain\.tld)
                String registeredAlias = siteUrl.getURL().getHost().replace(".", "\\.");
                registeredAlias = registeredAlias.replace("*", ".*");
                if (hostName.matches(registeredAlias)) {
                  logger.info("Replacing wildcard registration for {} with exact match '{}'", url, site);
                  replace = true;
                  break;
                }
              } catch (PatternSyntaxException ex) {
                logger.warn("Error while trying to find a host wildcard match: ".concat(ex.getMessage()));
              }
            }
          }

          // This is a real conflict
          if (!replace) {
            logger.error("Another site is already registered to {}. Site is not registered", url);
            continue;
          }
        }

        logger.info("Site '{}' will be reachable on host {}", site.getIdentifier(), hostName);
        sitesByServerName.put(hostName, site);
      }

    }

    logger.debug("Site '{}' registered", site);

    // Look for content repositories
    ContentRepository repository = repositoriesBySite.get(site.getIdentifier());
    if (repository != null && site.getContentRepository() == null) {
      try {
        repository.connect(site);
        site.setContentRepository(repository);
        logger.info("Site '{}' connected to content repository at {}", site, repository);
      } catch (ContentRepositoryException e) {
        logger.warn("Error connecting content repository " + repository + " to site '" + site + "'", e);
      }
    } else {
      try {
        Configuration config = configurationAdmin.createFactoryConfiguration("ch.entwine.weblounge.contentrepository.factory", null);
        Dictionary<Object, Object> properties = new Hashtable<Object, Object>();
        properties.put(Site.class.getName().toLowerCase(), site.getIdentifier());
        for (String name : site.getOptionNames()) {
          String[] values = site.getOptionValues(name);
          if (values.length == 1) {
            properties.put(name, values[0]);
          } else {
            properties.put(name, values);
          }
        }
        config.update(properties);
        repositoryConfigurations.put(site.getIdentifier(), config);
      } catch (IOException e) {
        logger.error("Unable to create configuration for content repository of site '" + site + "'", e);
      }
    }

    // Inform site listeners
    synchronized (listeners) {
      for (SiteServiceListener listener : listeners) {
        try {
          listener.siteAppeared(site, reference);
        } catch (Throwable t) {
          logger.error("Error during notifaction of site '{}': {}", site.getIdentifier(), t.getMessage());
          return;
        }
      }
    }

    // Start the site
    if (site.isStartedAutomatically()) {
      try {
        logger.debug("Starting site '{}'", site);
        // TODO: Make sure there is a *running* content repository for this site
        // Alternatively, have the site implementation use a reference to the
        // repository and start itself once the repository switches to "running"
        // state (requires a repository listener)
        site.start();
      } catch (IllegalStateException e) {
        logger.error("Site '{}' could not be started: {}", e.getMessage(), e);
      } catch (SiteException e) {
        logger.error("Site '{}' could not be started: {}", e.getMessage(), e);
      }
    }

  }

  /**
   * {@inheritDoc}
   *
   * @see ch.entwine.weblounge.dispatcher.SiteDispatcherService#removeSite(ch.entwine.weblounge.common.site.Site)
   */
  void removeSite(Site site) {

    // Inform site listeners
    synchronized (listeners) {
      for (SiteServiceListener listener : listeners) {
        listener.siteDisappeared(site);
      }
    }

    // Stop the site if it's running
    try {
      if (site.isOnline()) {
        site.stop();
      }
    } catch (Throwable t) {
      logger.error("Error stopping site '{}'", site.getIdentifier(), t);
    }

    // Remove the site's content repository. Note that the content repository
    // will be disconnected by the content repository tracker
    site.setContentRepository(null);

    // Tell the content repository factory to remove the repository
    Configuration configuration = repositoryConfigurations.get(site.getIdentifier());
    if (configuration != null) {
      try {
        configuration.delete();
      } catch (IOException e) {
        logger.error("Error deleting repository configuration for site '" + site.getIdentifier() + "'", e);
      }
    } else {
      logger.debug("No connected content repository found to shutdown for site '{}'", site.getIdentifier());
    }

    // Remove it from the registry
    synchronized (sites) {
      sites.remove(site);
      siteBundles.remove(site);
      Iterator<Site> si = sitesByServerName.values().iterator();
      while (si.hasNext()) {
        Site s = si.next();
        if (site.equals(s)) {
          si.remove();
        }
      }
    }

    logger.debug("Site {} unregistered", site);
  }

  /**
   * Adds the content repository to the list of registered repositories.
   *
   * @param siteIdentifier
   *          the site identifier
   * @param repository
   *          the content repository
   * @throws ContentRepositoryException
   *           if connecting the content repository to the site fails
   */
  synchronized void addContentRepository(String siteIdentifier,
      ContentRepository repository) throws ContentRepositoryException {
    if (StringUtils.isBlank(siteIdentifier))
      throw new IllegalArgumentException("Site identifier must not be null");
    if (repository == null)
      throw new IllegalArgumentException("Content repository must not be null");

    Site site = findSiteByIdentifier(siteIdentifier);
    if (site != null) {
      try {
        repository.connect(site);
        logger.info("Site '{}' connected to content repository at {}", site, repository);
        site.setContentRepository(repository);
      } catch (ContentRepositoryException e) {
        logger.warn("Error connecting content repository " + repository + " to site '" + site + "'", e);
        throw e;
      }
    }

    repositoriesBySite.put(siteIdentifier, repository);
  }

  /**
   * Adds the content repository to the list of registered repositories.
   *
   * @param repository
   *          the content repository
   */
  synchronized void removeContentRepository(ContentRepository repository) {
    if (repository == null)
      throw new IllegalArgumentException("Content repository must not be null");

    // Find the site that is associated with the content repository
    String siteIdentifier = null;
    for (Map.Entry<String, ContentRepository> entry : repositoriesBySite.entrySet()) {
      if (entry.getValue().equals(repository)) {
        siteIdentifier = entry.getKey();
        break;
      }
    }

    // Tell the site to no longer use it
    if (siteIdentifier != null) {
      Site site = findSiteByIdentifier(siteIdentifier);
      repositoriesBySite.remove(siteIdentifier);

      // Tell the repository to clean up
      if (site != null && site.getContentRepository() != null) {
        try {
          site.setContentRepository(null);
          repository.disconnect();
        } catch (ContentRepositoryException e) {
          logger.warn("Error disconnecting content repository " + repository, e);
        }
      }
    }

  }

  /**
   * OSGi environment callback that passes in the configuration admin service.
   *
   * @param configurationAdmin
   *          the configuration admin service
   */
  void setConfigurationAdmin(ConfigurationAdmin configurationAdmin) {
    this.configurationAdmin = configurationAdmin;
  }

  /**
   * OSGi callback that passes in the environment.
   *
   * @param environment
   *          the environment
   */
  void setEnvironment(Environment environment) {
    this.environment = environment;
  }

  /**
   * OSGi callback that removes the environment.
   *
   * @param environment
   *          the environment
   */
  void removeEnvironment(Environment environment) {
    if (Environment.Production.equals(this.environment)) {
      logger.info("Changing site environments to {}", Environment.Production);
      for (Site site : sites) {
        try {
          site.initialize(Environment.Production);
        } catch (Throwable t) {
          logger.warn("Error switching environment of site '{}' to '{}': {}", new Object[] {
              site.getIdentifier(),
              Environment.Production,
              t.getMessage() });
        }
      }
    }
    this.environment = Environment.Production;
  }

  /**
   * This tracker is used to track <code>Site</code> services. Once a site is
   * detected, it registers that site with the
   * <code>SiteDispatcherService</code>.
   */
  private final class SiteTracker extends ServiceTracker {

    /** The site dispatcher */
    private SiteManager siteManager = null;

    /**
     * Creates a new <code>SiteTracker</code>.
     *
     * @param siteManager
     *          the site dispatcher
     * @param context
     *          the site dispatcher's bundle context
     */
    public SiteTracker(SiteManager siteManager, BundleContext context) {
      super(context, Site.class.getName(), null);
      this.siteManager = siteManager;
    }

    /**
     * {@inheritDoc}
     *
     * @see org.osgi.util.tracker.ServiceTrackerCustomizer#addingService(org.osgi.framework.ServiceReference)
     */
    @Override
    public Object addingService(final ServiceReference reference) {
      final Site site = (Site) super.addingService(reference);
      Thread daemonThread = new Thread(new Runnable() {
        @Override
        public void run() {
          siteManager.addSite(site, reference);
        }
      });
      daemonThread.setDaemon(true);
      daemonThread.start();
      return site;
    }

    /**
     * {@inheritDoc}
     *
     * @see org.osgi.util.tracker.ServiceTrackerCustomizer#removedService(org.osgi.framework.ServiceReference,
     *      java.lang.Object)
     */
    @Override
    public void removedService(ServiceReference reference, Object service) {
      final Site site = (Site) service;
      Thread daemonThread = new Thread(new Runnable() {
        @Override
        public void run() {
          siteManager.removeSite(site);
        }
      });
      daemonThread.setDaemon(true);
      daemonThread.start();

      if (reference.getBundle() != null) {
        try {
          super.removedService(reference, service);
        } catch (IllegalStateException e) {
          // The service has been removed, probably due to bundle shutdown
        } catch (Throwable t) {
          logger.warn("Error removing service: {}", t.getMessage());
        }
      }
    }
  }

  /**
   * This tracker is used to track <code>ContentRepository</code> services. When
   * a repository either shows up or disappears, the associated site as updated
   * accordingly.
   */
  private final class ContentRepositoryTracker extends ServiceTracker {

    /** The site dispatcher */
    private SiteManager siteManager = null;

    /**
     * Creates a new <code>ContentRepositoryTracker</code>.
     *
     * @param siteManager
     *          the site dispatcher
     * @param context
     *          the site dispatcher's bundle context
     */
    public ContentRepositoryTracker(SiteManager siteManager,
        BundleContext context) {
      super(context, ContentRepository.class.getName(), null);
      this.siteManager = siteManager;
    }

    /**
     * {@inheritDoc}
     *
     * @see org.osgi.util.tracker.ServiceTrackerCustomizer#addingService(org.osgi.framework.ServiceReference)
     */
    @Override
    public Object addingService(ServiceReference reference) {
      String siteIdentifier = (String) reference.getProperty(Site.class.getName().toLowerCase());
      if (siteIdentifier == null) {
        logger.warn("Found content repository without site property");
        return super.addingService(reference);
      }

      // Register the content repository
      ContentRepository repository = (ContentRepository) super.addingService(reference);
      try {
        siteManager.addContentRepository(siteIdentifier, repository);
      } catch (ContentRepositoryException e) {
        return null;
      }

      return repository;
    }

    /**
     * {@inheritDoc}
     *
     * @see org.osgi.util.tracker.ServiceTrackerCustomizer#removedService(org.osgi.framework.ServiceReference,
     *      java.lang.Object)
     */
    @Override
    public void removedService(ServiceReference reference, Object service) {
      ContentRepository repository = (ContentRepository) service;
      siteManager.removeContentRepository(repository);
    }

  }

}
TOP

Related Classes of ch.entwine.weblounge.kernel.site.SiteManager

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.