ch.entwine.weblounge.kernel.site.SiteManager.java Source code

Java tutorial

Introduction

Here is the source code for ch.entwine.weblounge.kernel.site.SiteManager.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.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);
        }

    }

}