Copyright 2014 Netflix, Inc. Licensed 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.
/** * Copyright 2014 Netflix, Inc. * * Licensed 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 com.netflix.config; import java.util.Collection; import java.util.List; import java.util.Map; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.collect.Lists;
A property that has multiple possible values associated with it and determines the value according to the runtime context, which can include deployment context, values of other properties or attributes of user input.

The value is defined as a JSON blob that consists of multiple conditions and value associated with each condition. The following is an example:


[
{
"if":  
{"@environment":["prod"],
"@region":["us-east-1"]
 },
"value":5
 },
{
"if":
{"@environment":["test", "dev"]},
"value":10
 },
{
"value":2
 }
]
This blob means that
  • If "@enviornment" is "prod" and "@region" is "us-east-1", value of the property is integer 5; (Note: if you use ConfigurationManager, "@enviornment" and "@region" are automatically exported as properties from DeploymentContext)
  • Else if "@environment" is either "test" or "dev", the value is 10;
  • Otherwise, the default value of the property is 2

    In order to make this work, a Predicate is needed to determine if the current runtime context matches any of the conditions described in the JSON blob. The predicate can be passed in as a parameter of the constructor, otherwise the default one will interpret each key in the "dimensions" as a key of a DynamicProperty, and the list of values are the acceptable values of the DynamicProperty.

    For example:

    
         String json = ... // string as the above JSON blob
         ConfigurationManager.getConfigInstance().setProperty("@environment", "test"); // no need to do this in real application as property is automatically set 
    ConfigurationManager.getConfigInstance().setProperty("contextualProp", json);
    DynamicContextualProperty<Integer> prop = new DynamicContextualProperty<Integer>("contextualProp", 0);
    prop.get(); // returns 10 as "@environment" == "test" matches the second condition 
    
  • Author:awang
    Type parameters:
    • <T> – Data type of the property, e.g., Integer, Boolean
    /** * A property that has multiple possible values associated with it and determines * the value according to the runtime context, which can include deployment context, * values of other properties or attributes of user input. * <p> * The value is defined as a JSON blob that * consists of multiple conditions and value associated with each condition. The following * is an example: * * <pre>{@code * [ { "if": {"@environment":["prod"], "@region":["us-east-1"] }, "value":5 }, { "if": {"@environment":["test", "dev"]}, "value":10 }, { "value":2 } ] * }</pre> * * This blob means that <li>If "@enviornment" is "prod" <b>and</b> "@region" is "us-east-1", value of the property is integer 5; (<b>Note:</b> if you use ConfigurationManager, "@enviornment" and "@region" are automatically exported as properties from DeploymentContext) <li>Else if "@environment" is either "test" or "dev", the value is 10; <li>Otherwise, the default value of the property is 2 * <p> * In order to make this work, a Predicate is needed to determine if the current runtime context matches any of the conditions * described in the JSON blob. The predicate can be passed in as a parameter of the constructor, otherwise the default one * will interpret each key in the "dimensions" as a key of a DynamicProperty, and the list of values are the acceptable * values of the DynamicProperty. * <p> * For example:<p> * * <pre>{@code * * String json = ... // string as the above JSON blob * ConfigurationManager.getConfigInstance().setProperty("@environment", "test"); // no need to do this in real application as property is automatically set ConfigurationManager.getConfigInstance().setProperty("contextualProp", json); DynamicContextualProperty<Integer> prop = new DynamicContextualProperty<Integer>("contextualProp", 0); prop.get(); // returns 10 as "@environment" == "test" matches the second condition * }</pre> * * @author awang * * @param <T> Data type of the property, e.g., Integer, Boolean */
    public class DynamicContextualProperty<T> extends PropertyWrapper<T> { public static class Value<T> { private Map<String, Collection<String>> dimensions; private T value; private String comment; private boolean runtimeEval = false; public Value() { } public Value(T value) { this.value = value; } @JsonProperty("if") public final Map<String, Collection<String>> getDimensions() { return dimensions; } @JsonProperty("if") public final void setDimensions(Map<String, Collection<String>> dimensions) { this.dimensions = dimensions; } public final T getValue() { return value; } public final void setValue(T value) { this.value = value; } public final String getComment() { return comment; } public final void setComment(String comment) { this.comment = comment; } public final boolean isRuntimeEval() { return runtimeEval; } public final void setRuntimeEval(boolean runtimeEval) { this.runtimeEval = runtimeEval; } } private static Predicate<Map<String, Collection<String>>> defaultPredicate = DefaultContextualPredicate.PROPERTY_BASED; private final Predicate<Map<String, Collection<String>>> predicate; @VisibleForTesting volatile List<Value<T>> values; private final ObjectMapper mapper = new ObjectMapper(); private final Class<T> classType; @SuppressWarnings("unchecked") public DynamicContextualProperty(String propName, T defaultValue, Predicate<Map<String, Collection<String>>> predicate) { super(propName, defaultValue); classType = (Class<T>) defaultValue.getClass(); this.predicate = predicate; propertyChangedInternal(); } @SuppressWarnings("unchecked") public DynamicContextualProperty(String propName, T defaultValue) { super(propName, defaultValue); classType = (Class<T>) defaultValue.getClass(); this.predicate = defaultPredicate; propertyChangedInternal(); } private final void propertyChangedInternal() { if (prop.getString() != null) { try { values = mapper.readValue(prop.getString(), new TypeReference<List<Value<T>>>(){}); } catch (Throwable e) { // this could be that the property is not set up as JSON based multi-dimensional contextual property // and only has one textual value, try to parse this textual value Optional<T> cachedValue = prop.getCachedValue(classType); if (cachedValue.isPresent()) { T value = cachedValue.get(); values = Lists.newArrayList(); values.add(new Value<T>(value)); } else { values = null; } if (values == null) { throw new RuntimeException("Unable to parse the property value: " + prop.getString(), e); } } } else { values = null; } } @Override protected final void propertyChanged() { propertyChangedInternal(); propertyChanged(this.getValue()); } @Override public T getValue() { if (values != null) { for (Value<T> v: values) { if (v.getDimensions() == null || v.getDimensions().isEmpty() || predicate.apply(v.getDimensions())) { return v.getValue(); } } } return defaultValue; } }