001/*
002 *  Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com).
003 *  <p>
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *  <p>
008 *  http://www.apache.org/licenses/LICENSE-2.0
009 *  <p>
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package com.mybatisflex.core.util;
017
018import com.mybatisflex.annotation.Column;
019import org.apache.ibatis.reflection.Reflector;
020
021import java.lang.reflect.*;
022import java.util.Collection;
023import java.util.Map;
024import java.util.concurrent.ConcurrentHashMap;
025
026public class FieldWrapper {
027
028    public static Map<Class<?>, Map<String, FieldWrapper>> cache = new ConcurrentHashMap<>();
029
030    private Field field;
031    private boolean isIgnore = false;
032    private Class<?> fieldType;
033    private Class<?> mappingType;
034    private Class<?> keyType;
035    private Method getterMethod;
036    private Method setterMethod;
037
038    public static FieldWrapper of(Class<?> clazz, String fieldName) {
039        Map<String, FieldWrapper> wrapperMap = cache.get(clazz);
040        if (wrapperMap == null) {
041            synchronized (clazz) {
042                if (wrapperMap == null) {
043                    wrapperMap = new ConcurrentHashMap<>();
044                    cache.put(clazz, wrapperMap);
045                }
046            }
047        }
048
049        FieldWrapper fieldWrapper = wrapperMap.get(fieldName);
050        if (fieldWrapper == null) {
051            synchronized (clazz) {
052                fieldWrapper = wrapperMap.get(fieldName);
053                if (fieldWrapper == null) {
054                    Field findField = ClassUtil.getFirstField(clazz, field -> field.getName().equals(fieldName));
055                    if (findField == null) {
056                        throw new IllegalStateException("Can not find field \"" + fieldName + "\" in class: " + clazz);
057                    }
058
059                    String setterName = "set" + StringUtil.firstCharToUpperCase(fieldName);
060                    Method setter = ClassUtil.getFirstMethod(clazz, method ->
061                            method.getParameterCount() == 1
062                                    && Modifier.isPublic(method.getModifiers())
063                                    && method.getName().equals(setterName));
064
065                    fieldWrapper = new FieldWrapper();
066                    fieldWrapper.field = findField;
067                    fieldWrapper.fieldType = findField.getType();
068                    initMappingTypeAndKeyType(clazz, findField, fieldWrapper);
069
070                    Column column = findField.getAnnotation(Column.class);
071                    if (column != null && column.ignore()) {
072                        fieldWrapper.isIgnore = true;
073                    }
074
075                    fieldWrapper.setterMethod = setter;
076
077                    String[] getterNames = new String[]{"get" + StringUtil.firstCharToUpperCase(fieldName), "is" + StringUtil.firstCharToUpperCase(fieldName)};
078                    fieldWrapper.getterMethod = ClassUtil.getFirstMethod(clazz, method -> method.getParameterCount() == 0
079                            && Modifier.isPublic(method.getModifiers())
080                            && ArrayUtil.contains(getterNames, method.getName()));
081
082                    wrapperMap.put(fieldName, fieldWrapper);
083                }
084            }
085        }
086
087        return fieldWrapper;
088    }
089
090    private static void initMappingTypeAndKeyType(Class<?> clazz, Field field, FieldWrapper fieldWrapper) {
091        Reflector reflector = Reflectors.of(clazz);
092        Class<?> fieldType = reflector.getGetterType(field.getName());
093
094        if (Collection.class.isAssignableFrom(fieldType)) {
095            Type genericType = field.getGenericType();
096            if (genericType instanceof ParameterizedType) {
097                Type actualTypeArgument = ((ParameterizedType) genericType).getActualTypeArguments()[0];
098                fieldWrapper.mappingType = (Class<?>) actualTypeArgument;
099            }
100        } else if (Map.class.isAssignableFrom(fieldType)) {
101            Type genericType = field.getGenericType();
102            if (genericType instanceof ParameterizedType) {
103                fieldWrapper.keyType = (Class<?>) ((ParameterizedType) genericType).getActualTypeArguments()[0];
104                Type actualTypeArgument = ((ParameterizedType) genericType).getActualTypeArguments()[1];
105                if (actualTypeArgument instanceof ParameterizedType) {
106                    fieldWrapper.mappingType = (Class<?>) ((ParameterizedType) actualTypeArgument).getRawType();
107                } else {
108                    fieldWrapper.mappingType = (Class<?>) actualTypeArgument;
109                }
110            }
111        } else {
112            fieldWrapper.mappingType = fieldType;
113        }
114    }
115
116
117    public void set(Object value, Object to) {
118        try {
119            if (setterMethod == null) {
120                throw new IllegalStateException("Can not find method \"set" + StringUtil.firstCharToUpperCase(field.getName()) + "\" in class: " + to.getClass());
121            }
122            setterMethod.invoke(to, value);
123        } catch (Exception e) {
124            throw new RuntimeException(e);
125        }
126    }
127
128    public Object get(Object target) {
129        try {
130            if (getterMethod == null) {
131                throw new IllegalStateException("Can not find method \"get" + StringUtil.firstCharToUpperCase(field.getName()) + ", is" + StringUtil.firstCharToUpperCase(field.getName()) + "\" in class: " + target.getClass());
132            }
133            return getterMethod.invoke(target);
134        } catch (Exception e) {
135            throw new RuntimeException(e);
136        }
137    }
138
139    public Class<?> getFieldType() {
140        return fieldType;
141    }
142
143    public Class<?> getMappingType() {
144        return mappingType;
145    }
146
147    public Class<?> getKeyType() {
148        return keyType;
149    }
150
151    public Field getField() {
152        return field;
153    }
154
155    public boolean isIgnore() {
156        return isIgnore;
157    }
158}