/* ==================================================================
* DynamicServiceTracker.java - Mar 24, 2012 8:32:05 PM
*
* Copyright 2007-2012 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.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.PropertyAccessor;
import org.springframework.beans.PropertyAccessorFactory;
/**
* 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>
* The configurable properties of this class are:
* </p>
*
* <dl class="class-properties">
* <dt>bundleContext</dt>
* <dd>The OSGi {@link BundleContext} to use.</dd>
*
* <dt>serviceClassName</dt>
* <dd>The OSGi service class name to look for, or <em>null</em> for all
* services.</dd>
*
* <dt>serviceFilter</dt>
* <dd>An OSGi service filter to match services to, or <em>null</em> for no
* filter.</dd>
*
* <dt>propertyFilters</dt>
* <dd>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.</dd>
*
* <dt>ignoreEmptyPropertyFilterValues</dt>
* <dd>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>.</dd>
*
* <dt>fallbackService</dt>
* <dd>If no matching service is available and this property is configured, then
* this service will be returned as a fallback.</dd>
* </dl>
*
* @param <T>
* the tracked service type
* @author matt
* @version 1.1
*/
public class DynamicServiceTracker<T> implements OptionalService<T>, OptionalServiceCollection<T>,
FilterableService {
private BundleContext bundleContext;
private String serviceClassName;
private String serviceFilter;
private Map<String, Object> propertyFilters;
private T fallbackService;
private boolean ignoreEmptyPropertyFilterValues = true;
private final Logger log = LoggerFactory.getLogger(getClass());
/**
* Get the tracked service, or <em>null</em> if no service currently
* available.
*
* @return the service, or <em>null</em> if not available
*/
@Override
@SuppressWarnings("unchecked")
public T service() {
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;
}
}
}
if ( fallbackService != null ) {
log.debug("Using fallback {} service {}, no matching service found matching properties {}",
serviceClassName, fallbackService.getClass().getName(), propertyFilters);
}
return fallbackService;
}
@SuppressWarnings("unchecked")
@Override
public Iterable<T> services() {
ServiceReference<?>[] refs;
try {
refs = bundleContext.getServiceReferences(serviceClassName, serviceFilter);
} catch ( InvalidSyntaxException e ) {
log.error("Error in service filter {}: {}", serviceFilter, e);
return Collections.emptyList();
}
log.debug("Found {} possible services of type {} matching filter {}", new Object[] {
(refs == null ? 0 : refs.length), serviceClassName, serviceFilter });
if ( refs == null ) {
return Collections.emptyList();
}
List<T> results = new ArrayList<T>(refs.length);
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 });
results.add((T) service);
}
}
if ( results.size() == 0 && fallbackService != null ) {
log.debug("Using fallback {} service {}, no matching service found matching properties {}",
serviceClassName, fallbackService.getClass().getName(), propertyFilters);
results.add(fallbackService);
}
return results;
}
private boolean serviceMatchesFilters(Object service) {
if ( service == null ) {
return false;
}
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;
}
public BundleContext getBundleContext() {
return bundleContext;
}
public void setBundleContext(BundleContext bundleContext) {
this.bundleContext = bundleContext;
}
public String getServiceClassName() {
return serviceClassName;
}
public void setServiceClassName(String serviceClassName) {
this.serviceClassName = serviceClassName;
}
public String getServiceFilter() {
return serviceFilter;
}
public void setServiceFilter(String serviceFilter) {
this.serviceFilter = serviceFilter;
}
@Override
public Map<String, Object> getPropertyFilters() {
return propertyFilters;
}
public void setPropertyFilters(Map<String, Object> propertyFilters) {
this.propertyFilters = propertyFilters;
}
public T getFallbackService() {
return fallbackService;
}
public void setFallbackService(T fallbackService) {
this.fallbackService = fallbackService;
}
public boolean isIgnoreEmptyPropertyFilterValues() {
return ignoreEmptyPropertyFilterValues;
}
public void setIgnoreEmptyPropertyFilterValues(boolean ignoreEmptyPropertyFilterValues) {
this.ignoreEmptyPropertyFilterValues = ignoreEmptyPropertyFilterValues;
}
}