// ========================================================================
// Copyright (c) 2009 Intalio, Inc.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
package org.eclipse.jetty.osgi.boot.internal.webapp;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jetty.osgi.boot.JettyBootstrapActivator;
import org.eclipse.jetty.osgi.boot.OSGiServerConstants;
import org.eclipse.jetty.osgi.boot.OSGiWebappConstants;
import org.eclipse.jetty.osgi.boot.internal.serverfactory.DefaultJettyAtJettyHomeHelper;
import org.eclipse.jetty.osgi.boot.internal.serverfactory.IManagedJettyServerRegistry;
import org.eclipse.jetty.osgi.boot.internal.serverfactory.ServerInstanceWrapper;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.Scanner;
import org.eclipse.jetty.webapp.WebAppContext;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
/**
* When a {@link ContextHandler} service is activated we look into it and if the
* corresponding webapp is actually not configured then we go and register it.
* <p>
* The idea is to always go through this class when we deploy a new webapp on
* jetty.
* </p>
* <p>
* We are exposing each web-application as an OSGi service. This lets us update
* the webapps and stop/start them directly at the OSGi layer. It also give us
* many ways to declare those services: Declarative Services for example. <br/>
* It is a bit different from the way the HttpService works where we would have
* a WebappService and we woud register a webapp onto it. <br/>
* It does not go against RFC-66 nor does it prevent us from supporting the
* WebappContainer semantics.
* </p>
*/
public class JettyContextHandlerServiceTracker implements ServiceListener
{
private static Logger __logger = Log.getLogger(WebBundleDeployerHelper.class.getName());
/** New style: ability to manage multiple jetty instances */
private final IManagedJettyServerRegistry _registry;
/** The context-handler to deactivate indexed by context handler */
private Map<ServiceReference, ContextHandler> _indexByServiceReference = new HashMap<ServiceReference, ContextHandler>();
/**
* The index is the bundle-symbolic-name/path/to/context/file when there is
* such thing
*/
private Map<String, ServiceReference> _indexByContextFile = new HashMap<String, ServiceReference>();
/** in charge of detecting changes in the osgi contexts home folder. */
private Scanner _scanner;
/**
* @param registry
*/
public JettyContextHandlerServiceTracker(IManagedJettyServerRegistry registry) throws Exception
{
_registry = registry;
}
public void stop() throws Exception
{
if (_scanner != null)
{
_scanner.stop();
}
// the class that created the server is also in charge of stopping it.
// nothing to stop in the WebappRegistrationHelper
}
/**
* @param contextHome Parent folder where the context files can override the
* context files defined in the web bundles: equivalent to the
* contexts folder in a traditional jetty installation. when
* null, just do nothing.
*/
protected void setupContextHomeScanner(File contextHome) throws IOException
{
if (contextHome == null) { return; }
final String osgiContextHomeFolderCanonicalPath = contextHome.getCanonicalPath();
_scanner = new Scanner();
_scanner.setRecursive(true);
_scanner.setReportExistingFilesOnStartup(false);
_scanner.addListener(new Scanner.DiscreteListener()
{
public void fileAdded(String filename) throws Exception
{
// adding a file does not create a new app,
// it just reloads it with the new custom file.
// well, if the file does not define a context handler,
// then in fact it does remove it.
reloadJettyContextHandler(filename, osgiContextHomeFolderCanonicalPath);
}
public void fileChanged(String filename) throws Exception
{
reloadJettyContextHandler(filename, osgiContextHomeFolderCanonicalPath);
}
public void fileRemoved(String filename) throws Exception
{
// removing a file does not remove the app:
// it just goes back to the default embedded in the bundle.
// well, if there was no default then it does remove it.
reloadJettyContextHandler(filename, osgiContextHomeFolderCanonicalPath);
}
});
}
/**
* Receives notification that a service has had a lifecycle change.
*
* @param ev The <code>ServiceEvent</code> object.
*/
public void serviceChanged(ServiceEvent ev)
{
ServiceReference sr = ev.getServiceReference();
switch (ev.getType())
{
case ServiceEvent.MODIFIED:
case ServiceEvent.UNREGISTERING:
{
ContextHandler ctxtHandler = unregisterInIndex(ev.getServiceReference());
if (ctxtHandler != null && !ctxtHandler.isStopped())
{
try
{
getWebBundleDeployerHelp(sr).unregister(ctxtHandler);
}
catch (Exception e)
{
__logger.warn(e);
}
}
}
if (ev.getType() == ServiceEvent.UNREGISTERING)
{
break;
}
else
{
// modified, meaning: we reload it. now that we stopped it;
// we can register it.
}
case ServiceEvent.REGISTERED:
{
Bundle contributor = sr.getBundle();
BundleContext context = FrameworkUtil.getBundle(JettyBootstrapActivator.class).getBundleContext();
ContextHandler contextHandler = (ContextHandler) context.getService(sr);
if (contextHandler.getServer() != null)
{
// is configured elsewhere.
return;
}
String contextFilePath = (String) sr.getProperty(OSGiWebappConstants.SERVICE_PROP_CONTEXT_FILE_PATH);
if (contextHandler instanceof WebAppContext && contextFilePath == null)
// it could be a web-application that will in fact be configured
// via a context file.
// that case is identified by the fact that the contextFilePath
// is not null
// in that case we must use the register context methods.
{
WebAppContext webapp = (WebAppContext) contextHandler;
String contextPath = (String) sr.getProperty(OSGiWebappConstants.SERVICE_PROP_CONTEXT_PATH);
if (contextPath == null)
{
contextPath = webapp.getContextPath();
}
String webXmlPath = (String) sr.getProperty(OSGiWebappConstants.SERVICE_PROP_WEB_XML_PATH);
if (webXmlPath == null)
{
webXmlPath = webapp.getDescriptor();
}
String defaultWebXmlPath = (String) sr.getProperty(OSGiWebappConstants.SERVICE_PROP_DEFAULT_WEB_XML_PATH);
if (defaultWebXmlPath == null)
{
String jettyHome = System.getProperty(DefaultJettyAtJettyHomeHelper.SYS_PROP_JETTY_HOME);
if (jettyHome != null)
{
File etc = new File(jettyHome, "etc");
if (etc.exists() && etc.isDirectory())
{
File webDefault = new File(etc, "webdefault.xml");
if (webDefault.exists())
defaultWebXmlPath = webDefault.getAbsolutePath();
else
defaultWebXmlPath = webapp.getDefaultsDescriptor();
}
else
defaultWebXmlPath = webapp.getDefaultsDescriptor();
}
}
String war = (String) sr.getProperty("war");
try
{
IWebBundleDeployerHelper deployerHelper = getWebBundleDeployerHelp(sr);
if (deployerHelper == null)
{
}
else
{
WebAppContext handler = deployerHelper.registerWebapplication(contributor,
war,
contextPath,
(String) sr.getProperty(OSGiWebappConstants.SERVICE_PROP_EXTRA_CLASSPATH),
(String) sr.getProperty(OSGiWebappConstants.SERVICE_PROP_BUNDLE_INSTALL_LOCATION_OVERRIDE),
(String) sr.getProperty(OSGiWebappConstants.SERVICE_PROP_REQUIRE_TLD_BUNDLE),
webXmlPath, defaultWebXmlPath, webapp);
if (handler != null)
{
registerInIndex(handler, sr);
}
}
}
catch (Throwable e)
{
__logger.warn(e);
}
}
else
{
// consider this just an empty skeleton:
if (contextFilePath == null) { throw new IllegalArgumentException("the property contextFilePath is required"); }
try
{
IWebBundleDeployerHelper deployerHelper = getWebBundleDeployerHelp(sr);
if (deployerHelper == null)
{
// more warnings?
}
else
{
if (Boolean.TRUE.toString().equals(sr.getProperty(IWebBundleDeployerHelper.INTERNAL_SERVICE_PROP_UNKNOWN_CONTEXT_HANDLER_TYPE)))
{
contextHandler = null;
}
ContextHandler handler = deployerHelper.registerContext(contributor,
contextFilePath,
(String) sr.getProperty(OSGiWebappConstants.SERVICE_PROP_EXTRA_CLASSPATH),
(String) sr.getProperty(OSGiWebappConstants.SERVICE_PROP_BUNDLE_INSTALL_LOCATION_OVERRIDE),
(String) sr.getProperty(OSGiWebappConstants.SERVICE_PROP_REQUIRE_TLD_BUNDLE),
contextHandler);
if (handler != null)
{
registerInIndex(handler, sr);
}
}
}
catch (Throwable e)
{
__logger.warn(e);
}
}
}
break;
}
}
private void registerInIndex(ContextHandler handler, ServiceReference sr)
{
_indexByServiceReference.put(sr, handler);
String key = getSymbolicNameAndContextFileKey(sr);
if (key != null)
{
_indexByContextFile.put(key, sr);
}
}
/**
* Returns the ContextHandler to stop.
*
* @param reg
* @return the ContextHandler to stop.
*/
private ContextHandler unregisterInIndex(ServiceReference sr)
{
ContextHandler handler = _indexByServiceReference.remove(sr);
String key = getSymbolicNameAndContextFileKey(sr);
if (key != null)
{
_indexByContextFile.remove(key);
}
if (handler == null)
{
// a warning?
return null;
}
return handler;
}
/**
* @param sr
* @return The key for a context file within the osgi contexts home folder.
*/
private String getSymbolicNameAndContextFileKey(ServiceReference sr)
{
String contextFilePath = (String) sr.getProperty(OSGiWebappConstants.SERVICE_PROP_CONTEXT_FILE_PATH);
if (contextFilePath != null) { return sr.getBundle().getSymbolicName() + "/" + contextFilePath; }
return null;
}
/**
* Called by the scanner when one of the context files is changed.
*
* @param contextFileFully
*/
public void reloadJettyContextHandler(String canonicalNameOfFileChanged, String osgiContextHomeFolderCanonicalPath)
{
String key = getNormalizedRelativePath(canonicalNameOfFileChanged, osgiContextHomeFolderCanonicalPath);
if (key == null) { return; }
ServiceReference sr = _indexByContextFile.get(key);
if (sr == null)
{
// nothing to do?
return;
}
serviceChanged(new ServiceEvent(ServiceEvent.MODIFIED, sr));
}
/**
* @param canFilename
* @return
*/
private String getNormalizedRelativePath(String canFilename, String osgiContextHomeFolderCanonicalPath)
{
if (!canFilename.startsWith(osgiContextHomeFolderCanonicalPath))
{
// why are we here: this does not look like a child of the osgi
// contexts home.
// warning?
return null;
}
return canFilename.substring(osgiContextHomeFolderCanonicalPath.length()).replace('\\', '/');
}
/**
* @return The server on which this webapp is meant to be deployed
*/
private ServerInstanceWrapper getServerInstanceWrapper(String managedServerName)
{
if (_registry == null) { return null; }
if (managedServerName == null)
{
managedServerName = OSGiServerConstants.MANAGED_JETTY_SERVER_DEFAULT_NAME;
}
ServerInstanceWrapper wrapper = _registry.getServerInstanceWrapper(managedServerName);
// System.err.println("Returning " + managedServerName + " = " +
// wrapper);
return wrapper;
}
private IWebBundleDeployerHelper getWebBundleDeployerHelp(ServiceReference sr)
{
if (_registry == null) { return null; }
String managedServerName = (String) sr.getProperty(OSGiServerConstants.MANAGED_JETTY_SERVER_NAME);
ServerInstanceWrapper wrapper = getServerInstanceWrapper(managedServerName);
return wrapper != null ? wrapper.getWebBundleDeployerHelp() : null;
}
}