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