/*
* Copyright 2002-2006 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Created on 23-Jan-2006 by Adrian Colyer
*/
package org.springframework.osgi.service;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.osgi.context.BundleContextAware;
import org.springframework.util.StringUtils;
/**
* Factory bean for OSGi services. Returns a proxy implementing the service
* interface. This allows Spring to manage service lifecycle events
* (such as the bundle providing the service being stopped and restarted)
* and transparently rebind to a new service instance if one is available.
*
* @author Adrian Colyer
* @since 2.0
*/
public class OsgiServiceProxyFactoryBean implements FactoryBean,
InitializingBean, DisposableBean, BundleContextAware, ApplicationContextAware {
public static final long DEFAULT_MILLIS_BETWEEN_RETRIES = 1000;
public static final int DEFAULT_MAX_RETRIES = 3;
/**
* Logger, available to subclasses.
*/
protected final Log logger = LogFactory.getLog(getClass());
private BundleContext bundleContext;
private ServiceReference serviceReference;
private boolean retryOnUnregisteredService = true;
private int maxRetries = DEFAULT_MAX_RETRIES;
private long millisBetweenRetries = DEFAULT_MILLIS_BETWEEN_RETRIES;
// not required to be an interface, but usually should be...
private Class serviceType;
// filter used to narrow service matches, may be null
private String filter;
// if looking for a bean published as a service, this is the name we're after
private String beanName;
// reference to our app context (we need the classloader for proxying...)
private ApplicationContext applicationContext;
/* (non-Javadoc)
* @see org.springframework.beans.factory.FactoryBean#getObject()
*/
public Object getObject() throws Exception {
// try to find the service
String lookupFilter = getFilterStringForServiceLookup();
this.serviceReference = OsgiServiceUtils.getService(this.bundleContext, getServiceType(), lookupFilter);
Object target = this.bundleContext.getService(this.serviceReference);
return getServiceProxyFor(target,lookupFilter);
}
/* (non-Javadoc)
* @see org.springframework.beans.factory.FactoryBean#getObjectType()
*/
public Class getObjectType() {
return getServiceType();
}
/* (non-Javadoc)
* @see org.springframework.beans.factory.FactoryBean#isSingleton()
*/
public boolean isSingleton() {
return true;
}
/* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() throws IllegalArgumentException {
if (this.bundleContext == null) {
throw new IllegalArgumentException("Required bundleContext property was not set");
}
if (getServiceType() == null) {
throw new IllegalArgumentException("Required serviceType property was not set");
}
if (getFilter() != null) {
// this call forces parsing of the filter string to generate an exception right
// now if it is not well-formed
try {
FrameworkUtil.createFilter(getFilterStringForServiceLookup());
}
catch (InvalidSyntaxException ex) {
throw new IllegalArgumentException(
"Filter string '" + getFilter() +
"' set on OsgiServiceProxyFactoryBean has invalid syntax: " +
ex.getMessage(),ex);
}
}
if (this.applicationContext == null) {
throw new IllegalArgumentException("Required applicationContext property was not set");
}
if (! (this.applicationContext instanceof DefaultResourceLoader)) {
throw new IllegalArgumentException(
"ApplicationContext does not provide access to classloader, " +
"provided type was : '" + this.applicationContext.getClass().getName() +
"' which does not extend DefaultResourceLoader");
}
}
/**
* @return Returns the serviceType.
*/
public Class getServiceType() {
return this.serviceType;
}
/**
* The type that the OSGi service was registered with
*/
public void setServiceType(Class serviceType) {
this.serviceType = serviceType;
}
/**
* @return Returns the filter.
*/
public String getFilter() {
return this.filter;
}
/**
* An OSGi filter used to narrow service matches. If you
* just want to find a spring bean published as a service by
* the OsgiServiceExporter, use the beanName property instead.
*/
public void setFilter(String filter) {
this.filter = filter;
}
/**
* @return Returns the beanName.
*/
public String getBeanName() {
return this.beanName;
}
/**
* To find a bean published as a service by the OsgiServiceExporter,
* simply set this property. You may specify additional filtering
* criteria if needed (using the filter property) but this is not
* required.
*/
public void setBeanName(String beanName) {
this.beanName = beanName;
}
/* (non-Javadoc)
* @see org.springframework.osgi.context.BundleContextAware#setBundleContext(org.osgi.framework.BundleContext)
*/
public void setBundleContext(BundleContext context) {
this.bundleContext = context;
}
/* (non-Javadoc)
* @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext)
*/
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* If the target OSGi service is unregistered, should we attempt to rebind
* to a replacement? (For example, if the bundle providing the service is
* stopped and then subsequently started again).
*
* By default retry *will* be attempted.
*
* Changing this property after initialization is complete has no effect.
*
* @param retryOnUnregisteredService The retryOnUnregisteredService to set.
*/
public void setRetryOnUnregisteredService(boolean retryOnUnregisteredService) {
this.retryOnUnregisteredService = retryOnUnregisteredService;
}
/**
* How many times should we attempt to rebind to a target service if the
* service we are currently using is unregistered. Default is 3 times.
*
* Changing this property after initialization is complete has no effect.
*
* @param maxRetries The maxRetries to set.
*/
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
/**
* How long should we wait between failed attempts at rebinding to a service
* that has been unregistered.
*
* Changing this property after initialization is complete has no effect.
*
* @param millisBetweenRetries The millisBetweenRetries to set.
*/
public void setMillisBetweenRetries(long millisBetweenRetries) {
this.millisBetweenRetries = millisBetweenRetries;
}
// this is as nasty as dynamic sql generation.
// build an osgi filter string to find the service we are
// looking for.
private String getFilterStringForServiceLookup() {
StringBuffer sb = new StringBuffer();
boolean andFilterWithBeanName = ((getFilter() != null) && (getBeanName() != null));
if (andFilterWithBeanName) {
sb.append("(&");
}
if (getFilter() != null) {
sb.append(getFilter());
}
if (getBeanName() != null) {
sb.append("(");
sb.append(BeanNameServicePropertiesResolver.BEAN_NAME_PROPERTY_KEY);
sb.append("=");
sb.append(getBeanName());
sb.append(")");
}
if (andFilterWithBeanName) {
sb.append(")");
}
String filter = sb.toString();
if (StringUtils.hasText(filter)) {
return filter;
}
else {
return null;
}
}
/* (non-Javadoc)
* @see org.springframework.beans.factory.DisposableBean#destroy()
*/
public void destroy() throws Exception {
if (this.serviceReference != null) {
this.bundleContext.ungetService(this.serviceReference);
}
}
/**
* We proxy the actual service so that we can listen to service events
* and rebind transparently if the service goes down and comes back up
* for example
*/
private Object getServiceProxyFor(Object target, String lookupFilter) {
ProxyFactory pf = new ProxyFactory();
if (getServiceType().isInterface()) {
pf.setInterfaces(new Class[] {getServiceType()});
}
HotSwappableTargetSource targetSource = new HotSwappableTargetSource(target);
pf.setTargetSource(targetSource);
OsgiServiceInterceptor interceptor = new OsgiServiceInterceptor(this.bundleContext,this.serviceReference,targetSource,getServiceType(),lookupFilter);
interceptor.setMaxRetries(this.retryOnUnregisteredService ? this.maxRetries : 0);
interceptor.setRetryIntervalMillis(this.millisBetweenRetries);
pf.addAdvice(interceptor);
ClassLoader classLoader = ((DefaultResourceLoader)this.applicationContext).getClassLoader();
return pf.getProxy(classLoader);
}
}