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}