001/* 002 * (C) Copyright 2017 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Florent Guillaume 018 */ 019package org.nuxeo.ecm.core.storage.mongodb; 020 021import static org.nuxeo.ecm.core.storage.State.NOP; 022import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ID; 023import static org.nuxeo.ecm.core.storage.mongodb.MongoDBRepository.MONGODB_EACH; 024import static org.nuxeo.ecm.core.storage.mongodb.MongoDBRepository.MONGODB_ID; 025import static org.nuxeo.ecm.core.storage.mongodb.MongoDBRepository.MONGODB_INC; 026import static org.nuxeo.ecm.core.storage.mongodb.MongoDBRepository.MONGODB_PUSH; 027import static org.nuxeo.ecm.core.storage.mongodb.MongoDBRepository.MONGODB_SET; 028import static org.nuxeo.ecm.core.storage.mongodb.MongoDBRepository.MONGODB_UNSET; 029import static org.nuxeo.ecm.core.storage.mongodb.MongoDBRepository.ONE; 030 031import java.io.Serializable; 032import java.lang.reflect.Array; 033import java.util.ArrayList; 034import java.util.Arrays; 035import java.util.Calendar; 036import java.util.Date; 037import java.util.HashSet; 038import java.util.List; 039import java.util.Set; 040import java.util.Map.Entry; 041 042import org.nuxeo.ecm.core.api.model.Delta; 043import org.nuxeo.ecm.core.storage.State; 044import org.nuxeo.ecm.core.storage.State.ListDiff; 045import org.nuxeo.ecm.core.storage.State.StateDiff; 046 047import com.mongodb.BasicDBObject; 048import com.mongodb.DBObject; 049 050/** 051 * Convert to and from MongoDB types (bson) and DBS types (diff, state, list, serializable). 052 * 053 * @since 9.1 054 */ 055public class MongoDBConverter { 056 057 protected final String idKey; 058 059 protected final boolean useCustomId; 060 061 public MongoDBConverter(String idKey) { 062 this.idKey = idKey; 063 this.useCustomId = KEY_ID.equals(idKey); 064 } 065 066 /** 067 * Constructs a list of MongoDB updates from the given {@link StateDiff}. 068 * <p> 069 * We need a list because some cases need two operations to avoid conflicts. 070 */ 071 public List<DBObject> diffToBson(StateDiff diff) { 072 UpdateBuilder updateBuilder = new UpdateBuilder(); 073 return updateBuilder.build(diff); 074 } 075 076 public String keyToBson(String key) { 077 if (useCustomId) { 078 return key; 079 } else { 080 return KEY_ID.equals(key) ? idKey : key; 081 } 082 } 083 084 public Object valueToBson(Object value) { 085 if (value instanceof State) { 086 return stateToBson((State) value); 087 } else if (value instanceof List) { 088 @SuppressWarnings("unchecked") 089 List<Object> values = (List<Object>) value; 090 return listToBson(values); 091 } else if (value instanceof Object[]) { 092 return listToBson(Arrays.asList((Object[]) value)); 093 } else { 094 return serializableToBson(value); 095 } 096 } 097 098 public DBObject stateToBson(State state) { 099 DBObject ob = new BasicDBObject(); 100 for (Entry<String, Serializable> en : state.entrySet()) { 101 Object val = valueToBson(en.getValue()); 102 if (val != null) { 103 ob.put(keyToBson(en.getKey()), val); 104 } 105 } 106 return ob; 107 } 108 109 public List<Object> listToBson(List<Object> values) { 110 ArrayList<Object> objects = new ArrayList<Object>(values.size()); 111 for (Object value : values) { 112 objects.add(valueToBson(value)); 113 } 114 return objects; 115 } 116 117 public String bsonToKey(String key) { 118 if (useCustomId) { 119 return key; 120 } else { 121 return idKey.equals(key) ? KEY_ID : key; 122 } 123 } 124 125 public State bsonToState(DBObject ob) { 126 if (ob == null) { 127 return null; 128 } 129 State state = new State(ob.keySet().size()); 130 for (String key : ob.keySet()) { 131 if (useCustomId && MONGODB_ID.equals(key)) { 132 // skip native id 133 continue; 134 } 135 state.put(bsonToKey(key), bsonToValue(ob.get(key))); 136 } 137 return state; 138 } 139 140 public Serializable bsonToValue(Object value) { 141 if (value instanceof List) { 142 @SuppressWarnings("unchecked") 143 List<Object> list = (List<Object>) value; 144 if (list.isEmpty()) { 145 return null; 146 } else { 147 Class<?> klass = Object.class; 148 for (Object o : list) { 149 if (o != null) { 150 klass = scalarToSerializableClass(o.getClass()); 151 break; 152 } 153 } 154 if (DBObject.class.isAssignableFrom(klass)) { 155 List<Serializable> l = new ArrayList<>(list.size()); 156 for (Object el : list) { 157 l.add(bsonToState((DBObject) el)); 158 } 159 return (Serializable) l; 160 } else { 161 // turn the list into a properly-typed array 162 Object[] ar = (Object[]) Array.newInstance(klass, list.size()); 163 int i = 0; 164 for (Object el : list) { 165 ar[i++] = scalarToSerializable(el); 166 } 167 return ar; 168 } 169 } 170 } else if (value instanceof DBObject) { 171 return bsonToState((DBObject) value); 172 } else { 173 return scalarToSerializable(value); 174 } 175 } 176 177 public Object serializableToBson(Object value) { 178 if (value instanceof Calendar) { 179 return ((Calendar) value).getTime(); 180 } 181 return value; 182 } 183 184 public Serializable scalarToSerializable(Object val) { 185 if (val instanceof Date) { 186 Calendar cal = Calendar.getInstance(); 187 cal.setTime((Date) val); 188 return cal; 189 } 190 return (Serializable) val; 191 } 192 193 public Class<?> scalarToSerializableClass(Class<?> klass) { 194 if (Date.class.isAssignableFrom(klass)) { 195 return Calendar.class; 196 } 197 return klass; 198 } 199 200 201 /** 202 * Update list builder to prevent several updates of the same field. 203 * <p> 204 * This happens if two operations act on two fields where one is a prefix of the other. 205 * <p> 206 * Example: Cannot update 'mylist.0.string' and 'mylist' at the same time (error 16837) 207 * 208 * @since 5.9.5 209 */ 210 public class UpdateBuilder { 211 212 protected final BasicDBObject set = new BasicDBObject(); 213 214 protected final BasicDBObject unset = new BasicDBObject(); 215 216 protected final BasicDBObject push = new BasicDBObject(); 217 218 protected final BasicDBObject inc = new BasicDBObject(); 219 220 protected final List<DBObject> updates = new ArrayList<>(10); 221 222 protected DBObject update; 223 224 protected Set<String> prefixKeys; 225 226 protected Set<String> keys; 227 228 public List<DBObject> build(StateDiff diff) { 229 processStateDiff(diff, null); 230 newUpdate(); 231 for (Entry<String, Object> en : set.entrySet()) { 232 update(MONGODB_SET, en.getKey(), en.getValue()); 233 } 234 for (Entry<String, Object> en : unset.entrySet()) { 235 update(MONGODB_UNSET, en.getKey(), en.getValue()); 236 } 237 for (Entry<String, Object> en : push.entrySet()) { 238 update(MONGODB_PUSH, en.getKey(), en.getValue()); 239 } 240 for (Entry<String, Object> en : inc.entrySet()) { 241 update(MONGODB_INC, en.getKey(), en.getValue()); 242 } 243 return updates; 244 } 245 246 protected void processStateDiff(StateDiff diff, String prefix) { 247 String elemPrefix = prefix == null ? "" : prefix + '.'; 248 for (Entry<String, Serializable> en : diff.entrySet()) { 249 String name = elemPrefix + en.getKey(); 250 Serializable value = en.getValue(); 251 if (value instanceof StateDiff) { 252 processStateDiff((StateDiff) value, name); 253 } else if (value instanceof ListDiff) { 254 processListDiff((ListDiff) value, name); 255 } else if (value instanceof Delta) { 256 processDelta((Delta) value, name); 257 } else { 258 // not a diff 259 processValue(name, value); 260 } 261 } 262 } 263 264 protected void processListDiff(ListDiff listDiff, String prefix) { 265 if (listDiff.diff != null) { 266 String elemPrefix = prefix == null ? "" : prefix + '.'; 267 int i = 0; 268 for (Object value : listDiff.diff) { 269 String name = elemPrefix + i; 270 if (value instanceof StateDiff) { 271 processStateDiff((StateDiff) value, name); 272 } else if (value != NOP) { 273 // set value 274 set.put(name, valueToBson(value)); 275 } 276 i++; 277 } 278 } 279 if (listDiff.rpush != null) { 280 Object pushed; 281 if (listDiff.rpush.size() == 1) { 282 // no need to use $each for one element 283 pushed = valueToBson(listDiff.rpush.get(0)); 284 } else { 285 pushed = new BasicDBObject(MONGODB_EACH, listToBson(listDiff.rpush)); 286 } 287 push.put(prefix, pushed); 288 } 289 } 290 291 protected void processDelta(Delta delta, String prefix) { 292 // MongoDB can $inc a field that doesn't exist, it's treated as 0 BUT it doesn't work on null 293 // so we ensure (in diffToUpdates) that we never store a null but remove the field instead 294 Object incValue = valueToBson(delta.getDeltaValue()); 295 inc.put(prefix, incValue); 296 } 297 298 protected void processValue(String name, Serializable value) { 299 if (value == null) { 300 // for null values, beyond the space saving, 301 // it's important to unset the field instead of setting the value to null 302 // because $inc does not work on nulls but works on non-existent fields 303 unset.put(name, ONE); 304 } else { 305 set.put(name, valueToBson(value)); 306 } 307 } 308 309 protected void newUpdate() { 310 updates.add(update = new BasicDBObject()); 311 prefixKeys = new HashSet<>(); 312 keys = new HashSet<>(); 313 } 314 315 protected void update(String op, String key, Object value) { 316 checkForConflict(key); 317 DBObject map = (DBObject) update.get(op); 318 if (map == null) { 319 update.put(op, map = new BasicDBObject()); 320 } 321 map.put(key, value); 322 } 323 324 /** 325 * Checks if the key conflicts with one of the previous keys. 326 * <p> 327 * A conflict occurs if one key is equals to or is a prefix of the other. 328 */ 329 protected void checkForConflict(String key) { 330 List<String> pKeys = getPrefixKeys(key); 331 if (conflictKeys(key, pKeys)) { 332 newUpdate(); 333 } 334 prefixKeys.addAll(pKeys); 335 keys.add(key); 336 } 337 338 protected boolean conflictKeys(String key, List<String> subkeys) { 339 if (prefixKeys.contains(key)) { 340 return true; 341 } 342 for (String sk: subkeys) { 343 if (keys.contains(sk)) { 344 return true; 345 } 346 } 347 return false; 348 } 349 350 /** 351 * return a list of parents key 352 * foo.0.bar -> [foo, foo.0, foo.0.bar] 353 */ 354 protected List<String> getPrefixKeys(String key) { 355 List<String> ret = new ArrayList<>(10); 356 int i=0; 357 while ((i = key.indexOf('.', i)) > 0) { 358 ret.add(key.substring(0, i++)); 359 } 360 ret.add(key); 361 return ret; 362 } 363 364 } 365 366}