com.brambolt.util.Maps Maven / Gradle / Ivy
Show all versions of brambolt-rt Show documentation
package com.brambolt.util;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.stream.Collectors;
import static java.util.Arrays.asList;
/**
* Convenience functions for working with maps.
*/
public class Maps {
/**
* A convenience method for creating maps.
*
* The parameters are a sequence of key-value pairs, such that odd-numbered
* parameters are the keys, and the following even-numbered parameters are
* the values to assign to the respective keys.
*
* For example, asMap<Integer, Integer>(1, 2, 3, 4)
* produces the map [ 1: 2, 3: 4 ]
.
*
* @param The key type for the map to create
* @param The value type for the map to create
* @param pairs The pairs for the map to create
* @return The newly created map holding the pairs
* @throws ArrayIndexOutOfBoundsException if the number of parameters is odd
* @throws ClassCastException if an odd-numbered parameter is not of the key
* type K
, or an even-numbered parameter is not of the
* value type V
*/
@SuppressWarnings({"unchecked", "unused"})
public static Map asMap(Object... pairs) {
Map map = new HashMap<>();
for (int i = 0; i < pairs.length; ++i)
map.put((K) pairs[i++], (V) pairs[i]);
return map;
}
/**
* Formats the parameter map as a string, with one line per key.
*
* For example, the map [a: 1, b: 2]
converts to:
*
*
* a=1
* b=2
*
*
* @param map The map to format.
* @param The key type of the map.
* @param The value type of the map.
* @return A string representation of the parameter map.
*/
public static String format(Map map) {
return format(map, "\n");
}
/**
* Formats the parameter map as a string, using the parameter delimiter.
*
* For example, if the map [a: 1, b: 2]
is formatted with
* the delimiter "," the result is a=1,b=2
.
*
* @param map The map to format as a string.
* @param delimiter The delimiter to separate the key-value pairs with.
* @param The key type of the map.
* @param The value type of the map.
* @return The formatted map.
*/
public static String format(Map map, String delimiter) {
return map.keySet().stream().sorted()
.map(key -> String.format("%s=%s", key, map.get(key)))
.collect(Collectors.joining(delimiter));
}
/**
* Merges the two parameter maps by overlaying the second onto the first.
*
* The intersection of the two maps must have consistent structure.
* This means that for a given key in the intersection, either both values
* are maps or neither value is a map. If one value is a map but the other
* is a scalar then an IllegalStateException
is thrown.
*
* @param existing The original map
* @param overwrites The overlay map to override the original map with.
* @return The merged map.
* @throws IllegalStateException If the two maps have inconsistent structure.
*/
public static Map merge(Map existing, Map overwrites) {
Map results = new HashMap<>(existing);
overwrites.forEach((k, v) -> results.merge(k, v, Maps::overwrite));
return results;
}
@SuppressWarnings("unchecked")
private static Object overwrite(Object v1, Object v2) {
if (!(v1 instanceof Map))
if (v2 instanceof Map)
throw invalidOverwrite(v1, v2);
else return v2;
else {
if (!(v2 instanceof Map))
throw invalidOverwrite(v1, v2);
else
return merge((Map) v1, (Map) v2);
}
}
private static IllegalStateException invalidOverwrite(Object v1, Object v2) {
return new IllegalStateException(
"Invalid replacement of " + v1.toString() + " with " + v2.toString());
}
/**
* Locates the last segment of the parameter key. For example, if the
* parameter key is a.b.c
then c
is returned.
* @param segmentedKey The segmented key to locate the last segment of.
* @return The last segment of the key.
*/
@SuppressWarnings("unused")
public static String getLastSegment(String segmentedKey) {
return null == segmentedKey
? null
: (segmentedKey.contains(".")
? segmentedKey.substring(segmentedKey.lastIndexOf(".") + 1)
: segmentedKey);
}
/**
* Segments the parameter key and descents the parameter map to locate a
* value, or throws NoSuchElementException
if no value exists.
*
* For example, if the map is [a: [b: [c: 1]]
and the
* parameter key is a.b
then the return value will be
* [c: 1]
.
*
* @param nested The map to look for a value in.
* @param segmentedKey The segmented key to split and look up.
* @return The value produced by splitting the key and descending the map.
* @throws NoSuchElementException If the key does not map to a value.
*/
@SuppressWarnings("unused")
public static Object segmentedGet(Map nested, String segmentedKey) {
return segmentedGet(nested, convert(segmentedKey));
}
/**
* Descents the parameter map to locate a value for the parameter key
* segments, or throws NoSuchElementException
if no value
* exists.
*
* For example, if the map is [a: [b: [c: 1]]
and the
* parameter key is [a, b]
then the return value will be
* [c: 1]
.
*
* @param nested The map to look for a value in.
* @param segmentedKey The segmented key to split and look up.
* @return The value produced by splitting the key and descending the map.
* @throws NoSuchElementException If the key does not map to a value.
*/
@SuppressWarnings("unchecked")
public static Object segmentedGet(Map nested, List segmentedKey) {
if (null == segmentedKey)
return nested;
switch (segmentedKey.size()) {
case 0:
return nested;
case 1:
String key = segmentedKey.get(0);
throwIfNotFound(nested, key);
return nested.get(key);
default:
String segment = segmentedKey.get(0);
throwIfNotFound(nested, segment);
return segmentedGet(
(Map) nested.get(segment),
segmentedKey.subList(1, segmentedKey.size()));
}
}
private static void throwIfNotFound(Map map, String key) {
if (!map.containsKey(key))
throw new NoSuchElementException(
String.format("Not found: %s [%s]", key, format(map)));
}
/**
* Converts the segmented map key to a list of segments. For example,
* a.b.c
converts to [a, b, c]
.
* @param segmentedKey The segmented key to convert.
* @return A list holding the segments of the parameter key.
*/
public static List convert(String segmentedKey) {
return asList(segmentedKey.trim().split("\\."));
}
/**
* Converts a properties object into a nested map. The nesting is derived
* by splitting segmented property names. For example, the property
* a.b.c=1
results in [a: [b: [c: 1]]]
.
* @param properties The properties to convert to a nested map.
* @return A nested map derived by segmenting the property names.
*/
@SuppressWarnings("unused")
public static Map convert(Properties properties) {
return convert(properties, getKeys(properties));
}
/**
* Creates a list of the property names in the parameter properties.
* @param properties The properties to extract property names from .
* @return A list holding the property names in the parameter properties.
*/
@SuppressWarnings("unchecked")
public static List getKeys(Properties properties) {
return (List) java.util.Collections.list(properties.propertyNames());
}
/**
* Extracts a nested map of property values from the parameter properties.
* The nesting is defined by segmenting the parameter keys. The values are
* retrieved by looking each key up in the parameter properties. For
* example, [a.b=1, c.d=2], a.b
converts to
* [a: [b: 1]]
.
* @param properties The properties to look up values in.
* @param keys The property names to create the map from.
* @return A nested map holding the requested values.
*/
public static Map convert(Properties properties, Collection keys) {
return keys.stream().collect(
HashMap::new,
(Map map, String name) -> insert(properties, name, map),
(Map m, Map u) -> {});
}
private static void insert(Properties properties, String name, Map map) {
insert(properties, name, map, asList(name.split("\\.")));
}
/**
* Modifies the parameter map in-place, by reading the property value
* for the parameter name and creating a nested structure with the value
* "at the bottom" of the nesting.
*
* For example, for the parameters a.b.c.d
, {}
and
* c.d
, and property value X
for the
* parameter name, the empty map will be modified to {c:{d:X}}
.
*
* If no value is found the nesting is still created. If the previous example
* is modified by removing the value X
for a.b.c.d
* then the resulting map will be {c:{}}
. This is so the caller
* can look up a.b.c
without a null pointer exception.
*
* @param properties The underlying properties collection
* @param name The property name to look up and use for nesting
* @param map The out-parameter map to be modified
* @param segments The segments to create the nesting from
* @return The modified out-parameter map
*/
private static Map insert(Properties properties, String name, Map map, List segments) {
if (segments.size() < 1)
throw new IllegalArgumentException("Empty segments list [$name] [$map]");
String key = segments.get(0);
if (1 < segments.size())
// We're not yet at the end of the segments list... insert the tail:
insertTail(properties, name, map, key, segments.subList(1, segments.size()));
else
// We've reached the end of the segments list; time to put in the value:
insertValue(name, map, key, properties.getProperty(name));
// Return the map, modified to hold the parameter segments with the
// value for the parameter name at the botton of the nesting:
return map;
}
@SuppressWarnings("unchecked")
private static void insertTail(Properties properties, String name, Map map, String key, List tail) {
if (!map.containsKey(key)) {
// The parameter map does not have a value for the key; then
// we create a new map that just contains the tail segments
// with the last segment holding an empty map:
map.put(key, insert(properties, name, new HashMap<>(), tail));
return;
}
try {
Object value = map.get(key);
// The parameter map does have a value for the key; we attempt
// adding the tail segments and throw an exception if the key
// value is not a map, or there is a similar conflict further
// down:
insert(properties, name, (Map) value, tail);
} catch (Throwable t) {
// We can get a ClassCastException here, for example if the value
// of the key `version` is 1, and we attempt to add `version.short`.
//
// Oops, the parameter segments are incompatible with the
// existing properties; there is no way to proceed (because
// overwriting is not allowed):
throw new RuntimeException(String.format(
"Property insertion for %s failed: [%s]", name, map.get(key)), t);
}
}
private static void insertValue(String name, Map map, String key, String value) {
if (null == value)
return; // Nothing to do
// Don't trim the value or check for the empty string
if (map.containsKey(key))
// There is a conflict (and overwriting is not allowed):
throw new IllegalStateException("Second value found for property " + name + ": [" + map.get(key) + "] [" + value + "]");
else
// There is a value, and no conflict - insert it:
map.put(key, value);
// If there is no value then we do nothing
}
}