/* * 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.sensor; import static com.google.common.base.Preconditions.checkNotNull; import java.util.Collection; import java.util.Collections; import java.util.Map; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.sensor.AttributeSensor; import org.apache.brooklyn.core.BrooklynLogging; import org.apache.brooklyn.core.entity.AbstractEntity; import org.apache.brooklyn.util.core.flags.TypeCoercions; import org.apache.brooklyn.util.guava.Maybe; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; /** * A {@link Map} of {@link Entity} attribute values. */ public final class AttributeMap { static final Logger log = LoggerFactory.getLogger(AttributeMap.class); private static enum Marker { NULL; } private final AbstractEntity entity; // Assumed to be something like a ConcurrentMap passed in. private final Map<Collection<String>, Object> values; /** * Creates a new AttributeMap. * * @param entity the EntityLocal this AttributeMap belongs to. * @throws NullPointerException if entity is null */ public AttributeMap(AbstractEntity entity) { // Not using ConcurrentMap, because want to (continue to) allow null values. // Could use ConcurrentMapAcceptingNullVals (with the associated performance hit on entrySet() etc). this(entity, Collections.synchronizedMap(Maps.<Collection<String>, Object>newLinkedHashMap())); } /** * Creates a new AttributeMap. * * @param entity the EntityLocal this AttributeMap belongs to. * @param storage the Map in which to store the values - should be concurrent or synchronized. * @throws NullPointerException if entity is null */ public AttributeMap(AbstractEntity entity, Map<Collection<String>, Object> storage) { this.entity = checkNotNull(entity, "entity must be specified"); this.values = checkNotNull(storage, "storage map must not be null"); } public Map<Collection<String>, Object> asRawMap() { synchronized (values) { return ImmutableMap.copyOf(values); } } public Map<String, Object> asMap() { Map<String, Object> result = Maps.newLinkedHashMap(); synchronized (values) { for (Map.Entry<Collection<String>, Object> entry : values.entrySet()) { String sensorName = Joiner.on('.').join(entry.getKey()); Object val = (isNull(entry.getValue())) ? null : entry.getValue(); result.put(sensorName, val); } } return result; } /** * Updates the value. * * @param path the path to the value. * @param newValue the new value * @return the old value. * @throws IllegalArgumentException if path is null or empty */ // TODO path must be ordered(and legal to contain duplicates like "a.b.a"; list would be better public <T> T update(Collection<String> path, T newValue) { checkPath(path); if (newValue == null) { newValue = typedNull(); } if (log.isTraceEnabled()) { log.trace("setting sensor {}={} for {}", new Object[] {path, newValue, entity}); } @SuppressWarnings("unchecked") T oldValue = (T) values.put(path, newValue); return (isNull(oldValue)) ? null : oldValue; } private void checkPath(Collection<String> path) { Preconditions.checkNotNull(path, "path can't be null"); Preconditions.checkArgument(!path.isEmpty(), "path can't be empty"); } public <T> T update(AttributeSensor<T> attribute, T newValue) { T oldValue = updateWithoutPublishing(attribute, newValue); entity.emitInternal(attribute, newValue); return oldValue; } public <T> T updateWithoutPublishing(AttributeSensor<T> attribute, T newValue) { if (log.isTraceEnabled()) { Object oldValue = getValue(attribute); if (!Objects.equal(oldValue, newValue != null)) { log.trace("setting attribute {} to {} (was {}) on {}", new Object[] {attribute.getName(), newValue, oldValue, entity}); } else { log.trace("setting attribute {} to {} (unchanged) on {}", new Object[] {attribute.getName(), newValue, this}); } } T oldValue = (T) update(attribute.getNameParts(), newValue); return (isNull(oldValue)) ? null : oldValue; } /** * Where atomicity is desired, the methods in this class synchronize on the {@link #values} map. */ public <T> T modify(AttributeSensor<T> attribute, Function<? super T, Maybe<T>> modifier) { synchronized (values) { T oldValue = getValue(attribute); Maybe<? extends T> newValue = modifier.apply(oldValue); if (newValue.isPresent()) { if (log.isTraceEnabled()) log.trace("modified attribute {} to {} (was {}) on {}", new Object[] {attribute.getName(), newValue, oldValue, entity}); return update(attribute, newValue.get()); } else { if (log.isTraceEnabled()) log.trace("modified attribute {} unchanged; not emitting on {}", new Object[] {attribute.getName(), newValue, this}); return oldValue; } } } public void remove(AttributeSensor<?> attribute) { BrooklynLogging.log(log, BrooklynLogging.levelDebugOrTraceIfReadOnly(entity), "removing attribute {} on {}", attribute.getName(), entity); remove(attribute.getNameParts()); } // TODO path must be ordered(and legal to contain duplicates like "a.b.a"; list would be better public void remove(Collection<String> path) { checkPath(path); if (log.isTraceEnabled()) { log.trace("removing sensor {} for {}", new Object[] {path, entity}); } values.remove(path); } /** * Gets the value * * @param path the path of the value to get * @return the value * @throws IllegalArgumentException path is null or empty. */ public Object getValue(Collection<String> path) { // TODO previously this would return a map of the sub-tree if the path matched a prefix of a group of sensors, // or the leaf value if only one value. Arguably that is not required - what is/was the use-case? // checkPath(path); Object result = values.get(path); return (isNull(result)) ? null : result; } @SuppressWarnings("unchecked") public <T> T getValue(AttributeSensor<T> sensor) { return (T) TypeCoercions.coerce(getValue(sensor.getNameParts()), sensor.getType()); } @SuppressWarnings("unchecked") private <T> T typedNull() { return (T) Marker.NULL; } private boolean isNull(Object t) { return t == Marker.NULL; } }