diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cacb75ad0..b65a81fd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,7 +86,7 @@ jobs: restore-keys: | ${{ runner.os }}-m2 - name: Test with Maven - run: ./mvnw clean package -B -Dmaven.test.skip=false -pl fesod-common,fesod-shaded,fesod-sheet,fesod-examples/fesod-sheet-examples + run: ./mvnw clean package -B -Dmaven.test.skip=false -pl fesod-common,fesod-shaded,fesod-beans/fesod-beans-cglib,fesod-sheet,fesod-examples/fesod-sheet-examples - name: Publish Unit Test Results uses: EnricoMi/publish-unit-test-result-action@v2 if: (!cancelled()) diff --git a/fesod-beans/fesod-beans-cglib/pom.xml b/fesod-beans/fesod-beans-cglib/pom.xml new file mode 100644 index 000000000..156bbacba --- /dev/null +++ b/fesod-beans/fesod-beans-cglib/pom.xml @@ -0,0 +1,53 @@ + + + + 4.0.0 + + + org.apache.fesod + fesod-beans + ${revision} + + + fesod-beans-cglib + jar + Fesod Beans Cglib + + + false + + + + + org.apache.fesod + fesod-common + ${revision} + + + + org.apache.fesod + fesod-shaded + ${revision} + + + diff --git a/fesod-beans/fesod-beans-cglib/src/main/java/org/apache/fesod/beans/cglib/BeanPropertyScanner.java b/fesod-beans/fesod-beans-cglib/src/main/java/org/apache/fesod/beans/cglib/BeanPropertyScanner.java new file mode 100644 index 000000000..28211f93b --- /dev/null +++ b/fesod-beans/fesod-beans-cglib/src/main/java/org/apache/fesod/beans/cglib/BeanPropertyScanner.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.fesod.beans.cglib; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.fesod.shaded.cglib.core.CodeGenerationException; + +/** + * Utility class for Bean property introspection. + * + *

+ * Extends the standard JavaBean introspection ({@link Introspector}) to further support fluent-style accessors. + *

+ * + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +class BeanPropertyScanner { + + private static final PropertyDescriptor[] EMPTY_DESCRIPTORS = new PropertyDescriptor[0]; + + /** + * Retrieve descriptors for all readable properties (including standard getters and fluent getters) of the target class. + * + * @param type target class + */ + public static PropertyDescriptor[] getBeanGetters(Class type) { + return getBeanProperties(type, true, false); + } + + /** + * Retrieve descriptors for all writeable properties (including standard setters and fluent setters) of the target class. + * + * @param type target class + */ + public static PropertyDescriptor[] getBeanSetters(Class type) { + return getBeanProperties(type, false, true); + } + + private static PropertyDescriptor[] getBeanProperties(Class type, boolean read, boolean write) { + try { + Map propertyMap = new LinkedHashMap<>(); + + BeanInfo info = Introspector.getBeanInfo(type, Object.class); + for (PropertyDescriptor pd : info.getPropertyDescriptors()) { + propertyMap.put(pd.getName(), pd); + } + + // Check for fluent-style accessors without prefix: for example, "lastName()", "lastName(String lastName)" + collectFluentAccessors(type, propertyMap); + + if (propertyMap.isEmpty()) { + return EMPTY_DESCRIPTORS; + } + + List properties = new ArrayList<>(propertyMap.size()); + for (PropertyDescriptor pd : propertyMap.values()) { + if ((read && pd.getReadMethod() != null) || (write && pd.getWriteMethod() != null)) { + properties.add(pd); + } + } + + return properties.toArray(EMPTY_DESCRIPTORS); + } catch (IntrospectionException e) { + throw new CodeGenerationException(e); + } + } + + private static void collectFluentAccessors(Class type, Map propertyMap) + throws IntrospectionException { + Map fieldMap = getAllFields(type); + + for (Method method : type.getMethods()) { + if (Modifier.isStatic(method.getModifiers()) + || method.isSynthetic() + || method.getDeclaringClass() == Object.class + || method.getDeclaringClass() == Class.class) { + continue; + } + + Field field = fieldMap.get(method.getName()); + if (field == null) { + continue; + } + + if (isFluentGetter(method, field)) { + PropertyDescriptor pd = propertyMap.get(method.getName()); + if (pd == null) { + pd = new PropertyDescriptor(method.getName(), type, null, null); + propertyMap.put(method.getName(), pd); + } + if (pd.getReadMethod() == null) { + pd.setReadMethod(method); + } + } else if (isFluentSetter(method, field, type)) { + PropertyDescriptor pd = propertyMap.get(method.getName()); + if (pd == null) { + pd = new PropertyDescriptor(method.getName(), type, null, null); + propertyMap.put(method.getName(), pd); + } + if (pd.getWriteMethod() == null) { + pd.setWriteMethod(method); + } + } + } + } + + private static boolean isFluentSetter(Method method, Field field, Class targetType) { + return method.getParameterCount() == 1 + && method.getParameterTypes()[0] == field.getType() + && (method.getReturnType() == void.class + || method.getReturnType().isAssignableFrom(targetType) + || method.getReturnType().isAssignableFrom(method.getDeclaringClass())); + } + + private static boolean isFluentGetter(Method method, Field field) { + return method.getParameterCount() == 0 && method.getReturnType() == field.getType(); + } + + private static Map getAllFields(Class type) { + Map fields = new HashMap<>(); + Class current = type; + while (current != null && current != Object.class) { + for (Field field : current.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers()) && !field.isSynthetic()) { + fields.putIfAbsent(field.getName(), field); + } + } + current = current.getSuperclass(); + } + return fields; + } +} diff --git a/fesod-beans/fesod-beans-cglib/src/main/java/org/apache/fesod/beans/cglib/CglibBeanWrapper.java b/fesod-beans/fesod-beans-cglib/src/main/java/org/apache/fesod/beans/cglib/CglibBeanWrapper.java new file mode 100644 index 000000000..848309bcd --- /dev/null +++ b/fesod-beans/fesod-beans-cglib/src/main/java/org/apache/fesod/beans/cglib/CglibBeanWrapper.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.fesod.beans.cglib; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.fesod.common.beans.BeanWrapper; +import org.apache.fesod.common.util.ValidateUtils; +import org.apache.fesod.shaded.cglib.beans.BeanMap; +import org.apache.fesod.shaded.cglib.core.DefaultNamingPolicy; + +/** + * A {@link BeanWrapper} implementation backed by CGLIB's {@link BeanMap}. + */ +public final class CglibBeanWrapper implements BeanWrapper { + + private static final Map, BeanMap> BEAN_MAP_CACHE = new ConcurrentHashMap<>(); + private final BeanMap delegate; + + public CglibBeanWrapper(Object bean) { + ValidateUtils.notNull(bean, "The bean instance must not be null"); + + this.delegate = initBeanMap(bean); + } + + private BeanMap initBeanMap(Object bean) { + BeanMap beanMap = BEAN_MAP_CACHE.computeIfAbsent(bean.getClass(), clazz -> { + EnhancedBeanMapGenerator gen = new EnhancedBeanMapGenerator(); + gen.setBeanClass(clazz); + gen.setContextClass(clazz); + gen.setNamingPolicy(FesodSheetNamingPolicy.INSTANCE); + return gen.create(); + }); + + return beanMap.newInstance(bean); + } + + @Override + public Object getProperty(String propertyName) { + return delegate.get(propertyName); + } + + @Override + public void setProperty(String propertyName, Object value) { + delegate.put(propertyName, value); + } + + @Override + public void setProperties(Map properties) { + delegate.putAll(properties); + } + + @SuppressWarnings("unchecked") + @Override + public Set getPropertyNames() { + return delegate.keySet(); + } + + @Override + public boolean containsProperty(String propertyName) { + return delegate.containsKey(propertyName); + } + + @Override + public int getPropertySize() { + return delegate.size(); + } + + @Override + public Class getPropertyType(String propertyName) { + return delegate.getPropertyType(propertyName); + } + + @Override + public Object unwrap() { + return delegate.getBean(); + } + + public static class FesodSheetNamingPolicy extends DefaultNamingPolicy { + public static final FesodSheetNamingPolicy INSTANCE = new FesodSheetNamingPolicy(); + + @Override + protected String getTag() { + return "ByFesodCGLIB"; + } + } +} diff --git a/fesod-beans/fesod-beans-cglib/src/main/java/org/apache/fesod/beans/cglib/CglibBeanWrapperProvider.java b/fesod-beans/fesod-beans-cglib/src/main/java/org/apache/fesod/beans/cglib/CglibBeanWrapperProvider.java new file mode 100644 index 000000000..7c4024d16 --- /dev/null +++ b/fesod-beans/fesod-beans-cglib/src/main/java/org/apache/fesod/beans/cglib/CglibBeanWrapperProvider.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.fesod.beans.cglib; + +import org.apache.fesod.common.beans.BeanWrapper; +import org.apache.fesod.common.beans.BeanWrapperProvider; + +/** + * An SPI {@link BeanWrapperProvider} implementation backed by CGLIB. + */ +public class CglibBeanWrapperProvider implements BeanWrapperProvider { + + @Override + public BeanWrapper create(Object bean) { + return new CglibBeanWrapper(bean); + } +} diff --git a/fesod-beans/fesod-beans-cglib/src/main/java/org/apache/fesod/beans/cglib/EnhancedBeanMapEmitter.java b/fesod-beans/fesod-beans-cglib/src/main/java/org/apache/fesod/beans/cglib/EnhancedBeanMapEmitter.java new file mode 100644 index 000000000..3b4f98bd6 --- /dev/null +++ b/fesod-beans/fesod-beans-cglib/src/main/java/org/apache/fesod/beans/cglib/EnhancedBeanMapEmitter.java @@ -0,0 +1,225 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.fesod.beans.cglib; + +import java.beans.PropertyDescriptor; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import org.apache.fesod.shaded.asm.ClassVisitor; +import org.apache.fesod.shaded.asm.Label; +import org.apache.fesod.shaded.asm.Type; +import org.apache.fesod.shaded.cglib.beans.BeanMap; +import org.apache.fesod.shaded.cglib.beans.FixedKeySet; +import org.apache.fesod.shaded.cglib.core.ClassEmitter; +import org.apache.fesod.shaded.cglib.core.CodeEmitter; +import org.apache.fesod.shaded.cglib.core.Constants; +import org.apache.fesod.shaded.cglib.core.EmitUtils; +import org.apache.fesod.shaded.cglib.core.MethodInfo; +import org.apache.fesod.shaded.cglib.core.ObjectSwitchCallback; +import org.apache.fesod.shaded.cglib.core.ReflectUtils; +import org.apache.fesod.shaded.cglib.core.Signature; +import org.apache.fesod.shaded.cglib.core.TypeUtils; + +/** + * Copied from {@link org.apache.fesod.shaded.cglib.beans.BeanMapEmitter}, + * with only the property-discovery logic enhanced via {@link BeanPropertyScanner}. + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +class EnhancedBeanMapEmitter extends ClassEmitter { + // APACHE FESOD PATCH BEGIN + private static final Type BEAN_MAP = Type.getType(BeanMap.class); + private static final Type FIXED_KEY_SET = Type.getType(FixedKeySet.class); + // APACHE FESOD PATCH END + private static final Signature CSTRUCT_OBJECT = TypeUtils.parseConstructor("Object"); + private static final Signature CSTRUCT_STRING_ARRAY = TypeUtils.parseConstructor("String[]"); + private static final Signature BEAN_MAP_GET = TypeUtils.parseSignature("Object get(Object, Object)"); + private static final Signature BEAN_MAP_PUT = TypeUtils.parseSignature("Object put(Object, Object, Object)"); + private static final Signature KEY_SET = TypeUtils.parseSignature("java.util.Set keySet()"); + private static final Signature NEW_INSTANCE = + new Signature("newInstance", BEAN_MAP, new Type[] {Constants.TYPE_OBJECT}); + private static final Signature GET_PROPERTY_TYPE = TypeUtils.parseSignature("Class getPropertyType(String)"); + + public EnhancedBeanMapEmitter(ClassVisitor v, String className, Class type, int require) { + super(v); + + // Byte code level cannot be higher than 1.8 due to STATICHOOK methods + // which set static final fields outside the initializer method . + begin_class(Constants.V1_8, Constants.ACC_PUBLIC, className, BEAN_MAP, null, Constants.SOURCE_FILE); + EmitUtils.null_constructor(this); + EmitUtils.factory_method(this, NEW_INSTANCE); + generateConstructor(); + + // APACHE FESOD PATCH BEGIN + Map getters = makePropertyMap(BeanPropertyScanner.getBeanGetters(type)); + Map setters = makePropertyMap(BeanPropertyScanner.getBeanSetters(type)); + // APACHE FESOD PATCH END + Map allProps = new HashMap(); + allProps.putAll(getters); + allProps.putAll(setters); + + if (require != 0) { + for (Iterator it = allProps.keySet().iterator(); it.hasNext(); ) { + String name = (String) it.next(); + if ((((require & BeanMap.REQUIRE_GETTER) != 0) && !getters.containsKey(name)) + || (((require & BeanMap.REQUIRE_SETTER) != 0) && !setters.containsKey(name))) { + it.remove(); + getters.remove(name); + setters.remove(name); + } + } + } + generateGet(type, getters); + generatePut(type, setters); + + String[] allNames = getNames(allProps); + generateKeySet(allNames); + generateGetPropertyType(allProps, allNames); + end_class(); + } + + private Map makePropertyMap(PropertyDescriptor[] props) { + Map names = new HashMap(); + for (PropertyDescriptor prop : props) { + names.put(prop.getName(), prop); + } + return names; + } + + private String[] getNames(Map propertyMap) { + return (String[]) propertyMap.keySet().toArray(new String[propertyMap.size()]); + } + + private void generateConstructor() { + CodeEmitter e = begin_method(Constants.ACC_PUBLIC, CSTRUCT_OBJECT, null); + e.load_this(); + e.load_arg(0); + e.super_invoke_constructor(CSTRUCT_OBJECT); + e.return_value(); + e.end_method(); + } + + private void generateGet(Class type, final Map getters) { + final CodeEmitter e = begin_method(Constants.ACC_PUBLIC, BEAN_MAP_GET, null); + e.load_arg(0); + e.checkcast(Type.getType(type)); + e.load_arg(1); + e.checkcast(Constants.TYPE_STRING); + EmitUtils.string_switch(e, getNames(getters), Constants.SWITCH_STYLE_HASH, new ObjectSwitchCallback() { + @Override + public void processCase(Object key, Label end) { + PropertyDescriptor pd = (PropertyDescriptor) getters.get(key); + MethodInfo method = ReflectUtils.getMethodInfo(pd.getReadMethod()); + e.invoke(method); + e.box(method.getSignature().getReturnType()); + e.return_value(); + } + + @Override + public void processDefault() { + e.aconst_null(); + e.return_value(); + } + }); + e.end_method(); + } + + private void generatePut(Class type, final Map setters) { + final CodeEmitter e = begin_method(Constants.ACC_PUBLIC, BEAN_MAP_PUT, null); + e.load_arg(0); + e.checkcast(Type.getType(type)); + e.load_arg(1); + e.checkcast(Constants.TYPE_STRING); + EmitUtils.string_switch(e, getNames(setters), Constants.SWITCH_STYLE_HASH, new ObjectSwitchCallback() { + @Override + public void processCase(Object key, Label end) { + PropertyDescriptor pd = (PropertyDescriptor) setters.get(key); + if (pd.getReadMethod() == null) { + e.aconst_null(); + } else { + MethodInfo read = ReflectUtils.getMethodInfo(pd.getReadMethod()); + e.dup(); + e.invoke(read); + e.box(read.getSignature().getReturnType()); + } + e.swap(); // move old value behind bean + e.load_arg(2); // new value + MethodInfo write = ReflectUtils.getMethodInfo(pd.getWriteMethod()); + e.unbox(write.getSignature().getArgumentTypes()[0]); + e.invoke(write); + // APACHE FESOD PATCH BEGIN + if (pd.getWriteMethod().getReturnType() != void.class) { + e.pop(); + } + // APACHE FESOD PATCH END + e.return_value(); + } + + @Override + public void processDefault() { + // fall-through + } + }); + e.aconst_null(); + e.return_value(); + e.end_method(); + } + + private void generateKeySet(String[] allNames) { + // static initializer + declare_field(Constants.ACC_STATIC | Constants.ACC_PRIVATE, "keys", FIXED_KEY_SET, null); + + CodeEmitter e = begin_static(); + e.new_instance(FIXED_KEY_SET); + e.dup(); + EmitUtils.push_array(e, allNames); + e.invoke_constructor(FIXED_KEY_SET, CSTRUCT_STRING_ARRAY); + e.putfield("keys"); + e.return_value(); + e.end_method(); + + // keySet + e = begin_method(Constants.ACC_PUBLIC, KEY_SET, null); + e.load_this(); + e.getfield("keys"); + e.return_value(); + e.end_method(); + } + + private void generateGetPropertyType(final Map allProps, String[] allNames) { + final CodeEmitter e = begin_method(Constants.ACC_PUBLIC, GET_PROPERTY_TYPE, null); + e.load_arg(0); + EmitUtils.string_switch(e, allNames, Constants.SWITCH_STYLE_HASH, new ObjectSwitchCallback() { + @Override + public void processCase(Object key, Label end) { + PropertyDescriptor pd = (PropertyDescriptor) allProps.get(key); + EmitUtils.load_class(e, Type.getType(pd.getPropertyType())); + e.return_value(); + } + + @Override + public void processDefault() { + e.aconst_null(); + e.return_value(); + } + }); + e.end_method(); + } +} diff --git a/fesod-beans/fesod-beans-cglib/src/main/java/org/apache/fesod/beans/cglib/EnhancedBeanMapGenerator.java b/fesod-beans/fesod-beans-cglib/src/main/java/org/apache/fesod/beans/cglib/EnhancedBeanMapGenerator.java new file mode 100644 index 000000000..38ce23c98 --- /dev/null +++ b/fesod-beans/fesod-beans-cglib/src/main/java/org/apache/fesod/beans/cglib/EnhancedBeanMapGenerator.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.fesod.beans.cglib; + +import java.security.ProtectionDomain; +import org.apache.fesod.shaded.asm.ClassVisitor; +import org.apache.fesod.shaded.cglib.beans.BeanMap; +import org.apache.fesod.shaded.cglib.core.AbstractClassGenerator; +import org.apache.fesod.shaded.cglib.core.KeyFactory; +import org.apache.fesod.shaded.cglib.core.ReflectUtils; + +/** + * Copied from {@link org.apache.fesod.shaded.cglib.beans.BeanMap.Generator}, + * with only the class generation delegated to {@link EnhancedBeanMapEmitter}. + */ +class EnhancedBeanMapGenerator extends AbstractClassGenerator { + private static final Source SOURCE = new Source(BeanMap.class.getName()); + + private static final BeanMapKey KEY_FACTORY = + (BeanMapKey) KeyFactory.create(BeanMapKey.class, KeyFactory.CLASS_BY_NAME); + + interface BeanMapKey { + public Object newInstance(Class type, int require); + } + + private Object bean; + private Class beanClass; + private int require; + + public EnhancedBeanMapGenerator() { + super(SOURCE); + } + + /** + * Set the bean that the generated map should reflect. The bean may be swapped + * out for another bean of the same type using {@link #setBean}. + * Calling this method overrides any value previously set using {@link #setBeanClass}. + * You must call either this method or {@link #setBeanClass} before {@link #create}. + * + * @param bean the initial bean + */ + public void setBean(Object bean) { + this.bean = bean; + if (bean != null) { + beanClass = bean.getClass(); + // SPRING PATCH BEGIN + setContextClass(beanClass); + // SPRING PATCH END + } + } + + /** + * Set the class of the bean that the generated map should support. + * You must call either this method or {@link #setBeanClass} before {@link #create}. + * + * @param beanClass the class of the bean + */ + public void setBeanClass(Class beanClass) { + this.beanClass = beanClass; + } + + /** + * Limit the properties reflected by the generated map. + * + * @param require any combination of {@link BeanMap#REQUIRE_GETTER} and + * {@link BeanMap#REQUIRE_SETTER}; default is zero (any property allowed) + */ + public void setRequire(int require) { + this.require = require; + } + + @Override + protected ClassLoader getDefaultClassLoader() { + return beanClass.getClassLoader(); + } + + @Override + protected ProtectionDomain getProtectionDomain() { + return ReflectUtils.getProtectionDomain(beanClass); + } + + /** + * Create a new instance of the BeanMap. An existing + * generated class will be reused if possible. + */ + public BeanMap create() { + if (beanClass == null) { + throw new IllegalArgumentException("Class of bean unknown"); + } + setNamePrefix(beanClass.getName()); + return (BeanMap) super.create(KEY_FACTORY.newInstance(beanClass, require)); + } + + @Override + public void generateClass(ClassVisitor v) throws Exception { + // APACHE FESOD PATCH BEGIN + new EnhancedBeanMapEmitter(v, getClassName(), beanClass, require); + // APACHE FESOD PATCH END + } + + @Override + protected Object firstInstance(Class type) { + return ((BeanMap) ReflectUtils.newInstance(type)).newInstance(bean); + } + + @Override + protected Object nextInstance(Object instance) { + return ((BeanMap) instance).newInstance(bean); + } +} diff --git a/fesod-beans/fesod-beans-cglib/src/main/resources/META-INF/services/org.apache.fesod.common.beans.BeanWrapperProvider b/fesod-beans/fesod-beans-cglib/src/main/resources/META-INF/services/org.apache.fesod.common.beans.BeanWrapperProvider new file mode 100644 index 000000000..51f5d7ba9 --- /dev/null +++ b/fesod-beans/fesod-beans-cglib/src/main/resources/META-INF/services/org.apache.fesod.common.beans.BeanWrapperProvider @@ -0,0 +1 @@ +org.apache.fesod.beans.cglib.CglibBeanWrapperProvider diff --git a/fesod-beans/fesod-beans-cglib/src/test/java/org/apache/fesod/beans/BeanWrapperTest.java b/fesod-beans/fesod-beans-cglib/src/test/java/org/apache/fesod/beans/BeanWrapperTest.java new file mode 100644 index 000000000..e693f7781 --- /dev/null +++ b/fesod-beans/fesod-beans-cglib/src/test/java/org/apache/fesod/beans/BeanWrapperTest.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.fesod.beans; + +import org.apache.fesod.beans.cglib.CglibBeanWrapper; +import org.apache.fesod.common.beans.BeanWrappers; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link BeanWrappers}. + */ +class BeanWrapperTest { + + static class MockBean {} + + @Test + void shouldReturnCglibBeanWrapper() { + Assertions.assertThat(BeanWrappers.create(new MockBean())).isInstanceOf(CglibBeanWrapper.class); + } +} diff --git a/fesod-beans/fesod-beans-cglib/src/test/java/org/apache/fesod/beans/cglib/BeanPropertyScannerTest.java b/fesod-beans/fesod-beans-cglib/src/test/java/org/apache/fesod/beans/cglib/BeanPropertyScannerTest.java new file mode 100644 index 000000000..fd37d2a57 --- /dev/null +++ b/fesod-beans/fesod-beans-cglib/src/test/java/org/apache/fesod/beans/cglib/BeanPropertyScannerTest.java @@ -0,0 +1,199 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.fesod.beans.cglib; + +import java.beans.PropertyDescriptor; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link BeanPropertyScanner}. + */ +class BeanPropertyScannerTest { + + @Test + void shouldExposeFluentAccessorsBackedByField() { + PropertyDescriptor[] getters = BeanPropertyScanner.getBeanGetters(FluentBean.class); + PropertyDescriptor[] setters = BeanPropertyScanner.getBeanSetters(FluentBean.class); + + PropertyDescriptor getter = findPropertyDescriptor(getters, "firstName"); + Assertions.assertNotNull(getter); + Assertions.assertEquals("firstName", getter.getReadMethod().getName()); + + PropertyDescriptor setter = findPropertyDescriptor(setters, "firstName"); + Assertions.assertNotNull(setter); + Assertions.assertEquals("firstName", setter.getWriteMethod().getName()); + } + + @Test + void shouldIgnoreFluentAccessorsNotMatchingFieldType() { + Set getterNames = propertyNames(BeanPropertyScanner.getBeanGetters(MismatchedAccessorBean.class)); + Set setterNames = propertyNames(BeanPropertyScanner.getBeanSetters(MismatchedAccessorBean.class)); + + Assertions.assertTrue(getterNames.contains("token")); + Assertions.assertFalse(setterNames.contains("token")); + } + + @Test + void shouldIgnoreMethodsWithoutBackingField() { + Set getterNames = propertyNames(BeanPropertyScanner.getBeanGetters(HelperBean.class)); + Set setterNames = propertyNames(BeanPropertyScanner.getBeanSetters(HelperBean.class)); + + Assertions.assertFalse(getterNames.contains("describe")); + Assertions.assertFalse(setterNames.contains("rename")); + } + + @Test + void shouldPreferStandardAccessorsOverFluentOnes() { + PropertyDescriptor pd = + findPropertyDescriptor(BeanPropertyScanner.getBeanGetters(MixedAccessorBean.class), "name"); + + Assertions.assertNotNull(pd); + Assertions.assertEquals("getName", pd.getReadMethod().getName()); + } + + @Test + void shouldReturnReadOnlyAndWriteOnlyPropertiesOnMatchingSideOnly() { + Set getterNames = propertyNames(BeanPropertyScanner.getBeanGetters(PartialFluentBean.class)); + Set setterNames = propertyNames(BeanPropertyScanner.getBeanSetters(PartialFluentBean.class)); + + Assertions.assertTrue(getterNames.contains("id")); + Assertions.assertFalse(setterNames.contains("id")); + Assertions.assertTrue(setterNames.contains("secret")); + Assertions.assertFalse(getterNames.contains("secret")); + } + + @Test + void shouldRecognizeInheritedFluentAccessors() { + Set getterNames = propertyNames(BeanPropertyScanner.getBeanGetters(ChildBean.class)); + + Assertions.assertTrue(getterNames.contains("code")); + Assertions.assertTrue(getterNames.contains("extra")); + } + + private static PropertyDescriptor findPropertyDescriptor(PropertyDescriptor[] descriptors, String name) { + for (PropertyDescriptor pd : descriptors) { + if (pd.getName().equals(name)) { + return pd; + } + } + return null; + } + + private static Set propertyNames(PropertyDescriptor[] descriptors) { + Set names = new HashSet<>(); + for (PropertyDescriptor pd : descriptors) { + names.add(pd.getName()); + } + return names; + } + + static class FluentBean { + private String firstName; + + public String firstName() { + return firstName; + } + + public FluentBean firstName(String firstName) { + this.firstName = firstName.toString(); + return this; + } + } + + static class MismatchedAccessorBean { + private String token; + + public String token() { + return token; + } + + public void token(StringBuilder token) { + this.token = token.toString(); + } + } + + static class HelperBean { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String describe() { + return "helper"; + } + + public void rename(String newName) { + this.name = newName; + } + } + + static class MixedAccessorBean { + private String name; + + public String getName() { + return name; + } + + public String name() { + return name; + } + } + + static class PartialFluentBean { + private Long id; + private String secret; + + public Long id() { + return id; + } + + public void secret(String secret) { + this.secret = secret; + } + } + + static class BaseBean { + private String code; + + public String code() { + return code; + } + + public void code(String code) { + this.code = code; + } + } + + static class ChildBean extends BaseBean { + private String extra; + + public String extra() { + return extra; + } + } +} diff --git a/fesod-beans/fesod-beans-cglib/src/test/java/org/apache/fesod/beans/cglib/CglibBeanWrapperTest.java b/fesod-beans/fesod-beans-cglib/src/test/java/org/apache/fesod/beans/cglib/CglibBeanWrapperTest.java new file mode 100644 index 000000000..9a476e98a --- /dev/null +++ b/fesod-beans/fesod-beans-cglib/src/test/java/org/apache/fesod/beans/cglib/CglibBeanWrapperTest.java @@ -0,0 +1,247 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.fesod.beans.cglib; + +import java.util.LinkedHashMap; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.apache.fesod.common.beans.BeanWrapper; +import org.apache.fesod.shaded.cglib.beans.BeanMap; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link CglibBeanWrapper}. + */ +class CglibBeanWrapperTest { + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + static class SampleBean { + private String name; + private int age; + private boolean active; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Accessors(fluent = true) + static class FluentSampleBean { + private String name; + private int age; + } + + @Test + void shouldRejectNullBean() { + Assertions.assertThatThrownBy(() -> new CglibBeanWrapper(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("The bean instance must not be null"); + } + + @Test + void shouldGetPropertyValue() { + SampleBean bean = new SampleBean("Jack", 18, true); + + BeanWrapper wrapper = new CglibBeanWrapper(bean); + + Assertions.assertThat(wrapper.getProperty("name")).isEqualTo("Jack"); + Assertions.assertThat(wrapper.getProperty("age")).isEqualTo(18); + Assertions.assertThat(wrapper.getProperty("active")).isEqualTo(true); + } + + @Test + void shouldReturnNullForUnknownProperty() { + BeanWrapper wrapper = new CglibBeanWrapper(new SampleBean()); + + Assertions.assertThat(wrapper.getProperty("nope")).isNull(); + } + + @Test + void shouldSetProperty() { + SampleBean bean = new SampleBean(); + BeanWrapper wrapper = new CglibBeanWrapper(bean); + + wrapper.setProperty("name", "Tom"); + + Assertions.assertThat(bean.getName()).isEqualTo("Tom"); + Assertions.assertThat(wrapper.getProperty("name")).isEqualTo("Tom"); + } + + @Test + void shouldSetPrimitiveWithBoxing() { + SampleBean bean = new SampleBean(); + BeanWrapper wrapper = new CglibBeanWrapper(bean); + + wrapper.setProperty("age", 21); + + Assertions.assertThat(bean.getAge()).isEqualTo(21); + Assertions.assertThat(wrapper.getProperty("age")).isEqualTo(21); + } + + @Test + void shouldBatchSetProperties() { + SampleBean bean = new SampleBean(); + BeanWrapper wrapper = new CglibBeanWrapper(bean); + Map properties = new LinkedHashMap<>(); + properties.put("name", "Tomas"); + properties.put("age", 30); + properties.put("active", false); + + wrapper.setProperties(properties); + + Assertions.assertThat(bean.getName()).isEqualTo("Tomas"); + Assertions.assertThat(bean.getAge()).isEqualTo(30); + Assertions.assertThat(bean.isActive()).isFalse(); + } + + @Test + void shouldReportPropertyNames() { + BeanWrapper wrapper = new CglibBeanWrapper(new SampleBean()); + + Assertions.assertThat(wrapper.getPropertyNames()).contains("name", "age", "active"); + } + + @Test + void shouldReportContainsProperty() { + BeanWrapper wrapper = new CglibBeanWrapper(new SampleBean()); + + Assertions.assertThat(wrapper.containsProperty("name")).isTrue(); + Assertions.assertThat(wrapper.containsProperty("age")).isTrue(); + Assertions.assertThat(wrapper.containsProperty("active")).isTrue(); + Assertions.assertThat(wrapper.containsProperty("nope")).isFalse(); + } + + @Test + void shouldReportPropertySize() { + BeanWrapper wrapper = new CglibBeanWrapper(new SampleBean()); + + Assertions.assertThat(wrapper.getPropertySize()) + .isEqualTo(wrapper.getPropertyNames().size()); + } + + @Test + void shouldReportPropertyType() { + BeanWrapper wrapper = new CglibBeanWrapper(new SampleBean()); + + Assertions.assertThat(wrapper.getPropertyType("name")).isEqualTo(String.class); + Assertions.assertThat(wrapper.getPropertyType("age")).isEqualTo(int.class); + Assertions.assertThat(wrapper.getPropertyType("active")).isEqualTo(boolean.class); + Assertions.assertThat(wrapper.getPropertyType("nope")).isNull(); + } + + @Test + void shouldUnwrapOriginalBean() { + SampleBean bean = new SampleBean(); + + BeanWrapper wrapper = new CglibBeanWrapper(bean); + + Assertions.assertThat(wrapper.unwrap()).isSameAs(bean); + } + + @Test + void shouldReturnWrappedClass() { + SampleBean bean = new SampleBean(); + BeanWrapper wrapper = new CglibBeanWrapper(bean); + + Assertions.assertThat(wrapper.getWrappedClass()) + .isEqualTo(SampleBean.class) + .isEqualTo(bean.getClass()); + } + + @Test + void shouldReflectExternalBeanMutation() { + SampleBean bean = new SampleBean("Jackson", 1, false); + BeanWrapper wrapper = new CglibBeanWrapper(bean); + + bean.setName("Kendall"); + + Assertions.assertThat(wrapper.getProperty("name")).isEqualTo("Kendall"); + } + + @Test + void shouldReadFluentAccessorProperties() { + BeanWrapper wrapper = new CglibBeanWrapper(new FluentSampleBean("Rose", 20)); + + Assertions.assertThat(wrapper.getProperty("name")).isEqualTo("Rose"); + Assertions.assertThat(wrapper.getProperty("age")).isEqualTo(20); + } + + @Test + void shouldWriteFluentAccessorProperties() { + FluentSampleBean bean = new FluentSampleBean(); + BeanWrapper wrapper = new CglibBeanWrapper(bean); + + wrapper.setProperty("name", "Lily"); + wrapper.setProperty("age", 21); + + Assertions.assertThat(bean.name()).isEqualTo("Lily"); + Assertions.assertThat(bean.age()).isEqualTo(21); + } + + @Test + void shouldReturnPreviousValueWhenPuttingFluentProperty() { + EnhancedBeanMapGenerator generator = new EnhancedBeanMapGenerator(); + generator.setBeanClass(FluentSampleBean.class); + BeanMap map = generator.create().newInstance(new FluentSampleBean("Rose", 20)); + + Assertions.assertThat(map.put("name", "Lily")).isEqualTo("Rose"); + Assertions.assertThat(map.put("age", 21)).isEqualTo(20); + } + + @Test + void shouldExposeFluentAccessorPropertiesInMetadata() { + BeanWrapper wrapper = new CglibBeanWrapper(new FluentSampleBean()); + + Assertions.assertThat(wrapper.getPropertyNames()).contains("name", "age"); + Assertions.assertThat(wrapper.containsProperty("name")).isTrue(); + Assertions.assertThat(wrapper.containsProperty("nope")).isFalse(); + Assertions.assertThat(wrapper.getPropertyType("name")).isEqualTo(String.class); + } + + @Test + void shouldIsolateWrappersOfSameBeanClass() { + SampleBean first = new SampleBean("Rose", 20, true); + SampleBean second = new SampleBean("Jack", 40, false); + + BeanWrapper firstWrapper = new CglibBeanWrapper(first); + BeanWrapper secondWrapper = new CglibBeanWrapper(second); + firstWrapper.setProperty("name", "Changed"); + + Assertions.assertThat(firstWrapper.getProperty("name")).isEqualTo("Changed"); + Assertions.assertThat(secondWrapper.getProperty("name")).isEqualTo("Jack"); + Assertions.assertThat(firstWrapper.unwrap()).isSameAs(first); + Assertions.assertThat(secondWrapper.unwrap()).isSameAs(second); + } + + @Test + void shouldUseFesodCglibNamingPolicy() { + CglibBeanWrapper.FesodSheetNamingPolicy policy = CglibBeanWrapper.FesodSheetNamingPolicy.INSTANCE; + + Assertions.assertThat(policy.getTag()).isEqualTo("ByFesodCGLIB"); + } +} diff --git a/fesod-beans/pom.xml b/fesod-beans/pom.xml new file mode 100644 index 000000000..6be562fc1 --- /dev/null +++ b/fesod-beans/pom.xml @@ -0,0 +1,59 @@ + + + + 4.0.0 + + org.apache.fesod + fesod-parent + ${revision} + + + fesod-beans + pom + Fesod Beans + + + UTF-8 + true + true + + + fesod-beans-cglib + + + + + + com.diffplug.spotless + spotless-maven-plugin + + + + ${maven.multiModuleProjectDirectory}/tools/spotless/license-header.txt + + + + + + + diff --git a/fesod-common/src/main/java/org/apache/fesod/common/beans/BeanWrapper.java b/fesod-common/src/main/java/org/apache/fesod/common/beans/BeanWrapper.java new file mode 100644 index 000000000..18f9075b8 --- /dev/null +++ b/fesod-common/src/main/java/org/apache/fesod/common/beans/BeanWrapper.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.fesod.common.beans; + +import java.util.Map; +import java.util.Set; + +/** + * An interface for classes that can access named properties. + */ +public interface BeanWrapper { + + /** + * Get the current value of the property. + * + * @param propertyName the name of the property to get the value + * @return the value of the property + */ + Object getProperty(String propertyName); + + /** + * Set the value to current property. + * + * @param propertyName the name of the property to set the value + * @param value the value for setting + */ + void setProperty(String propertyName, Object value); + + /** + * Batch set from a {@link Map}. + * + * @param properties a Map to take properties from + */ + void setProperties(Map properties); + + /** + * Returns a {@link Set} of the bean property names. + * + * @return a {@link Set} of the bean property names + */ + Set getPropertyNames(); + + boolean containsProperty(String propertyName); + + int getPropertySize(); + + /** + * Returns the type of the named property. + * + * @param propertyName the name of the property + * @return the {@link Class} of the property, or {@code null} if no such property exists + */ + Class getPropertyType(String propertyName); + + /** + * Returns the type of the wrapped bean instance. + */ + default Class getWrappedClass() { + return unwrap().getClass(); + } + + /** + * Returns the wrapped bean object. + */ + Object unwrap(); +} diff --git a/fesod-common/src/main/java/org/apache/fesod/common/beans/BeanWrapperProvider.java b/fesod-common/src/main/java/org/apache/fesod/common/beans/BeanWrapperProvider.java new file mode 100644 index 000000000..c8e0333bc --- /dev/null +++ b/fesod-common/src/main/java/org/apache/fesod/common/beans/BeanWrapperProvider.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.fesod.common.beans; + +/** + * Strategy interface for resolving a {@link BeanWrapper}. + */ +public interface BeanWrapperProvider { + + int DEFAULT_ORDER = 500; + + BeanWrapper create(Object bean); + + default int getOrder() { + return DEFAULT_ORDER; + } +} diff --git a/fesod-common/src/main/java/org/apache/fesod/common/beans/BeanWrappers.java b/fesod-common/src/main/java/org/apache/fesod/common/beans/BeanWrappers.java new file mode 100644 index 000000000..884865e18 --- /dev/null +++ b/fesod-common/src/main/java/org/apache/fesod/common/beans/BeanWrappers.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.fesod.common.beans; + +import java.util.Map; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.fesod.common.util.ValidateUtils; + +/** + * This class is to be used provide access to the default {@link BeanWrapper} instances. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class BeanWrappers { + + private static BeanWrapperProvider provider; + + static { + ServiceLoader loader = ServiceLoader.load(BeanWrapperProvider.class); + + BeanWrapperProvider tmpProvider = null; + for (BeanWrapperProvider candidate : loader) { + if (tmpProvider == null || candidate.getOrder() < tmpProvider.getOrder()) { + tmpProvider = candidate; + } + } + + if (tmpProvider == null) { + throw new ServiceConfigurationError("No valid BeanWrapperProvider found on the classpath"); + } + provider = tmpProvider; + } + + public static BeanWrapper create(Object bean) { + if (bean == null) { + return null; + } + if (bean instanceof Map) { + return new MapBeanWrapper((Map) bean); + } + if (bean instanceof BeanWrapper) { + return (BeanWrapper) bean; + } + return provider.create(bean); + } + + public static void setProvider(BeanWrapperProvider provider) { + BeanWrappers.provider = ValidateUtils.notNull(provider, "BeanWrapperProvider cannot be null"); + } +} diff --git a/fesod-common/src/main/java/org/apache/fesod/common/beans/MapBeanWrapper.java b/fesod-common/src/main/java/org/apache/fesod/common/beans/MapBeanWrapper.java new file mode 100644 index 000000000..4b22b2a9e --- /dev/null +++ b/fesod-common/src/main/java/org/apache/fesod/common/beans/MapBeanWrapper.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.fesod.common.beans; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import org.apache.fesod.common.util.ValidateUtils; + +/** + * A {@link BeanWrapper} implementation that adapts a {@link Map} into a bean property accessor. + */ +public final class MapBeanWrapper implements BeanWrapper { + + private final Map delegate; + + public MapBeanWrapper(Map map) { + this.delegate = ValidateUtils.notNull(map, "The map must not be null"); + } + + @Override + public Object getProperty(String propertyName) { + return delegate.get(propertyName); + } + + @Override + public void setProperty(String propertyName, Object value) { + delegate.put(propertyName, value); + } + + @Override + public void setProperties(Map properties) { + if (properties != null && !properties.isEmpty()) { + delegate.putAll(properties); + } + } + + @Override + public Set getPropertyNames() { + return Collections.unmodifiableSet(delegate.keySet()); + } + + @Override + public boolean containsProperty(String propertyName) { + return delegate.containsKey(propertyName); + } + + @Override + public int getPropertySize() { + return delegate.size(); + } + + @Override + public Class getPropertyType(String propertyName) { + Object value = delegate.get(propertyName); + return value != null ? value.getClass() : null; + } + + @Override + public Object unwrap() { + return delegate; + } +} diff --git a/fesod-common/src/test/java/org/apache/fesod/common/beans/MapBeanWrapperTest.java b/fesod-common/src/test/java/org/apache/fesod/common/beans/MapBeanWrapperTest.java new file mode 100644 index 000000000..3109219f4 --- /dev/null +++ b/fesod-common/src/test/java/org/apache/fesod/common/beans/MapBeanWrapperTest.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.fesod.common.beans; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link MapBeanWrapper}. + */ +class MapBeanWrapperTest { + + private static Map sampleMap() { + Map map = new LinkedHashMap<>(); + map.put("name", "Jack"); + map.put("age", 18); + map.put("active", true); + return map; + } + + @Test + void shouldRejectNullMap() { + Assertions.assertThrows(NullPointerException.class, () -> new MapBeanWrapper(null)); + } + + @Test + void shouldGetPropertyValue() { + MapBeanWrapper wrapper = new MapBeanWrapper(sampleMap()); + + Assertions.assertEquals("Jack", wrapper.getProperty("name")); + Assertions.assertEquals(18, wrapper.getProperty("age")); + Assertions.assertEquals(true, wrapper.getProperty("active")); + } + + @Test + void shouldReturnNullForMissingKey() { + MapBeanWrapper wrapper = new MapBeanWrapper(sampleMap()); + + Assertions.assertNull(wrapper.getProperty("nope")); + } + + @Test + void shouldSetProperty() { + Map map = sampleMap(); + MapBeanWrapper wrapper = new MapBeanWrapper(map); + + wrapper.setProperty("name", "Tom"); + + Assertions.assertEquals("Tom", map.get("name")); + Assertions.assertEquals("Tom", wrapper.getProperty("name")); + } + + @Test + void shouldBatchSetProperties() { + Map map = sampleMap(); + MapBeanWrapper wrapper = new MapBeanWrapper(map); + Map extra = new LinkedHashMap<>(); + extra.put("name", "Tomas"); + extra.put("city", "Beijing"); + + wrapper.setProperties(extra); + + Assertions.assertEquals("Tomas", map.get("name")); + Assertions.assertEquals("Beijing", map.get("city")); + } + + @Test + void shouldTreatNullOrEmptyPropertiesAsNoOp() { + Map map = sampleMap(); + MapBeanWrapper wrapper = new MapBeanWrapper(map); + + wrapper.setProperties(null); + wrapper.setProperties(Collections.emptyMap()); + + Assertions.assertEquals(3, map.size()); + } + + @Test + void shouldReportPropertyNames() { + MapBeanWrapper wrapper = new MapBeanWrapper(sampleMap()); + + Assertions.assertTrue(wrapper.getPropertyNames().contains("name")); + Assertions.assertTrue(wrapper.getPropertyNames().contains("age")); + Assertions.assertTrue(wrapper.getPropertyNames().contains("active")); + } + + @Test + void shouldExposeUnmodifiablePropertyNames() { + MapBeanWrapper wrapper = new MapBeanWrapper(sampleMap()); + + Assertions.assertThrows(UnsupportedOperationException.class, () -> wrapper.getPropertyNames() + .add("evil")); + } + + @Test + void shouldReportContainsProperty() { + MapBeanWrapper wrapper = new MapBeanWrapper(sampleMap()); + + Assertions.assertTrue(wrapper.containsProperty("name")); + Assertions.assertFalse(wrapper.containsProperty("nope")); + } + + @Test + void shouldReportPropertySize() { + Map map = sampleMap(); + MapBeanWrapper wrapper = new MapBeanWrapper(map); + + Assertions.assertEquals(map.size(), wrapper.getPropertySize()); + } + + @Test + void shouldInferPropertyTypeFromValue() { + Map map = sampleMap(); + map.put("nullable", null); + MapBeanWrapper wrapper = new MapBeanWrapper(map); + + Assertions.assertEquals(String.class, wrapper.getPropertyType("name")); + Assertions.assertEquals(Integer.class, wrapper.getPropertyType("age")); + Assertions.assertEquals(Boolean.class, wrapper.getPropertyType("active")); + Assertions.assertNull(wrapper.getPropertyType("nullable")); + Assertions.assertNull(wrapper.getPropertyType("nope")); + } + + @Test + void shouldUnwrapOriginalMap() { + Map map = sampleMap(); + MapBeanWrapper wrapper = new MapBeanWrapper(map); + + Assertions.assertSame(map, wrapper.unwrap()); + } + + @Test + void shouldReflectLiveKeyChanges() { + Map map = sampleMap(); + MapBeanWrapper wrapper = new MapBeanWrapper(map); + + Assertions.assertFalse(wrapper.containsProperty("newKey")); + + wrapper.setProperty("newKey", "value"); + + Assertions.assertTrue(wrapper.containsProperty("newKey")); + Assertions.assertTrue(wrapper.getPropertyNames().contains("newKey")); + Assertions.assertEquals(map.size(), wrapper.getPropertySize()); + } + + @Test + void shouldReturnWrappedClass() { + Map map = sampleMap(); + MapBeanWrapper wrapper = new MapBeanWrapper(map); + + Assertions.assertEquals(map.getClass(), wrapper.getWrappedClass()); + } +} diff --git a/fesod-sheet/pom.xml b/fesod-sheet/pom.xml index 5c26670ef..f688e78c4 100644 --- a/fesod-sheet/pom.xml +++ b/fesod-sheet/pom.xml @@ -55,7 +55,7 @@ org.apache.fesod - fesod-shaded + fesod-beans-cglib ${project.version} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java index fd118305d..ef020107a 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java @@ -35,6 +35,7 @@ import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.MapUtils; +import org.apache.fesod.common.beans.BeanWrapper; import org.apache.fesod.common.util.ListUtils; import org.apache.fesod.common.util.StringUtils; import org.apache.fesod.sheet.enums.HeaderMergeStrategy; @@ -363,7 +364,7 @@ private void addOneRowOfHeadDataToExcel( Head head = entry.getValue(); int columnIndex = entry.getKey(); ExcelContentProperty excelContentProperty = ClassUtils.declaredExcelContentProperty( - null, + (BeanWrapper) null, currentWriteHolder.excelWriteHeadProperty().getHeadClazz(), head.getFieldName(), currentWriteHolder); diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/listener/ModelBuildEventListener.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/listener/ModelBuildEventListener.java index 746f4c998..9c8388f11 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/listener/ModelBuildEventListener.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/listener/ModelBuildEventListener.java @@ -29,8 +29,9 @@ import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Map; +import org.apache.fesod.common.beans.BeanWrapper; +import org.apache.fesod.common.beans.BeanWrappers; import org.apache.fesod.common.util.MapUtils; -import org.apache.fesod.shaded.cglib.beans.BeanMap; import org.apache.fesod.sheet.context.AnalysisContext; import org.apache.fesod.sheet.enums.CellDataTypeEnum; import org.apache.fesod.sheet.enums.HeadKindEnum; @@ -41,7 +42,6 @@ import org.apache.fesod.sheet.metadata.data.ReadCellData; import org.apache.fesod.sheet.read.metadata.holder.ReadSheetHolder; import org.apache.fesod.sheet.read.metadata.property.ExcelReadHeadProperty; -import org.apache.fesod.sheet.util.BeanMapUtils; import org.apache.fesod.sheet.util.ClassUtils; import org.apache.fesod.sheet.util.ConverterUtils; import org.apache.fesod.sheet.util.DateUtils; @@ -176,7 +176,7 @@ private Object buildUserModel( e); } Map headMap = excelReadHeadProperty.getHeadMap(); - BeanMap dataMap = BeanMapUtils.create(resultModel); + BeanWrapper dataWrapper = BeanWrappers.create(resultModel); for (Map.Entry entry : headMap.entrySet()) { Integer index = entry.getKey(); Head head = entry.getValue(); @@ -189,7 +189,7 @@ private Object buildUserModel( cellData, head.getField(), ClassUtils.declaredExcelContentProperty( - dataMap, + dataWrapper, readSheetHolder.excelReadHeadProperty().getHeadClazz(), fieldName, readSheetHolder), @@ -198,11 +198,11 @@ private Object buildUserModel( context.readRowHolder().getRowIndex(), index); if (value != null) { - dataMap.put(fieldName, value); + dataWrapper.setProperty(fieldName, value); // 规避由于实体类 setter 不规范导致无法赋值的问题 - if (dataMap.get(fieldName) == null) { - Object bean = dataMap.getBean(); + if (dataWrapper.getProperty(fieldName) == null) { + Object bean = dataWrapper.unwrap(); try { Field field = bean.getClass().getDeclaredField(fieldName); field.setAccessible(true); diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/BeanMapUtils.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/BeanMapUtils.java deleted file mode 100644 index 57be51235..000000000 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/BeanMapUtils.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/* - * This file is part of the Apache Fesod (Incubating) project, which was derived from Alibaba EasyExcel. - * - * Copyright (C) 2018-2024 Alibaba Group Holding Ltd. - */ - -package org.apache.fesod.sheet.util; - -import org.apache.fesod.shaded.cglib.beans.BeanMap; -import org.apache.fesod.shaded.cglib.core.DefaultNamingPolicy; - -/** - * bean utils - * - * - */ -public class BeanMapUtils { - - /** - * Helper method to create a new BeanMap. For finer - * control over the generated instance, use a new instance of - * BeanMap.Generator instead of this static method. - * - * Custom naming policy to prevent null pointer exceptions. - * - * @param bean the JavaBean underlying the map - * @return a new BeanMap instance - */ - public static BeanMap create(Object bean) { - BeanMap.Generator gen = new BeanMap.Generator(); - gen.setBean(bean); - gen.setContextClass(bean.getClass()); - gen.setNamingPolicy(FesodSheetNamingPolicy.INSTANCE); - return gen.create(); - } - - public static class FesodSheetNamingPolicy extends DefaultNamingPolicy { - public static final FesodSheetNamingPolicy INSTANCE = new FesodSheetNamingPolicy(); - - @Override - protected String getTag() { - return "ByFesodCGLIB"; - } - } -} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ClassUtils.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ClassUtils.java index 980fc8548..6c4134337 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ClassUtils.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ClassUtils.java @@ -45,9 +45,11 @@ import lombok.Getter; import lombok.Setter; import org.apache.commons.collections4.CollectionUtils; +import org.apache.fesod.common.beans.BeanWrapper; +import org.apache.fesod.common.beans.BeanWrappers; +import org.apache.fesod.common.beans.MapBeanWrapper; import org.apache.fesod.common.util.ListUtils; import org.apache.fesod.common.util.MapUtils; -import org.apache.fesod.shaded.cglib.beans.BeanMap; import org.apache.fesod.sheet.annotation.ExcelIgnore; import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated; import org.apache.fesod.sheet.annotation.ExcelProperty; @@ -109,16 +111,28 @@ public class ClassUtils { * @param dataMap * @param headClazz * @param fieldName - * @return + * @deprecated use {@link ClassUtils#declaredExcelContentProperty(BeanWrapper, Class, String, ConfigurationHolder)} */ + @Deprecated public static ExcelContentProperty declaredExcelContentProperty( Map dataMap, Class headClazz, String fieldName, ConfigurationHolder configurationHolder) { + BeanWrapper beanWrapper = BeanWrappers.create(dataMap); + return declaredExcelContentProperty(beanWrapper, headClazz, fieldName, configurationHolder); + } + + /** + * Calculate the configuration information for the class + * + * @param dataWrapper + * @param headClazz + * @param fieldName + * @return + */ + public static ExcelContentProperty declaredExcelContentProperty( + BeanWrapper dataWrapper, Class headClazz, String fieldName, ConfigurationHolder configurationHolder) { Class clazz = null; - if (dataMap instanceof BeanMap) { - Object bean = ((BeanMap) dataMap).getBean(); - if (bean != null) { - clazz = bean.getClass(); - } + if (dataWrapper != null && !(dataWrapper instanceof MapBeanWrapper)) { + clazz = dataWrapper.getWrappedClass(); } return getExcelContentProperty(clazz, headClazz, fieldName, configurationHolder); } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/FieldUtils.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/FieldUtils.java index f80accc6d..5a35b6c0f 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/FieldUtils.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/FieldUtils.java @@ -28,9 +28,10 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Map; +import org.apache.fesod.common.beans.BeanWrapper; +import org.apache.fesod.common.beans.BeanWrappers; import org.apache.fesod.common.util.MemberUtils; import org.apache.fesod.common.util.StringUtils; -import org.apache.fesod.shaded.cglib.beans.BeanMap; import org.apache.fesod.sheet.metadata.NullObject; public class FieldUtils { @@ -39,9 +40,18 @@ public class FieldUtils { private static final int START_RESOLVE_FIELD_LENGTH = 2; + /** + * @deprecated use {@link FieldUtils#getFieldClass(BeanWrapper, String, Object)} + */ + @Deprecated public static Class getFieldClass(Map dataMap, String fieldName, Object value) { - if (dataMap instanceof BeanMap) { - Class fieldClass = ((BeanMap) dataMap).getPropertyType(fieldName); + BeanWrapper beanWrapper = BeanWrappers.create(dataMap); + return getFieldClass(beanWrapper, fieldName, value); + } + + public static Class getFieldClass(BeanWrapper beanWrapper, String fieldName, Object value) { + if (beanWrapper != null) { + Class fieldClass = beanWrapper.getPropertyType(fieldName); if (fieldClass != null) { return fieldClass; } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteAddExecutor.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteAddExecutor.java index 1d9951094..645c0fc72 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteAddExecutor.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteAddExecutor.java @@ -31,14 +31,14 @@ import java.util.Map; import java.util.Set; import org.apache.commons.collections4.CollectionUtils; -import org.apache.fesod.shaded.cglib.beans.BeanMap; +import org.apache.fesod.common.beans.BeanWrapper; +import org.apache.fesod.common.beans.BeanWrappers; import org.apache.fesod.sheet.context.WriteContext; import org.apache.fesod.sheet.enums.HeadKindEnum; import org.apache.fesod.sheet.metadata.FieldCache; import org.apache.fesod.sheet.metadata.FieldWrapper; import org.apache.fesod.sheet.metadata.Head; import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; -import org.apache.fesod.sheet.util.BeanMapUtils; import org.apache.fesod.sheet.util.ClassUtils; import org.apache.fesod.sheet.util.FieldUtils; import org.apache.fesod.sheet.util.WorkBookUtil; @@ -149,7 +149,7 @@ private void doAddBasicTypeToExcel( int dataIndex, int columnIndex) { ExcelContentProperty excelContentProperty = ClassUtils.declaredExcelContentProperty( - null, + (BeanWrapper) null, writeContext.currentWriteHolder().excelWriteHeadProperty().getHeadClazz(), head == null ? null : head.getFieldName(), writeContext.currentWriteHolder()); @@ -173,10 +173,10 @@ private void doAddBasicTypeToExcel( private void addJavaObjectToExcel(Object oneRowData, Row row, int rowIndex, int relativeRowIndex) { WriteHolder currentWriteHolder = writeContext.currentWriteHolder(); - BeanMap beanMap = BeanMapUtils.create(oneRowData); - // Bean the contains of the Map Key method with poor performance,So to create a keySet here - Set beanKeySet = new HashSet<>(beanMap.keySet()); - Set beanMapHandledSet = new HashSet<>(); + BeanWrapper beanWrapper = BeanWrappers.create(oneRowData); + // Use Set to optimize contains queries + Set beanPropertyNames = beanWrapper.getPropertyNames(); + Set handledPropertyNames = new HashSet<>(); int maxCellIndex = -1; // If it's a class it needs to be cast by type if (HeadKindEnum.CLASS.equals( @@ -187,12 +187,15 @@ private void addJavaObjectToExcel(Object oneRowData, Row row, int rowIndex, int int columnIndex = entry.getKey(); Head head = entry.getValue(); String name = head.getFieldName(); - if (!beanKeySet.contains(name)) { + if (!beanPropertyNames.contains(name)) { continue; } ExcelContentProperty excelContentProperty = ClassUtils.declaredExcelContentProperty( - beanMap, currentWriteHolder.excelWriteHeadProperty().getHeadClazz(), name, currentWriteHolder); + beanWrapper, + currentWriteHolder.excelWriteHeadProperty().getHeadClazz(), + name, + currentWriteHolder); CellWriteHandlerContext cellWriteHandlerContext = WriteHandlerUtils.createCellWriteHandlerContext( writeContext, row, @@ -209,18 +212,18 @@ private void addJavaObjectToExcel(Object oneRowData, Row row, int rowIndex, int WriteHandlerUtils.afterCellCreate(cellWriteHandlerContext); - cellWriteHandlerContext.setOriginalValue(beanMap.get(name)); + cellWriteHandlerContext.setOriginalValue(beanWrapper.getProperty(name)); cellWriteHandlerContext.setOriginalFieldClass(head.getField().getType()); converterAndSet(cellWriteHandlerContext); WriteHandlerUtils.afterCellDispose(cellWriteHandlerContext); - beanMapHandledSet.add(name); + handledPropertyNames.add(name); maxCellIndex = Math.max(maxCellIndex, columnIndex); } } // Finish - if (beanMapHandledSet.size() == beanMap.size()) { + if (handledPropertyNames.size() == beanWrapper.getPropertySize()) { return; } maxCellIndex++; @@ -230,13 +233,16 @@ private void addJavaObjectToExcel(Object oneRowData, Row row, int rowIndex, int fieldCache.getSortedFieldMap().entrySet()) { FieldWrapper field = entry.getValue(); String fieldName = field.getFieldName(); - boolean uselessData = !beanKeySet.contains(fieldName) || beanMapHandledSet.contains(fieldName); + boolean uselessData = !beanPropertyNames.contains(fieldName) || handledPropertyNames.contains(fieldName); if (uselessData) { continue; } - Object value = beanMap.get(fieldName); + Object value = beanWrapper.getProperty(fieldName); ExcelContentProperty excelContentProperty = ClassUtils.declaredExcelContentProperty( - beanMap, currentWriteHolder.excelWriteHeadProperty().getHeadClazz(), fieldName, currentWriteHolder); + beanWrapper, + currentWriteHolder.excelWriteHeadProperty().getHeadClazz(), + fieldName, + currentWriteHolder); CellWriteHandlerContext cellWriteHandlerContext = WriteHandlerUtils.createCellWriteHandlerContext( writeContext, row, @@ -255,7 +261,7 @@ private void addJavaObjectToExcel(Object oneRowData, Row row, int rowIndex, int WriteHandlerUtils.afterCellCreate(cellWriteHandlerContext); cellWriteHandlerContext.setOriginalValue(value); - cellWriteHandlerContext.setOriginalFieldClass(FieldUtils.getFieldClass(beanMap, fieldName, value)); + cellWriteHandlerContext.setOriginalFieldClass(FieldUtils.getFieldClass(beanWrapper, fieldName, value)); converterAndSet(cellWriteHandlerContext); WriteHandlerUtils.afterCellDispose(cellWriteHandlerContext); diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteFillExecutor.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteFillExecutor.java index fb743c8f1..1ebc3cf90 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteFillExecutor.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteFillExecutor.java @@ -40,6 +40,8 @@ import lombok.Getter; import lombok.Setter; import org.apache.commons.collections4.CollectionUtils; +import org.apache.fesod.common.beans.BeanWrapper; +import org.apache.fesod.common.beans.BeanWrappers; import org.apache.fesod.common.util.ListUtils; import org.apache.fesod.common.util.MapUtils; import org.apache.fesod.common.util.StringUtils; @@ -50,7 +52,6 @@ import org.apache.fesod.sheet.exception.ExcelGenerateException; import org.apache.fesod.sheet.metadata.data.WriteCellData; import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; -import org.apache.fesod.sheet.util.BeanMapUtils; import org.apache.fesod.sheet.util.ClassUtils; import org.apache.fesod.sheet.util.FieldUtils; import org.apache.fesod.sheet.util.PoiUtils; @@ -215,13 +216,8 @@ private void doFill( if (CollectionUtils.isEmpty(analysisCellList) || oneRowData == null) { return; } - Map dataMap; - if (oneRowData instanceof Map) { - dataMap = (Map) oneRowData; - } else { - dataMap = BeanMapUtils.create(oneRowData); - } - Set dataKeySet = new HashSet<>(dataMap.keySet()); + BeanWrapper beanWrapper = BeanWrappers.create(oneRowData); + Set beanPropertyNames = beanWrapper.getPropertyNames(); RowWriteHandlerContext rowWriteHandlerContext = WriteHandlerUtils.createRowWriteHandlerContext(writeContext, null, relativeRowIndex, Boolean.FALSE); @@ -240,11 +236,11 @@ private void doFill( if (analysisCell.getOnlyOneVariable()) { String variable = analysisCell.getVariableList().get(0); Object value = null; - if (dataKeySet.contains(variable)) { - value = dataMap.get(variable); + if (beanPropertyNames.contains(variable)) { + value = beanWrapper.getProperty(variable); } ExcelContentProperty excelContentProperty = ClassUtils.declaredExcelContentProperty( - dataMap, + beanWrapper, writeContext .currentWriteHolder() .excelWriteHeadProperty() @@ -255,7 +251,7 @@ private void doFill( createCell(analysisCell, fillConfig, cellWriteHandlerContext, rowWriteHandlerContext); cellWriteHandlerContext.setOriginalValue(value); - cellWriteHandlerContext.setOriginalFieldClass(FieldUtils.getFieldClass(dataMap, variable, value)); + cellWriteHandlerContext.setOriginalFieldClass(FieldUtils.getFieldClass(beanWrapper, variable, value)); converterAndSet(cellWriteHandlerContext); WriteCellData cellData = cellWriteHandlerContext.getFirstCellData(); @@ -280,11 +276,11 @@ private void doFill( for (String variable : analysisCell.getVariableList()) { cellValueBuild.append(analysisCell.getPrepareDataList().get(index++)); Object value = null; - if (dataKeySet.contains(variable)) { - value = dataMap.get(variable); + if (beanPropertyNames.contains(variable)) { + value = beanWrapper.getProperty(variable); } ExcelContentProperty excelContentProperty = ClassUtils.declaredExcelContentProperty( - dataMap, + beanWrapper, writeContext .currentWriteHolder() .excelWriteHeadProperty() @@ -292,7 +288,8 @@ private void doFill( variable, writeContext.currentWriteHolder()); cellWriteHandlerContext.setOriginalValue(value); - cellWriteHandlerContext.setOriginalFieldClass(FieldUtils.getFieldClass(dataMap, variable, value)); + cellWriteHandlerContext.setOriginalFieldClass( + FieldUtils.getFieldClass(beanWrapper, variable, value)); cellWriteHandlerContext.setExcelContentProperty(excelContentProperty); cellWriteHandlerContext.setTargetCellDataType(CellDataTypeEnum.STRING); diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/BeanMapUtilsTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/BeanMapUtilsTest.java deleted file mode 100644 index d7bdef049..000000000 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/BeanMapUtilsTest.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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.apache.fesod.sheet.util; - -import lombok.Getter; -import lombok.Setter; -import org.apache.fesod.shaded.cglib.beans.BeanMap; -import org.apache.fesod.sheet.testkit.Tags; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; - -/** - * Tests {@link BeanMapUtils} - */ -@Tag(Tags.UNIT) -class BeanMapUtilsTest { - - @Setter - @Getter - public static class TestUser { - private String name; - private int age; - } - - @Test - void test_create_Functionality() { - TestUser user = new TestUser(); - user.setName("Fesod"); - user.setAge(18); - - BeanMap beanMap = BeanMapUtils.create(user); - - Assertions.assertNotNull(beanMap); - Assertions.assertEquals("Fesod", beanMap.get("name")); - Assertions.assertEquals(18, beanMap.get("age")); - beanMap.put("name", "Fesod"); - Assertions.assertEquals("Fesod", user.getName()); - } - - @Test - void test_create_NamingPolicy() { - TestUser user = new TestUser(); - BeanMap beanMap = BeanMapUtils.create(user); - - String generatedClassName = beanMap.getClass().getName(); - - Assertions.assertTrue(generatedClassName.contains("ByFesodCGLIB")); - } - - @Test - void test_NamingPolicy_tag() { - BeanMapUtils.FesodSheetNamingPolicy policy = BeanMapUtils.FesodSheetNamingPolicy.INSTANCE; - - Assertions.assertDoesNotThrow(() -> { - java.lang.reflect.Method getTagMethod = policy.getClass().getDeclaredMethod("getTag"); - getTagMethod.setAccessible(true); - String tag = (String) getTagMethod.invoke(policy); - Assertions.assertEquals("ByFesodCGLIB", tag); - }); - } - - @Test - void test_create_null() { - Assertions.assertThrows(NullPointerException.class, () -> { - BeanMapUtils.create(null); - }); - } -} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java index 18664737a..39643ec6b 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java @@ -25,7 +25,7 @@ import java.util.Date; import java.util.List; import java.util.Map; -import org.apache.fesod.shaded.cglib.beans.BeanMap; +import org.apache.fesod.common.beans.BeanWrapper; import org.apache.fesod.sheet.annotation.ExcelIgnore; import org.apache.fesod.sheet.annotation.ExcelProperty; import org.apache.fesod.sheet.annotation.format.DateTimeFormat; @@ -265,7 +265,7 @@ void test_declaredExcelContentProperty() { Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.NONE); ExcelContentProperty property = - ClassUtils.declaredExcelContentProperty(null, FormatEntity.class, "date", writeHolder); + ClassUtils.declaredExcelContentProperty((BeanWrapper) null, FormatEntity.class, "date", writeHolder); Assertions.assertNotNull(property); Assertions.assertNotNull(property.getDateTimeFormatProperty()); @@ -278,7 +278,7 @@ void test_declaredExcelContentProperty_cache_memory() { Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.MEMORY); ExcelContentProperty property = - ClassUtils.declaredExcelContentProperty(null, FormatEntity.class, "date", writeHolder); + ClassUtils.declaredExcelContentProperty((BeanWrapper) null, FormatEntity.class, "date", writeHolder); Assertions.assertNotNull(property); Assertions.assertNotNull(property.getDateTimeFormatProperty()); @@ -291,7 +291,7 @@ void test_declaredExcelContentProperty_cache_ThreadLocal() { Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.THREAD_LOCAL); ExcelContentProperty property = - ClassUtils.declaredExcelContentProperty(null, FormatEntity.class, "date", writeHolder); + ClassUtils.declaredExcelContentProperty((BeanWrapper) null, FormatEntity.class, "date", writeHolder); Assertions.assertNotNull(property); Assertions.assertNotNull(property.getDateTimeFormatProperty()); @@ -300,15 +300,13 @@ void test_declaredExcelContentProperty_cache_ThreadLocal() { } @Test - void test_declaredExcelContentProperty_BeanMap() { - BeanMap beanMapMocked = Mockito.mock(BeanMap.class); - FormatEntity beanMocked = Mockito.mock(FormatEntity.class); - + void test_declaredExcelContentProperty_BeanWrapper() { + BeanWrapper beanWrapperMocked = Mockito.mock(BeanWrapper.class); Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.NONE); - Mockito.when(beanMapMocked.getBean()).thenReturn(beanMocked); + Mockito.doReturn(FormatEntity.class).when(beanWrapperMocked).getWrappedClass(); ExcelContentProperty property = - ClassUtils.declaredExcelContentProperty(beanMapMocked, FormatEntity.class, "date", writeHolder); + ClassUtils.declaredExcelContentProperty(beanWrapperMocked, FormatEntity.class, "date", writeHolder); Assertions.assertNotNull(property); Assertions.assertNotNull(property.getDateTimeFormatProperty()); @@ -320,8 +318,8 @@ void test_declaredExcelContentProperty_BeanMap() { void test_declaredExcelContentProperty_converter() { Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.NONE); - ExcelContentProperty property = - ClassUtils.declaredExcelContentProperty(null, FormatEntity.class, "customConvert", writeHolder); + ExcelContentProperty property = ClassUtils.declaredExcelContentProperty( + (BeanWrapper) null, FormatEntity.class, "customConvert", writeHolder); Assertions.assertNotNull(property); Assertions.assertNotNull(property.getConverter()); diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/FieldUtilsTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/FieldUtilsTest.java index e72a27362..ab7ec4583 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/FieldUtilsTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/FieldUtilsTest.java @@ -23,7 +23,8 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; -import org.apache.fesod.shaded.cglib.beans.BeanMap; +import org.apache.fesod.common.beans.BeanWrapper; +import org.apache.fesod.common.beans.MapBeanWrapper; import org.apache.fesod.sheet.metadata.NullObject; import org.apache.fesod.sheet.testkit.Tags; import org.junit.jupiter.api.Assertions; @@ -42,34 +43,38 @@ class FieldUtilsTest { @Test void test_getFieldClass_normalValue() { - Class clazz = FieldUtils.getFieldClass(null, "any", "StringValue"); + Class clazz1 = FieldUtils.getFieldClass((Map) null, "any", "StringValue"); + Class clazz2 = FieldUtils.getFieldClass((BeanWrapper) null, "any", "StringValue"); - Assertions.assertEquals(String.class, clazz); + Assertions.assertEquals(String.class, clazz1); + Assertions.assertEquals(String.class, clazz2); } @Test void test_getFieldClass_nullValue() { - Class clazz = FieldUtils.getFieldClass(null, "any", null); + Class clazz1 = FieldUtils.getFieldClass((Map) null, "any", null); + Class clazz2 = FieldUtils.getFieldClass((BeanWrapper) null, "any", null); - Assertions.assertEquals(NullObject.class, clazz); + Assertions.assertEquals(NullObject.class, clazz1); + Assertions.assertEquals(NullObject.class, clazz2); } @Test - void test_getFieldClass_BeanMap() { - BeanMap mockBeanMap = Mockito.mock(BeanMap.class); - Mockito.when(mockBeanMap.getPropertyType("name")).thenReturn(Integer.class); + void test_getFieldClass_BeanWrapper() { + BeanWrapper mockBeanWrapper = Mockito.mock(BeanWrapper.class); + Mockito.doReturn(Integer.class).when(mockBeanWrapper).getPropertyType("name"); - Class clazz = FieldUtils.getFieldClass(mockBeanMap, "name", "123"); + Class clazz = FieldUtils.getFieldClass(mockBeanWrapper, "name", "123"); Assertions.assertEquals(Integer.class, clazz); } @Test - void test_getFieldClass_BeanMap_fallback() { - BeanMap mockBeanMap = Mockito.mock(BeanMap.class); - Mockito.when(mockBeanMap.getPropertyType("unknown")).thenReturn(null); + void test_getFieldClass_BeanWrapper_fallback() { + BeanWrapper mockBeanWrapper = Mockito.mock(BeanWrapper.class); + Mockito.when(mockBeanWrapper.getPropertyType("unknown")).thenReturn(null); - Class clazz = FieldUtils.getFieldClass(mockBeanMap, "unknown", "Value"); + Class clazz = FieldUtils.getFieldClass(mockBeanWrapper, "unknown", "Value"); Assertions.assertEquals(String.class, clazz); } @@ -82,6 +87,14 @@ void test_getFieldClass_normalMap() { Assertions.assertEquals(Long.class, clazz); } + @Test + void test_getFieldClass_normalBeanWrapper() { + MapBeanWrapper beanWrapper = new MapBeanWrapper(new HashMap<>()); + Class clazz = FieldUtils.getFieldClass(beanWrapper, "any", 100L); + + Assertions.assertEquals(Long.class, clazz); + } + @Test void test_resolveCglibFieldName_nullField() { Assertions.assertNull(FieldUtils.resolveCglibFieldName(null)); diff --git a/pom.xml b/pom.xml index c4bcff65f..ba5a1f984 100644 --- a/pom.xml +++ b/pom.xml @@ -77,6 +77,7 @@ fesod-common fesod-shaded fesod-examples + fesod-beans fesod-sheet