/* * (C) Copyright 2017 Nuxeo (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Florent Guillaume */ package org.nuxeo.ecm.core.storage.mongodb; import static org.nuxeo.ecm.core.storage.State.NOP; import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ID; import static org.nuxeo.ecm.core.storage.mongodb.MongoDBRepository.MONGODB_EACH; import static org.nuxeo.ecm.core.storage.mongodb.MongoDBRepository.MONGODB_ID; import static org.nuxeo.ecm.core.storage.mongodb.MongoDBRepository.MONGODB_INC; import static org.nuxeo.ecm.core.storage.mongodb.MongoDBRepository.MONGODB_PUSH; import static org.nuxeo.ecm.core.storage.mongodb.MongoDBRepository.MONGODB_SET; import static org.nuxeo.ecm.core.storage.mongodb.MongoDBRepository.MONGODB_UNSET; import static org.nuxeo.ecm.core.storage.mongodb.MongoDBRepository.ONE; import java.io.Serializable; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Map.Entry; import org.nuxeo.ecm.core.api.model.Delta; import org.nuxeo.ecm.core.storage.State; import org.nuxeo.ecm.core.storage.State.ListDiff; import org.nuxeo.ecm.core.storage.State.StateDiff; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; /** * Convert to and from MongoDB types (bson) and DBS types (diff, state, list, serializable). * * @since 9.1 */ public class MongoDBConverter { protected final String idKey; protected final boolean useCustomId; public MongoDBConverter(String idKey) { this.idKey = idKey; this.useCustomId = KEY_ID.equals(idKey); } /** * Constructs a list of MongoDB updates from the given {@link StateDiff}. * <p> * We need a list because some cases need two operations to avoid conflicts. */ public List<DBObject> diffToBson(StateDiff diff) { UpdateBuilder updateBuilder = new UpdateBuilder(); return updateBuilder.build(diff); } public String keyToBson(String key) { if (useCustomId) { return key; } else { return KEY_ID.equals(key) ? idKey : key; } } public Object valueToBson(Object value) { if (value instanceof State) { return stateToBson((State) value); } else if (value instanceof List) { @SuppressWarnings("unchecked") List<Object> values = (List<Object>) value; return listToBson(values); } else if (value instanceof Object[]) { return listToBson(Arrays.asList((Object[]) value)); } else { return serializableToBson(value); } } public DBObject stateToBson(State state) { DBObject ob = new BasicDBObject(); for (Entry<String, Serializable> en : state.entrySet()) { Object val = valueToBson(en.getValue()); if (val != null) { ob.put(keyToBson(en.getKey()), val); } } return ob; } public List<Object> listToBson(List<Object> values) { ArrayList<Object> objects = new ArrayList<Object>(values.size()); for (Object value : values) { objects.add(valueToBson(value)); } return objects; } public String bsonToKey(String key) { if (useCustomId) { return key; } else { return idKey.equals(key) ? KEY_ID : key; } } public State bsonToState(DBObject ob) { if (ob == null) { return null; } State state = new State(ob.keySet().size()); for (String key : ob.keySet()) { if (useCustomId && MONGODB_ID.equals(key)) { // skip native id continue; } state.put(bsonToKey(key), bsonToValue(ob.get(key))); } return state; } public Serializable bsonToValue(Object value) { if (value instanceof List) { @SuppressWarnings("unchecked") List<Object> list = (List<Object>) value; if (list.isEmpty()) { return null; } else { Class<?> klass = Object.class; for (Object o : list) { if (o != null) { klass = scalarToSerializableClass(o.getClass()); break; } } if (DBObject.class.isAssignableFrom(klass)) { List<Serializable> l = new ArrayList<>(list.size()); for (Object el : list) { l.add(bsonToState((DBObject) el)); } return (Serializable) l; } else { // turn the list into a properly-typed array Object[] ar = (Object[]) Array.newInstance(klass, list.size()); int i = 0; for (Object el : list) { ar[i++] = scalarToSerializable(el); } return ar; } } } else if (value instanceof DBObject) { return bsonToState((DBObject) value); } else { return scalarToSerializable(value); } } public Object serializableToBson(Object value) { if (value instanceof Calendar) { return ((Calendar) value).getTime(); } return value; } public Serializable scalarToSerializable(Object val) { if (val instanceof Date) { Calendar cal = Calendar.getInstance(); cal.setTime((Date) val); return cal; } return (Serializable) val; } public Class<?> scalarToSerializableClass(Class<?> klass) { if (Date.class.isAssignableFrom(klass)) { return Calendar.class; } return klass; } /** * Update list builder to prevent several updates of the same field. * <p> * This happens if two operations act on two fields where one is a prefix of the other. * <p> * Example: Cannot update 'mylist.0.string' and 'mylist' at the same time (error 16837) * * @since 5.9.5 */ public class UpdateBuilder { protected final BasicDBObject set = new BasicDBObject(); protected final BasicDBObject unset = new BasicDBObject(); protected final BasicDBObject push = new BasicDBObject(); protected final BasicDBObject inc = new BasicDBObject(); protected final List<DBObject> updates = new ArrayList<>(10); protected DBObject update; protected Set<String> prefixKeys; protected Set<String> keys; public List<DBObject> build(StateDiff diff) { processStateDiff(diff, null); newUpdate(); for (Entry<String, Object> en : set.entrySet()) { update(MONGODB_SET, en.getKey(), en.getValue()); } for (Entry<String, Object> en : unset.entrySet()) { update(MONGODB_UNSET, en.getKey(), en.getValue()); } for (Entry<String, Object> en : push.entrySet()) { update(MONGODB_PUSH, en.getKey(), en.getValue()); } for (Entry<String, Object> en : inc.entrySet()) { update(MONGODB_INC, en.getKey(), en.getValue()); } return updates; } protected void processStateDiff(StateDiff diff, String prefix) { String elemPrefix = prefix == null ? "" : prefix + '.'; for (Entry<String, Serializable> en : diff.entrySet()) { String name = elemPrefix + en.getKey(); Serializable value = en.getValue(); if (value instanceof StateDiff) { processStateDiff((StateDiff) value, name); } else if (value instanceof ListDiff) { processListDiff((ListDiff) value, name); } else if (value instanceof Delta) { processDelta((Delta) value, name); } else { // not a diff processValue(name, value); } } } protected void processListDiff(ListDiff listDiff, String prefix) { if (listDiff.diff != null) { String elemPrefix = prefix == null ? "" : prefix + '.'; int i = 0; for (Object value : listDiff.diff) { String name = elemPrefix + i; if (value instanceof StateDiff) { processStateDiff((StateDiff) value, name); } else if (value != NOP) { // set value set.put(name, valueToBson(value)); } i++; } } if (listDiff.rpush != null) { Object pushed; if (listDiff.rpush.size() == 1) { // no need to use $each for one element pushed = valueToBson(listDiff.rpush.get(0)); } else { pushed = new BasicDBObject(MONGODB_EACH, listToBson(listDiff.rpush)); } push.put(prefix, pushed); } } protected void processDelta(Delta delta, String prefix) { // MongoDB can $inc a field that doesn't exist, it's treated as 0 BUT it doesn't work on null // so we ensure (in diffToUpdates) that we never store a null but remove the field instead Object incValue = valueToBson(delta.getDeltaValue()); inc.put(prefix, incValue); } protected void processValue(String name, Serializable value) { if (value == null) { // for null values, beyond the space saving, // it's important to unset the field instead of setting the value to null // because $inc does not work on nulls but works on non-existent fields unset.put(name, ONE); } else { set.put(name, valueToBson(value)); } } protected void newUpdate() { updates.add(update = new BasicDBObject()); prefixKeys = new HashSet<>(); keys = new HashSet<>(); } protected void update(String op, String key, Object value) { checkForConflict(key); DBObject map = (DBObject) update.get(op); if (map == null) { update.put(op, map = new BasicDBObject()); } map.put(key, value); } /** * Checks if the key conflicts with one of the previous keys. * <p> * A conflict occurs if one key is equals to or is a prefix of the other. */ protected void checkForConflict(String key) { List<String> pKeys = getPrefixKeys(key); if (conflictKeys(key, pKeys)) { newUpdate(); } prefixKeys.addAll(pKeys); keys.add(key); } protected boolean conflictKeys(String key, List<String> subkeys) { if (prefixKeys.contains(key)) { return true; } for (String sk: subkeys) { if (keys.contains(sk)) { return true; } } return false; } /** * return a list of parents key * foo.0.bar -> [foo, foo.0, foo.0.bar] */ protected List<String> getPrefixKeys(String key) { List<String> ret = new ArrayList<>(10); int i=0; while ((i = key.indexOf('.', i)) > 0) { ret.add(key.substring(0, i++)); } ret.add(key); return ret; } } }