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

oracle.kv.impl.api.table.TableJsonUtils Maven / Gradle / Ivy

/*-
 * Copyright (C) 2011, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This file was distributed by Oracle as part of a version of Oracle NoSQL
 * Database made available at:
 *
 * http://www.oracle.com/technetwork/database/database-technologies/nosqldb/downloads/index.html
 *
 * Please see the LICENSE file included in the top-level directory of the
 * appropriate version of Oracle NoSQL Database for a copy of the license and
 * additional information.
 */

package oracle.kv.impl.api.table;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import oracle.kv.impl.security.ResourceOwner;
import oracle.kv.impl.util.JsonUtils;
import oracle.kv.table.FieldDef;
import oracle.kv.table.Index;
import oracle.kv.table.SequenceDef;
import oracle.kv.table.Table;
import oracle.kv.table.TimeToLive;

import org.apache.avro.io.DecoderFactory;
import org.apache.avro.io.EncoderFactory;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ObjectWriter;
import org.codehaus.jackson.node.ArrayNode;
import org.codehaus.jackson.node.ObjectNode;

/**
 * This class provides utilities for interaction with Jackson JSON processing
 * libraries, as well as helpful JSON operations, for use in implementing
 * Tables.
 */
public class TableJsonUtils extends JsonUtils {

    /*
     * There are a number of string constants used for JSON input/output
     * as well as for generating proper Avro schema names.  These are all
     * defined here for simplicity.
     */

    /*
     * Tables JSON input/output format labels
     */
    final static String PARENT = "parent";
    final static String OWNER = "owner";
    final static String SHARDKEY = "shardKey";
    final static String PRIMARYKEY = "primaryKey";
    final static String CHILDREN = "children";
    final static String FIELDS = "fields";
    final static String JSON_VERSION = "json_version";
    final static String R2COMPAT = "r2compat";
    final static String CHILDTABLES = "childTables";
    final static String SYSTABLE = "sysTable";
    final static String DESC = "comment";
    final static String NULLABLE = "nullable";
    final static String MIN = "min";
    final static String MAX = "max";
    final static String MIN_INCL = "min_inclusive";
    final static String MAX_INCL = "max_inclusive";
    final static String COLLECTION = "collection";
    final static String ENUM_NAME = "enum_name";
    final static String TTL = "ttl";
    final static String PKEY_SIZES = "primaryKeySizes";
    /* related to indexes */
    final static String INDEXES = "indexes";
    final static String ANNOTATIONS = "annotations";
    final static String PROPERTIES = "properties";
    final static String TABLE = "table";

    /*
     * These are used for construction of JSON nodes representing FieldDef
     * instances.  Some are used for both tables and Avro schemas.
     *
     * Avro and Tables
     */
    final static String NAME = "name";
    final static String NAMESPACE = "namespace";
    final static String TYPE = "type";
    final static String TYPES = "types";
    final static String DEFAULT = "default";
    final static String ENUM_VALS = "symbols";
    final static String NULL = "null";
    final static String IDENTITY = "identity";
    final static String ALWAYS = "always";
    final static String SEQUENCE = "sequence";
    final static String START = "start";
    final static String INCREMENT = "increment";
    final static String CACHE = "cache";
    final static String CYCLE = "cycle";


    /*
     * Avro type strings
     */
    final static String RECORD = "record";
    final static String ENUM = "enum";
    final static String ARRAY = "array";
    final static String MAP = "map";
    final static String INT = "int";
    final static String LONG = "long";
    final static String STRING = "string";
    final static String BOOLEAN = "boolean";
    final static String DOUBLE = "double";
    final static String FLOAT = "float";
    final static String BYTES = "bytes";
    final static String FIXED = "fixed";
    final static String FIXED_SIZE = "size";
    final static String TIMESTAMP = "timestamp";
    final static String TIMESTAMP_PRECISION = "precision";

    private static final DecoderFactory decoderFactory =
        DecoderFactory.get();
    private static final EncoderFactory encoderFactory =
        EncoderFactory.get();

    public static ObjectMapper getObjectMapper() {
        return mapper;
    }

    static JsonFactory getJsonFactory() {
        return mapper.getJsonFactory();
    }

    static DecoderFactory getDecoderFactory() {
        return decoderFactory;
    }

    static EncoderFactory getEncoderFactory() {
        return encoderFactory;
    }

    /**
     * Translate the specified Base64 string into a byte array.
     */
    public static String encodeBase64(byte[] buf) {
        return mapper.convertValue(buf, String.class);
    }

    /**
     * Decode the specified Base64 string into a byte array.
     */
    public static byte[] decodeBase64(String str) {
        return mapper.convertValue(str, byte[].class);
    }

    /*
     * From here down are utility methods used to help construct tables
     * and fields from JSON.
     */

    /**
     * This is a generic method used to construct FieldDef objects from
     * the JSON representation of a table.  This code could be spread out
     * across the various classes in per-class fromJson() methods but there
     * is no particular advantage in doing so.  Code is better shared in one
     */
    static FieldDefImpl fromJson(ObjectNode node) {
        String nameString = getStringFromNode(node, NAME, false);
        String descString = getStringFromNode(node, DESC, false);
        String minString = getStringFromNode(node, MIN, false);
        String maxString = getStringFromNode(node, MAX, false);
        String sizeString = getStringFromNode(node, FIXED_SIZE, false);
        String typeString = getStringFromNode(node, TYPE, true);
        String precisionString = getStringFromNode(node, TIMESTAMP_PRECISION,
                                                   false);

        FieldDef.Type type = FieldDef.Type.valueOf(typeString.toUpperCase());
        switch (type) {
        case INTEGER:
            if (descString == null && minString == null && maxString == null) {
                return FieldDefImpl.integerDef;
            }
            return new IntegerDefImpl
                (descString,
                minString != null ? Integer.valueOf(minString) : null,
                maxString != null ? Integer.valueOf(maxString) : null);
        case LONG:
            if (descString == null && minString == null && maxString == null) {
                return FieldDefImpl.longDef;
            }
            return new LongDefImpl
                (descString,
                 minString != null ? Long.valueOf(minString) : null,
                 maxString != null ? Long.valueOf(maxString) : null);
        case DOUBLE:
            if (descString == null && minString == null && maxString == null) {
                return FieldDefImpl.doubleDef;
            }
            return new DoubleDefImpl
                (descString,
                 minString != null ? Double.valueOf(minString) : null,
                 maxString != null ? Double.valueOf(maxString) : null);
        case FLOAT:
            if (descString == null && minString == null && maxString == null) {
                return FieldDefImpl.floatDef;
            }
            return new FloatDefImpl
                (descString,
                 minString != null ? Float.valueOf(minString) : null,
                 maxString != null ? Float.valueOf(maxString) : null);
        case STRING:
            if (descString == null && minString == null && maxString == null) {
                return FieldDefImpl.stringDef;
            }
            Boolean minInclusive = getBoolean(node, MIN_INCL);
            Boolean maxInclusive = getBoolean(node, MAX_INCL);
            return new StringDefImpl
                (descString, minString, maxString, minInclusive, maxInclusive);
        case NUMBER:
            if (descString == null) {
                return FieldDefImpl.numberDef;
            }
            return new NumberDefImpl(descString);
        case BINARY:
            if (descString == null) {
                return FieldDefImpl.binaryDef;
            }
            return new BinaryDefImpl(descString);
        case FIXED_BINARY:
            int size = (sizeString == null ? 0 : Integer.valueOf(sizeString));
            return new FixedBinaryDefImpl(nameString, size, descString);
        case BOOLEAN:
            if (descString == null) {
                return FieldDefImpl.booleanDef;
            }
            return new BooleanDefImpl(descString);
        case TIMESTAMP:
            int precision = (precisionString == null ?
                             TimestampDefImpl.DEF_PRECISION :
                             Integer.valueOf(precisionString));
            return new TimestampDefImpl(precision, descString);
        case ARRAY:
        case MAP:
            JsonNode jnode = node.get(COLLECTION);
            if (jnode == null) {
                throw new IllegalArgumentException
                    ("Map and Array require a collection object");
            }
            FieldDefImpl elementDef = fromJson((ObjectNode) jnode);
            if (type == FieldDef.Type.ARRAY) {
                return FieldDefFactory.createArrayDef(elementDef, descString);
            }
            return  FieldDefFactory.createMapDef(elementDef, descString);
        case RECORD:
            JsonNode fieldsNode = node.get(FIELDS);
            if (fieldsNode == null) {
                throw new IllegalArgumentException
                    ("Record is missing fields object");
            }
            final RecordBuilder builder =
                TableBuilder.createRecordBuilder(nameString, descString);
            ArrayNode arrayNode = (ArrayNode) fieldsNode;
            for (int i = 0; i < arrayNode.size(); i++) {
                ObjectNode o = (ObjectNode) arrayNode.get(i);
                String fieldName = getStringFromNode(o, NAME, true);
                builder.fromJson(fieldName, o);
            }
            try {
                return (FieldDefImpl) builder.build();
            } catch (Exception e) {
                throw new IllegalArgumentException
                ("Failed to build record from JSON, field name: " + nameString );
            }
        case ENUM: {
            JsonNode valuesNode = node.get(ENUM_VALS);
            if (valuesNode == null) {
                throw new IllegalArgumentException
                    ("Enumeration is missing values");
            }
            arrayNode = (ArrayNode) valuesNode;
            String enumName = getStringFromNode(node, ENUM_NAME, true);
            String values[] = new String[arrayNode.size()];
            for (int i = 0; i < arrayNode.size(); i++) {
                values[i] = arrayNode.get(i).asText();
            }
            return new EnumDefImpl(enumName, values, descString);
        }
        case JSON:
            if (descString == null) {
                return FieldDefImpl.jsonDef;
            }
            return new JsonDefImpl(descString);
        case ANY:
            return FieldDefImpl.anyDef;
        case ANY_ATOMIC:
            return FieldDefImpl.anyAtomicDef;
        case ANY_RECORD:
            return FieldDefImpl.anyRecordDef;
        case ANY_JSON_ATOMIC:
            return FieldDefImpl.anyJsonAtomicDef;
        case EMPTY:
        default:
            throw new IllegalArgumentException
                ("Cannot construct FieldDef type from JSON: " + type);
        }
    }

    /**
     * Adds an index definition from the ObjectNode (JSON) to the table.
     */
    static void indexFromJsonNode(ObjectNode node, TableImpl table) {
        ArrayNode fields = (ArrayNode) node.get(FIELDS);
        ArrayList fieldStrings = new ArrayList(fields.size());
        for (int i = 0; i < fields.size(); i++) {
            fieldStrings.add(fields.get(i).asText());
        }
        ArrayList typeStrings = null;
        ArrayNode types = (ArrayNode) node.get(TYPES);
        if (types != null) {
            typeStrings = new ArrayList(types.size());
            for (int i = 0; i < fields.size(); i++) {
                JsonNode typeNode = types.get(i);
                if (typeNode.isNull()) {
                    typeStrings.add(null);
                } else {
                    typeStrings.add(FieldDef.Type.valueOf(typeNode.asText()));
                }
            }
        }

        String name = getStringFromNode(node, NAME, true);
        String desc = getStringFromNode(node, DESC, false);
        Map annotations = getMapFromNode(node, ANNOTATIONS);
        Map properties = getMapFromNode(node, PROPERTIES);
        table.addIndex(new IndexImpl(name, table, fieldStrings, typeStrings,
                                     annotations, properties, desc));
    }

    /**
     * Build Table from JSON string
     *
     * NOTE: this format was test-only in R3, but export/import has made use
     * of it for R4. This means that changes must be made carefully and if
     * state is added to a table or index it needs to be reflected in the JSON
     * format.
     */
    public static TableImpl fromJsonString(String jsonString,
                                           TableImpl parent) {
        JsonNode rootNode = null;
        try {
            rootNode = getObjectMapper().readTree(jsonString);
        } catch (IOException ioe) {
            throw new IllegalArgumentException("IOException parsing Json: " +
                                               ioe);
        }
        return fromJson(rootNode, parent);
    }

    private static TableImpl fromJson(JsonNode rootNode,
                                      TableImpl parent) {
        /*
         * Create a TableBuilder for the table.
         */
        final String namespace = (rootNode.get(NAMESPACE) != null ?
                                  rootNode.get(NAMESPACE).asText() : null);

        TableBuilder tb = null;
        if (rootNode.get(SYSTABLE) != null) {
            tb = TableBuilder.createSystemTableBuilder(rootNode.get(NAME).
                                                       asText());
        } else {
            tb = TableBuilder.createTableBuilder
                (namespace, rootNode.get(NAME).asText(),
                 null, /* handle description below */
                 parent, true);
        }

        /*
         * Create the primary key and shard key lists
         */
        tb.primaryKey(makeListFromArray(rootNode, PRIMARYKEY));
        if (parent == null) {
            tb.shardKey(makeListFromArray(rootNode, SHARDKEY));
        }

        if (rootNode.get(DESC) != null) {
            tb.setDescription(rootNode.get(DESC).asText());
        }

        if (rootNode.get(R2COMPAT) != null) {
            tb.setR2compat(true);
        }

        /*
         * The current version doesn't put this field if there is no owner, but
         * older versions may have put null. Handle that.
         */
        if (rootNode.get(OWNER) != null &&
            !rootNode.get(OWNER).isNull()) {
            tb.setOwner(ResourceOwner.fromString(rootNode.get(OWNER).asText()));
        }

        if (rootNode.get(TTL) != null) {
            String ttlString = rootNode.get(TTL).asText();
            String[] ttlArray = ttlString.split(" ");
            if (ttlArray.length != 2) {
                throw new IllegalArgumentException(
                    "Invalid value for ttl string: " + ttlString);
            }
            tb.setDefaultTTL(TimeToLive.createTimeToLive(
                          Long.parseLong(ttlArray[0]),
                          TimeUnit.valueOf(ttlArray[1])));
        }

        if (rootNode.get(PKEY_SIZES) != null) {
            ArrayNode pks = (ArrayNode) rootNode.get(PKEY_SIZES);
            List pkey = tb.getPrimaryKey();
            assert pks.size() == pkey.size();
            for (int i = 0; i < pks.size(); i++) {
                int size = pks.get(i).asInt();
                if (size > 0) {
                    tb.primaryKeySize(pkey.get(i), size);
                }
            }
        }

        /*
         * Add fields.
         */
        ArrayNode arrayNode = (ArrayNode) rootNode.get(FIELDS);
        for (int i = 0; i < arrayNode.size(); i++) {
            ObjectNode node = (ObjectNode) arrayNode.get(i);
            String fieldName =
                getStringFromNode(node, NAME, true);
            if (parent == null ||
                !(parent).isKeyComponent(fieldName)) {
                tb.fromJson(fieldName, node);
            }
        }

        TableImpl newTable = tb.buildTable();

        /*
         * Add indexes if present
         */
        if (rootNode.get(INDEXES) != null) {
            arrayNode = (ArrayNode) rootNode.get(INDEXES);
            for (int i = 0; i < arrayNode.size(); i++) {
                ObjectNode node = (ObjectNode) arrayNode.get(i);
                indexFromJsonNode(node, newTable);
            }
        }

        /*
         * If child tables are fully specified here, add them. Note that
         * the JSON output format may include a simple list of child table
         * names. If that is the case there is nothing that can be done on
         * input because the child tables cannot be constructed from that
         * list, so they are ignored.
         */
        if (rootNode.get(CHILDTABLES) != null) {
            arrayNode = (ArrayNode) rootNode.get(CHILDTABLES);
            for (int i = 0; i < arrayNode.size(); i++) {
                ObjectNode node = (ObjectNode) arrayNode.get(i);
                TableImpl child = fromJson(node, newTable);
                newTable.getMutableChildTables().put(child.getName(), child);
            }
        }

        return newTable;
    }

    /**
     * Entry point for turning a TableImpl into a String
     */
    static String toJsonString(TableImpl table,
                               boolean pretty,
                               boolean includeChildren) {
        ObjectWriter writer = JsonUtils.createWriter(pretty);
        ObjectNode o = JsonUtils.createObjectNode();
        toJsonString(table, o, includeChildren);

        /*
         * Format the JSON into a string
         */
        try {
            return writer.writeValueAsString(o);
        } catch (IOException ioe) {
            return ioe.toString();
        }
    }

    /**
     * Create a JSON representation of the table and format
     * @param includeChildren if true, embed the JSON representation of the
     * child tables in the same JSON document.
     *
     * The general pattern is for optional fields to be left out entirely.
     * A format version is written so that the deserialization code can handle
     * changes in format over time.
     */
    public static void toJsonString(TableImpl table,
                                    ObjectNode o,
                                    boolean includeChildren) {
        /*
         * This is the serialization version of the table. It is new as of
         * release 4.4 and should increment whenever the format changes.
         */
        o.put(JSON_VERSION, 1);
        o.put(TYPE, "table");
        o.put(NAME, table.getName());

        if (table.getInternalNamespace() != null) {
            o.put(NAMESPACE, table.getInternalNamespace());
        }

        if (table.getDefaultTTL() != null) {
            o.put(TTL, table.getDefaultTTL().toString());
        }

        if (table.getOwner() != null) {
            o.put(OWNER, table.getOwner().toString());
        }

        if (table.isSystemTable()) {
            o.put(SYSTABLE, true);
        }

        if (table.isR2compatible()) {
            o.put(R2COMPAT, true);
        }

        if (table.getDescription() != null) {
            o.put(DESC, table.getDescription());
        }

        if (table.getParent() != null) {
            o.put(PARENT, table.getParent().getName());
        }
        ArrayNode key = o.putArray(SHARDKEY);
        for (String fieldName : table.getShardKeyInternal()) {
            key.add(fieldName);
        }
        key = o.putArray(PRIMARYKEY);
        for (String fieldName : table.getPrimaryKeyInternal()) {
            key.add(fieldName);
        }
        if (table.getPrimaryKeySizes() != null) {
            key = o.putArray(PKEY_SIZES);
            for (int size : table.getPrimaryKeySizes()) {
                key.add(size);
            }
        }

        /* Display limits if this is a top level table */
        if (table.isTop()) {
            final TableLimits limits = table.getTableLimits();
            if (limits != null) {
                limits.putLimits(o);
            }
        }

        /*
         * Add child tables as an array of table names.
         */
        final Map childTables = table.getChildTables();
        if (!childTables.isEmpty()) {
            ArrayNode childArray = o.putArray(CHILDREN);
            for (Map.Entry childEntry : childTables.entrySet()) {
                childArray.add(childEntry.getKey());
            }
        }

        /*
         * Add the fields.
         */
        table.getFieldMap().putFields(o);

        /*
         * Add indexes
         */
        final Map indexes = table.getIndexes();
        if (indexes.size() != 0) {
            ArrayNode indexArray = o.putArray(INDEXES);
            for (Map.Entry indexEntry : indexes.entrySet()) {
                IndexImpl impl = (IndexImpl) indexEntry.getValue();
                impl.toJsonNode(indexArray.addObject());
            }
        }

        if (!childTables.isEmpty() && includeChildren) {
            ArrayNode childArray = o.putArray(CHILDTABLES);
            for (Map.Entry childEntry : childTables.entrySet()) {
                ObjectNode cnode = childArray.addObject();
                toJsonString((TableImpl)childEntry.getValue(), cnode,
                             includeChildren);
            }
        }

        /*
         * Add identity.
         */
        if (table.hasIdentityColumn()) {
            ObjectNode idObj = o.putObject(IDENTITY);
            idObj.put(NAME, table.getFieldMap().getFieldName(
                table.getIdentityColumn()));
            idObj.put(ALWAYS, table.isIdentityGeneratedAlways());
            idObj.put(NULL, table.isIdentityOnNull());
            SequenceDef idSeq = table.getIdentitySequenceDef();
            if (idSeq != null) {
                idObj.put(SEQUENCE, ((SequenceDefImpl)idSeq).toJsonNode());
            }
        }
    }

    public static BigDecimal jsonParserGetDecimalValue(JsonParser parser)
        throws IOException {

        assert(parser != null);

        try {
            return parser.getDecimalValue();
        } catch (NumberFormatException nfe) {
            throw new JsonParseException("Malformed numeric value: '" +
                parser.getText(), parser.getCurrentLocation(), nfe);
        }
    }

    private static List makeListFromArray(JsonNode node,
                                                  String fieldName) {
        ArrayNode arrayNode = (ArrayNode) node.get(fieldName);
        if (arrayNode == null) {
            return null;
        }
        ArrayList keyList = new ArrayList(arrayNode.size());
        for (int i = 0; i < arrayNode.size(); i++) {
            keyList.add(i, arrayNode.get(i).asText());
        }
        return keyList;
    }

    /**
     * Returns the string value of the named field in the ObjectNode
     * if it exists, otherwise null.
     * @param node the containing node
     * @param name the name of the field in the node
     * @param required true if the field must exist
     * @return the string value of the field, or null
     * @throws IllegalArgumentException if the named field does not
     * exist in the node and required is true
     */
    private static String getStringFromNode(ObjectNode node,
                                            String name,
                                            boolean required) {
        JsonNode jnode = node.get(name);
        if (jnode != null) {
            return jnode.asText();
        } else if (required) {
            throw new IllegalArgumentException
                ("Missing required node in JSON table representation: " + name);
        }
        return null;
    }

    /**
     * Returns a Map of the named field in the ObjectNode
     * if it exists, otherwise null.
     * @param node the containing node
     * @param name the name of the field in the node
     * @return a map of the name/value pairs in the object, or null
     * @throws IllegalArgumentException if the node exists and it's not an
     * ObjectNode
     */
    private static Map getMapFromNode(ObjectNode node,
                                                     String name) {
        JsonNode jnode = node.get(name);
        if (jnode == null) {
            return null;
        }
        if (!(jnode instanceof ObjectNode)) {
            throw new IllegalArgumentException("Node is not an ObjectNode: " +
                                               name);
        }
        Map map = new HashMap();
        Iterator> iter = jnode.getFields();
        while (iter.hasNext()) {
            Map.Entry entry = iter.next();
            map.put(entry.getKey(), entry.getValue().asText());
        }
        return map;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy