Java tutorial
/* * 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); } } }