/** * Copyright (c) 2011-2012, Thilo Planz. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package v7cr.v7db; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import org.apache.commons.lang.ArrayUtils; import org.bson.BSON; import org.bson.BSONObject; import org.bson.BasicBSONObject; import org.bson.types.ObjectId; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; /** * An object whose properties are contained in a BSONObject, optionally using a * SchemaDefinition. * * The object is immutable, subclasses must adhere to this contract. * * It also provides a selection of useful accessor and mutator methods, * including support for nested fields. Since the object is immutable, the * mutator methods return a modified copy. * * */ public class BSONBackedObject { private final BasicBSONObject bson; private final SchemaDefinition schema; private final static BasicBSONObject EMPTY = new BasicBSONObject(); /** * creates an empty object. You can use this as a starting point for * building instances using the "append" methods. */ public BSONBackedObject() { bson = EMPTY; schema = null; } BSONBackedObject(BasicBSONObject b, SchemaDefinition schema) { this.bson = b; this.schema = schema; } protected BSONBackedObject(BSONBackedObject b, SchemaDefinition schema) { this.bson = b.bson; this.schema = schema; } public boolean isEmpty() { return bson.keySet().isEmpty(); } // for nested fields // returns { parentObject, localFieldName } private static Object[] drillDownToParent(Map<?, ?> data, String field) { if (field == null) { return null; } int idx = field.indexOf('.'); if (idx == -1) { return new Object[] { data, field }; } String head = field.substring(0, idx); String tail = field.substring(idx + 1); Object o = data.get(head); if (o instanceof Map<?, ?>) { return drillDownToParent((Map<?, ?>) o, tail); } return null; } // for nested fields private Object drillDown(String field) { Object[] d = drillDownToParent(bson, field); if (d == null) return null; return ((Map<?, ?>) d[0]).get(d[1]); } public boolean containsField(String field) { Object[] d = drillDownToParent(bson, field); if (d == null) return false; return ((Map<?, ?>) d[0]).containsKey(field); } public Set<String> getFieldNames() { return new TreeSet<String>(bson.keySet()); } public String getStringField(String field) { return (String) getField(field); } /** * for a String field that can contain multiple values (cardinality > 1), * return all of them as an array. An empty array will be returned as null. */ public String[] getStringFieldAsArray(String field) { Object o = drillDown(field); if (o == null) return null; if (o instanceof String) return new String[] { (String) o }; if (o instanceof List<?>) { List<?> l = (List<?>) o; if (l.isEmpty()) return null; return l.toArray(new String[] {}); } if (o instanceof Object[]) { Object[] a = (Object[]) o; if (a.length == 0) return null; String[] x = new String[a.length]; System.arraycopy(a, 0, x, 0, a.length); return x; } throw new RuntimeException("not a string field " + o); } public ObjectId getObjectIdField(String field) { return (ObjectId) getField(field); } public Long getLongField(String field) { return (Long) getField(field); } public Date getDateField(String field) { return (Date) getField(field); } public Boolean getBooleanField(String field) { return (Boolean) getField(field); } public Integer getIntegerField(String field) { return (Integer) getField(field); } /** * returns the field value, which can be any type of object. Use this only, * if you do not know the type in advance, otherwise the typed methods (such * as getStringField) are preferred. * * <p> * If the field has multiple values, an array is returned. * * <p> * Always returns immutable objects or copies of the original data, so any * changes made to it later do not affect this object. * */ public Object getField(String field) { Object o = drillDown(field); if (o == null) return null; if (o instanceof String || o instanceof Boolean || o instanceof Long || o instanceof ObjectId || o instanceof Integer) return o; // Date is mutable... if (o instanceof Date) return ((Date) o).clone(); if (o instanceof BasicBSONObject) { return new BSONBackedObject((BasicBSONObject) o, null); } if (o instanceof List<?>) { o = ((List<?>) o).toArray(); } if (o instanceof Object[]) { Object[] a = (Object[]) o; int i = 0; for (Object m : a) { if (m instanceof Date) { a[i] = ((Date) m).clone(); } else if (m instanceof BasicBSONObject) { a[i] = new BSONBackedObject((BasicBSONObject) m, null); } else if (m instanceof String || m instanceof Boolean || m instanceof Long || m instanceof ObjectId || m instanceof Integer) { // immutable, no need to do anything } else throw new RuntimeException("unsupported field type " + o.getClass().getName() + " for '" + field + "' : " + o); i++; } return a; } if (o instanceof byte[]) { byte[] b = (byte[]) o; return ArrayUtils.clone(b); } throw new RuntimeException("unsupported field type " + o.getClass().getName() + " for '" + field + "' : " + o); } /** * returns the string representation of the BSON data that backs this * object. */ @Override public String toString() { return bson.toString(); } public SchemaDefinition getSchemaDefinition() { return schema; } /** * @return a copy of the underlying BSON data */ public BasicBSONObject getBSONObject() { return (BasicBSONObject) BSON.decode(BSON.encode(bson)); } /** * @return a copy of the underlying BSON data */ public DBObject getDBObject() { return new BasicDBObject(getBSONObject()); } public BSONBackedObject getObjectField(String fieldName) { Object o = drillDown(fieldName); if (o == null) return null; if (o instanceof BasicBSONObject) { return new BSONBackedObject((BasicBSONObject) o, null); } throw new RuntimeException("not an object field " + o); } /** * for a field that can contain multiple values (cardinality > 1), return * all of them as an array. An empty array will be returned as null. */ public BSONBackedObject[] getObjectFieldAsArray(String field) { Object o = drillDown(field); if (o == null) return null; if (o instanceof Object[]) { o = Arrays.asList((Object[]) o); } if (o instanceof List<?>) { List<?> l = (List<?>) o; if (l.isEmpty()) return null; BSONBackedObject[] r = new BSONBackedObject[l.size()]; int i = 0; for (Object b : l) { if (b instanceof BasicBSONObject) { r[i++] = (new BSONBackedObject((BasicBSONObject) b, null)); } else { throw new RuntimeException("not an object field " + b); } } return r; } if (o instanceof BasicBSONObject) return new BSONBackedObject[] { new BSONBackedObject( (BasicBSONObject) o, null) }; throw new RuntimeException("not an object field " + o); } /** * If the two objects are of the same class and their BSON data is equal, * then they are considered equal */ @Override public boolean equals(Object obj) { if (obj instanceof BSONBackedObject && obj.getClass() == getClass()) { return ((BSONBackedObject) obj).bson.equals(bson); } return false; } /** * @return the hashCode of the underlying BSON data */ @Override public int hashCode() { return bson.hashCode(); } // for nested fields // returns { root, parent } private static BasicBSONObject[] createPath(Map<?, ?> data, String field) { if (field == null) return null; int idx = field.indexOf('.'); if (idx == -1) { BasicBSONObject copy = new BasicBSONObject(); copy.putAll((BSONObject) data); return new BasicBSONObject[] { copy, copy }; } String head = field.substring(0, idx); String tail = field.substring(idx + 1); Object x = data.get(head); if (x instanceof Map<?, ?>) { BasicBSONObject copy = new BasicBSONObject(); copy.putAll((BSONObject) data); try { BasicBSONObject[] r = createPath((Map<?, ?>) x, tail); copy.put(head, r[0]); return new BasicBSONObject[] { copy, r[1] }; } catch (UnsupportedOperationException e) { throw new UnsupportedOperationException(field + " is not a valid path", e); } } if (x == null) { BasicBSONObject copy = new BasicBSONObject(); copy.putAll((BSONObject) data); BasicBSONObject vivi = new BasicBSONObject(); copy.put(head, vivi); return new BasicBSONObject[] { copy, vivi }; } throw new UnsupportedOperationException(field + " is not a valid path"); } private BSONBackedObject _append(String key, Object value) { if (value == null) return unset(key); if (value instanceof List<?> && ((List<?>) value).isEmpty()) return unset(key); if (value instanceof Object[] && ((Object[]) value).length == 0) return unset(key); BasicBSONObject[] path = createPath(bson, key); if (path == null) throw new UnsupportedOperationException(key + " is not a valid path"); int idx = key.lastIndexOf('.'); String localKey = idx == -1 ? key : key.substring(idx + 1); path[1].put(localKey, value); return new BSONBackedObject(path[0], schema); } /** * Since the object is immutable, all "append" methods return a modified * copy that contains the new value. * <p> * Auto-vivification: In case of a nested field, non-existing objects on the * path will be created if necessary. */ public BSONBackedObject append(String key, String value) { return _append(key, value); } /** * Since the object is immutable, all "append" methods return a modified * copy that contains the new value. * <p> * Auto-vivification: In case of a nested field, non-existing objects on the * path will be created if necessary. */ public BSONBackedObject append(String key, BSONBackedObject value) { return _append(key, value.bson); } private List<Object> getList(String key) { Object o = drillDown(key); if (o == null) return new ArrayList<Object>(); if (o instanceof List<?>) return new ArrayList<Object>((List<?>) o); if (o instanceof Object[]) { return new ArrayList<Object>(Arrays.asList((Object[]) o)); } List<Object> l = new ArrayList<Object>(); l.add(o); return l; } private BSONBackedObject _push(String key, Object value) { List<Object> l = getList(key); if (l == null) { createPath(bson, key); l = new ArrayList<Object>(1); } l.add(value); return _append(key, l); } /** * Adds the value to the end of the array. A new array will be created if * missing, and a previously single element will be turned into an array * (does not raise an error like MongoDB's $push operator would). * <p> * Since the object is immutable, all "push" methods return a modified copy * that contains the new value. * <p> * Auto-vivification: In case of a nested field, non-existing objects on the * path will be created if necessary. */ public BSONBackedObject push(String key, String value) { return _push(key, value); } /** * Adds the value to the end of the array. A new array will be created if * missing, and a previously single element will be turned into an array * (does not raise an error like MongoDB's $push operator would). * <p> * Since the object is immutable, all "push" methods return a modified copy * that contains the new value. * <p> * Auto-vivification: In case of a nested field, non-existing objects on the * path will be created if necessary. */ public BSONBackedObject push(String key, BSONBackedObject value) { return _push(key, value.bson); } private BSONBackedObject _pushAll(String key, Object... values) { List<Object> l = getList(key); if (l == null) { createPath(bson, key); l = new ArrayList<Object>(values.length); } for (Object o : values) l.add(o); return _append(key, l); } /** * Adds the value to the end of the array. A new array will be created if * missing, and a previously single element will be turned into an array * (does not raise an error like MongoDB's $push operator would). * <p> * Since the object is immutable, all "push" methods return a modified copy * that contains the new value. * <p> * Auto-vivification: In case of a nested field, non-existing objects on the * path will be created if necessary. */ public BSONBackedObject pushAll(String key, String... values) { return _pushAll(key, (Object[]) values); } /** * Adds the value to the end of the array. A new array will be created if * missing, and a previously single element will be turned into an array * (does not raise an error like MongoDB's $push operator would). * <p> * Since the object is immutable, all "push" methods return a modified copy * that contains the new value. * <p> * Auto-vivification: In case of a nested field, non-existing objects on the * path will be created if necessary. */ public BSONBackedObject pushAll(String key, BSONBackedObject... values) { Object[] bsons = new BSONObject[values.length]; for (int i = 0; i < bsons.length; i++) { bsons[i] = values[i].bson; } return _pushAll(key, bsons); } /** * Adds the value to the end of the array, but only if the entry is not * already there. A new array will be created if missing, and a previously * single element will be turned into an array (does not raise an error like * MongoDB's $addToSet operator would). * <p> * Since the object is immutable, all "addToSet" methods return a modified * copy that contains the new value. * <p> * Auto-vivification: In case of a nested field, non-existing objects on the * path will be created if necessary. */ public BSONBackedObject addToSet(String key, String value) { List<Object> l = getList(key); if (l.contains(value)) return this; l.add(value); return _append(key, l); } /** * Adds the value to the end of the array, but only if the entry is not * already there. A new array will be created if missing, and a previously * single element will be turned into an array (does not raise an error like * MongoDB's $addToSet operator would). * <p> * Since the object is immutable, all "addToSet" methods return a modified * copy that contains the new value. * <p> * Auto-vivification: In case of a nested field, non-existing objects on the * path will be created if necessary. */ public BSONBackedObject addToSet(String key, BSONBackedObject value) { List<Object> l = getList(key); if (l.contains(value.bson)) return this; l.add(value.bson); return _append(key, l); } /** * Removes the last element in an array. If the field is not an array, * deletes the field ("removes the only element"). If the array is left * empty, removes the field completely. If the field is missing, does * nothing. * <p> * Since the object is immutable, all "modifier" methods return the modified * copy. * * @param keys * you can specify multiple field names at once */ public BSONBackedObject popLast(String... keys) { if (keys == null || keys.length == 0) return this; // TODO: this recursion and its temporary copies are very inefficient // if we are supporting multiple keys, we might as well implement it // in a tighter fashion if (keys.length == 1) { String k = keys[0]; List<Object> l = getList(k); if (l.isEmpty()) return this; l.remove(l.size() - 1); return _append(k, l); } String[] head = Arrays.copyOf(keys, keys.length - 1); return popLast(keys[head.length]).popLast(head); } /** * Deletes the gives fields (if they exist). * <p> * Since the object is immutable, all "modifier" methods return the modified * copy. * * @param keys * you can specify multiple field names at once */ public BSONBackedObject unset(String... keys) { if (keys == null || keys.length == 0) return this; // TODO: this recursion and its temporary copies are very inefficient // if we are supporting multiple keys, we might as well implement it // in a tighter fashion if (keys.length == 1) { String field = keys[0]; Object[] d = drillDownToParent(bson, field); if (d == null) return this; String localKey = (String) d[1]; BasicBSONObject[] path = createPath(bson, field); if (path == null) throw new UnsupportedOperationException(field + " is not a valid path"); path[1].removeField(localKey); return new BSONBackedObject(path[0], schema); } String[] head = Arrays.copyOf(keys, keys.length - 1); return unset(keys[head.length]).unset(head); } private final static BSONBackedObject BUILDER_START = new BSONBackedObject(); public static final BSONBackedObject start() { return BUILDER_START; } }