com.cinchapi.common.collect.Association.java Source code

Java tutorial

Introduction

Here is the source code for com.cinchapi.common.collect.Association.java

Source

/*
 * 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.
 * <p>
 *
 * @author Jeff Nelson
 */
@NotThreadSafe
public abstract class Association extends AbstractMap<String, Object> {

    /**
     * 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}.
     * <p>
     * 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.
     * </p>
     * 
     * @param map
     * @return an {@link Association} containing all of the data in the
     *         {@code map}
     */
    public static Association ensure(Map<String, Object> 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.
     * <p>
     * NOTE: The returned {@link Association} DOES NOT read through to the
     * provided {@code map} and the state of both structures will immediately
     * diverge.
     * </p>
     * 
     * @return the new {@link Association} containing all of the data in the
     *         {@code map}
     */
    public static Association of(Map<String, Object> 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<String, Object> 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<Entry<String, Object>> 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> 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<T>) source).size() > index) {
                        source = Iterables.get((Collection<T>) source, index);
                    } else {
                        source = null;
                    }
                } else {
                    source = source instanceof Map ? ((Map<String, Object>) 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}.
     * <p>
     * NOTE: The returned value may be {@code null} if this {@link Associaiton}
     * permits the storage of {@code null} values.
     * </p>
     * 
     * @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> 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<String, Object> flatten() {
        Map<String, Object> 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<String, Object> 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<String, Object> map, BiFunction<Object, Object, Object> strategy) {
        Associations.forEachFlattened(map, (key, value) -> {
            Object stored = get(key);
            Object computed = stored == null ? value : strategy.apply(stored, value);
            set(key, computed);
        });
    }

    /**
     * Return a Set that contains all the fetchable paths in this
     * {@link Association}.
     * <p>
     * The returned {@link Set} does not "read-through" to the underlying
     * {@link Association} for subsequent changes.
     * </p>
     * 
     * @return the paths
     */
    public Set<String> paths() {
        Set<String> paths = Sets.newLinkedHashSet();
        flatten().keySet().forEach(key -> {
            String[] parts = key.split("\\.");
            StringBuilder sb = new StringBuilder();
            for (String path : parts) {
                sb.append(path);
                paths.add(sb.toString());
                sb.append(".");
            }
        });
        return paths;
    }

    @Override
    public Object put(String key, Object value) {
        return set(key, value);
    }

    /**
     * Set {@code value} at the end of the {@code path}.
     * 
     * @param path
     * @param value
     * @return the value that was previously at the end of the {@code path} or
     *         {@code null} if there was no value there before
     */
    @SuppressWarnings("unchecked")
    public <T> T set(String path, Object value) {
        T stored = (T) exploded.get(path);
        if (stored == null) {
            T prev = fetch(path);
            String[] components = path.split("\\.");
            Verify.thatArgument(components.length > 0, "Invalid path: {}", path);
            Verify.thatArgument(Ints.tryParse(components[0]) == null,
                    "The map cannot contain keys that start with a numeric component. "
                            + "Therefore '{}' is an invalid path.",
                    path);
            Stack<String> stack = new Stack<>();
            for (String component : components) {
                stack.add(component);
            }
            Object val = value;
            while (!stack.isEmpty()) {
                String key = stack.pop();
                Integer index;
                Object container = (index = Ints.tryParse(key)) != null ? collectionSupplier().get()
                        : mapSupplier().get();
                if (container instanceof Collection) {
                    Collection<Object> collection = (Collection<Object>) container;
                    for (int i = 0; i < index; ++i) {
                        collection.add(null);
                    }
                    collection.add(val);
                } else { // container instanceof Map
                    ((Map<String, Object>) container).put(key, val);
                }
                val = container;
            }
            Map<String, Object> map = (Map<String, Object>) val; // NOTE: This
                                                                 // cast is safe
                                                                 // because, the
                                                                 // verification
                                                                 // that the
                                                                 // first
                                                                 // component
                                                                 // isn't an
                                                                 // integer
                                                                 // ensures that
                                                                 // the path
                                                                 // itself is
                                                                 // begins with
                                                                 // a map.
            MergeStrategies.upsert(exploded, map); // Upsert the #val into the
                                                   // exploded collection
            return prev;
        } else {
            // The path was added directly to the map, so do the same on this
            // update
            return (T) exploded.put(path, value);
        }
    }

    @Override
    public String toString() {
        return exploded.toString();
    }

    /**
     * Return a {@link Supplier} for {@link Collection} containers.
     * 
     * @return the supplier
     */
    protected abstract Supplier<Collection<Object>> collectionSupplier();

    /**
     * Return a {@link Supplier} for {@link Map} containers.
     * 
     * @return the supplier
     */
    protected abstract Supplier<Map<String, Object>> mapSupplier();

    /**
     * An {@link Association} that is based on {@link LinkedHashMap} sorting.
     *
     * @author Jeff Nelson
     */
    private static class LinkedHashAssociation extends Association {

        @Override
        protected Supplier<Collection<Object>> collectionSupplier() {
            return ArrayList::new;
        }

        @Override
        protected Supplier<Map<String, Object>> mapSupplier() {
            return LinkedHashMap::new;
        }

    }

}