blob: 188f88cfc012ee706727b2f361a482846cb4ae9d [file] [log] [blame]
/**
*
* Copyright 2005 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.gbean.reflect;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import javax.management.ObjectName;
import net.sf.cglib.reflect.FastClass;
import net.sf.cglib.reflect.FastMethod;
import org.apache.geronimo.gbean.DynamicGBean;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.gbean.kernel.Kernel;
import org.gbean.kernel.LifecycleAdapter;
import org.gbean.kernel.OperationSignature;
import org.gbean.kernel.ServiceNotFoundException;
import org.gbean.kernel.NoSuchAttributeException;
import org.gbean.kernel.NoSuchOperationException;
import org.gbean.kernel.runtime.ServiceState;
/**
* @version $Revision$ $Date$
*/
public class ServiceInvoker {
/**
* Our log stream
*/
private static final Log log = LogFactory.getLog(ServiceInvoker.class);
/**
* The kernel in which the service is loaded
*/
private final Kernel kernel;
/**
* The name of the service in the kernel
*/
private final ObjectName name;
/**
* The listener that is notified when the service goes offline.
*/
private final ServiceInvokerLifecycleListener lifecycleListener;
/**
* The actual target service
*/
private Object serviceInstance;
/**
* Is the service currently running?
*/
private boolean serviceRunning;
/**
* Property invokers
*/
private PropertyInvokerImpl[] properties;
/**
* Property name to index number
*/
private final Map propertyIndex = new HashMap();
/**
* Operations lookup table
*/
private OperationInvokerImpl[] operations;
/**
* Operations supported by the service by OperationSignature
*/
private final Map operationIndex = new HashMap();
/**
* Has the ServiceInvoker itself been started?
*/
private boolean serviceInvokerStarted = false;
public ServiceInvoker(Kernel kernel, ObjectName name) {
this.kernel = kernel;
this.name = name;
lifecycleListener = new ServiceInvokerLifecycleListener();
}
public synchronized void start() throws ServiceNotFoundException {
if (serviceRunning) {
return;
}
kernel.addLifecycleListener(lifecycleListener, name);
serviceInvokerStarted = true;
assureRunning();
}
public synchronized void stop() {
kernel.removeLifecycleListener(lifecycleListener);
properties = null;
propertyIndex.clear();
operations = null;
operationIndex.clear();
serviceInstance = null;
serviceRunning = false;
serviceInvokerStarted = false;
}
public synchronized ObjectName getServiceName() {
return name;
}
public synchronized Class getServiceType() {
assureRunning();
return serviceInstance.getClass();
}
public synchronized List getPropertyIndex() {
assureRunning();
return Collections.unmodifiableList(Arrays.asList(properties));
}
public synchronized List getOperationIndex() {
assureRunning();
return Collections.unmodifiableList(Arrays.asList(operations));
}
/**
* Gets the attribute value using the attribute index. This is the most efficient way to get
* an attribute as it avoids a HashMap lookup.
*
* @param index the index of the attribute
* @return the attribute value
* @throws Exception if a target instance throws and exception
* @throws IndexOutOfBoundsException if the index is invalid
*/
public Object getAttribute(int index) throws Exception {
// copy target into local variables from within a synchronized block to gaurentee a consistent read
Object instance;
PropertyInvokerImpl property;
synchronized (this) {
assureRunning();
instance = serviceInstance;
property = properties[index];
}
if (!property.isReadable()) {
throw new IllegalArgumentException("Property " + property.getPropertyName() + " is not readable");
}
try {
Object value = property.getterFastMethod.invoke(instance, null);
return value;
} catch (InvocationTargetException e) {
throw unwrap(e);
}
}
/**
* Gets an attribute's value by name. This get style is less efficient becuse the attribute must
* first be looked up in a HashMap.
*
* @param name the name of the property to retrieve
* @return the property value
* @throws Exception if a problem occurs while getting the value
* @throws NoSuchAttributeException if the attribute name is not found in the map
*/
public Object getAttribute(String name) throws NoSuchAttributeException, Exception {
// copy target into local variables from within a synchronized block to gaurentee a consistent read
Object instance;
PropertyInvokerImpl property = null;
synchronized (this) {
assureRunning();
instance = serviceInstance;
Integer index = (Integer) propertyIndex.get(name);
if (index != null) {
property = properties[index.intValue()];
}
}
if (property != null) {
if (!property.isReadable()) {
throw new IllegalArgumentException("Property " + property.getPropertyName() + " is not readable");
}
try {
Object value = property.getterFastMethod.invoke(instance, null);
return value;
} catch (InvocationTargetException e) {
throw unwrap(e);
}
} else if (instance instanceof DynamicGBean) {
Object value = ((DynamicGBean) instance).getAttribute(name);
return value;
}
throw new NoSuchAttributeException("Unknown property '" + name + "' in service " + name);
}
/**
* Sets the attribute value using the attribute index. This is the most efficient way to set
* an attribute as it avoids a HashMap lookup.
*
* @param index the index of the attribute
* @param value the new value of attribute value
* @throws Exception if a target instance throws and exception
* @throws IndexOutOfBoundsException if the index is invalid
*/
public void setAttribute(int index, Object value) throws Exception, IndexOutOfBoundsException {
// copy target into local variables from within a synchronized block to gaurentee a consistent read
Object instance;
PropertyInvokerImpl property;
synchronized (this) {
assureRunning();
instance = serviceInstance;
property = properties[index];
}
if (!property.isWritable()) {
throw new IllegalArgumentException("Property " + property.getPropertyName() + " is not writable");
}
try {
property.setterFastMethod.invoke(instance, new Object[] {value});
} catch (InvocationTargetException e) {
throw unwrap(e);
}
}
/**
* Sets an attribute's value by name. This set style is less efficient becuse the attribute must
* first be looked up in a HashMap.
*
* @param attributeName the name of the attribute to retrieve
* @param attributeValue the new attribute value
* @throws Exception if a target instance throws and exception
* @throws NoSuchAttributeException if the attribute name is not found in the map
*/
public void setAttribute(String attributeName, Object attributeValue) throws Exception, NoSuchAttributeException {
// copy target into local variables from within a synchronized block to gaurentee a consistent read
Object instance;
PropertyInvokerImpl property = null;
synchronized (this) {
assureRunning();
instance = serviceInstance;
Integer index = (Integer) propertyIndex.get(attributeName);
if (index != null) {
property = properties[index.intValue()];
}
}
if (property != null) {
if (!property.isWritable()) {
throw new IllegalArgumentException("Property " + property.getPropertyName() + " is not writable");
}
try {
property.setterFastMethod.invoke(instance, new Object[] {attributeValue});
} catch (InvocationTargetException e) {
throw unwrap(e);
}
} else if (instance instanceof DynamicGBean) {
((DynamicGBean) instance).setAttribute(attributeName, attributeValue);
} else {
throw new NoSuchAttributeException("Unknown attribute '" + attributeName + "' in service " + name);
}
}
/**
* Invokes an opreation using the operation index. This is the most efficient way to invoke
* an operation as it avoids a HashMap lookup.
*
* @param index the index of the attribute
* @param arguments the arguments to the operation
* @return the result of the operation
* @throws Exception if a target instance throws and exception
* @throws IndexOutOfBoundsException if the index is invalid
* @throws IllegalStateException if the service has been destroyed
*/
public Object invoke(int index, Object[] arguments) throws Exception {
// copy target into local variables from within a synchronized block to gaurentee a consistent read
Object instance;
OperationInvokerImpl operation;
synchronized (this) {
assureRunning();
instance = serviceInstance;
operation = operations[index];
}
try {
Object value = operation.fastMethod.invoke(instance, arguments);
return value;
} catch (InvocationTargetException e) {
throw unwrap(e);
}
}
/**
* Invokes an operation on the service by method signature. This style if invocation is
* inefficient, because the target method must be looked up in a hashmap using a freshly constructed
* OperationSignature object.
*
* @param operationName the name of the operation to invoke
* @param arguments arguments to the operation
* @param types types of the operation arguemtns
* @return the result of the operation
* @throws Exception if a target instance throws and exception
* @throws NoSuchOperationException if the operation signature is not found in the map
* @throws IllegalStateException if the service has been destroyed
*/
public Object invoke(String operationName, Object[] arguments, String[] types) throws Exception, NoSuchOperationException {
// copy target into local variables from within a synchronized block to gaurentee a consistent read
Object instance;
OperationInvokerImpl operation;
synchronized (this) {
assureRunning();
instance = serviceInstance;
OperationSignature signature = new OperationSignature(operationName, types);
Integer index = (Integer) operationIndex.get(signature);
if (index == null) {
throw new NoSuchOperationException("Unknown operation " + signature);
}
operation = operations[index.intValue()];
}
try {
Object value = operation.fastMethod.invoke(instance, arguments);
return value;
} catch (InvocationTargetException e) {
throw unwrap(e);
}
}
public synchronized void assureRunning() {
if (!serviceRunning) {
try {
updateState();
if (!serviceRunning) {
throw new IllegalStateException("Service must be in the running or stopping state: name=" + name + ", state=" + ServiceState.fromIndex(kernel.getServiceState(name)));
}
} catch (ServiceNotFoundException e) {
throw new IllegalStateException("Service is not loaded: " + name);
}
}
}
private synchronized void updateState() throws ServiceNotFoundException {
if (!serviceInvokerStarted) {
throw new IllegalStateException("Service invoker has not been started: name=" + name);
}
try {
serviceRunning = false;
// get the current state
int serviceState = kernel.getServiceState(name);
// we must be in a running state
boolean running = serviceState == ServiceState.RUNNING_INDEX || serviceState == ServiceState.STOPPING_INDEX;
if (running) {
if (serviceInstance == null) {
serviceInstance = kernel.getService(name);
// if we don't have a service instance something is wrong
if (serviceInstance == null) {
throw new IllegalStateException("Could not get service instance: name=" + name);
}
createIndex(serviceInstance.getClass());
}
serviceRunning = true;
}
} finally {
if (!serviceRunning) {
kernel.removeLifecycleListener(lifecycleListener);
serviceInvokerStarted = false;
properties = null;
propertyIndex.clear();
operations = null;
operationIndex.clear();
serviceInstance = null;
}
}
}
private class ServiceInvokerLifecycleListener extends LifecycleAdapter {
public void stopped(ObjectName objectName) {
try {
updateState();
} catch (Exception e) {
log.info("Unable to update service invoker for service " + name, e);
}
}
public void unloaded(ObjectName objectName) {
try {
updateState();
} catch (Exception e) {
log.info("Unable to update service invoker for service " + name, e);
}
}
}
private void createIndex(Class type) {
// attributes
Method[] methods = type.getMethods();
// map the getters
Map getterMap = new HashMap(methods.length);
for (int i = 0; i < methods.length; i++) {
Method method = methods[i];
String methodName = method.getName();
if (Modifier.isPublic(method.getModifiers()) &&
!Modifier.isStatic(method.getModifiers()) &&
method.getParameterTypes().length == 0 &&
method.getReturnType() != Void.TYPE) {
if (methodName.length() > 3 && methodName.startsWith("get") && !methodName.equals("getClass")) {
String attributeName = fixAttributeName(methodName.substring(3));
// if this attribute also has an "is" accessor make sure the return type is boolean
Method isAccessor = (Method) getterMap.get(attributeName);
if (isAccessor != null && method.getReturnType() != Boolean.TYPE) {
throw new IllegalArgumentException("Getter has both a get<name> and is<name> accessor but the getter return type is not boolean:" +
" class=" + type.getName() +
", attribute=" + attributeName +
", getAccessorType=" + method.getReturnType().getName());
}
// add it
getterMap.put(attributeName, method);
} else if (methodName.length() > 2 && methodName.startsWith("is")) {
String attributeName = fixAttributeName(methodName.substring(2));
// an is accessor must return boolean
if (method.getReturnType() != Boolean.TYPE) {
throw new IllegalArgumentException("An is<name> accessor must return boolean:" +
" class=" + type.getName() +
", attribute=" + attributeName +
", attributeType=" + method.getReturnType().getName());
}
// if this attribute also has a "get" accessor make sure the getter return type is boolean
Method getAccessor = (Method) getterMap.get(attributeName);
if (getAccessor != null && method.getReturnType() != Boolean.TYPE) {
throw new IllegalArgumentException("Getter has both a get<name> and is<name> accessor but the getter return type is not boolean:" +
" class=" + type.getName() +
", attribute=" + attributeName +
", getAccessorType=" + getAccessor.getReturnType().getName());
}
// add it
getterMap.put(attributeName, method);
}
}
}
// map the setters
Map setterMap = new HashMap(methods.length);
for (int i = 0; i < methods.length; i++) {
Method method = methods[i];
String methodName = method.getName();
if (Modifier.isPublic(method.getModifiers()) &&
!Modifier.isStatic(method.getModifiers()) &&
method.getParameterTypes().length == 1 &&
method.getReturnType() == Void.TYPE &&
methodName.length() > 3 &&
methodName.startsWith("set")) {
String attributeName = fixAttributeName(methodName.substring(3));
if (setterMap.containsKey(attributeName)) {
// this bean attributte has multiple setters with different types, so treat only as operations
setterMap.put(attributeName, null);
} else {
setterMap.put(attributeName, method);
}
// the getter and setter types must match
Method getterMethod = (Method) getterMap.get(attributeName);
if (getterMethod != null && !getterMethod.getReturnType().equals(method.getParameterTypes()[0])) {
throw new IllegalArgumentException("Getter and setter types do not match:" +
" class=" + type.getName() +
", attribute=" + attributeName +
", getAccessorType=" + getterMethod.getReturnType().getName() +
", setAccessorType=" + method.getParameterTypes()[0].getName());
}
}
}
// remove any setter with a null method (these setters have multiple methods with different types)
for (Iterator iterator = setterMap.entrySet().iterator(); iterator.hasNext();) {
Map.Entry entry = (Map.Entry) iterator.next();
Method setter = (Method) entry.getValue();
if (setter == null) {
iterator.remove();
}
}
TreeSet propertyNames = new TreeSet();
propertyNames.addAll(getterMap.keySet());
propertyNames.addAll(setterMap.keySet());
List propertyList = new ArrayList(getterMap.size());
for (Iterator iterator = propertyNames.iterator(); iterator.hasNext();) {
String propertyName = (String) iterator.next();
Method getter = (Method) getterMap.get(propertyName);
Method setter = (Method) setterMap.get(propertyName);
propertyIndex.put(propertyName, new Integer(propertyList.size()));
propertyList.add(new PropertyInvokerImpl(propertyName, getter, setter));
}
properties = (PropertyInvokerImpl[]) propertyList.toArray(new PropertyInvokerImpl[propertyList.size()]);
// operations
List operationList = new ArrayList(methods.length);
for (int i = 0; i < methods.length; i++) {
Method method = methods[i];
if (Modifier.isPublic(method.getModifiers()) && !Modifier.isStatic(method.getModifiers())) {
operationIndex.put(new OperationSignature(method), new Integer(operationList.size()));
operationList.add(new OperationInvokerImpl(method));
}
}
operations = (OperationInvokerImpl[]) operationList.toArray(new OperationInvokerImpl[operationList.size()]);
}
private static Exception unwrap(InvocationTargetException e) throws Exception {
Throwable cause = e.getTargetException();
if (cause instanceof Exception) {
return (Exception) cause;
} else if (cause instanceof Error) {
throw (Error) cause;
}
return e;
}
private static String fixAttributeName(String attributeName) {
if (Character.isUpperCase(attributeName.charAt(0))) {
return Character.toLowerCase(attributeName.charAt(0)) + attributeName.substring(1);
}
return attributeName;
}
private final class OperationInvokerImpl implements OperationInvoker {
private final OperationSignature signature;
private final FastMethod fastMethod;
public OperationInvokerImpl(Method method) {
assert method != null: "method is null";
this.signature = new OperationSignature(method);
this.fastMethod = FastClass.create(method.getDeclaringClass()).getMethod(method);
}
public OperationSignature getSignature() {
return signature;
}
public Object invoke(Object[] arguments) throws Exception {
Object instance;
synchronized (ServiceInvoker.this) {
assureRunning();
instance = serviceInstance;
}
try {
Object value = fastMethod.invoke(instance, arguments);
return value;
} catch (InvocationTargetException e) {
throw unwrap(e);
}
}
public int hashCode() {
return signature.hashCode();
}
public boolean equals(Object obj) {
if (!(obj instanceof OperationInvokerImpl)) {
return false;
}
OperationInvokerImpl operationInvoker = (OperationInvokerImpl)obj;
return signature.equals(operationInvoker.signature);
}
public String toString() {
return "[MethodInvoker: " + getSignature().toString() + "]";
}
}
public class PropertyInvokerImpl implements PropertyInvoker {
private final String propertyName;
private final Class type;
private final OperationSignature getterSignature;
private final FastMethod getterFastMethod;
private final OperationSignature setterSignature;
private final FastMethod setterFastMethod;
public PropertyInvokerImpl(String propertyName, Method getter, Method setter) {
assert propertyName != null: "propertyName is null";
assert getter != null || setter != null: "getter and setter are null";
this.propertyName = propertyName;
if (getter != null) {
type = getter.getReturnType();
} else {
type = setter.getParameterTypes()[0];
}
if (getter != null) {
getterSignature = new OperationSignature(getter);
getterFastMethod = FastClass.create(getter.getDeclaringClass()).getMethod(getter);
} else {
getterSignature = null;
getterFastMethod = null;
}
if (setter != null) {
setterSignature = new OperationSignature(setter);
setterFastMethod = FastClass.create(setter.getDeclaringClass()).getMethod(setter);
} else {
setterSignature = null;
setterFastMethod = null;
}
}
public String getPropertyName() {
return propertyName;
}
public Class getType() {
return type;
}
public boolean isReadable() {
return getterFastMethod != null;
}
public OperationSignature getGetterSignature() {
return getterSignature;
}
public Object invokeGetter() throws Exception {
// copy target into local variables from within a synchronized block to gaurentee a consistent read
Object instance;
synchronized (ServiceInvoker.this) {
assureRunning();
instance = serviceInstance;
}
if (!isReadable()) {
throw new IllegalArgumentException("Property " + propertyName + " is not readable");
}
try {
Object value = getterFastMethod.invoke(instance, null);
return value;
} catch (InvocationTargetException e) {
throw unwrap(e);
}
}
public boolean isWritable() {
return setterFastMethod != null;
}
public OperationSignature getSetterSignature() {
return setterSignature;
}
public void invokeSetter(Object value) throws Exception {
// copy target into local variables from within a synchronized block to gaurentee a consistent read
Object instance;
synchronized (ServiceInvoker.this) {
assureRunning();
instance = serviceInstance;
}
if (!isWritable()) {
throw new IllegalArgumentException("Property " + propertyName + " is not writable");
}
try {
setterFastMethod.invoke(instance, new Object[] {value});
} catch (InvocationTargetException e) {
throw unwrap(e);
}
}
public int hashCode() {
return propertyName.hashCode();
}
public boolean equals(Object obj) {
if (!(obj instanceof PropertyInvokerImpl)) {
return false;
}
PropertyInvokerImpl fastPropertyInvoker = (PropertyInvokerImpl)obj;
return propertyName.equals(fastPropertyInvoker.propertyName);
}
public String toString() {
return "[PropertySetter: name=" + propertyName + ", getter=" + getterSignature + ", setter=" + setterSignature + "]";
}
}
}