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.impl.util.WebloungeDateFormat; import ch.entwine.weblounge.common.impl.util.config.ConfigurationUtils; import ch.entwine.weblounge.common.repository.ContentRepository; import ch.entwine.weblounge.common.security.SecurityService; import ch.entwine.weblounge.common.site.Action; import ch.entwine.weblounge.common.site.Environment; import ch.entwine.weblounge.common.site.Module; import ch.entwine.weblounge.common.site.Site; import ch.entwine.weblounge.common.site.SiteListener; import ch.entwine.weblounge.common.url.PathUtils; import ch.entwine.weblounge.common.url.UrlUtils; import ch.entwine.weblounge.dispatcher.ActionRequestHandler; import ch.entwine.weblounge.dispatcher.DispatcherConfiguration; import ch.entwine.weblounge.dispatcher.SharedHttpContext; import ch.entwine.weblounge.dispatcher.SiteDispatcherService; import ch.entwine.weblounge.kernel.http.WebXml; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceReference; import org.osgi.framework.ServiceRegistration; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.osgi.service.component.ComponentContext; import org.osgi.service.prefs.BackingStoreException; import org.osgi.service.prefs.Preferences; import org.osgi.service.prefs.PreferencesService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.Date; import java.util.Dictionary; import java.util.Enumeration; import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import java.util.Properties; import java.util.TreeMap; import java.util.WeakHashMap; import javax.servlet.Servlet; import javax.servlet.http.HttpServletRequest; /** * The site dispatcher watches sites coming and going and registers them with * the weblounge dispatcher. */ public class SiteDispatcherServiceImpl implements SiteDispatcherService, SiteListener, SiteServiceListener, ManagedService { /** Logging facility */ private static final Logger logger = LoggerFactory.getLogger(SiteDispatcherServiceImpl.class); /** Default value for the <code>WEBAPP_CONTEXT_ROOT</code> property */ public static final String DEFAULT_WEBAPP_CONTEXT_ROOT = "/"; /** Default value for the <code>BUNDLE_ROOT_URI</code> property */ public static final String DEFAULT_BUNDLE_CONTEXT_ROOT_URI = "/weblounge-sites"; /** Service pid, used to look up the service configuration */ public static final String SERVICE_PID = "ch.entwine.weblounge.sitedispatcher"; /** Configuration key prefix for jsp precompilation */ public static final String OPT_PRECOMPILE = "precompile"; /** Configuration key prefix for jsp precompilation logging */ public static final String OPT_PRECOMPILE_LOGGING = "precompile.logerrors"; /** Configuration key prefix for jasper configuration values */ public static final String OPT_JASPER_PREFIX = "jasper."; /** Configuration option for jasper's scratch directory */ public static final String OPT_JASPER_SCRATCHDIR = "scratchdir"; /** Default value for jasper's <code>scratchDir</code> compiler context */ public static final String DEFAULT_JASPER_SCRATCH_DIR = "jasper"; /** Prefix for the precompilation date */ public static final String X_COMPILE_PREFIX = "X-COMPILE-"; /** Path to the scratch directory */ private String jasperScratchDir = null; /** The action request handler */ private ActionRequestHandler actionRequestHandler = null; /** The site manager */ private SiteManager siteManager = null; /** The default environment */ private Environment environment = Environment.Production; /** Init parameters for jetty */ private final TreeMap<String, String> jasperConfig = new TreeMap<String, String>(); /** Maps sites to site servlets */ private final Map<Site, SiteServlet> siteServlets = new HashMap<Site, SiteServlet>(); /** The site servlet registrations */ private Map<Site, ServiceRegistration> servletRegistrations = null; /** The security service */ private SecurityService securityService = null; /** The preferences service */ private PreferencesService preferencesService = null; /** The precompiler for java server pages */ private boolean precompile = true; /** Shutdown flag for running threads */ private boolean shutdown = false; /** List of precompilers */ private final WeakHashMap<Site, Precompiler> precompilers = new WeakHashMap<Site, Precompiler>(); /** True to log errors that appear during precompilation */ private boolean logCompileErrors = false; /** * Callback for OSGi's declarative services component activation. * * @param context * the component context * @throws IOException * if reading from the configuration admin service fails * @throws ConfigurationException * if service configuration fails */ void activate(ComponentContext context) throws IOException, ConfigurationException { BundleContext bundleContext = context.getBundleContext(); logger.info("Starting site dispatcher"); // Configure the jasper work directory where compiled java classes go String tmpDir = System.getProperty("java.io.tmpdir"); jasperScratchDir = PathUtils.concat(tmpDir, DEFAULT_JASPER_SCRATCH_DIR); jasperConfig.put(OPT_JASPER_SCRATCHDIR, jasperScratchDir); // Try to get hold of the service configuration ServiceReference configAdminRef = bundleContext.getServiceReference(ConfigurationAdmin.class.getName()); if (configAdminRef != null) { ConfigurationAdmin configAdmin = (ConfigurationAdmin) bundleContext.getService(configAdminRef); Dictionary<?, ?> config = configAdmin.getConfiguration(SERVICE_PID).getProperties(); if (config != null) { configure(config); } else { logger.debug("No customized configuration for site dispatcher found"); } } else { logger.debug("No configuration admin service found while looking for site dispatcher configuration"); } servletRegistrations = new HashMap<Site, ServiceRegistration>(); logger.debug("Site dispatcher activated"); // Register for changing sites siteManager.addSiteListener(this); // Process sites that have already been registered Thread t = new Thread(new Runnable() { @Override public void run() { for (Iterator<Site> si = siteManager.sites(); si.hasNext();) { addSite(si.next()); } } }); t.setDaemon(true); t.start(); } /** * 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 dispatcher"); // Don't listen to new sites anymore siteManager.removeSiteListener(this); // Tell everyone to finish their work shutdown = true; synchronized (servletRegistrations) { servletRegistrations.notifyAll(); } // Stop precompilers for (Precompiler compiler : precompilers.values()) { if (compiler.isRunning()) { compiler.stop(); } else if (preferencesService != null) { Preferences preferences = preferencesService.getSystemPreferences(); String date = WebloungeDateFormat.formatStatic(new Date()); preferences.put(compiler.getCompilerKey(), date); try { preferences.flush(); } catch (BackingStoreException e) { logger.warn("Failed to store precompiler results: {}", e.getMessage()); } } else if (preferencesService == null) { logger.warn("Unable to store precompiler results: preference service unavailable"); } } logger.info("Site dispatcher stopped"); } /** * {@inheritDoc} * * @see org.osgi.service.cm.ManagedService#updated(java.util.Dictionary) */ public void updated(Dictionary properties) throws ConfigurationException { if (properties == null) return; configure(properties); } /** * Configures this service using the given configuration properties. * * @param config * the service configuration * @throws ConfigurationException * if configuration fails */ private boolean configure(Dictionary<?, ?> config) throws ConfigurationException { logger.debug("Configuring the site registration service"); boolean configurationChanged = true; // Activate precompilation? String precompileSetting = StringUtils.trimToNull((String) config.get(OPT_PRECOMPILE)); precompile = precompileSetting == null || ConfigurationUtils.isTrue(precompileSetting); logger.debug("Jsp precompilation {}", precompile ? "activated" : "deactivated"); // Log compilation errors? String logPrecompileErrors = StringUtils.trimToNull((String) config.get(OPT_PRECOMPILE_LOGGING)); logCompileErrors = logPrecompileErrors != null && ConfigurationUtils.isTrue(logPrecompileErrors); logger.debug("Precompilation errors will {} logged", logCompileErrors ? "be" : "not be"); // Store the jasper configuration keys Enumeration<?> keys = config.keys(); while (keys.hasMoreElements()) { String key = (String) keys.nextElement(); if (key.startsWith(OPT_JASPER_PREFIX) && key.length() > OPT_JASPER_PREFIX.length()) { String value = (String) config.get(key); if (StringUtils.trimToNull(value) == null) continue; value = ConfigurationUtils.processTemplate(value); key = key.substring(OPT_JASPER_PREFIX.length()); boolean optionChanged = value.equalsIgnoreCase(jasperConfig.get(key)); configurationChanged |= !optionChanged; if (optionChanged) logger.debug("Jetty jsp parameter '{}' configured to '{}'", key, value); jasperConfig.put(key, value); // This is a work around for jasper's horrible implementation of the // compiler context configuration. Some keys are camel case, others are // lower case. jasperConfig.put(key.toLowerCase(), value); } } return configurationChanged; } /** * Callback from the OSGi environment which registers the request handler with * the site observer. * * @param handler * the action request handler */ void setActionRequestHandler(ActionRequestHandler handler) { logger.debug("Registering {}", handler); this.actionRequestHandler = handler; if (siteManager == null) return; for (Iterator<Site> si = siteManager.sites(); si.hasNext();) { for (Module module : si.next().getModules()) { for (Action action : module.getActions()) { actionRequestHandler.register(action); } } } } /** * Callback from the OSGi environment which removes the action request handler * from the site observer. * * @param handler * the action request handler */ void removeActionRequestHandler(ActionRequestHandler handler) { logger.debug("Unregistering {}", handler); if (siteManager == null) return; for (Iterator<Site> si = siteManager.sites(); si.hasNext();) { for (Module module : si.next().getModules()) { for (Action action : module.getActions()) { actionRequestHandler.unregister(action); } } } this.actionRequestHandler = null; } /** * {@inheritDoc} * * @see ch.entwine.weblounge.dispatcher.SiteDispatcherService#getSiteServlet(ch.entwine.weblounge.common.site.Site) */ public Servlet getSiteServlet(Site site) { return siteServlets.get(site); } /** * {@inheritDoc} * * @see ch.entwine.weblounge.dispatcher.SiteDispatcherService#findSiteByIdentifier(java.lang.String) */ public Site findSiteByIdentifier(String identifier) { return siteManager.findSiteByIdentifier(identifier); } /** * {@inheritDoc} * * @see ch.entwine.weblounge.dispatcher.SiteDispatcherService#findSiteByURL(java.lang.String) */ public Site findSiteByURL(URL siteURL) { return siteManager.findSiteByURL(siteURL); } /** * {@inheritDoc} * * @see ch.entwine.weblounge.dispatcher.SiteDispatcherService#findSiteByRequest(javax.servlet.http.HttpServletRequest) */ public Site findSiteByRequest(HttpServletRequest request) { if (request == null) throw new IllegalArgumentException("Request must not be null"); return findSiteByURL(UrlUtils.toURL(request, false, false)); } /** * Adds a new site. * * This method may be long-running and therefore is executed in its own * thread. * * @param site * the site */ private void addSite(final Site site) { Thread t = new Thread(new Runnable() { public void run() { Bundle siteBundle = siteManager.getSiteBundle(site); WebXml webXml = createWebXml(site, siteBundle); Properties initParameters = new Properties(); // Prepare the init parameters // initParameters.put("load-on-startup", Integer.toString(1)); initParameters.putAll(webXml.getContextParams()); initParameters.putAll(jasperConfig); // Create the site URI String contextRoot = webXml.getContextParam(DispatcherConfiguration.WEBAPP_CONTEXT_ROOT, DEFAULT_WEBAPP_CONTEXT_ROOT); String bundleURI = webXml.getContextParam(DispatcherConfiguration.BUNDLE_URI, site.getIdentifier()); String siteContextURI = webXml.getContextParam(DispatcherConfiguration.BUNDLE_CONTEXT_ROOT_URI, DEFAULT_BUNDLE_CONTEXT_ROOT_URI); String siteRoot = UrlUtils.concat(contextRoot, siteContextURI, bundleURI); // Prepare the Jasper work directory String scratchDirPath = PathUtils.concat(jasperConfig.get(OPT_JASPER_SCRATCHDIR), site.getIdentifier()); File scratchDir = new File(scratchDirPath); boolean jasperArtifactsExist = scratchDir.isDirectory() && scratchDir.list().length > 0; try { FileUtils.forceMkdir(scratchDir); logger.debug("Temporary jsp source files and classes go to {}", scratchDirPath); } catch (IOException e) { logger.warn("Unable to create jasper scratch directory at {}: {}", scratchDirPath, e.getMessage()); } try { // Create and register the site servlet SiteServlet siteServlet = new SiteServlet(site, siteBundle, environment); siteServlet.setSecurityService(securityService); Dictionary<String, String> servletRegistrationProperties = new Hashtable<String, String>(); servletRegistrationProperties.put(Site.class.getName().toLowerCase(), site.getIdentifier()); servletRegistrationProperties.put(SharedHttpContext.ALIAS, siteRoot); servletRegistrationProperties.put(SharedHttpContext.SERVLET_NAME, site.getIdentifier()); servletRegistrationProperties.put(SharedHttpContext.CONTEXT_ID, SharedHttpContext.WEBLOUNGE_CONTEXT_ID); servletRegistrationProperties.put(SharedHttpContext.INIT_PREFIX + OPT_JASPER_SCRATCHDIR, scratchDirPath); ServiceRegistration servletRegistration = siteBundle.getBundleContext() .registerService(Servlet.class.getName(), siteServlet, servletRegistrationProperties); servletRegistrations.put(site, servletRegistration); // We are using the Whiteboard pattern to register servlets. Wait for // the http service to pick up the servlet and initialize it synchronized (servletRegistrations) { boolean warnedOnce = false; while (!siteServlet.isInitialized()) { if (!warnedOnce) { logger.info("Waiting for site '{}' to be online", site.getIdentifier()); warnedOnce = true; } logger.debug("Waiting for http service to pick up {}", siteServlet); servletRegistrations.wait(500); if (shutdown) { logger.info("Giving up waiting for registration of site '{}'"); servletRegistrations.remove(site); return; } } } siteServlets.put(site, siteServlet); logger.info("Site '{}' is online and registered under site://{}", site, siteRoot); // Did we already miss the "siteStarted()" event? If so, we trigger it // for ourselves, so the modules are being started. site.addSiteListener(SiteDispatcherServiceImpl.this); if (site.isOnline()) { siteStarted(site); } // Start the precompiler if requested if (precompile) { String compilationKey = X_COMPILE_PREFIX + siteBundle.getBundleId(); Date compileDate = null; boolean needsCompilation = true; // Check if this site has been precompiled already if (preferencesService != null) { Preferences preferences = preferencesService.getSystemPreferences(); String compileDateString = preferences.get(compilationKey, null); if (compileDateString != null) { compileDate = WebloungeDateFormat.parseStatic(compileDateString); needsCompilation = false; logger.info("Site '{}' has already been precompiled on {}", site.getIdentifier(), compileDate); } } else { logger.info( "Precompilation status cannot be determined, consider deploying a preferences service implementation"); } // Does the scratch dir exist? if (!jasperArtifactsExist) { needsCompilation = true; logger.info("Precompiled artifacts for '{}' have been removed", site.getIdentifier()); } // Let's do the work anyways if (needsCompilation) { Precompiler precompiler = new Precompiler(compilationKey, siteServlet, environment, securityService, logCompileErrors); precompilers.put(site, precompiler); precompiler.precompile(); } } logger.debug("Site '{}' registered under site://{}", site, siteRoot); } catch (Throwable t) { logger.error("Error setting up site '{}' for http requests: {}", new Object[] { site, t.getMessage() }); logger.error(t.getMessage(), t); } } }); t.setDaemon(true); t.start(); } /** * Removes a site from the dispatcher. * * @param site * the site to remove */ private void removeSite(Site site) { // Remove site dispatcher servlet ServiceRegistration servletRegistration = servletRegistrations.remove(site); try { servletRegistration.unregister(); } catch (IllegalStateException e) { // Never mind, the service has been unregistered already } catch (Throwable t) { logger.error("Unregistering site '{}' failed: {}", site.getIdentifier(), t.getMessage()); } // We are no longer interested in site events site.removeSiteListener(this); // Stop the site's precompiler Precompiler compiler = precompilers.get(site); if (compiler != null) { if (compiler.isRunning()) { compiler.stop(); } else if (preferencesService != null) { Preferences preferences = preferencesService.getSystemPreferences(); String date = WebloungeDateFormat.formatStatic(new Date()); preferences.put(compiler.getCompilerKey(), date); try { preferences.flush(); } catch (BackingStoreException e) { logger.warn("Failed to store precompiler results: {}", e.getMessage()); } } else if (preferencesService == null) { logger.warn("Unable to store precompiler results: preference service unavailable"); } } siteServlets.remove(site); // TODO: unregister site dispatcher logger.debug("Site {} unregistered", site); } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.site.SiteListener#siteStarted(ch.entwine.weblounge.common.site.Site) */ public void siteStarted(Site site) { if (actionRequestHandler != null) { for (Module module : site.getModules()) { for (Action action : module.getActions()) { actionRequestHandler.register(action); } } } } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.site.SiteListener#siteStopped(ch.entwine.weblounge.common.site.Site) */ public void siteStopped(Site site) { if (actionRequestHandler != null) { for (Module module : site.getModules()) { for (Action action : module.getActions()) { actionRequestHandler.unregister(action); } } } } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.site.SiteListener#repositoryConnected(ch.entwine.weblounge.common.site.Site, * ch.entwine.weblounge.common.repository.ContentRepository) */ public void repositoryConnected(Site site, ContentRepository repository) { } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.site.SiteListener#repositoryDisconnected(ch.entwine.weblounge.common.site.Site, * ch.entwine.weblounge.common.repository.ContentRepository) */ public void repositoryDisconnected(Site site, ContentRepository repository) { } /** * {@inheritDoc} * * @see ch.entwine.weblounge.kernel.site.SiteServiceListener#siteAppeared(ch.entwine.weblounge.common.site.Site, * org.osgi.framework.ServiceReference) */ public void siteAppeared(Site site, ServiceReference reference) { addSite(site); } /** * {@inheritDoc} * * @see ch.entwine.weblounge.kernel.site.SiteServiceListener#siteDisappeared(ch.entwine.weblounge.common.site.Site) */ public void siteDisappeared(Site site) { removeSite(site); } /** * Returns the <code>web.xml</code> representation that is used to register * the site dispatcher servlets with the <code>HttpService</code>. * <p> * The method registers the following init parameters in the * <code>web.xml</code> with appropriate default values: * <ul> * <li>weblounge.http.WEBAPP_CONTEXT_ROOT</li> * <li>weblounge.http.BUNDLE_CONTEXT_ROOT</li> * <li>weblounge.http.BUNDLE_CONTEXT_ROOT_URI</li> * <li>weblounge.http.BUNDLE_NAME</li> * <li>weblounge.http.BUNDLE_ROOT</li> * <li>weblounge.http.BUNDLE_URI</li> * <li>weblounge.http.BUNDLE_ENTRY</li> * </ul> * <p> * <b>Note:</b> almost all of these properties can be overwritten using either * the system properties or the service properties. */ public WebXml createWebXml(Site site, Bundle siteBundle) { ServiceReference reference = null; ServiceReference[] references = siteBundle.getRegisteredServices(); for (ServiceReference ref : references) { if (site.equals(siteBundle.getBundleContext().getService(ref))) { reference = ref; break; } } WebXml webXml = new WebXml(); webXml.addContextParam(DispatcherConfiguration.BUNDLE_NAME, siteBundle.getSymbolicName()); webXml.addContextParam(DispatcherConfiguration.BUNDLE_ENTRY, Site.BUNDLE_PATH); // Webapp context root String webappRoot = null; if (reference != null && reference.getProperty(DispatcherConfiguration.WEBAPP_CONTEXT_ROOT) != null) webappRoot = (String) reference.getProperty(DispatcherConfiguration.WEBAPP_CONTEXT_ROOT); else if (System.getProperty(DispatcherConfiguration.WEBAPP_CONTEXT_ROOT) != null) webappRoot = System.getProperty(DispatcherConfiguration.WEBAPP_CONTEXT_ROOT); if (webappRoot == null) webappRoot = DEFAULT_WEBAPP_CONTEXT_ROOT; if (!webappRoot.startsWith("/")) webappRoot = "/" + webappRoot; webXml.addContextParam(DispatcherConfiguration.WEBAPP_CONTEXT_ROOT, webappRoot); // Bundle name webXml.addContextParam(DispatcherConfiguration.BUNDLE_NAME, siteBundle.getSymbolicName().toLowerCase()); // Bundle context root uri String sitesRoot = null; if (reference != null && reference.getProperty(DispatcherConfiguration.BUNDLE_CONTEXT_ROOT_URI) != null) sitesRoot = (String) reference.getProperty(DispatcherConfiguration.BUNDLE_CONTEXT_ROOT_URI); else if (System.getProperty(DispatcherConfiguration.BUNDLE_CONTEXT_ROOT_URI) != null) sitesRoot = System.getProperty(DispatcherConfiguration.BUNDLE_CONTEXT_ROOT_URI); if (sitesRoot == null) sitesRoot = DEFAULT_BUNDLE_CONTEXT_ROOT_URI; if (!sitesRoot.startsWith("/")) sitesRoot = "/" + sitesRoot; webXml.addContextParam(DispatcherConfiguration.BUNDLE_CONTEXT_ROOT_URI, sitesRoot); // Bundle context root sitesRoot = UrlUtils.concat(webappRoot, sitesRoot); webXml.addContextParam(DispatcherConfiguration.BUNDLE_CONTEXT_ROOT, sitesRoot); // Bundle uri webXml.addContextParam(DispatcherConfiguration.BUNDLE_URI, site.getIdentifier()); // Bundle root String bundleRoot = UrlUtils.concat(sitesRoot, site.getIdentifier()); webXml.addContextParam(DispatcherConfiguration.BUNDLE_ROOT, bundleRoot); // Bundle entry String bundleEntry = null; if (reference != null && reference.getProperty(DispatcherConfiguration.BUNDLE_ENTRY) != null) bundleEntry = (String) reference.getProperty(DispatcherConfiguration.BUNDLE_ENTRY); else if (System.getProperty(DispatcherConfiguration.BUNDLE_ENTRY) != null) bundleEntry = System.getProperty(DispatcherConfiguration.BUNDLE_ENTRY); if (bundleEntry == null) bundleEntry = Site.BUNDLE_PATH; if (!bundleEntry.startsWith("/")) bundleEntry = "/" + bundleEntry; webXml.addContextParam(DispatcherConfiguration.BUNDLE_ENTRY, bundleEntry); return webXml; } /** * OSGi callback that will set the site manager. * * @param siteManager * the site manager */ void setSiteManager(SiteManager siteManager) { this.siteManager = siteManager; } /** * OSGi callback that will unset the site manager. * * @param siteManager * the site manager */ void removeSiteManager(SiteManager siteManager) { this.siteManager = null; } /** * Sets the default environment; * * @param environment * the environment */ void setEnvironment(Environment environment) { this.environment = environment; for (SiteServlet servlet : siteServlets.values()) { servlet.setEnvironment(environment); } } /** * Sets the security service. * * @param securityService * the security service */ void setSecurityService(SecurityService securityService) { this.securityService = securityService; } /** * Sets the preferences service. * * @param preferencesService * the OSGi preferences service */ void setPreferencesService(PreferencesService preferencesService) { this.preferencesService = preferencesService; } }