Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.1.x-GH-5092-SNAPSHOT</version>
<packaging>pom</packaging>

<name>Spring Data MongoDB</name>
Expand Down
2 changes: 1 addition & 1 deletion spring-data-mongodb-distribution/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.1.x-GH-5092-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
2 changes: 1 addition & 1 deletion spring-data-mongodb/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.1.x-GH-5092-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ public class MappingMongoConverter extends AbstractMongoConverter
expressionParser);
private final CachingValueExpressionEvaluatorFactory expressionEvaluatorFactory = new CachingValueExpressionEvaluatorFactory(
expressionParser, this, o -> spELContext.getEvaluationContext(o));
private ObjectIdConversion<?> objectIdConversion = ObjectIdConversion.enforceHexStringAsObjectId();

/**
* Creates a new {@link MappingMongoConverter} given the new {@link DbRefResolver} and {@link MappingContext}.
Expand Down Expand Up @@ -303,6 +304,10 @@ public MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentPropert
return mappingContext;
}

public void setObjectIdConversion(ObjectIdConversion<?> objectIdConversion) {
this.objectIdConversion = objectIdConversion;
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {

Expand Down Expand Up @@ -333,6 +338,11 @@ public Environment getEnvironment() {
return environment;
}

@Override
public ObjectIdConversion<?> objectIdConversion() {
return objectIdConversion;
}

/**
* Set the {@link EntityCallbacks} instance to use when invoking
* {@link org.springframework.data.mapping.callback.EntityCallback callbacks} like the {@link AfterConvertCallback}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
import org.jspecify.annotations.Nullable;

import org.springframework.core.convert.ConversionException;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.EntityConverter;
Expand Down Expand Up @@ -102,7 +101,7 @@ public interface MongoConverter
* @throws IllegalArgumentException if {@literal targetType} is {@literal null}.
* @since 2.1
*/
@SuppressWarnings({"unchecked","NullAway"})
@SuppressWarnings({ "unchecked", "NullAway" })
default <S, T> @Nullable T mapValueToTargetType(S source, Class<T> targetType, DbRefResolver dbRefResolver) {

Assert.notNull(targetType, "TargetType must not be null");
Expand Down Expand Up @@ -158,6 +157,7 @@ public interface MongoConverter
* @since 2.2
*/
@Nullable
@SuppressWarnings({ "unchecked", "rawtypes" })
default Object convertId(@Nullable Object id, Class<?> targetType) {

if (id == null || ClassUtils.isAssignableValue(targetType, id)) {
Expand All @@ -166,14 +166,10 @@ default Object convertId(@Nullable Object id, Class<?> targetType) {

if (ClassUtils.isAssignable(ObjectId.class, targetType)) {

if (id instanceof String) {

if (ObjectId.isValid(id.toString())) {
return new ObjectId(id.toString());
}
if (objectIdConversion().isApplicable(id)) {

// avoid ConversionException as convertToMongoType will return String anyways.
return id;
Object converted = ((ObjectIdConversion) objectIdConversion()).convert(id);
return converted != null ? converted : id;
}
}

Expand All @@ -182,7 +178,93 @@ default Object convertId(@Nullable Object id, Class<?> targetType) {
? getConversionService().convert(id, targetType)
: convertToMongoType(id, (TypeInformation<?>) null);
} catch (ConversionException o_O) {
return convertToMongoType(id,(TypeInformation<?>) null);
return convertToMongoType(id, (TypeInformation<?>) null);
}
}

/**
* @return the IdConversion to apply for {@link #convertId(Object, Class)}. A hex string to {@link ObjectId} enforcing
* conversion by default.
* @since 5.1
*/
default ObjectIdConversion<?> objectIdConversion() {
return ObjectIdConversion.enforceHexStringAsObjectId();
}

/**
* Conversion function to apply to non {@literal null} id values of {@link #sourceType()} that do not match the
* desired store id type.
*
* @param <S>
* @since 5.1
*/
interface ObjectIdConversion<S> {

/**
* Converts a {@link String} value that represent a {@link ObjectId#isValid(String) valid ObjectId} into the
* according {@link ObjectId}.
*/
ObjectIdConversion<String> hexToString = new ObjectIdConversion<>() {

@Override
public Class<String> sourceType() {
return String.class;
}

@Override
public @Nullable ObjectId convert(String source) {

if (!ObjectId.isValid(source)) {
return null;
}
return new ObjectId(source);
}
};

/**
* Uses a raw {@link String} value no matter what by always returning {@literal null} for {@link #convert(Object)}.
*/
ObjectIdConversion<String> rawString = new ObjectIdConversion<>() {

@Override
public Class<String> sourceType() {
return String.class;
}

@Override
public @Nullable ObjectId convert(String source) {
return null;
}
};

static ObjectIdConversion<String> enforceHexStringAsObjectId() {
return hexToString;
}

static ObjectIdConversion<String> retainRawString() {
return rawString;
}

/**
* @param source must not be {@literal null}.
* @return {@literal null} to indicate (though {@link #isApplicable(Object)) that no conversion happened.
*/
@Nullable
ObjectId convert(S source);

/**
* The source type for which a potential conversion should happen.
*
* @return must not be {@literal null}.
*/
Class<S> sourceType();

default boolean isApplicable(Object probe) {
return isApplicable(probe.getClass());
}

default boolean isApplicable(Class<?> probe) {
return ClassUtils.isAssignable(sourceType(), probe);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2095,6 +2095,20 @@ void convertStringIdThatIsAnObjectIdHexToObjectIdIfTargetIsObjectId() {
assertThat(converter.convertId(source.toHexString(), ObjectId.class)).isEqualTo(source);
}

@Test // GH-5092
void doNotConvertStringIdThatIsAnObjectIdHexToObjectIdIfTargetIsObjectIdButConfigTellsOtherwise() {

ObjectId source = new ObjectId();
converter = new MappingMongoConverter(resolver, mappingContext) {
@Override
public ObjectIdConversion<?> objectIdConversion() {
return ObjectIdConversion.retainRawString();
}
};
converter.afterPropertiesSet();
assertThat(converter.convertId(source.toHexString(), ObjectId.class)).isEqualTo(source.toHexString());
}

@Test // DATAMONGO-1798
void donNotConvertStringIdThatIsAnObjectIdHexToObjectIdIfTargetIsString() {

Expand Down
Loading