/* * 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.Map.Entry; import java.util.Set; import java.util.regex.Pattern; import org.bson.types.ObjectId; import org.mongojack.Aggregation; import org.mongojack.Aggregation.Expression; import org.mongojack.Aggregation.Group.Accumulator; import org.mongojack.Aggregation.Pipeline; import org.mongojack.Aggregation.Pipeline.Stage; import org.mongojack.DBProjection.ProjectionBuilder; 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.BasicDBList; 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) fieldSerializer, 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) fieldSerializer, 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; } } public static List<DBObject> serializePipeline(ObjectMapper objectMapper, JavaType type, Pipeline<?> pipeline) { SerializerProvider serializerProvider = JacksonAccessor .getSerializerProvider(objectMapper); JsonSerializer<?> serializer = JacksonAccessor.findValueSerializer( serializerProvider, type); List<DBObject> serializedPipeline = new ArrayList<DBObject>(); for (Pipeline.Stage<?> stage: pipeline.stages()) { serializedPipeline.add(serializePipelineStage(serializerProvider, serializer, stage)); } return serializedPipeline; } private static DBObject serializePipelineStage( SerializerProvider serializerProvider, JsonSerializer<?> serializer, Pipeline.Stage<?> stage) { if (stage instanceof Aggregation.Limit) { return new BasicDBObject("$limit", ((Aggregation.Limit) stage).limit()); } if (stage instanceof Aggregation.Skip) { return new BasicDBObject("$skip", ((Aggregation.Skip) stage).skip()); } if (stage instanceof Aggregation.Sort) { return new BasicDBObject("$sort", ((Aggregation.Sort) stage).builder()); } if (stage instanceof Aggregation.Unwind) { return new BasicDBObject("$unwind", ((Object) ((Aggregation.Unwind) stage).path()).toString()); } if (stage instanceof Aggregation.Match) { return new BasicDBObject("$match", serializeQuery(serializerProvider, serializer, ((Aggregation.Match) stage).query())); } if (stage instanceof Aggregation.Project) { ProjectionBuilder builder = ((Aggregation.Project) stage).builder(); BasicDBObject object = new BasicDBObject(); for (Entry<String, Object> entry : builder.entrySet()) { if (entry.getValue() instanceof Expression<?>) { object.append(entry.getKey(), serializeExpression(serializerProvider, serializer, (Expression<?>) entry.getValue())); } else { object.append(entry.getKey(), entry.getValue()); } } return new BasicDBObject("$project", object); } if (stage instanceof Aggregation.Group) { Aggregation.Group group = (Aggregation.Group) stage; BasicDBObject object = new BasicDBObject("_id", serializeExpression(serializerProvider, serializer, group.key())); for (Map.Entry<String, Aggregation.Group.Accumulator> field : group.calculatedFields()) { object.append(field.getKey(), serializeAccumulator(serializerProvider, serializer, field.getValue())); } return new BasicDBObject("$group", object); } if (stage instanceof Aggregation.Out) { return new BasicDBObject("$out", ((Object) ((Aggregation.Out) stage).collectionName())); } throw new IllegalArgumentException(stage.getClass().getName()); } private static DBObject serializeAccumulator(SerializerProvider serializerProvider, JsonSerializer<?> serializer, Accumulator accumulator) { return new BasicDBObject(accumulator.operator.name(), serializeExpression(serializerProvider, serializer, accumulator.expression)); } private static Object serializeExpression(SerializerProvider serializerProvider, JsonSerializer<?> serializer, Expression<?> expression) { if (expression instanceof Aggregation.FieldPath) { return ((Aggregation.FieldPath) expression).toString(); } if (expression instanceof Aggregation.Literal<?>) { return new BasicDBObject("$literal", ((Aggregation.Literal<?>) expression).value()); } if (expression instanceof Aggregation.ExpressionObject) { BasicDBObject object = new BasicDBObject(); for (Entry<String, Expression<?>> property : ((Aggregation.ExpressionObject) expression).properties()) { object.append(property.getKey(), serializeExpression(serializerProvider, serializer, property.getValue())); } return object; } if (expression instanceof Aggregation.OperatorExpression) { Aggregation.OperatorExpression<?> oe = (Aggregation.OperatorExpression<?>) expression; BasicDBList operands = new BasicDBList(); for (Expression<?> e : oe.operands()) { operands.add(serializeExpression(serializerProvider, serializer, e)); } return new BasicDBObject(oe.operator(), operands); } throw new IllegalArgumentException(expression.getClass().getName()); } }