Skip to content

Commit

Permalink
Allow object instantiation without calling the default constructor (#133
Browse files Browse the repository at this point in the history
)

* Allow object deserialization without the default constructor

* JUnits for ConfectorySettings added

* Add Objenesis optional dependency

* Additional Junit test cases

* Update ObjectFactory.java
  • Loading branch information
oswaldobapvicjr authored Jun 9, 2023
1 parent 7627192 commit a9f6c53
Show file tree
Hide file tree
Showing 17 changed files with 419 additions and 75 deletions.
8 changes: 8 additions & 0 deletions confectory-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<json-path.version>2.8.0</json-path.version>
<json-smart.version>2.4.10</json-smart.version>
<performetrics.version>2.4.0</performetrics.version>
<objenesis.version>3.3</objenesis.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -82,6 +83,13 @@
<artifactId>jsonmerge-core</artifactId>
<version>${jsonmerge.version}</version>
</dependency>

<dependency>
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
<version>${objenesis.version}</version>
<optional>true</optional>
</dependency>

</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public static ConfigurationContainer container()
*/
public static ConfectorySettings settings()
{
return ConfectorySettings.getInstance();
return ConfectorySettings.instance();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public ConfigurationContainer(Configuration<?>... configs)
public ConfigurationContainer(DataFetchStrategy dataFetchStrategy, Configuration<?>... configs)
{
ConfectorySettings settings = Confectory.settings();
setDataFetchStrategy(ObjectUtils.defaultIfNull(dataFetchStrategy, settings.getDefaultDataFetchStrategy()));
setDataFetchStrategy(ObjectUtils.defaultIfNull(dataFetchStrategy, settings.getDataFetchStrategy()));

Arrays.stream(configs).forEach(this::add);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Objects;

import org.apache.commons.lang3.reflect.ConstructorUtils;
import org.apache.commons.lang3.reflect.FieldUtils;

import net.obvj.confectory.ConfigurationException;
import net.obvj.confectory.internal.helper.BeanConfigurationHelper;
import net.obvj.confectory.internal.helper.ConfigurationHelper;
import net.obvj.confectory.settings.ConfectorySettings;
import net.obvj.confectory.util.*;

/**
Expand Down Expand Up @@ -66,15 +67,31 @@ public class INIToObjectMapper<T> extends AbstractINIMapper<T> implements Mapper
private static final String MSG_UNPARSABLE_PROPERTY_VALUE = "Unable to parse the value of the property %s into a field of type '%s'";

private final Class<T> targetType;
private final ObjectFactory objectFactory;

/**
* Builds a new {@code INIToObjectMapper} with the specified target type.
*
* @param targetType the target type to be produced by this {@code Mapper}
*/
public INIToObjectMapper(Class<T> targetType)
{
this(targetType, ConfectorySettings.instance().getObjectFactory());
}

/**
* Builds a new {@code INIToObjectMapper} with the specified target type and a custom
* object factory.
*
* @param targetType the target type to be produced by this {@code Mapper}
* @param objectFactory the {@link ObjectFactory} to produce objects; not null
* @since 2.5.0
*/
public INIToObjectMapper(Class<T> targetType, ObjectFactory objectFactory)
{
this.targetType = targetType;
this.objectFactory = Objects.requireNonNull(objectFactory,
"the ObjectFactory must not be null");
}

@Override
Expand All @@ -90,7 +107,7 @@ Object newObject(Context context)
Class<?> type = getCurrentType(context);
try
{
return type != null ? ConstructorUtils.invokeConstructor(type) : null;
return type != null ? objectFactory.newObject(type) : null;
}
catch (ReflectiveOperationException exception)
{
Expand All @@ -105,7 +122,7 @@ Object parseValue(Context context, String value)
Field field = PropertyUtils.findFieldByPropertyKeyOrName(currentType, context.currentKey);
try
{
return field != null ? PropertyUtils.parseValue(value, field) : null;
return field != null ? PropertyUtils.parseValue(value, field, objectFactory) : null;
}
catch (ParseException | ReflectiveOperationException exception)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,16 @@
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Objects;
import java.util.Properties;

import org.apache.commons.lang3.reflect.ConstructorUtils;
import org.apache.commons.lang3.reflect.FieldUtils;

import net.obvj.confectory.ConfigurationException;
import net.obvj.confectory.internal.helper.BeanConfigurationHelper;
import net.obvj.confectory.internal.helper.ConfigurationHelper;
import net.obvj.confectory.util.ParseException;
import net.obvj.confectory.util.Property;
import net.obvj.confectory.util.PropertyUtils;
import net.obvj.confectory.util.ReflectionUtils;
import net.obvj.confectory.settings.ConfectorySettings;
import net.obvj.confectory.util.*;

/**
* A specialized {@code Mapper} that loads the contents of a {@code Source} (e.g.: file,
Expand Down Expand Up @@ -67,15 +65,31 @@ public class PropertiesToObjectMapper<T> implements Mapper<T>
private static final String MSG_UNABLE_TO_PARSE_PROPERTY = "Unable to parse the value of the property '%s' into a field of type '%s'";

private final Class<T> targetType;
private final ObjectFactory objectFactory;

/**
* Builds a new Properties Mapper with the specified target type.
*
* @param targetType the target type to be produced by this {@code Mapper}
*/
public PropertiesToObjectMapper(Class<T> targetType)
{
this(targetType, ConfectorySettings.instance().getObjectFactory());
}

/**
* Builds a new Properties Mapper with the specified target type and a custom object
* factory.
*
* @param targetType the target type to be produced by this {@code Mapper}
* @param objectFactory the {@link ObjectFactory} to produce objects; not null
* @since 2.5.0
*/
public PropertiesToObjectMapper(Class<T> targetType, ObjectFactory objectFactory)
{
this.targetType = targetType;
this.objectFactory = Objects.requireNonNull(objectFactory,
"the ObjectFactory must not be null");
}

@Override
Expand All @@ -97,7 +111,7 @@ private T asObject(Properties properties)
Field[] fields = FieldUtils.getAllFields(targetType);
try
{
T targetObject = ConstructorUtils.invokeConstructor(targetType);
T targetObject = objectFactory.newObject(targetType);
for (Field field : fields)
{
writeField(targetObject, field, properties);
Expand Down Expand Up @@ -136,7 +150,7 @@ private void writeField(T targetObject, Field field, Properties properties)
Class<?> fieldType = field.getType();
try
{
Object parsedValue = PropertyUtils.parseValue(propertyValue, fieldType, annotation);
Object parsedValue = PropertyUtils.parseValue(propertyValue, fieldType, annotation, objectFactory);
FieldUtils.writeDeclaredField(targetObject, field.getName(), parsedValue, true);
}
catch (ParseException exception)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.Objects;

import net.obvj.confectory.DataFetchStrategy;
import net.obvj.confectory.util.ObjectFactory;

/**
* An object that defines the global settings for the {@code Confectory} project.
Expand All @@ -34,11 +35,17 @@ public class ConfectorySettings
*/
static final DataFetchStrategy INITIAL_DATA_FETCH_STRATEGY = DataFetchStrategy.LENIENT;

/**
* The initial {@link ObjectFactory} applied by default
*/
static final ObjectFactory INITIAL_OBJECT_FACTORY = ObjectFactory.UNSAFE;

private static final ConfectorySettings INSTANCE = new ConfectorySettings();

// Settings - start

private DataFetchStrategy defaultDataFetchStrategy;
private ObjectFactory objectFactory;

/*
* Private constructor to hide the default, implicit one
Expand All @@ -54,12 +61,13 @@ private ConfectorySettings()
public void reset()
{
defaultDataFetchStrategy = INITIAL_DATA_FETCH_STRATEGY;
objectFactory = INITIAL_OBJECT_FACTORY;
}

/**
* @return a reference to the the current {@link ConfectorySettings} instance.
*/
public static ConfectorySettings getInstance()
public static ConfectorySettings instance()
{
return INSTANCE;
}
Expand All @@ -70,7 +78,7 @@ public static ConfectorySettings getInstance()
*
* @return the default {@link DataFetchStrategy} to be applied
*/
public DataFetchStrategy getDefaultDataFetchStrategy()
public DataFetchStrategy getDataFetchStrategy()
{
return defaultDataFetchStrategy;
}
Expand All @@ -82,10 +90,32 @@ public DataFetchStrategy getDefaultDataFetchStrategy()
* @param strategy the default {@link DataFetchStrategy} to set; not null
* @throws NullPointerException if the specified strategy is null
*/
public void setDefaultDataFetchStrategy(DataFetchStrategy strategy)
public void setDataFetchStrategy(DataFetchStrategy strategy)
{
this.defaultDataFetchStrategy = Objects.requireNonNull(strategy,
"the default DataFetchStrategy must not be null");
}

/**
* @return the {@link ObjectFactory} to be produce new objects
* @since 2.5.0
*/
public ObjectFactory getObjectFactory()
{
return objectFactory;
}

/**
* Defines the {@link ObjectFactory} to produce new objects.
*
* @param objectFactory the {@link ObjectFactory} to set; not null
* @throws NullPointerException if the specified {@link ObjectFactory} is null
* @since 2.5.0
*/
public void setObjectFactory(ObjectFactory objectFactory)
{
this.objectFactory = Objects.requireNonNull(objectFactory,
"the ObjectFactory must not be null");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright 2023 obvj.net
*
* Licensed 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 net.obvj.confectory.util;

import org.apache.commons.lang3.reflect.ConstructorUtils;
import org.objenesis.ObjenesisStd;

/**
* A factory that encapsulates the logic to reflectively produce new objects depending on
* the desired strategy.
*
* @author oswaldo.bapvic.jr
* @since 2.5.0
*/
public enum ObjectFactory
{
/**
* Constructor-based object factory.
* <p>
* It's safer but requires the existence of a public, default constructor available in the
* class to allow the instantiation.
*/
CLASSIC
{
@Override
public <T> T newObject(Class<T> type) throws ReflectiveOperationException
{
return ConstructorUtils.invokeConstructor(type);
}
},

/**
* Object factory that builds objects by allocating an instance directly on the heap,
* without any constructor being called.
* <p>
* Final fields are assigned with
* <a href="https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html">
* default values</a>.
* <p>
* <b>Note:</b> This is the default strategy since 2.5.0.
*/
UNSAFE
{
@Override
@SuppressWarnings("restriction")
public <T> T newObject(Class<T> type) throws ReflectiveOperationException
{
return type.cast(UnsafeAccessor.UNSAFE.allocateInstance(type));
}
},

/**
* Alternative object factory that uses a variety of approaches to attempt to instantiate
* the object, depending on the type of object, JVM version, JVM vendor and Security
* Manager present.
* <p>
* <b>IMPORTANT:</b> This strategy requires the <b>optional dependency</b>
* {@code org.objenesis:objenesis} in the class path.
*/
OBJENESIS
{
@Override
public <T> T newObject(Class<T> type)
{
return new ObjenesisStd().newInstance(type);
}
};

/**
* Creates a new instance of the specified class.
*
* @param <T> the target type
* @param type the target type
* @return a new instance of the specified class
*
* @throws ReflectiveOperationException in case of failure during the instantiation
*/
public abstract <T> T newObject(Class<T> type) throws ReflectiveOperationException;

}
Loading

0 comments on commit a9f6c53

Please sign in to comment.