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 *     Funsho David
018 *
019 */
020
021package org.nuxeo.directory.mongodb;
022
023import java.io.Serializable;
024import java.lang.reflect.Array;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Calendar;
028import java.util.Collections;
029import java.util.Date;
030import java.util.HashMap;
031import java.util.List;
032import java.util.Map;
033
034import org.bson.Document;
035import org.nuxeo.ecm.core.schema.types.Type;
036import org.nuxeo.ecm.core.schema.types.primitives.IntegerType;
037import org.nuxeo.ecm.core.schema.types.primitives.LongType;
038
039/**
040 * Helper for serialization/deserialization of BSON objects
041 *
042 * @since 9.1
043 */
044public class MongoDBSerializationHelper {
045
046    public static final String MONGODB_ID = "_id";
047
048    public static final String MONGODB_SEQ = "seq";
049
050    private MongoDBSerializationHelper() {
051        // empty
052    }
053
054    /**
055     * Create a BSON object with a single field from a pair key/value
056     *
057     * @param key the key which corresponds to the field id in the object
058     * @param value the value which corresponds to the field value in the object
059     * @return the new BSON object
060     */
061    public static Document fieldMapToBson(String key, Object value) {
062        return fieldMapToBson(Collections.singletonMap(key, value));
063    }
064
065    /**
066     * Create a BSON object from a map
067     *
068     * @param fieldMap a map of keys/values
069     * @return the new BSON object
070     */
071    public static Document fieldMapToBson(Map<String, Object> fieldMap) {
072        Document doc = new Document();
073        for (Map.Entry<String, Object> entry : fieldMap.entrySet()) {
074            Object val = valueToBson(entry.getValue());
075            if (val != null) {
076                doc.put(entry.getKey(), val);
077            }
078        }
079        return doc;
080    }
081
082    /**
083     * Cast an object according to its instance
084     *
085     * @param value the object to transform
086     * @return the BSON object
087     */
088    public static Object valueToBson(Object value) {
089        if (value instanceof Map) {
090            @SuppressWarnings("unchecked")
091            Map<String, Object> map = (Map<String, Object>) value;
092            return fieldMapToBson(map);
093        } else if (value instanceof List) {
094            @SuppressWarnings("unchecked")
095            List<Object> values = (List<Object>) value;
096            return listToBson(values);
097        } else if (value instanceof Object[]) {
098            return listToBson(Arrays.asList((Object[]) value));
099        } else {
100            return serializableToBson(value);
101        }
102    }
103
104    /**
105     * Cast an object according to its instance ans its type
106     *
107     * @param value the object to transform
108     * @param type the object type
109     * @return the BSON object
110     * @since 9.2
111     */
112    public static Object valueToBson(Object value, Type type) {
113        if (value != null && type instanceof IntegerType) {
114            return Integer.valueOf(String.valueOf(value));
115        } else if (value != null && type instanceof LongType) {
116            return Long.valueOf(String.valueOf(value));
117        } else {
118            return valueToBson(value);
119        }
120    }
121
122    protected static List<Object> listToBson(List<Object> values) {
123        List<Object> objects = new ArrayList<>(values.size());
124        for (Object value : values) {
125            objects.add(valueToBson(value));
126        }
127        return objects;
128    }
129
130    protected static Object serializableToBson(Object value) {
131        if (value instanceof Calendar) {
132            return ((Calendar) value).getTime();
133        }
134        return value;
135    }
136
137    /**
138     * Create a map from a BSON object
139     *
140     * @param doc the BSON object to parse
141     * @return the new map
142     */
143    public static Map<String, Object> bsonToFieldMap(Document doc) {
144        Map<String, Object> fieldMap = new HashMap<>();
145        for (String key : doc.keySet()) {
146            if (MONGODB_ID.equals(key)) {
147                // skip native id
148                continue;
149            }
150            fieldMap.put(key, bsonToValue(doc.get(key)));
151        }
152        return fieldMap;
153    }
154
155    protected static Serializable bsonToValue(Object value) {
156        if (value instanceof List) {
157            @SuppressWarnings("unchecked")
158            List<Object> list = (List<Object>) value;
159            if (list.isEmpty()) {
160                return null;
161            } else {
162                Class<?> klass = Object.class;
163                for (Object o : list) {
164                    if (o != null) {
165                        klass = scalarToSerializableClass(o.getClass());
166                        break;
167                    }
168                }
169                if (Document.class.isAssignableFrom(klass)) {
170                    List<Serializable> l = new ArrayList<>(list.size());
171                    for (Object o : list) {
172                        l.add((Serializable) bsonToFieldMap((Document) o));
173                    }
174                    return (Serializable) l;
175                } else {
176                    // turn the list into a properly-typed array
177                    Object[] array = (Object[]) Array.newInstance(klass, list.size());
178                    int i = 0;
179                    for (Object o : list) {
180                        array[i++] = scalarToSerializable(o);
181                    }
182                    return array;
183                }
184            }
185        } else if (value instanceof Document) {
186            return (Serializable) bsonToFieldMap((Document) value);
187        } else {
188            return scalarToSerializable(value);
189        }
190    }
191
192    protected static Class<?> scalarToSerializableClass(Class<?> klass) {
193        if (Date.class.isAssignableFrom(klass)) {
194            return Calendar.class;
195        }
196        return klass;
197    }
198
199    protected static Serializable scalarToSerializable(Object value) {
200        if (value instanceof Date) {
201            Calendar cal = Calendar.getInstance();
202            cal.setTime((Date) value);
203            return cal;
204        }
205        return (Serializable) value;
206    }
207
208}