/** * 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 v7db.files.mongodb; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.bson.BSON; import org.bson.BSONObject; /** * Utility class to work with BSON data. * <ul> * <li>performs loss-less type conversions * <li>supports nested fields ("a.b.c"), including auto-vivification * <li>fields that are null, empty array, or empty object are treated as missing * (setting to null removes the field) * </ul> * */ public class BSONUtils { public static <T> T notNull(T x) { if (x == null) return x; if (x instanceof Map<?, ?>) { if (((Map<?, ?>) x).isEmpty()) return null; return x; } if (x instanceof Object[]) { if (((Object[]) x).length == 0) return null; return x; } if (x instanceof byte[]) { if (((byte[]) x).length == 0) return null; return x; } if (x instanceof Collection<?>) { if (((Collection<?>) x).isEmpty()) return null; return x; } return x; } public static Object get(BSONObject b, String fieldName) { if (!fieldName.contains(".")) return notNull(b.get(fieldName)); String[] path = StringUtils.split(fieldName, ".", 2); Object nested = b.get(path[0]); if (nested == null) return null; if (nested instanceof BSONObject) return get((BSONObject) nested, path[1]); throw new IllegalArgumentException("cannot get field `" + fieldName + "` of " + b); } private static Object getRequired(BSONObject b, String fieldName) { Object x = get(b, fieldName); if (x == null) throw new IllegalArgumentException("required field `" + fieldName + "` is missing in " + b); return x; } public static Long toLong(Object x) { if (x == null) return null; if (x instanceof Long) return (Long) x; if (x instanceof Integer) return Long.valueOf(((Integer) x).intValue()); if (x instanceof String) return Long.valueOf((String) x); throw new IllegalArgumentException("cannot convert `" + x + "` into a Long"); } static Integer toInteger(Object x) { if (x == null) return null; if (x instanceof Integer) return (Integer) x; if (x instanceof Long) return Integer.valueOf(x.toString()); if (x instanceof String) return Integer.valueOf((String) x); throw new IllegalArgumentException("cannot convert `" + x + "` into a Long"); } public static String toString(Object x) { if (x == null) return null; if (x instanceof String) return (String) x; if (x instanceof Number) return x.toString(); throw new IllegalArgumentException("cannot convert `" + x + "` into a String"); } static Boolean toBoolean(Object x) { if (x == null) return null; if (x instanceof Boolean) return (Boolean) x; if (x instanceof String) { Boolean y = BooleanUtils.toBooleanObject((String) x); if (y != null) return y; } throw new IllegalArgumentException("cannot convert `" + x + "` into a Boolean"); } static Long getLong(BSONObject b, String fieldName) { return toLong(get(b, fieldName)); } static Integer getInteger(BSONObject b, String fieldName) { return toInteger(get(b, fieldName)); } static int getRequiredInt(BSONObject b, String fieldName) { return toInteger(getRequired(b, fieldName)).intValue(); } public static long getRequiredLong(BSONObject b, String fieldName) { return toLong(getRequired(b, fieldName)).longValue(); } public static String getString(BSONObject b, String fieldName) { return toString(get(b, fieldName)); } public static BSONObject getObject(BSONObject b, String fieldName) { Object x = get(b, fieldName); if (x == null) return null; if (x instanceof BSONObject) return (BSONObject) x; throw new IllegalArgumentException("cannot convert `" + x + "` into a BSONObject"); } /** * @return true, if the field exists, can be converted to a boolean, and is * "true" */ public static boolean isTrue(BSONObject b, String fieldName) { return Boolean.TRUE.equals(toBoolean(get(b, fieldName))); } static Object removeField(BSONObject b, String fieldName) { if (fieldName.contains(".")) throw new UnsupportedOperationException("not yet implemented"); return b.removeField(fieldName); } private static void put(BSONObject b, String fieldName, Object x) { x = notNull(x); if (x == null) { removeField(b, fieldName); } else { if (fieldName.contains(".")) throw new UnsupportedOperationException("not yet implemented"); b.put(fieldName, x); } } static Integer putInteger(BSONObject b, String fieldName, Object x) { Integer i = toInteger(x); put(b, fieldName, i); return i; } static Long putLong(BSONObject b, String fieldName, Object x) { Long l = toLong(x); put(b, fieldName, l); return l; } static String putString(BSONObject b, String fieldName, Object x) { String s = toString(x); put(b, fieldName, s); return s; } private static final Long MAX_INT = Long.valueOf(Integer.MAX_VALUE); private static final Long MIN_INT = Long.valueOf(Integer.MIN_VALUE); /** * stores a number as Integer, if it fits, or as a Long, if not. This saves * space in the database, but you lose the ability to sort or do range * queries. */ static Number putIntegerOrLong(BSONObject b, String fieldName, Object x) { if (x instanceof Integer) return putInteger(b, fieldName, x); Long l = toLong(x); if (l == null) { removeField(b, fieldName); return null; } if (l.compareTo(MAX_INT) < 0 && l.compareTo(MIN_INT) > 0) { Integer i = l.intValue(); b.put(fieldName, i); return i; } b.put(fieldName, l); return l; } /** * BSON objects are considered equal when their binary encoding matches */ static boolean equals(BSONObject a, BSONObject b) { return a.keySet().equals(b.keySet()) && Arrays.equals(BSON.encode(a), BSON.encode(b)); } /** * @return true, if the field contains (in case of an array) or is equal to * (in case of a single value) the given BSONObject */ static boolean contains(BSONObject b, String fieldName, BSONObject toLookFor) { Object list = get(b, fieldName); if (list == null) return false; if (list instanceof List<?>) { for (Object o : (List<?>) list) { if (o instanceof BSONObject) { BSONObject x = (BSONObject) o; if (equals(x, toLookFor)) return true; } } return false; } if (list instanceof Object[]) { for (Object o : (Object[]) list) { if (o instanceof BSONObject) { BSONObject x = (BSONObject) o; if (equals(x, toLookFor)) return true; } } return false; } if (list instanceof BSONObject) { BSONObject x = (BSONObject) list; if (equals(x, toLookFor)) return true; } return false; } public static Object[] values(BSONObject b, String fieldName) { Object x = get(b, fieldName); if (x == null) return ArrayUtils.EMPTY_OBJECT_ARRAY; if (x instanceof List<?>) return ((List<?>) x).toArray(); if (x instanceof Object[]) return ArrayUtils.clone((Object[]) x); return new Object[] { x }; } }