/* * Copyright 2017 MongoDB, Inc. * * 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.bson.codecs.pojo; import org.bson.BsonInvalidOperationException; import org.bson.BsonReader; import org.bson.BsonReaderMark; import org.bson.BsonType; import org.bson.BsonWriter; import org.bson.codecs.Codec; import org.bson.codecs.DecoderContext; import org.bson.codecs.EncoderContext; import org.bson.codecs.configuration.CodecConfigurationException; import org.bson.codecs.configuration.CodecRegistry; import org.bson.diagnostics.Logger; import org.bson.diagnostics.Loggers; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import static java.lang.String.format; import static org.bson.codecs.configuration.CodecRegistries.fromCodecs; import static org.bson.codecs.configuration.CodecRegistries.fromRegistries; final class PojoCodec<T> implements Codec<T> { private static final Logger LOGGER = Loggers.getLogger("PojoCodec"); private final ClassModel<T> classModel; private final PojoCodecProvider codecProvider; private final CodecRegistry registry; private final DiscriminatorLookup discriminatorLookup; private final ConcurrentMap<ClassModel<?>, Codec<?>> codecCache; private final boolean specialized; PojoCodec(final ClassModel<T> classModel, final PojoCodecProvider codecProvider, final CodecRegistry registry, final DiscriminatorLookup discriminatorLookup) { this(classModel, codecProvider, registry, discriminatorLookup, new ConcurrentHashMap<ClassModel<?>, Codec<?>>(), !classModel.hasTypeParameters()); } PojoCodec(final ClassModel<T> classModel, final PojoCodecProvider codecProvider, final CodecRegistry registry, final DiscriminatorLookup discriminatorLookup, final ConcurrentMap<ClassModel<?>, Codec<?>> codecCache, final boolean specialized) { this.classModel = classModel; this.codecProvider = codecProvider; this.registry = fromRegistries(fromCodecs(this), registry); this.discriminatorLookup = discriminatorLookup; this.codecCache = codecCache; this.specialized = specialized; if (specialized) { codecCache.put(classModel, this); for (FieldModel<?> fieldModel : classModel.getFieldModels()) { addToCache(fieldModel); } } } @Override public void encode(final BsonWriter writer, final T value, final EncoderContext encoderContext) { if (!specialized) { throw new CodecConfigurationException("Cannot encode an unspecialized generic ClassModel"); } writer.writeStartDocument(); FieldModel<?> idFieldModel = classModel.getIdFieldModel(); if (idFieldModel != null) { encodeField(writer, value, encoderContext, idFieldModel); } if (classModel.useDiscriminator()) { writer.writeString(classModel.getDiscriminatorKey(), classModel.getDiscriminator()); } for (FieldModel<?> fieldModel : classModel.getFieldModels()) { if (fieldModel.equals(classModel.getIdFieldModel())) { continue; } encodeField(writer, value, encoderContext, fieldModel); } writer.writeEndDocument(); } @Override public T decode(final BsonReader reader, final DecoderContext decoderContext) { if (decoderContext.hasCheckedDiscriminator()) { if (!specialized) { throw new CodecConfigurationException("Cannot decode using an unspecialized generic ClassModel"); } InstanceCreator<T> instanceCreator = classModel.getInstanceCreator(); decodeFields(reader, decoderContext, instanceCreator); return instanceCreator.getInstance(); } else { return getCodecFromDocument(reader, classModel.useDiscriminator(), classModel.getDiscriminatorKey(), registry, discriminatorLookup, this).decode(reader, DecoderContext.builder().checkedDiscriminator(true).build()); } } @Override public Class<T> getEncoderClass() { return classModel.getType(); } @Override public String toString() { return format("PojoCodec<%s>", classModel); } ClassModel<T> getClassModel() { return classModel; } @SuppressWarnings("unchecked") private <S> void encodeField(final BsonWriter writer, final T instance, final EncoderContext encoderContext, final FieldModel<S> fieldModel) { S fieldValue = fieldModel.getFieldAccessor().get(instance); if (fieldModel.shouldSerialize(fieldValue)) { writer.writeName(fieldModel.getDocumentFieldName()); if (fieldValue == null) { writer.writeNull(); } else { getInstanceCodec(fieldModel, fieldValue.getClass()).encode(writer, fieldValue, encoderContext); } } } @SuppressWarnings("unchecked") private void decodeFields(final BsonReader reader, final DecoderContext decoderContext, final InstanceCreator<T> instanceCreator) { reader.readStartDocument(); while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) { String name = reader.readName(); if (classModel.useDiscriminator() && classModel.getDiscriminatorKey().equals(name)) { reader.readString(); } else { decodeFieldModel(reader, decoderContext, instanceCreator, name, classModel.getFieldModel(name)); } } reader.readEndDocument(); } @SuppressWarnings("unchecked") private <S> void decodeFieldModel(final BsonReader reader, final DecoderContext decoderContext, final InstanceCreator<T> instanceCreator, final String name, final FieldModel<S> fieldModel) { if (fieldModel != null) { try { S value = null; if (reader.getCurrentBsonType() == BsonType.NULL) { reader.readNull(); } else { value = decoderContext.decodeWithChildContext(fieldModel.getCachedCodec(), reader); } instanceCreator.set(value, fieldModel); } catch (BsonInvalidOperationException e) { throw new CodecConfigurationException(format("Failed to decode '%s'. %s", name, e.getMessage()), e); } catch (CodecConfigurationException e) { throw new CodecConfigurationException(format("Failed to decode '%s'. %s", name, e.getMessage()), e); } } else { if (LOGGER.isTraceEnabled()) { LOGGER.trace(format("Found field not present in the ClassModel: %s", name)); } reader.skipValue(); } } private <S> void addToCache(final FieldModel<S> fieldModel) { Codec<S> codec = fieldModel.getCodec() != null ? fieldModel.getCodec() : specializePojoCodec(fieldModel, getCodecFromTypeData(fieldModel.getTypeData())); fieldModel.cachedCodec(codec); } @SuppressWarnings({"rawtypes", "unchecked"}) private <S> Codec<S> getCodecFromTypeData(final TypeData<S> typeData) { Codec<S> codec = null; Class<S> head = typeData.getType(); if (Collection.class.isAssignableFrom(head)) { codec = new CollectionCodec(head, getCodecFromTypeData(typeData.getTypeParameters().get(0))); } else if (Map.class.isAssignableFrom(head)) { codec = new MapCodec(head, getCodecFromTypeData(typeData.getTypeParameters().get(1))); } else { codec = getCodecFromClass(head); } return codec; } @SuppressWarnings("unchecked") private <S, V> Codec<S> getInstanceCodec(final FieldModel<S> fieldModel, final Class<V> instanceType) { Codec<S> codec = fieldModel.getCachedCodec(); if (!areEquivalentTypes(codec.getEncoderClass(), instanceType)) { codec = (Codec<S>) registry.get(instanceType); } return codec; } private <S, V> boolean areEquivalentTypes(final Class<S> t1, final Class<V> t2) { if (t1.equals(t2)) { return true; } else if (Collection.class.isAssignableFrom(t1) && Collection.class.isAssignableFrom(t2)) { return true; } else if (Map.class.isAssignableFrom(t1) && Map.class.isAssignableFrom(t2)) { return true; } return false; } @SuppressWarnings("unchecked") private <S> Codec<S> getCodecFromClass(final Class<S> clazz) { Codec<S> codec = null; if (classModel.getType().equals(clazz)) { codec = (Codec<S>) this; } else { codec = codecProvider.getPojoCodec(clazz, registry); } if (codec == null) { codec = registry.get(clazz); } return codec; } @SuppressWarnings("unchecked") private <S> Codec<S> specializePojoCodec(final FieldModel<S> fieldModel, final Codec<S> defaultCodec) { Codec<S> codec = defaultCodec; if (codec != null && codec instanceof PojoCodec) { PojoCodec<S> pojoCodec = (PojoCodec<S>) codec; ClassModel<S> specialized = getSpecializedClassModel(pojoCodec.getClassModel(), fieldModel); if (codecCache.containsKey(specialized)) { codec = (Codec<S>) codecCache.get(specialized); } else { codec = new LazyPojoCodec<S>(specialized, codecProvider, registry, discriminatorLookup, codecCache); } } return codec; } @SuppressWarnings({"rawtypes", "unchecked"}) private <S, V> ClassModel<S> getSpecializedClassModel(final ClassModel<S> clazzModel, final FieldModel<V> fieldModel) { boolean useDiscriminator = fieldModel.useDiscriminator() == null ? clazzModel.useDiscriminator() : fieldModel.useDiscriminator(); boolean validDiscriminator = clazzModel.getDiscriminatorKey() != null && clazzModel.getDiscriminator() != null; boolean changeTheDiscriminator = (useDiscriminator != clazzModel.useDiscriminator()) && validDiscriminator; if (fieldModel.getTypeData().getTypeParameters().isEmpty() && !changeTheDiscriminator){ return clazzModel; } ArrayList<FieldModel<?>> concreteFieldModels = new ArrayList<FieldModel<?>>(clazzModel.getFieldModels()); FieldModel<?> concreteIdField = clazzModel.getIdFieldModel(); List<TypeData<?>> fieldTypeParameters = fieldModel.getTypeData().getTypeParameters(); for (int i = 0; i < concreteFieldModels.size(); i++) { FieldModel<?> model = concreteFieldModels.get(i); String fieldName = model.getFieldName(); TypeParameterMap typeParameterMap = clazzModel.getFieldNameToTypeParameterMap().get(fieldName); if (typeParameterMap.hasTypeParameters()) { FieldModel<?> concreteFieldModel = getSpecializedFieldModel(model, typeParameterMap, fieldTypeParameters); concreteFieldModels.set(i, concreteFieldModel); if (concreteIdField != null && concreteIdField.getFieldName().equals(fieldName)) { concreteIdField = concreteFieldModel; } } } boolean discriminatorEnabled = changeTheDiscriminator ? fieldModel.useDiscriminator() : clazzModel.useDiscriminator(); return new ClassModel<S>(clazzModel.getType(), clazzModel.getFieldNameToTypeParameterMap(), clazzModel.getInstanceCreatorFactory(), discriminatorEnabled, clazzModel.getDiscriminatorKey(), clazzModel.getDiscriminator(), concreteIdField, concreteFieldModels); } @SuppressWarnings("unchecked") private <V> FieldModel<V> getSpecializedFieldModel(final FieldModel<V> fieldModel, final TypeParameterMap typeParameterMap, final List<TypeData<?>> fieldTypeParameters) { TypeData<V> specializedFieldType = fieldModel.getTypeData(); Map<Integer, Integer> fieldToClassParamIndexMap = typeParameterMap.getFieldToClassParamIndexMap(); Integer classTypeParamRepresentsWholeField = fieldToClassParamIndexMap.get(-1); if (classTypeParamRepresentsWholeField != null) { specializedFieldType = (TypeData<V>) fieldTypeParameters.get(classTypeParamRepresentsWholeField); } else { TypeData.Builder<V> builder = TypeData.builder(fieldModel.getTypeData().getType()); List<TypeData<?>> typeParameters = new ArrayList<TypeData<?>>(fieldModel.getTypeData().getTypeParameters()); for (int i = 0; i < typeParameters.size(); i++) { for (Map.Entry<Integer, Integer> mapping : fieldToClassParamIndexMap.entrySet()) { if (mapping.getKey().equals(i)) { typeParameters.set(i, fieldTypeParameters.get(mapping.getValue())); } } } builder.addTypeParameters(typeParameters); specializedFieldType = builder.build(); } if (fieldModel.getTypeData().equals(specializedFieldType)) { return fieldModel; } return new FieldModel<V>(fieldModel.getFieldName(), fieldModel.getDocumentFieldName(), specializedFieldType, null, fieldModel.getFieldSerialization(), fieldModel.useDiscriminator(), fieldModel.getFieldAccessor()); } @SuppressWarnings("unchecked") private Codec<T> getCodecFromDocument(final BsonReader reader, final boolean useDiscriminator, final String discriminatorKey, final CodecRegistry registry, final DiscriminatorLookup discriminatorLookup, final Codec<T> defaultCodec) { Codec<T> codec = defaultCodec; if (useDiscriminator) { BsonReaderMark mark = reader.getMark(); reader.readStartDocument(); boolean discriminatorKeyFound = false; while (!discriminatorKeyFound && reader.readBsonType() != BsonType.END_OF_DOCUMENT) { String name = reader.readName(); if (discriminatorKey.equals(name)) { discriminatorKeyFound = true; codec = (Codec<T>) registry.get(discriminatorLookup.lookup(reader.readString())); } else { reader.skipValue(); } } mark.reset(); } return codec; } }