/*
* 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.util.core.task;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.mgmt.ExecutionContext;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.api.mgmt.TaskAdaptable;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
import org.apache.brooklyn.util.core.flags.TypeCoercions;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.javalang.JavaClassNames;
import org.apache.brooklyn.util.repeat.Repeater;
import org.apache.brooklyn.util.time.CountdownTimer;
import org.apache.brooklyn.util.time.Duration;
import org.apache.brooklyn.util.time.Durations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.Beta;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.reflect.TypeToken;
/**
* Resolves a given object, as follows:
* <li> If it is a {@link Tasks} or a {@link DeferredSupplier} then get its contents
* <li> If it's a map and {@link #deep(boolean)} is requested, it applies resolution to contents
* <li> It applies coercion
* <p>
* Fluent-style API exposes a number of other options.
*/
public class ValueResolver<T> implements DeferredSupplier<T> {
// TODO most of these usages should be removed when we have
// an ability to run resolution in a non-blocking mode
// (i.e. running resolution tasks in the same thread,
// or in a context where they can only wait on subtasks
// which are guaranteed to have the same constraint)
/**
* Period to wait if we're expected to return real quick
* but we want fast things to have time to finish.
* <p>
* Timings are always somewhat arbitrary but this at least
* allows some intention to be captured in code rather than arbitrary values. */
@Beta
public static Duration REAL_QUICK_WAIT = Duration.millis(50);
/**
* Like {@link #REAL_QUICK_WAIT} but even smaller, for use when potentially
* resolving multiple items in sequence. */
@Beta
public static Duration REAL_REAL_QUICK_WAIT = Duration.millis(5);
/**
* Period to wait if we're expected to return quickly
* but we want to be a bit more generous for things to finish,
* without letting a caller get annoyed.
* <p>
* See {@link #REAL_QUICK_WAIT}. */
public static Duration PRETTY_QUICK_WAIT = Duration.millis(200);
/** Period to wait when we have to poll but want to give the illusion of no wait.
* See {@link Repeater#DEFAULT_REAL_QUICK_PERIOD} */
public static Duration REAL_QUICK_PERIOD = Repeater.DEFAULT_REAL_QUICK_PERIOD;
private static final Logger log = LoggerFactory.getLogger(ValueResolver.class);
final Object value;
final Class<T> type;
ExecutionContext exec;
String description;
boolean forceDeep;
/** null means do it if you can; true means always, false means never */
Boolean embedResolutionInTask;
/** timeout on execution, if possible, or if embedResolutionInTask is true */
Duration timeout;
boolean isTransientTask = true;
T defaultValue = null;
boolean returnDefaultOnGet = false;
boolean swallowExceptions = false;
// internal fields
final Object parentOriginalValue;
final CountdownTimer parentTimer;
AtomicBoolean started = new AtomicBoolean(false);
boolean expired;
ValueResolver(Object v, Class<T> type) {
this.value = v;
this.type = type;
checkTypeNotNull();
parentOriginalValue = null;
parentTimer = null;
}
ValueResolver(Object v, Class<T> type, ValueResolver<?> parent) {
this.value = v;
this.type = type;
checkTypeNotNull();
exec = parent.exec;
description = parent.description;
forceDeep = parent.forceDeep;
embedResolutionInTask = parent.embedResolutionInTask;
parentOriginalValue = parent.getOriginalValue();
timeout = parent.timeout;
parentTimer = parent.parentTimer;
if (parentTimer!=null && parentTimer.isExpired())
expired = true;
// default value and swallow exceptions do not need to be nested
}
public static class ResolverBuilderPretype {
final Object v;
public ResolverBuilderPretype(Object v) {
this.v = v;
}
public <T> ValueResolver<T> as(Class<T> type) {
return new ValueResolver<T>(v, type);
}
}
/** returns a copy of this resolver which can be queried, even if the original (single-use instance) has already been copied */
public ValueResolver<T> clone() {
ValueResolver<T> result = new ValueResolver<T>(value, type)
.context(exec).description(description)
.embedResolutionInTask(embedResolutionInTask)
.deep(forceDeep)
.timeout(timeout);
if (returnDefaultOnGet) result.defaultValue(defaultValue);
if (swallowExceptions) result.swallowExceptions();
return result;
}
/** execution context to use when resolving; required if resolving unsubmitted tasks or running with a time limit */
public ValueResolver<T> context(ExecutionContext exec) {
this.exec = exec;
return this;
}
/** as {@link #context(ExecutionContext)} for use from an entity */
public ValueResolver<T> context(Entity entity) {
return context(entity!=null ? ((EntityInternal)entity).getExecutionContext() : null);
}
/** sets a message which will be displayed in status reports while it waits (e.g. the name of the config key being looked up) */
public ValueResolver<T> description(String description) {
this.description = description;
return this;
}
/** sets a default value which will be returned on a call to {@link #get()} if the task does not complete
* or completes with an error
* <p>
* note that {@link #getMaybe()} returns an absent object even in the presence of
* a default, so that any error can still be accessed */
public ValueResolver<T> defaultValue(T defaultValue) {
this.defaultValue = defaultValue;
this.returnDefaultOnGet = true;
return this;
}
/** indicates that no default value should be returned on a call to {@link #get()}, and instead it should throw
* (this is the default; this method is provided to undo a call to {@link #defaultValue(Object)}) */
public ValueResolver<T> noDefaultValue() {
this.returnDefaultOnGet = false;
this.defaultValue = null;
return this;
}
/** indicates that exceptions in resolution should not be thrown on a call to {@link #getMaybe()},
* but rather used as part of the {@link Maybe#get()} if it's absent,
* and swallowed altogether on a call to {@link #get()} in the presence of a {@link #defaultValue(Object)} */
public ValueResolver<T> swallowExceptions() {
this.swallowExceptions = true;
return this;
}
/** whether the task should be marked as transient; defaults true */
public ValueResolver<T> transientTask(boolean isTransientTask) {
this.isTransientTask = isTransientTask;
return this;
}
public Maybe<T> getDefault() {
if (returnDefaultOnGet) return Maybe.of(defaultValue);
else return Maybe.absent("No default value set");
}
/** causes nested structures (maps, lists) to be descended and nested unresolved values resolved */
public ValueResolver<T> deep(boolean forceDeep) {
this.forceDeep = forceDeep;
return this;
}
/** if true, forces execution of a deferred supplier to be run in a task;
* if false, it prevents it (meaning time limits may not be applied);
* if null, the default, it runs in a task if a time limit is applied.
* <p>
* running inside a task is required for some {@link DeferredSupplier}
* instances which look up a task {@link ExecutionContext}. */
public ValueResolver<T> embedResolutionInTask(Boolean embedResolutionInTask) {
this.embedResolutionInTask = embedResolutionInTask;
return this;
}
/** sets a time limit on executions
* <p>
* used for {@link Task} and {@link DeferredSupplier} instances.
* may require an execution context at runtime. */
public ValueResolver<T> timeout(Duration timeout) {
this.timeout = timeout;
return this;
}
protected void checkTypeNotNull() {
if (type==null)
throw new NullPointerException("type must be set to resolve, for '"+value+"'"+(description!=null ? ", "+description : ""));
}
public T get() {
Maybe<T> m = getMaybe();
if (m.isPresent()) return m.get();
if (returnDefaultOnGet) return defaultValue;
return m.get();
}
public Maybe<T> getMaybe() {
Maybe<T> result = getMaybeInternal();
if (log.isTraceEnabled()) {
log.trace(this+" evaluated as "+result);
}
return result;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
protected Maybe<T> getMaybeInternal() {
if (started.getAndSet(true))
throw new IllegalStateException("ValueResolver can only be used once");
if (expired) return Maybe.absent("Nested resolution of "+getOriginalValue()+" did not complete within "+timeout);
ExecutionContext exec = this.exec;
if (exec==null) {
// if execution context not specified, take it from the current task if present
exec = BasicExecutionContext.getCurrentExecutionContext();
}
CountdownTimer timerU = parentTimer;
if (timerU==null && timeout!=null)
timerU = timeout.countdownTimer();
final CountdownTimer timer = timerU;
if (timer!=null && !timer.isRunning())
timer.start();
checkTypeNotNull();
Object v = this.value;
//if the expected type is a closure or map and that's what we have, we're done (or if it's null);
//but not allowed to return a future or DeferredSupplier as the resolved value
if (v==null || (!forceDeep && type.isInstance(v) && !Future.class.isInstance(v) && !DeferredSupplier.class.isInstance(v)))
return Maybe.of((T) v);
try {
//if it's a task or a future, we wait for the task to complete
if (v instanceof TaskAdaptable<?>) {
//if it's a task, we make sure it is submitted
if (!((TaskAdaptable<?>) v).asTask().isSubmitted() ) {
if (exec==null)
return Maybe.absent("Value for unsubmitted task '"+getDescription()+"' requested but no execution context available");
exec.submit(((TaskAdaptable<?>) v).asTask());
}
}
if (v instanceof Future) {
final Future<?> vfuture = (Future<?>) v;
//including tasks, above
if (!vfuture.isDone()) {
Callable<Maybe> callable = new Callable<Maybe>() {
public Maybe call() throws Exception {
return Durations.get(vfuture, timer);
} };
String description = getDescription();
Maybe vm = Tasks.withBlockingDetails("Waiting for "+description, callable);
if (vm.isAbsent()) return vm;
v = vm.get();
} else {
v = vfuture.get();
}
} else if (v instanceof DeferredSupplier<?>) {
final DeferredSupplier<?> ds = (DeferredSupplier<?>) v;
if ((!Boolean.FALSE.equals(embedResolutionInTask) && (exec!=null || timeout!=null)) || Boolean.TRUE.equals(embedResolutionInTask)) {
if (exec==null)
return Maybe.absent("Embedding in task needed for '"+getDescription()+"' but no execution context available");
Callable<Object> callable = new Callable<Object>() {
public Object call() throws Exception {
try {
Tasks.setBlockingDetails("Retrieving "+ds);
return ds.get();
} finally {
Tasks.resetBlockingDetails();
}
} };
String description = getDescription();
TaskBuilder<Object> tb = Tasks.<Object>builder().body(callable).displayName("Resolving dependent value").description(description);
if (isTransientTask) tb.tag(BrooklynTaskTags.TRANSIENT_TASK_TAG);
Task<Object> vt = exec.submit(tb.build());
// TODO to handle immediate resolution, it would be nice to be able to submit
// so it executes in the current thread,
// or put a marker in the target thread or task while it is running that the task
// should never wait on anything other than another value being resolved
// (though either could recurse infinitely)
Maybe<Object> vm = Durations.get(vt, timer);
vt.cancel(true);
if (vm.isAbsent()) return (Maybe<T>)vm;
v = vm.get();
} else {
try {
Tasks.setBlockingDetails("Retrieving (non-task) "+ds);
v = ((DeferredSupplier<?>) ds).get();
} finally {
Tasks.resetBlockingDetails();
}
}
} else if (v instanceof Map) {
//and if a map or list we look inside
Map result = Maps.newLinkedHashMap();
for (Map.Entry<?,?> entry : ((Map<?,?>)v).entrySet()) {
Maybe<?> kk = new ValueResolver(entry.getKey(), type, this)
.description( (description!=null ? description+", " : "") + "map key "+entry.getKey() )
.getMaybe();
if (kk.isAbsent()) return (Maybe<T>)kk;
Maybe<?> vv = new ValueResolver(entry.getValue(), type, this)
.description( (description!=null ? description+", " : "") + "map value for key "+kk.get() )
.getMaybe();
if (vv.isAbsent()) return (Maybe<T>)vv;
result.put(kk.get(), vv.get());
}
return Maybe.of((T) result);
} else if (v instanceof Set) {
Set result = Sets.newLinkedHashSet();
int count = 0;
for (Object it : (Set)v) {
Maybe<?> vv = new ValueResolver(it, type, this)
.description( (description!=null ? description+", " : "") + "entry "+count )
.getMaybe();
if (vv.isAbsent()) return (Maybe<T>)vv;
result.add(vv.get());
count++;
}
return Maybe.of((T) result);
} else if (v instanceof Iterable) {
List result = Lists.newArrayList();
int count = 0;
for (Object it : (Iterable)v) {
Maybe<?> vv = new ValueResolver(it, type, this)
.description( (description!=null ? description+", " : "") + "entry "+count )
.getMaybe();
if (vv.isAbsent()) return (Maybe<T>)vv;
result.add(vv.get());
count++;
}
return Maybe.of((T) result);
} else {
return TypeCoercions.tryCoerce(v, TypeToken.of(type));
}
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
IllegalArgumentException problem = new IllegalArgumentException("Error resolving "+(description!=null ? description+", " : "")+v+", in "+exec+": "+e, e);
if (swallowExceptions) {
if (log.isDebugEnabled())
log.debug("Resolution of "+this+" failed, swallowing and returning: "+e);
return Maybe.absent(problem);
}
if (log.isDebugEnabled())
log.debug("Resolution of "+this+" failed, throwing: "+e);
throw problem;
}
return new ValueResolver(v, type, this).getMaybe();
}
protected String getDescription() {
return description!=null ? description : ""+value;
}
protected Object getOriginalValue() {
if (parentOriginalValue!=null) return parentOriginalValue;
return value;
}
@Override
public String toString() {
return JavaClassNames.cleanSimpleClassName(this)+"["+JavaClassNames.cleanSimpleClassName(type)+" "+value+"]";
}
}