// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.processing.request; import java.util.Map; import java.util.HashMap; import java.util.Collections; /** * The properties of a request * * @author bratseth */ public class Properties implements Cloneable { private final static CloneHelper cloneHelper = new CloneHelper(); private Properties chained = null; /** * Sets the properties chained to this. * * @param chained the properties to chain to this, or null to make this the last in the chain * @return the given chained object to allow setting up a chain by dotting in one statement */ public Properties chain(Properties chained) { this.chained = chained; return chained; } /** * Returns the properties chained to this, or null if this is the last in the chain */ public Properties chained() { return chained; } /** * Returns the first instance of the given class in this chain, or null if none */ @SuppressWarnings("unchecked") public final T getInstance(Class propertyClass) { if (propertyClass.isAssignableFrom(this.getClass())) return (T) this; if (chained == null) return null; return chained.getInstance(propertyClass); } /** Lists all properties of this with no context, by delegating to listProperties(""). */ public final Map listProperties() { return listProperties(CompoundName.empty); } /** Returns a snapshot of all properties of this - same as listProperties("", context). */ public final Map listProperties(Map context) { return listProperties(CompoundName.empty, context, this); } /** Returns a snapshot of all properties by calling listProperties(path, null). */ public final Map listProperties(CompoundName path) { return listProperties(path, null, this); } /** Returns a snapshot of all properties by calling listProperties(path, null). */ public final Map listProperties(String path) { return listProperties(new CompoundName(path), null, this); } /** Returns a snapshot of all properties by calling listProperties(path, null). */ public final Map listProperties(CompoundName path, Map context) { return listProperties(path, context, this); } /** Returns a snapshot of all properties by calling listProperties(path, null). */ public final Map listProperties(String path, Map context) { return listProperties(new CompoundName(path), context, this); } /** * Returns a snapshot of all properties of this having a given path prefix * Some sources of properties may not be list-able and will not be included in this snapshot. * * @param path the prefix (up to a ".") of the properties to return, or null or the empty string * to return all properties * @param context the context used to resolve the properties, or null if none * @param substitution the properties which will be used to do string substitution in the values added to the map */ public Map listProperties(CompoundName path, Map context, Properties substitution) { if (path == null) path = CompoundName.empty; if (chained() == null) return new HashMap<>(); else return chained().listProperties(path, context, substitution); } /** * Returns a snapshot of all properties of this having a given path prefix * Some sources of properties may not be list-able and will not be included in this snapshot. * * @param path the prefix (up to a ".") of the properties to return, or null or the empty string to return all properties * @param context the context used to resolve the properties, or null if none * @param substitution the properties which will be used to do string substitution in the values added to the map */ public final Map listProperties(String path, Map context, Properties substitution) { return listProperties(new CompoundName(path), context, substitution); } /** * Gets a named value which (if necessary) is resolved using a property context. * * @param name the name of the property to return * @param context the variant resolution context, or null if none * @param substitution the properties used to substitute in these properties, or null if none */ public Object get(CompoundName name, Map context, Properties substitution) { if (chained == null) return null; return chained.get(name, context, substitution); } /** * Gets a named value which (if necessary) is resolved using a property context. * * @param name the name of the property to return * @param context the variant resolution context, or null if none * @param substitution the properties used to substitute in these properties, or null if none */ public final Object get(String name, Map context, Properties substitution) { return get(new CompoundName(name), context, substitution); } /** Gets a named value from the first chained instance which has one by calling get(name,context,this). */ public final Object get(CompoundName name, Map context) { return get(name, context, this); } /** Gets a named value from the first chained instance which has one by calling get(name,context,this). */ public final Object get(String name, Map context) { return get(new CompoundName(name), context, this); } /** Gets a named value from the first chained instance which has one by calling get(name,null,this). */ public final Object get(CompoundName name) { return get(name, null, this); } /** Gets a named value from the first chained instance which has one by calling get(name,null,this). */ public final Object get(String name) { return get(new CompoundName(name), null, this); } /** * Gets a named value from the first chained instance which has one, * or the default value if no value is set, or if the first value encountered is explicitly set to null. * This default implementation simply forwards to the chained instance, or returns the default if none. * * @param name the name of the property to return * @param defaultValue the default value returned if the value returned is null */ public final Object get(CompoundName name, Object defaultValue) { Object value = get(name); if (value == null) return defaultValue; return value; } /** * Gets a named value from the first chained instance which has one, * or the default value if no value is set, or if the first value encountered is explicitly set to null. * This default implementation simply forwards to the chained instance, or returns the default if none. * * @param name the name of the property to return * @param defaultValue the default value returned if the value returned is null */ public final Object get(String name, Object defaultValue) { return get(new CompoundName(name), defaultValue); } /** * Sets a value to the first chained instance which accepts it. * This default implementation forwards to the chained instance or throws * a RuntimeException if there is not chained instance. * * @param name the name of the property * @param value the value to set. Setting a property to null clears it. * @param context the context used to resolve where the values should be set, or null if none * @throws RuntimeException if no instance in the chain accepted this name-value pair */ public void set(CompoundName name, Object value, Map context) { if (chained == null) throw new RuntimeException("Property '" + name + "->" + value + "' was not accepted in this property chain"); chained.set(name, value, context); } /** * Sets a value to the first chained instance which accepts it. * This default implementation forwards to the chained instance or throws * a RuntimeException if there is not chained instance. * * @param name the name of the property * @param value the value to set. Setting a property to null clears it. * @param context the context used to resolve where the values should be set, or null if none * @throws RuntimeException if no instance in the chain accepted this name-value pair */ public final void set(String name, Object value, Map context) { set(new CompoundName(name), value, context); } /** * Sets a value to the first chained instance which accepts it by calling set(name, value, null). * * @param name the name of the property * @param value the value to set. Setting a property to null clears it. * @throws RuntimeException if no instance in the chain accepted this name-value pair */ public final void set(CompoundName name, Object value) { set(name, value, null); } /** * Sets a value to the first chained instance which accepts it by calling set(name, value, null). * * @param name the name of the property * @param value the value to set. Setting a property to null clears it. * @throws RuntimeException if no instance in the chain accepted this name-value pair */ public final void set(String name, Object value) { set(new CompoundName(name), value, Map.of()); } /** * Sets all properties having this name as a compound prefix to null. * I.e clearAll("a") will clear the value of "a" and "a.b" but not "ab". * This default implementation forwards to the chained instance or throws * a RuntimeException if there is not chained instance. * * @param name the compound prefix of the properties to clear * @param context the context used to resolve where the values should be cleared, or null if none * @throws RuntimeException if no instance in the chain accepted this name-value pair */ public void clearAll(CompoundName name, Map context) { if (chained == null) throw new RuntimeException("Property '" + name + "' was not accepted in this property chain"); chained.clearAll(name, context); } /** * Sets all properties having this name as a compound prefix to null. * I.e clearAll("a") will clear the value of "a" and "a.b" but not "ab". * * @param name the compound prefix of the properties to clear * @param context the context used to resolve where the values should be cleared, or null if none * @throws RuntimeException if no instance in the chain accepted this name-value pair */ public final void clearAll(String name, Object value, Map context) { set(new CompoundName(name), value, context); } /** * Sets all properties having this name as a compound prefix to null. * I.e clearAll("a") will clear the value of "a" and "a.b" but not "ab". * * @param name the compound prefix of the properties to clear * @throws RuntimeException if no instance in the chain accepted this name-value pair */ public final void clearAll(CompoundName name) { clearAll(name, null); } /** * Sets all properties having this name as a compound prefix to null. * I.e clearAll("a") will clear the value of "a" and "a.b" but not "ab". * * @param name the compound prefix of the properties to clear * @throws RuntimeException if no instance in the chain accepted this name-value pair */ public final void clearAll(String name) { clearAll(new CompoundName(name), Collections.emptyMap()); } /** * Gets a property as a boolean - if this value can reasonably be interpreted as a boolean, this will return * the value. Returns false if this property is null. */ public final boolean getBoolean(CompoundName name) { return getBoolean(name, false); } /** * Gets a property as a boolean - if this value can reasonably be interpreted as a boolean, this will return * the value. Returns false if this property is null. */ public final boolean getBoolean(String name) { return getBoolean(name, false); } /** * Gets a property as a boolean. * This will return true only if the value is either the empty string, * or any Object which has a toString which is case-insensitive equal to "true" * * @param defaultValue the value to return if this property is null */ public final boolean getBoolean(CompoundName key, boolean defaultValue) { return asBoolean(get(key), defaultValue); } /** * Gets a property as a boolean. * This will return true only if the value is either the empty string, * or any Object which has a toString which is case-insensitive equal to "true" * * @param defaultValue the value to return if this property is null */ public final boolean getBoolean(String key, boolean defaultValue) { return asBoolean(get(key), defaultValue); } /** * Converts a value to boolean - this will be true only if the value is either the empty string, * or any Object which has a toString which is case-insensitive equal to "true" */ protected static boolean asBoolean(Object value, boolean defaultValue) { if (value == null) return defaultValue; String s = value.toString(); int sz = s.length(); return switch (sz) { case 0: yield true; case 4: yield ((s.charAt(0) | 0x20) == 't') && ((s.charAt(1) | 0x20) == 'r') && ((s.charAt(2) | 0x20) == 'u') && ((s.charAt(3) | 0x20) == 'e'); default: yield false; }; } /** * Returns this property as a string. * * @return this property as a string, or null if the property is null */ public final String getString(CompoundName key) { return getString(key, null); } /** * Returns this property as a string. * * @return this property as a string, or null if the property is null */ public final String getString(String key) { return getString(key, null); } /** * Returns this property as a string. * * @param key the property key * @param defaultValue the value to return if this property is null * @return this property as a string */ public final String getString(CompoundName key, String defaultValue) { return asString(get(key), defaultValue); } /** * Returns this property as a string. * * @param key the property key * @param defaultValue the value to return if this property is null * @return this property as a string */ public final String getString(String key, String defaultValue) { return asString(get(key), defaultValue); } protected static String asString(Object value, String defaultValue) { if (value == null) return defaultValue; return value.toString(); } /** * Returns a property as an Integer. * * @return the integer value of the name, or null if the property is null * @throws NumberFormatException if the given parameter exists but * have a toString which is not parseable as a number */ public final Integer getInteger(CompoundName name) { return getInteger(name, null); } /** * Returns a property as an Integer. * * @return the integer value of the name, or null if the property is null * @throws NumberFormatException if the given parameter exists but * have a toString which is not parseable as a number */ public final Integer getInteger(String name) { return getInteger(name, null); } /** * Returns a property as an Integer. * * @param name the property name * @param defaultValue the value to return if this property is null * @return the integer value for the name * @throws NumberFormatException if the given parameter does not exist * or does not have a toString parseable as a number */ public final Integer getInteger(CompoundName name, Integer defaultValue) { return asInteger(get(name), defaultValue); } /** * Returns a property as an Integer. * * @param name the property name * @param defaultValue the value to return if this property is null * @return the integer value for the name * @throws NumberFormatException if the given parameter does not exist * or does not have a toString parseable as a number */ public final Integer getInteger(String name, Integer defaultValue) { return asInteger(get(name), defaultValue); } protected static Integer asInteger(Object value, Integer defaultValue) { try { if (value == null) return defaultValue; if (value instanceof Number) return ((Number)value).intValue(); String stringValue = value.toString(); if (stringValue.isEmpty()) return defaultValue; return Integer.valueOf(stringValue); } catch (IllegalArgumentException e) { throw new NumberFormatException("'" + value + "' is not a valid integer"); } } /** * Returns a property as a Long. * * @return the long value of the name, or null if the property is null * @throws NumberFormatException if the given parameter exists but have a value which * is not parseable as a number */ public final Long getLong(CompoundName name) { return getLong(name, null); } /** * Returns a property as a Long. * * @return the long value of the name, or null if the property is null * @throws NumberFormatException if the given parameter exists but have a value which * is not parseable as a number */ public final Long getLong(String name) { return getLong(name, null); } /** * Returns a property as a Long. * * @param name the property name * @param defaultValue the value to return if this property is null * @return the integer value for this name * @throws NumberFormatException if the given parameter exists but have a value which * is not parseable as a number */ public final Long getLong(CompoundName name, Long defaultValue) { return asLong(get(name), defaultValue); } /** * Returns a property as a Long. * * @param name the property name * @param defaultValue the value to return if this property is null * @return the integer value for this name * @throws NumberFormatException if the given parameter exists but have a value which * is not parseable as a number */ public final Long getLong(String name, Long defaultValue) { return asLong(get(name), defaultValue); } protected static Long asLong(Object value, Long defaultValue) { try { if (value == null) return defaultValue; if (value instanceof Long) return (Long) value; String stringValue = value.toString(); if (stringValue.isEmpty()) return defaultValue; return Long.valueOf(value.toString()); } catch (IllegalArgumentException e) { throw new NumberFormatException("Not a valid long"); } } /** * Returns a property as a Double. * * @return the double value of the name, or null if the property is null * @throws NumberFormatException if the given parameter exists but have a value which * is not parseable as a number */ public final Double getDouble(CompoundName name) { return getDouble(name, null); } /** * Returns a property as a Double. * * @return the double value of the name, or null if the property is null * @throws NumberFormatException if the given parameter exists but have a value which * is not parseable as a number */ public final Double getDouble(String name) { return getDouble(name, null); } /** * Returns a property as a Double. * * @param name the property name * @param defaultValue the value to return if this property is null * @return the integer value for this name * @throws NumberFormatException if the given parameter exists but have a value which * is not parseable as a number */ public final Double getDouble(CompoundName name, Double defaultValue) { return asDouble(get(name), defaultValue); } /** * Returns a property as a Double. * * @param name the property name * @param defaultValue the value to return if this property is null * @return the integer value for this name * @throws NumberFormatException if the given parameter exists but have a value which * is not parseable as a number */ public final Double getDouble(String name, Double defaultValue) { return asDouble(get(name), defaultValue); } protected static Double asDouble(Object value, Double defaultValue) { try { if (value == null) return defaultValue; if (value instanceof Double) return (Double) value; String stringValue = value.toString(); if (stringValue.isEmpty()) return defaultValue; return Double.valueOf(value.toString()); } catch (IllegalArgumentException e) { throw new NumberFormatException("Not a valid double"); } } /** * Clones this instance and recursively all chained instance. * Implementations should call this and clone their own state as appropriate */ public Properties clone() { try { Properties clone = (Properties) super.clone(); if (chained != null) clone.chained = this.chained.clone(); return clone; } catch (CloneNotSupportedException e) { throw new RuntimeException("Will never happen"); } } /** Clones a map by deep cloning each value which is cloneable and shallow copying all other values. */ public static Map cloneMap(Map map) { return cloneHelper.cloneMap(map); } /** Clones this object if it is clonable, and the clone is public. Returns null if not. */ public static Object clone(Object object) { return cloneHelper.clone(object); } }