| /* |
| * 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.mgmt.internal; |
| |
| import static org.apache.brooklyn.util.groovy.GroovyJavaMethods.truth; |
| |
| import java.lang.reflect.Method; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| import java.util.Set; |
| |
| 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.api.mgmt.Task; |
| import org.apache.brooklyn.core.effector.BasicParameterType; |
| import org.apache.brooklyn.core.entity.EntityInternal; |
| import org.apache.brooklyn.core.mgmt.BrooklynTaskTags; |
| import org.apache.brooklyn.core.mgmt.entitlement.Entitlements; |
| import org.apache.brooklyn.util.collections.MutableList; |
| import org.apache.brooklyn.util.collections.MutableMap; |
| import org.apache.brooklyn.util.core.config.ConfigBag; |
| import org.apache.brooklyn.util.core.flags.TypeCoercions; |
| import org.apache.brooklyn.util.exceptions.Exceptions; |
| import org.apache.brooklyn.util.exceptions.PropagatedRuntimeException; |
| import org.apache.brooklyn.util.guava.Maybe; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| |
| /** |
| * Utility methods for invoking effectors. |
| */ |
| public class EffectorUtils { |
| |
| private static final Logger log = LoggerFactory.getLogger(EffectorUtils.class); |
| |
| /** prepares arguments for an effector either accepting: |
| * an array, which should contain the arguments in order, optionally omitting those which have defaults defined; |
| * or a map, which should contain the arguments by name, again optionally omitting those which have defaults defined, |
| * and in this case also performing type coercion. |
| */ |
| public static Object[] prepareArgsForEffector(Effector<?> eff, Object args) { |
| if (args != null && args.getClass().isArray()) { |
| return prepareArgsForEffectorFromArray(eff, (Object[]) args); |
| } |
| if (args instanceof Map) { |
| return prepareArgsForEffectorFromMap(eff, (Map) args); |
| } |
| log.warn("Deprecated effector invocation style for call to "+eff+", expecting a map or an array, got: "+args); |
| if (log.isDebugEnabled()) { |
| log.debug("Deprecated effector invocation style for call to "+eff+", expecting a map or an array, got: "+args, |
| new Throwable("Trace for deprecated effector invocation style")); |
| } |
| return oldPrepareArgsForEffector(eff, args); |
| } |
| |
| /** method used for calls such as entity.effector(arg1, arg2) |
| * get routed here from AbstractEntity.invokeMethod */ |
| private static Object[] prepareArgsForEffectorFromArray(Effector<?> eff, Object args[]) { |
| int newArgsNeeded = eff.getParameters().size(); |
| if (args.length==1 && args[0] instanceof Map) { |
| if (newArgsNeeded!=1 || !eff.getParameters().get(0).getParameterClass().isAssignableFrom(args[0].getClass())) { |
| // treat a map in an array as a map passed directly (unless the method takes a single-arg map) |
| // this is to support effector(param1: val1) |
| return prepareArgsForEffectorFromMap(eff, (Map) args[0]); |
| } |
| } |
| return prepareArgsForEffectorAsMapFromArray(eff, args).values().toArray(new Object[0]); |
| } |
| |
| public static Map prepareArgsForEffectorAsMapFromArray(Effector<?> eff, Object args[]) { |
| int newArgsNeeded = eff.getParameters().size(); |
| List l = Lists.newArrayList(); |
| l.addAll(Arrays.asList(args)); |
| Map newArgs = new LinkedHashMap(); |
| |
| for (int index = 0; index < eff.getParameters().size(); index++) { |
| ParameterType<?> it = eff.getParameters().get(index); |
| |
| if (l.size() >= newArgsNeeded) { |
| //all supplied (unnamed) arguments must be used; ignore map |
| newArgs.put(it.getName(), l.remove(0)); |
| // TODO do we ignore arguments in the same order that groovy does? |
| } else if (!l.isEmpty() && it.getParameterClass().isInstance(l.get(0))) { |
| //if there are parameters supplied, and type is correct, they get applied before default values |
| //(this is akin to groovy) |
| newArgs.put(it.getName(), l.remove(0)); |
| } else if (it instanceof BasicParameterType && ((BasicParameterType)it).hasDefaultValue()) { |
| //finally, default values are used to make up for missing parameters |
| newArgs.put(it.getName(), ((BasicParameterType)it).getDefaultValue()); |
| } else { |
| throw new IllegalArgumentException("Invalid arguments (count mismatch) for effector "+eff+": "+args); |
| } |
| |
| newArgsNeeded--; |
| } |
| if (newArgsNeeded > 0) { |
| throw new IllegalArgumentException("Invalid arguments (missing "+newArgsNeeded+") for effector "+eff+": "+args); |
| } |
| if (!l.isEmpty()) { |
| throw new IllegalArgumentException("Invalid arguments ("+l.size()+" extra) for effector "+eff+": "+args); |
| } |
| return newArgs; |
| } |
| |
| private static Object[] prepareArgsForEffectorFromMap(Effector<?> eff, Map m) { |
| m = Maps.newLinkedHashMap(m); //make editable copy |
| List newArgs = Lists.newArrayList(); |
| int newArgsNeeded = eff.getParameters().size(); |
| |
| for (int index = 0; index < eff.getParameters().size(); index++) { |
| ParameterType<?> it = eff.getParameters().get(index); |
| Object v; |
| if (truth(it.getName()) && m.containsKey(it.getName())) { |
| // argument is in the map |
| v = m.remove(it.getName()); |
| } else if (it instanceof BasicParameterType && ((BasicParameterType)it).hasDefaultValue()) { |
| //finally, default values are used to make up for missing parameters |
| v = ((BasicParameterType)it).getDefaultValue(); |
| } else { |
| throw new IllegalArgumentException("Invalid arguments (missing argument "+it+") for effector "+eff+": "+m); |
| } |
| |
| newArgs.add(TypeCoercions.coerce(v, it.getParameterClass())); |
| newArgsNeeded--; |
| } |
| if (newArgsNeeded>0) |
| throw new IllegalArgumentException("Invalid arguments (missing "+newArgsNeeded+") for effector "+eff+": "+m); |
| if (!m.isEmpty()) { |
| log.warn("Unsupported parameter to "+eff+" (ignoring): "+m); |
| } |
| return newArgs.toArray(new Object[newArgs.size()]); |
| } |
| |
| /** |
| * Takes arguments, and returns an array of arguments suitable for use by the Effector |
| * according to the ParameterTypes it exposes. |
| * <p> |
| * The args can be: |
| * <ol> |
| * <li>an array of ordered arguments |
| * <li>a collection (which will be automatically converted to an array) |
| * <li>a single argument (which will then be wrapped in an array) |
| * <li>a map containing the (named) arguments |
| * <li>an array or collection single entry of a map (treated same as 5 above) |
| * <li>a semi-populated array or collection that also containing a map as first arg - |
| * uses ordered args in array, but uses named values from map in preference. |
| * <li>semi-populated array or collection, where default values will otherwise be used. |
| * </ol> |
| */ |
| public static Object[] oldPrepareArgsForEffector(Effector<?> eff, Object args) { |
| //attempt to coerce unexpected types |
| Object[] argsArray; |
| if (args==null) { |
| argsArray = new Object[0]; |
| } else if (args.getClass().isArray()) { |
| argsArray = (Object[]) args; |
| } else { |
| if (args instanceof Collection) { |
| argsArray = ((Collection) args).toArray(new Object[((Collection) args).size()]); |
| } else { |
| argsArray = new Object[] { args }; |
| } |
| } |
| |
| //if args starts with a map, assume it contains the named arguments |
| //(but only use it when we have insufficient supplied arguments) |
| List l = Lists.newArrayList(); |
| l.addAll(Arrays.asList(argsArray)); |
| Map m = (argsArray.length > 0 && argsArray[0] instanceof Map ? Maps.newLinkedHashMap((Map) l.remove(0)) : null); |
| List newArgs = Lists.newArrayList(); |
| int newArgsNeeded = eff.getParameters().size(); |
| boolean mapUsed = false; |
| |
| for (int index = 0; index < eff.getParameters().size(); index++) { |
| ParameterType<?> it = eff.getParameters().get(index); |
| |
| if (l.size() >= newArgsNeeded) { |
| //all supplied (unnamed) arguments must be used; ignore map |
| newArgs.add(l.remove(0)); |
| } else if (truth(m) && truth(it.getName()) && m.containsKey(it.getName())) { |
| //some arguments were not supplied, and this one is in the map |
| newArgs.add(m.remove(it.getName())); |
| } else if (index == 0 && Map.class.isAssignableFrom(it.getParameterClass())) { |
| //if first arg is a map it takes the supplied map |
| newArgs.add(m); |
| mapUsed = true; |
| } else if (!l.isEmpty() && it.getParameterClass().isInstance(l.get(0))) { |
| //if there are parameters supplied, and type is correct, they get applied before default values |
| //(this is akin to groovy) |
| newArgs.add(l.remove(0)); |
| } else if (it instanceof BasicParameterType && ((BasicParameterType)it).hasDefaultValue()) { |
| //finally, default values are used to make up for missing parameters |
| newArgs.add(((BasicParameterType)it).getDefaultValue()); |
| } else { |
| throw new IllegalArgumentException("Invalid arguments (count mismatch) for effector "+eff+": "+args); |
| } |
| |
| newArgsNeeded--; |
| } |
| if (newArgsNeeded > 0) { |
| throw new IllegalArgumentException("Invalid arguments (missing "+newArgsNeeded+") for effector "+eff+": "+args); |
| } |
| if (!l.isEmpty()) { |
| throw new IllegalArgumentException("Invalid arguments ("+l.size()+" extra) for effector "+eff+": "+args); |
| } |
| if (truth(m) && !mapUsed) { |
| throw new IllegalArgumentException("Invalid arguments ("+m.size()+" extra named) for effector "+eff+": "+args); |
| } |
| return newArgs.toArray(new Object[newArgs.size()]); |
| } |
| |
| /** |
| * Invokes a method effector so that its progress is tracked. For internal use only, when we know the effector is backed by a method which is local. |
| */ |
| public static <T> T invokeMethodEffector(Entity entity, Effector<T> eff, Object[] args) { |
| String name = eff.getName(); |
| |
| try { |
| if (log.isDebugEnabled()) log.debug("Invoking effector {} on {}", new Object[] {name, entity}); |
| if (log.isTraceEnabled()) log.trace("Invoking effector {} on {} with args {}", new Object[] {name, entity, args}); |
| EntityManagementSupport mgmtSupport = ((EntityInternal)entity).getManagementSupport(); |
| if (!mgmtSupport.isDeployed()) { |
| mgmtSupport.attemptLegacyAutodeployment(name); |
| } |
| ManagementContextInternal mgmtContext = (ManagementContextInternal) ((EntityInternal) entity).getManagementContext(); |
| |
| mgmtSupport.getEntityChangeListener().onEffectorStarting(eff, args); |
| try { |
| return mgmtContext.invokeEffectorMethodSync(entity, eff, args); |
| } finally { |
| mgmtSupport.getEntityChangeListener().onEffectorCompleted(eff); |
| } |
| } catch (Exception e) { |
| handleEffectorException(entity, eff, e); |
| // (won't return below) |
| return null; |
| } |
| } |
| |
| public static void handleEffectorException(Entity entity, Effector<?> effector, Throwable throwable) { |
| String message = "Error invoking " + effector.getName() + " at " + entity; |
| // Avoid throwing a PropagatedRuntimeException that just repeats the last PropagatedRuntimeException. |
| if (throwable instanceof PropagatedRuntimeException && |
| throwable.getMessage() != null && |
| throwable.getMessage().startsWith(message)) { |
| throw PropagatedRuntimeException.class.cast(throwable); |
| } else { |
| log.warn(message + ": " + Exceptions.collapseText(throwable)); |
| throw new PropagatedRuntimeException(message, throwable); |
| } |
| } |
| |
| public static <T> Task<T> invokeEffectorAsync(Entity entity, Effector<T> eff, Map<String,?> parameters) { |
| String name = eff.getName(); |
| |
| if (log.isDebugEnabled()) log.debug("Invoking-async effector {} on {}", new Object[] { name, entity }); |
| if (log.isTraceEnabled()) log.trace("Invoking-async effector {} on {} with args {}", new Object[] { name, entity, parameters }); |
| EntityManagementSupport mgmtSupport = ((EntityInternal)entity).getManagementSupport(); |
| if (!mgmtSupport.isDeployed()) { |
| mgmtSupport.attemptLegacyAutodeployment(name); |
| } |
| ManagementContextInternal mgmtContext = (ManagementContextInternal) ((EntityInternal)entity).getManagementContext(); |
| |
| // FIXME seems brittle to have the listeners in the Utils method; better to move into the context.invokeEff |
| // (or whatever the last mile before invoking the effector is - though currently there is not such a canonical place!) |
| mgmtSupport.getEntityChangeListener().onEffectorStarting(eff, parameters); |
| try { |
| return mgmtContext.invokeEffector(entity, eff, parameters); |
| } finally { |
| // FIXME this is really Effector submitted |
| mgmtSupport.getEntityChangeListener().onEffectorCompleted(eff); |
| } |
| } |
| |
| /** @deprecated since 0.7.0, not used */ |
| @Deprecated |
| public static Effector<?> findEffectorMatching(Entity entity, Method method) { |
| outer: for (Effector<?> effector : entity.getEntityType().getEffectors()) { |
| if (!effector.getName().equals(entity)) continue; |
| if (effector.getParameters().size() != method.getParameterTypes().length) continue; |
| for (int i = 0; i < effector.getParameters().size(); i++) { |
| if (effector.getParameters().get(i).getParameterClass() != method.getParameterTypes()[i]) continue outer; |
| } |
| return effector; |
| } |
| return null; |
| } |
| |
| /** @deprecated since 0.7.0, expects parameters but does not use them! */ |
| @Deprecated |
| public static Effector<?> findEffectorMatching(Set<Effector<?>> effectors, String effectorName, Map<String, ?> parameters) { |
| // TODO Support overloading: check parameters as well |
| for (Effector<?> effector : effectors) { |
| if (effector.getName().equals(effectorName)) { |
| return effector; |
| } |
| } |
| return null; |
| } |
| |
| /** matches effectors by name only (not parameters) */ |
| public static Maybe<Effector<?>> findEffector(Collection<? extends Effector<?>> effectors, String effectorName) { |
| for (Effector<?> effector : effectors) { |
| if (effector.getName().equals(effectorName)) { |
| return Maybe.<Effector<?>>of(effector); |
| } |
| } |
| return Maybe.absent(new NoSuchElementException("No effector with name "+effectorName+" (contenders "+effectors+")")); |
| } |
| |
| /** matches effectors by name only (not parameters), based on what is declared on the entity static type */ |
| public static Maybe<Effector<?>> findEffectorDeclared(Entity entity, String effectorName) { |
| return findEffector(entity.getEntityType().getEffectors(), effectorName); |
| } |
| |
| /** @deprecated since 0.7.0 use {@link #getTaskFlagsForEffectorInvocation(Entity, Effector, ConfigBag)} */ |
| public static Map<Object,Object> getTaskFlagsForEffectorInvocation(Entity entity, Effector<?> effector) { |
| return getTaskFlagsForEffectorInvocation(entity, effector, null); |
| } |
| |
| /** returns a (mutable) map of the standard flags which should be placed on an effector */ |
| public static Map<Object,Object> getTaskFlagsForEffectorInvocation(Entity entity, Effector<?> effector, ConfigBag parameters) { |
| List<Object> tags = MutableList.of( |
| BrooklynTaskTags.EFFECTOR_TAG, |
| BrooklynTaskTags.tagForEffectorCall(entity, effector.getName(), parameters), |
| BrooklynTaskTags.tagForTargetEntity(entity)); |
| if (Entitlements.getEntitlementContext() != null) { |
| tags.add(BrooklynTaskTags.tagForEntitlement(Entitlements.getEntitlementContext())); |
| } |
| return MutableMap.builder() |
| .put("description", "Invoking effector "+effector.getName() |
| +" on "+entity.getDisplayName() |
| +(parameters!=null ? " with parameters "+parameters.getAllConfig() : "")) |
| .put("displayName", effector.getName()) |
| .put("tags", tags) |
| .build(); |
| } |
| |
| } |