/*
* 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.Map;
import org.apache.brooklyn.api.entity.EntityLocal;
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.config.ConfigKeys;
import org.apache.brooklyn.core.enricher.AbstractEnricher;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.flags.SetFromFlag;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.reflect.TypeToken;
/**
* Enricher which updates an entry in a sensor map ({@link #TARGET_SENSOR})
* based on the value of another sensor ({@link #SOURCE_SENSOR}.
* <p>
* The key used defaults to the name of the source sensor but can be specified with {@link #KEY_IN_TARGET_SENSOR}.
* The value placed in the map is the result of applying the function in {@link #COMPUTING} to the sensor value,
* with default behaviour being to remove an entry if <code>null</code> is returned
* but this can be overriden by setting {@link #REMOVING_IF_RESULT_IS_NULL} false.
* {@link Entities#REMOVE} and {@link Entities#UNCHANGED} are also respeced as return values for the computation
* (ignoring generics).
* Unlike most other enrichers, this defaults to {@link AbstractEnricher#SUPPRESS_DUPLICATES} being true
*
* @author alex
*
* @param <S> source sensor type
* @param <TKey> key type in target sensor map
* @param <TVal> value type in target sensor map
*/
@SuppressWarnings("serial")
public class UpdatingMap<S,TKey,TVal> extends AbstractEnricher implements SensorEventListener<S> {
private static final Logger LOG = LoggerFactory.getLogger(UpdatingMap.class);
@SetFromFlag("fromSensor")
public static final ConfigKey<Sensor<?>> SOURCE_SENSOR = ConfigKeys.newConfigKey(new TypeToken<Sensor<?>>() {}, "enricher.sourceSensor");
@SetFromFlag("targetSensor")
public static final ConfigKey<Sensor<?>> TARGET_SENSOR = ConfigKeys.newConfigKey(new TypeToken<Sensor<?>>() {}, "enricher.targetSensor");
@SetFromFlag("key")
public static final ConfigKey<?> KEY_IN_TARGET_SENSOR = ConfigKeys.newConfigKey(Object.class, "enricher.updatingMap.keyInTargetSensor",
"Key to update in the target sensor map, defaulting to the name of the source sensor");
@SetFromFlag("computing")
public static final ConfigKey<Function<?, ?>> COMPUTING = ConfigKeys.newConfigKey(new TypeToken<Function<?,?>>() {}, "enricher.updatingMap.computing");
@SetFromFlag("removingIfResultIsNull")
public static final ConfigKey<Boolean> REMOVING_IF_RESULT_IS_NULL = ConfigKeys.newBooleanConfigKey("enricher.updatingMap.removingIfResultIsNull",
"Whether the key in the target map is removed if the result if the computation is null");
protected AttributeSensor<S> sourceSensor;
protected AttributeSensor<Map<TKey,TVal>> targetSensor;
protected TKey key;
protected Function<S,? extends TVal> computing;
protected Boolean removingIfResultIsNull;
public UpdatingMap() {
this(Maps.newLinkedHashMap());
}
public UpdatingMap(Map<Object, Object> flags) {
super(flags);
}
@Override
public void init() {
super.init();
// this always suppresses duplicates, but it updates the same map *in place* so the usual suppress duplicates logic should not be applied
// TODO clean up so that we have synchronization guarantees and can inspect the item to see whether it has changed
if (Boolean.TRUE.equals(getConfig(SUPPRESS_DUPLICATES))) {
LOG.warn("suppress-duplicates must not be set on "+this+" because map is updated in-place; unsetting config; will always implicitly suppress duplicates");
config().set(SUPPRESS_DUPLICATES, (Boolean)null);
}
}
@Override
protected <T> void doReconfigureConfig(ConfigKey<T> key, T val) {
if (key.getName().equals(SUPPRESS_DUPLICATES.getName())) {
if (Boolean.TRUE.equals(val)) {
throw new UnsupportedOperationException("suppress-duplicates must not be set on "+this+" because map is updated in-place; will always implicitly suppress duplicates");
}
}
super.doReconfigureConfig(key, val);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public void setEntity(EntityLocal entity) {
super.setEntity(entity);
this.sourceSensor = (AttributeSensor<S>) getRequiredConfig(SOURCE_SENSOR);
this.targetSensor = (AttributeSensor<Map<TKey,TVal>>) getRequiredConfig(TARGET_SENSOR);
this.key = (TKey) getConfig(KEY_IN_TARGET_SENSOR);
this.computing = (Function) getRequiredConfig(COMPUTING);
this.removingIfResultIsNull = getConfig(REMOVING_IF_RESULT_IS_NULL);
subscriptions().subscribe(ImmutableMap.of("notifyOfInitialValue", true), entity, sourceSensor, this);
}
@Override
public void onEvent(SensorEvent<S> event) {
onUpdated();
}
/**
* Called whenever the values for the set of producers changes (e.g. on an event, or on a member added/removed).
*/
@SuppressWarnings("unchecked")
protected void onUpdated() {
try {
Object v = computing.apply(entity.getAttribute(sourceSensor));
if (v == null && !Boolean.FALSE.equals(removingIfResultIsNull)) {
v = Entities.REMOVE;
}
if (v == Entities.UNCHANGED) {
// nothing
} else {
// TODO check synchronization
TKey key = this.key;
if (key==null) key = (TKey) sourceSensor.getName();
Map<TKey, TVal> map = entity.getAttribute(targetSensor);
boolean created = (map==null);
if (created) map = MutableMap.of();
boolean changed;
if (v == Entities.REMOVE) {
changed = map.containsKey(key);
if (changed)
map.remove(key);
} else {
TVal oldV = map.get(key);
if (oldV==null)
changed = (v!=null || !map.containsKey(key));
else
changed = !oldV.equals(v);
if (changed)
map.put(key, (TVal)v);
}
if (changed || created)
emit(targetSensor, map);
}
} catch (Throwable t) {
LOG.warn("Error calculating map update for enricher "+this, t);
throw Exceptions.propagate(t);
}
}
}