BasicMongoPersistentEntity.java
/*
* Copyright 2011-present 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.mapping;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.data.annotation.Id;
import org.springframework.data.core.TypeInformation;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.AssociationHandler;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.mongodb.MongoCollectionUtils;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.util.encryption.EncryptionUtils;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.data.util.Lazy;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* MongoDB specific {@link MongoPersistentEntity} implementation that adds Mongo specific meta-data such as the
* collection name and the like.
*
* @author Jon Brisbin
* @author Oliver Gierke
* @author Thomas Darimont
* @author Christoph Strobl
* @author Mark Paluch
*/
public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, MongoPersistentProperty>
implements MongoPersistentEntity<T> {
private static final String AMBIGUOUS_FIELD_MAPPING = "Ambiguous field mapping detected; Both %s and %s map to the same field name %s; Disambiguate using @Field annotation";
private static final ValueExpressionParser PARSER = ValueExpressionParser.create(SpelExpressionParser::new);
private final String collection;
private final String language;
private final @Nullable ValueExpression expression;
private final @Nullable String collation;
private final @Nullable ValueExpression collationExpression;
private final ShardKey shardKey;
/**
* Creates a new {@link BasicMongoPersistentEntity} with the given {@link TypeInformation}. Will default the
* collection name to the entities simple type name.
*
* @param typeInformation must not be {@literal null}.
*/
public BasicMongoPersistentEntity(TypeInformation<T> typeInformation) {
super(typeInformation, MongoPersistentPropertyComparator.INSTANCE);
Class<?> rawType = typeInformation.getType();
String fallback = MongoCollectionUtils.getPreferredCollectionName(rawType);
if (this.isAnnotationPresent(Document.class)) {
Document document = this.getRequiredAnnotation(Document.class);
this.collection = StringUtils.hasText(document.collection()) ? document.collection() : fallback;
this.language = StringUtils.hasText(document.language()) ? document.language() : "";
this.expression = detectExpression(document.collection());
this.collation = document.collation();
this.collationExpression = detectExpression(document.collation());
} else {
this.collection = fallback;
this.language = "";
this.expression = null;
this.collation = null;
this.collationExpression = null;
}
this.shardKey = detectShardKey();
}
private ShardKey detectShardKey() {
if (!isAnnotationPresent(Sharded.class)) {
return ShardKey.none();
}
Sharded sharded = getRequiredAnnotation(Sharded.class);
String[] keyProperties = sharded.shardKey();
if (ObjectUtils.isEmpty(keyProperties)) {
keyProperties = new String[] { FieldName.ID.name() };
}
ShardKey shardKey = ShardingStrategy.HASH.equals(sharded.shardingStrategy()) ? ShardKey.hash(keyProperties)
: ShardKey.range(keyProperties);
return sharded.immutableKey() ? ShardKey.immutable(shardKey) : shardKey;
}
@Override
public String getCollection() {
return expression == null //
? collection //
: ObjectUtils.nullSafeToString(expression.evaluate(getValueEvaluationContext(null)));
}
@Override
public String getLanguage() {
return this.language;
}
@Override
public @Nullable MongoPersistentProperty getTextScoreProperty() {
return getPersistentProperty(TextScore.class);
}
@Override
public boolean hasTextScoreProperty() {
return getTextScoreProperty() != null;
}
@Override
public @Nullable Collation getCollation() {
Object collationValue = collationExpression != null
? collationExpression.evaluate(getValueEvaluationContext(null))
: this.collation;
if (collationValue == null) {
return null;
}
if (collationValue instanceof org.bson.Document document) {
return org.springframework.data.mongodb.core.query.Collation.from(document);
}
if (collationValue instanceof org.springframework.data.mongodb.core.query.Collation collation) {
return collation;
}
return StringUtils.hasText(collationValue.toString())
? org.springframework.data.mongodb.core.query.Collation.parse(collationValue.toString())
: null;
}
@Override
public ShardKey getShardKey() {
return shardKey;
}
@Override
public void verify() {
super.verify();
verifyFieldUniqueness();
verifyFieldTypes();
}
@Override
public boolean isNew(Object bean) {
return super.isNew(Wrapped.getTargetObject(bean));
}
@Override
public <B> PersistentPropertyAccessor<B> getPropertyAccessor(B bean) {
return super.getPropertyAccessor(Wrapped.getTargetObject(bean));
}
@Override
public EvaluationContext getEvaluationContext(@Nullable Object rootObject) {
return super.getEvaluationContext(rootObject);
}
@Override
public EvaluationContext getEvaluationContext(@Nullable Object rootObject, ExpressionDependencies dependencies) {
return super.getEvaluationContext(rootObject, dependencies);
}
@Override
public ValueEvaluationContext getValueEvaluationContext(@Nullable Object rootObject) {
return super.getValueEvaluationContext(rootObject);
}
@Override
public ValueEvaluationContext getValueEvaluationContext(@Nullable Object rootObject, ExpressionDependencies dependencies) {
return super.getValueEvaluationContext(rootObject, dependencies);
}
private void verifyFieldUniqueness() {
AssertFieldNameUniquenessHandler handler = new AssertFieldNameUniquenessHandler();
doWithProperties(handler);
doWithAssociations(handler);
}
private void verifyFieldTypes() {
doWithProperties(new PropertyTypeAssertionHandler());
}
/**
* {@link Comparator} implementation inspecting the {@link MongoPersistentProperty}'s order.
*
* @author Oliver Gierke
*/
enum MongoPersistentPropertyComparator implements Comparator<MongoPersistentProperty> {
INSTANCE;
public int compare(@Nullable MongoPersistentProperty o1, @Nullable MongoPersistentProperty o2) {
if (o1 != null && o1.getFieldOrder() == Integer.MAX_VALUE) {
return 1;
}
if (o2 != null && o2.getFieldOrder() == Integer.MAX_VALUE) {
return -1;
}
if (o1 == null && o2 == null) {
return -1;
}
if(o1 != null && o2 != null) {
return o1.getFieldOrder() - o2.getFieldOrder();
}
return o1 != null ? o1.getFieldOrder() : -1;
}
}
/**
* As a general note: An implicit id property has a name that matches "id" or "_id". An explicit id property is one
* that is annotated with @see {@link Id}. The property id is updated according to the following rules: 1) An id
* property which is defined explicitly takes precedence over an implicitly defined id property. 2) In case of any
* ambiguity a @see {@link MappingException} is thrown.
*
* @param property - the new id property candidate
* @return can be {@literal null}.
*/
@Override
protected @Nullable MongoPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNull(MongoPersistentProperty property) {
Assert.notNull(property, "MongoPersistentProperty must not be null");
if (!property.isIdProperty()) {
return null;
}
MongoPersistentProperty currentIdProperty = getIdProperty();
boolean currentIdPropertyIsSet = currentIdProperty != null;
@SuppressWarnings("NullAway")
boolean currentIdPropertyIsExplicit = currentIdPropertyIsSet && currentIdProperty.isExplicitIdProperty();
boolean newIdPropertyIsExplicit = property.isExplicitIdProperty();
if (!currentIdPropertyIsSet) {
return property;
}
@SuppressWarnings("NullAway")
Field currentIdPropertyField = currentIdProperty.getField();
if (newIdPropertyIsExplicit && currentIdPropertyIsExplicit) {
throw new MappingException(
String.format("Attempt to add explicit id property %s but already have an property %s registered "
+ "as explicit id; Check your mapping configuration", property.getField(), currentIdPropertyField));
} else if (newIdPropertyIsExplicit && !currentIdPropertyIsExplicit) {
// explicit id property takes precedence over implicit id property
return property;
} else if (!newIdPropertyIsExplicit && currentIdPropertyIsExplicit) {
// no id property override - current property is explicitly defined
} else {
throw new MappingException(
String.format("Attempt to add id property %s but already have an property %s registered "
+ "as id; Check your mapping configuration", property.getField(), currentIdPropertyField));
}
return null;
}
/**
* Returns a Value {@link Expression} if the given {@link String} is actually an expression that does not evaluate to
* a literal expression (indicating that no subsequent evaluation is necessary).
*
* @param potentialExpression can be {@literal null}
* @return can be {@literal null}.
*/
private static @Nullable ValueExpression detectExpression(@Nullable String potentialExpression) {
if (!StringUtils.hasText(potentialExpression)) {
return null;
}
ValueExpression expression = PARSER.parse(potentialExpression);
return expression.isLiteral() ? null : expression;
}
/**
* Handler to collect {@link MongoPersistentProperty} instances and check that each of them is mapped to a distinct
* field name.
*
* @author Oliver Gierke
*/
private static class AssertFieldNameUniquenessHandler
implements PropertyHandler<MongoPersistentProperty>, AssociationHandler<MongoPersistentProperty> {
private final Map<String, MongoPersistentProperty> properties = new HashMap<String, MongoPersistentProperty>();
public void doWithPersistentProperty(MongoPersistentProperty persistentProperty) {
assertUniqueness(persistentProperty);
}
public void doWithAssociation(Association<MongoPersistentProperty> association) {
assertUniqueness(association.getInverse());
}
private void assertUniqueness(MongoPersistentProperty property) {
String fieldName = property.getFieldName();
MongoPersistentProperty existingProperty = properties.get(fieldName);
if (existingProperty != null) {
throw new MappingException(String.format(AMBIGUOUS_FIELD_MAPPING, property, existingProperty, fieldName));
}
properties.put(fieldName, property);
}
}
@Override
public @Nullable Collection<Object> getEncryptionKeyIds() {
Encrypted encrypted = findAnnotation(Encrypted.class);
if (encrypted == null) {
return null;
}
if (ObjectUtils.isEmpty(encrypted.keyId())) {
return Collections.emptySet();
}
Lazy<EvaluationContext> evaluationContext = Lazy.of(() -> {
EvaluationContext ctx = getEvaluationContext(null);
ctx.setVariable("target", getType().getSimpleName());
return ctx;
});
List<Object> target = new ArrayList<>();
for (String keyId : encrypted.keyId()) {
target.add(EncryptionUtils.resolveKeyId(keyId, evaluationContext));
}
return target;
}
/**
* @author Christoph Strobl
* @since 1.6
*/
private static class PropertyTypeAssertionHandler implements PropertyHandler<MongoPersistentProperty> {
@Override
public void doWithPersistentProperty(MongoPersistentProperty persistentProperty) {
potentiallyAssertTextScoreType(persistentProperty);
potentiallyAssertLanguageType(persistentProperty);
potentiallyAssertDBRefTargetType(persistentProperty);
}
private static void potentiallyAssertLanguageType(MongoPersistentProperty persistentProperty) {
if (persistentProperty.isExplicitLanguageProperty()) {
assertPropertyType(persistentProperty, String.class);
}
}
private static void potentiallyAssertTextScoreType(MongoPersistentProperty persistentProperty) {
if (persistentProperty.isTextScoreProperty()) {
assertPropertyType(persistentProperty, Float.class, Double.class);
}
}
@SuppressWarnings("NullAway")
private static void potentiallyAssertDBRefTargetType(MongoPersistentProperty persistentProperty) {
if (persistentProperty.isDbReference() && persistentProperty.getDBRef().lazy()) {
if (persistentProperty.isArray() || Modifier.isFinal(persistentProperty.getActualType().getModifiers())) {
throw new MappingException(
String.format("Invalid lazy DBRef property for %s; Found %s which must not be an array nor a final class",
persistentProperty.getField(), persistentProperty.getActualType()));
}
}
}
private static void assertPropertyType(MongoPersistentProperty persistentProperty, Class<?>... validMatches) {
for (Class<?> potentialMatch : validMatches) {
if (ClassUtils.isAssignable(potentialMatch, persistentProperty.getActualType())) {
return;
}
}
throw new MappingException(
String.format("Mismatching types for %s; Found %s expected one of %s", persistentProperty.getField(),
persistentProperty.getActualType(), StringUtils.arrayToCommaDelimitedString(validMatches)));
}
}
}