/*
* 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.enricher.stock;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.sensor.AttributeSensor;
import org.apache.brooklyn.api.sensor.Sensor;
import org.apache.brooklyn.api.sensor.SensorEvent;
import org.apache.brooklyn.api.sensor.SensorEventListener;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.BrooklynLogging;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.collections.QuorumCheck.QuorumChecks;
import org.apache.brooklyn.util.core.flags.SetFromFlag;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.text.StringPredicates;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.reflect.TypeToken;
/** Building on {@link AbstractAggregator} for a single source sensor (on multiple children and/or members) */
@SuppressWarnings("serial")
//@Catalog(name="Aggregator", description="Aggregates attributes from multiple entities into a single attribute value; see Enrichers.builder().aggregating(...)")
public class Aggregator<T,U> extends AbstractAggregator<T,U> implements SensorEventListener<T> {
private static final Logger LOG = LoggerFactory.getLogger(Aggregator.class);
public static final ConfigKey<Sensor<?>> SOURCE_SENSOR = ConfigKeys.newConfigKey(new TypeToken<Sensor<?>>() {}, "enricher.sourceSensor");
@SetFromFlag("transformation")
public static final ConfigKey<Object> TRANSFORMATION_UNTYPED = ConfigKeys.newConfigKey(Object.class, "enricher.transformation.untyped",
"Specifies a transformation, as a function from a collection to the value, or as a string matching a pre-defined named transformation, "
+ "such as 'average' (for numbers), 'sum' (for numbers), or 'list' (the default, putting any collection of items into a list)");
public static final ConfigKey<Function<? super Collection<?>, ?>> TRANSFORMATION = ConfigKeys.newConfigKey(new TypeToken<Function<? super Collection<?>, ?>>() {}, "enricher.transformation");
public static final ConfigKey<Boolean> EXCLUDE_BLANK = ConfigKeys.newBooleanConfigKey("enricher.aggregator.excludeBlank", "Whether explicit nulls or blank strings should be excluded (default false); this only applies if no value filter set", false);
/**
* @see QuorumChecks
*/
public static final ConfigKey<String> QUORUM_CHECK_TYPE = ConfigKeys.newStringConfigKey("quorum.check.type", "The requirement to be considered quorate -- possible values: 'all', 'allAndAtLeastOne', 'atLeastOne', 'atLeastOneUnlessEmpty', 'alwaysHealthy'", "allAndAtLeastOne");
public static final ConfigKey<Integer> QUORUM_TOTAL_SIZE = ConfigKeys.newIntegerConfigKey("quorum.total.size", "The total size to consider when determining if quorate", 1);
protected Sensor<T> sourceSensor;
protected Function<? super Collection<T>, ? extends U> transformation;
/**
* Users of values should either on it synchronize when iterating over its entries or use
* copyOfValues to obtain an immutable copy of the map.
*/
// We use a synchronizedMap over a ConcurrentHashMap for entities that store null values.
protected final Map<Entity, T> values = Collections.synchronizedMap(new LinkedHashMap<Entity, T>());
public Aggregator() {}
@SuppressWarnings("unchecked")
protected void setEntityLoadingConfig() {
super.setEntityLoadingConfig();
this.sourceSensor = (Sensor<T>) getRequiredConfig(SOURCE_SENSOR);
this.transformation = (Function<? super Collection<T>, ? extends U>) config().get(TRANSFORMATION);
Object t1 = config().get(TRANSFORMATION_UNTYPED);
Function<? super Collection<?>, ?> t2 = null;
if (t1 instanceof String) {
t2 = lookupTransformation((String)t1);
if (t2==null) {
LOG.warn("Unknown transformation '"+t1+"' for "+this+"; will use default transformation");
}
}
if (this.transformation==null) {
this.transformation = (Function<? super Collection<T>, ? extends U>) t2;
} else if (t1!=null && !Objects.equals(t2, this.transformation)) {
throw new IllegalStateException("Cannot supply both "+TRANSFORMATION_UNTYPED+" and "+TRANSFORMATION+" unless they are equal.");
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
protected Function<? super Collection<?>, ?> lookupTransformation(String t1) {
if ("average".equalsIgnoreCase(t1)) return new Enrichers.ComputingAverage(null, null, targetSensor.getTypeToken());
if ("sum".equalsIgnoreCase(t1)) return new Enrichers.ComputingSum(null, null, targetSensor.getTypeToken());
if ("isQuorate".equalsIgnoreCase(t1)) return new Enrichers.ComputingIsQuorate(targetSensor.getTypeToken(),
QuorumChecks.of(config().get(QUORUM_CHECK_TYPE)), config().get(QUORUM_TOTAL_SIZE));
if ("list".equalsIgnoreCase(t1)) return new ComputingList();
return null;
}
private class ComputingList<TT> implements Function<Collection<TT>, List<TT>> {
@Override
public List<TT> apply(Collection<TT> input) {
if (input==null) return null;
return MutableList.copyOf(input).asUnmodifiable();
}
}
@Override
protected void setEntityBeforeSubscribingProducerChildrenEvents() {
BrooklynLogging.log(LOG, BrooklynLogging.levelDebugOrTraceIfReadOnly(producer),
"{} subscribing to children of {}", this, producer);
subscriptions().subscribeToChildren(producer, sourceSensor, this);
}
@Override
protected void addProducerHardcoded(Entity producer) {
subscriptions().subscribe(producer, sourceSensor, this);
onProducerAdded(producer);
}
@Override
protected void addProducerChild(Entity producer) {
// no subscription needed here, due to the subscribeToChildren call
onProducerAdded(producer);
}
@Override
protected void addProducerMember(Entity producer) {
subscriptions().subscribe(producer, sourceSensor, this);
onProducerAdded(producer);
}
@Override
protected void onProducerAdded(Entity producer) {
BrooklynLogging.log(LOG, BrooklynLogging.levelDebugOrTraceIfReadOnly(producer),
"{} listening to {}", this, producer);
synchronized (values) {
T vo = values.get(producer);
if (vo==null) {
T initialVal;
if (sourceSensor instanceof AttributeSensor) {
initialVal = producer.getAttribute((AttributeSensor<T>)sourceSensor);
} else {
initialVal = null;
}
values.put(producer, initialVal != null ? initialVal : defaultMemberValue);
//we might skip in onEvent in the short window while !values.containsKey(producer)
//but that's okay because the put which would have been done there is done here now
} else {
//vo will be null unless some weird race with addProducer+removeProducer is occuring
//(and that's something we can tolerate i think)
if (LOG.isDebugEnabled()) LOG.debug("{} already had value ({}) for producer ({}); but that producer has just been added", new Object[] {this, vo, producer});
}
}
}
@Override
protected Predicate<?> getDefaultValueFilter() {
if (getConfig(EXCLUDE_BLANK))
return StringPredicates.isNonBlank();
else
return Predicates.alwaysTrue();
}
@Override
protected void onProducerRemoved(Entity producer) {
values.remove(producer);
onUpdated();
}
@Override
public void onEvent(SensorEvent<T> event) {
Entity e = event.getSource();
synchronized (values) {
if (values.containsKey(e)) {
values.put(e, event.getValue());
} else {
if (LOG.isDebugEnabled()) LOG.debug("{} received event for unknown producer ({}); presumably that producer has recently been removed", this, e);
}
}
onUpdated();
}
protected void onUpdated() {
try {
emit(targetSensor, compute());
} catch (Throwable t) {
LOG.warn("Error calculating and setting aggregate for enricher "+this, t);
throw Exceptions.propagate(t);
}
}
@Override
protected Object compute() {
synchronized (values) {
// TODO Could avoid copying when filter not needed
List<T> vs = MutableList.copyOf(Iterables.filter(values.values(), valueFilter));
if (transformation==null) return vs;
return transformation.apply(vs);
}
}
protected Map<Entity, T> copyOfValues() {
// Don't use ImmutableMap, as can contain null values
synchronized (values) {
return Collections.unmodifiableMap(MutableMap.copyOf(values));
}
}
}