
org.microbean.helm.chart.MapTree Maven / Gradle / Ivy
Show all versions of microbean-helm Show documentation
/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
*
* Copyright © 2017-2018 microBean.
*
* 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.microbean.helm.chart;
import java.util.Map;
import java.util.HashMap;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
final class MapTree {
/**
* A {@link Pattern} matching the shortest-length occurrence of a
* sequence of non-periods followed by an period.
*
* Given {@code a.b.c}, this should yield {@code a}, {@code
*/
private static final Pattern keyPattern = Pattern.compile("([^.]+)\\.?");
private final Map map;
MapTree(final Map map) {
super();
this.map = map;
}
/**
* Given a {@link Map} named {@code data} conceptually representing
* a leaf node in a tree structure, and a dot-separated {@code path}
* to that leaf node, returns a new {@link Map} of {@link Map}s
* reflecting the path in question with the supplied {@code data}
* {@link Map} as the leaf node in the return value.
*
* This method may return {@code null} in certain
* circumstances.
*
* As an example, given a {@code path} parameter value of "{@code
* a.b}" and a {@code data} {@link Map} containing one key, "{@code
* c}", whose value is "{@code d}", the return value will be a
* {@link Map} with one key, "{@code a}". The value of that key
* will be a {@link Map} with one key, "{@code b}". The value of
* that key will be the supplied {@code data} {@code Map}.
*
* @param path a period ("{@code .}")-separated path; must not be
* {@code null}
*
* @param data the {@link Map} serving as the terminal leaf; may be
* {@code null}
*
* @return a {@link Map} reflecting the new structure, or {@code
* null} if {@code path} is non-{@code null} but {@linkplain
* String#isEmpty() empty}, or if {@code data} is {@code null} and
* {@code path} is equal to "{@code .}"
*
* @exception NullPointerException if {@code path} is {@code null}
*/
static final Map newMapChain(final String path, final Map data) {
Objects.requireNonNull(path);
Map returnValue = null;
if (path.equals(".")) {
returnValue = data;
} else if (!path.isEmpty()) {
/*
Ported from pkg/chartutil/requirements.go.pathToMap().
The Go code in pkg/chartutil/requirements.go (pathToMap())
allocates too many objects only to throw them away. For a
path of "A.B.C", it does this:
n[0]: [A]->m0
n[1]: [B]->m1
n[2]: [C]->m2
That is, a list, n, of maps contains (in this case) three
maps: n0, n1 and n2 at positions 0, 1 and 2.
Each map has one key. The key is initially pointed to a new
map, m0, m1 and m2, which is allocated and then thrown away
without ever being used!
Then the list is walked. i is the number of the map being
looked at. z is the next index. k is the key under
consideration.
n0: [A]->m0 (i=0) (A is the sole key; k=A) (z=1)
n1: [B]->m1 (i=1) (B is the sole key; k=B) (z=2)
n2: [C]->m2 (i=2) (C is the sole key; k=C) (z=3)
If z == 3, then we're looking at n[2] and so n[2][C] = data.
Otherwise: n[0][A] = n[1] = [B]->m1 and n[1][B] = n[2] = [C]->m2
We can do lots better by simply using a regular expression and
tracking when it is done, setting our links as we go along.
*/
final Matcher matcher = keyPattern.matcher(path);
assert matcher != null;
Map priorMap = null;
while (matcher.find()) {
// While there are still keys in the path...
final String key = matcher.group(1);
assert key != null;
final Map newMap = new HashMap<>();
if (matcher.hitEnd()) {
// If we are on the last key in the path, the value of the
// key will be the supplied data Map.
newMap.put(key, data);
} else {
// If we are not on the last key in the path, the value of
// the key is not yet known.
newMap.put(key, null);
}
if (priorMap == null) {
// We're working on the first (possibly only) key in the
// path. The Map housing it is by definition the Map to be
// returned.
assert returnValue == null;
returnValue = newMap;
} else {
// We're working on a key deep in the path. The returnValue
// must have already been set.
assert returnValue != null;
assert priorMap.size() == 1;
priorMap.entrySet().iterator().next().setValue(newMap);
}
priorMap = newMap;
}
}
return returnValue;
}
/**
* Calls the {@link #get(String, Class)} method with the supplied
* {@code path} and {@link Map Map.class} as parameter values and
* returns the result.
*
* This method may return {@code null}.
*
* This method deliberately—unsafely—casts the result
* to a Map<String, Object>
.
*
* @param path a period ("{@code .}")-separated path of {@link
* String}s that will serve as keys in a {@link Map}; must not be
* {@code null}
*
* @return a {@link Map}, or {@code null}
*
* @exception NullPointerException if {@code path} is {@code null}
*
* @see #get(String, Class)
*/
final Map getMap(final String path) {
final Map, ?> map = this.get(path, Map.class);
if (map == null) {
return null;
}
@SuppressWarnings("unchecked")
final Map returnValue = (Map)map;
return returnValue;
}
/**
* Given a dot-separated {@code path} and a {@code type} to cast the
* return value to, traverses the path and returns the terminal
* result, provided it {@linkplain Class#isInstance(Object) is an
* instance} of the supplied {@code type}.
*
* This method may return {@code null}.
*
* @param path a period ("{@code .}")-separated path of {@link
* String}s that will serve as keys in a {@link Map}; must not be
* {@code null}
*
* @param type the {@link Class} to attempt to cast the terminal
* result to; must not be {@code null}; if the terminal result is
* not {@linkplain Class#isInstance(Object) an instance of} this
* {@link Class} then {@code null} will be returned
*
* @return the result of traversing the supplied {@code path},
* provided that it exists and is {@linkplain
* Class#isInstance(Object) an instance of} the supplied {@code
* type}, or {@code null}
*
* @exception NullPointerException if either {@code path} or {@code
* type} is {@code null}
*/
final V get(final String path, final Class type) {
Objects.requireNonNull(path);
Objects.requireNonNull(type);
Object returnValue = null;
Map map = this.map;
if (map != null && !map.isEmpty() && !path.isEmpty()) {
final Matcher matcher = keyPattern.matcher(path);
assert matcher != null;
while (map != null && matcher.find()) {
final String key = matcher.group(1);
assert key != null;
returnValue = map.get(key);
if (returnValue instanceof Map) {
@SuppressWarnings("unchecked")
final Map temp = (Map)returnValue;
map = temp;
} else {
if (!matcher.hitEnd()) {
returnValue = null;
}
map = null;
}
}
}
if (returnValue == null || !type.isInstance(returnValue)) {
return null;
} else {
return type.cast(returnValue);
}
}
final Object put(final String path, final Object value) {
Map map = this.map;
Object returnValue = null;
if (path == null || path.isEmpty()) {
if (map != null) {
returnValue = map.put(path, value);
}
} else {
final Matcher matcher = keyPattern.matcher(path);
assert matcher != null;
while (map != null && matcher.find()) {
final String key = matcher.group(1);
assert key != null;
assert !key.isEmpty();
final Object object = map.get(key);
if (object instanceof Map) {
@SuppressWarnings("unchecked")
final Map temp = (Map)object;
map = temp;
} else if (matcher.hitEnd()) {
returnValue = map.put(key, value);
} else {
final Map newMap = new HashMap<>();
map.put(key, newMap); // destructive operation
map = newMap;
}
}
}
return returnValue;
}
}