/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.brooklyn.core.effector;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.apache.brooklyn.api.effector.Effector;
import org.apache.brooklyn.api.effector.ParameterType;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.core.annotation.EffectorParam;
import org.apache.brooklyn.core.entity.AbstractEntity;
import org.apache.brooklyn.core.mgmt.internal.EffectorUtils;
import org.apache.brooklyn.util.core.flags.TypeCoercions;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.groovy.GroovyJavaMethods;
import org.codehaus.groovy.runtime.MethodClosure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Lists;
/** concrete class for providing an Effector implementation that gets its information from annotations on a method;
* see Effector*Test for usage example.
* <p>
* note that the method must be on an interface in order for it to be remoted, with the current implementation.
* see comments in {@link #call(Entity, Map)} for more details.
*/
public class MethodEffector<T> extends AbstractEffector<T> {
private static final long serialVersionUID = 6989688364011965968L;
private static final Logger log = LoggerFactory.getLogger(MethodEffector.class);
@SuppressWarnings("rawtypes")
public static Effector<?> create(Method m) {
return new MethodEffector(m);
}
protected static class AnnotationsOnMethod {
final Class<?> clazz;
final String name;
final String description;
final Class<?> returnType;
final List<ParameterType<?>> parameters;
public AnnotationsOnMethod(Class<?> clazz, String methodName) {
this(clazz, inferBestMethod(clazz, methodName));
}
public AnnotationsOnMethod(Class<?> clazz, Method method) {
this.clazz = clazz;
this.name = method.getName();
this.returnType = method.getReturnType();
// Get the description
org.apache.brooklyn.core.annotation.Effector effectorAnnotation = method.getAnnotation(org.apache.brooklyn.core.annotation.Effector.class);
description = (effectorAnnotation != null) ? effectorAnnotation.description() : null;
// Get the parameters
parameters = Lists.newArrayList();
int numParameters = method.getParameterTypes().length;
for (int i = 0; i < numParameters; i++) {
parameters.add(toParameterType(method, i));
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
protected static ParameterType<?> toParameterType(Method method, int paramIndex) {
Annotation[] anns = method.getParameterAnnotations()[paramIndex];
Class<?> type = method.getParameterTypes()[paramIndex];
EffectorParam paramAnnotation = findAnnotation(anns, EffectorParam.class);
// TODO if blank, could do "param"+(i+1); would that be better?
// TODO this will now give "" if name is blank, rather than previously null. Is that ok?!
String name = (paramAnnotation != null) ? paramAnnotation.name() : null;
String paramDescription = (paramAnnotation == null || EffectorParam.MAGIC_STRING_MEANING_NULL.equals(paramAnnotation.description())) ? null : paramAnnotation.description();
String description = (paramDescription != null) ? paramDescription : null;
String paramDefaultValue = (paramAnnotation == null || EffectorParam.MAGIC_STRING_MEANING_NULL.equals(paramAnnotation.defaultValue())) ? null : paramAnnotation.defaultValue();
Object defaultValue = (paramDefaultValue != null) ? TypeCoercions.coerce(paramDefaultValue, type) : null;
return new BasicParameterType(name, type, description, defaultValue);
}
@SuppressWarnings("unchecked")
protected static <T extends Annotation> T findAnnotation(Annotation[] anns, Class<T> type) {
for (Annotation ann : anns) {
if (type.isInstance(ann)) return (T) ann;
}
return null;
}
protected static Method inferBestMethod(Class<?> clazz, String methodName) {
Method best = null;
for (Method it : clazz.getMethods()) {
if (it.getName().equals(methodName)) {
if (best==null || best.getParameterTypes().length < it.getParameterTypes().length) best=it;
}
}
if (best==null) {
throw new IllegalStateException("Cannot find method "+methodName+" on "+clazz.getCanonicalName());
}
return best;
}
}
/** Defines a new effector whose details are supplied as annotations on the given type and method name */
public MethodEffector(Class<?> whereEffectorDefined, String methodName) {
this(new AnnotationsOnMethod(whereEffectorDefined, methodName), null);
}
public MethodEffector(Method method) {
this(new AnnotationsOnMethod(method.getDeclaringClass(), method), null);
}
public MethodEffector(MethodClosure mc) {
this(new AnnotationsOnMethod((Class<?>)mc.getDelegate(), mc.getMethod()), null);
}
@SuppressWarnings("unchecked")
protected MethodEffector(AnnotationsOnMethod anns, String description) {
super(anns.name, (Class<T>)anns.returnType, anns.parameters, GroovyJavaMethods.<String>elvis(description, anns.description));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public T call(Entity entity, Map parameters) {
Object[] parametersArray = EffectorUtils.prepareArgsForEffector(this, parameters);
if (entity instanceof AbstractEntity) {
return EffectorUtils.invokeMethodEffector(entity, this, parametersArray);
} else {
// we are dealing with a proxy here
// this implementation invokes the method on the proxy
// (requiring it to be on the interface)
// and letting the proxy deal with the remoting / runAtEntity;
// alternatively we could create the task here and pass it to runAtEntity;
// the latter may allow us to simplify/remove a lot of the stuff from
// EffectorUtils and possibly Effectors and Entities
// TODO Should really find method with right signature, rather than just the right args.
// TODO prepareArgs can miss things out that have "default values"! Code below will probably fail if that happens.
Method[] methods = entity.getClass().getMethods();
for (Method method : methods) {
if (method.getName().equals(getName())) {
if (parametersArray.length == method.getParameterTypes().length) {
try {
return (T) method.invoke(entity, parametersArray);
} catch (Exception e) {
// exception handled by the proxy invocation (which leads to EffectorUtils.invokeEffectorMethod...)
throw Exceptions.propagate(e);
}
}
}
}
String msg = "Could not find method for effector "+getName()+" with "+parametersArray.length+" parameters on "+entity;
log.warn(msg+" (throwing); available methods are: "+Arrays.toString(methods));
throw new IllegalStateException(msg);
}
}
}