All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.firebase.database.utilities.encoding.CustomClassMapper Maven / Gradle / Ivy

Go to download

This is the official Firebase Admin Java SDK. Build extraordinary native JVM apps in minutes with Firebase. The Firebase platform can power your app’s backend, user authentication, static hosting, and more.

The newest version!
/*
 * Copyright 2017 Google 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 com.google.firebase.database.utilities.encoding;

import static com.google.firebase.database.utilities.Utilities.hardAssert;

import com.google.firebase.database.DatabaseException;
import com.google.firebase.database.Exclude;
import com.google.firebase.database.GenericTypeIndicator;
import com.google.firebase.database.IgnoreExtraProperties;
import com.google.firebase.database.PropertyName;
import com.google.firebase.database.ThrowOnExtraProperties;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Helper class to convert to/from custom POJO classes and plain Java types. */
public class CustomClassMapper {

  private static final Logger logger = LoggerFactory.getLogger(CustomClassMapper.class);

  private static final ConcurrentMap, BeanMapper> mappers = new ConcurrentHashMap<>();

  /**
   * Converts a Java representation of JSON data to standard library Java data types: Map, Array,
   * String, Double, Integer and Boolean. POJOs are converted to Java Maps.
   *
   * @param object The representation of the JSON data
   * @return JSON representation containing only standard library Java types
   */
  public static Object convertToPlainJavaTypes(Object object) {
    return serialize(object);
  }

  @SuppressWarnings("unchecked")
  public static Map convertToPlainJavaTypes(Map update) {
    Object converted = serialize(update);
    hardAssert(converted instanceof Map);
    return (Map) converted;
  }

  /**
   * Converts a standard library Java representation of JSON data to an object of the provided
   * class.
   *
   * @param object The representation of the JSON data
   * @param clazz The class of the object to convert to
   * @return The POJO object.
   */
  public static  T convertToCustomClass(Object object, Class clazz) {
    return deserializeToClass(object, clazz);
  }

  /**
   * Converts a standard library Java representation of JSON data to an object of the class provided
   * through the GenericTypeIndicator
   *
   * @param object The representation of the JSON data
   * @param typeIndicator The indicator providing class of the object to convert to
   * @return The POJO object.
   */
  public static  T convertToCustomClass(Object object, GenericTypeIndicator typeIndicator) {
    Class clazz = typeIndicator.getClass();
    Type genericTypeIndicatorType = clazz.getGenericSuperclass();
    if (genericTypeIndicatorType instanceof ParameterizedType) {
      ParameterizedType parameterizedType = (ParameterizedType) genericTypeIndicatorType;
      if (!parameterizedType.getRawType().equals(GenericTypeIndicator.class)) {
        throw new DatabaseException(
            "Not a direct subclass of GenericTypeIndicator: " + genericTypeIndicatorType);
      }
      // We are guaranteed to have exactly one type parameter
      Type type = parameterizedType.getActualTypeArguments()[0];
      return deserializeToType(object, type);
    } else {
      throw new DatabaseException(
          "Not a direct subclass of GenericTypeIndicator: " + genericTypeIndicatorType);
    }
  }

  @SuppressWarnings("unchecked")
  private static  Object serialize(T obj) {
    if (obj == null) {
      return null;
    } else if (obj instanceof Number) {
      if (obj instanceof Float || obj instanceof Double) {
        double doubleValue = ((Number) obj).doubleValue();
        if (doubleValue <= Long.MAX_VALUE
            && doubleValue >= Long.MIN_VALUE
            && Math.floor(doubleValue) == doubleValue) {
          return ((Number) obj).longValue();
        }
        return doubleValue;
      } else if (obj instanceof Long || obj instanceof Integer) {
        return obj;
      } else {
        throw new DatabaseException(
            String.format(
                "Numbers of type %s are not supported, please use an int, long, float or double",
                obj.getClass().getSimpleName()));
      }
    } else if (obj instanceof String) {
      return obj;
    } else if (obj instanceof Boolean) {
      return obj;
    } else if (obj instanceof Character) {
      throw new DatabaseException("Characters are not supported, please use Strings");
    } else if (obj instanceof Map) {
      Map result = new HashMap<>();
      for (Map.Entry entry : ((Map) obj).entrySet()) {
        Object key = entry.getKey();
        if (key instanceof String) {
          String keyString = (String) key;
          result.put(keyString, serialize(entry.getValue()));
        } else {
          throw new DatabaseException("Maps with non-string keys are not supported");
        }
      }
      return result;
    } else if (obj instanceof Collection) {
      if (obj instanceof List) {
        List list = (List) obj;
        List result = new ArrayList<>(list.size());
        for (Object object : list) {
          result.add(serialize(object));
        }
        return result;
      } else {
        throw new DatabaseException(
            "Serializing Collections is not supported, please use Lists instead");
      }
    } else if (obj.getClass().isArray()) {
      throw new DatabaseException(
          "Serializing Arrays is not supported, please use Lists instead");
    } else if (obj instanceof Enum) {
      return ((Enum) obj).name();
    } else {
      Class clazz = (Class) obj.getClass();
      BeanMapper mapper = loadOrCreateBeanMapperForClass(clazz);
      return mapper.serialize(obj);
    }
  }

  @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
  private static  T deserializeToType(Object obj, Type type) {
    if (obj == null) {
      return null;
    } else if (type instanceof ParameterizedType) {
      return deserializeToParameterizedType(obj, (ParameterizedType) type);
    } else if (type instanceof Class) {
      return deserializeToClass(obj, (Class) type);
    } else if (type instanceof WildcardType) {
      throw new DatabaseException("Generic wildcard types are not supported");
    } else if (type instanceof GenericArrayType) {
      throw new DatabaseException(
          "Generic Arrays are not supported, please use Lists instead");
    } else {
      throw new IllegalStateException("Unknown type encountered: " + type);
    }
  }

  @SuppressWarnings("unchecked")
  private static  T deserializeToClass(Object obj, Class clazz) {
    if (obj == null) {
      return null;
    } else if (clazz.isPrimitive()
        || Number.class.isAssignableFrom(clazz)
        || Boolean.class.isAssignableFrom(clazz)
        || Character.class.isAssignableFrom(clazz)) {
      return deserializeToPrimitive(obj, clazz);
    } else if (String.class.isAssignableFrom(clazz)) {
      return (T) convertString(obj);
    } else if (clazz.isArray()) {
      throw new DatabaseException(
          "Converting to Arrays is not supported, please use Lists instead");
    } else if (clazz.getTypeParameters().length > 0) {
      throw new DatabaseException(
          "Class "
              + clazz.getName()
              + " has generic type "
              + "parameters, please use GenericTypeIndicator instead");
    } else if (clazz.equals(Object.class)) {
      return (T) obj;
    } else if (clazz.isEnum()) {
      return deserializeToEnum(obj, clazz);
    } else {
      return convertBean(obj, clazz);
    }
  }

  @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
  private static  T deserializeToParameterizedType(Object obj, ParameterizedType type) {
    // getRawType should always return a Class
    Class rawType = (Class) type.getRawType();
    if (List.class.isAssignableFrom(rawType)) {
      Type genericType = type.getActualTypeArguments()[0];
      if (obj instanceof List) {
        List list = (List) obj;
        List result = new ArrayList<>(list.size());
        for (Object object : list) {
          result.add(deserializeToType(object, genericType));
        }
        return (T) result;
      } else {
        throw new DatabaseException(
            "Expected a List while deserializing, but got a " + obj.getClass());
      }
    } else if (Map.class.isAssignableFrom(rawType)) {
      Type keyType = type.getActualTypeArguments()[0];
      Type valueType = type.getActualTypeArguments()[1];
      if (!keyType.equals(String.class)) {
        throw new DatabaseException(
            "Only Maps with string keys are supported, "
                + "but found Map with key type "
                + keyType);
      }
      Map map = expectMap(obj);
      HashMap result = new HashMap<>();
      for (Map.Entry entry : map.entrySet()) {
        result.put(entry.getKey(), deserializeToType(entry.getValue(), valueType));
      }
      return (T) result;
    } else if (Collection.class.isAssignableFrom(rawType)) {
      throw new DatabaseException("Collections are not supported, please use Lists instead");
    } else {
      Map map = expectMap(obj);
      BeanMapper mapper = (BeanMapper) loadOrCreateBeanMapperForClass(rawType);
      HashMap>, Type> typeMapping = new HashMap<>();
      TypeVariable>[] typeVariables = mapper.clazz.getTypeParameters();
      Type[] types = type.getActualTypeArguments();
      if (types.length != typeVariables.length) {
        throw new IllegalStateException("Mismatched lengths for type variables and actual types");
      }
      for (int i = 0; i < typeVariables.length; i++) {
        typeMapping.put(typeVariables[i], types[i]);
      }
      return mapper.deserialize(map, typeMapping);
    }
  }

  @SuppressWarnings("unchecked")
  private static  T deserializeToPrimitive(Object obj, Class clazz) {
    if (Integer.class.isAssignableFrom(clazz) || int.class.isAssignableFrom(clazz)) {
      return (T) convertInteger(obj);
    } else if (Boolean.class.isAssignableFrom(clazz) || boolean.class.isAssignableFrom(clazz)) {
      return (T) convertBoolean(obj);
    } else if (Double.class.isAssignableFrom(clazz) || double.class.isAssignableFrom(clazz)) {
      return (T) convertDouble(obj);
    } else if (Long.class.isAssignableFrom(clazz) || long.class.isAssignableFrom(clazz)) {
      return (T) convertLong(obj);
    } else if (Float.class.isAssignableFrom(clazz) || float.class.isAssignableFrom(clazz)) {
      return (T) (Float) convertDouble(obj).floatValue();
    } else {
      throw new DatabaseException(
          String.format("Deserializing values to %s is not supported", clazz.getSimpleName()));
    }
  }

  @SuppressWarnings("unchecked")
  private static  T deserializeToEnum(Object object, Class clazz) {
    if (object instanceof String) {
      String value = (String) object;
      // We cast to Class without generics here since we can't prove the bound
      // T extends Enum statically
      try {
        return (T) Enum.valueOf((Class) clazz, value);
      } catch (IllegalArgumentException e) {
        throw new DatabaseException(
            "Could not find enum value of " + clazz.getName() + " for value \"" + value + "\"");
      }
    } else {
      throw new DatabaseException(
          "Expected a String while deserializing to enum "
              + clazz
              + " but got a "
              + object.getClass());
    }
  }

  @SuppressWarnings("unchecked")
  private static  BeanMapper loadOrCreateBeanMapperForClass(Class clazz) {
    BeanMapper mapper = (BeanMapper) mappers.get(clazz);
    if (mapper == null) {
      mapper = new BeanMapper<>(clazz);
      // Inserting without checking is fine because mappers are "pure" and it's okay
      // if we create and use multiple by different threads temporarily
      mappers.put(clazz, mapper);
    }
    return mapper;
  }

  @SuppressWarnings("unchecked")
  private static Map expectMap(Object object) {
    if (object instanceof Map) {
      // TODO: runtime validation of keys?
      return (Map) object;
    } else {
      throw new DatabaseException(
          "Expected a Map while deserializing, but got a " + object.getClass());
    }
  }

  private static Integer convertInteger(Object obj) {
    if (obj instanceof Integer) {
      return (Integer) obj;
    } else if (obj instanceof Long || obj instanceof Double) {
      double value = ((Number) obj).doubleValue();
      if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) {
        return ((Number) obj).intValue();
      } else {
        throw new DatabaseException(
            "Numeric value out of 32-bit integer range: "
                + value
                + ". Did you mean to use a long or double instead of an int?");
      }
    } else {
      throw new DatabaseException(
          "Failed to convert a value of type " + obj.getClass().getName() + " to int");
    }
  }

  private static Long convertLong(Object obj) {
    if (obj instanceof Integer) {
      return ((Integer) obj).longValue();
    } else if (obj instanceof Long) {
      return (Long) obj;
    } else if (obj instanceof Double) {
      Double value = (Double) obj;
      if (value >= Long.MIN_VALUE && value <= Long.MAX_VALUE) {
        return value.longValue();
      } else {
        throw new DatabaseException(
            "Numeric value out of 64-bit long range: "
                + value
                + ". Did you mean to use a double instead of a long?");
      }
    } else {
      throw new DatabaseException(
          "Failed to convert a value of type " + obj.getClass().getName() + " to long");
    }
  }

  private static Double convertDouble(Object obj) {
    if (obj instanceof Integer) {
      return ((Integer) obj).doubleValue();
    } else if (obj instanceof Long) {
      Long value = (Long) obj;
      Double doubleValue = ((Long) obj).doubleValue();
      if (doubleValue.longValue() == value) {
        return doubleValue;
      } else {
        throw new DatabaseException(
            "Loss of precision while converting number to "
                + "double: "
                + obj
                + ". Did you mean to use a 64-bit long instead?");
      }
    } else if (obj instanceof Double) {
      return (Double) obj;
    } else {
      throw new DatabaseException(
          "Failed to convert a value of type " + obj.getClass().getName() + " to double");
    }
  }

  private static Boolean convertBoolean(Object obj) {
    if (obj instanceof Boolean) {
      return (Boolean) obj;
    } else {
      throw new DatabaseException(
          "Failed to convert value of type " + obj.getClass().getName() + " to boolean");
    }
  }

  private static String convertString(Object obj) {
    if (obj instanceof String) {
      return (String) obj;
    } else {
      throw new DatabaseException(
          "Failed to convert value of type " + obj.getClass().getName() + " to String");
    }
  }

  private static  T convertBean(Object obj, Class clazz) {
    BeanMapper mapper = loadOrCreateBeanMapperForClass(clazz);
    if (obj instanceof Map) {
      return mapper.deserialize(expectMap(obj));
    } else {
      throw new DatabaseException(
          "Can't convert object of type "
              + obj.getClass().getName()
              + " to type "
              + clazz.getName());
    }
  }

  private static class BeanMapper {

    private final Class clazz;
    private final Constructor constructor;
    private final boolean throwOnUnknownProperties;
    private final boolean warnOnUnknownProperties;
    // Case insensitive mapping of properties to their case sensitive versions
    private final Map properties;

    private final Map getters;
    private final Map setters;
    private final Map fields;

    public BeanMapper(Class clazz) {
      this.clazz = clazz;
      this.throwOnUnknownProperties = clazz.isAnnotationPresent(ThrowOnExtraProperties.class);
      this.warnOnUnknownProperties = !clazz.isAnnotationPresent(IgnoreExtraProperties.class);
      this.properties = new HashMap<>();

      this.setters = new HashMap<>();
      this.getters = new HashMap<>();
      this.fields = new HashMap<>();

      Constructor constructor = null;
      try {
        constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
      } catch (NoSuchMethodException e) {
        // We will only fail at deserialization time if no constructor is present
        constructor = null;
      }
      this.constructor = constructor;
      // Add any public getters to properties (including isXyz())
      for (Method method : clazz.getMethods()) {
        if (shouldIncludeGetter(method)) {
          String propertyName = propertyName(method);
          addProperty(propertyName);
          method.setAccessible(true);
          if (getters.containsKey(propertyName)) {
            throw new DatabaseException("Found conflicting getters for name: " + method.getName());
          }
          getters.put(propertyName, method);
        }
      }

      // Add any public fields to properties
      for (Field field : clazz.getFields()) {
        if (shouldIncludeField(field)) {
          String propertyName = propertyName(field);

          addProperty(propertyName);
        }
      }

      // We can use private setters and fields for known (public) properties/getters. Since
      // getMethods/getFields only returns public methods/fields we need to traverse the
      // class hierarchy to find the appropriate setter or field.
      Class currentClass = clazz;
      do {
        // Add any setters
        for (Method method : currentClass.getDeclaredMethods()) {
          if (shouldIncludeSetter(method)) {
            String propertyName = propertyName(method);
            String existingPropertyName = properties.get(propertyName.toLowerCase());
            if (existingPropertyName != null) {
              if (!existingPropertyName.equals(propertyName)) {
                throw new DatabaseException(
                    "Found setter with invalid case-sensitive name: " + method.getName());
              } else {
                Method existingSetter = setters.get(propertyName);
                if (existingSetter == null) {
                  method.setAccessible(true);
                  setters.put(propertyName, method);
                } else if (!isSetterOverride(method, existingSetter)) {
                  // We require that setters with conflicting property names are
                  // overrides from a base class
                  throw new DatabaseException(
                      "Found a conflicting setters "
                          + "with name: "
                          + method.getName()
                          + " (conflicts with "
                          + existingSetter.getName()
                          + " defined on "
                          + existingSetter.getDeclaringClass().getName()
                          + ")");
                }
              }
            }
          }
        }

        for (Field field : currentClass.getDeclaredFields()) {
          String propertyName = propertyName(field);

          // Case sensitivity is checked at deserialization time
          // Fields are only added if they don't exist on a subclass
          if (properties.containsKey(propertyName.toLowerCase())
              && !fields.containsKey(propertyName)) {
            field.setAccessible(true);
            fields.put(propertyName, field);
          }
        }

        // Traverse class hierarchy until we reach java.lang.Object which contains a bunch
        // of fields/getters we don't want to serialize
        currentClass = currentClass.getSuperclass();
      } while (currentClass != null && !currentClass.equals(Object.class));

      if (properties.isEmpty()) {
        throw new DatabaseException("No properties to serialize found on class " + clazz.getName());
      }
    }

    private static boolean shouldIncludeGetter(Method method) {
      if (!method.getName().startsWith("get") && !method.getName().startsWith("is")) {
        return false;
      }
      // Exclude methods from Object.class
      if (method.getDeclaringClass().equals(Object.class)) {
        return false;
      }
      // Non-public methods
      if (!Modifier.isPublic(method.getModifiers())) {
        return false;
      }
      // Static methods
      if (Modifier.isStatic(method.getModifiers())) {
        return false;
      }
      // No return type
      if (method.getReturnType().equals(Void.TYPE)) {
        return false;
      }
      // Non-zero parameters
      if (method.getParameterTypes().length != 0) {
        return false;
      }
      // Excluded methods
      if (method.isAnnotationPresent(Exclude.class)) {
        return false;
      }
      return true;
    }

    private static boolean shouldIncludeSetter(Method method) {
      if (!method.getName().startsWith("set")) {
        return false;
      }
      // Exclude methods from Object.class
      if (method.getDeclaringClass().equals(Object.class)) {
        return false;
      }
      // Static methods
      if (Modifier.isStatic(method.getModifiers())) {
        return false;
      }
      // Has a return type
      if (!method.getReturnType().equals(Void.TYPE)) {
        return false;
      }
      // Methods without exactly one parameters
      if (method.getParameterTypes().length != 1) {
        return false;
      }
      // Excluded methods
      if (method.isAnnotationPresent(Exclude.class)) {
        return false;
      }
      return true;
    }

    private static boolean shouldIncludeField(Field field) {
      // Exclude methods from Object.class
      if (field.getDeclaringClass().equals(Object.class)) {
        return false;
      }
      // Non-public fields
      if (!Modifier.isPublic(field.getModifiers())) {
        return false;
      }
      // Static fields
      if (Modifier.isStatic(field.getModifiers())) {
        return false;
      }
      // Transient fields
      if (Modifier.isTransient(field.getModifiers())) {
        return false;
      }
      // Excluded fields
      if (field.isAnnotationPresent(Exclude.class)) {
        return false;
      }
      return true;
    }

    private static boolean isSetterOverride(Method base, Method override) {
      // We expect an overridden setter here
      hardAssert(
          base.getDeclaringClass().isAssignableFrom(override.getDeclaringClass()),
          "Expected override from a base class");
      hardAssert(base.getReturnType().equals(Void.TYPE), "Expected void return type");
      hardAssert(override.getReturnType().equals(Void.TYPE), "Expected void return type");

      Type[] baseParameterTypes = base.getParameterTypes();
      Type[] overrideParameterTypes = override.getParameterTypes();
      hardAssert(baseParameterTypes.length == 1, "Expected exactly one parameter");
      hardAssert(overrideParameterTypes.length == 1, "Expected exactly one parameter");

      return base.getName().equals(override.getName())
          && baseParameterTypes[0].equals(overrideParameterTypes[0]);
    }

    private static String propertyName(Field field) {
      String annotatedName = annotatedName(field);
      return annotatedName != null ? annotatedName : field.getName();
    }

    private static String propertyName(Method method) {
      String annotatedName = annotatedName(method);
      return annotatedName != null ? annotatedName : serializedName(method.getName());
    }

    private static String annotatedName(AccessibleObject obj) {
      if (obj.isAnnotationPresent(PropertyName.class)) {
        PropertyName annotation = obj.getAnnotation(PropertyName.class);
        return annotation.value();
      }

      return null;
    }

    private static String serializedName(String methodName) {
      String[] prefixes = new String[] {"get", "set", "is"};
      String methodPrefix = null;
      for (String prefix : prefixes) {
        if (methodName.startsWith(prefix)) {
          methodPrefix = prefix;
        }
      }
      if (methodPrefix == null) {
        throw new IllegalArgumentException("Unknown Bean prefix for method: " + methodName);
      }
      String strippedName = methodName.substring(methodPrefix.length());

      // Make sure the first word or upper-case prefix is converted to lower-case
      char[] chars = strippedName.toCharArray();
      int pos = 0;
      while (pos < chars.length && Character.isUpperCase(chars[pos])) {
        chars[pos] = Character.toLowerCase(chars[pos]);
        pos++;
      }
      return new String(chars);
    }

    private void addProperty(String property) {
      String oldValue = this.properties.put(property.toLowerCase(), property);
      if (oldValue != null && !property.equals(oldValue)) {
        throw new DatabaseException(
            "Found two getters or fields with conflicting case "
                + "sensitivity for property: "
                + property.toLowerCase());
      }
    }

    public T deserialize(Map values) {
      return deserialize(values, Collections.>, Type>emptyMap());
    }

    public T deserialize(Map values, Map>, Type> types) {
      if (this.constructor == null) {
        throw new DatabaseException(
            "Class " + this.clazz.getName() + " is missing a constructor with no arguments");
      }
      T instance;
      try {
        instance = this.constructor.newInstance();
      } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
        throw new RuntimeException(e);
      }
      for (Map.Entry entry : values.entrySet()) {
        String propertyName = entry.getKey();
        if (this.setters.containsKey(propertyName)) {
          Method setter = this.setters.get(propertyName);
          Type[] params = setter.getGenericParameterTypes();
          if (params.length != 1) {
            throw new IllegalStateException("Setter does not have exactly one parameter");
          }
          Type resolvedType = resolveType(params[0], types);
          Object value = CustomClassMapper.deserializeToType(entry.getValue(), resolvedType);
          try {
            setter.invoke(instance, value);
          } catch (IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException(e);
          }
        } else if (this.fields.containsKey(propertyName)) {
          Field field = this.fields.get(propertyName);
          Type resolvedType = resolveType(field.getGenericType(), types);
          Object value = CustomClassMapper.deserializeToType(entry.getValue(), resolvedType);
          try {
            field.set(instance, value);
          } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
          }
        } else {
          String message =
              "No setter/field for "
                  + propertyName
                  + " found "
                  + "on class "
                  + this.clazz.getName();
          if (this.properties.containsKey(propertyName.toLowerCase())) {
            message += " (fields/setters are case sensitive!)";
          }
          if (this.throwOnUnknownProperties) {
            throw new DatabaseException(message);
          } else if (this.warnOnUnknownProperties) {
            logger.warn(message);
          }
        }
      }
      return instance;
    }

    private Type resolveType(Type type, Map>, Type> types) {
      if (type instanceof TypeVariable) {
        Type resolvedType = types.get(type);
        if (resolvedType == null) {
          throw new IllegalStateException("Could not resolve type " + type);
        } else {
          return resolvedType;
        }
      } else {
        return type;
      }
    }

    public Map serialize(T object) {
      if (!clazz.isAssignableFrom(object.getClass())) {
        throw new IllegalArgumentException(
            "Can't serialize object of class "
                + object.getClass()
                + " with BeanMapper for class "
                + clazz);
      }
      Map result = new HashMap<>();
      for (String property : this.properties.values()) {
        Object propertyValue;
        if (this.getters.containsKey(property)) {
          Method getter = this.getters.get(property);
          try {
            propertyValue = getter.invoke(object);
          } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
          } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
          }
        } else {
          // Must be a field
          Field field = this.fields.get(property);
          if (field == null) {
            throw new IllegalStateException("Bean property without field or getter:" + property);
          }
          try {
            propertyValue = field.get(object);
          } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
          }
        }
        Object serializedValue = CustomClassMapper.serialize(propertyValue);
        result.put(property, serializedValue);
      }
      return result;
    }
  }
}