/*
* 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.publisher;
import ch.entwine.weblounge.common.site.Environment;
import ch.entwine.weblounge.common.site.Site;
import ch.entwine.weblounge.common.url.UrlUtils;
import ch.entwine.weblounge.dispatcher.SharedHttpContext;
import ch.entwine.weblounge.kernel.site.SiteManager;
import ch.entwine.weblounge.kernel.site.SiteServiceListener;
import org.apache.commons.lang.StringUtils;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.Servlet;
import javax.ws.rs.Path;
/**
* Listens for JAX-RS annotated services and publishes them to the global URL
* space using a single shared HttpContext.
*/
public class EndpointPublishingService implements ManagedService, SiteServiceListener {
/** Logging facility */
static final Logger logger = LoggerFactory.getLogger(EndpointPublishingService.class);
/** Service pid, used to look up the service configuration */
public static final String SERVICE_PID = "ch.entwine.weblounge.restpublisher";
/** Configuration key prefix for rest publisher configuration */
public static final String OPT_PREFIX = "restpublisher";
/** Configuration key for the rest services path prefix */
public static final String OPT_PATH = OPT_PREFIX + ".path";
/** Configuration key used to configure the endpoint's alias */
public static final String DEFAULT_PATH = "/system/weblounge";
/** The context path option used to override the default context path */
public static final String OPT_CONTEXTPATH = "rest.path";
/** Context of this component */
protected ComponentContext componentContext = null;
/** The bundle context */
protected BundleContext bundleContext = null;
/** The JSR 311 service listener */
protected ServiceListener jsr311ServiceListener = null;
/** The mountpoint for REST services */
protected String defaultContextPathPrefix = DEFAULT_PATH;
/** Mapping of registered endpoints */
protected Map<String, ServiceRegistration> endpointRegistrations = null;
/** Mapping of registered servlets */
protected Map<String, JAXRSServlet> endpointServlets = null;
/** The site manager */
protected SiteManager sites = null;
/** The environment */
protected Environment environment = null;
/**
* Creates a new publishing service for JSR 311 annotated classes.
*/
public EndpointPublishingService() {
endpointRegistrations = new ConcurrentHashMap<String, ServiceRegistration>();
endpointServlets = new ConcurrentHashMap<String, JAXRSServlet>();
jsr311ServiceListener = new JSR311AnnotatedServiceListener();
}
/**
* Callback for OSGi's declarative services component activation.
*
* @param context
* the component context
* @throws Exception
* if component activation fails
*/
void activate(ComponentContext componentContext) throws Exception {
this.componentContext = componentContext;
this.bundleContext = componentContext.getBundleContext();
sites.addSiteListener(this);
logger.info("Starting rest publishing service");
// 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 rest publisher found");
}
} else {
logger.debug("No configuration admin service found while looking for rest publisher configuration");
}
// Make sure we are notified in case of new services
bundleContext.addServiceListener(jsr311ServiceListener);
// Register JAX-RS services that have already been loaded
for (Bundle bundle : bundleContext.getBundles()) {
// Skip bundles that are not active
if (Bundle.ACTIVE != bundle.getState()) {
logger.trace("Skipping bundle '{}' in state {} while looking for JAXRS endpoints", bundle, bundle.getState());
continue;
}
// Skip bundles that don't have any services registered
ServiceReference[] refs = bundle.getRegisteredServices();
if (refs == null)
continue;
// Explicitly register the JAXB service by crafting a manual ServiceEvent
for (ServiceReference ref : refs) {
try {
ServiceEvent event = new ServiceEvent(ServiceEvent.REGISTERED, ref);
jsr311ServiceListener.serviceChanged(event);
} catch (Throwable t) {
logger.error("Error registering JAXRS annotated service {} : {}", ref);
}
}
}
}
/**
* Callback for OSGi's declarative services component inactivation.
*
* @param context
* the component context
* @throws Exception
* if component inactivation fails
*/
void deactivate(ComponentContext componentContext) throws Exception {
if (jsr311ServiceListener != null) {
bundleContext.removeServiceListener(jsr311ServiceListener);
}
// Unregister the current jsr311 servlets
for (String path : endpointRegistrations.keySet()) {
unregisterEndpoint(path);
}
endpointRegistrations.clear();
// Stop listening to sites
sites.removeSiteListener(this);
}
/**
* {@inheritDoc}
*
* @see org.osgi.service.cm.ManagedService#updated(java.util.Dictionary)
*/
public void updated(Dictionary properties) throws ConfigurationException {
if (properties == null)
return;
boolean changed = configure(properties);
if (!changed)
return;
// Unregister all current endpoints
for (String path : endpointRegistrations.keySet()) {
unregisterEndpoint(path);
}
// Register any existing JAX-RS services that have already been loaded
for (Bundle bundle : componentContext.getBundleContext().getBundles()) {
ServiceReference[] refs = bundle.getRegisteredServices();
if (refs == null)
continue;
for (ServiceReference ref : refs) {
ServiceEvent event = new ServiceEvent(ServiceEvent.REGISTERED, ref);
jsr311ServiceListener.serviceChanged(event);
}
}
}
/**
* Returns a list of all service endpoints along with their paths.
*
* @return the service paths
*/
public Map<String, ServiceRegistration> getEndpoints() {
Map<String, ServiceRegistration> services = new HashMap<String, ServiceRegistration>(endpointRegistrations.size());
services.putAll(endpointRegistrations);
return services;
}
/**
* Configures this service using the given configuration properties.
*
* @param config
* the service configuration
* @throws ConfigurationException
* if configuration fails
*/
private synchronized boolean configure(Dictionary<?, ?> config)
throws ConfigurationException {
boolean changed = false;
// context path
String updatedRestMountpoint = StringUtils.trim((String) config.get(OPT_PATH));
if (updatedRestMountpoint != null) {
if (!updatedRestMountpoint.startsWith("/"))
throw new IllegalArgumentException("Context path (" + OPT_PATH + ") must start with a '/'");
changed |= !updatedRestMountpoint.equals(defaultContextPathPrefix);
defaultContextPathPrefix = updatedRestMountpoint;
}
return changed;
}
/**
* Creates a REST endpoint for the JAX-RS annotated service.
*
* @param service
* The jsr311 annotated service
* @param contextPath
* the http context
* @param bundle
* the registering bundle
* @param the
* endpoint's path
*/
protected void registerEndpoint(Object service, String contextPath,
String endpointPath, Bundle bundle) {
try {
JAXRSServlet servlet = new JAXRSServlet(endpointPath, service, bundle);
servlet.setSite(sites.findSiteByBundle(bundle));
servlet.setEnvironment(environment);
Dictionary<String, String> initParams = new Hashtable<String, String>();
initParams.put(SharedHttpContext.ALIAS, contextPath);
initParams.put(SharedHttpContext.SERVLET_NAME, service.toString());
initParams.put(SharedHttpContext.CONTEXT_ID, SharedHttpContext.WEBLOUNGE_CONTEXT_ID);
ServiceRegistration reg = bundleContext.registerService(Servlet.class.getName(), servlet, initParams);
endpointRegistrations.put(contextPath, reg);
endpointServlets.put(contextPath, servlet);
logger.debug("Registering {} at {}", service, contextPath);
} catch (Throwable t) {
logger.error("Error registering rest service at " + contextPath, t);
return;
}
}
/**
* Removes an endpoint from the OSGi http service.
*
* @param contextPath
* The endpoint's url space
*/
protected void unregisterEndpoint(String contextPath) {
logger.debug("Unregistering rest endpoint {}", contextPath);
// Remove the servlet from the http service
try {
ServiceRegistration reg = endpointRegistrations.get(contextPath);
reg.unregister();
} catch (IllegalStateException e) {
// Never mind, the service has been unregistered already
} catch (Throwable t) {
logger.error("Unregistering endpoint at '{}' failed: {}", contextPath, t.getMessage());
}
// Unregister the servlet
endpointRegistrations.remove(contextPath);
endpointServlets.remove(contextPath);
}
/**
* Implementation of a service listener which looks for services featuring
* JSR311 <code>@Path</code> annotations.
*/
class JSR311AnnotatedServiceListener implements ServiceListener {
/**
* {@inheritDoc}
*
* @see org.osgi.framework.ServiceListener#serviceChanged(org.osgi.framework.ServiceEvent)
*/
public void serviceChanged(ServiceEvent event) {
ServiceReference ref = event.getServiceReference();
Object service = null;
try {
service = bundleContext.getService(ref);
} catch (IllegalStateException e) {
// This is happening when the system is going down and the referenced
// bundle context has already become invalid
logger.debug("Endpoint publishing service is already down");
return;
}
// Sometimes, there is a service reference without a service
if (service == null)
return;
// Is this a JSR 311 annotated class?
Path pathAnnotation = service.getClass().getAnnotation(Path.class);
if (pathAnnotation == null)
return;
// Is there a context path?
Object contextPathProperty = ref.getProperty(OPT_CONTEXTPATH);
if (contextPathProperty == null)
return;
// Adjust relative context paths
String contextPath = contextPathProperty.toString();
if (!contextPath.startsWith("/")) {
contextPath = UrlUtils.concat(defaultContextPathPrefix, contextPath);
}
// Find the registering bundle
Bundle bundle = ref.getBundle();
// Process the event
switch (event.getType()) {
case ServiceEvent.REGISTERED:
logger.debug("Registering JAX-RS service {} at {}", service, contextPath);
// Make sure there is no clash in context paths
ServiceRegistration existingRef = endpointRegistrations.get(contextPath);
if (existingRef != null) {
Object s = bundleContext.getService(existingRef.getReference());
logger.error("Endpoint {} cannot be registered since the context path {} has already been claimed", new Object[] {
service,
s,
contextPath });
return;
}
registerEndpoint(service, contextPath, pathAnnotation.value(), bundle);
break;
case ServiceEvent.MODIFIED:
logger.debug("JAX-RS service {} modified", service);
break;
case ServiceEvent.UNREGISTERING:
logger.debug("Unregistering JAX-RS service {} from {}", service, contextPath);
unregisterEndpoint(contextPath);
break;
default:
// We don't care about these cases
break;
}
}
}
/**
* OSGi callback to set the sites manager.
*
* @param sites
* the sites manager
*/
void setSites(SiteManager sites) {
this.sites = sites;
}
/**
* OSGi callback to set the environment.
*
* @param environment
* the environment
*/
void setEnvironment(Environment environment) {
this.environment = environment;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.kernel.site.SiteServiceListener#siteAppeared(ch.entwine.weblounge.common.site.Site,
* org.osgi.framework.ServiceReference)
*/
@Override
public void siteAppeared(Site site, ServiceReference reference) {
Bundle siteBundle = reference.getBundle();
if (siteBundle == null)
return;
for (Map.Entry<String, JAXRSServlet> r : endpointServlets.entrySet()) {
JAXRSServlet servlet = r.getValue();
if (siteBundle.equals(servlet.getBundle())) {
servlet.setSite(site);
}
}
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.kernel.site.SiteServiceListener#siteDisappeared(ch.entwine.weblounge.common.site.Site)
*/
@Override
public void siteDisappeared(Site site) {
// Nothing to do, the associated endpoints will soon be gone, too
}
}