/*
* Copyright 2008 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.
*/
package org.rioproject.impl.associations;
import org.rioproject.associations.Association;
import org.rioproject.associations.AssociationDescriptor;
import org.rioproject.associations.AssociationListener;
import org.rioproject.associations.AssociationProxy;
import org.rioproject.impl.associations.strategy.FailOver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* The AssociationInjector is an AssociationListener implementation that provides
* support for setter-based dependency injection by calling setters on a target
* object method when it receives association events.
*
* For example, we have a bean called Foo and it requires JavaSpace. In order to
* have the JavaSpace proxy set, a setter method is required :
*
* <pre>
* public class Foo {
* JavaSpace myJavaSpace;
*
* ...
* public void setMyJavaSpace(JavaSpace space) {
* myJavaSpace = space;
* }
* ...
* }
* </pre>
*
* Finally, in the OperationalString, you'll need to create an Association with a
* Property attribute which matches the setter.
* <pre>
* associations {
* association type:"uses", name="JavaSpace", property="myJavaSpace"
* }
* </pre>
*
* @author Dennis Reedy
*/
public class AssociationInjector<T> implements AssociationListener<T> {
private Object target;
/**
* The ClassLoader which will be used to provide the caller/client
* with a properly annotated proxy for associated services
*/
private ClassLoader callerCL;
/**
* Map of Associations and injection methods
*/
private final Map<Association, String> injectedMap = new HashMap<Association, String>();
/**
* Map of Associations to their generated proxy
*/
private final Map<Association, AssociationProxy<T>> proxyMap = new HashMap<Association, AssociationProxy<T>>();
private String targetPropertyName;
private static final Logger logger = LoggerFactory.getLogger(AssociationInjector.class);
/**
* Create an AssociationInjector
*
* @param target The object that will have dependencies injected
*/
public AssociationInjector(Object target) {
this.target = target;
}
/**
* Set the object that will have dependencies injected
*
* @param target The object that will have dependencies injected
*/
public void setBackend(Object target) {
this.target = target;
}
/**
* Set The ClassLoader which will be used to load proxies
*
* @param callerCL The classloader for the target
*/
public void setCallerClassLoader(ClassLoader callerCL) {
this.callerCL = callerCL;
}
private synchronized ClassLoader getCallerClassLoader() {
if(callerCL==null)
callerCL = Thread.currentThread().getContextClassLoader();
return callerCL;
}
/**
* If any AssociationProxy instances were created, make sure they are
* terminated
*/
public void terminate() {
logger.trace("Terminating injector, proxyMap size={}", proxyMap.size());
for(Map.Entry<Association, AssociationProxy<T>> entry : proxyMap.entrySet()) {
AssociationProxy<T> aProxy = entry.getValue();
logger.trace("Terminating association proxy {}", aProxy.toString());
aProxy.terminate();
}
this.target = null;
this.callerCL = null;
this.injectedMap.clear();
this.proxyMap.clear();
}
/**
* For each generated association proxy, get the corresponding
* AssociationDescriptor and how many successful invocations were made to
* all services in the association.
*
* @return A Map whose keys are AssociationDescriptor, and values are the
* number of successful invocations made to all services in the association.
* If there are no generated association proxy instances, a Map with no
* entries is returned. A new Map is created each time this method is called.
*/
public Map<AssociationDescriptor, Long> getInvocationMap() {
Map<AssociationDescriptor, Long> map = new HashMap<AssociationDescriptor, Long>();
for(Map.Entry<Association, AssociationProxy<T>> entry : proxyMap.entrySet()) {
AssociationProxy<T> aProxy = entry.getValue();
map.put(aProxy.getAssociation().getAssociationDescriptor(), aProxy.getInvocationCount());
}
return map;
}
void setTargetPropertyName(String targetPropertyName) {
this.targetPropertyName = targetPropertyName;
}
private String getTargetPropertyName(Association association) {
if(targetPropertyName==null)
return association.getAssociationDescriptor().getPropertyName();
return targetPropertyName;
}
@SuppressWarnings("unchecked")
private void inject(Association association, T service) {
String propertyName = getTargetPropertyName(association);
if(propertyName==null) {
logger.trace("Association [{}], does not have a declared propertyName, injection aborted",
association.toString());
return;
}
Method method = getInjectionMethod(propertyName);
if (method != null) {
synchronized(injectedMap) {
if(injectedMap.containsKey(association)) {
AssociationProxy<T> associationProxy = proxyMap.get(association);
if(associationProxy!=null)
associationProxy.discovered(association, service);
logger.debug("Association [{}] has already been injected to [{}]",
association.getName(), method.toString());
return;
} else {
logger.debug("Association [{}] not found in map, create new AssociationProxy", association.getName());
}
String proxyClass = association.getAssociationDescriptor().getProxyClass();
/*
* Check for null proxyFactoryClass. If null,
* AssociationProxySupport is default
*/
proxyClass = (proxyClass==null? AssociationProxySupport.class.getName() : proxyClass);
String strategyClass = association.getAssociationDescriptor().getServiceSelectionStrategy();
/*
* Check for null strategyClass. If null, FailOver is default
*/
strategyClass = (strategyClass==null? FailOver.class.getName() : strategyClass);
try {
AssociationProxy associationProxy =
(AssociationProxy)AssociationProxyFactory.createProxy(proxyClass,
strategyClass,
association,
getCallerClassLoader());
logger.debug("Association [{}], is DISCOVERED, inject dependency", association.getName());
method.invoke(target, getInjectionArg(method, association, (T)associationProxy));
if (logger.isDebugEnabled()) {
String targetClass=target==null?"null":target.getClass().getName();
logger.debug("Association [{}], is INJECTED using method [{}] on target class [{}]",
association.getName(), method.toString(), targetClass);
}
proxyMap.put(association, associationProxy);
//injected.add(method.toString());
injectedMap.put(association, method.toString());
} catch (Throwable t) {
String svc = "eager injection";
if(service!=null)
svc = service.getClass().getName();
String nested = (t.getCause() == null ? "" :"\nCAUSE:");
StringBuilder s = new StringBuilder();
s.append("Injecting Association [").append(association.getName()).append("], Service [");
s.append(svc).append("]").append(nested);
logger.warn(s.toString(), (t.getCause() == null ? t : t.getCause()));
}
}
} else {
logger.warn("Association [{}], with declared propertyName [{}] not found on target object "+
"[{}], Check method name matches setXXX and that the setter has a single " +
"parameter", association.getName(), propertyName, target.getClass().getName());
}
}
private Method getInjectionMethod(String propertyName) {
Method method=null;
StringBuilder sb = new StringBuilder();
sb.append("\nDeclared property name: [").append(propertyName).append("]\n");
for (Method m : target.getClass().getMethods()) {
String mName = m.getName();
if(mName.startsWith("set")) {
mName = mName.substring(3);
mName = Character.toLowerCase(mName.charAt(0))+mName.substring(1);
String mods = Modifier.toString(m.getModifiers());
sb.append("property=").append(mName).append(" modifiers: [").append(mods).append("], ").append(m).append("\n");
if(mName.equals(propertyName) && mods.contains("public")) {
method = m;
break;
}
}
}
if(method!=null) {
sb.append("Selected method: ").append(method);
} else {
sb.append("No method selected, unable to match property name: ").append(propertyName);
}
logger.trace(sb.toString());
return(method);
}
private Object getInjectionArg(Method method,
Association association,
T service) {
Object arg;
Type[] types = method.getParameterTypes();
if(types.length!=1)
throw new IllegalArgumentException("method ["+method.getName()+"], " +
"expected [1] type, " +
"found ["+types.length+"] " +
"types ["+ Arrays.toString(types)+"]");
if(Iterable.class.isAssignableFrom((Class<?>)types[0])) {
arg = association;
} else {
/* Assume that the arg is the injection of the service */
arg = service;
}
return(arg);
}
public void injectEmpty(Association<T> association) {
inject(association, null);
}
/**
* @see AssociationListener#discovered(Association, Object)
*/
public void discovered(Association<T> association, T service) {
inject(association, service);
}
/*
* If AssociationProxy is not null, delegate to AssociationProxy
*/
public void changed(Association<T> association, T service) {
AssociationProxy<T> aProxy = proxyMap.get(association);
if(aProxy!=null)
aProxy.changed(association, service);
}
/*
* If AssociationProxy is not null, delegate to AssociationProxy
*/
public void broken(Association<T> association, T service) {
AssociationProxy<T> aProxy = proxyMap.get(association);
if(aProxy!=null)
aProxy.broken(association, service);
}
}