
com.cinchapi.common.collect.Association Maven / Gradle / Ivy
Show all versions of accent4j Show documentation
/*
* Copyright (c) 2013-2018 Cinchapi 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.cinchapi.common.collect;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import com.cinchapi.common.base.Verify;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.primitives.Ints;
/**
* An {@link Association} is a possibly nested/complex mapping from navigable
* paths (e.g. keys that use dots to indicate traversal into sub lists or
* objects) to values that are either other {@link Association Associations}
* (e.g. maps), collections or flat (e.g. primitive) objects.
*
*
* @author Jeff Nelson
*/
@NotThreadSafe
public abstract class Association extends AbstractMap {
/**
* Ensure that the {@code map} is already an {@link Association} or create a
* new {@link Association} that contains all of the contents in {@code map}.
*
* Unlike the {@link #of(Map)} factory, this one isn't guaranteed to create
* a new object that has a distinct state from the input. In particular, if
* the input is already an {@link Association}, the value returned from this
* factory will be the same instance. Otherwise, a new object is returned.
*
*
* @param map
* @return an {@link Association} containing all of the data in the
* {@code map}
*/
public static Association ensure(Map map) {
return map instanceof Association ? (Association) map : of(map);
}
/**
* Return an empty {@link Association}.
*
* @return the new {@link Association}
*/
public static Association of() {
return new LinkedHashAssociation();
}
/**
* Return an {@link Association} that contains the data in the {@code map},
* to facilitate path traversals.
*
* NOTE: The returned {@link Association} DOES NOT read through to the
* provided {@code map} and the state of both structures will immediately
* diverge.
*
*
* @return the new {@link Association} containing all of the data in the
* {@code map}
*/
public static Association of(Map map) {
LinkedHashAssociation association = new LinkedHashAssociation();
if(map instanceof Association) {
((Association) association).exploded = new LinkedHashMap<>(
((Association) map).exploded);
}
else {
// NOTE: The provided #map cannot be directly assigned as the
// #exploded member of the created Association because it is
// necessary to flatten the input map and go through the #set
// routine to ensure that any nested containers are properly
// flattened and made mutable.
Associations.forEachFlattened(map,
(key, value) -> association.set(key, value));
}
return association;
}
/**
* The entries in this {@link Association}. Internally, the data is
* maintained in exploded form to support efficient retrieval of partial
* paths.
*/
private Map exploded;
/**
* Construct a new instance.
*/
protected Association() {
this.exploded = mapSupplier().get();
}
@Override
public boolean containsKey(Object key) {
return get(key) != null;
}
@Override
public boolean containsValue(Object value) {
for (String path : paths()) {
Object stored = fetch(path);
if(stored.equals(value)) {
return true;
}
}
return false;
}
@Override
public Set> entrySet() {
return exploded.entrySet();
}
/**
* Return a possibly nested value from within the {@link Association}.
*
* @param path a navigable path key (e.g. foo.bar.1.baz)
* @return the value
*/
@SuppressWarnings("unchecked")
@Nullable
public T fetch(String path) {
T value = (T) exploded.get(path); // first, check to see if the path has
// been directly added to the map
if(value == null) {
String[] components = path.split("\\.");
Verify.thatArgument(components.length > 0, "Invalid path " + path);
Object source = exploded;
for (String component : components) {
Integer index;
if(source == null) {
break;
}
else if((index = Ints.tryParse(component)) != null) {
if(source instanceof Collection
&& ((Collection) source).size() > index) {
source = Iterables.get((Collection) source, index);
}
else {
source = null;
}
}
else {
source = source instanceof Map
? ((Map) source).get(component)
: null;
}
}
return source != null ? (T) source : null;
}
else {
return value;
}
}
/**
* Return a possibly nested value from with the {@link Associaiton}, if it
* is present. Otherwise, return the {@code defaultValue}.
*
* NOTE: The returned value may be {@code null} if this {@link Associaiton}
* permits the storage of {@code null} values.
*
*
* @param path a navigable path key (e.g. foo.bar.1.baz)
* @param defaultValue
* @return the associated value, if it exists or the {@code defaultValue}
*/
@Nullable
public T fetchOrDefault(String path, T defaultValue) {
T value;
return ((value = fetch(path)) != null || containsKey(path)) ? value
: defaultValue;
}
/**
* Return a one-dimensional map where the keys in this {@link Association}
* are flattened into paths and mapped to the values at the destination.
*
* @return a "flat" version of this {@link Association} as a {@link Map}
*/
public Map flatten() {
Map flattened = Maps.newLinkedHashMap();
Associations.forEachFlattened(exploded,
(key, value) -> flattened.put(key, value));
return flattened;
}
@Override
public Object get(Object key) {
return key instanceof String ? fetch((String) key) : null;
}
/**
* Merge the contents of the {@code map} into this {@link Association} using
* the {@link MergeStrategies#theirs() theirs} merge strategy.
*
* @param map
*/
public void merge(Map map) {
merge(map, MergeStrategies::theirs);
}
/**
* Merge the contents of the {@code map} into this {@link Association} using
* the provided merge {@code strategy}.
*
* @param map
* @param strategy
*/
public void merge(Map map,
BiFunction