From a9f6c532c0e216c5c05bf108dc578041c5206176 Mon Sep 17 00:00:00 2001 From: Oswaldo Baptista Vicente Junior <45291656+oswaldobapvicjr@users.noreply.github.com> Date: Thu, 8 Jun 2023 23:54:06 -0300 Subject: [PATCH] Allow object instantiation without calling the default constructor (#133) * Allow object deserialization without the default constructor * JUnits for ConfectorySettings added * Add Objenesis optional dependency * Additional Junit test cases * Update ObjectFactory.java --- confectory-core/pom.xml | 8 ++ .../java/net/obvj/confectory/Confectory.java | 2 +- .../confectory/ConfigurationContainer.java | 2 +- .../confectory/mapper/INIToObjectMapper.java | 23 ++++- .../mapper/PropertiesToObjectMapper.java | 28 ++++-- .../settings/ConfectorySettings.java | 36 ++++++- .../obvj/confectory/util/ObjectFactory.java | 94 +++++++++++++++++++ .../obvj/confectory/util/PropertyUtils.java | 29 +++--- .../obvj/confectory/util/UnsafeAccessor.java | 54 +++++++++++ .../net/obvj/confectory/ConfectoryTest.java | 2 +- .../ConfigurationContainerTest.java | 2 +- .../TypeSafeConfigurationContainerTest.java | 2 +- .../mapper/INIToObjectMapperTest.java | 84 ++++++++++++----- .../mapper/PropertiesToObjectMapperTest.java | 43 +++++++-- .../settings/ConfectorySettingsTest.java | 44 ++++++--- .../ConfectoryTestDriveContainerLenient.java | 2 +- .../confectory/util/UnsafeAccessorTest.java | 39 ++++++++ 17 files changed, 419 insertions(+), 75 deletions(-) create mode 100644 confectory-core/src/main/java/net/obvj/confectory/util/ObjectFactory.java create mode 100644 confectory-core/src/main/java/net/obvj/confectory/util/UnsafeAccessor.java create mode 100644 confectory-core/src/test/java/net/obvj/confectory/util/UnsafeAccessorTest.java diff --git a/confectory-core/pom.xml b/confectory-core/pom.xml index 3a5b9adb..5a8d19b6 100644 --- a/confectory-core/pom.xml +++ b/confectory-core/pom.xml @@ -37,6 +37,7 @@ 2.8.0 2.4.10 2.4.0 + 3.3 @@ -82,6 +83,13 @@ jsonmerge-core ${jsonmerge.version} + + + org.objenesis + objenesis + ${objenesis.version} + true + diff --git a/confectory-core/src/main/java/net/obvj/confectory/Confectory.java b/confectory-core/src/main/java/net/obvj/confectory/Confectory.java index 212a5889..dc5db0fe 100644 --- a/confectory-core/src/main/java/net/obvj/confectory/Confectory.java +++ b/confectory-core/src/main/java/net/obvj/confectory/Confectory.java @@ -70,7 +70,7 @@ public static ConfigurationContainer container() */ public static ConfectorySettings settings() { - return ConfectorySettings.getInstance(); + return ConfectorySettings.instance(); } } diff --git a/confectory-core/src/main/java/net/obvj/confectory/ConfigurationContainer.java b/confectory-core/src/main/java/net/obvj/confectory/ConfigurationContainer.java index 8f8b9229..c12da17d 100644 --- a/confectory-core/src/main/java/net/obvj/confectory/ConfigurationContainer.java +++ b/confectory-core/src/main/java/net/obvj/confectory/ConfigurationContainer.java @@ -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); } diff --git a/confectory-core/src/main/java/net/obvj/confectory/mapper/INIToObjectMapper.java b/confectory-core/src/main/java/net/obvj/confectory/mapper/INIToObjectMapper.java index 97af267b..b9554d2a 100644 --- a/confectory-core/src/main/java/net/obvj/confectory/mapper/INIToObjectMapper.java +++ b/confectory-core/src/main/java/net/obvj/confectory/mapper/INIToObjectMapper.java @@ -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.*; /** @@ -66,6 +67,7 @@ public class INIToObjectMapper extends AbstractINIMapper 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 targetType; + private final ObjectFactory objectFactory; /** * Builds a new {@code INIToObjectMapper} with the specified target type. @@ -73,8 +75,23 @@ public class INIToObjectMapper extends AbstractINIMapper implements Mapper * @param targetType the target type to be produced by this {@code Mapper} */ public INIToObjectMapper(Class 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 targetType, ObjectFactory objectFactory) { this.targetType = targetType; + this.objectFactory = Objects.requireNonNull(objectFactory, + "the ObjectFactory must not be null"); } @Override @@ -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) { @@ -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) { diff --git a/confectory-core/src/main/java/net/obvj/confectory/mapper/PropertiesToObjectMapper.java b/confectory-core/src/main/java/net/obvj/confectory/mapper/PropertiesToObjectMapper.java index 8a542444..2886eaaf 100644 --- a/confectory-core/src/main/java/net/obvj/confectory/mapper/PropertiesToObjectMapper.java +++ b/confectory-core/src/main/java/net/obvj/confectory/mapper/PropertiesToObjectMapper.java @@ -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, @@ -67,6 +65,7 @@ public class PropertiesToObjectMapper implements Mapper 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 targetType; + private final ObjectFactory objectFactory; /** * Builds a new Properties Mapper with the specified target type. @@ -74,8 +73,23 @@ public class PropertiesToObjectMapper implements Mapper * @param targetType the target type to be produced by this {@code Mapper} */ public PropertiesToObjectMapper(Class 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 targetType, ObjectFactory objectFactory) { this.targetType = targetType; + this.objectFactory = Objects.requireNonNull(objectFactory, + "the ObjectFactory must not be null"); } @Override @@ -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); @@ -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) diff --git a/confectory-core/src/main/java/net/obvj/confectory/settings/ConfectorySettings.java b/confectory-core/src/main/java/net/obvj/confectory/settings/ConfectorySettings.java index 4671697c..0c970db0 100644 --- a/confectory-core/src/main/java/net/obvj/confectory/settings/ConfectorySettings.java +++ b/confectory-core/src/main/java/net/obvj/confectory/settings/ConfectorySettings.java @@ -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. @@ -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 @@ -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; } @@ -70,7 +78,7 @@ public static ConfectorySettings getInstance() * * @return the default {@link DataFetchStrategy} to be applied */ - public DataFetchStrategy getDefaultDataFetchStrategy() + public DataFetchStrategy getDataFetchStrategy() { return defaultDataFetchStrategy; } @@ -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"); + } + } diff --git a/confectory-core/src/main/java/net/obvj/confectory/util/ObjectFactory.java b/confectory-core/src/main/java/net/obvj/confectory/util/ObjectFactory.java new file mode 100644 index 00000000..b129709b --- /dev/null +++ b/confectory-core/src/main/java/net/obvj/confectory/util/ObjectFactory.java @@ -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. + *

+ * It's safer but requires the existence of a public, default constructor available in the + * class to allow the instantiation. + */ + CLASSIC + { + @Override + public T newObject(Class 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. + *

+ * Final fields are assigned with + * + * default values. + *

+ * Note: This is the default strategy since 2.5.0. + */ + UNSAFE + { + @Override + @SuppressWarnings("restriction") + public T newObject(Class 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. + *

+ * IMPORTANT: This strategy requires the optional dependency + * {@code org.objenesis:objenesis} in the class path. + */ + OBJENESIS + { + @Override + public T newObject(Class type) + { + return new ObjenesisStd().newInstance(type); + } + }; + + /** + * Creates a new instance of the specified class. + * + * @param 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 newObject(Class type) throws ReflectiveOperationException; + +} diff --git a/confectory-core/src/main/java/net/obvj/confectory/util/PropertyUtils.java b/confectory-core/src/main/java/net/obvj/confectory/util/PropertyUtils.java index 63756265..5151cf90 100644 --- a/confectory-core/src/main/java/net/obvj/confectory/util/PropertyUtils.java +++ b/confectory-core/src/main/java/net/obvj/confectory/util/PropertyUtils.java @@ -16,13 +16,12 @@ package net.obvj.confectory.util; -import static net.obvj.confectory.util.StringUtils.*; +import static net.obvj.confectory.util.StringUtils.defaultIfEmpty; import java.lang.reflect.Field; import java.util.Optional; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.reflect.ConstructorUtils; import org.apache.commons.lang3.reflect.FieldUtils; /** @@ -130,18 +129,20 @@ public static boolean isFieldAnnotated(final Field field, final String name) * class configured in the {@link Property} annotation (if not null), or standard * conversion by using the {@link TypeFactory} by default. * - * @param string the string to be parsed - * @param field the target field (not null) + * @param string the string to be parsed + * @param field the target field (not null) + * @param objectFactory the {@link ObjectFactory} to produce a new instance of the custom + * converter class, if specified in the annotation (not null) * * @return the object resulting from the parse operation * * @throws ReflectiveOperationException if an error occurs in the reflective operation * @throws ParseException if an error is encountered while parsing */ - public static Object parseValue(final String string, final Field field) - throws ReflectiveOperationException, ParseException + public static Object parseValue(final String string, final Field field, + final ObjectFactory objectFactory) throws ReflectiveOperationException, ParseException { - return parseValue(string, field.getType(), field.getAnnotation(Property.class)); + return parseValue(string, field.getType(), field.getAnnotation(Property.class), objectFactory); } /** @@ -151,9 +152,11 @@ public static Object parseValue(final String string, final Field field) * class configured in the {@link Property} annotation (if not null), or standard * conversion by using the {@link TypeFactory} by default. * - * @param string the string to be parsed - * @param targetType the target type (not null) - * @param property the {@link Property} annotation to be evaluated (null is allowed) + * @param string the string to be parsed + * @param targetType the target type (not null) + * @param property the {@link Property} annotation to be evaluated (null is allowed) + * @param objectFactory the {@link ObjectFactory} to produce a new instance of the custom + * converter class, if specified in the annotation (not null) * * @return the object resulting from the parse operation * @@ -161,15 +164,17 @@ public static Object parseValue(final String string, final Field field) * @throws ParseException if an error is encountered while parsing */ public static Object parseValue(final String string, final Class targetType, - final Property property) throws ReflectiveOperationException, ParseException + final Property property, ObjectFactory objectFactory) + throws ReflectiveOperationException, ParseException { if (property != null && property.converter().length > 0) { // Apply custom converter specified in the annotation - TypeConverter converter = ConstructorUtils.invokeConstructor(property.converter()[0]); + TypeConverter converter = objectFactory.newObject(property.converter()[0]); return converter.convert(string); } // Apply standard/default conversion return TypeFactory.parse(targetType, string); } + } diff --git a/confectory-core/src/main/java/net/obvj/confectory/util/UnsafeAccessor.java b/confectory-core/src/main/java/net/obvj/confectory/util/UnsafeAccessor.java new file mode 100644 index 00000000..5b9077db --- /dev/null +++ b/confectory-core/src/main/java/net/obvj/confectory/util/UnsafeAccessor.java @@ -0,0 +1,54 @@ +/* + * 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 java.lang.reflect.Constructor; + +import sun.misc.Unsafe; + +/** + * A class that allows to get access to {@code sun.misc.Unsafe}. + * + * @author oswaldo.bapvic.jr + * @since 2.5.0 + */ +@SuppressWarnings("restriction") +public final class UnsafeAccessor +{ + public static final Unsafe UNSAFE = getUnsafe(); + + private static Unsafe getUnsafe() + { + try + { + Constructor constructor = Unsafe.class.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } + catch (ReflectiveOperationException exception) + { + throw new UnsupportedOperationException( + "Unable to get hold of an instance of sun.misc.Unsafe", exception); + } + } + + private UnsafeAccessor() + { + throw new UnsupportedOperationException("Instantiation not allowed"); + } + +} diff --git a/confectory-core/src/test/java/net/obvj/confectory/ConfectoryTest.java b/confectory-core/src/test/java/net/obvj/confectory/ConfectoryTest.java index 1b177ef7..6fbb6dd3 100644 --- a/confectory-core/src/test/java/net/obvj/confectory/ConfectoryTest.java +++ b/confectory-core/src/test/java/net/obvj/confectory/ConfectoryTest.java @@ -48,7 +48,7 @@ void ensure_global_default_configuration_exists() @Test void settings_sameInstanceDefaultConfiguration() { - assertThat(Confectory.settings(), equalTo(ConfectorySettings.getInstance())); + assertThat(Confectory.settings(), equalTo(ConfectorySettings.instance())); } } diff --git a/confectory-core/src/test/java/net/obvj/confectory/ConfigurationContainerTest.java b/confectory-core/src/test/java/net/obvj/confectory/ConfigurationContainerTest.java index f6520d74..b2d0a0af 100644 --- a/confectory-core/src/test/java/net/obvj/confectory/ConfigurationContainerTest.java +++ b/confectory-core/src/test/java/net/obvj/confectory/ConfigurationContainerTest.java @@ -88,7 +88,7 @@ private static String join(String... lines) private void assertDefaultDataFetchStrategy() { assertThat(container.getDataFetchStrategy(), - equalTo(ConfectorySettings.getInstance().getDefaultDataFetchStrategy())); + equalTo(ConfectorySettings.instance().getDataFetchStrategy())); } @Test diff --git a/confectory-core/src/test/java/net/obvj/confectory/TypeSafeConfigurationContainerTest.java b/confectory-core/src/test/java/net/obvj/confectory/TypeSafeConfigurationContainerTest.java index 77b0e6e9..e50a4ced 100644 --- a/confectory-core/src/test/java/net/obvj/confectory/TypeSafeConfigurationContainerTest.java +++ b/confectory-core/src/test/java/net/obvj/confectory/TypeSafeConfigurationContainerTest.java @@ -61,7 +61,7 @@ void constructor_empty_default() container = new TypeSafeConfigurationContainer<>(); assertThat(container.size(), equalTo(0L)); assertThat(container.getInternal().getDataFetchStrategy(), - equalTo(Confectory.settings().getDefaultDataFetchStrategy())); + equalTo(Confectory.settings().getDataFetchStrategy())); } @Test diff --git a/confectory-core/src/test/java/net/obvj/confectory/mapper/INIToObjectMapperTest.java b/confectory-core/src/test/java/net/obvj/confectory/mapper/INIToObjectMapperTest.java index b1d170ea..cec40cc0 100644 --- a/confectory-core/src/test/java/net/obvj/confectory/mapper/INIToObjectMapperTest.java +++ b/confectory-core/src/test/java/net/obvj/confectory/mapper/INIToObjectMapperTest.java @@ -42,6 +42,7 @@ import net.obvj.confectory.mapper.PropertiesToObjectMapperTest.MyIntsConverter; import net.obvj.confectory.mapper.PropertiesToObjectMapperTest.MyPairConverter; import net.obvj.confectory.mapper.model.MyIni; +import net.obvj.confectory.util.ObjectFactory; import net.obvj.confectory.util.ParseException; import net.obvj.confectory.util.Property; @@ -116,6 +117,9 @@ class INIToObjectMapperTest static class MyBeanPrivateConstructor { + @Property("rootProperty") + String myString; + private MyBeanPrivateConstructor() {} } @@ -125,7 +129,10 @@ static class MyBeanPrivateSection static class Section { - private Section() {} // unsupported + @Property("section_string") + String myString; + + private Section() {} // unsupported in ObjectFactory.CLASSIC } } @@ -133,8 +140,6 @@ static class MyIniDate { @Property("my_date") LocalDate myDate; - - public MyIniDate() {} } static class MyIniOtherTypes @@ -143,8 +148,6 @@ static class MyIniOtherTypes Class myClass; @Property("my_ip") InetAddress myIp; - - public MyIniOtherTypes() {} } static class MyBeanCustomConverters @@ -156,26 +159,24 @@ static class MyBeanCustomConverters @Property("section1") Section section; - public MyBeanCustomConverters() {} - static class Section { @Property("myBigDecimal") BigDecimal myDecimal; - - public Section() {} } } private Mapper mapper = new INIToObjectMapper<>(MyIni.class); + private Mapper mapperClassic = new INIToObjectMapper<>(MyIni.class, ObjectFactory.CLASSIC); + private Mapper mapperUnsafe = new INIToObjectMapper<>(MyIni.class, ObjectFactory.UNSAFE); private ByteArrayInputStream toInputStream(String content) { return new ByteArrayInputStream(content.getBytes()); } - private MyIni testWithString(String string) + private MyIni applyString(Mapper mapper, String string) { try { @@ -189,9 +190,26 @@ private MyIni testWithString(String string) } @Test - void apply_validIni_validJSONObject() throws IOException + void apply_validIniAndObjectFactoryClassic_validObject() throws IOException { - MyIni result = testWithString(VALID_INI_1); + MyIni result = applyString(mapperClassic, VALID_INI_1); + assertThat(result.getRootProperty(), is(equalTo("myRootValue"))); + + assertThat(result.getSection1().getSectionString(), is(equalTo("mySection1Value"))); + assertThat(result.getSection1().getSectionNumber(), is(equalTo(1))); + assertThat(result.getSection1().isSectionBoolean(), is(equalTo(false))); + assertThat(result.getSection1().getTransientField(), is(equalTo(null))); + + assertThat(result.getSection2().getSectionString(), is(equalTo("mySection2Value"))); + assertThat(result.getSection2().getSectionNumber(), is(equalTo(2))); + assertThat(result.getSection2().isSectionBoolean(), is(equalTo(true))); + assertThat(result.getSection2().getTransientField(), is(equalTo(null))); + } + + @Test + void apply_validIniAndObjectFactoryUnsafe_validObject() throws IOException + { + MyIni result = applyString(mapperUnsafe, VALID_INI_1); assertThat(result.getRootProperty(), is(equalTo("myRootValue"))); assertThat(result.getSection1().getSectionString(), is(equalTo("mySection1Value"))); @@ -208,28 +226,32 @@ void apply_validIni_validJSONObject() throws IOException @Test void apply_missingTokenInSectionDeclaration_exception() throws IOException { - assertThat(() -> testWithString(INVALID_INI_1), throwsException(ConfigurationSourceException.class) + assertThat(() -> applyString(mapper, INVALID_INI_1), + throwsException(ConfigurationSourceException.class) .withMessage(equalTo("Malformed INI: expected token ']' at line 2: \"[section1\""))); } @Test void apply_sectionDeclarationNoName_exception() throws IOException { - assertThat(() -> testWithString(INVALID_INI_2), throwsException(ConfigurationSourceException.class) + assertThat(() -> applyString(mapper, INVALID_INI_2), + throwsException(ConfigurationSourceException.class) .withMessage(equalTo("Malformed INI: expected section name at line 2: \"[]\""))); } @Test void apply_valueWithoutProperty_exception() throws IOException { - assertThat(() -> testWithString(INVALID_INI_3), throwsException(ConfigurationSourceException.class) + assertThat(() -> applyString(mapper, INVALID_INI_3), + throwsException(ConfigurationSourceException.class) .withMessage(equalTo("Malformed INI: expected property key at line 1: \"=value\""))); } @Test void apply_invalidLine_exception() throws IOException { - assertThat(() -> testWithString(INVALID_INI_4), throwsException(ConfigurationSourceException.class) + assertThat(() -> applyString(mapper, INVALID_INI_4), + throwsException(ConfigurationSourceException.class) .withMessage(equalTo("Malformed INI: expected property at line 1: \"invalid line\""))); } @@ -237,7 +259,7 @@ void apply_invalidLine_exception() throws IOException void apply_invalidType_exception() throws IOException { ConfigurationException exception = assertThrows(ConfigurationException.class, - () -> testWithString(INVALID_INI_5)); + () -> applyString(mapper, INVALID_INI_5)); assertThat(exception.getMessage(), equalTo( "Unable to parse the value of the property ['number'] into a field of type 'double'")); @@ -255,7 +277,7 @@ void apply_invalidType_exception() throws IOException void apply_invalidTypeInsideSection_exception() throws IOException { ConfigurationException exception = assertThrows(ConfigurationException.class, - () -> testWithString(INVALID_INI_6)); + () -> applyString(mapper, INVALID_INI_6)); assertThat(exception.getMessage(), equalTo( "Unable to parse the value of the property ['section1']['section_number'] into a field of type 'int'")); @@ -272,7 +294,7 @@ void apply_invalidTypeInsideSection_exception() throws IOException @Test void apply_sectionNotMapped_sectionSkipped() throws IOException { - MyIni result = testWithString(VALID_INI_2); + MyIni result = applyString(mapper, VALID_INI_2); assertThat(result.getRootProperty(), is(equalTo("myRootValue"))); assertThat(result.getSection1(), is(equalTo(null))); assertThat(result.getSection2().getSectionString(), is(equalTo("mySection2Value"))); @@ -281,25 +303,41 @@ void apply_sectionNotMapped_sectionSkipped() throws IOException } @Test - void apply_beanWithPrivateConstructor_configurationException() + void apply_beanWithPrivateConstructorAndClassicFactory_configurationException() { assertThat( - () -> new INIToObjectMapper<>(MyBeanPrivateConstructor.class) + () -> new INIToObjectMapper<>(MyBeanPrivateConstructor.class, ObjectFactory.CLASSIC) .apply(toInputStream(VALID_INI_1)), throwsException(ConfigurationException.class) .withCause(ReflectiveOperationException.class)); } @Test - void apply_beanWithPrivateConstructorInSection_configurationException() + void apply_beanWithPrivateConstructorAndEnhancedFactory_success() throws IOException + { + MyBeanPrivateConstructor bean = new INIToObjectMapper<>(MyBeanPrivateConstructor.class, + ObjectFactory.UNSAFE).apply(toInputStream(VALID_INI_1)); + assertThat(bean.myString, equalTo("myRootValue")); + } + + @Test + void apply_beanWithPrivateConstructorInSectionAndClassicFactory_configurationException() { assertThat( - () -> new INIToObjectMapper<>(MyBeanPrivateSection.class) + () -> new INIToObjectMapper<>(MyBeanPrivateSection.class, ObjectFactory.CLASSIC) .apply(toInputStream(VALID_INI_1)), throwsException(ConfigurationException.class) .withCause(ReflectiveOperationException.class)); } + @Test + void apply_beanWithPrivateConstructorInSectionAndEnhancedFactory_success() throws IOException + { + MyBeanPrivateSection bean = new INIToObjectMapper<>(MyBeanPrivateSection.class, + ObjectFactory.UNSAFE).apply(toInputStream(VALID_INI_1)); + assertThat(bean.section1.myString, equalTo("mySection1Value")); + } + @Test void apply_validIniDate_validObject() throws IOException { diff --git a/confectory-core/src/test/java/net/obvj/confectory/mapper/PropertiesToObjectMapperTest.java b/confectory-core/src/test/java/net/obvj/confectory/mapper/PropertiesToObjectMapperTest.java index b004bbdb..6397e221 100644 --- a/confectory-core/src/test/java/net/obvj/confectory/mapper/PropertiesToObjectMapperTest.java +++ b/confectory-core/src/test/java/net/obvj/confectory/mapper/PropertiesToObjectMapperTest.java @@ -40,6 +40,7 @@ import net.obvj.confectory.ConfigurationException; import net.obvj.confectory.TestUtils; import net.obvj.confectory.internal.helper.BeanConfigurationHelper; +import net.obvj.confectory.util.ObjectFactory; import net.obvj.confectory.util.ParseException; import net.obvj.confectory.util.Property; import net.obvj.confectory.util.TypeConverter; @@ -74,8 +75,6 @@ static class MyBeanNoExplicitMapping Boolean booleanValue; // implicit mapping String stringValue; // implicit mapping Integer intValue; // implicit mapping - - public MyBeanNoExplicitMapping() {} } static class MyBeanExplicitMapping @@ -83,8 +82,6 @@ static class MyBeanExplicitMapping @Property("booleanValue") boolean b; // explicit mapping @Property("stringValue") String s; // explicit mapping @Property("intValue") int i; // explicit mapping - - public MyBeanExplicitMapping() {} } static class MyBeanHybrid @@ -93,8 +90,6 @@ static class MyBeanHybrid @Property String stringValue; // implicit mapping int intValue; // implicit mapping double unknownDouble; // invalid property - - public MyBeanHybrid() {} } static class MyBeanAllFieldsTransient @@ -102,12 +97,18 @@ static class MyBeanAllFieldsTransient transient boolean booleanValue; // implicit, but transient transient String stringValue; // implicit, but transient transient int intValue; // implicit, but transient - - public MyBeanAllFieldsTransient() {} } static class MyBeanPrivateConstructor { + boolean booleanValue; + String stringValue; + int intValue; + + // since this value is assigned during constructor, + // it will not happen when using the ObjectFactor.UNSAFE + double undefined = -1.0; + private MyBeanPrivateConstructor() {} } @@ -217,15 +218,37 @@ void apply_beanWithAllFieldsTransient_noDataCopied() throws IOException } @Test - void apply_beanWithPrivateConstructor_configurationException() + void apply_beanWithPrivateConstructorAndClassicObjectFactory_configurationException() { assertThat( - () -> new PropertiesToObjectMapper<>(MyBeanPrivateConstructor.class) + () -> new PropertiesToObjectMapper<>(MyBeanPrivateConstructor.class, ObjectFactory.CLASSIC) .apply(newInputStream()), throwsException(ConfigurationException.class) .withCause(ReflectiveOperationException.class)); } + @Test + void apply_beanWithPrivateConstructorAndUnsafedObjectFactory_success() throws IOException + { + MyBeanPrivateConstructor bean = new PropertiesToObjectMapper<>( + MyBeanPrivateConstructor.class, ObjectFactory.UNSAFE).apply(newInputStream()); + assertThat(bean.booleanValue, equalTo(true)); + assertThat(bean.stringValue, equalTo("string1")); + assertThat(bean.intValue, equalTo(1910)); + assertThat(bean.undefined, equalTo(0.0)); // Default value assigned + } + + @Test + void apply_beanWithPrivateConstructorAndObjenesisObjectFactory_success() throws IOException + { + MyBeanPrivateConstructor bean = new PropertiesToObjectMapper<>( + MyBeanPrivateConstructor.class, ObjectFactory.OBJENESIS).apply(newInputStream()); + assertThat(bean.booleanValue, equalTo(true)); + assertThat(bean.stringValue, equalTo("string1")); + assertThat(bean.intValue, equalTo(1910)); + assertThat(bean.undefined, equalTo(0.0)); // Default value assigned + } + @Test void apply_propertiesWithDate_success() throws IOException { diff --git a/confectory-core/src/test/java/net/obvj/confectory/settings/ConfectorySettingsTest.java b/confectory-core/src/test/java/net/obvj/confectory/settings/ConfectorySettingsTest.java index 8090f97f..d57f3850 100644 --- a/confectory-core/src/test/java/net/obvj/confectory/settings/ConfectorySettingsTest.java +++ b/confectory-core/src/test/java/net/obvj/confectory/settings/ConfectorySettingsTest.java @@ -16,7 +16,7 @@ package net.obvj.confectory.settings; -import static net.obvj.confectory.settings.ConfectorySettings.INITIAL_DATA_FETCH_STRATEGY; +import static net.obvj.confectory.settings.ConfectorySettings.*; import static net.obvj.junit.utils.matchers.AdvancedMatchers.throwsException; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -28,6 +28,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import net.obvj.confectory.DataFetchStrategy; +import net.obvj.confectory.util.ObjectFactory; /** * Unit tests for the {@link ConfectorySettings}. @@ -40,8 +41,10 @@ class ConfectorySettingsTest { @Mock private DataFetchStrategy dataFetchStrategy; + @Mock + private ObjectFactory objectFactory; - private ConfectorySettings settings = ConfectorySettings.getInstance(); + private ConfectorySettings settings = ConfectorySettings.instance(); @AfterEach void reset() @@ -50,20 +53,39 @@ void reset() } @Test - void setDefaultDataFetchStrategy_null_exceptionAndNoChangePerformed() + void setDataFetchStrategy_null_exceptionAndNoChangePerformed() + { + assertThat(settings.getDataFetchStrategy(), is(INITIAL_DATA_FETCH_STRATEGY)); + assertThat(() -> settings.setDataFetchStrategy(null), + throwsException(NullPointerException.class) + .withMessageContaining("DataFetchStrategy must not be null")); + assertThat(settings.getDataFetchStrategy(), is(INITIAL_DATA_FETCH_STRATEGY)); + } + + @Test + void setDataFetchStrategy_valid_success() + { + assertThat(settings.getDataFetchStrategy(), is(INITIAL_DATA_FETCH_STRATEGY)); + settings.setDataFetchStrategy(dataFetchStrategy); + assertThat(settings.getDataFetchStrategy(), is(dataFetchStrategy)); + } + + @Test + void setDefaultObjectFactory_null_exceptionAndNoChangePerformed() { - assertThat(settings.getDefaultDataFetchStrategy(), is(INITIAL_DATA_FETCH_STRATEGY)); - assertThat(() -> settings.setDefaultDataFetchStrategy(null), throwsException(NullPointerException.class) - .withMessageContaining("DataFetchStrategy must not be null")); - assertThat(settings.getDefaultDataFetchStrategy(), is(INITIAL_DATA_FETCH_STRATEGY)); + assertThat(settings.getObjectFactory(), is(INITIAL_OBJECT_FACTORY)); + assertThat(() -> settings.setObjectFactory(null), + throwsException(NullPointerException.class) + .withMessageContaining("ObjectFactory must not be null")); + assertThat(settings.getObjectFactory(), is(INITIAL_OBJECT_FACTORY)); } @Test - void setDefaultDataFetchStrategy_valid_success() + void setObjectFactory_valid_success() { - assertThat(settings.getDefaultDataFetchStrategy(), is(INITIAL_DATA_FETCH_STRATEGY)); - settings.setDefaultDataFetchStrategy(dataFetchStrategy); - assertThat(settings.getDefaultDataFetchStrategy(), is(dataFetchStrategy)); + assertThat(settings.getObjectFactory(), is(INITIAL_OBJECT_FACTORY)); + settings.setObjectFactory(objectFactory); + assertThat(settings.getObjectFactory(), is(objectFactory)); } } diff --git a/confectory-core/src/test/java/net/obvj/confectory/testdrive/ConfectoryTestDriveContainerLenient.java b/confectory-core/src/test/java/net/obvj/confectory/testdrive/ConfectoryTestDriveContainerLenient.java index d4725492..52bf7431 100644 --- a/confectory-core/src/test/java/net/obvj/confectory/testdrive/ConfectoryTestDriveContainerLenient.java +++ b/confectory-core/src/test/java/net/obvj/confectory/testdrive/ConfectoryTestDriveContainerLenient.java @@ -29,7 +29,7 @@ public class ConfectoryTestDriveContainerLenient { public static void main(String[] args) { - ConfectorySettings.getInstance().setDefaultDataFetchStrategy(DataFetchStrategy.LENIENT); + ConfectorySettings.instance().setDataFetchStrategy(DataFetchStrategy.LENIENT); Configuration config1 = Configuration.builder() .namespace("test") diff --git a/confectory-core/src/test/java/net/obvj/confectory/util/UnsafeAccessorTest.java b/confectory-core/src/test/java/net/obvj/confectory/util/UnsafeAccessorTest.java new file mode 100644 index 00000000..bbd7000a --- /dev/null +++ b/confectory-core/src/test/java/net/obvj/confectory/util/UnsafeAccessorTest.java @@ -0,0 +1,39 @@ +/* + * 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 static net.obvj.junit.utils.matchers.AdvancedMatchers.instantiationNotAllowed; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the {@link UnsafeAccessor} class. + * + * @author oswaldo.bapvic.jr + * @since 2.5.0 + */ +class UnsafeAccessorTest +{ + @Test + void constructor_instantiationNotAllowed() + { + assertThat(UnsafeAccessor.class, + instantiationNotAllowed().throwing(UnsupportedOperationException.class)); + } + +}