InfinityDBMap Example Code

All InfinityDB database access can optionally use a standard java.util.concurrent.ConcurrentNavigableMap interface. There are many extensions to provide the full generality of the underlying ‘ItemSpace’ data model.  Map access can be freely intermixed with access via the lower-level ItemSpace. This code is in the distribution zip.

/*
Copyright (c) 2019 Boiler Bay Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

package com.infinitydb.examples;

import java.io.BufferedReader;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Set;

import com.boilerbay.parsing.Parser;
import com.infinitydb.Attribute;
import com.infinitydb.ByteString;
import com.infinitydb.EntityClass;
import com.infinitydb.Index;
import com.infinitydb.InfinityDB;
import com.infinitydb.map.db.InfinityDBMap;
import com.infinitydb.map.db.InfinityDBSet;


/**
 * MapHelloWorld. a hello world for InfinityDB Maps.
 * 

 * The InfinityDBMap is a wrapper that provides the standard java.util.Map model
 * for easy usage atop the lower-level InfinityDB 'engine'. The InfinityDBMap is
 * a ConcurrentNavigableMap, meaning it is not only Thread-safe, but that it
 * efficiently uses all cores and has specialized methods. It is also Navigable,
 * which means it is a subset of SortedSet and has many other features.
 * 

 * The InfinityDBMaps are not themselves stored, but are only used for access to
 * a set of logical 'tuples' (which are physically 'Items' inside the
 * InfinityDB). The tuples hold a variable number of primitive elements, sorted
 * by element data type and then element value, with shorter ones coming first.
 * Each tuple element is strongly typed internally (not just embedded in a
 * string for example, which requires slow parsing and conversion during access
 * like JSON or XML). The fact that the Maps are not actually stored but are
 * only accessors for tuples allows for various kinds of forwards and backwards
 * database compatibility.
 * 

 * An InfinityDBMap is a light-weight object, and Maps can be efficiently
 * created dynamically. They are thread-safe and immutable. Each has a fixed
 * 'prefix' which is the initial elements of the tuple that the Map corresponds
 * to. The Map can access tuples having that prefix. The prefix is determined by
 * the nesting of the Maps.
 * 

 * It is possible to drop down to the basic InfinityDB database operations for
 * more flexibility and more speed in some cases. Some of the InfinityDBMap
 * operations are not atomic, while all InfinityDB operations are atomic. To get
 * atomicity, use the InfinityDB transactionality (see
 * InfinityDB.getTransactionManager()).
 * 

 * There is also an optional 'schema' pattern not shown here for
 * entityClass/Entity/Attribute/Value structures, which are a superset of the
 * relational model. These features help with forwards- and backwards-compatible
 * schema evolution. However, unlimited applications can be built using just the
 * InfinityDBMap with nesting shown here.
 * 

 * Supported data types include the Java primitives, primitive wrappers like
 * String and Long etc., short byte strings, short byte arrays, Binary Long
 * Objects (via OutputStream/InputStream), Character Long Objects (via
 * Writer/Readers), and an "EntityClass" and an "Attribute" special type. The
 * iterators provide entries in ascending native key order. (Integral types are
 * all converted to Longs).
 * 

 * A tuple like ("hello world!") is seen as a single element by a Set, but by a
 * Map it is a key/value pair with a null value. InfinityDBMap by default allows
 * null values for flexibility as shown here, but if desired,
 * InfinityDBMap.isAllowingNullValues(false) may be used to change the behavior
 * so that nulls will throw NullValueException for put() and so on. The default
 * behavior is like HashMap and TreeMap, but ConcurrentSkipListMap and
 * ConcurrentHashMap do not allow null values.
 * 
 * @author Roger Deran
 */
public class MapHelloWorld {
    static final String DB_NAME = "hello_world_database.infdb";
    static final String ELEMENT = "hello world!";
    static final String KEY = "key";
    static final String KEY1 = "key1";
    static final String KEY2 = "key2";
    static final String SUB_KEY = "sub_key";
    static final String SUB_KEY2 = "sub_key2";

    public static void main(String... args) {
        try {
            helloWorldUsingSet();
            helloWorldUsingMap();
            helloWorldUsingNestedMaps();
            helloWorldUsingAMultiValueMap();
            helloWorldUsingACharacterLongObject();
            helloWorldMultipleMaps();
            helloWorldRealDataNestedLoops();
            helloWorldRealDataRecursively();
            helloWorldRealDataRecursivelyDensely();
            helloWorldRealDataPrintingJSON();
            helloWorldRealDataPrintingXML();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    /**
     * Put an element into an InfinityDBSet and retrieve. The InfinityDB can
     * also be used for Maps or nested Maps mixed in. This stores a tuple like
     * (ELEMENT).
     * 
     * One does not normally open and close InfinityDB instances on every use as
     * shown here, since it is relatively heavy. A cache is opened having at
     * least a few 5KB blocks. The cache can by default grow to 100MB. Also, an
     * I/O thread is created. If data is to be forced to disk, a db.commit() is
     * invoked, or else to drop the data, a db.rollBack() is invoked. Try to
     * keep the database open all the time, but do close() so the file lock is
     * removed.
     */
    static void helloWorldUsingSet() throws Exception {
        try (InfinityDB db = InfinityDB.create(DB_NAME, true/* overwrite */)) {
            Set<String> set = new InfinityDBSet<String>(db);
            set.add(ELEMENT);
        }

        try (InfinityDB db = InfinityDB.open(DB_NAME, true/* writeable */)) {
            NavigableSet<String> set = new InfinityDBSet<String>(db);
            System.out.println("set: " + set.first());
        }
    }

    /**
     * Put a (KEY, "hello world!") pair into an InfinityDBMap and retrieve it.
     * This also causes the KEY to appear in an InfinityDBSet on the same db.
     */
    static void helloWorldUsingMap() throws Exception {
        try (InfinityDB db = InfinityDB.create(DB_NAME, true/* overwrite */)) {
            Map<String, String> map = new InfinityDBMap<String, String>(db);
            map.put(KEY, "hello world!");
            System.out.println("map: " + map.get(KEY));
        }
    }

    /**
     * Put a (KEY, SUB_KEY, "hello world!") triple into the db using a nested
     * InfinityDBMap and retrieve.
     * 
     * These nested Maps or Sets are not stored - only pairs, triples or longer
     * tuples are stored. Thus nesting the Maps actually only provides a way to
     * deal with tuples having certain fixed prefixes determined by the nesting
     * keys. It is reasonable to make KEY or SUB_KEY into a variable: the
     * getMap() is actually very fast.
     */
    static void helloWorldUsingNestedMaps() throws Exception {
        // Store a string into an InfinityDBMap nested inside another one
        try (InfinityDB db = InfinityDB.create(DB_NAME, true/* overwrite */)) {
            InfinityDBMap<String, String> map = new InfinityDBMap<String, String>(db);
            Map<String, String> nestedMap = map.getMap(KEY);
            nestedMap.put(SUB_KEY, "hello world!");
            System.out.println("nestedMap: " + nestedMap.get(SUB_KEY));
        }
    }

    /**
     * Put a (KEY, "hello world!") pair into a multi-value InfinityDBMap and
     * retrieve. An unlimited number of additional pairs or tuples like (KEY,
     * "hello world! again!") can be used.
     * 
     * Any existing InfinityDBMap can become capable of storing multiple values
     * simply by using the Set returned by its getSet(key).
     * 
     * These Map and Set Objects are not stored - only pairs, triples or longer
     * tuples are stored as determined by each Map's or Set's immutable prefix
     * tuple.
     */
    static void helloWorldUsingAMultiValueMap() throws Exception {
        try (InfinityDB db = InfinityDB.create(DB_NAME, true/* overwrite */)) {
            NavigableSet<String> set = new InfinityDBMap<String, String>(db).getSet(KEY);
            // Note that "hello world again!" comes out last! Not a bug.
            set.add("hello world! again!");
            set.add("hello world!");

            System.out.println("multi-value Map, first value: " + set.first());
            for (String s : set)
                System.out.println("multi-value Map, values iterator: " + s);
        }
    }

    /**
     * Put a Character Long Object into an InfinityDBMap as a value and retrieve
     * it. (It is stored internally as a set of triples like <KEY, block#, * charArrayBlock>).
     */
    static void helloWorldUsingACharacterLongObject() throws Exception {
        // Store a string into an InfinityDBMap nested inside another one
        try (InfinityDB db = InfinityDB.create(DB_NAME, true/* overwrite */)) {
            Writer writer = new InfinityDBMap<String, Writer>(db).getWriter(KEY);
            PrintWriter printer = new PrintWriter(writer);
            printer.println("hello world!");
            writer.close();

            Reader reader = new InfinityDBMap<String, Reader>(db).getReader(KEY);
            BufferedReader br = new BufferedReader(reader);
            System.out.println("Character Long Object: " + br.readLine());
        }
    }

    /**
     * Keep multiple nested Maps around for quick use. They are immutable and
     * thread-safe. It prints "multipleNestedMaps: hello world!, hello world!"
     */
    static void helloWorldMultipleMaps() throws Exception {
        // Store a string into an InfinityDBMap nested inside another one
        try (InfinityDB db = InfinityDB.create(DB_NAME, true/* overwrite */)) {
            InfinityDBMap<String, String> map = new InfinityDBMap<String, String>(db);
            Map<String, String> nestedMap1 = map.getMap("map1");
            Map<String, String> nestedMap2 = map.getMap("map2");

            nestedMap1.put(SUB_KEY, "hello world!");
            nestedMap2.put(SUB_KEY, "hello world!");
            System.out.println("multipleNestedMaps: " +
                    nestedMap1.get(SUB_KEY) + ", " +
                    nestedMap2.get(SUB_KEY));
        }
    }

    /**
     * Iterate the set of tuples using nested Maps. We assume a maximum depth of
     * 5 here. Of course this is only a demonstration of iterators. See the
     * proper recursive technique below.
     */
    static void helloWorldRealDataNestedLoops() throws Exception {
        try (InfinityDB db = InfinityDB.create(DB_NAME, true/* overwrite */)) {

            InfinityDBMap rootMap = makeRealDataMap(db);
            System.out.println("\nReal data printed with nested loops:");

            // Simple way to dump - non-recursive, limited depth.
            for (Object o0 : rootMap.keySet()) {
                printlnIndented(System.out, o0, 0);
                InfinityDBMap<Object, Object> map1 = rootMap.getMap(o0);
                for (Object o1 : map1.keySet()) {
                    printlnIndented(System.out, o1, 4);
                    InfinityDBMap<Object, Object> map2 = map1.getMap(o1);
                    for (Object o2 : map2.keySet()) {
                        printlnIndented(System.out, o2, 8);
                        InfinityDBMap<Object, Object> map3 = map2.getMap(o2);
                        for (Object o3 : map3.keySet()) {
                            printlnIndented(System.out, o3, 12);
                            InfinityDBMap<Object, Object> map4 = map3.getMap(o3);
                            for (Object o4 : map4.keySet()) {
                                printlnIndented(System.out, o4, 16);
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Print a set of tuples using nested Maps recursively.
     */
    static void helloWorldRealDataRecursively() throws Exception {
        System.out.println("\nReal data printed recursively:");
        try (InfinityDB db = InfinityDB.create(DB_NAME, true/* overwrite */)) {
            InfinityDBMap<Object, Object> rootMap = makeRealDataMap(db);
            printMapRecursively(System.out, rootMap, 0);
        }
    }

    static void printMapRecursively(PrintStream out, InfinityDBMap<Object, Object> map, int depth) {
        for (Object o : map.keySet()) {
            printlnIndented(out, o, depth);
            InfinityDBMap<Object, Object> subMap = map.getMap(o);
            if (!subMap.isEmpty()) {
                printMapRecursively(out, subMap, depth + 4);
            }
        }
    }

    static void helloWorldRealDataRecursivelyDensely() throws Exception {
        System.out.println("\nReal data printed recursively densely:");
        try (InfinityDB db = InfinityDB.create(DB_NAME, true/* overwrite */)) {
            InfinityDBMap<Object, Object> rootMap = makeRealDataMap(db);
            printMapRecursivelyDensely(System.out, rootMap, 0);
        }
        System.out.println();
    }

    /**
     * Print an InfinityDBMap, squashing lines together when possible. This
     * results in one line per tuple, which represents the database directly and
     * simply. Doesn't work with char[], byte[], or ByteString.
     */
    static void printMapRecursivelyDensely(PrintStream out, InfinityDBMap<Object, Object> map, int indention) {
        boolean isFirst = true;
        String s = null;
        for (Object o : map.keySet()) {
            s = o instanceof String ? Parser.formatJavaQuotedString((String)o) : o.toString();
            if (isFirst) {
                isFirst = false;
                out.print(s + " ");
            } else {
                out.println();
                printIndented(out, o, indention);
                out.print(" ");
            }
            InfinityDBMap<Object, Object> subMap = map.getMap(o);
            if (!subMap.isEmpty()) {
                printMapRecursivelyDensely(out, subMap, indention + 1 + s.length());
            }
        }
    }

    static void printlnIndented(PrintStream out, Object o, int depth) {
        printIndented(out, o, depth);
        out.println();
    }

    /**
     * Properly handles String, Boolean, Double, Float, Long, EntityClass,
     * Attribute, and Index. Does not handle byte[] char[] or ByteString.
     * The byte[] and char[] are necessary to handle LOBs.
     */
    static void printIndented(PrintStream out, Object o, int depth) {
        for (int i = 0; i < depth; i++)
            out.print(' ');
        out.print(o instanceof String ? Parser.formatJavaQuotedString((String)o) :
                o instanceof Float ? o + "f" : o);
    }

    /**
     * Print a real-data InfinityDBMap as JSON.
     */
    static void helloWorldRealDataPrintingJSON() throws Exception {
        try (InfinityDB db = InfinityDB.create(DB_NAME, true/* overwrite */)) {
            System.out.println("\nReal Data as JSON:");
            InfinityDBMap rootMap = makeRealDataMap(db);

            new JSONPrinter().print(System.out, rootMap, 0);

            System.out.println();
        }
    }

    /**
     * A recursive JSON printer for an InfinityDBMap.
     * 
     * This is only a demonstration. LOBs are not handled. Double quotes and
     * other special chars are not escaped in Strings. Sparse arrays can output
     * intermediate nulls for gaps. JSON restricts keys to quoted strings, while
     * we have any type at every position. The Index class is used to indicate
     * sparse arrays, rather than Longs, so Longs can be keys. Floats are
     * printed as Doubles.
     */
    static class JSONPrinter {

        void print(PrintStream out, InfinityDBMap<Object, Object> map, int depth)
                throws UnsupportedEncodingException {
            if (map.isEmpty()) {
                out.print("null");
                return;
            }
            Object o = map.firstKey();
            if (o instanceof Index) {
                // An array.
                long counter = 0;
                out.println('[');
                for (Object index : map.keySet()) {
                    if (counter > 0)
                        out.println(",");
                    InfinityDBMap<Object, Object> nestedMap = map.getMap(index);
                    // The index may have gaps if the array is sparse. Put in
                    // nulls if so.
                    while (((Index)index).getIndex() > counter) {
                        printIndented(out, "null", depth + 4);
                        out.println(",");
                        counter++;
                    }
                    printIndented(out, "", depth + 4);
                    print(out, nestedMap, depth + 4);
                    counter++;
                }
                out.println();
                printIndented(out, "]", depth);
            } else {
                // 'complex object' with keys and values.
                boolean isMultiValue = map.higherKey(o) != null;
                InfinityDBMap<Object, Object> nestedMap = map.getMap(o);
                Object key = nestedMap.get(o);
                boolean isComplex = !nestedMap.getMap(key).isEmpty();
                if (isMultiValue || isComplex) {
                    out.println('{');
                    long counter = 0;
                    for (Object key2 : map.keySet()) {
                        if (counter++ > 0)
                            out.println(",");
                        printIndented(out, jsonValue(key2) + " : ", depth + 4);
                        print(out, map.getMap(key2), depth + 4);
                    }
                    out.println("");
                    printIndented(out, "}", depth);
                } else {
                    // Simple value.
                    out.print("" + jsonValue(o));
                }
            }
        }

        /*
         * UTF-8 is not handled. Special InfinityDB types char[], byte[],
         * ByteString, EntityClass, and Attribute are not possible to represent
         * (we could use arrays of numbers or strings for byte[] and char[]).
         * Floats are output like Doubles, without the trailing 'f', to be
         * compatible with JSON numbers, but this means on parsing back, Floats
         * turn to Doubles, which is a narrowing cast!
         */
        static String jsonValue(Object o) throws UnsupportedEncodingException {
            return o == null ? "null" :
                    o instanceof String ? Parser.formatJavaQuotedString((String)o) :
                            o.toString();
        }

        static void printIndented(PrintStream out, String s, int depth) {
            for (int i = 0; i < depth; i++) {
                out.print(' ');
            }
            out.print(s);
        }
    }

    /**
     * Print a real-data InfinityDBMap as XML.
     */
    static void helloWorldRealDataPrintingXML() throws Exception {
        try (InfinityDB db = InfinityDB.create(DB_NAME, true/* overwrite */)) {
            System.out.println("\nReal Data as XML:");
            InfinityDBMap rootMap = makeRealDataMap(db);

            new XMLPrinter().print(System.out, rootMap);
        }
    }

    /**
     * Print an InfinityDBMap as XML for demonstration.
     * 
     * String content is not escaped. LOBs are not handled. InfinityDB byte[],
     * char[], and ByteString types are not handled. Otherwise, however, the
     * types of values are indicated properly: for example Float and Double and
     * Long are indicated in the 'type' attribute of the tag. Any element of the
     * tuple can be of any type (tuples and tuple elements correspond in the
     * underlying direct-access engine level to 'Items' and 'components').
     * 

     * Array elements are for sparse arrays - in other words they have indexes
     * that may but are not guaranteed to have gaps. Some array elements may
     * exist without corresponding contents, and these are called
     * 'null-array-element's, and they are not guaranteed to exist but may be
     * removed by a future parser possibly retaining the last one to preserve
     * the array length if there are nulls at the end.
     * 

     * Everything is in sorted order by key and value, not input tag sequence,
     * and any XML parser in the future will probably sort too. This format is
     * not guaranteed to be compatible with future XML printers or parsers, but
     * is only a demonstration.
     */
    static class XMLPrinter {

        void print(PrintStream out, InfinityDBMap<Object, Object> map)
                throws UnsupportedEncodingException {
            printlnIndented(out, "<infinitydb-map>", 0);
            print(out, map, 4);
            printlnIndented(out, "</infinitydb-map>", 0);
        }

        void print(PrintStream out, InfinityDBMap<Object, Object> map, int depth) {
            for (Object o : map.keySet()) {
                if (map.getMap(o).isEmpty()) {
                    // Each tuple generates one of these terminals
                    if (o instanceof Index)
                        printlnIndented(out, "<null-array-element index=\"" + ((Index)o).getIndex() + "\">", depth);
                    else
                        printlnIndented(out, "<element value=\"" + valueToString(o) + "\" type=\"" + valueTypeToString(o) + "\">", depth);
                } else {
                    // recurse to reach the end of the tuples.
                    if (o instanceof Index) {
                        printlnIndented(out, "<array-element index=\"" + ((Index)o).getIndex() + "\">", depth);
                        print(out, map.getMap(o), depth + 4);
                        printlnIndented(out, "</array-element>", depth);
                    } else {
                        printlnIndented(out, "<object key=\"" + valueToString(o) + "\" type=\"" + valueTypeToString(o) + "\">", depth);                        print(out, map.getMap(o), depth + 4);                        printlnIndented(out, "</object>", depth);
                    }
                }
            }
        }

        static String valueTypeToString(Object o) {
            if (o instanceof String)
                return "string";
            else if (o instanceof Boolean)
                return "boolean";
            else if (o instanceof Long)
                return "long";
            else if (o instanceof Double)
                return "double";
            else if (o instanceof Float)
                return "float";
            else if (o instanceof Index)
                return "index";
            else if (o instanceof Attribute)
                return "attribute";
            else if (o instanceof EntityClass)
                return "entity.class";
            else if (o instanceof ByteString)
                return "byte.string";
            else if (o instanceof char[])
                return "char.array";
            else if (o instanceof byte[])
                return "byte.array";
            else
                return "unhandled.type";
        }

        /*
         * Doesn't work for LOBs or char[] or byte[] or ByteString, whose
         * contents are lost.
         */
        static String valueToString(Object o) {
            return String.valueOf(
                    o instanceof Index ? ((Index)o).getIndex() :
                            o instanceof Attribute ? ((Attribute)o).getId() :
                                    o instanceof EntityClass ? ((EntityClass)o).getId() :
                                            o);
        }

        static void printlnIndented(PrintStream out, String s, int depth) {
            for (int i = 0; i < depth; i++) {
                out.print(' ');
            }
            out.println(s);
        }
    }

    /**
     * The data creation is order-independent because we do not overwrite any
     * tuples. In other words, here we never do a <code>put(k,v)</code> when
     * there are already tuples having (..,k) as a prefix, or do an
     * <code>add(t)</code> when there are already tuples having (..,t) as a
     * prefix and so on.
     * 

     * The <code>putIndexed(long index, Object value)</code> is identical to but
     * clearer and possibly faster than <code>put(new Index(index),value)</code>
     * . An Index element in a tuple designates the position of the tuple in a
     * 'vertical' or 'huge' sparse array of one or more tuples (possibly
     * nested). A null element is indicated either by a tuple ending in an index
     * or else by a tuple for a given index being absent. Null array elements
     * read back in by a future parser would probably be preserved only for the
     * final element if null, but are optional elsewhere. The indexes have gaps
     * here intentionally to show possible sparseness.
     */
    static InfinityDBMap<Object, Object> makeRealDataMap(InfinityDB db) {
        InfinityDBMap<Object, Object> rootMap = new InfinityDBMap<Object, Object>(db);
        InfinityDBMap<Object, Object> greetings = rootMap.getMap("greetings from");
        InfinityDBMap<Object, Object> person = rootMap.getMap("person");

        // We could make Maps 'roger' and 'alden' to simplify here.
        greetings.getMap("roger").put("message", "hello world!");
        greetings.getMap("roger").put("duration", 42);
        greetings.getMap("roger").put("sincere", true);
        greetings.put("alden", "null valued tuple");
        greetings.getMap("alden").getMap("message").putIndexed(0, null);
        greetings.getMap("alden").getMap("message").putIndexed(1, "hello\n\"\n");
        greetings.getMap("alden").getMap("message").putIndexed(2, "world!");
        greetings.getMap("alden").getMap("message").putIndexed(4, "from inside a sparse array");
        greetings.getMap("alden").getMap("message").putIndexed(6, null);
        greetings.getMap("alden").put("duration", 42.0f);
        greetings.getMap("alden").put("another null valued tuple", null);
        greetings.getMap("alden").put("sincere", true);
        person.getMap("roger").put("age", 62);
        person.getMap("alden").put("age", 24);
        person.getMap("alden").getMap("work").getMapIndexed(0).put("location", "UCSC");
        return rootMap;
    }
}