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.
+ *
+ *
+ * - Standard accessors: {@code getName()} / {@code setName(String)}.
+ * - Fluent accessors: {@code name()} / {@code name(String)} / {@code T name(String)}.
+ *
+ */
+@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