/* ==================================================================
* DynamicServiceProxy.java - 8/06/2015 2:55:41 pm
*
* Copyright 2007-2015 SolarNetwork.net Dev Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU 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
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
* ==================================================================
*/
package net.solarnetwork.util;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.wiring.BundleWiring;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.PropertyAccessor;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.beans.factory.FactoryBean;
/**
* Utility for dynamically obtaining an OSGi service based on comparing bean
* properties of a filtered subset of available services for matching values.
*
* <p>
* An example scenario for this class would be in the case of a factory service
* that publishes differently configured service instances. This class can be
* used to pick a single service published by the factory, based on properties
* of that instance. For example, a factory that publishes a serial port service
* might expose the serial port identifier as a bean property, which could be
* used to match on the desired port.
* </p>
*
* <p>
* This class is similar in purpose to the {@link DynamicServiceTracker} class,
* except that it implements {@link FactoryBean} and returns a {@link Proxy}
* that implements the configured {@code serviceClass} directly. Thus you can
* configure this as a Spring bean (or Gemini Blueprint bean) and inject it
* directly without needing the {@link OptionalService} API. Any method defined
* in the {@code serviceClass} interface can be called on the proxy returned by
* {@link #getObject()}, which will dynamically resolve an OSGi service matching
* the filters configured on <em>this</em> class and then invoke the same method
* on the resolved service.
* </p>
*
* <p>
* The exposed proxy will also implement the {@link FilterableService}
* interface.
* </p>
*
* @param <T>
* the tracked service type
* @author matt
* @version 1.0
*/
public class DynamicServiceProxy<T> implements InvocationHandler, FactoryBean<T>, FilterableService {
private BundleContext bundleContext;
private Class<? extends T> serviceClass;
private String serviceFilter;
private Map<String, Object> propertyFilters;
private boolean ignoreEmptyPropertyFilterValues = true;
private final Logger log = LoggerFactory.getLogger(getClass());
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Method ownMethod = this.getClass().getMethod(method.getName(), method.getParameterTypes());
return ownMethod.invoke(this, args);
} catch ( NoSuchMethodException e ) {
// call on the service!
T delegate = getServiceInstance();
if ( delegate == null ) {
throw new DynamicServiceUnavailableException("No " + serviceClass.getName()
+ " service available matching service filter " + serviceFilter
+ ", property filters " + propertyFilters);
}
Method delegateMethod = delegate.getClass().getMethod(method.getName(),
method.getParameterTypes());
try {
return delegateMethod.invoke(delegate, args);
} catch ( InvocationTargetException proxyException ) {
log.debug("Exception calling proxy method " + method.getName());
if ( proxyException.getCause() != null ) {
throw proxyException.getCause();
}
throw proxyException;
}
}
}
@SuppressWarnings("unchecked")
@Override
public T getObject() throws Exception {
BundleWiring wiring = bundleContext.getBundle().adapt(BundleWiring.class);
ClassLoader cl = wiring.getClassLoader();
if ( cl == null ) {
cl = serviceClass.getClassLoader();
}
return (T) Proxy.newProxyInstance(cl, new Class<?>[] { serviceClass, FilterableService.class },
this);
}
@Override
public Class<?> getObjectType() {
return null;
}
@Override
public boolean isSingleton() {
return true;
}
@SuppressWarnings("unchecked")
private T getServiceInstance() {
final String serviceClassName = serviceClass.getName();
ServiceReference<?>[] refs;
try {
refs = bundleContext.getServiceReferences(serviceClassName, serviceFilter);
} catch ( InvalidSyntaxException e ) {
log.error("Error in service filter {}: {}", serviceFilter, e);
return null;
}
log.debug("Found {} possible services of type {} matching filter {}", new Object[] {
(refs == null ? 0 : refs.length), serviceClassName, serviceFilter });
if ( refs != null ) {
for ( ServiceReference<?> ref : refs ) {
Object service = bundleContext.getService(ref);
final boolean match = serviceMatchesFilters(service);
if ( match ) {
log.debug("Found {} service matching properties {}: {}", new Object[] {
serviceClassName, propertyFilters, service });
return (T) service;
}
}
}
return null;
}
private boolean serviceMatchesFilters(Object service) {
if ( service == null ) {
return false;
}
final String serviceClassName = serviceClass.getName();
if ( propertyFilters == null || propertyFilters.size() < 1 ) {
log.debug("No property filter configured, {} service matches", serviceClassName);
return true;
}
log.trace("Examining service {} for property match {}", service, propertyFilters);
PropertyAccessor accessor = PropertyAccessorFactory.forBeanPropertyAccess(service);
for ( Map.Entry<String, Object> me : propertyFilters.entrySet() ) {
if ( accessor.isReadableProperty(me.getKey()) ) {
Object requiredValue = me.getValue();
if ( ignoreEmptyPropertyFilterValues
&& (requiredValue == null || ((requiredValue instanceof String) && ((String) requiredValue)
.length() == 0)) ) {
// ignore empty filter values, so this is a matching property... skip to the next filter
continue;
}
Object serviceValue = accessor.getPropertyValue(me.getKey());
if ( requiredValue == null ) {
if ( serviceValue == null ) {
continue;
}
return false;
}
if ( serviceValue instanceof Collection<?> ) {
// for collections, we test for containment
Collection<?> collection = (Collection<?>) serviceValue;
if ( !collection.contains(requiredValue) ) {
return false;
}
} else if ( !requiredValue.equals(serviceValue) ) {
return false;
}
} else {
return false;
}
}
return true;
}
@Override
public void setPropertyFilter(String key, Object value) {
Map<String, Object> filters = propertyFilters;
if ( filters == null ) {
filters = new LinkedHashMap<String, Object>(8);
propertyFilters = filters;
}
filters.put(key, value);
}
@Override
public Object removePropertyFilter(String key) {
Object result = null;
Map<String, Object> filters = propertyFilters;
if ( filters != null ) {
result = filters.remove(key);
}
return result;
}
/**
* Get the OSGi bundle context.
*
* @return The configured bundle context.
*/
public BundleContext getBundleContext() {
return bundleContext;
}
/**
* Set the OSGi {@link BundleContext} to use.
*
* @param bundleContext
* The bundle context for resolving services with.
*/
public void setBundleContext(BundleContext bundleContext) {
this.bundleContext = bundleContext;
}
/**
* Get the OSGi service interface to proxy.
*
* @return The interface to proxy.
*/
public Class<? extends T> getServiceClass() {
return serviceClass;
}
/**
* Set the OSGi service interface to proxy.
*
* @param serviceClass
* The interface to proxy.
*/
public void setServiceClass(Class<? extends T> serviceClass) {
this.serviceClass = serviceClass;
}
/**
* Get the OSGi service filter to use.
*
* @return The service filter, or <em>null</em> if none.
* @see #setServiceFilter(String)
*/
public String getServiceFilter() {
return serviceFilter;
}
/**
* Set an OSGi service filter to restrict services to, or <em>null</em> for
* no filter.
*
* @param serviceFilter
* The service filter to use.
*/
public void setServiceFilter(String serviceFilter) {
this.serviceFilter = serviceFilter;
}
@Override
public Map<String, Object> getPropertyFilters() {
return propertyFilters;
}
/**
* Set a map of bean property names and associated values to match against
* all found OSGi services. The first service to match will be returned in
* {@link #service()}. This can be <em>null</em>, in which case the first
* service found matching {@code serviceClassName} and {@code serviceFilter}
* will be returned.
*
* @param propertyFilters
* The property filter map to set.
*/
public void setPropertyFilters(Map<String, Object> propertyFilters) {
this.propertyFilters = propertyFilters;
}
/**
* Get the flag to ignore empty property filter values.
*
* @return The flag value.
* @see #setIgnoreEmptyPropertyFilterValues(boolean)
*/
public boolean isIgnoreEmptyPropertyFilterValues() {
return ignoreEmptyPropertyFilterValues;
}
/**
* Set the flag to ignore empty property filter values or not. If
* <em>true</em>, then ignore property filter values that are <em>null</em>
* or, if strings, have no length, for purposes of filtering services. If
* <em>false</em> then the property filters must match even if empty, that
* is a <em>null</em> filter value will only match services whose
* corresponding property is also <em>null</em>. Defaults to <em>true</em>.
*
* @param ignoreEmptyPropertyFilterValues
* The flag to set.
*/
public void setIgnoreEmptyPropertyFilterValues(boolean ignoreEmptyPropertyFilterValues) {
this.ignoreEmptyPropertyFilterValues = ignoreEmptyPropertyFilterValues;
}
}