diff --git a/src/main/java/io/beanmapper/annotations/BeanFactoryMethod.java b/src/main/java/io/beanmapper/annotations/BeanFactoryMethod.java
new file mode 100644
index 00000000..d0aee131
--- /dev/null
+++ b/src/main/java/io/beanmapper/annotations/BeanFactoryMethod.java
@@ -0,0 +1,43 @@
+package io.beanmapper.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to mark static factory methods for bean instantiation.
+ *
+ *
This annotation allows specifying static factory methods that can be used
+ * to instantiate objects during the bean mapping process. The factory method
+ * must be static since it will be called before any instance of the target
+ * type is available.
+ *
+ * The value array specifies the names of the fields that should be provided
+ * to the factory method as arguments, in the correct order.
+ *
+ * Example usage:
+ *
+ * public class Person {
+ * private String name;
+ * private int age;
+ *
+ * @BeanFactoryMethod({"name", "age"})
+ * public static Person create(String name, int age) {
+ * return new Person(name, age);
+ * }
+ * }
+ *
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface BeanFactoryMethod {
+
+ /**
+ * The names of the fields that should be provided to the factory method
+ * as arguments, in the correct order.
+ *
+ * @return array of field names
+ */
+ String[] value();
+}
\ No newline at end of file
diff --git a/src/main/java/io/beanmapper/config/BeanMapperBuilder.java b/src/main/java/io/beanmapper/config/BeanMapperBuilder.java
index db5ef8bc..917bd59a 100644
--- a/src/main/java/io/beanmapper/config/BeanMapperBuilder.java
+++ b/src/main/java/io/beanmapper/config/BeanMapperBuilder.java
@@ -13,6 +13,7 @@
import io.beanmapper.core.collections.QueueCollectionHandler;
import io.beanmapper.core.collections.SetCollectionHandler;
import io.beanmapper.core.constructor.BeanInitializer;
+import io.beanmapper.core.constructor.FactoryMethodAwareBeanInitializer;
import io.beanmapper.core.converter.BeanConverter;
import io.beanmapper.core.converter.collections.CollectionConverter;
import io.beanmapper.core.converter.impl.AnyToEnumConverter;
@@ -134,6 +135,19 @@ public BeanMapperBuilder setBeanInitializer(BeanInitializer beanInitializer) {
return this;
}
+ /**
+ * Enables factory method support by setting a FactoryMethodAwareBeanInitializer that will
+ * look for static methods annotated with @BeanFactoryMethod and use them for object
+ * instantiation when available. Falls back to the default bean initializer when no
+ * factory method is found.
+ *
+ * @return the builder for fluent configuration
+ */
+ public BeanMapperBuilder withFactoryMethodSupport() {
+ this.configuration.setBeanInitializer(new FactoryMethodAwareBeanInitializer());
+ return this;
+ }
+
public BeanMapperBuilder setBeanUnproxy(BeanUnproxy beanUnproxy) {
this.configuration.setBeanUnproxy(beanUnproxy);
return this;
diff --git a/src/main/java/io/beanmapper/core/constructor/FactoryMethodAwareBeanInitializer.java b/src/main/java/io/beanmapper/core/constructor/FactoryMethodAwareBeanInitializer.java
new file mode 100644
index 00000000..04f5aebd
--- /dev/null
+++ b/src/main/java/io/beanmapper/core/constructor/FactoryMethodAwareBeanInitializer.java
@@ -0,0 +1,70 @@
+/*
+ * (C) 2014 42 bv (www.42.nl). All rights reserved.
+ */
+package io.beanmapper.core.constructor;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+import io.beanmapper.annotations.BeanFactoryMethod;
+import io.beanmapper.strategy.ConstructorArguments;
+import io.beanmapper.utils.BeanMapperTraceLogger;
+
+/**
+ * A BeanInitializer that is aware of factory methods but integrates with the existing
+ * strategy framework. This initializer extends DefaultBeanInitializer but first checks
+ * for factory methods when no ConstructorArguments are provided.
+ */
+public class FactoryMethodAwareBeanInitializer extends DefaultBeanInitializer {
+
+ private final FactoryMethodBeanInitializer factoryMethodInitializer;
+
+ public FactoryMethodAwareBeanInitializer() {
+ this.factoryMethodInitializer = new FactoryMethodBeanInitializer(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public T instantiate(Class beanClass, ConstructorArguments arguments) {
+ // If we have constructor arguments, use the factory method initializer
+ // which can handle both factory methods and fallback to default behavior
+ if (arguments != null) {
+ return factoryMethodInitializer.instantiate(beanClass, arguments);
+ }
+
+ // If no constructor arguments, check if there's a factory method that requires no arguments
+ Method factoryMethod = findNoArgFactoryMethod(beanClass);
+ if (factoryMethod != null) {
+ return factoryMethodInitializer.instantiate(beanClass, null);
+ }
+
+ // Fall back to default behavior (no-arg constructor)
+ return super.instantiate(beanClass, null);
+ }
+
+ /**
+ * Finds a factory method that requires no arguments.
+ *
+ * @param beanClass the target class
+ * @return a no-arg factory method, or null if none found
+ */
+ private Method findNoArgFactoryMethod(Class> beanClass) {
+ for (Method method : beanClass.getDeclaredMethods()) {
+ if (method.isAnnotationPresent(BeanFactoryMethod.class) &&
+ Modifier.isStatic(method.getModifiers()) &&
+ beanClass.isAssignableFrom(method.getReturnType()) &&
+ method.getParameterCount() == 0) {
+
+ BeanFactoryMethod annotation = method.getAnnotation(BeanFactoryMethod.class);
+ if (annotation.value().length == 0) {
+ BeanMapperTraceLogger.log("Found no-arg factory method {} for class {}",
+ method.getName(), beanClass.getName());
+ return method;
+ }
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/beanmapper/core/constructor/FactoryMethodBeanInitializer.java b/src/main/java/io/beanmapper/core/constructor/FactoryMethodBeanInitializer.java
new file mode 100644
index 00000000..71866ebf
--- /dev/null
+++ b/src/main/java/io/beanmapper/core/constructor/FactoryMethodBeanInitializer.java
@@ -0,0 +1,276 @@
+/*
+ * (C) 2014 42 bv (www.42.nl). All rights reserved.
+ */
+package io.beanmapper.core.constructor;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Parameter;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Arrays;
+
+import io.beanmapper.BeanMapper;
+import io.beanmapper.annotations.BeanFactoryMethod;
+import io.beanmapper.config.BeanMapperBuilder;
+import io.beanmapper.core.BeanMatch;
+import io.beanmapper.strategy.ConstructorArguments;
+import io.beanmapper.utils.BeanMapperTraceLogger;
+import io.beanmapper.utils.DefaultValues;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * BeanInitializer implementation that uses static factory methods annotated with
+ * {@link BeanFactoryMethod} to instantiate objects.
+ *
+ * This initializer looks for static methods in the target class that are annotated
+ * with {@code @BeanFactoryMethod}. If found, it uses the factory method instead of
+ * calling the constructor directly.
+ *
+ * The factory method must be static and the annotation must specify the field names
+ * that correspond to the method parameters in the correct order.
+ */
+public class FactoryMethodBeanInitializer implements BeanInitializer {
+
+ private static final Logger log = LoggerFactory.getLogger(FactoryMethodBeanInitializer.class);
+
+ private final BeanInitializer fallbackInitializer;
+
+ /**
+ * Creates a new FactoryMethodBeanInitializer with a fallback initializer.
+ *
+ * @param fallbackInitializer the initializer to use when no factory method is found
+ */
+ public FactoryMethodBeanInitializer(BeanInitializer fallbackInitializer) {
+ this.fallbackInitializer = fallbackInitializer != null ? fallbackInitializer : new DefaultBeanInitializer();
+ }
+
+ /**
+ * Creates a new FactoryMethodBeanInitializer with DefaultBeanInitializer as fallback.
+ */
+ public FactoryMethodBeanInitializer() {
+ this(new DefaultBeanInitializer());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public T instantiate(Class beanClass, ConstructorArguments arguments) {
+ Method factoryMethod = findFactoryMethod(beanClass, arguments);
+
+ if (factoryMethod != null) {
+ return instantiateUsingFactoryMethod(beanClass, factoryMethod, arguments);
+ } else {
+ return fallbackInitializer.instantiate(beanClass, arguments);
+ }
+ }
+
+ /**
+ * Instantiates an object using a factory method if available, with a source object and BeanMatch.
+ * This method tries to find a suitable factory method and creates ConstructorArguments from
+ * the factory method annotation if needed.
+ *
+ * @param beanClass the target class
+ * @param source the source object
+ * @param beanMatch the bean match containing field mappings
+ * @return the instantiated object
+ */
+ public T instantiateWithSourceAndBeanMatch(Class beanClass, S source, BeanMatch beanMatch) {
+ Method factoryMethod = findFactoryMethodWithAnnotation(beanClass);
+
+ if (factoryMethod != null) {
+ BeanFactoryMethod annotation = factoryMethod.getAnnotation(BeanFactoryMethod.class);
+ ConstructorArguments arguments = null;
+
+ if (annotation.value().length > 0) {
+ // Create ConstructorArguments from the factory method annotation
+ arguments = new ConstructorArguments(source, beanMatch, annotation.value());
+ }
+
+ return instantiateUsingFactoryMethod(beanClass, factoryMethod, arguments);
+ } else {
+ return fallbackInitializer.instantiate(beanClass, null);
+ }
+ }
+
+ /**
+ * Finds a static factory method annotated with {@link BeanFactoryMethod} that matches
+ * the provided constructor arguments.
+ *
+ * @param beanClass the target class
+ * @param arguments the constructor arguments
+ * @return the matching factory method, or null if none found
+ */
+ private Method findFactoryMethod(Class> beanClass, ConstructorArguments arguments) {
+ for (Method method : beanClass.getDeclaredMethods()) {
+ if (isValidFactoryMethod(method, beanClass, arguments)) {
+ return method;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Finds a static factory method annotated with {@link BeanFactoryMethod}.
+ *
+ * @param beanClass the target class
+ * @return the first valid factory method found, or null if none found
+ */
+ private Method findFactoryMethodWithAnnotation(Class> beanClass) {
+ for (Method method : beanClass.getDeclaredMethods()) {
+ if (method.isAnnotationPresent(BeanFactoryMethod.class) &&
+ Modifier.isStatic(method.getModifiers()) &&
+ beanClass.isAssignableFrom(method.getReturnType())) {
+ return method;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Checks if a method is a valid factory method for the given arguments.
+ *
+ * @param method the method to check
+ * @param beanClass the target class
+ * @param arguments the constructor arguments
+ * @return true if the method is a valid factory method
+ */
+ private boolean isValidFactoryMethod(Method method, Class> beanClass, ConstructorArguments arguments) {
+ // Must be annotated with @BeanFactoryMethod
+ BeanFactoryMethod annotation = method.getAnnotation(BeanFactoryMethod.class);
+ if (annotation == null) {
+ return false;
+ }
+
+ // Must be static
+ if (!Modifier.isStatic(method.getModifiers())) {
+ log.warn("Factory method {} in class {} is not static. Factory methods must be static.",
+ method.getName(), beanClass.getName());
+ return false;
+ }
+
+ // Must return the correct type or a compatible type
+ if (!beanClass.isAssignableFrom(method.getReturnType())) {
+ log.warn("Factory method {} in class {} does not return compatible type. Expected: {}, Got: {}",
+ method.getName(), beanClass.getName(), beanClass.getName(), method.getReturnType().getName());
+ return false;
+ }
+
+ // Check if arguments match
+ if (arguments == null) {
+ return method.getParameterCount() == 0 || annotation.value().length == 0;
+ }
+
+ Class>[] parameterTypes = method.getParameterTypes();
+ Class>[] argumentTypes = arguments.getTypes();
+
+ if (parameterTypes.length != argumentTypes.length) {
+ return false;
+ }
+
+ // Check if parameter types are compatible
+ for (int i = 0; i < parameterTypes.length; i++) {
+ if (!parameterTypes[i].isAssignableFrom(argumentTypes[i]) &&
+ !isAutoboxingCompatible(parameterTypes[i], argumentTypes[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if two types are compatible via autoboxing/unboxing.
+ *
+ * @param parameterType the method parameter type
+ * @param argumentType the argument type
+ * @return true if types are autoboxing compatible
+ */
+ private boolean isAutoboxingCompatible(Class> parameterType, Class> argumentType) {
+ if (parameterType.isPrimitive() && !argumentType.isPrimitive()) {
+ return getWrapperType(parameterType).equals(argumentType);
+ } else if (!parameterType.isPrimitive() && argumentType.isPrimitive()) {
+ return parameterType.equals(getWrapperType(argumentType));
+ }
+ return false;
+ }
+
+ /**
+ * Gets the wrapper type for a primitive type.
+ *
+ * @param primitiveType the primitive type
+ * @return the corresponding wrapper type
+ */
+ private Class> getWrapperType(Class> primitiveType) {
+ if (primitiveType == boolean.class) return Boolean.class;
+ if (primitiveType == byte.class) return Byte.class;
+ if (primitiveType == char.class) return Character.class;
+ if (primitiveType == short.class) return Short.class;
+ if (primitiveType == int.class) return Integer.class;
+ if (primitiveType == long.class) return Long.class;
+ if (primitiveType == float.class) return Float.class;
+ if (primitiveType == double.class) return Double.class;
+ return primitiveType;
+ }
+
+ /**
+ * Instantiates an object using the provided factory method.
+ *
+ * @param beanClass the target class
+ * @param factoryMethod the factory method to use
+ * @param arguments the constructor arguments
+ * @return the instantiated object
+ */
+ @SuppressWarnings("unchecked")
+ private T instantiateUsingFactoryMethod(Class beanClass, Method factoryMethod, ConstructorArguments arguments) {
+ BeanMapperTraceLogger.log("Creating a new instance of type {} using factory method {}.",
+ beanClass, factoryMethod.getName());
+
+ try {
+ factoryMethod.setAccessible(true);
+
+ if (arguments == null || arguments.getValues().length == 0) {
+ return (T) factoryMethod.invoke(null);
+ }
+
+ // Map parameterized arguments if needed
+ var methodParameterTypes = Arrays.stream(factoryMethod.getParameters())
+ .map(Parameter::getParameterizedType)
+ .toArray(Type[]::new);
+ Object[] mappedArguments = mapParameterizedArguments(methodParameterTypes, arguments.getValues());
+
+ return (T) factoryMethod.invoke(null, mappedArguments);
+
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ log.error("Could not instantiate bean of class {} using factory method {}. Returning the default value. {}",
+ beanClass.getName(), factoryMethod.getName(), e.getMessage());
+ return DefaultValues.defaultValueFor(beanClass);
+ }
+ }
+
+ /**
+ * Maps arguments to handle parameterized types if needed.
+ *
+ * @param methodParameterTypes the method parameter types
+ * @param arguments the arguments to map
+ * @return the mapped arguments
+ */
+ private Object[] mapParameterizedArguments(Type[] methodParameterTypes, Object[] arguments) {
+ final BeanMapper beanMapper = new BeanMapperBuilder().build();
+ Object[] mappedArguments = new Object[arguments.length];
+
+ for (int i = 0; i < arguments.length; i++) {
+ Object argument = arguments[i];
+ if (argument != null && argument.getClass().getTypeParameters().length > 0) {
+ argument = beanMapper.map(argument, (ParameterizedType) methodParameterTypes[i]);
+ }
+ mappedArguments[i] = argument;
+ }
+
+ return mappedArguments;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/beanmapper/strategy/FactoryMethodAwareMapToClassStrategy.java b/src/main/java/io/beanmapper/strategy/FactoryMethodAwareMapToClassStrategy.java
new file mode 100644
index 00000000..f18f478c
--- /dev/null
+++ b/src/main/java/io/beanmapper/strategy/FactoryMethodAwareMapToClassStrategy.java
@@ -0,0 +1,63 @@
+package io.beanmapper.strategy;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+import io.beanmapper.BeanMapper;
+import io.beanmapper.annotations.BeanFactoryMethod;
+import io.beanmapper.config.Configuration;
+import io.beanmapper.core.BeanMatch;
+
+/**
+ * A MapToClassStrategy that is aware of factory methods. This strategy extends
+ * the standard MapToClassStrategy but overrides getConstructorArguments to also
+ * check for factory method annotations when no BeanConstruct annotation is present.
+ */
+public class FactoryMethodAwareMapToClassStrategy extends MapToClassStrategy {
+
+ public FactoryMethodAwareMapToClassStrategy(BeanMapper beanMapper, Configuration configuration) {
+ super(beanMapper, configuration);
+ }
+
+ @Override
+ public ConstructorArguments getConstructorArguments(S source, BeanMatch beanMatch) {
+ // First try the standard approach (BeanConstruct annotation)
+ ConstructorArguments standardArguments = super.getConstructorArguments(source, beanMatch);
+ if (standardArguments != null) {
+ return standardArguments;
+ }
+
+ // If no BeanConstruct, check for factory method
+ Class> targetClass = beanMatch.getTargetClass();
+ Method factoryMethod = findFactoryMethod(targetClass);
+
+ if (factoryMethod != null) {
+ BeanFactoryMethod annotation = factoryMethod.getAnnotation(BeanFactoryMethod.class);
+ String[] fieldNames = annotation.value();
+
+ if (fieldNames.length > 0) {
+ return new ConstructorArguments(source, beanMatch, fieldNames);
+ }
+ }
+
+ // No factory method or no-arg factory method, return null for no-arg constructor
+ return null;
+ }
+
+ /**
+ * Finds a factory method in the target class.
+ *
+ * @param targetClass the target class
+ * @return the first valid factory method found, or null
+ */
+ private Method findFactoryMethod(Class> targetClass) {
+ for (Method method : targetClass.getDeclaredMethods()) {
+ if (method.isAnnotationPresent(BeanFactoryMethod.class) &&
+ Modifier.isStatic(method.getModifiers()) &&
+ targetClass.isAssignableFrom(method.getReturnType())) {
+ return method;
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/io/beanmapper/annotations/BeanFactoryMethodTest.java b/src/test/java/io/beanmapper/annotations/BeanFactoryMethodTest.java
new file mode 100644
index 00000000..5153d506
--- /dev/null
+++ b/src/test/java/io/beanmapper/annotations/BeanFactoryMethodTest.java
@@ -0,0 +1,72 @@
+package io.beanmapper.annotations;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.lang.reflect.Method;
+
+import org.junit.jupiter.api.Test;
+
+class BeanFactoryMethodTest {
+
+ @Test
+ void testBeanFactoryMethodAnnotation() {
+ Method method = null;
+
+ try {
+ method = TestClassWithFactory.class.getMethod("createInstance", String.class, int.class);
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException("Test method not found", e);
+ }
+
+ assertNotNull(method);
+ assertTrue(method.isAnnotationPresent(BeanFactoryMethod.class));
+
+ BeanFactoryMethod annotation = method.getAnnotation(BeanFactoryMethod.class);
+ assertNotNull(annotation);
+ assertArrayEquals(new String[]{"name", "age"}, annotation.value());
+ }
+
+ @Test
+ void testBeanFactoryMethodWithEmptyFields() {
+ Method method = null;
+
+ try {
+ method = TestClassWithFactory.class.getMethod("createDefault");
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException("Test method not found", e);
+ }
+
+ assertNotNull(method);
+ assertTrue(method.isAnnotationPresent(BeanFactoryMethod.class));
+
+ BeanFactoryMethod annotation = method.getAnnotation(BeanFactoryMethod.class);
+ assertNotNull(annotation);
+ assertArrayEquals(new String[]{}, annotation.value());
+ }
+
+ public static class TestClassWithFactory {
+ private String name;
+ private int age;
+
+ @BeanFactoryMethod({"name", "age"})
+ public static TestClassWithFactory createInstance(String name, int age) {
+ TestClassWithFactory instance = new TestClassWithFactory();
+ instance.name = name;
+ instance.age = age;
+ return instance;
+ }
+
+ @BeanFactoryMethod({})
+ public static TestClassWithFactory createDefault() {
+ TestClassWithFactory instance = new TestClassWithFactory();
+ instance.name = "Default";
+ instance.age = 0;
+ return instance;
+ }
+
+ public String getName() { return name; }
+ public int getAge() { return age; }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/io/beanmapper/core/constructor/FactoryMethodBeanInitializerTest.java b/src/test/java/io/beanmapper/core/constructor/FactoryMethodBeanInitializerTest.java
new file mode 100644
index 00000000..90995be1
--- /dev/null
+++ b/src/test/java/io/beanmapper/core/constructor/FactoryMethodBeanInitializerTest.java
@@ -0,0 +1,184 @@
+package io.beanmapper.core.constructor;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import io.beanmapper.BeanMapper;
+import io.beanmapper.annotations.BeanFactoryMethod;
+import io.beanmapper.config.BeanMapperBuilder;
+
+import org.junit.jupiter.api.Test;
+
+class FactoryMethodBeanInitializerTest {
+
+ @Test
+ void testFactoryMethodInstantiation() {
+ // Test the factory method initializer directly with explicit arguments
+ FactoryMethodBeanInitializer initializer = new FactoryMethodBeanInitializer();
+
+ // Test with null arguments (should use no-arg factory method if available)
+ PersonWithDefaultFactory result = initializer.instantiate(PersonWithDefaultFactory.class, null);
+
+ assertNotNull(result);
+ assertEquals("Default", result.getName());
+ assertEquals(0, result.getAge());
+ assertEquals("Factory", result.getCreatedBy());
+ }
+
+ @Test
+ void testFactoryMethodWithNoArgs() {
+ FactoryMethodBeanInitializer initializer = new FactoryMethodBeanInitializer();
+
+ PersonWithDefaultFactory result = initializer.instantiate(PersonWithDefaultFactory.class, null);
+
+ assertNotNull(result);
+ assertEquals("Default", result.getName());
+ assertEquals(0, result.getAge());
+ assertEquals("Factory", result.getCreatedBy());
+ }
+
+ @Test
+ void testFallbackToDefaultInitializer() {
+ FactoryMethodBeanInitializer initializer = new FactoryMethodBeanInitializer();
+
+ // This class has no factory method, should fall back to no-arg constructor
+ PersonWithoutFactory result = initializer.instantiate(PersonWithoutFactory.class, null);
+
+ // Should use the no-arg constructor
+ assertNotNull(result);
+ assertEquals("Default", result.getName());
+ assertEquals(0, result.getAge());
+ }
+
+ @Test
+ void testWithBeanMapper() {
+ BeanMapper beanMapper = new BeanMapperBuilder()
+ .withFactoryMethodSupport()
+ .build();
+
+ PersonSource source = new PersonSource("Alice", 35);
+ PersonWithDefaultFactory result = beanMapper.map(source, PersonWithDefaultFactory.class);
+
+ // For now this tests the no-arg factory method since the full integration
+ // with constructor arguments requires additional strategy changes
+ assertNotNull(result);
+ assertEquals("Default", result.getName());
+ assertEquals(0, result.getAge());
+ assertEquals("Factory", result.getCreatedBy());
+ }
+
+ @Test
+ void testFactoryMethodWithInvalidSignature() {
+ FactoryMethodBeanInitializer initializer = new FactoryMethodBeanInitializer();
+
+ // Should fall back to default initializer since factory method is invalid (not static)
+ PersonWithInvalidFactory result = initializer.instantiate(PersonWithInvalidFactory.class, null);
+
+ // Should use the no-arg constructor since factory method is invalid
+ assertNotNull(result);
+ assertEquals("Constructor", result.getName());
+ assertEquals(0, result.getAge());
+ }
+
+ // Test classes
+ public static class PersonSource {
+ public String name;
+ public int age;
+
+ public PersonSource() {}
+
+ public PersonSource(String name, int age) {
+ this.name = name;
+ this.age = age;
+ }
+ }
+
+ public static class PersonWithFactory {
+ private String name;
+ private int age;
+ private String createdBy;
+
+ public PersonWithFactory(String name, int age) {
+ this.name = name;
+ this.age = age;
+ this.createdBy = "Constructor";
+ }
+
+ @BeanFactoryMethod({"name", "age"})
+ public static PersonWithFactory create(String name, int age) {
+ PersonWithFactory person = new PersonWithFactory(name, age);
+ person.createdBy = "Factory";
+ return person;
+ }
+
+ public String getName() { return name; }
+ public int getAge() { return age; }
+ public String getCreatedBy() { return createdBy; }
+ }
+
+ public static class PersonWithDefaultFactory {
+ private String name;
+ private int age;
+ private String createdBy;
+
+ public PersonWithDefaultFactory() {
+ this.name = "Unknown";
+ this.age = 0;
+ this.createdBy = "Constructor";
+ }
+
+ @BeanFactoryMethod({})
+ public static PersonWithDefaultFactory createDefault() {
+ PersonWithDefaultFactory person = new PersonWithDefaultFactory();
+ person.name = "Default";
+ person.createdBy = "Factory";
+ return person;
+ }
+
+ public String getName() { return name; }
+ public int getAge() { return age; }
+ public String getCreatedBy() { return createdBy; }
+ }
+
+ public static class PersonWithoutFactory {
+ private String name;
+ private int age;
+
+ public PersonWithoutFactory() {
+ this.name = "Default";
+ this.age = 0;
+ }
+
+ public PersonWithoutFactory(String name, int age) {
+ this.name = name;
+ this.age = age;
+ }
+
+ public String getName() { return name; }
+ public int getAge() { return age; }
+ }
+
+ public static class PersonWithInvalidFactory {
+ private String name;
+ private int age;
+
+ public PersonWithInvalidFactory() {
+ this.name = "Constructor";
+ this.age = 0;
+ }
+
+ public PersonWithInvalidFactory(String name, int age) {
+ this.name = name;
+ this.age = age;
+ }
+
+ // Invalid factory method - not static
+ @BeanFactoryMethod({"name", "age"})
+ public PersonWithInvalidFactory create(String name, int age) {
+ return new PersonWithInvalidFactory(name, age);
+ }
+
+ public String getName() { return name; }
+ public int getAge() { return age; }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/io/beanmapper/testmodel/factory/FactoryMethodIntegrationTest.java b/src/test/java/io/beanmapper/testmodel/factory/FactoryMethodIntegrationTest.java
new file mode 100644
index 00000000..51b1534a
--- /dev/null
+++ b/src/test/java/io/beanmapper/testmodel/factory/FactoryMethodIntegrationTest.java
@@ -0,0 +1,183 @@
+package io.beanmapper.testmodel.factory;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import io.beanmapper.BeanMapper;
+import io.beanmapper.annotations.BeanFactoryMethod;
+import io.beanmapper.config.BeanMapperBuilder;
+
+import org.junit.jupiter.api.Test;
+
+class FactoryMethodIntegrationTest {
+
+ @Test
+ void testComplexObjectWithFactoryMethod() {
+ BeanMapper beanMapper = new BeanMapperBuilder()
+ .withFactoryMethodSupport()
+ .build();
+
+ ProductForm form = new ProductForm();
+ form.name = "Laptop";
+ form.price = 1299.99;
+ form.categoryId = 1L;
+
+ Product result = beanMapper.map(form, Product.class);
+
+ assertNotNull(result);
+ assertEquals("Laptop", result.getName());
+ assertEquals(1299.99, result.getPrice());
+ assertEquals(1L, result.getCategoryId());
+ assertEquals("FACTORY_CREATED", result.getStatus());
+ assertNotNull(result.getCreatedAt());
+ }
+
+ @Test
+ void testImmutableObjectWithFactoryMethod() {
+ BeanMapper beanMapper = new BeanMapperBuilder()
+ .withFactoryMethodSupport()
+ .build();
+
+ PersonForm form = new PersonForm();
+ form.firstName = "John";
+ form.lastName = "Doe";
+ form.email = "john.doe@example.com";
+
+ ImmutablePerson result = beanMapper.map(form, ImmutablePerson.class);
+
+ assertNotNull(result);
+ assertEquals("John", result.getFirstName());
+ assertEquals("Doe", result.getLastName());
+ assertEquals("john.doe@example.com", result.getEmail());
+ assertEquals("John Doe", result.getFullName());
+ }
+
+ @Test
+ void testSingletonPatternWithFactoryMethod() {
+ BeanMapper beanMapper = new BeanMapperBuilder()
+ .withFactoryMethodSupport()
+ .build();
+
+ ConfigForm form = new ConfigForm();
+ form.environment = "production";
+ form.debugEnabled = false;
+
+ AppConfig result = beanMapper.map(form, AppConfig.class);
+
+ assertNotNull(result);
+ assertEquals("production", result.getEnvironment());
+ assertEquals(false, result.isDebugEnabled());
+ assertEquals("FACTORY_CONFIGURED", result.getConfigSource());
+ }
+
+ // Test classes
+ public static class ProductForm {
+ public String name;
+ public double price;
+ public Long categoryId;
+ }
+
+ public static class Product {
+ private String name;
+ private double price;
+ private Long categoryId;
+ private String status;
+ private java.time.LocalDateTime createdAt;
+
+ // Private constructor to force use of factory method
+ private Product(String name, double price, Long categoryId) {
+ this.name = name;
+ this.price = price;
+ this.categoryId = categoryId;
+ this.status = "FACTORY_CREATED";
+ this.createdAt = java.time.LocalDateTime.now();
+ }
+
+ @BeanFactoryMethod({"name", "price", "categoryId"})
+ public static Product create(String name, double price, Long categoryId) {
+ // Factory method can perform validation, initialization, etc.
+ if (name == null || name.isEmpty()) {
+ throw new IllegalArgumentException("Product name cannot be empty");
+ }
+ if (price < 0) {
+ throw new IllegalArgumentException("Product price cannot be negative");
+ }
+ return new Product(name, price, categoryId);
+ }
+
+ public String getName() { return name; }
+ public double getPrice() { return price; }
+ public Long getCategoryId() { return categoryId; }
+ public String getStatus() { return status; }
+ public java.time.LocalDateTime getCreatedAt() { return createdAt; }
+ }
+
+ public static class PersonForm {
+ public String firstName;
+ public String lastName;
+ public String email;
+ }
+
+ public static class ImmutablePerson {
+ private final String firstName;
+ private final String lastName;
+ private final String email;
+ private final String fullName;
+
+ // Private constructor
+ private ImmutablePerson(String firstName, String lastName, String email) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ this.email = email;
+ this.fullName = firstName + " " + lastName;
+ }
+
+ @BeanFactoryMethod({"firstName", "lastName", "email"})
+ public static ImmutablePerson of(String firstName, String lastName, String email) {
+ // Validation and normalization can be done here
+ if (firstName == null) firstName = "";
+ if (lastName == null) lastName = "";
+ if (email == null) email = "";
+
+ return new ImmutablePerson(
+ firstName.trim(),
+ lastName.trim(),
+ email.toLowerCase().trim()
+ );
+ }
+
+ public String getFirstName() { return firstName; }
+ public String getLastName() { return lastName; }
+ public String getEmail() { return email; }
+ public String getFullName() { return fullName; }
+ }
+
+ public static class ConfigForm {
+ public String environment;
+ public boolean debugEnabled;
+ }
+
+ public static class AppConfig {
+ private String environment;
+ private boolean debugEnabled;
+ private String configSource;
+
+ // Private constructor
+ private AppConfig() {}
+
+ @BeanFactoryMethod({"environment", "debugEnabled"})
+ public static AppConfig createConfig(String environment, boolean debugEnabled) {
+ AppConfig config = new AppConfig();
+ config.environment = environment != null ? environment : "development";
+ config.debugEnabled = debugEnabled;
+ config.configSource = "FACTORY_CONFIGURED";
+
+ // Could load additional configuration from files, environment variables, etc.
+ return config;
+ }
+
+ public String getEnvironment() { return environment; }
+ public boolean isDebugEnabled() { return debugEnabled; }
+ public String getConfigSource() { return configSource; }
+ }
+}
\ No newline at end of file