package org.sql2o.reflection;

import org.sql2o.Sql2oException;
import org.sql2o.tools.AbstractCache;
import org.sql2o.tools.FeatureDetector;
import org.sql2o.tools.UnderscoreToCamelCase;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * Stores metadata for a POJO.
 */
public class PojoMetadata {

    private static final Cache caseSensitiveFalse = new Cache();
    private static final Cache caseSensitiveTrue = new Cache();
    private final PropertyAndFieldInfo propertyInfo;
    private final Map<String, String> columnMappings;
    private final FactoryFacade factoryFacade = FactoryFacade.getInstance();

    public boolean isCaseSensitive() {
        return caseSensitive;
    }

    public boolean isAutoDeriveColumnNames() {
        return autoDeriveColumnNames;
    }

    private boolean caseSensitive;
    private boolean autoDeriveColumnNames;
    private Class clazz;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        PojoMetadata that = (PojoMetadata) o;

        return autoDeriveColumnNames == that.autoDeriveColumnNames
                && caseSensitive == that.caseSensitive
                && clazz.equals(that.clazz)
                && columnMappings.equals(that.columnMappings)
                && propertyInfo.equals(that.propertyInfo);

    }

    @Override
    public int hashCode() {
        int result = (caseSensitive ? 1 : 0);
        result = 31 * result + clazz.hashCode();
        return result;
    }

    public PojoMetadata(Class clazz, boolean caseSensitive, boolean autoDeriveColumnNames, Map<String, String> columnMappings) {
        this.caseSensitive = caseSensitive;
        this.autoDeriveColumnNames = autoDeriveColumnNames;
        this.clazz = clazz;
        this.columnMappings = columnMappings == null ? Collections.<String,String>emptyMap() : columnMappings;

        this.propertyInfo = getPropertyInfoThroughCache();

    }

    public ObjectConstructor getObjectConstructor() {
        return propertyInfo.objectConstructor;
    }

    private PropertyAndFieldInfo getPropertyInfoThroughCache() {
        return (caseSensitive
                ? caseSensitiveTrue
                : caseSensitiveFalse)
                .get(clazz, this);
    }

    private PropertyAndFieldInfo initializePropertyInfo() {

        HashMap<String, Getter> propertyGetters = new HashMap<String, Getter>();
        HashMap<String, Setter> propertySetters = new HashMap<String, Setter>();
        HashMap<String, Field> fields = new HashMap<String, Field>();

        Class theClass = clazz;
        ObjectConstructor objectConstructor = factoryFacade.newConstructor(theClass);
        do {
            for (Field f : theClass.getDeclaredFields()) {
                String propertyName = f.getName();
                propertyName = caseSensitive ? propertyName : propertyName.toLowerCase();
                propertyGetters.put(propertyName, factoryFacade.newGetter(f));
                propertySetters.put(propertyName, factoryFacade.newSetter(f));
                fields.put(propertyName, f);
            }

            // prepare methods. Methods will override fields, if both exists.
            for (Method m : theClass.getDeclaredMethods()) {
                if (m.getParameterTypes().length!=1) continue;

                if (m.getName().startsWith("get")) {
                    String propertyName = m.getName().substring(3);
                    if (caseSensitive) {
                        propertyName = propertyName.substring(0, 1).toLowerCase() + propertyName.substring(1);
                    } else {
                        propertyName = propertyName.toLowerCase();
                    }

                    propertyGetters.put(propertyName, factoryFacade.newGetter(m));
                }

                if (m.getName().startsWith("set")) {
                    String propertyName = m.getName().substring(3);
                    if (caseSensitive) {
                        propertyName = propertyName.substring(0, 1).toLowerCase() + propertyName.substring(1);
                    } else {
                        propertyName = propertyName.toLowerCase();
                    }

                    propertySetters.put(propertyName, factoryFacade.newSetter(m));
                }
            }
            theClass = theClass.getSuperclass();
        } while (!theClass.equals(Object.class));

        return new PropertyAndFieldInfo(propertyGetters, propertySetters, fields, objectConstructor);

    }

    public Map<String, String> getColumnMappings() {
        return columnMappings;
    }

    public Getter getPropertyGetter(String propertyName) {

        Getter getter = getPropertyGetterIfExists(propertyName);

        if (getter != null) {
            return getter;
        } else {
            String errorMsg = "Property with name '" + propertyName + "' not found on class " + this.clazz.toString();
            if (this.caseSensitive) {
                errorMsg += " (You have turned on case sensitive property search. Is this intentional?)";
            }
            throw new Sql2oException(errorMsg);
        }
    }

    public Getter getPropertyGetterIfExists(String propertyName) {

        String name = this.caseSensitive ? propertyName : propertyName.toLowerCase();

        if (this.columnMappings.containsKey(name)) {
            name = this.columnMappings.get(name);
        }

        if (autoDeriveColumnNames) {
            name = UnderscoreToCamelCase.convert(name);
            if (!this.caseSensitive) name = name.toLowerCase();
        }

        return propertyInfo.propertyGetters.get(name);
    }

    public Setter getPropertySetter(String propertyName) {

        Setter setter = getPropertySetterIfExists(propertyName);

        if (setter != null) {
            return setter;
        } else {
            String errorMsg = "Property with name '" + propertyName + "' not found on class " + this.clazz.toString();
            if (this.caseSensitive) {
                errorMsg += " (You have turned on case sensitive property search. Is this intentional?)";
            }
            throw new Sql2oException(errorMsg);
        }
    }

    public Setter getPropertySetterIfExists(String propertyName) {

        String name = this.caseSensitive ? propertyName : propertyName.toLowerCase();

        if (this.columnMappings.containsKey(name)) {
            name = this.columnMappings.get(name);
        }

        if (autoDeriveColumnNames) {
            name = UnderscoreToCamelCase.convert(name);
            if (!this.caseSensitive) name = name.toLowerCase();
        }

        return propertyInfo.propertySetters.get(name);
    }

    public Class getType() {
        return this.clazz;
    }

    public Object getValueOfProperty(String propertyName, Object object) {
        String name = this.caseSensitive ? propertyName : propertyName.toLowerCase();

        Field field = this.propertyInfo.fields.get(name);

        boolean accesible = field.isAccessible();

        try {
            field.setAccessible(true);

            return field.get(object);
        } catch (IllegalAccessException e) {
            throw new Sql2oException("could not read value of field " + field.getName() + " on class " + object.getClass().toString(), e);
        }
        finally {
            field.setAccessible(accesible);
        }
    }

    private static class Cache extends AbstractCache<Class, PropertyAndFieldInfo, PojoMetadata> {
        @Override
        protected PropertyAndFieldInfo evaluate(Class key, PojoMetadata param) {
            return param.initializePropertyInfo();
        }
    }

    private static class PropertyAndFieldInfo {
        // since this class is private we can just use field access
        // to make HotSpot a little less work for inlining
        public final Map<String, Getter> propertyGetters;
        public final Map<String, Setter> propertySetters;
        public final Map<String, Field> fields;
        public final ObjectConstructor objectConstructor;

        private PropertyAndFieldInfo(
            Map<String, Getter> propertyGetters, Map<String, Setter> propertySetters,
            Map<String, Field> fields, ObjectConstructor objectConstructor) {

            this.propertyGetters = propertyGetters;
            this.propertySetters = propertySetters;
            this.fields = fields;
            this.objectConstructor = objectConstructor;
        }
    }
}


@@ -6984,554 +6984,100 @@
     
-String name = this.caseSensitive ? propertyName : propertyName.toLowerCase();%0A%0A        Field field = this.propertyInfo.fields.get(name);%0A%0A        boolean accesible = field.isAccessible();%0A%0A        try %7B%0A            field.setAccessible(true);%0A%0A            return field.get(object);%0A        %7D catch (IllegalAccessException e) %7B%0A            throw new Sql2oException(%22could not read value of field %22 + field.getName() + %22 on class %22 + object.getClass().toString(), e);%0A        %7D%0A        finally %7B%0A            field.setAccessible(accesible);%0A        %7D
+Getter getter = getPropertyGetter(propertyName);%0A%0A        return getter.getProperty(object);
 %0A   

Use the getter method for the property instead of directly accessing the field (See 47d60c21e7d88488a932223c554cdf928b2cfac1)