oracle.kv.impl.api.table.MapValueImpl 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 static oracle.kv.impl.api.table.TableJsonUtils.jsonParserGetDecimalValue;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import oracle.kv.impl.api.table.ValueSerializer.FieldValueSerializer;
import oracle.kv.impl.api.table.ValueSerializer.MapValueSerializer;
import oracle.kv.impl.util.SizeOf;
import oracle.kv.table.ArrayValue;
import oracle.kv.table.FieldDef;
import oracle.kv.table.FieldValue;
import oracle.kv.table.MapDef;
import oracle.kv.table.MapValue;
import oracle.kv.table.RecordValue;
import com.sleepycat.persist.model.Persistent;
import org.codehaus.jackson.JsonLocation;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonParser.NumberType;
import org.codehaus.jackson.JsonToken;
import org.codehaus.jackson.node.JsonNodeFactory;
import org.codehaus.jackson.node.ObjectNode;
import org.codehaus.jackson.util.CharTypes;
/**
* MapValueImpl implements the MapValue interface and is a container object
* that holds a map of FieldValue objects all of the same type. The getters
* and setters use the same semantics as Java Map.
*
* TODO: JSON: if duplicate values are to be handled in some cases one option is
* to do this:
* 1. detect the duplicates on put by looking at the return value. If not null,
* save duplicates in a standalone list attached to this object, to be
* serialized separately.
* 2. on toString() output, or any output that might want to see all dups,
* output the dups first (because last-in wins).
* 3. normal map operations work on the actual copies and will not create dups.
*/
@Persistent(version=1)
public class MapValueImpl extends ComplexValueImpl
implements MapValue, MapValueSerializer {
private static final long serialVersionUID = 1L;
private final Map fields;
MapValueImpl(MapDef def) {
super(def);
fields = new TreeMap();
}
/* DPL */
private MapValueImpl() {
super(null);
fields = null;
}
/*
* Public api methods from Object and FieldValue
*/
@Override
public MapValueImpl clone() {
MapValueImpl map = new MapValueImpl(getDefinition());
for (Map.Entry entry : fields.entrySet()) {
map.put(entry.getKey(), entry.getValue().clone());
}
return map;
}
@Override
public long sizeof() {
long size = super.sizeof();
size += (SizeOf.OBJECT_REF_OVERHEAD + SizeOf.TREEMAP_OVERHEAD);
for (Map.Entry entry : fields.entrySet()) {
size += SizeOf.TREEMAP_ENTRY_OVERHEAD;
size += SizeOf.stringSize(entry.getKey());
size += ((FieldValueImpl)entry.getValue()).sizeof();
}
return size;
}
@Override
public int hashCode() {
int code = size();
for (Map.Entry entry : fields.entrySet()) {
code += entry.getKey().hashCode() + entry.getValue().hashCode();
}
return code;
}
@Override
public boolean equals(Object other) {
if (other instanceof MapValueImpl) {
MapValueImpl otherValue = (MapValueImpl) other;
/* maybe avoid some work */
if (this == otherValue) {
return true;
}
/*
* detailed comparison
*/
if (size() == otherValue.size() &&
getElementDef().equals(otherValue.getElementDef()) &&
getDefinition().equals(otherValue.getDefinition())) {
for (Map.Entry entry : fields.entrySet()) {
if (!entry.getValue().
equals(otherValue.get(entry.getKey()))) {
return false;
}
}
return true;
}
}
return false;
}
/**
* FieldDef must match.
*
* Compare field values in order of keys. The algorithm relies on the fact
* that fields is a SortedMap (TreeMap). Return as soon as there is a
* difference. If this object has a field the other does not, return >
* 0. If this object is missing a field the other has, return < 0.
* Compare both keys and values, keys first.
*/
@Override
public int compareTo(FieldValue other) {
if (other instanceof MapValueImpl) {
MapValueImpl otherImpl = (MapValueImpl) other;
if (!getDefinition().equals(otherImpl.getDefinition())) {
throw new IllegalArgumentException
("Cannot compare MapValues with different definitions");
}
/* this relies on the maps being sorted */
assert fields instanceof TreeMap;
assert otherImpl.fields instanceof TreeMap;
Iterator keyIter = fields.keySet().iterator();
Iterator otherIter = otherImpl.fields.keySet().iterator();
while (keyIter.hasNext() && otherIter.hasNext()) {
String key = keyIter.next();
String otherKey = otherIter.next();
int keyCompare = key.compareTo(otherKey);
if (keyCompare != 0) {
return keyCompare;
}
/*
* Keys are equal, values must exist.
*/
FieldValue val = fields.get(key);
FieldValue otherVal = otherImpl.fields.get(key);
int valCompare = val.compareTo(otherVal);
if (valCompare != 0) {
return valCompare;
}
}
/*
* The object with more keys is greater, otherwise they are equal.
*/
if (keyIter.hasNext()) {
return 1;
} else if (otherIter.hasNext()) {
return -1;
}
return 0;
}
throw new ClassCastException
("Object is not a MapValue");
}
@Override
public FieldDef.Type getType() {
return FieldDef.Type.MAP;
}
@Override
public boolean isMap() {
return true;
}
@Override
public MapValue asMap() {
return this;
}
/*
* Public api methods from MapValue
*/
@Override
public MapDefImpl getDefinition() {
return (MapDefImpl)fieldDef;
}
@Override
public int size() {
return fields.size();
}
@Override
public Map getFields() {
return Collections.unmodifiableMap(fields);
}
@Override
public FieldValue remove(String fieldName) {
return fields.remove(fieldName);
}
@Override
public FieldValueImpl get(String fieldName) {
return (FieldValueImpl)fields.get(fieldName);
}
@Override
public MapValue put(String name, int value) {
putScalar(name, getElementDef().createInteger(value));
return this;
}
@Override
public MapValue put(String name, long value) {
putScalar(name, getElementDef().createLong(value));
return this;
}
@Override
public MapValue put(String name, String value) {
putScalar(name, getElementDef().createString(value));
return this;
}
@Override
public MapValue put(String name, double value) {
putScalar(name, getElementDef().createDouble(value));
return this;
}
@Override
public MapValue put(String name, float value) {
putScalar(name, getElementDef().createFloat(value));
return this;
}
@Override
public MapValue putNumber(String name, int value) {
putScalar(name, getElementDef().createNumber(value));
return this;
}
@Override
public MapValue putNumber(String name, long value) {
putScalar(name, getElementDef().createNumber(value));
return this;
}
@Override
public MapValue putNumber(String name, float value) {
putScalar(name, getElementDef().createNumber(value));
return this;
}
@Override
public MapValue putNumber(String name, double value) {
putScalar(name, getElementDef().createNumber(value));
return this;
}
@Override
public MapValue putNumber(String name, BigDecimal value) {
putScalar(name, getElementDef().createNumber(value));
return this;
}
@Override
public MapValue put(String name, boolean value) {
putScalar(name, getElementDef().createBoolean(value));
return this;
}
@Override
public MapValue put(String name, byte[] value) {
putScalar(name, getElementDef().createBinary(value));
return this;
}
@Override
public MapValue putJsonNull(String name) {
if (!getElementDef().isJson()) {
throw new IllegalArgumentException(
"Cannot insert a JSON null into a non-JSON map");
}
fields.put(name, NullJsonValueImpl.getInstance());
return this;
}
/*
* This is only used internally for queries involving indexes on map keys
*/
MapValue putNull(String name) {
fields.put(name, NullValueImpl.getInstance());
return this;
}
@Override
public MapValue putFixed(String name, byte[] value) {
putScalar(name, getElementDef().createFixedBinary(value));
return this;
}
@Override
public MapValue putEnum(String name, String value) {
putScalar(name, getElementDef().createEnum(value));
return this;
}
@Override
public MapValue put(String name, Timestamp value) {
putScalar(name, getElementDef().createTimestamp(value));
return this;
}
@Override
public MapValue put(String fieldName, FieldValue value) {
value = validate(value, getElementDef());
fields.put(fieldName, value);
return this;
}
@Override
public RecordValueImpl putRecord(String fieldName) {
RecordValue val = getElementDef().createRecord();
fields.put(fieldName, val);
return (RecordValueImpl) val;
}
@Override
public MapValueImpl putMap(String fieldName) {
MapValue val = getElementDef().createMap();
fields.put(fieldName, val);
return (MapValueImpl) val;
}
@Override
public ArrayValueImpl putArray(String fieldName) {
ArrayValue val = getElementDef().createArray();
fields.put(fieldName, val);
return (ArrayValueImpl) val;
}
@Override
public MapValue putJson(String fieldName,
String jsonInput) {
Reader reader = new StringReader(jsonInput);
try {
return putJson(fieldName, reader);
} finally {
try { reader.close(); } catch (IOException ioe) {}
}
}
@Override
public MapValue putJson(String fieldName,
Reader jsonReader) {
put(fieldName, JsonDefImpl.createFromReader(jsonReader));
return this;
}
/*
* ComplexValueImpl internal api methods
*/
@Override
public Map getMap() {
return fields;
}
/**
* Add JSON fields to the map.
*/
@Override
public void addJsonFields(
JsonParser jp,
String currentFieldName,
boolean exact,
boolean addMissingFields) {
try {
FieldDef element = getElementDef();
JsonToken t = jp.getCurrentToken();
JsonLocation location = jp.getCurrentLocation();
if (t != JsonToken.START_OBJECT) {
jsonParseException(("Expected { token to start map, instead "
+ "found " + t), location);
}
while ((t = jp.nextToken()) != JsonToken.END_OBJECT) {
if (t == null || t == JsonToken.END_ARRAY) {
jsonParseException("Did not find end of object", location);
}
String fieldname = jp.getCurrentName();
JsonToken token = jp.nextToken();
/*
* A json null is valid only if the element type of
* the map is JSON.
*/
if (token == JsonToken.VALUE_NULL && !element.isJson()) {
throw new IllegalArgumentException
("Invalid null value in JSON input for field "
+ fieldname);
}
switch (element.getType()) {
case INTEGER:
checkNumberType(fieldname, NumberType.INT, jp);
put(fieldname, jp.getIntValue());
break;
case LONG:
checkNumberType(fieldname, NumberType.LONG, jp);
put(fieldname, jp.getLongValue());
break;
case DOUBLE:
checkNumberType(fieldname, NumberType.DOUBLE, jp);
put(fieldname, jp.getDoubleValue());
break;
case FLOAT:
checkNumberType(fieldname, NumberType.FLOAT, jp);
put(fieldname, jp.getFloatValue());
break;
case NUMBER:
checkNumberType(fieldname, NumberType.BIG_DECIMAL, jp);
putNumber(fieldname, jsonParserGetDecimalValue(jp));
break;
case STRING:
put(fieldname, jp.getText());
break;
case BINARY:
put(fieldname, jp.getBinaryValue());
break;
case FIXED_BINARY:
putFixed(fieldname, jp.getBinaryValue());
break;
case BOOLEAN:
put(fieldname, jp.getBooleanValue());
break;
case TIMESTAMP:
put(fieldname,
element.asTimestamp().fromString(jp.getText()));
break;
case ARRAY:
/*
* current token is '[', then array elements
* TODO: need to have a full-on switch for adding
* array elements of the right type.
*/
ArrayValueImpl array = putArray(fieldname);
array.addJsonFields(jp, null, exact, addMissingFields);
break;
case MAP:
MapValueImpl map = putMap(fieldname);
map.addJsonFields(jp, null, exact, addMissingFields);
break;
case RECORD:
RecordValueImpl record = putRecord(fieldname);
record.addJsonFields(jp, null, exact, addMissingFields);
break;
case ENUM:
putEnum(fieldname, jp.getText());
break;
case JSON:
case ANY_JSON_ATOMIC:
put(fieldname, JsonDefImpl.createFromJson(jp, false));
break;
case ANY:
case ANY_ATOMIC:
case ANY_RECORD:
case EMPTY:
case GEOMETRY:
case POINT:
throw new IllegalStateException(
"A map type cannot have " + element.getType() +
" as its element type");
}
}
} catch (IOException ioe) {
throw new IllegalArgumentException(
"Failed to parse JSON input: " + ioe.getMessage(), ioe);
} catch (RuntimeException re) {
if (re instanceof IllegalArgumentException) {
throw re;
}
throw new IllegalArgumentException(
"Failed to parse JSON input: " + re.toString(), re);
}
}
/*
* FieldValueImpl internal api methods
*/
@Override
public FieldValueImpl getFieldValue(String fieldName) {
return (FieldValueImpl)fields.get(fieldName);
}
/**
* Map is represented as ObjectNode. Jackson does not have a MapNode
*/
@Override
public JsonNode toJsonNode() {
ObjectNode node = JsonNodeFactory.instance.objectNode();
for (Map.Entry entry : fields.entrySet()) {
node.put(entry.getKey(),
((FieldValueImpl)entry.getValue()).toJsonNode());
}
return node;
}
@Override
public void toStringBuilder(StringBuilder sb) {
sb.append('{');
int i = 0;
for (Map.Entry entry : fields.entrySet()) {
String key = entry.getKey();
FieldValueImpl val = (FieldValueImpl)entry.getValue();
if (val != null) {
if (i > 0) {
sb.append(',');
}
sb.append('\"');
CharTypes.appendQuoted(sb, key);
sb.append('\"');
sb.append(':');
val.toStringBuilder(sb);
i++;
}
}
sb.append('}');
}
@SuppressWarnings("unchecked")
static MapValueImpl fromJavaObjectValue(FieldDef def, Object o) {
Map javaMap = (Map) o;
MapValue map = def.createMap();
for (Map.Entry entry : javaMap.entrySet()) {
String key = entry.getKey().toString();
map.put(
key,
FieldValueImpl.fromJavaObjectValue(
map.getDefinition().getElement(),
entry.getValue()));
}
return (MapValueImpl)map;
}
/*
* local methods
*/
/**
* Clears the map.
*/
void clearMap() {
fields.clear();
}
/*
* Cheap validation for scalars. If types match, nothing to do; if not,
* do the more expensive work in validate().
*/
private MapValueImpl putScalar(String fieldName, FieldValue value) {
if (getDefinition().getType() != value.getType()) {
value = validate(value, getElementDef());
}
fields.put(fieldName, value);
return this;
}
public FieldDefImpl getElementDef() {
return getDefinition().getElement();
}
public Map getFieldsInternal() {
return fields;
}
public Set getFieldNames() {
return fields.keySet();
}
/**
* This version is used internally for index deserialization. Enums are
* stored as an integer index into the enumeration values in indexes.
*/
MapValue putEnum(String name, int index) {
fields.put(name, ((EnumDefImpl)getElementDef()).createEnum(index));
return this;
}
@Override
public MapValueSerializer asMapValueSerializer() {
return this;
}
@SuppressWarnings("unchecked")
@Override
public Iterator> iterator() {
final Map values = getFields();
return ((Map)values).entrySet().iterator();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy