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}