/*
* Copyright 2011 VZ Netzwerke Ltd
* Copyright 2014 devbliss GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mongojack.internal.util;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.bson.types.ObjectId;
import org.mongojack.DBQuery;
import org.mongojack.DBRef;
import org.mongojack.MongoJsonMappingException;
import org.mongojack.internal.ObjectIdSerializer;
import org.mongojack.internal.object.BsonObjectGenerator;
import org.mongojack.internal.query.CollectionQueryCondition;
import org.mongojack.internal.query.CompoundQueryCondition;
import org.mongojack.internal.query.QueryCondition;
import org.mongojack.internal.query.SimpleQueryCondition;
import org.mongojack.internal.update.MultiUpdateOperationValue;
import org.mongojack.internal.update.UpdateOperationValue;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.ContainerSerializer;
import com.fasterxml.jackson.databind.ser.std.BeanSerializerBase;
import com.fasterxml.jackson.databind.ser.std.MapSerializer;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* Utilities for helping with serialisation
*/
public class SerializationUtils {
private static final Set<Class<?>> BASIC_TYPES;
static {
Set<Class<?>> types = new HashSet<Class<?>>();
types.add(String.class);
types.add(Integer.class);
types.add(Boolean.class);
types.add(Short.class);
types.add(Long.class);
types.add(BigInteger.class);
types.add(Float.class);
types.add(Double.class);
types.add(Byte.class);
types.add(Character.class);
types.add(BigDecimal.class);
types.add(int[].class);
types.add(boolean[].class);
types.add(short[].class);
types.add(long[].class);
types.add(float[].class);
types.add(double[].class);
types.add(byte[].class);
types.add(char[].class);
types.add(Date.class);
// Patterns are used by the regex method of the query builder
types.add(Pattern.class);
// Native types that we support
types.add(ObjectId.class);
types.add(DBRef.class);
BASIC_TYPES = types;
}
/**
* Serialize the fields of the given object using the given object mapper.
* This will convert POJOs to DBObjects where necessary.
*
* @param objectMapper
* The object mapper to use to do the serialization
* @param object
* The object to serialize the fields of
* @return The DBObject, safe for serialization to MongoDB
*/
public static DBObject serializeFields(ObjectMapper objectMapper,
DBObject object) {
BasicDBObject serialised = null;
for (String field : object.keySet()) {
Object value = object.get(field);
Object serialisedValue = serializeField(objectMapper, value);
if (value != serialisedValue) {
// It's changed
if (serialised == null) {
// Make a shallow copy of the object
serialised = new BasicDBObject();
for (String f : object.keySet()) {
serialised.put(f, object.get(f));
}
}
serialised.put(field, serialisedValue);
}
}
if (serialised != null) {
return serialised;
} else {
return object;
}
}
public static DBObject serializeQuery(ObjectMapper objectMapper,
JavaType type, DBQuery.Query query) {
SerializerProvider serializerProvider = JacksonAccessor
.getSerializerProvider(objectMapper);
JsonSerializer serializer = JacksonAccessor.findValueSerializer(
serializerProvider, type);
return serializeQuery(serializerProvider, serializer, query);
}
private static DBObject serializeQuery(
SerializerProvider serializerProvider, JsonSerializer serializer,
DBQuery.Query query) {
DBObject serializedQuery = new BasicDBObject();
for (Map.Entry<String, QueryCondition> field : query.conditions()) {
String key = field.getKey();
QueryCondition condition = field.getValue();
serializedQuery.put(
key,
serializeQueryCondition(serializerProvider, serializer,
key, condition));
}
return serializedQuery;
}
public static Object serializeQueryCondition(ObjectMapper objectMapper,
JavaType type, String key, QueryCondition condition) {
SerializerProvider serializerProvider = JacksonAccessor
.getSerializerProvider(objectMapper);
JsonSerializer serializer = JacksonAccessor.findValueSerializer(
serializerProvider, type);
return serializeQueryCondition(serializerProvider, serializer, key,
condition);
}
private static Object serializeQueryCondition(
SerializerProvider serializerProvider, JsonSerializer serializer,
String key, QueryCondition condition) {
if (condition instanceof SimpleQueryCondition) {
SimpleQueryCondition simple = (SimpleQueryCondition) condition;
if (!simple.requiresSerialization() || simple.getValue() == null) {
return simple.getValue();
} else {
if (!key.startsWith("$")) {
serializer = findQuerySerializer(false, key,
serializerProvider, serializer);
}
return serializeQueryField(simple.getValue(), serializer,
serializerProvider, key);
}
} else if (condition instanceof CollectionQueryCondition) {
CollectionQueryCondition coll = (CollectionQueryCondition) condition;
if (!key.startsWith("$")) {
serializer = findQuerySerializer(coll.targetIsCollection(),
key, serializerProvider, serializer);
}
List<Object> serializedConditions = new ArrayList<Object>();
for (QueryCondition item : coll.getValues()) {
serializedConditions.add(serializeQueryCondition(
serializerProvider, serializer, "$", item));
}
return serializedConditions;
} else {
CompoundQueryCondition compound = (CompoundQueryCondition) condition;
if (!key.startsWith("$")) {
serializer = findQuerySerializer(false, key,
serializerProvider, serializer);
}
return serializeQuery(serializerProvider, serializer,
compound.getQuery());
}
}
private static Object serializeQueryField(Object value,
JsonSerializer serializer, SerializerProvider serializerProvider,
String op) {
if (serializer == null) {
if (value == null || BASIC_TYPES.contains(value.getClass())) {
// Return as is
return value;
} else if (value instanceof Collection) {
Collection<?> coll = (Collection<?>) value;
List<Object> copy = null;
int position = 0;
for (Object item : coll) {
Object returned = serializeQueryField(item, null,
serializerProvider, op);
if (returned != item) {
if (copy == null) {
copy = new ArrayList<Object>(coll);
}
copy.set(position, returned);
}
position++;
}
if (copy != null) {
return copy;
} else {
return coll;
}
} else if (value.getClass().isArray()) {
if (BASIC_TYPES.contains(value.getClass().getComponentType())) {
return value;
}
Object[] array = (Object[]) value;
Object[] copy = null;
for (int i = 0; i < array.length; i++) {
Object returned = serializeQueryField(array[i], null,
serializerProvider, op);
if (returned != array[i]) {
if (copy == null) {
copy = new Object[array.length];
System.arraycopy(array, 0, copy, 0, array.length);
}
copy[i] = returned;
}
}
if (copy != null) {
return copy;
} else {
return array;
}
} else {
// We don't know what it is, just find a serializer for it
serializer = JacksonAccessor.findValueSerializer(
serializerProvider, value.getClass());
}
}
BsonObjectGenerator objectGenerator = new BsonObjectGenerator();
try {
serializer.serialize(value, objectGenerator, serializerProvider);
} catch (IOException e) {
throw new MongoJsonMappingException("Error serializing value "
+ value + " in DBQuery operation " + op, e);
}
return objectGenerator.getValue();
}
/**
* Serialize the given field
*
* @param objectMapper
* The object mapper to serialize it with
* @param value
* The value to serialize
* @return The serialized field. May return the same object if no
* serialization was necessary.
*/
public static Object serializeField(ObjectMapper objectMapper, Object value) {
if (value == null || BASIC_TYPES.contains(value.getClass())) {
// Return as is
return value;
} else if (value instanceof DBObject) {
return serializeFields(objectMapper, (DBObject) value);
} else if (value instanceof Collection) {
Collection<?> coll = (Collection<?>) value;
List<Object> copy = null;
int position = 0;
for (Object item : coll) {
Object returned = serializeField(objectMapper, item);
if (returned != item) {
if (copy == null) {
copy = new ArrayList<Object>(coll);
}
copy.set(position, returned);
}
position++;
}
if (copy != null) {
return copy;
} else {
return coll;
}
} else if (value.getClass().isArray()) {
if (BASIC_TYPES.contains(value.getClass().getComponentType())) {
return value;
}
Object[] array = (Object[]) value;
Object[] copy = null;
for (int i = 0; i < array.length; i++) {
Object returned = serializeField(objectMapper, array[i]);
if (returned != array[i]) {
if (copy == null) {
copy = new Object[array.length];
System.arraycopy(array, 0, copy, 0, array.length);
}
copy[i] = returned;
}
}
if (copy != null) {
return copy;
} else {
return array;
}
} else {
// We don't know what it is, serialise it
BsonObjectGenerator generator = new BsonObjectGenerator();
try {
objectMapper.writeValue(generator, value);
} catch (JsonMappingException e) {
throw new MongoJsonMappingException(e);
} catch (IOException e) {
throw new RuntimeException(
"Somehow got an IOException writing to memory", e);
}
return generator.getValue();
}
}
public static DBObject serializeDBUpdate(
Map<String, Map<String, UpdateOperationValue>> update,
ObjectMapper objectMapper, JavaType javaType) {
SerializerProvider serializerProvider = JacksonAccessor
.getSerializerProvider(objectMapper);
BasicDBObject dbObject = new BasicDBObject();
JsonSerializer serializer = null;
for (Map.Entry<String, Map<String, UpdateOperationValue>> op : update
.entrySet()) {
BasicDBObject opObject = new BasicDBObject();
for (Map.Entry<String, UpdateOperationValue> field : op.getValue()
.entrySet()) {
Object value;
if (field.getValue().requiresSerialization()) {
if (serializer == null) {
serializer = JacksonAccessor.findValueSerializer(
serializerProvider, javaType);
}
JsonSerializer fieldSerializer = findUpdateSerializer(field
.getValue().isTargetCollection(), field.getKey(),
serializerProvider, serializer);
if (fieldSerializer != null) {
value = serializeUpdateField(field.getValue(),
fieldSerializer, serializerProvider,
op.getKey(), field.getKey());
} else {
// Try default serializers
value = serializeField(objectMapper, field.getValue()
.getValue());
}
} else {
value = field.getValue().getValue();
}
if (op.getKey().equals("$addToSet")
&& field.getValue() instanceof MultiUpdateOperationValue) {
// Add to set needs $each for multi values
opObject.put(field.getKey(), new BasicDBObject("$each",
value));
} else {
opObject.put(field.getKey(), value);
}
}
dbObject.append(op.getKey(), opObject);
}
return dbObject;
}
private static Object serializeUpdateField(UpdateOperationValue value,
JsonSerializer serializer, SerializerProvider serializerProvider,
String op, String field) {
if (value instanceof MultiUpdateOperationValue) {
List<Object> results = new ArrayList<Object>();
for (Object item : ((MultiUpdateOperationValue) value).getValues()) {
results.add(serializeUpdateField(item, serializer,
serializerProvider, op, field));
}
return results;
} else {
return serializeUpdateField(value.getValue(), serializer,
serializerProvider, op, field);
}
}
private static Object serializeUpdateField(Object value,
JsonSerializer serializer, SerializerProvider serializerProvider,
String op, String field) {
BsonObjectGenerator objectGenerator = new BsonObjectGenerator();
try {
serializer.serialize(value, objectGenerator, serializerProvider);
} catch (IOException e) {
throw new MongoJsonMappingException(
"Error serializing value in DBUpdate operation " + op
+ " field " + field, e);
}
return objectGenerator.getValue();
}
private static JsonSerializer<?> findUpdateSerializer(
boolean targetIsCollection, String fieldPath,
SerializerProvider serializerProvider, JsonSerializer serializer) {
if (serializer instanceof BeanSerializerBase) {
JsonSerializer<?> fieldSerializer = serializer;
// Iterate through the components of the field name
String[] fields = fieldPath.split("\\.");
for (String field : fields) {
if (fieldSerializer == null) {
// We don't have a field serializer to look up the field on,
// so give up
return null;
}
if (field.equals("$") || field.matches("\\d+")) {
// The current serializer must be a collection
if (fieldSerializer instanceof ContainerSerializer) {
JsonSerializer contentSerializer = ((ContainerSerializer) fieldSerializer)
.getContentSerializer();
if (contentSerializer == null) {
// Work it out
JavaType contentType = ((ContainerSerializer) fieldSerializer)
.getContentType();
if (contentType != null) {
contentSerializer = JacksonAccessor
.findValueSerializer(
serializerProvider, contentType);
}
}
fieldSerializer = contentSerializer;
} else {
// Give up, don't attempt to serialise it
return null;
}
} else if (fieldSerializer instanceof BeanSerializerBase) {
BeanPropertyWriter writer = JacksonAccessor
.findPropertyWriter(
(BeanSerializerBase) serializer, field);
if (writer != null) {
fieldSerializer = writer.getSerializer();
if (fieldSerializer == null) {
// Do a generic lookup
fieldSerializer = JacksonAccessor
.findValueSerializer(serializerProvider,
writer.getType());
}
} else {
// Give up
return null;
}
} else if (fieldSerializer instanceof MapSerializer) {
fieldSerializer = ((MapSerializer) fieldSerializer)
.getContentSerializer();
} else {
// Don't know how to find what the serialiser for this field
// is
return null;
}
}
// Now we have a serializer for the field, see if we're supposed to
// be serialising for a collection
if (targetIsCollection) {
if (fieldSerializer instanceof ContainerSerializer) {
fieldSerializer = ((ContainerSerializer) fieldSerializer)
.getContentSerializer();
} else if (fieldSerializer instanceof ObjectIdSerializer) {
// Special case for ObjectIdSerializer, leave as is, the
// ObjectIdSerializer handles both single
// values as well as collections with no problems.
} else {
// Give up
return null;
}
}
return fieldSerializer;
} else {
return null;
}
}
private static JsonSerializer<?> findQuerySerializer(
boolean targetIsCollection, String fieldPath,
SerializerProvider serializerProvider, JsonSerializer serializer) {
if (serializer instanceof BeanSerializerBase
|| serializer instanceof MapSerializer) {
JsonSerializer<?> fieldSerializer = serializer;
// Iterate through the components of the field name
String[] fields = fieldPath.split("\\.");
for (String field : fields) {
if (fieldSerializer == null) {
// We don't have a field serializer to look up the field on,
// so give up
return null;
}
boolean isIndex = field.matches("\\d+");
// First step into the collection if there is one
if (!isIndex) {
while (fieldSerializer instanceof ContainerSerializer) {
JsonSerializer contentSerializer = ((ContainerSerializer) fieldSerializer)
.getContentSerializer();
if (contentSerializer == null) {
// Work it out
JavaType contentType = ((ContainerSerializer) fieldSerializer)
.getContentType();
if (contentType != null) {
contentSerializer = JacksonAccessor
.findValueSerializer(
serializerProvider, contentType);
}
}
fieldSerializer = contentSerializer;
}
}
if (isIndex) {
if (fieldSerializer instanceof ContainerSerializer) {
JsonSerializer contentSerializer = ((ContainerSerializer) fieldSerializer)
.getContentSerializer();
if (contentSerializer == null) {
// Work it out
JavaType contentType = ((ContainerSerializer) fieldSerializer)
.getContentType();
if (contentType != null) {
contentSerializer = JacksonAccessor
.findValueSerializer(
serializerProvider, contentType);
}
}
fieldSerializer = contentSerializer;
} else {
// Give up, don't attempt to serialise it
return null;
}
} else if (fieldSerializer instanceof BeanSerializerBase) {
BeanPropertyWriter writer = JacksonAccessor
.findPropertyWriter(
(BeanSerializerBase) serializer, field);
if (writer != null) {
fieldSerializer = writer.getSerializer();
if (fieldSerializer == null) {
// Do a generic lookup
fieldSerializer = JacksonAccessor
.findValueSerializer(serializerProvider,
writer.getType());
}
} else {
// Give up
return null;
}
} else if (fieldSerializer instanceof MapSerializer) {
fieldSerializer = ((MapSerializer) fieldSerializer)
.getContentSerializer();
} else {
// Don't know how to find what the serialiser for this field
// is
return null;
}
}
// Now we have a serializer for the field, see if we're supposed to
// be serialising for a collection
if (targetIsCollection) {
if (fieldSerializer instanceof ContainerSerializer) {
fieldSerializer = ((ContainerSerializer) fieldSerializer)
.getContentSerializer();
} else if (fieldSerializer instanceof ObjectIdSerializer) {
// Special case for ObjectIdSerializer, leave as is, the
// ObjectIdSerializer handles both single
// values as well as collections with no problems.
} else {
// Give up
return null;
}
}
return fieldSerializer;
} else {
return null;
}
}
}