All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.brettonw.bedrock.bag.BagObject Maven / Gradle / Ivy

Go to download

Bag provides two container classes for text-based storage of constrained types in an array (BagArray) or as a map (BagObject), with a means of serializing objects to and from these container types.

The newest version!
package com.brettonw.bedrock.bag;

import com.brettonw.bedrock.bag.formats.FormatReader;
import com.brettonw.bedrock.bag.formats.FormatWriter;
import com.brettonw.bedrock.bag.formats.MimeType;
import com.brettonw.bedrock.logger.*;


/**
 * A collection of text-based values store in key/value pairs (maintained in a sorted array).
 */
public class BagObject extends Bag implements Selectable {
    private static final Logger log = LogManager.getLogger (BagObject.class);

    private static final int UNKNOWN_SIZE = -1;
    private static final int DEFAULT_CONTAINER_SIZE = 1;
    private static final int DOUBLING_CAP = 16;
    static final String PATH_SEPARATOR = "/";

    private class Pair {
        final String key;
        Object value;

        Pair (String key) {
            this.key = key;
        }
    }

    private Pair[] container;
    private int count;

    /**
     * Create a new BagObject with a default underlying storage size.
     */
    public BagObject () {
        this (UNKNOWN_SIZE);
    }

    /**
     * Create a new BagObject with hint for the underlying storage size.
     * @param size The expected number of elements in the BagObject, treated as a hint to optimize
     *             memory allocation. If additional elements are stored, the BagObject will revert
     *             to normal allocation behavior.
     */
    public BagObject (int size) {
        count = 0;
        container = new Pair[Math.max (size, DEFAULT_CONTAINER_SIZE)];
    }

    BagObject (SourceAdapter sourceAdapter) throws ReadException {
        // make the victim
        var victim = FormatReader.readBagObject (sourceAdapter);
        if (victim == null) {
            throw new ReadException ();
        }

        // now steal the victim's soul and leave them to die
        container = victim.container;
        count = victim.count;
    }

    /**
     * Create a new BagObject as deep copy of another BagObject
     */
    public BagObject (BagObject bagObject) {
        this (new SourceAdapter (bagObject.toString (MimeType.DEFAULT), MimeType.DEFAULT));
    }

    /**
     * Return the number of elements stored in the BagObject.
     *
     * @return the count of elements in the underlying store. This is distinct from the capacity of
     * the underlying store.
     */
    public int getCount () {
        return count;
    }

    private void grow (int gapIndex) {
        var src = container;
        if (count == container.length) {
            // if the array is smaller than the cap then double its size, otherwise just add the block
            var newSize = (count > DOUBLING_CAP) ? (count + DOUBLING_CAP) : (count * 2);
            container = new Pair[newSize];
            System.arraycopy (src, 0, container, 0, gapIndex);
        }
        System.arraycopy (src, gapIndex, container, gapIndex + 1, count - gapIndex);
        ++count;
    }

    private int binarySearch (String key) {
        // starting conditions mapped to either end of the internal store
        var low = 0;
        var high = count - 1;

        // loop as long as the bounds have not crossed
        while (low <= high) {
            // compute the midpoint, and compare the search term against the key stored there, this
            // uses the unsigned right shift in lieu of division by 2
            var mid = (low + high) >>> 1;
            var cmp = container[mid].key.compareTo (key);

            // check the result of the comparison
            if (cmp < 0) {
                // the current midpoint is below the target value, set 'low' to one past it so the
                // next loop will look only at the part of the array above the midpoint
                low = mid + 1;
            } else if (cmp > 0) {
                // the current midpoint is above the target value, set 'high' to one below it so the
                // next loop will look only at the part of the array below the midpoint
                high = mid - 1;
            } else {
                // "Found it!" she says in a sing-song voice
                return mid;
            }
        }
        // key not found, return an encoded version of where the key SHOULD be
        return -(low + 1);
    }

    private Pair getOrAddPair (String key) {
        // conduct a binary search for where the pair should be
        var index = binarySearch (key);
        if (index < 0) {
            // the binary search returns a funky encoding of the index where the new value
            // should go when it's not there, so we have to decode that number (-index - 1)
            index = -(index + 1);

            // make sure there is room in the underlying container, then store a new (empty) Pair
            grow (index);
            container[index] = new Pair (key);
        }
        return container[index];
    }

    /**
     * Return an object stored at the requested key value. The key may be a simple name, or it may
     * be a path (with keys separated by "/") to create a hierarchical "bedrock-of-bags" that is indexed
     * recursively.
     * 

* Using a binary search of the underlying store, finds where the first component of the path * should be and returns it. * * @param key A string value used to index the element, using "/" as separators, for example: * "com/brettonw/bedrock/key". * @return The indexed element (if found), or null */ @Override public Object getObject (String key) { // separate the key into path components, the "local" key value is the first component, so // use that to conduct the search. We are only interested in values that indicate the search // found the requested key var path = Key.split (key); var index = binarySearch (path[0]); if (index >= 0) { // grab the found element... if the path was only one element long, this is the element // we were looking for, otherwise recur on the found element as another BagObject var pair = container[index]; var found = pair.value; return (path.length == 1) ? found : ((Bag) found).getObject (path[1]); } return null; } /** * Store an object at the requested key value. The key may be a simple name, or it may be a path * (with keys separated by "/") to create a hierarchical "bedrock-of-bags" that is indexed * recursively. *

* Using a binary search of the underlying store, finds where the first component of the path * should be. If it does not already exist, it is created (recursively in the case of a path), * and the underlying store is shifted to make a space for it. The shift might cause the * underlying store to be resized if there is insufficient room. *

* Note that null values for the element are NOT stored at the leaf of the tree denoted by * a path, as returning null from getObject would be indistinguishable from a call to getObject * with an unknown key. This check is performed before the tree traversal, so the underlying * store will NOT contain the path after an attempt to add a null value. * * @param key A string value used to index the element, using "/" as separators, for example: * "com/brettonw/bedrock/key". * @param object The element to store. * @return The BagObject, so that operations can be chained together. */ public BagObject put (String key, Object object) { // convert the element to internal storage format, and don't bother with the rest if that's // a null value (per the docs above) object = objectify (object); if (object != null) { // separate the key into path components, the "local" key value is the first component, // so use that to conduct the search. If there is an element there, we want to get it, // otherwise we want to create it. var path = Key.split (key); var pair = getOrAddPair (path[0]); if (path.length == 1) { // this was the only key in the path, so it's the end of the line, store the value pair.value = object; } else { // this is not the leaf key, so we set the pair value to be a new BagObject if // necessary, then traverse via recursion, var bagObject = (BagObject) pair.value; if (bagObject == null) { pair.value = (bagObject = new BagObject ()); } bagObject.put (path[1], object); } } return this; } /** * Create a new BagObject and "put" the object using its key value. The key may be a * simple name, or it may be a path (with keys separated by "/") to create a * hierarchical "bedrock-of-bags" that is indexed recursively. * @param key A string value used to index the element, using "/" as separators, for example: * "com/brettonw/bedrock/key". * @param object The element to store. * @return The newly created BagObject. */ public static BagObject open (String key, Object object) { return new BagObject ().put (key, object); } /** * Add an object to a BagArray stored at the requested key. The key may be a simple name, or it may be a path * (with keys separated by "/") to create a hierarchical "bedrock-of-bags" that is indexed * recursively. If the key does not already exist a non-null value will be stored as a bare * value, just as if "put" had been called. If it does exist, and is not already an array or the * stored value is null, then a new array will be created to store any existing values and the * requested element. *

* Using a binary search of the underlying store, finds where the first component of the path * should be. If it does not already exist, it is created (recursively in the case of a path), * and the underlying store is shifted to make a space for it. The shift might cause the * underlying store to be resized if there is insufficient room. *

* Note that null values for the BagArray ARE stored per the design decision for arrays. * * @param key A string value used to index the element, using "/" as separators, for example: * "com/brettonw/bedrock/key". * @param object The element to store. * @return The BagObject, so that operations can be chained together. */ public BagObject add (String key, Object object) { // separate the key into path components, the "local" key value is the first component, // so use that to conduct the search. If there is an element there, we want to get it, // otherwise we want to create it. var path = Key.split (key); var pair = getOrAddPair (path[0]); if (path.length == 1) { // this is the end of the line, so we want to store the requested object var bagArray = (BagArray) null; var found = pair.value; if ((object = objectify (object)) == null) { if (found == null) { // 1) object is null, key does not exist - create array pair.value = (bagArray = new BagArray ()); } else if (found instanceof BagArray) { // 2) object is null, key exists (is array) bagArray = (BagArray) found; } else { // 3) object is null, key exists (is not array) - create array, store existing value pair.value = (bagArray = new BagArray (2)); bagArray.add (found); } // and store the null value in the array bagArray.add (null); } else { if (found == null) { // 4) object is not null, key does not exist - store as bare value pair.value = object; } else { if (found instanceof BagArray) { // 5) object is not null, key exists (is array) - add new value to array bagArray = (BagArray) found; } else { // 6) object is not null, key exists (is not array) - create array, store existing value, store new value pair.value = (bagArray = new BagArray (2)); bagArray.add (found); } bagArray.add (object); } } } else { // this is not the leaf key, so we set the pair value to be a new BagObject if // necessary, then traverse via recursion, var bagObject = (BagObject) pair.value; if (bagObject == null) { pair.value = (bagObject = new BagObject ()); } bagObject.add (path[1], object); } return this; } /** * Remove an object stored at the requested key. The key may be a simple name, or it may be a * path (with keys separated by "/") to create a hierarchical "bedrock-of-bags" that is indexed * recursively. *

* Using a binary search of the underlying store, finds where the element mapped to the key * should be, and removes it. If the element doesn't exist, nothing happens. If * the element is removed, the underlying store is shifted to close the space where it was. * removing elements will never cause the underlying store to shrink. * * @param key A string value used to index the element, using "/" as separators, for example: * "com/brettonw/bedrock/key". * @return The BagObject, so that operations can be chained together. */ public BagObject remove (String key) { var path = Key.split (key); var index = binarySearch (path[0]); if (index >= 0) { if (path.length == 1) { var gapIndex = index + 1; System.arraycopy (container, gapIndex, container, index, count - gapIndex); --count; } else { var found = (BagObject) container[index].value; found.remove (path[1]); } } return this; } /** * Return whether or not the requested key or path is present in the BagObject or hierarchical * "bedrock-of-bags" * * @param key A string value used to index the element, using "/" as separators, for example: * "com/brettonw/bedrock/key". * @return A boolean value, true if the key is present in the underlying store. Note that null * values are not stored (design decision), so this equivalent to checking for null. */ public boolean has (String key) { var path = Key.split (key); var index = binarySearch (path[0]); try { return (index >= 0) && ((path.length == 1) || ((BagObject) container[index].value).has (path[1])); } catch (ClassCastException classCastException) { // if a requested value is not a BagObject - this should be an exceptional case return false; } } /** * Returns an array of the keys contained in the underlying container. it does not enumerate the * container and all of its children. * * @return The keys in the underlying map as an array of Strings. */ public String[] keys () { var keys = new String[count]; for (int i = 0; i < count; ++i) { keys[i] = container[i].key; } return keys; } @Override public String toString (String format) { return FormatWriter.write (this, format); } @Override public BagObject select (SelectKey selectKey) { if (selectKey != null) { var bagObject = new BagObject (); var keys = keys (); for (var key : keys) { // XXX I need to think about this a bit, so that the selection keys can hoist // XXX sub-trees out of the object, I did this so it could be a simple yes/no // XXX query on the keys in the object, so the select key could be include or // XXX exclude... Maybe I should not try to be so clever... var asKey = selectKey.select (key); if (asKey != null) { bagObject.put (asKey, getObject (key)); } } return bagObject; } return this; } public static BagObject merge (BagObject... bagObjects) { var totalCount = 0; for (var bagObject : bagObjects) { totalCount += bagObject.getCount (); } var mergedBagObject = new BagObject (totalCount); for (var bagObject : bagObjects) { var keys = bagObject.keys (); for (var key : bagObject.keys ()) { mergedBagObject.put (key, bagObject.getObject (key)); } } return mergedBagObject; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy