blob: e2bd4a20a1019143d9d72ee97c8a718ae1e7b018 [file] [log] [blame]
/*
* 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.lang.reflect.Field;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.brooklyn.config.ConfigInheritance;
import org.apache.brooklyn.config.ConfigInheritances;
import org.apache.brooklyn.config.ConfigInheritances.BasicConfigValueAtContainer;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.config.ConfigValueAtContainer;
import org.apache.brooklyn.util.collections.CollectionMerger;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.exceptions.ReferenceWithError;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@SuppressWarnings("serial")
public class BasicConfigInheritance implements ConfigInheritance {
private static final Logger log = LoggerFactory.getLogger(BasicConfigInheritance.class);
private static final long serialVersionUID = -5916548049057961051L;
public static final String CONFLICT_RESOLUTION_STRATEGY_DEEP_MERGE = "deep_merge";
public static final String CONFLICT_RESOLUTION_STRATEGY_OVERWRITE = "overwrite";
/** This class allows us to have constant instances of {@link ConfigInheritance}
* which have no internal fields (ie they don't extend {@link BasicConfigInheritance})
* so are pretty on serialization, but otherwise act identically. */
public static abstract class DelegatingConfigInheritance implements ConfigInheritance {
protected abstract ConfigInheritance getDelegate();
@Override @Deprecated public InheritanceMode isInherited(ConfigKey<?> key, Object from, Object to) {
return getDelegate().isInherited(key, from, to);
}
@Override public <TContainer, TValue> boolean isReinheritable(ConfigValueAtContainer<TContainer, TValue> parent, ConfigInheritanceContext context) {
return getDelegate().isReinheritable(parent, context);
}
@Override public <TContainer, TValue> boolean considerParent(ConfigValueAtContainer<TContainer, TValue> local, ConfigValueAtContainer<TContainer, TValue> parent, ConfigInheritanceContext context) {
return getDelegate().considerParent(local, parent, context);
}
@Override
public <TContainer, TValue> ReferenceWithError<ConfigValueAtContainer<TContainer, TValue>> resolveWithParent(ConfigValueAtContainer<TContainer, TValue> local, ConfigValueAtContainer<TContainer, TValue> resolvedParent, ConfigInheritanceContext context) {
return getDelegate().resolveWithParent(local, resolvedParent, context);
}
@Override
public boolean equals(Object obj) {
return super.equals(obj) || getDelegate().equals(obj);
}
@Override
public int hashCode() {
return getDelegate().hashCode();
}
// standard deserialization method
protected ConfigInheritance readResolve() {
return returnEquivalentConstant(this);
}
}
private static ConfigInheritance returnEquivalentConstant(ConfigInheritance candidate) {
for (ConfigInheritance knownMode: Arrays.asList(
NOT_REINHERITED, NOT_REINHERITED_ELSE_DEEP_MERGE, NEVER_INHERITED, OVERWRITE, BasicConfigInheritance.DEEP_MERGE)) {
if (candidate.equals(knownMode)) return knownMode;
}
if (candidate.equals(NEVER_INHERITED_OLD)) {
return NEVER_INHERITED;
}
return candidate;
}
/*
* use of delegate is so that stateless classes can be defined to make the serialization nice,
* both the name and hiding the implementation detail (also making it easier for that detail to change);
* with aliased type names the field names here could even be aliases for the type names.
* (we could alternatively have an "alias-for-final-instance" mode in serialization,
* to optimize where we know that a java instance is final.)
*/
private static class NotReinherited extends DelegatingConfigInheritance {
final static BasicConfigInheritance DELEGATE = new BasicConfigInheritance(false, CONFLICT_RESOLUTION_STRATEGY_OVERWRITE, false, true);
@Override protected ConfigInheritance getDelegate() { return DELEGATE; }
}
/** Indicates that a config key value should not be passed down from a container where it is defined.
* Unlike {@link #NEVER_INHERITED} these values can be passed down if set as anonymous keys at a container
* (ie the container does not expect it) to a container which does expect it, but it will not be passed down further.
* If the inheritor also defines a value the parent's value is ignored irrespective
* (as in {@link #OVERWRITE}; see {@link #NOT_REINHERITED_ELSE_DEEP_MERGE} if merging is desired). */
public static final ConfigInheritance NOT_REINHERITED = new NotReinherited();
private static class NotReinheritedElseDeepMerge extends DelegatingConfigInheritance {
final static BasicConfigInheritance DELEGATE = new BasicConfigInheritance(false, CONFLICT_RESOLUTION_STRATEGY_DEEP_MERGE, false, true);
@Override protected ConfigInheritance getDelegate() { return DELEGATE; }
}
/** As {@link #NOT_REINHERITED} but in cases where a value is inherited because a parent did not recognize it,
* if the inheritor also defines a value the two values should be merged. */
public static final ConfigInheritance NOT_REINHERITED_ELSE_DEEP_MERGE = new NotReinheritedElseDeepMerge();
private static class NeverInherited extends DelegatingConfigInheritance {
final static BasicConfigInheritance DELEGATE = new BasicConfigInheritance(false, CONFLICT_RESOLUTION_STRATEGY_OVERWRITE, true, false);
@Override protected ConfigInheritance getDelegate() { return DELEGATE; }
}
/** Indicates that a key's value should never be inherited, even if inherited from a value set on a container that does not know the key.
* (Many usages will prefer {@link #NOT_REINHERITED}, to allow unknown dynamic keys set in an ancestor accessible from descendants.) */
public static final ConfigInheritance NEVER_INHERITED = new NeverInherited();
/** In case we deserialize an old copy; the last arg (ancestor inherit default) is irrelevant
* so accept this as a synonym for {@link #NEVER_INHERITED}. */
private static final ConfigInheritance NEVER_INHERITED_OLD = new BasicConfigInheritance(false, CONFLICT_RESOLUTION_STRATEGY_OVERWRITE, true, true);
private static class Overwrite extends DelegatingConfigInheritance {
final static BasicConfigInheritance DELEGATE = new BasicConfigInheritance(true, CONFLICT_RESOLUTION_STRATEGY_OVERWRITE, false, true);
@Override protected ConfigInheritance getDelegate() { return DELEGATE; }
}
/** Indicates that if a key has a value at both an ancestor and a descendant, the descendant and his descendants
* will prefer the value at the descendant. */
public static final ConfigInheritance OVERWRITE = new Overwrite();
private static class DeepMerge extends DelegatingConfigInheritance {
final static BasicConfigInheritance DELEGATE = new BasicConfigInheritance(true, CONFLICT_RESOLUTION_STRATEGY_DEEP_MERGE, false, true);
@Override protected ConfigInheritance getDelegate() { return DELEGATE; }
}
/** Indicates that if a key has a value at both an ancestor and a descendant, the descendant and his descendants
* should attempt to merge the values. If the values are not mergeable behaviour is undefined
* (and often the descendant's value will simply overwrite). */
public static final ConfigInheritance DEEP_MERGE = new DeepMerge();
// support conversion from these legacy fields
@SuppressWarnings("deprecation")
private static void registerReplacements() {
ConfigInheritance.Legacy.registerReplacement(ConfigInheritance.DEEP_MERGE, DEEP_MERGE);
ConfigInheritance.Legacy.registerReplacement(ConfigInheritance.ALWAYS, OVERWRITE);
ConfigInheritance.Legacy.registerReplacement(ConfigInheritance.NONE, NOT_REINHERITED);
}
static { registerReplacements(); }
/** whether a value on a key defined locally should be inheritable by descendants.
* if false at a point where a key is defined,
* children/descendants/inheritors will not be able to see its value, whether explicit or default.
* default true: things are normally reinherited.
* <p>
* note that this only takes effect where a key is defined locally.
* if a key is not defined at an ancestor, a descendant setting this value false will not prevent it
* from inheriting values from ancestors.
* <p>
* typical use case for setting this to false is where a key is consumed and descendants should not
* "reconsume" it. for example setting files to install on a VM need only be applied once,
* and if it has <b>runtime management</b> hierarchy descendants which also understand that field they
* should not install the same files.
* (there is normally no reason to set this false in the context of <b>type hierarchy</b> inheritance because
* an ancestor type cannot "consume" a value.) */
protected final boolean isReinherited;
/** a symbol indicating a conflict-resolution-strategy understood by the implementation.
* in {@link BasicConfigInheritance} supported values are
* {@link #CONFLICT_RESOLUTION_STRATEGY_DEEP_MERGE} and {@link #CONFLICT_RESOLUTION_STRATEGY_OVERWRITE}.
* subclasses may pass null or a different string if they provide a custom implementation
* of {@link #resolveWithParentCustomStrategy(ConfigValueAtContainer, ConfigValueAtContainer, org.apache.brooklyn.config.ConfigInheritance.ConfigInheritanceContext)} */
@Nullable protected final String conflictResolutionStrategy;
/** @deprecated since 0.11.0 when this was introduced, now renamed {@link #localDefaultResolvesWithAncestorValue} */
@Deprecated protected final Boolean useLocalDefaultValue;
/** whether a local default value should be considered for resolution in the presence of an ancestor value.
* can use true with {@link #CONFLICT_RESOLUTION_STRATEGY_OVERWRITE} to mean don't inherit,
* or true with {@link #CONFLICT_RESOLUTION_STRATEGY_DEEP_MERGE} to mean local default merged on top of inherited
* (but be careful here, if local default is null in a merge it will delete ancestor values).
* <p>
* in most cases this is false, meaning a default value is ignored if the parent has a value,
* but it can be used when a key supplies a default which should conflict-resolve with an ancestor value:
* a trivial example is when not reinheriting, a default should conflict-resolve (overwriting) an explicit ancestor value.
* more interesting potentially, this could indicate
* that a default value is being introduced which should be merged/combined with ancestors;
* we don't use this (config inheritance is complex enough and we don't have a compelling use case
* to expose more complexity to users) but having this as a concept, and the related
* {@link #ancestorDefaultInheritable} specifying (in this case) whether a local default should
* resolve/merge/combine with ancestor defaults in addition to ancestor explicit values,
* means the logic in this refers to the right control dimensions rather than taking shortcuts.
* <p>
* null should not be used. a boxed object is taken (as opposed to a primitive boolean) only in order to support migration.
*/
@Nonnull
protected final Boolean localDefaultResolvesWithAncestorValue;
/** whether a default set in an ancestor container's key definition will be considered as the
* local default value at descendants who don't define any other value (nothing set locally and local default is null).
* <p>
* if true (now the usual behaviour), if an ancestor defines a default and a descendant doesn't, the ancestor's value will be taken as a default.
* if it is also the case that localDefaultResolvesWithAncestorValue is true at the <i>ancestor</i> then a descendant who
* defines a local default value (with this field true) will have its conflict resolution strategy
* applied with the ancestor's default value.
* <p>
* if this is false, ancestor defaults are completely ignored; prior to 0.11.0 this was the normal behaviour,
* but it caused surprises where default values in parameters did not take effect.
* <p>
* as currently we only really use {@link #localDefaultResolvesWithAncestorValue} true when we
* wish to block reinheritance, this is false in that case,
* and true in all other cases (where we wish to have ancestor inheritance, but only if there
* is no local value that should trump);
* however logically there are other possibilities, such as a local default expecting to
* merge/combine with an ancestor value, in which case we'd probably want this to be true
* to indicate the local default should combine with the ancestor default,
* but it could also make sense for this to be false, meaning the local default would
* merge with an explicit ancestor value but not with an ancestor default
* (see javadoc on {@link #localDefaultResolvesWithAncestorValue} for more info).
* <p>
* it gets complex and we've tried to simplify the config modes that we actually use,
* but have made efforts in the code to account for the different logical possibilities,
* hence the complexity of this.
* <p>
* null should not be used. a boxed object is taken (as opposed to a primitive boolean) only in order to support migration.
*/
@Nonnull
protected final Boolean ancestorDefaultInheritable;
@Deprecated /** @deprecated since 0.11.0 use four-arg constructor */
protected BasicConfigInheritance(boolean isReinherited, @Nullable String conflictResolutionStrategy, boolean localDefaultResolvesWithAncestorValue) {
this(isReinherited, conflictResolutionStrategy, localDefaultResolvesWithAncestorValue, true);
}
protected BasicConfigInheritance(boolean isReinherited, @Nullable String conflictResolutionStrategy, boolean localDefaultResolvesWithAncestorValue, boolean ancestorDefaultInheritable) {
super();
this.isReinherited = isReinherited;
this.conflictResolutionStrategy = conflictResolutionStrategy;
this.useLocalDefaultValue = null;
this.localDefaultResolvesWithAncestorValue = localDefaultResolvesWithAncestorValue;
this.ancestorDefaultInheritable = ancestorDefaultInheritable;
}
@Override @Deprecated
public InheritanceMode isInherited(ConfigKey<?> key, Object from, Object to) {
return null;
}
protected <TContainer, TValue> void checkInheritanceContext(ConfigValueAtContainer<TContainer, TValue> local, ConfigInheritanceContext context) {
ConfigInheritance rightInheritance = ConfigInheritances.findInheritance(local, context, this);
if (!isSameRootInstanceAs(rightInheritance)) {
throw new IllegalStateException("Low level inheritance computation error: caller should invoke on "+rightInheritance+" "
+ "(the inheritance at "+local+"), not "+this);
}
}
private boolean isSameRootInstanceAs(ConfigInheritance other) {
if (other==null) return false;
if (this==other) return true;
// we should consider the argument the same if it is a delegate to us,
// e.g. for when this method is invoked by the delegate comparing against its definition
if (other instanceof DelegatingConfigInheritance) return isSameRootInstanceAs( ((DelegatingConfigInheritance)other).getDelegate() );
return false;
}
@Override
public <TContainer, TValue> boolean isReinheritable(ConfigValueAtContainer<TContainer, TValue> parent, ConfigInheritanceContext context) {
checkInheritanceContext(parent, context);
return isReinherited();
}
@Override
public <TContainer,TValue> boolean considerParent(
ConfigValueAtContainer<TContainer,TValue> local,
ConfigValueAtContainer<TContainer,TValue> parent,
ConfigInheritanceContext context) {
checkInheritanceContext(local, context);
if (parent==null) return false;
if (CONFLICT_RESOLUTION_STRATEGY_OVERWRITE.equals(conflictResolutionStrategy)) {
// overwrite means ignore if there's an explicit value, or we're using the local default
return !local.isValueExplicitlySet() && !getLocalDefaultResolvesWithAncestorValue();
}
return true;
}
@Override
public <TContainer,TValue> ReferenceWithError<ConfigValueAtContainer<TContainer,TValue>> resolveWithParent(
ConfigValueAtContainer<TContainer,TValue> local,
ConfigValueAtContainer<TContainer,TValue> parent,
ConfigInheritanceContext context) {
checkInheritanceContext(local, context);
if (!parent.isValueExplicitlySet() && !getLocalDefaultResolvesWithAncestorValue()) {
if (getAncestorDefaultInheritable() && !local.isValueExplicitlySet() && local.getDefaultValue().isAbsentOrNull() && parent.getDefaultValue().isPresentAndNonNull()) {
return ReferenceWithError.newInstanceWithoutError(new BasicConfigValueAtContainer<TContainer,TValue>(parent));
}
return ReferenceWithError.newInstanceWithoutError(new BasicConfigValueAtContainer<TContainer,TValue>(local));
}
// parent explicitly set (or we might have to merge defaults),
// and by the contract of this method we can assume reinheritable
if (!local.isValueExplicitlySet() && !getLocalDefaultResolvesWithAncestorValue())
return ReferenceWithError.newInstanceWithoutError(new BasicConfigValueAtContainer<TContainer,TValue>(parent));
// both explicitly set or defaults applicable, and not overwriting; it should be merge, or something from child
if (CONFLICT_RESOLUTION_STRATEGY_DEEP_MERGE.equals(conflictResolutionStrategy)) {
BasicConfigValueAtContainer<TContainer, TValue> result = new BasicConfigValueAtContainer<TContainer,TValue>(local);
ReferenceWithError<Maybe<? extends TValue>> resolvedValue = deepMerge(
local.isValueExplicitlySet() ? local.asMaybe() : local.getDefaultValue(),
parent.isValueExplicitlySet() ? parent.asMaybe() :
getAncestorDefaultInheritable() ? parent.getDefaultValue() : Maybe.<TValue>absent());
result.setValue(resolvedValue.getWithoutError());
return ReferenceWithError.newInstanceThrowingError(result, resolvedValue.getError());
}
return resolveWithParentCustomStrategy(local, parent, context);
}
/** Provided for subclasses to override. Invoked by {@link #resolveWithParent(ConfigValueAtContainer, ConfigValueAtContainer, org.apache.brooklyn.config.ConfigInheritance.ConfigInheritanceContext)}
* if there is a value for the parent and the child. Default implementation throws {@link IllegalStateException}. */
protected <TContainer, TValue> ReferenceWithError<ConfigValueAtContainer<TContainer, TValue>> resolveWithParentCustomStrategy(
ConfigValueAtContainer<TContainer, TValue> local, ConfigValueAtContainer<TContainer, TValue> parent,
ConfigInheritanceContext context) {
throw new IllegalStateException("Unknown config conflict resolution strategy '"+conflictResolutionStrategy+"' evaluating "+local+"/"+parent);
}
private static <T> ReferenceWithError<Maybe<? extends T>> deepMerge(Maybe<? extends T> val1, Maybe<? extends T> val2) {
if (val2.isAbsent() || val2.isNull()) {
return ReferenceWithError.newInstanceWithoutError(val1);
} else if (val1.isAbsent()) {
return ReferenceWithError.newInstanceWithoutError(val2);
} else if (val1.isNull()) {
return ReferenceWithError.newInstanceWithoutError(val1); // an explicit null means an override; don't merge
} else if (val1.get() instanceof Map && val2.get() instanceof Map) {
@SuppressWarnings({ "unchecked", "rawtypes" })
Maybe<T> result = (Maybe)Maybe.of(CollectionMerger.builder().build().merge((Map<?,?>)val1.get(), (Map<?,?>)val2.get()));
return ReferenceWithError.newInstanceWithoutError(result);
} else {
// cannot merge; just return val1
return ReferenceWithError.newInstanceThrowingError(val1, new IllegalArgumentException("Cannot merge '"+val1.get()+"' and '"+val2.get()+"'"));
}
}
public boolean isReinherited() {
return isReinherited;
}
public String getConflictResolutionStrategy() {
return conflictResolutionStrategy;
}
@Deprecated /** @deprecated since 0.11.0 when it was introduced, prefer {@link #getLocalDefaultResolvesWithAncestorValue()} */
public boolean getUseLocalDefaultValue() {
return getLocalDefaultResolvesWithAncestorValue();
}
/** see {@link #localDefaultResolvesWithAncestorValue} */
public boolean getLocalDefaultResolvesWithAncestorValue() {
if (localDefaultResolvesWithAncestorValue==null) {
// in case some legacy path is using an improperly deserialized object
log.warn("Encountered legacy "+this+" with null localDefaultResolvesWithAncestorValue; transforming", new Throwable("stack trace for legacy "+this));
readResolve();
}
return localDefaultResolvesWithAncestorValue;
}
public boolean getAncestorDefaultInheritable() {
if (ancestorDefaultInheritable==null) {
log.warn("Encountered legacy "+this+" with null ancestorDefaultInheritable; transforming", new Throwable("stack trace for legacy "+this));
readResolve();
}
return ancestorDefaultInheritable;
}
// standard deserialization method
private ConfigInheritance readResolve() {
try {
// uses reflection because fields are declared final
if (useLocalDefaultValue!=null) {
// move away from useLocalDefaultValue to localDefaultResolvesWithAncestorValue
Field fNew = getClass().getDeclaredField("localDefaultResolvesWithAncestorValue");
fNew.setAccessible(true);
Field fOld = getClass().getDeclaredField("useLocalDefaultValue");
fOld.setAccessible(true);
if (fNew.get(this)==null) {
fNew.set(this, useLocalDefaultValue);
} else {
if (!fNew.get(this).equals(useLocalDefaultValue)) {
throw new IllegalStateException("Incompatible values detected for "+fOld+" ("+fOld.get(this)+") and "+fNew+" ("+fNew.get(this)+")");
}
}
fOld.set(this, null);
}
if (ancestorDefaultInheritable==null) {
Field f = getClass().getDeclaredField("ancestorDefaultInheritable");
f.setAccessible(true);
f.set(this, true);
}
} catch (Exception e) {
throw Exceptions.propagate(e);
}
return returnEquivalentConstant(this);
}
@Override
public int hashCode() {
return Objects.hash(conflictResolutionStrategy, isReinherited, localDefaultResolvesWithAncestorValue, ancestorDefaultInheritable);
}
@Override
public boolean equals(Object obj) {
if (obj==null) return false;
if (obj instanceof DelegatingConfigInheritance) {
return equals( ((DelegatingConfigInheritance)obj).getDelegate() );
}
if (obj instanceof BasicConfigInheritance) {
return equalsAfterResolvingDelegate((BasicConfigInheritance)obj);
}
return false;
}
protected boolean equalsAfterResolvingDelegate(BasicConfigInheritance b) {
return (Objects.equals(conflictResolutionStrategy, b.conflictResolutionStrategy) &&
Objects.equals(isReinherited, b.isReinherited) &&
Objects.equals(getLocalDefaultResolvesWithAncestorValue(), b.getLocalDefaultResolvesWithAncestorValue()) &&
Objects.equals(getAncestorDefaultInheritable(), b.getAncestorDefaultInheritable()));
}
@Override
public String toString() {
return super.toString()+"[reinherit="+isReinherited()+"; strategy="+getConflictResolutionStrategy()+"; localDefaultResolvesWithAncestor="+localDefaultResolvesWithAncestorValue+"]";
}
public static ConfigInheritance fromString(String val) {
if (Strings.isBlank(val)) return null;
// in all cases below the first value is preferred, but to preserve backwards compatibility we
// need to support some of the other names/strategies; yoml should give us a way to migrate to canonical form
switch (val.toLowerCase().trim()) {
case "not_reinherited":
case "notreinherited":
case "none":
return NOT_REINHERITED;
case "never":
return NEVER_INHERITED;
case "overwrite":
case "always":
return OVERWRITE;
case "deep_merge":
case "merge":
case "deepmerge":
return DEEP_MERGE;
default:
throw new IllegalArgumentException("Invalid config-inheritance '"+val+"' (legal values are none, always or deep_merge)");
}
}
}