Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package dev.morphia.mapping.codec;

import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import com.mongodb.lang.Nullable;
Expand All @@ -17,44 +15,31 @@
* Determines if a property should be serialized or not
*/
public class MorphiaPropertySerialization implements PropertySerialization<Object> {
private final List<Annotation> annotations;
private final MorphiaConfig config;

private final PropertyModel property;
// all fixed once the property is mapped, so worked out here rather than on every value
private final boolean storeNulls;
private final boolean storeEmpties;
private final boolean neverSerialized;

/**
* @param config the configuration to use
* @param property the property in question
*/
public MorphiaPropertySerialization(MorphiaConfig config, PropertyModel property) {
this.config = config;
annotations = property.getAnnotations();
this.property = property;
storeNulls = config.storeNulls();
storeEmpties = config.storeEmpties();
neverSerialized = property.hasAnnotation(LoadOnly.class) || config.ignoreFinals() && property.isFinal();
}

@Override
public boolean shouldSerialize(@Nullable Object value) {
if (!config.storeNulls() && value == null) {
return false;
}
if (config.ignoreFinals() && property.isFinal()) {
if (neverSerialized) {
return false;
}
if (!config.storeEmpties()) {
if (value instanceof Map && ((Map<?, ?>) value).isEmpty()
|| value instanceof Collection && ((Collection<?>) value).isEmpty()) {
return false;
}
}
return isNotLoadOnly();
}

private boolean isNotLoadOnly() {
for (int i = 0; i < annotations.size(); i++) {
if (annotations.get(i).annotationType().equals(LoadOnly.class)) {
return false;
}
if (value == null) {
return storeNulls;
}
return true;
return storeEmpties
|| !(value instanceof Map && ((Map<?, ?>) value).isEmpty()
|| value instanceof Collection && ((Collection<?>) value).isEmpty());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import com.mongodb.lang.Nullable;

import dev.morphia.annotations.LoadOnly;
import dev.morphia.annotations.internal.MorphiaInternal;
import dev.morphia.mapping.codec.EnumCodec;
import dev.morphia.mapping.codec.MorphiaDateCodec;
Expand Down Expand Up @@ -84,8 +85,9 @@ public final class CodecPlan {
directBsonTypes[slot] = codecs[slot] != null ? DIRECT_BSON_TYPES.getOrDefault(codecs[slot].getClass(), 0) : 0;
}
PropertyModel idProperty = model.getIdProperty();
// the id is written first, separately, and @LoadOnly properties are never written by any encoder
encodeOrder = Arrays.stream(properties)
.filter(property -> property != idProperty)
.filter(property -> property != idProperty && !property.hasAnnotation(LoadOnly.class))
.toArray(PropertyModel[]::new);
}

Expand Down Expand Up @@ -128,7 +130,8 @@ private static int mask(BsonType... types) {
}

/**
* @return the properties to encode after the id, in order. Callers must not modify the array.
* @return the properties to encode after the id, in order, without {@code @LoadOnly} properties. Callers must not
* modify the array.
*/
PropertyModel[] encodeOrder() {
return encodeOrder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static dev.morphia.mapping.codec.CodecHelper.document;

/**
* @param <T> the entity type
* @hidden
Expand All @@ -42,19 +40,16 @@ public void encode(BsonWriter writer, T value, EncoderContext encoderContext) {
if (areEquivalentTypes(value.getClass(), model.getType())) {
LOG.debug("Encoding document using codec for {}", morphiaCodec.getEntityModel().getType().getName());

document(writer, () -> {

PropertyModel idModel = model.getIdProperty();
encodeIdProperty(writer, value, encoderContext, idModel);

if (model.useDiscriminator()) {
encodeDiscriminator(writer, model);
}

for (PropertyModel propertyModel : morphiaCodec.getPlan().encodeOrder()) {
encodeProperty(writer, propertyModel, value, encoderContext);
}
});
// written out rather than through CodecHelper.document, whose lambda would capture these locals per encode
writer.writeStartDocument();
encodeIdProperty(writer, value, encoderContext, model.getIdProperty());
if (model.useDiscriminator()) {
encodeDiscriminator(writer, model);
}
for (PropertyModel propertyModel : morphiaCodec.getPlan().encodeOrder()) {
encodeProperty(writer, propertyModel, value, encoderContext);
}
writer.writeEndDocument();
} else {
morphiaCodec.codecForSubtype((Class) value.getClass())
.encode(writer, value, encoderContext);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package dev.morphia.mapping.codec.pojo;

import java.util.Arrays;
import java.util.List;

import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;

import dev.morphia.MorphiaDatastore;
import dev.morphia.annotations.LoadOnly;
import dev.morphia.config.ManualMorphiaConfig;
import dev.morphia.mapping.DiscriminatorFunction;
import dev.morphia.mapping.MapperType;
import dev.morphia.test.mapping.codec.harness.models.HarnessRenames;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

/**
* The plan's encode order holds every property an encoder may write, in model order: everything but the id, which is
* written first on its own, and {@code @LoadOnly} properties, which no encoder writes.
*/
public class TestCodecPlanEncodeOrder {
@ParameterizedTest
@EnumSource(MapperType.class)
void leavesOutTheIdAndLoadOnlyProperties(MapperType mapper) {
// nothing here talks to a server
try (MongoClient client = MongoClients.create("mongodb://localhost:1")) {
MorphiaDatastore datastore = new MorphiaDatastore(client, ManualMorphiaConfig.configure()
.database("codec_plan_encode_order")
.mapper(mapper)
.packages(List.of(HarnessRenames.class.getPackageName()))
.discriminator(DiscriminatorFunction.className())
.applyIndexes(false)
.applyCaps(false)
.applyDocumentValidations(false));
CodecPlan plan = ((MorphiaCodec<?>) datastore.getCodecRegistry().get(HarnessRenames.class)).getPlan();
EntityModel model = plan.model();

List<PropertyModel> expected = model.getProperties().stream()
.filter(property -> property != model.getIdProperty() && !property.hasAnnotation(LoadOnly.class))
.toList();
Assertions.assertEquals(expected, Arrays.asList(plan.encodeOrder()));
Assertions.assertTrue(Arrays.stream(plan.encodeOrder()).noneMatch(property -> property.getName().equals("legacy")),
mapper + ": the @LoadOnly property is left out");
Assertions.assertEquals(model.getProperties().size() - 2, plan.encodeOrder().length, mapper.toString());
}
}
}
2 changes: 1 addition & 1 deletion design/mapping-game-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ JMH numbers on both mappers.
- Cache `ArrayCodec`'s component codec.
- Add a per-codec subtype-codec cache for encode.

### 3.6 Precompute serialization flags and the encode order (#6, B11/A7)
### ~~3.6 Precompute serialization flags and the encode order (#6, B11/A7)~~
- Give `MorphiaPropertySerialization` final booleans.
- Put `encodeOrder` in the plan, without the id property and without properties that are always skipped.
- Use `==` instead of `equals(idModel)`. The deep `equals` is already gone after 2.7.
Expand Down
Loading