/* * 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.config; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import org.apache.brooklyn.api.mgmt.ExecutionContext; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.internal.AbstractStructuredConfigKey; import org.apache.brooklyn.util.collections.Jsonya; import org.apache.brooklyn.util.collections.MutableMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Supplier; import com.google.common.collect.Maps; /** A config key which represents a map, where contents can be accessed directly via subkeys. * Items added directly to the map must be of type map, and can be updated by: * <ul> * <li>Putting individual subkeys ({@link SubElementConfigKey}) * <li>Passing an an appropriate {@link MapModification} from {@link MapModifications} * to clear, clear-and-set, or update * <li>Setting a value against a dot-extension of the key * (e.g. setting <code>a.map.subkey=1</code> will cause getConfig(a.map[type=MapConfigKey]) * to return {subkey=1}; but note the above are preferred where possible) * <li>Setting a map directly against the MapConfigKey (but note that the above are preferred where possible) * </ul> */ //TODO Create interface public class MapConfigKey<V> extends AbstractStructuredConfigKey<Map<String,V>,Map<String,Object>,V> { private static final long serialVersionUID = -6126481503795562602L; private static final Logger log = LoggerFactory.getLogger(MapConfigKey.class); public MapConfigKey(Class<V> subType, String name) { this(subType, name, name, null); } public MapConfigKey(Class<V> subType, String name, String description) { this(subType, name, description, null); } // TODO it isn't clear whether defaultValue is an initialValue, or a value to use when map is empty // probably the latter, currently ... but maybe better to say that map configs are never null, // and defaultValue is really an initial value? @SuppressWarnings({ "unchecked", "rawtypes" }) public MapConfigKey(Class<V> subType, String name, String description, Map<String, V> defaultValue) { super((Class)Map.class, subType, name, description, defaultValue); } public ConfigKey<V> subKey(String subName) { return super.subKey(subName); } public ConfigKey<V> subKey(String subName, String description) { return super.subKey(subName, description); } @SuppressWarnings("unchecked") @Override protected Map<String, Object> extractValueMatchingThisKey(Object potentialBase, ExecutionContext exec, boolean coerce) throws InterruptedException, ExecutionException { if (coerce) { potentialBase = resolveValue(potentialBase, exec); } if (potentialBase==null) return null; if (potentialBase instanceof Map<?,?>) { return Maps.<String,Object>newLinkedHashMap( (Map<String,Object>) potentialBase); } log.warn("Unable to extract "+getName()+" as Map; it is "+potentialBase.getClass().getName()+" "+potentialBase); return null; } @Override protected Map<String, Object> merge(Map<String, Object> base, Map<String, Object> subkeys, boolean unmodifiable) { Map<String, Object> result = MutableMap.copyOf(base).add(subkeys); if (unmodifiable) result = Collections.unmodifiableMap(result); return result; } @SuppressWarnings({ "rawtypes", "unchecked" }) @Override public Object applyValueToMap(Object value, Map target) { if (value == null) return null; if (value instanceof StructuredModification) return ((StructuredModification)value).applyToKeyInMap(this, target); if (value instanceof Map.Entry) return applyEntryValueToMap((Map.Entry)value, target); if (!(value instanceof Map)) throw new IllegalArgumentException("Cannot set non-map entries "+value+" on "+this); Map result = new MutableMap(); for (Object entry: ((Map)value).entrySet()) { Map.Entry entryT = (Map.Entry)entry; result.put(entryT.getKey(), applyEntryValueToMap(entryT, target)); } if (((Map)value).isEmpty() && !isSet(target)) target.put(this, MutableMap.of()); return result; } @SuppressWarnings({ "rawtypes", "unchecked" }) protected Object applyEntryValueToMap(Entry value, Map target) { Object k = value.getKey(); if (acceptsSubkeyStronglyTyped(k)) { // do nothing } else if (k instanceof ConfigKey<?>) { k = subKey( ((ConfigKey<?>)k).getName() ); } else if (k instanceof String) { k = subKey((String)k); } else { // supplier or other unexpected value if (k instanceof Supplier) { Object mapAtRoot = target.get(this); if (mapAtRoot==null) { mapAtRoot = new LinkedHashMap(); target.put(this, mapAtRoot); } // TODO above is not thread-safe, and below is assuming synching on map // is the best way to prevent CME's, which is often but not always true if (mapAtRoot instanceof Map) { if (mapAtRoot instanceof ConcurrentMap) { return ((Map)mapAtRoot).put(k, value.getValue()); } else { synchronized (mapAtRoot) { return ((Map)mapAtRoot).put(k, value.getValue()); } } } } log.warn("Unexpected subkey "+k+" being inserted into "+this+"; ignoring"); k = null; } if (k!=null) return target.put(k, value.getValue()); else return null; } public interface MapModification<V> extends StructuredModification<MapConfigKey<V>>, Map<String,V> { } public static class MapModifications extends StructuredModifications { /** when passed as a value to a MapConfigKey, causes each of these items to be put * (this Mod is redundant as no other value is really sensible) */ public static final <V> MapModification<V> put(final Map<String,V> itemsToPutInMapReplacing) { return new MapModificationBase<V>(itemsToPutInMapReplacing, false); } /** when passed as a value to a MapConfigKey, causes the map to be cleared and these items added */ public static final <V> MapModification<V> set(final Map<String,V> itemsToPutInMapAfterClearing) { return new MapModificationBase<V>(itemsToPutInMapAfterClearing, true); } /** when passed as a value to a MapConfigKey, causes the items to be added to the underlying map * using {@link Jsonya} add semantics (combining maps and lists) */ public static final <V> MapModification<V> add(final Map<String,V> itemsToAdd) { return new MapModificationBase<V>(itemsToAdd, false /* ignored */) { private static final long serialVersionUID = 1L; @SuppressWarnings("rawtypes") @Override public Object applyToKeyInMap(MapConfigKey<V> key, Map target) { return key.applyValueToMap(Jsonya.of(key.rawValue(target)).add(this).getRootMap(), target); } }; } } public static class MapModificationBase<V> extends LinkedHashMap<String,V> implements MapModification<V> { private static final long serialVersionUID = -1670820613292286486L; private final boolean clearFirst; public MapModificationBase(Map<String,V> delegate, boolean clearFirst) { super(delegate); this.clearFirst = clearFirst; } @SuppressWarnings({ "rawtypes" }) @Override public Object applyToKeyInMap(MapConfigKey<V> key, Map target) { if (clearFirst) { StructuredModification<StructuredConfigKey> clearing = StructuredModifications.clearing(); clearing.applyToKeyInMap(key, target); } return key.applyValueToMap(new LinkedHashMap<String,V>(this), target); } } }