/**
   * Reads the given {@link DBObject} into a {@link Map}. will recursively resolve nested {@link
   * Map}s as well.
   *
   * @param type the {@link Map} {@link TypeInformation} to be used to unmarshall this {@link
   *     DBObject}.
   * @param dbObject must not be {@literal null}
   * @param path must not be {@literal null}
   * @return
   */
  @SuppressWarnings("unchecked")
  protected Map<Object, Object> readMap(
      TypeInformation<?> type, DBObject dbObject, ObjectPath path) {

    Assert.notNull(dbObject, "DBObject must not be null!");
    Assert.notNull(path, "Object path must not be null!");

    Class<?> mapType = typeMapper.readType(dbObject, type).getType();

    TypeInformation<?> keyType = type.getComponentType();
    Class<?> rawKeyType = keyType == null ? null : keyType.getType();

    TypeInformation<?> valueType = type.getMapValueType();
    Class<?> rawValueType = valueType == null ? null : valueType.getType();

    Map<Object, Object> map =
        CollectionFactory.createMap(mapType, rawKeyType, dbObject.keySet().size());
    Map<String, Object> sourceMap = dbObject.toMap();

    for (Entry<String, Object> entry : sourceMap.entrySet()) {
      if (typeMapper.isTypeKey(entry.getKey())) {
        continue;
      }

      Object key = potentiallyUnescapeMapKey(entry.getKey());

      if (rawKeyType != null) {
        key = conversionService.convert(key, rawKeyType);
      }

      Object value = entry.getValue();

      if (value instanceof DBObject) {
        map.put(key, read(valueType, (DBObject) value, path));
      } else if (value instanceof DBRef) {
        map.put(
            key,
            DBRef.class.equals(rawValueType) ? value : read(valueType, readRef((DBRef) value)));
      } else {
        Class<?> valueClass = valueType == null ? null : valueType.getType();
        map.put(key, getPotentiallyConvertedSimpleRead(value, valueClass));
      }
    }

    return map;
  }
  /**
   * Adds custom type information to the given {@link DBObject} if necessary. That is if the value
   * is not the same as the one given. This is usually the case if you store a subtype of the actual
   * declared type of the property.
   *
   * @param type
   * @param value must not be {@literal null}.
   * @param dbObject must not be {@literal null}.
   */
  protected void addCustomTypeKeyIfNecessary(
      TypeInformation<?> type, Object value, DBObject dbObject) {

    TypeInformation<?> actualType = type != null ? type.getActualType() : null;
    Class<?> reference = actualType == null ? Object.class : actualType.getType();
    Class<?> valueType = ClassUtils.getUserClass(value.getClass());

    boolean notTheSameClass = !valueType.equals(reference);
    if (notTheSameClass) {
      typeMapper.writeType(valueType, dbObject);
    }
  }
  /**
   * Root entry method into write conversion. Adds a type discriminator to the {@link DBObject}.
   * Shouldn't be called for nested conversions.
   *
   * @see org.springframework.data.mongodb.core.core.convert.MongoWriter#write(java.lang.Object,
   *     com.mongodb.DBObject)
   */
  public void write(final Object obj, final DBObject dbo) {

    if (null == obj) {
      return;
    }

    Class<?> entityType = obj.getClass();
    boolean handledByCustomConverter =
        conversions.getCustomWriteTarget(entityType, DBObject.class) != null;
    TypeInformation<? extends Object> type = ClassTypeInformation.from(entityType);

    if (!handledByCustomConverter && !(dbo instanceof BasicDBList)) {
      typeMapper.writeType(type, dbo);
    }

    Object target = obj instanceof LazyLoadingProxy ? ((LazyLoadingProxy) obj).getTarget() : obj;

    writeInternal(target, dbo, type);
  }
  @SuppressWarnings("unchecked")
  private <S extends Object> S read(TypeInformation<S> type, DBObject dbo, ObjectPath path) {

    if (null == dbo) {
      return null;
    }

    TypeInformation<? extends S> typeToUse = typeMapper.readType(dbo, type);
    Class<? extends S> rawType = typeToUse.getType();

    if (conversions.hasCustomReadTarget(dbo.getClass(), rawType)) {
      return conversionService.convert(dbo, rawType);
    }

    if (DBObject.class.isAssignableFrom(rawType)) {
      return (S) dbo;
    }

    if (typeToUse.isCollectionLike() && dbo instanceof BasicDBList) {
      return (S) readCollectionOrArray(typeToUse, (BasicDBList) dbo, path);
    }

    if (typeToUse.isMap()) {
      return (S) readMap(typeToUse, dbo, path);
    }

    if (dbo instanceof BasicDBList) {
      throw new MappingException(
          String.format(INCOMPATIBLE_TYPES, dbo, BasicDBList.class, typeToUse.getType(), path));
    }

    // Retrieve persistent entity info
    MongoPersistentEntity<S> persistentEntity =
        (MongoPersistentEntity<S>) mappingContext.getPersistentEntity(typeToUse);
    if (persistentEntity == null) {
      throw new MappingException("No mapping metadata found for " + rawType.getName());
    }

    return read(persistentEntity, dbo, path);
  }
  /**
   * Removes the type information from the entire conversion result.
   *
   * @param object
   * @param recursively whether to apply the removal recursively
   * @return
   */
  private Object removeTypeInfo(Object object, boolean recursively) {

    if (!(object instanceof DBObject)) {
      return object;
    }

    DBObject dbObject = (DBObject) object;
    String keyToRemove = null;

    for (String key : dbObject.keySet()) {

      if (recursively) {

        Object value = dbObject.get(key);

        if (value instanceof BasicDBList) {
          for (Object element : (BasicDBList) value) {
            removeTypeInfo(element, recursively);
          }
        } else {
          removeTypeInfo(value, recursively);
        }
      }

      if (typeMapper.isTypeKey(key)) {

        keyToRemove = key;

        if (!recursively) {
          break;
        }
      }
    }

    if (keyToRemove != null) {
      dbObject.removeField(keyToRemove);
    }

    return dbObject;
  }