Remove duplicate code in MappingMongoConverter.

Also remove MapUtils and blend new methods into the existing BsonUtils.

Original Pull Request: #3575
This commit is contained in:
Christoph Strobl
2021-03-02 14:33:23 +01:00
parent 92079ca200
commit be8e70225a
4 changed files with 203 additions and 163 deletions

View File

@@ -1,111 +0,0 @@
/*
* Copyright 2021 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
*
* https://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.convert;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import com.mongodb.DBObject;
/**
* @author Mark Paluch
*/
class MapUtils {
/**
* Returns given object as {@link Collection}. Will return the {@link Collection} as is if the source is a
* {@link Collection} already, will convert an array into a {@link Collection} or simply create a single element
* collection for everything else.
*
* @param source
* @return
*/
static Collection<?> asCollection(Object source) {
if (source instanceof Collection) {
return (Collection<?>) source;
}
return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source);
}
@SuppressWarnings("unchecked")
static Map<String, Object> asMap(Bson bson) {
if (bson instanceof Document) {
return (Document) bson;
}
if (bson instanceof DBObject) {
return ((DBObject) bson).toMap();
}
throw new IllegalArgumentException(
String.format("Cannot read %s. as map. Given Bson must be a Document or DBObject!", bson.getClass()));
}
static void addToMap(Bson bson, String key, @Nullable Object value) {
if (bson instanceof Document) {
((Document) bson).put(key, value);
return;
}
if (bson instanceof DBObject) {
((DBObject) bson).put(key, value);
return;
}
throw new IllegalArgumentException(String.format(
"Cannot add key/value pair to %s. as map. Given Bson must be a Document or DBObject!", bson.getClass()));
}
static void addAllToMap(Bson bson, Map<String, ?> value) {
if (bson instanceof Document) {
((Document) bson).putAll(value);
return;
}
if (bson instanceof DBObject) {
((DBObject) bson).putAll(value);
return;
}
throw new IllegalArgumentException(
String.format("Cannot add all to %s. Given Bson must be a Document or DBObject.", bson.getClass()));
}
static void removeFromMap(Bson bson, String key) {
if (bson instanceof Document) {
((Document) bson).remove(key);
return;
}
if (bson instanceof DBObject) {
((DBObject) bson).removeField(key);
return;
}
throw new IllegalArgumentException(
String.format("Cannot remove from %s. Given Bson must be a Document or DBObject.", bson.getClass()));
}
}

View File

@@ -70,6 +70,7 @@ import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback;
import org.springframework.data.mongodb.core.mapping.event.AfterConvertEvent;
import org.springframework.data.mongodb.core.mapping.event.AfterLoadEvent;
import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
@@ -270,17 +271,25 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
protected <S extends Object> S read(TypeInformation<S> type, Bson bson) {
return doRead(getConversionContext(ObjectPath.ROOT), type, bson);
return readDocument(getConversionContext(ObjectPath.ROOT), bson, type);
}
/**
* Conversion method to materialize an object from a {@link Bson document}. Can be overridden by subclasses.
*
* @param context must not be {@literal null}
* @param bson must not be {@literal null}
* @param typeHint the {@link TypeInformation} to be used to unmarshall this {@link Document}.
* @return the converted object, will never be {@literal null}.
* @since 3.2
*/
@SuppressWarnings("unchecked")
private <S extends Object> S doRead(ConversionContext context, TypeInformation<S> type, Bson bson) {
protected <S extends Object> S readDocument(ConversionContext context, Bson bson,
TypeInformation<? extends S> typeHint) {
Assert.notNull(bson, "Bson must not be null!");
// TODO: Cleanup duplication
TypeInformation<? extends S> typeToUse = typeMapper.readType(bson, type);
Class<? extends S> rawType = typeToUse.getType();
Document document = bson instanceof BasicDBObject ? new Document((BasicDBObject) bson) : (Document) bson;
TypeInformation<? extends S> typeToRead = typeMapper.readType(document, typeHint);
Class<? extends S> rawType = typeToRead.getType();
if (conversions.hasCustomReadTarget(bson.getClass(), rawType)) {
return doConvert(bson, rawType);
@@ -303,34 +312,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return (S) bson;
}
return context.convert(bson, typeToUse);
}
/**
* Conversion method to materialize an object from a {@link Bson document}. Can be overridden by subclasses.
*
* @param context must not be {@literal null}
* @param bson must not be {@literal null}
* @param typeHint the {@link TypeInformation} to be used to unmarshall this {@link Document}.
* @return the converted object, will never be {@literal null}.
* @since 3.2
*/
@SuppressWarnings("unchecked")
protected <S extends Object> S readDocument(ConversionContext context, Bson bson,
TypeInformation<? extends S> typeHint) {
// TODO: Cleanup duplication
Document document = bson instanceof BasicDBObject ? new Document((BasicDBObject) bson) : (Document) bson;
TypeInformation<? extends S> typeToRead = typeMapper.readType(document, typeHint);
Class<? extends S> rawType = typeToRead.getType();
if (conversions.hasCustomReadTarget(bson.getClass(), rawType)) {
return doConvert(bson, rawType);
}
if (typeToRead.isMap()) {
return (S) bson;
return context.convert(bson, typeToRead);
}
if (BSON.isAssignableFrom(typeHint)) {
@@ -572,9 +555,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Object target = obj instanceof LazyLoadingProxy ? ((LazyLoadingProxy) obj).getTarget() : obj;
writeInternal(target, bson, type);
if (MapUtils.asMap(bson).containsKey("_id") && MapUtils.asMap(bson).get("_id") == null) {
MapUtils.removeFromMap(bson, "_id");
}
BsonUtils.removeNullId(bson);
if (requiresTypeHint(entityType)) {
typeMapper.writeType(type, bson);
@@ -608,7 +589,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (customTarget.isPresent()) {
Document result = doConvert(obj, Document.class);
MapUtils.addAllToMap(bson, result);
BsonUtils.addAllToMap(bson, result);
return;
}
@@ -709,12 +690,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
if (valueType.isCollectionLike()) {
List<Object> collectionInternal = createCollection(MapUtils.asCollection(obj), prop);
List<Object> collectionInternal = createCollection(BsonUtils.asCollection(obj), prop);
accessor.put(prop, collectionInternal);
return;
}
if (valueType.isMap()) {
Bson mapDbObj = createMap((Map<Object, Object>) obj, prop);
accessor.put(prop, mapDbObj);
return;
@@ -859,7 +842,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
collection.add(getPotentiallyConvertedSimpleWrite(element,
componentType != null ? componentType.getType() : Object.class));
} else if (element instanceof Collection || elementType.isArray()) {
collection.add(writeCollectionInternal(MapUtils.asCollection(element), componentType, new BasicDBList()));
collection.add(writeCollectionInternal(BsonUtils.asCollection(element), componentType, new BasicDBList()));
} else {
Document document = new Document();
writeInternal(element, document, componentType);
@@ -890,14 +873,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (val == null || conversions.isSimpleType(val.getClass())) {
writeSimpleInternal(val, bson, simpleKey);
} else if (val instanceof Collection || val.getClass().isArray()) {
MapUtils.addToMap(bson, simpleKey,
writeCollectionInternal(MapUtils.asCollection(val), propertyType.getMapValueType(), new BasicDBList()));
BsonUtils.addToMap(bson, simpleKey,
writeCollectionInternal(BsonUtils.asCollection(val), propertyType.getMapValueType(), new BasicDBList()));
} else {
Document document = new Document();
TypeInformation<?> valueTypeInfo = propertyType.isMap() ? propertyType.getMapValueType()
: ClassTypeInformation.OBJECT;
writeInternal(val, document, valueTypeInfo);
MapUtils.addToMap(bson, simpleKey, document);
BsonUtils.addToMap(bson, simpleKey, document);
}
} else {
throw new MappingException("Cannot use a complex object as a key value.");
@@ -997,7 +980,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
* @param key must not be {@literal null}.
*/
private void writeSimpleInternal(@Nullable Object value, Bson bson, String key) {
MapUtils.addToMap(bson, key, getPotentiallyConvertedSimpleWrite(value, Object.class));
BsonUtils.addToMap(bson, key, getPotentiallyConvertedSimpleWrite(value, Object.class));
}
private void writeSimpleInternal(@Nullable Object value, Bson bson, MongoPersistentProperty property) {
@@ -1035,7 +1018,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (value instanceof byte[]) {
return value;
}
return MapUtils.asCollection(value);
return BsonUtils.asCollection(value);
}
return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value;
@@ -1198,7 +1181,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Class<?> rawKeyType = keyType != null ? keyType.getType() : Object.class;
Class<?> rawValueType = valueType.getType();
Map<String, Object> sourceMap = MapUtils.asMap(bson);
Map<String, Object> sourceMap = BsonUtils.asMap(bson);
Map<Object, Object> map = CollectionFactory.createMap(mapType, rawKeyType, sourceMap.keySet().size());
if (!DBRef.class.equals(rawValueType) && isCollectionOfDbRefWhereBulkFetchIsPossible(sourceMap.values())) {
@@ -1322,7 +1305,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return newDocument;
}
// TODO: hide
// TODO: hide in 4.0
public List<Object> maybeConvertList(Iterable<?> source, @Nullable TypeInformation<?> typeInformation) {
List<Object> newDbl = new ArrayList<>();
@@ -1462,7 +1445,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
maybeEmitEvent(
new AfterLoadEvent<>(document, (Class<T>) type.getType(), collectionName));
target = (T) doRead(context, type, document);
target = (T) readDocument(context, document, type);
}
if (target != null) {

View File

@@ -17,12 +17,14 @@ package org.springframework.data.mongodb.util;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Map;
import java.util.StringJoiner;
import java.util.function.Function;
import java.util.stream.StreamSupport;
import org.bson.BSONObject;
import org.bson.BsonBinary;
import org.bson.BsonBoolean;
import org.bson.BsonDouble;
@@ -36,11 +38,11 @@ import org.bson.codecs.DocumentCodec;
import org.bson.conversions.Bson;
import org.bson.json.JsonParseException;
import org.bson.types.ObjectId;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mongodb.CodecRegistryProvider;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -50,6 +52,8 @@ import com.mongodb.DBRef;
import com.mongodb.MongoClientSettings;
/**
* Internal API for operations on {@link Bson} elements that can be either {@link Document} or {@link DBObject}.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.0
@@ -70,6 +74,9 @@ public class BsonUtils {
if (bson instanceof BasicDBObject) {
return ((BasicDBObject) bson);
}
if (bson instanceof DBObject) {
return ((DBObject) bson).toMap();
}
return (Map) bson.toBsonDocument(Document.class, MongoClientSettings.getDefaultCodecRegistry());
}
@@ -77,14 +84,110 @@ public class BsonUtils {
public static void addToMap(Bson bson, String key, @Nullable Object value) {
if (bson instanceof Document) {
((Document) bson).put(key, value);
return;
}
if (bson instanceof DBObject) {
((DBObject) bson).put(key, value);
if (bson instanceof BSONObject) {
((BSONObject) bson).put(key, value);
return;
}
throw new IllegalArgumentException("o_O what's that? Cannot add value to " + bson.getClass());
throw new IllegalArgumentException(String.format(
"Cannot add key/value pair to %s. as map. Given Bson must be a Document or BSONObject!", bson.getClass()));
}
/**
* Add all entries from the given {@literal source} {@link Map} to the {@literal target}.
*
* @param target must not be {@literal null}.
* @param source must not be {@literal null}.
* @since 3.2
*/
public static void addAllToMap(Bson target, Map<String, ?> source) {
if (target instanceof Document) {
((Document) target).putAll(source);
return;
}
if (target instanceof BSONObject) {
((BSONObject) target).putAll(source);
return;
}
throw new IllegalArgumentException(
String.format("Cannot add all to %s. Given Bson must be a Document or BSONObject.", target.getClass()));
}
/**
* Check if a given entry (key/value pair) is present in the given {@link Bson}.
*
* @param bson must not be {@literal null}.
* @param key must not be {@literal null}.
* @param value can be {@literal null}.
* @return {@literal true} if (key/value pair) is present.
* @since 3.2
*/
public static boolean contains(Bson bson, String key, @Nullable Object value) {
if (bson instanceof Document) {
Document doc = (Document) bson;
return doc.containsKey(key) && ObjectUtils.nullSafeEquals(doc.get(key), value);
}
if (bson instanceof BSONObject) {
BSONObject bsonObject = (BSONObject) bson;
return bsonObject.containsField(key) && ObjectUtils.nullSafeEquals(bsonObject.get(key), value);
}
Map<String, Object> map = asMap(bson);
return map.containsKey(key) && ObjectUtils.nullSafeEquals(map.get(key), value);
}
/**
* Remove {@code _id : null} from the given {@link Bson} if present.
*
* @param bson must not be {@literal null}.
* @since 3.2
*/
public static boolean removeNullId(Bson bson) {
if (!contains(bson, "_id", null)) {
return false;
}
removeFrom(bson, "_id");
return true;
}
/**
* Remove the given {@literal key} from the {@link Bson} value.
*
* @param bson must not be {@literal null}.
* @param key must not be {@literal null}.
* @since 3.2
*/
static void removeFrom(Bson bson, String key) {
if (bson instanceof Document) {
((Document) bson).remove(key);
return;
}
if (bson instanceof BSONObject) {
((BSONObject) bson).removeField(key);
return;
}
throw new IllegalArgumentException(
String.format("Cannot remove from %s. Given Bson must be a Document or BSONObject.", bson.getClass()));
}
/**
@@ -282,6 +385,24 @@ public class BsonUtils {
.orElseGet(() -> new DocumentCodec(codecRegistryProvider.getCodecRegistry())));
}
/**
* Returns given object as {@link Collection}. Will return the {@link Collection} as is if the source is a
* {@link Collection} already, will convert an array into a {@link Collection} or simply create a single element
* collection for everything else.
*
* @param source must not be {@literal null}.
* @return never {@literal null}.
* @since 3.2
*/
public static Collection<?> asCollection(Object source) {
if (source instanceof Collection) {
return (Collection<?>) source;
}
return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source);
}
@Nullable
private static String toJson(@Nullable Object value) {

View File

@@ -17,11 +17,16 @@ package org.springframework.data.mongodb.util.json;
import static org.assertj.core.api.Assertions.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.bson.BsonDouble;
import org.bson.BsonInt32;
import org.bson.BsonInt64;
import org.bson.BsonObjectId;
import org.bson.BsonString;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.junit.jupiter.api.Test;
import org.springframework.data.mongodb.util.BsonUtils;
@@ -64,4 +69,46 @@ class BsonUtilsTest {
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> BsonUtils.simpleToBsonValue(new Object()));
}
@Test // GH-3571
void removeNullIdIfNull() {
Document source = new Document("_id", null).append("value", "v-1");
assertThat(BsonUtils.removeNullId(source)).isTrue();
assertThat(source).doesNotContainKey("_id").containsKey("value");
}
@Test // GH-3571
void removeNullIdDoesNotTouchNonNullOn() {
Document source = new Document("_id", "id-value").append("value", "v-1");
assertThat(BsonUtils.removeNullId(source)).isFalse();
assertThat(source).containsKeys("_id", "value");
}
@Test // GH-3571
void asCollectionDoesNotModifyCollection() {
Object source = new ArrayList<>(0);
assertThat(BsonUtils.asCollection(source)).isSameAs(source);
}
@Test // GH-3571
void asCollectionConvertsArrayToCollection() {
Object source = new String[]{"one", "two"};
assertThat((Collection)BsonUtils.asCollection(source)).containsExactly("one", "two");
}
@Test // GH-3571
void asCollectionConvertsWrapsNonIterable() {
Object source = 100L;
assertThat((Collection)BsonUtils.asCollection(source)).containsExactly(source);
}
}