/* * Copyright 2015-present Open Networking Laboratory * * 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 org.onosproject.net.config; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.annotations.Beta; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import org.onlab.packet.IpAddress; import org.onlab.packet.IpPrefix; import org.onlab.packet.MacAddress; import org.onlab.packet.TpPort; import org.onosproject.net.ConnectPoint; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; /** * Base abstraction of a configuration facade for a specific subject. Derived * classes should keep all state in the specified JSON tree as that is the * only state that will be distributed or persisted; this class is merely * a facade for interacting with a particular facet of configuration on a * given subject. * * @param <S> type of subject */ @Beta public abstract class Config<S> { private static final String TRUE_LITERAL = "true"; private static final String FALSE_LITERAL = "false"; protected S subject; protected String key; protected JsonNode node; protected ObjectNode object; protected ArrayNode array; protected ObjectMapper mapper; protected ConfigApplyDelegate delegate; /** * Indicator of whether a configuration JSON field is required. */ public enum FieldPresence { /** * Signifies that config field is an optional one. */ OPTIONAL, /** * Signifies that config field is mandatory. */ MANDATORY } /** * Initializes the configuration behaviour with necessary context. * * @param subject configuration subject * @param key configuration key * @param node JSON node where configuration data is stored * @param mapper JSON object mapper * @param delegate delegate context, or null for detached configs. */ public final void init(S subject, String key, JsonNode node, ObjectMapper mapper, ConfigApplyDelegate delegate) { this.subject = checkNotNull(subject, "Subject cannot be null"); this.key = key; this.node = checkNotNull(node, "Node cannot be null"); this.object = node instanceof ObjectNode ? (ObjectNode) node : null; this.array = node instanceof ArrayNode ? (ArrayNode) node : null; this.mapper = checkNotNull(mapper, "Mapper cannot be null"); this.delegate = delegate; } /** * Indicates whether or not the backing JSON node contains valid data. * <p> * Default implementation returns true. * Subclasses are expected to override this with their own validation. * Implementations are free to throw a RuntimeException if data is invalid. * </p> * * @return true if the data is valid; false otherwise * @throws RuntimeException if configuration is invalid or completely foobar */ public boolean isValid() { // Derivatives should use the provided set of predicates to test // validity of their fields, e.g.: // isString(path) // isBoolean(path) // isNumber(path, [min, max]) // isIntegralNumber(path, [min, max]) // isDecimal(path, [min, max]) // isMacAddress(path) // isIpAddress(path) // isIpPrefix(path) // isConnectPoint(path) // isTpPort(path) return true; } /** * Returns the specific subject to which this configuration pertains. * * @return configuration subject */ public S subject() { return subject; } /** * Returns the configuration key. This is primarily aimed for use in * composite JSON trees in external representations and has no bearing on * the internal behaviours. * * @return configuration key */ public String key() { return key; } /** * Returns the JSON node that contains the configuration data. * * @return JSON node backing the configuration */ public JsonNode node() { return node; } /** * Applies any configuration changes made via this configuration. * <p> * Not effective for detached configs. * </p> */ public void apply() { checkState(delegate != null, "Cannot apply detached config"); delegate.onApply(this); } // Miscellaneous helpers for interacting with JSON /** * Gets the specified property as a string. * * @param name property name * @param defaultValue default value if property not set * @return property value or default value */ protected String get(String name, String defaultValue) { return object.path(name).asText(defaultValue); } /** * Sets the specified property as a string or clears it if null value given. * * @param name property name * @param value new value or null to clear the property * @return self */ protected Config<S> setOrClear(String name, String value) { if (value != null) { object.put(name, value); } else { object.remove(name); } return this; } /** * Gets the specified property as a boolean. * * @param name property name * @param defaultValue default value if property not set * @return property value or default value */ protected boolean get(String name, boolean defaultValue) { return object.path(name).asBoolean(defaultValue); } /** * Clears the specified property. * * @param name property name * @return self */ protected Config<S> clear(String name) { object.remove(name); return this; } /** * Sets the specified property as a boolean or clears it if null value given. * * @param name property name * @param value new value or null to clear the property * @return self */ protected Config<S> setOrClear(String name, Boolean value) { if (value != null) { object.put(name, value.booleanValue()); } else { object.remove(name); } return this; } /** * Gets the specified property as an integer. * * @param name property name * @param defaultValue default value if property not set * @return property value or default value */ protected int get(String name, int defaultValue) { return object.path(name).asInt(defaultValue); } /** * Sets the specified property as an integer or clears it if null value given. * * @param name property name * @param value new value or null to clear the property * @return self */ protected Config<S> setOrClear(String name, Integer value) { if (value != null) { object.put(name, value.intValue()); } else { object.remove(name); } return this; } /** * Gets the specified property as a long. * * @param name property name * @param defaultValue default value if property not set * @return property value or default value */ protected long get(String name, long defaultValue) { return object.path(name).asLong(defaultValue); } /** * Sets the specified property as a long or clears it if null value given. * * @param name property name * @param value new value or null to clear the property * @return self */ protected Config<S> setOrClear(String name, Long value) { if (value != null) { object.put(name, value.longValue()); } else { object.remove(name); } return this; } /** * Gets the specified property as a double. * * @param name property name * @param defaultValue default value if property not set * @return property value or default value */ protected double get(String name, double defaultValue) { return object.path(name).asDouble(defaultValue); } /** * Sets the specified property as a double or clears it if null value given. * * @param name property name * @param value new value or null to clear the property * @return self */ protected Config<S> setOrClear(String name, Double value) { if (value != null) { object.put(name, value.doubleValue()); } else { object.remove(name); } return this; } /** * Gets the specified property as an enum. * * @param name property name * @param defaultValue default value if property not set * @param enumClass the enum class * @param <E> type of enum * @return property value or default value */ protected <E extends Enum<E>> E get(String name, E defaultValue, Class<E> enumClass) { if (defaultValue != null) { return Enum.valueOf(enumClass, object.path(name).asText(defaultValue.toString())); } JsonNode node = object.get(name); return node == null ? null : Enum.valueOf(enumClass, node.asText()); } /** * Sets the specified property as a double or clears it if null value given. * * @param name property name * @param value new value or null to clear the property * @param <E> type of enum * @return self */ protected <E extends Enum> Config<S> setOrClear(String name, E value) { if (value != null) { object.put(name, value.toString()); } else { object.remove(name); } return this; } /** * Gets the specified array property as a list of items. * * @param name property name * @param function mapper from string to item * @param <T> type of item * @return list of items */ protected <T> List<T> getList(String name, Function<String, T> function) { List<T> list = Lists.newArrayList(); ArrayNode arrayNode = (ArrayNode) object.path(name); arrayNode.forEach(i -> list.add(function.apply(asString(i)))); return list; } /** * Converts JSON node to a String. * <p> * If the {@code node} was a text node, text is returned as-is, * all other node type will be converted to String by toString(). * * @param node JSON node to convert * @return String representation */ private static String asString(JsonNode node) { if (node.isTextual()) { return node.asText(); } else { return node.toString(); } } /** * Gets the specified array property as a list of items. * * @param name property name * @param function mapper from string to item * @param defaultValue default value if property not set * @param <T> type of item * @return list of items */ protected <T> List<T> getList(String name, Function<String, T> function, List<T> defaultValue) { List<T> list = Lists.newArrayList(); JsonNode jsonNode = object.path(name); if (jsonNode.isMissingNode()) { return defaultValue; } ArrayNode arrayNode = (ArrayNode) jsonNode; arrayNode.forEach(i -> list.add(function.apply(asString(i)))); return list; } /** * Sets the specified property as an array of items in a given collection * transformed into a String with supplied {@code function}. * * @param name propertyName * @param function to transform item to a String * @param value list of items * @param <T> type of items * @return self */ protected <T> Config<S> setList(String name, Function<? super T, String> function, List<T> value) { Collection<String> mapped = value.stream() .map(function) .collect(Collectors.toList()); return setOrClear(name, mapped); } /** * Sets the specified property as an array of items in a given collection or * clears it if null is given. * * @param name propertyName * @param collection collection of items * @param <T> type of items * @return self */ protected <T> Config<S> setOrClear(String name, Collection<T> collection) { if (collection == null) { object.remove(name); } else { ArrayNode arrayNode = mapper.createArrayNode(); collection.forEach(i -> arrayNode.add(i.toString())); object.set(name, arrayNode); } return this; } /** * Returns true if this config contains a field with the given name. * * @param name the field name * @return true if field is present, false otherwise */ protected boolean hasField(String name) { return hasField(object, name); } /** * Returns true if the given node contains a field with the given name. * * @param node the node to examine * @param name the name to look for * @return true if the node has a field with the given name, false otherwise */ protected boolean hasField(ObjectNode node, String name) { Iterator<String> fnames = node.fieldNames(); while (fnames.hasNext()) { if (fnames.next().equals(name)) { return true; } } return false; } /** * Indicates whether only the specified fields are present in the backing JSON. * * @param allowedFields allowed field names * @return true if only allowedFields are present; false otherwise */ protected boolean hasOnlyFields(String... allowedFields) { return hasOnlyFields(object, allowedFields); } /** * Indicates whether only the specified fields are present in a particular * JSON object. * * @param node node whose fields to check * @param allowedFields allowed field names * @return true if only allowedFields are present; false otherwise */ protected boolean hasOnlyFields(ObjectNode node, String... allowedFields) { Set<String> fields = ImmutableSet.copyOf(allowedFields); node.fieldNames().forEachRemaining(f -> { if (!fields.contains(f)) { throw new InvalidFieldException(f, "Field is not allowed"); } }); return true; } /** * Indicates whether all specified fields are present in the backing JSON. * * @param mandatoryFields mandatory field names * @return true if all mandatory fields are present; false otherwise */ protected boolean hasFields(String... mandatoryFields) { return hasFields(object, mandatoryFields); } /** * Indicates whether all specified fields are present in a particular * JSON object. * * @param node node whose fields to check * @param mandatoryFields mandatory field names * @return true if all mandatory fields are present; false otherwise */ protected boolean hasFields(ObjectNode node, String... mandatoryFields) { Set<String> fields = ImmutableSet.copyOf(mandatoryFields); fields.forEach(f -> { if (node.path(f).isMissingNode()) { throw new InvalidFieldException(f, "Mandatory field is not present"); } }); return true; } /** * Indicates whether the specified field holds a valid MAC address. * * @param field JSON field name * @param presence specifies if field is optional or mandatory * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isMacAddress(String field, FieldPresence presence) { return isMacAddress(object, field, presence); } /** * Indicates whether the specified field of a particular node holds a valid * MAC address. * * @param objectNode JSON node * @param field JSON field name * @param presence specifies if field is optional or mandatory * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isMacAddress(ObjectNode objectNode, String field, FieldPresence presence) { return isValid(objectNode, field, presence, n -> { MacAddress.valueOf(n.asText()); return true; }); } /** * Indicates whether the specified field holds a valid IP address. * * @param field JSON field name * @param presence specifies if field is optional or mandatory * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isIpAddress(String field, FieldPresence presence) { return isIpAddress(object, field, presence); } /** * Indicates whether the specified field of a particular node holds a valid * IP address. * * @param objectNode node from whom to access the field * @param field JSON field name * @param presence specifies if field is optional or mandatory * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isIpAddress(ObjectNode objectNode, String field, FieldPresence presence) { return isValid(objectNode, field, presence, n -> { IpAddress.valueOf(n.asText()); return true; }); } /** * Indicates whether the specified field holds a valid IP prefix. * * @param field JSON field name * @param presence specifies if field is optional or mandatory * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isIpPrefix(String field, FieldPresence presence) { return isIpPrefix(object, field, presence); } /** * Indicates whether the specified field of a particular node holds a valid * IP prefix. * * @param objectNode node from whom to access the field * @param field JSON field name * @param presence specifies if field is optional or mandatory * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isIpPrefix(ObjectNode objectNode, String field, FieldPresence presence) { return isValid(objectNode, field, presence, n -> { IpPrefix.valueOf(n.asText()); return true; }); } /** * Indicates whether the specified field holds a valid transport layer port. * * @param field JSON field name * @param presence specifies if field is optional or mandatory * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isTpPort(String field, FieldPresence presence) { return isTpPort(object, field, presence); } /** * Indicates whether the specified field of a particular node holds a valid * transport layer port. * * @param objectNode node from whom to access the field * @param field JSON field name * @param presence specifies if field is optional or mandatory * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isTpPort(ObjectNode objectNode, String field, FieldPresence presence) { return isValid(objectNode, field, presence, n -> { TpPort.tpPort(n.asInt()); return true; }); } /** * Indicates whether the specified field holds a valid connect point string. * * @param field JSON field name * @param presence specifies if field is optional or mandatory * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isConnectPoint(String field, FieldPresence presence) { return isConnectPoint(object, field, presence); } /** * Indicates whether the specified field of a particular node holds a valid * connect point string. * * @param objectNode JSON node * @param field JSON field name * @param presence specifies if field is optional or mandatory * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isConnectPoint(ObjectNode objectNode, String field, FieldPresence presence) { return isValid(objectNode, field, presence, n -> { ConnectPoint.deviceConnectPoint(n.asText()); return true; }); } /** * Indicates whether the specified field holds a valid string value. * * @param field JSON field name * @param presence specifies if field is optional or mandatory * @param pattern optional regex pattern * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isString(String field, FieldPresence presence, String... pattern) { return isString(object, field, presence, pattern); } /** * Indicates whether the specified field on a particular node holds a valid * string value. * * @param objectNode JSON node * @param field JSON field name * @param presence specifies if field is optional or mandatory * @param pattern optional regex pattern * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isString(ObjectNode objectNode, String field, FieldPresence presence, String... pattern) { return isValid(objectNode, field, presence, (node) -> { if (!(node.isTextual() && (pattern.length > 0 && node.asText().matches(pattern[0]) || pattern.length < 1))) { fail("Invalid string value"); } return true; }); } /** * Indicates whether the specified field holds a valid number. * * @param field JSON field name * @param presence specifies if field is optional or mandatory * @param minMax optional min/max values * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isNumber(String field, FieldPresence presence, long... minMax) { return isNumber(object, field, presence, minMax); } /** * Indicates whether the specified field of a particular node holds a * valid number. * * @param objectNode JSON object * @param field JSON field name * @param presence specifies if field is optional or mandatory * @param minMax optional min/max values * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isNumber(ObjectNode objectNode, String field, FieldPresence presence, long... minMax) { return isValid(objectNode, field, presence, n -> { long number = (n.isNumber()) ? n.asLong() : Long.parseLong(n.asText()); if (minMax.length > 1) { verifyRange(number, minMax[0], minMax[1]); } else if (minMax.length > 0) { verifyRange(number, minMax[0]); } return true; }); } /** * Indicates whether the specified field holds a valid integer. * * @param field JSON field name * @param presence specifies if field is optional or mandatory * @param minMax optional min/max values * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isIntegralNumber(String field, FieldPresence presence, long... minMax) { return isIntegralNumber(object, field, presence, minMax); } /** * Indicates whether the specified field of a particular node holds a valid * integer. * * @param objectNode JSON node * @param field JSON field name * @param presence specifies if field is optional or mandatory * @param minMax optional min/max values * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isIntegralNumber(ObjectNode objectNode, String field, FieldPresence presence, long... minMax) { return isValid(objectNode, field, presence, n -> { long number = (n.isIntegralNumber()) ? n.asLong() : Long.parseLong(n.asText()); if (minMax.length > 1) { verifyRange(number, minMax[0], minMax[1]); } else if (minMax.length > 0) { verifyRange(number, minMax[0]); } return true; }); } /** * Indicates whether the specified field holds a valid decimal number. * * @param field JSON field name * @param presence specifies if field is optional or mandatory * @param minMax optional min/max values * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isDecimal(String field, FieldPresence presence, double... minMax) { return isDecimal(object, field, presence, minMax); } /** * Indicates whether the specified field of a particular node holds a valid * decimal number. * * @param objectNode JSON node * @param field JSON field name * @param presence specifies if field is optional or mandatory * @param minMax optional min/max values * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isDecimal(ObjectNode objectNode, String field, FieldPresence presence, double... minMax) { return isValid(objectNode, field, presence, n -> { double number = (n.isDouble()) ? n.asDouble() : Double.parseDouble(n.asText()); if (minMax.length > 1) { verifyRange(number, minMax[0], minMax[1]); } else if (minMax.length > 0) { verifyRange(number, minMax[0]); } return true; }); } /** * Indicates whether the specified field holds a valid boolean value. * * @param field JSON field name * @param presence specifies if field is optional or mandatory * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isBoolean(String field, FieldPresence presence) { return isBoolean(object, field, presence); } /** * Indicates whether the specified field of a particular node holds a valid * boolean value. * * @param objectNode JSON object node * @param field JSON field name * @param presence specifies if field is optional or mandatory * @return true if valid; false otherwise * @throws InvalidFieldException if the field is present but not valid */ protected boolean isBoolean(ObjectNode objectNode, String field, FieldPresence presence) { return isValid(objectNode, field, presence, n -> { if (!(n.isBoolean() || (n.isTextual() && isBooleanString(n.asText())))) { fail("Field is not a boolean value"); } return true; }); } /** * Indicates whether a string holds a boolean literal value. * * @param str string to test * @return true if the string contains "true" or "false" (case insensitive), * otherwise false */ private boolean isBooleanString(String str) { return str.equalsIgnoreCase(TRUE_LITERAL) || str.equalsIgnoreCase(FALSE_LITERAL); } /** * Indicates whether a field in the node is present and of correct value or * not mandatory and absent. * * @param objectNode JSON object node containing field to validate * @param field name of field to validate * @param presence specified if field is optional or mandatory * @param validationFunction function which can be used to verify if the * node has the correct value * @return true if the field is as expected * @throws InvalidFieldException if the field is present but not valid */ private boolean isValid(ObjectNode objectNode, String field, FieldPresence presence, Function<JsonNode, Boolean> validationFunction) { JsonNode node = objectNode.path(field); boolean isMandatory = presence == FieldPresence.MANDATORY; if (isMandatory && node.isMissingNode()) { throw new InvalidFieldException(field, "Mandatory field not present"); } if (!isMandatory && (node.isNull() || node.isMissingNode())) { return true; } try { if (validationFunction.apply(node)) { return true; } else { throw new InvalidFieldException(field, "Validation error"); } } catch (IllegalArgumentException e) { throw new InvalidFieldException(field, e); } } private static void fail(String message) { throw new IllegalArgumentException(message); } private static <N extends Comparable> void verifyRange(N num, N min) { if (num.compareTo(min) < 0) { fail("Field must be greater than " + min); } } private static <N extends Comparable> void verifyRange(N num, N min, N max) { verifyRange(num, min); if (num.compareTo(max) > 0) { fail("Field must be less than " + max); } } @Override public String toString() { return String.valueOf(node); } }