From 651255ca5832974d59679469da3a42bba94992bc Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Thu, 14 Jun 2012 12:19:06 +0200 Subject: [PATCH] DATAMONGO-447 - Fixed broken log output in debug level. The debug output now uses the already mapped query object when concatenating the log string. Improved applying the id after save operations by inspecting whether the object already has the id set before trying to set it. This could have caused problems in case you use a complex id and don't provide a custom converter as it can be serialized out of the box. Fixed minor glitch in MappingMongoConverter which was not really a bug as another path through the code has covered the scenario later on. Introduced SerializationUtils class that provides a method to safely serialize objects to pseudo JSON. Pseudo in the sense that it simply renders a complex object as { $java : object.toString() }. This is useful for debug output before the DBObject was mapped into Mongo-native types. --- .../data/mongodb/core/MongoTemplate.java | 17 ++- .../data/mongodb/core/SerializationUtils.java | 110 ++++++++++++++++++ .../core/convert/MappingMongoConverter.java | 3 +- .../data/mongodb/core/query/Query.java | 12 ++ .../repository/query/MongoQueryCreator.java | 2 +- .../data/mongodb/core/MongoTemplateTests.java | 29 +++++ .../core/SerializationUtilsUnitTests.java | 66 +++++++++++ .../MappingMongoConverterUnitTests.java | 18 +++ 8 files changed, 251 insertions(+), 6 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SerializationUtils.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SerializationUtilsUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 0fcf67e29..f29125f44 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -50,6 +50,7 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.authentication.UserCredentials; import org.springframework.data.convert.EntityReader; +import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.BeanWrapper; import org.springframework.data.mapping.model.MappingException; @@ -938,7 +939,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware { entityClass, null, queryObject); WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); if (LOGGER.isDebugEnabled()) { - LOGGER.debug("remove using query: " + queryObject + " in collection: " + collection.getName()); + LOGGER.debug("remove using query: " + dboq + " in collection: " + collection.getName()); } if (writeConcernToUse == null) { wr = collection.remove(dboq); @@ -1364,9 +1365,19 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware { return; } + ConversionService conversionService = mongoConverter.getConversionService(); + BeanWrapper, Object> wrapper = BeanWrapper.create(savedObject, conversionService); + try { - BeanWrapper.create(savedObject, mongoConverter.getConversionService()).setProperty(idProp, id); - return; + + Object idValue = wrapper.getProperty(idProp); + + if (idValue != null) { + return; + } + + wrapper.setProperty(idProp, id); + } catch (IllegalAccessException e) { throw new MappingException(e.getMessage(), e); } catch (InvocationTargetException e) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SerializationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SerializationUtils.java new file mode 100644 index 000000000..d21098843 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SerializationUtils.java @@ -0,0 +1,110 @@ +/* + * Copyright 2012 the original author or authors. + * + * 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.springframework.data.mongodb.core; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.core.convert.converter.Converter; + +import com.mongodb.DBObject; +import com.mongodb.util.JSON; + +/** + * Utility methods for JSON serialization. + * + * @author Oliver Gierke + */ +public abstract class SerializationUtils { + + private SerializationUtils() { + + } + + /** + * Serializes the given object into pseudo-JSON meaning it's trying to create a JSON representation as far as possible + * but falling back to the given object's {@link Object#toString()} method if it's not serializable. Useful for + * printing raw {@link DBObject}s containing complex values before actually converting them into Mongo native types. + * + * @param value + * @return + */ + public static String serializeToJsonSafely(Object value) { + + if (value == null) { + return null; + } + + try { + return JSON.serialize(value); + } catch (Exception e) { + if (value instanceof Collection) { + return toString((Collection) value); + } else if (value instanceof Map) { + return toString((Map) value); + } else if (value instanceof DBObject) { + return toString(((DBObject) value).toMap()); + } else { + return String.format("{ $java : %s }", value.toString()); + } + } + } + + private static String toString(Map source) { + return iterableToDelimitedString(source.entrySet(), "{ ", " }", new Converter, Object>() { + public Object convert(Entry source) { + return String.format("\"%s\" : %s", source.getKey(), serializeToJsonSafely(source.getValue())); + } + }); + } + + private static String toString(Collection source) { + return iterableToDelimitedString(source, "[ ", " ]", new Converter() { + public Object convert(Object source) { + return serializeToJsonSafely(source); + } + }); + } + + /** + * Creates a string representation from the given {@link Iterable} prepending the postfix, applying the given + * {@link Converter} to each element before adding it to the result {@link String}, concatenating each element with + * {@literal ,} and applying the postfix. + * + * @param source + * @param prefix + * @param postfix + * @param transformer + * @return + */ + private static String iterableToDelimitedString(Iterable source, String prefix, String postfix, + Converter transformer) { + + StringBuilder builder = new StringBuilder(prefix); + Iterator iterator = source.iterator(); + + while (iterator.hasNext()) { + builder.append(transformer.convert(iterator.next())); + if (iterator.hasNext()) { + builder.append(", "); + } + } + + return builder.append(postfix).toString(); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index 96b1c7160..da069ad6f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -439,7 +439,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App * * @param collection must not be {@literal null}. * @param property must not be {@literal null}. - * * @return */ protected DBObject createCollection(Collection collection, MongoPersistentProperty property) { @@ -819,7 +818,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return null; } - Class target = conversions.getCustomWriteTarget(getClass()); + Class target = conversions.getCustomWriteTarget(obj.getClass()); if (target != null) { return conversionService.convert(obj, target); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java index 40565fd58..58798acaf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java @@ -15,6 +15,8 @@ */ package org.springframework.data.mongodb.core.query; +import static org.springframework.data.mongodb.core.SerializationUtils.*; + import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -140,4 +142,14 @@ public class Query { protected List getCriteria() { return new ArrayList(this.criteria.values()); } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("Query: %s, Fields: %s, Sort: %s", serializeToJsonSafely(getQueryObject()), + serializeToJsonSafely(getFieldsObject()), serializeToJsonSafely(getSortObject())); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java index c73aba066..1fe9bd706 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java @@ -157,7 +157,7 @@ class MongoQueryCreator extends AbstractQueryCreator { QueryUtils.applySorting(query, sort); if (LOG.isDebugEnabled()) { - LOG.debug("Created query " + query.getQueryObject()); + LOG.debug("Created query " + query); } return query; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java index 045c9351b..4f4d13f7f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java @@ -1103,6 +1103,35 @@ public class MongoTemplateTests { assertThat(result.get(0).field, is("Beauford")); } + /** + * @see DATAMONGO-447 + */ + @Test + public void storesAndRemovesTypeWithComplexId() { + + MyId id = new MyId(); + id.first = "foo"; + id.second = "bar"; + + TypeWithMyId source = new TypeWithMyId(); + source.id = id; + + template.save(source); + template.remove(query(where("id").is(id)), TypeWithMyId.class); + } + + static class MyId { + + String first; + String second; + } + + static class TypeWithMyId { + + @Id + MyId id; + } + public static class Sample { @Id diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SerializationUtilsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SerializationUtilsUnitTests.java new file mode 100644 index 000000000..883729bb6 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SerializationUtilsUnitTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012 the original author or authors. + * + * 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.springframework.data.mongodb.core; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.springframework.data.mongodb.core.SerializationUtils.*; + +import java.util.Arrays; + +import org.hamcrest.Matcher; +import org.junit.Test; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + +/** + * Unit tests for {@link SerializationUtils}. + * + * @author Oliver Gierke + */ +public class SerializationUtilsUnitTests { + + @Test + public void writesSimpleDBObject() { + + DBObject dbObject = new BasicDBObject("foo", "bar"); + assertThat(serializeToJsonSafely(dbObject), is("{ \"foo\" : \"bar\"}")); + } + + @Test + public void writesComplexObjectAsPlainToString() { + + DBObject dbObject = new BasicDBObject("foo", new Complex()); + assertThat(serializeToJsonSafely(dbObject), + startsWith("{ \"foo\" : { $java : org.springframework.data.mongodb.core.SerializationUtilsUnitTests$Complex")); + } + + @Test + public void writesCollection() { + + DBObject dbObject = new BasicDBObject("foo", Arrays.asList("bar", new Complex())); + Matcher expectedOutput = allOf( + startsWith("{ \"foo\" : [ \"bar\", { $java : org.springframework.data.mongodb.core.SerializationUtilsUnitTests$Complex"), + endsWith(" } ] }")); + assertThat(serializeToJsonSafely(dbObject), is(expectedOutput)); + } + + static class Complex { + + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index 41e051c4c..e982a1167 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -120,6 +120,24 @@ public class MappingMongoConverterUnitTests { assertThat(result.birthDate, is(notNullValue())); } + @Test + public void convertsCustomTypeOnConvertToMongoType() { + + List> converters = new ArrayList>(); + converters.add(new LocalDateToDateConverter()); + converters.add(new DateToLocalDateConverter()); + + CustomConversions conversions = new CustomConversions(converters); + mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder()); + + converter = new MappingMongoConverter(factory, mappingContext); + converter.setCustomConversions(conversions); + converter.afterPropertiesSet(); + + LocalDate date = new LocalDate(); + converter.convertToMongoType(date); + } + /** * @see DATAMONGO-130 */