| /* |
| * 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.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.function.Consumer; |
| import javax.annotation.Nullable; |
| |
| import com.google.common.base.Objects; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.reflect.TypeToken; |
| 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.TaskAdaptable; |
| import org.apache.brooklyn.api.mgmt.classloading.BrooklynClassLoadingContext; |
| import org.apache.brooklyn.config.ConfigKey; |
| import org.apache.brooklyn.core.config.ConfigKeys; |
| import org.apache.brooklyn.core.effector.EffectorTasks.EffectorBodyTaskFactory; |
| import org.apache.brooklyn.core.effector.EffectorTasks.EffectorMarkingTaskFactory; |
| import org.apache.brooklyn.core.effector.EffectorTasks.EffectorTaskFactory; |
| import org.apache.brooklyn.core.entity.Entities; |
| import org.apache.brooklyn.core.entity.EntityInternal; |
| import org.apache.brooklyn.core.mgmt.BrooklynTaskTags; |
| import org.apache.brooklyn.core.workflow.WorkflowEffector; |
| import org.apache.brooklyn.core.workflow.WorkflowExecutionContext; |
| import org.apache.brooklyn.util.collections.MutableSet; |
| import org.apache.brooklyn.util.core.config.ConfigBag; |
| import org.apache.brooklyn.util.core.flags.TypeCoercions; |
| import org.apache.brooklyn.util.core.task.Tasks; |
| import org.apache.brooklyn.util.text.Strings; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| public class Effectors { |
| |
| private static final Logger log = LoggerFactory.getLogger(Effectors.class); |
| |
| public static class EffectorBuilder<T> { |
| private Class<T> returnType; |
| private String effectorName; |
| private String description; |
| private Map<String,ParameterType<?>> parameters = new LinkedHashMap<String,ParameterType<?>>(); |
| private EffectorTaskFactory<T> impl; |
| |
| private EffectorBuilder(Class<T> returnType, String effectorName) { |
| this.returnType = returnType; |
| this.effectorName = effectorName; |
| } |
| protected EffectorBuilder(EffectorBuilder<T> other) { |
| this.returnType = other.returnType; |
| this.effectorName = other.effectorName; |
| this.description = other.description; |
| this.parameters = other.parameters; |
| this.impl = other.impl; |
| } |
| |
| public EffectorBuilder<T> name(String name) { |
| this.effectorName = name; |
| return this; |
| } |
| public EffectorBuilder<T> description(String description) { |
| this.description = description; |
| return this; |
| } |
| public EffectorBuilder<T> parameter(Class<?> paramType, String paramName) { |
| return parameter(paramType, paramName, null, null); |
| } |
| public EffectorBuilder<T> parameter(Class<?> paramType, String paramName, String paramDescription) { |
| return parameter(paramType, paramName, paramDescription, null); |
| } |
| public <V> EffectorBuilder<T> parameter(Class<V> paramType, String paramName, String paramDescription, V defaultValue) { |
| return parameter(new BasicParameterType<V>(paramName, paramType, paramDescription, defaultValue)); |
| } |
| public EffectorBuilder<T> parameter(TypeToken<?> paramType, String paramName) { |
| return parameter(paramType, paramName, null, null); |
| } |
| public EffectorBuilder<T> parameter(TypeToken<?> paramType, String paramName, String paramDescription) { |
| return parameter(paramType, paramName, paramDescription, null); |
| } |
| public <V> EffectorBuilder<T> parameter(TypeToken<V> paramType, String paramName, String paramDescription, V defaultValue) { |
| return parameter(new BasicParameterType<V>(paramName, paramType, paramDescription, defaultValue)); |
| } |
| public <V> EffectorBuilder<T> parameter(ConfigKey<V> key) { |
| return parameter(asParameterType(key)); |
| } |
| public EffectorBuilder<T> parameter(ParameterType<?> p) { |
| // allow redeclaring, e.g. for the case where we are overriding an existing effector |
| parameters.put(p.getName(), p); |
| return this; |
| } |
| |
| public EffectorBuilder<T> impl(EffectorBodyTaskFactory<T> taskFactory) { |
| this.impl = taskFactory; |
| return this; |
| } |
| public EffectorBuilder<T> impl(EffectorTaskFactory<T> taskFactory) { |
| this.impl = new EffectorMarkingTaskFactory<T>(taskFactory); |
| return this; |
| } |
| public EffectorBuilder<T> impl(EffectorBody<T> effectorBody) { |
| return impl(new EffectorBodyTaskFactory<T>(effectorBody)); |
| } |
| /** returns the effector, with an implementation (required); @see {@link #buildAbstract()} */ |
| public Effector<T> build() { |
| Preconditions.checkNotNull(impl, "Cannot create effector %s with no impl (did you forget impl? or did you mean to buildAbstract?)", effectorName); |
| return new EffectorAndBody<T>(effectorName, returnType, ImmutableList.copyOf(parameters.values()), description, impl); |
| } |
| |
| /** returns an abstract effector, where the body will be defined later/elsewhere |
| * (impl must not be set) */ |
| public Effector<T> buildAbstract() { |
| Preconditions.checkArgument(impl==null, "Cannot create abstract effector {} as an impl is defined", effectorName); |
| return new EffectorBase<T>(effectorName, returnType, ImmutableList.copyOf(parameters.values()), description); |
| } |
| } |
| |
| /** creates a new effector builder with the given name and return type */ |
| public static <T> EffectorBuilder<T> effector(Class<T> returnType, String effectorName) { |
| return new EffectorBuilder<T>(returnType, effectorName); |
| } |
| |
| /** creates a new effector builder to _override_ the given effector */ |
| public static <T> EffectorBuilder<T> effector(Effector<T> base) { |
| EffectorBuilder<T> builder = new EffectorBuilder<T>(base.getReturnType(), base.getName()); |
| for (ParameterType<?> p: base.getParameters()) |
| builder.parameter(p); |
| builder.description(base.getDescription()); |
| if (base instanceof EffectorWithBody) |
| builder.impl(((EffectorWithBody<T>) base).getBody()); |
| return builder; |
| } |
| |
| /** as {@link #invocation(Entity, Effector, Map)} but convenience for passing a {@link ConfigBag} */ |
| public static <T> TaskAdaptable<T> invocation(Entity entity, Effector<T> eff, ConfigBag parameters) { |
| return invocation(entity, eff, parameters==null ? ImmutableMap.of() : parameters.getAllConfig()); |
| } |
| |
| /** returns an unsubmitted task which invokes the given effector; use {@link Entities#invokeEffector(Entity, Entity, Effector, Map)} for a submitted variant */ |
| public static <T> TaskAdaptable<T> invocation(Entity entity, Effector<T> eff, @Nullable Map<?,?> parameters) { |
| return invocationPossiblySubWorkflow(entity, eff, parameters, null, null); |
| } |
| |
| public static <T> TaskAdaptable<T> invocationPossiblySubWorkflow(Entity entity, Effector<T> eff, @Nullable Map<?,?> parameters, WorkflowExecutionContext parent, Consumer<BrooklynTaskTags.WorkflowTaskTag> parentWorkflowInitializer) { |
| @SuppressWarnings("unchecked") |
| Effector<T> eff2 = (Effector<T>) ((EntityInternal)entity).getEffector(eff.getName()); |
| if (log.isTraceEnabled()) { |
| Object eff1Body = (eff instanceof EffectorWithBody<?> ? ((EffectorWithBody<?>) eff).getBody() : "bodyless"); |
| String message = String.format("Invoking %s/%s on entity %s", eff, eff1Body, entity); |
| if (eff != eff2) { |
| Object eff2Body = (eff2 instanceof EffectorWithBody<?> ? ((EffectorWithBody<?>) eff2).getBody() : "bodyless"); |
| message += String.format(" (actually %s/%s)", eff2, eff2Body); |
| } |
| log.trace(message); |
| } |
| if (eff2 != null) { |
| if (eff2 != eff) { |
| if (eff2 instanceof EffectorWithBody) { |
| log.debug("Replacing invocation of {} on {} with {} which is the impl defined at that entity", new Object[] { eff, entity, eff2 }); |
| return ((EffectorWithBody<T>)eff2).getBody().newTask(entity, eff2, getConfigBagWithParametersCoerced(eff2, parameters, false)); |
| } else { |
| log.warn("Effector {} defined on {} has no body; invoking caller-supplied {} instead", new Object[] { eff2, entity, eff }); |
| } |
| } |
| } else { |
| log.debug("Effector {} does not exist on {}; attempting to invoke anyway", new Object[] { eff, entity }); |
| } |
| |
| if (eff instanceof EffectorWithBody) { |
| if (eff instanceof WorkflowEffector.WorkflowEffectorAndBody) { |
| return (TaskAdaptable<T>) ((WorkflowEffector.WorkflowEffectorAndBody) eff).getBody().newSubWorkflowTask(entity, eff, getConfigBagWithParametersCoerced(eff, parameters, false), parent, parentWorkflowInitializer); |
| } else { |
| return ((EffectorWithBody<T>) eff).getBody().newTask(entity, eff, getConfigBagWithParametersCoerced(eff, parameters, false)); |
| } |
| } |
| |
| throw new UnsupportedOperationException("No implementation registered for effector "+eff+" on "+entity); |
| } |
| |
| public static ConfigBag getConfigBagWithParametersCoerced(Effector<?> eff, @Nullable Map<?,?> map, boolean requireParameter) { |
| ConfigBag bag = ConfigBag.newInstance(); |
| if (map!=null) { |
| map.forEach( (ko,v) -> { |
| String k; |
| if (ko instanceof String) k = (String) ko; |
| else if (ko instanceof ConfigKey) k = ((ConfigKey)ko).getName(); |
| else throw new IllegalArgumentException("Invalid parameter '"+ko+"' for effector "+eff.getName()+"; should be a string or config key"); |
| Optional<ParameterType<?>> p = eff.getParameters().stream().filter(pi -> k.equals(pi.getName())).findFirst(); |
| if (!p.isPresent()) { |
| if (!requireParameter) { |
| // many invocations pass ad hoc parameters |
| bag.putStringKey(k, v); |
| } else { |
| // might be nice to stop doing that, but even some workflow eg update children on_update takes parameters it doesn't declare |
| throw new IllegalArgumentException("No such parameter '" + k + "' for effector " + eff.getName()); |
| } |
| } else { |
| Object v2 = TypeCoercions.tryCoerce(v, p.get().getParameterType()).orThrow("Invalid parameter value for '" + k + "' for effector " + eff.getName() + "; cannot convert to " + p.get().getParameterType()); |
| bag.putStringKey(k, v2); |
| } |
| }); |
| } |
| return bag; |
| } |
| |
| public static <V> ParameterType<V> asParameterType(ConfigKey<V> key) { |
| return key.hasDefaultValue() |
| ? new BasicParameterType<V>(key.getName(), key.getTypeToken(), key.getDescription(), key.getDefaultValue()) |
| : new BasicParameterType<V>(key.getName(), key.getTypeToken(), key.getDescription()); |
| } |
| |
| public static <V> ConfigKey<V> asConfigKey(ParameterType<V> paramType) { |
| return ConfigKeys.newConfigKey(paramType.getParameterType(), paramType.getName(), paramType.getDescription(), paramType.getDefaultValue()); |
| } |
| |
| /** convenience for {@link #invocationParallel(Effector, Map, Iterable)} */ |
| public static TaskAdaptable<List<?>> invocation(Effector<?> eff, Map<?,?> params, Iterable<? extends Entity> entities) { |
| return invocationParallel(eff, params, entities); |
| } |
| |
| /** returns an unsubmitted task which will invoke the given effector on the given entities in parallel; |
| * return type is Task<List<T>> (but haven't put in the blood sweat toil and tears to make the generics work) */ |
| public static TaskAdaptable<List<?>> invocationParallel(Effector<?> eff, Map<?,?> params, Iterable<? extends Entity> entities) { |
| List<TaskAdaptable<?>> tasks = new ArrayList<TaskAdaptable<?>>(); |
| for (Entity e: entities) tasks.add(invocation(e, eff, params)); |
| return Tasks.parallel( |
| "invoking " + eff + " on " + tasks.size() + " node" + (Strings.s(tasks.size())), |
| tasks.toArray(new TaskAdaptable[tasks.size()])); |
| } |
| |
| /** as {@link #invocationParallel(Effector, Map, Iterable)} but executing sequentially */ |
| public static TaskAdaptable<List<?>> invocationSequential(Effector<?> eff, Map<?,?> params, Iterable<? extends Entity> entities) { |
| List<TaskAdaptable<?>> tasks = new ArrayList<TaskAdaptable<?>>(); |
| for (Entity e: entities) tasks.add(invocation(e, eff, params)); |
| return Tasks.sequential( |
| "invoking " + eff + " on " + tasks.size() + " node" + (Strings.s(tasks.size())), |
| tasks.toArray(new TaskAdaptable[tasks.size()])); |
| } |
| |
| /** returns an unsubmitted task which will invoke the given effector on the given entities |
| * (this form of method is a convenience for {@link #invocation(Effector, Map, Iterable)}) */ |
| public static TaskAdaptable<List<?>> invocation(Effector<?> eff, Map<?, ?> params, Entity ...entities) { |
| return invocation(eff, params, Arrays.asList(entities)); |
| } |
| |
| public static boolean sameSignature(Effector<?> e1, Effector<?> e2) { |
| return Objects.equal(e1.getName(), e2.getName()) && |
| Objects.equal(e1.getParameters(), e2.getParameters()) && |
| Objects.equal(e1.getReturnType(), e2.getReturnType()); |
| } |
| |
| // TODO sameSignatureAndBody |
| |
| public static boolean sameInstance(Effector<?> e1, Effector<?> e2) { |
| return e1 == e2; |
| } |
| |
| public static Collection<ParameterType<?>> parseParameters(Map<String,Object> paramDefs) { |
| return parseParameters(paramDefs, null); |
| } |
| public static Collection<ParameterType<?>> parseParameters(Map<String,Object> paramDefs, BrooklynClassLoadingContext loader) { |
| Set<ParameterType<?>> result = MutableSet.of(); |
| if (paramDefs==null) return result; |
| |
| for (Map.Entry<String, Object> paramDef: paramDefs.entrySet()){ |
| if (paramDef!=null) { |
| String paramName = paramDef.getKey(); |
| Object value = paramDef.getValue(); |
| if (value==null) value = Collections.emptyMap(); |
| if (!(value instanceof Map)) { |
| if (value instanceof CharSequence && Strings.isBlank((CharSequence) value)) |
| value = Collections.emptyMap(); |
| } |
| if (!(value instanceof Map)) |
| throw new IllegalArgumentException("Illegal argument of type "+value.getClass()+" value '"+value+"' supplied as parameter definition " |
| + "'"+paramName); |
| result.add(Effectors.asParameterType(ConfigKeys.DynamicKeys.newNamedInstance(paramName, (Map<?, ?>) value, loader))); |
| } |
| } |
| return result; |
| } |
| |
| } |