diff --git a/core/src/main/java/org/spongepowered/configurate/AbstractConfigurationNode.java b/core/src/main/java/org/spongepowered/configurate/AbstractConfigurationNode.java index 91643edb1..807561c4c 100644 --- a/core/src/main/java/org/spongepowered/configurate/AbstractConfigurationNode.java +++ b/core/src/main/java/org/spongepowered/configurate/AbstractConfigurationNode.java @@ -117,7 +117,7 @@ protected AbstractConfigurationNode(final @Nullable A parent, final A copyOf) { */ static V storeDefault(final ConfigurationNode node, final V defValue) { requireNonNull(defValue, "defValue"); - if (node.getOptions().shouldCopyDefaults()) { + if (node.getOptions().getShouldCopyDefaults()) { node.setValue(defValue); } return defValue; @@ -125,7 +125,7 @@ static V storeDefault(final ConfigurationNode node, final V defValue) { static V storeDefault(final ConfigurationNode node, final Type type, final V defValue) throws ObjectMappingException { requireNonNull(defValue, "defValue"); - if (node.getOptions().shouldCopyDefaults()) { + if (node.getOptions().getShouldCopyDefaults()) { node.setValue(type, defValue); } return defValue; @@ -143,11 +143,17 @@ static V storeDefault(final ConfigurationNode node, final Type type, final V throw new IllegalArgumentException("Raw types are not supported"); } - if (isVirtual()) { + final @Nullable TypeSerializer serial = getOptions().getSerializers().get(type); + if (this.value instanceof NullConfigValue) { + if (serial != null && getOptions().isImplicitInitialization()) { + final @Nullable Object emptyValue = serial.emptyValue(type, this.options); + if (emptyValue != null) { + return storeDefault(this, type, emptyValue); + } + } return null; } - final @Nullable TypeSerializer serial = getOptions().getSerializers().get(type); if (serial == null) { final @Nullable Object value = getValue(); final Class erasure = erase(type); diff --git a/core/src/main/java/org/spongepowered/configurate/ConfigurationNode.java b/core/src/main/java/org/spongepowered/configurate/ConfigurationNode.java index 36173c468..cb106d944 100644 --- a/core/src/main/java/org/spongepowered/configurate/ConfigurationNode.java +++ b/core/src/main/java/org/spongepowered/configurate/ConfigurationNode.java @@ -634,7 +634,7 @@ default String getString(final String def) { if (value != null) { return value; } - if (getOptions().shouldCopyDefaults()) { + if (getOptions().getShouldCopyDefaults()) { setValue(def); } return def; @@ -662,7 +662,7 @@ default float getFloat(float def) { if (val != null) { return val; } - if (getOptions().shouldCopyDefaults() && def != NUMBER_DEF) { + if (getOptions().getShouldCopyDefaults() && def != NUMBER_DEF) { setValue(def); } return def; @@ -690,7 +690,7 @@ default double getDouble(double def) { if (val != null) { return val; } - if (getOptions().shouldCopyDefaults() && def != NUMBER_DEF) { + if (getOptions().getShouldCopyDefaults() && def != NUMBER_DEF) { setValue(def); } return def; @@ -718,7 +718,7 @@ default int getInt(int def) { if (val != null) { return val; } - if (getOptions().shouldCopyDefaults() && def != NUMBER_DEF) { + if (getOptions().getShouldCopyDefaults() && def != NUMBER_DEF) { setValue(def); } return def; @@ -746,7 +746,7 @@ default long getLong(long def) { if (val != null) { return val; } - if (getOptions().shouldCopyDefaults() && def != NUMBER_DEF) { + if (getOptions().getShouldCopyDefaults() && def != NUMBER_DEF) { setValue(def); } return def; @@ -774,7 +774,7 @@ default boolean getBoolean(boolean def) { if (val != null) { return val; } - if (getOptions().shouldCopyDefaults()) { + if (getOptions().getShouldCopyDefaults()) { setValue(def); } return def; diff --git a/core/src/main/java/org/spongepowered/configurate/ConfigurationOptions.java b/core/src/main/java/org/spongepowered/configurate/ConfigurationOptions.java index 715fb36d9..8c7950b4e 100644 --- a/core/src/main/java/org/spongepowered/configurate/ConfigurationOptions.java +++ b/core/src/main/java/org/spongepowered/configurate/ConfigurationOptions.java @@ -18,6 +18,7 @@ import static java.util.Objects.requireNonNull; +import com.google.auto.value.AutoValue; import org.checkerframework.checker.nullness.qual.Nullable; import org.spongepowered.configurate.loader.ConfigurationLoader; import org.spongepowered.configurate.serialize.TypeSerializerCollection; @@ -40,24 +41,19 @@ * *

This class is immutable.

*/ -public final class ConfigurationOptions { - - private static final ConfigurationOptions DEFAULTS = new ConfigurationOptions(MapFactories.insertionOrdered(), null, - TypeSerializerCollection.defaults(), null, false); - - private final MapFactory mapFactory; - private final @Nullable String header; - private final TypeSerializerCollection serializers; - private final @Nullable Set> acceptedTypes; - private final boolean shouldCopyDefaults; - - private ConfigurationOptions(final MapFactory mapFactory, final @Nullable String header, final TypeSerializerCollection serializers, - final @Nullable Set> acceptedTypes, final boolean shouldCopyDefaults) { - this.mapFactory = mapFactory; - this.header = header; - this.serializers = serializers; - this.acceptedTypes = acceptedTypes == null ? null : UnmodifiableCollections.copyOf(acceptedTypes); - this.shouldCopyDefaults = shouldCopyDefaults; +@AutoValue +public abstract class ConfigurationOptions { + + static class Lazy { + + // avoid initialization cycles + + static final ConfigurationOptions DEFAULTS = new AutoValue_ConfigurationOptions(MapFactories.insertionOrdered(), null, + TypeSerializerCollection.defaults(), null, false, false); + + } + + ConfigurationOptions() { } /** @@ -69,7 +65,7 @@ private ConfigurationOptions(final MapFactory mapFactory, final @Nullable String * @return the default options */ public static ConfigurationOptions defaults() { - return DEFAULTS; + return Lazy.DEFAULTS; } /** @@ -77,9 +73,7 @@ public static ConfigurationOptions defaults() { * * @return The map factory */ - public MapFactory getMapFactory() { - return this.mapFactory; - } + public abstract MapFactory getMapFactory(); /** * Creates a new {@link ConfigurationOptions} instance, with the specified @@ -90,10 +84,11 @@ public MapFactory getMapFactory() { */ public ConfigurationOptions withMapFactory(final MapFactory mapFactory) { requireNonNull(mapFactory, "mapFactory"); - if (this.mapFactory == mapFactory) { + if (this.getMapFactory() == mapFactory) { return this; } - return new ConfigurationOptions(mapFactory, this.header, this.serializers, this.acceptedTypes, this.shouldCopyDefaults); + return new AutoValue_ConfigurationOptions(mapFactory, getHeader(), getSerializers(), getNativeTypes(), + getShouldCopyDefaults(), isImplicitInitialization()); } /** @@ -101,9 +96,7 @@ public ConfigurationOptions withMapFactory(final MapFactory mapFactory) { * * @return The current header. Lines are split by \n, */ - public @Nullable String getHeader() { - return this.header; - } + public abstract @Nullable String getHeader(); /** * Creates a new {@link ConfigurationOptions} instance, with the specified @@ -113,10 +106,11 @@ public ConfigurationOptions withMapFactory(final MapFactory mapFactory) { * @return The new options object */ public ConfigurationOptions withHeader(final @Nullable String header) { - if (Objects.equals(this.header, header)) { + if (Objects.equals(this.getHeader(), header)) { return this; } - return new ConfigurationOptions(this.mapFactory, header, this.serializers, this.acceptedTypes, this.shouldCopyDefaults); + return new AutoValue_ConfigurationOptions(getMapFactory(), header, getSerializers(), getNativeTypes(), + getShouldCopyDefaults(), isImplicitInitialization()); } /** @@ -124,9 +118,7 @@ public ConfigurationOptions withHeader(final @Nullable String header) { * * @return The type serializers */ - public TypeSerializerCollection getSerializers() { - return this.serializers; - } + public abstract TypeSerializerCollection getSerializers(); /** * Creates a new {@link ConfigurationOptions} instance, with the specified {@link TypeSerializerCollection} @@ -137,10 +129,11 @@ public TypeSerializerCollection getSerializers() { */ public ConfigurationOptions withSerializers(final TypeSerializerCollection serializers) { requireNonNull(serializers, "serializers"); - if (this.serializers.equals(serializers)) { + if (this.getSerializers().equals(serializers)) { return this; } - return new ConfigurationOptions(this.mapFactory, this.header, serializers, this.acceptedTypes, this.shouldCopyDefaults); + return new AutoValue_ConfigurationOptions(getMapFactory(), getHeader(), serializers, getNativeTypes(), + getShouldCopyDefaults(), isImplicitInitialization()); } /** @@ -153,13 +146,16 @@ public ConfigurationOptions withSerializers(final TypeSerializerCollection seria * be used in the returned options object. * @return The new options object */ - public ConfigurationOptions withSerializers(final Consumer serializerBuilder) { + public final ConfigurationOptions withSerializers(final Consumer serializerBuilder) { requireNonNull(serializerBuilder, "serializerBuilder"); - final TypeSerializerCollection.Builder builder = this.serializers.childBuilder(); + final TypeSerializerCollection.Builder builder = this.getSerializers().childBuilder(); serializerBuilder.accept(builder); - return new ConfigurationOptions(this.mapFactory, this.header, builder.build(), this.acceptedTypes, this.shouldCopyDefaults); + return withSerializers(builder.build()); } + @SuppressWarnings("AutoValueImmutableFields") // we don't use guava + abstract @Nullable Set> getNativeTypes(); + /** * Gets whether objects of the provided type are natively accepted as values * for nodes with this as their options object. @@ -167,26 +163,29 @@ public ConfigurationOptions withSerializers(final Consumer type) { + public final boolean acceptsType(final Class type) { requireNonNull(type, "type"); - if (this.acceptedTypes == null) { + final @Nullable Set> nativeTypes = getNativeTypes(); + + if (nativeTypes == null) { return true; } - if (this.acceptedTypes.contains(type)) { + + if (nativeTypes.contains(type)) { return true; } - if (type.isPrimitive() && this.acceptedTypes.contains(Typing.box(type))) { + if (type.isPrimitive() && nativeTypes.contains(Typing.box(type))) { return true; } final Type unboxed = Typing.unbox(type); - if (unboxed != type && this.acceptedTypes.contains(unboxed)) { + if (unboxed != type && nativeTypes.contains(unboxed)) { return true; } - for (Class clazz : this.acceptedTypes) { + for (Class clazz : nativeTypes) { if (clazz.isAssignableFrom(type)) { return true; } @@ -204,14 +203,15 @@ public boolean acceptsType(final Class type) { * *

Null indicates that all types are accepted.

* - * @param acceptedTypes The types that will be accepted to a call to {@link ConfigurationNode#setValue(Object)} + * @param nativeTypes The types that will be accepted to a call to {@link ConfigurationNode#setValue(Object)} * @return updated options object */ - public ConfigurationOptions withNativeTypes(final @Nullable Set> acceptedTypes) { - if (Objects.equals(this.acceptedTypes, acceptedTypes)) { + public ConfigurationOptions withNativeTypes(final @Nullable Set> nativeTypes) { + if (Objects.equals(this.getNativeTypes(), nativeTypes)) { return this; } - return new ConfigurationOptions(this.mapFactory, this.header, this.serializers, acceptedTypes, this.shouldCopyDefaults); + return new AutoValue_ConfigurationOptions(getMapFactory(), getHeader(), getSerializers(), + nativeTypes == null ? null : UnmodifiableCollections.copyOf(nativeTypes), getShouldCopyDefaults(), isImplicitInitialization()); } /** @@ -220,57 +220,54 @@ public ConfigurationOptions withNativeTypes(final @Nullable Set> accept * * @return Whether defaults should be copied into value */ - public boolean shouldCopyDefaults() { - return this.shouldCopyDefaults; - } + public abstract boolean getShouldCopyDefaults(); /** - * Creates a new {@link ConfigurationOptions} instance, with the specified 'copy defaults' setting - * set, and all other settings copied from this instance. + * Creates a new {@link ConfigurationOptions} instance, with the specified + * 'copy defaults' setting set, and all other settings copied from + * this instance. * - * @see #shouldCopyDefaults() for information on what this method does + * @see #getShouldCopyDefaults() for information on what this method does * @param shouldCopyDefaults whether to copy defaults * @return updated options object */ public ConfigurationOptions withShouldCopyDefaults(final boolean shouldCopyDefaults) { - if (this.shouldCopyDefaults == shouldCopyDefaults) { + if (this.getShouldCopyDefaults() == shouldCopyDefaults) { return this; } - return new ConfigurationOptions(this.mapFactory, this.header, this.serializers, this.acceptedTypes, shouldCopyDefaults); + + return new AutoValue_ConfigurationOptions(getMapFactory(), getHeader(), getSerializers(), getNativeTypes(), + shouldCopyDefaults, isImplicitInitialization()); } - @Override - public boolean equals(final Object other) { - if (this == other) { - return true; - } + /** + * Get whether values should be implicitly initialized. + * + *

When this is true, any value get operations will return an empty value + * rather than null. This extends through to fields loaded into + * object-mapped classes.

+ * + *

This option is disabled by default

+ * + * @return if implicit initialization is enabled. + */ + public abstract boolean isImplicitInitialization(); - if (!(other instanceof ConfigurationOptions)) { - return false; + /** + * Create a new {@link ConfigurationOptions} instance with the specified + * implicit initialization setting. + * + * @param implicitInitialization whether to initialize implicitly + * @return a new options object + * @see #isImplicitInitialization() for more details + */ + public ConfigurationOptions withImplicitInitialization(final boolean implicitInitialization) { + if (this.isImplicitInitialization() == implicitInitialization) { + return this; } - final ConfigurationOptions that = (ConfigurationOptions) other; - return Objects.equals(this.shouldCopyDefaults, that.shouldCopyDefaults) - && Objects.equals(this.mapFactory, that.mapFactory) - && Objects.equals(this.header, that.header) - && Objects.equals(this.serializers, that.serializers) - && Objects.equals(this.acceptedTypes, that.acceptedTypes); - } - - @Override - public int hashCode() { - return Objects.hash(this.mapFactory, this.header, this.serializers, this.acceptedTypes, this.shouldCopyDefaults); - } - - @Override - public String toString() { - return "ConfigurationOptions{" - + "mapFactory=" + this.mapFactory - + ", header='" + this.header + '\'' - + ", serializers=" + this.serializers - + ", acceptedTypes=" + this.acceptedTypes - + ", shouldCopyDefaults=" + this.shouldCopyDefaults - + '}'; + return new AutoValue_ConfigurationOptions(getMapFactory(), getHeader(), getSerializers(), getNativeTypes(), + getShouldCopyDefaults(), implicitInitialization); } } diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/FieldData.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/FieldData.java index 1a0a679e0..6d386a90d 100644 --- a/core/src/main/java/org/spongepowered/configurate/objectmapping/FieldData.java +++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/FieldData.java @@ -31,7 +31,7 @@ import java.lang.reflect.AnnotatedType; import java.util.List; -import java.util.function.BiConsumer; +import java.util.function.Supplier; /** * Holder for field-specific information. @@ -55,8 +55,8 @@ public abstract class FieldData { * @return new field data */ static FieldData of(final String name, final AnnotatedType resolvedFieldType, - final List> constraints, final List> processors, final BiConsumer deserializer, - final CheckedFunction serializer, final NodeResolver resolver) { + final List> constraints, final List> processors, + final Deserializer deserializer, final CheckedFunction serializer, final NodeResolver resolver) { return new AutoValue_FieldData<>(name, resolvedFieldType, UnmodifiableCollections.copyOf(constraints), UnmodifiableCollections.copyOf(processors), @@ -87,7 +87,7 @@ static FieldData of(final String name, final AnnotatedType resolved abstract List> processors(); - abstract BiConsumer deserializer(); + abstract Deserializer deserializer(); abstract CheckedFunction serializer(); @@ -143,4 +143,23 @@ TypeSerializer serializerFrom(final ConfigurationNode node) throws ObjectMapp return this.nodeResolver().resolve(source); } + /** + * A deserialization handler to appropriately place object data into fields. + * + * @param intermediate data type + */ + @FunctionalInterface + public interface Deserializer { + + /** + * Apply either a new value or implicit initializer to the + * {@code intermediate} object as appropriate. + * + * @param intermediate the intermediate container + * @param newValue new value to store + * @param implicitInitializer the implicit initializer + */ + void accept(I intermediate, @Nullable Object newValue, Supplier<@Nullable Object> implicitInitializer); + } + } diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/FieldDiscoverer.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/FieldDiscoverer.java index 4ab45177a..b35c08b78 100644 --- a/core/src/main/java/org/spongepowered/configurate/objectmapping/FieldDiscoverer.java +++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/FieldDiscoverer.java @@ -23,7 +23,6 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedType; -import java.util.function.BiConsumer; import java.util.function.Supplier; /** @@ -155,7 +154,7 @@ interface FieldCollector { * @param serializer a function to extract a value from a completed * object instance. */ - void accept(String name, AnnotatedType type, AnnotatedElement enclosing, BiConsumer deserializer, + void accept(String name, AnnotatedType type, AnnotatedElement enclosing, FieldData.Deserializer deserializer, CheckedFunction serializer); } diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectFieldDiscoverer.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectFieldDiscoverer.java index ab52517b6..242190337 100644 --- a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectFieldDiscoverer.java +++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectFieldDiscoverer.java @@ -90,7 +90,17 @@ public Map begin() { public void complete(final Object instance, final Map intermediate) throws ObjectMappingException { for (Map.Entry entry : intermediate.entrySet()) { try { - entry.getKey().set(instance, entry.getValue()); + // Handle implicit field initialization by detecting any existing information in the object + if (entry.getValue() instanceof ImplicitProvider) { + final @Nullable Object implicit = ((ImplicitProvider) entry.getValue()).provider.get(); + if (implicit != null) { + if (entry.getKey().get(instance) == null) { + entry.getKey().set(instance, implicit); + } + } + } else { + entry.getKey().set(instance, entry.getValue()); + } } catch (final IllegalAccessException e) { throw new ObjectMappingException(e); } @@ -124,12 +134,24 @@ private void collectFields(final AnnotatedType clazz, final FieldCollector { + (intermediate, val, implicitProvider) -> { if (val != null) { intermediate.put(field, val); + } else { + intermediate.put(field, new ImplicitProvider(implicitProvider)); } }, field::get); } } + static class ImplicitProvider { + + final Supplier provider; + + ImplicitProvider(final Supplier provider) { + this.provider = provider; + } + + } + } diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperFactoryImpl.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperFactoryImpl.java index 729bd1c24..58c7e4bbf 100644 --- a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperFactoryImpl.java +++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperFactoryImpl.java @@ -25,7 +25,9 @@ import io.leangen.geantyref.GenericTypeReflector; import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.BasicConfigurationNode; import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.ConfigurationOptions; import org.spongepowered.configurate.objectmapping.meta.Comment; import org.spongepowered.configurate.objectmapping.meta.Constraint; import org.spongepowered.configurate.objectmapping.meta.Matches; @@ -48,7 +50,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.function.BiConsumer; /** * Factory for a basic {@link ObjectMapper}. @@ -149,7 +150,7 @@ private ObjectMapper computeMapper(final Type type) throws ObjectMappingExcep */ @SuppressWarnings({"unchecked", "rawtypes"}) private void makeData(final List> fields, final String name, final AnnotatedType type, - final AnnotatedElement container, final BiConsumer deserializer, final CheckedFunction serializer) { + final AnnotatedElement container, final FieldData.Deserializer deserializer, final CheckedFunction serializer) { @Nullable NodeResolver resolver = null; for (NodeResolver.Factory factory : this.resolverFactories) { final @Nullable NodeResolver next = factory.make(name, container); @@ -247,6 +248,16 @@ public void serialize(final Type type, final @Nullable Object obj, final Configu ((ObjectMapper) mapper).save(obj, node); } + @Override + public @Nullable Object emptyValue(final Type specificType, final ConfigurationOptions options) { + try { + // preserve options, but don't copy defaults into temporary node + return get(specificType).load(BasicConfigurationNode.root(options.withShouldCopyDefaults(false))); + } catch (final ObjectMappingException ex) { + return null; + } + } + // Helpers to get value from map /** diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperImpl.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperImpl.java index 6d8f88f21..fb444b5b4 100644 --- a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperImpl.java +++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperImpl.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Supplier; class ObjectMapperImpl implements ObjectMapper { @@ -62,15 +63,25 @@ final V load0(final ConfigurationNode source, final CheckedFunction implicitInitializer; + if (newVal == null && node.getOptions().isImplicitInitialization()) { + implicitInitializer = () -> serial.emptyValue(field.resolvedType().getType(), node.getOptions()); + } else { + implicitInitializer = () -> null; + } + + // load field into intermediate object + field.deserializer().accept(intermediate, newVal, implicitInitializer); + + if (newVal == null && source.getOptions().getShouldCopyDefaults()) { if (unseenFields == null) { unseenFields = new ArrayList<>(); } unseenFields.add(field); } - - // load field into intermediate object - field.deserializer().accept(intermediate, newVal); } catch (final ObjectMappingException ex) { if (failure == null) { failure = ex; @@ -84,9 +95,8 @@ final V load0(final ConfigurationNode source, final CheckedFunction field : unseenFields) { saveSingle(field, complete, source); } diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/RecordFieldDiscoverer.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/RecordFieldDiscoverer.java index da4404c26..7b333b353 100644 --- a/core/src/main/java/org/spongepowered/configurate/objectmapping/RecordFieldDiscoverer.java +++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/RecordFieldDiscoverer.java @@ -114,7 +114,13 @@ private RecordFieldDiscoverer() { final AnnotatedElement annotationContainer = Typing.combinedAnnotations(component, backingField, accessor); final int targetIdx = i; collector.accept(name, resolvedType, annotationContainer, - (intermediate, el) -> intermediate[targetIdx] = el, accessor::invoke); + (intermediate, el, implicitSupplier) -> { + if (el != null) { + intermediate[targetIdx] = el; + } else { + intermediate[targetIdx] = implicitSupplier.get(); + } + }, accessor::invoke); } // canonical constructor, which we'll use to make new instances @@ -127,7 +133,8 @@ public Object[] begin() { return new Object[recordComponents.length]; } - @Override public Object complete(final Object[] intermediate) throws ObjectMappingException { + @Override + public Object complete(final Object[] intermediate) throws ObjectMappingException { try { return clazzConstructor.newInstance(intermediate); } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { @@ -135,7 +142,8 @@ public Object[] begin() { } } - @Override public boolean canCreateInstances() { + @Override + public boolean canCreateInstances() { return true; } }; diff --git a/core/src/main/java/org/spongepowered/configurate/reference/ValueReferenceImpl.java b/core/src/main/java/org/spongepowered/configurate/reference/ValueReferenceImpl.java index 1619a8762..7761881fc 100644 --- a/core/src/main/java/org/spongepowered/configurate/reference/ValueReferenceImpl.java +++ b/core/src/main/java/org/spongepowered/configurate/reference/ValueReferenceImpl.java @@ -72,7 +72,7 @@ class ValueReferenceImpl<@Nullable T, N extends ScopedConfigurationNode> impl final @Nullable T possible = this.serializer.deserialize(this.type.getType(), node); if (possible != null) { return possible; - } else if (defaultVal != null && node.getOptions().shouldCopyDefaults()) { + } else if (defaultVal != null && node.getOptions().getShouldCopyDefaults()) { this.serializer.serialize(this.type.getType(), defaultVal, node); } return defaultVal; diff --git a/core/src/main/java/org/spongepowered/configurate/serialize/AbstractListChildSerializer.java b/core/src/main/java/org/spongepowered/configurate/serialize/AbstractListChildSerializer.java index 62e6f48f5..eef135af3 100644 --- a/core/src/main/java/org/spongepowered/configurate/serialize/AbstractListChildSerializer.java +++ b/core/src/main/java/org/spongepowered/configurate/serialize/AbstractListChildSerializer.java @@ -18,6 +18,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.ConfigurationOptions; import org.spongepowered.configurate.objectmapping.ObjectMappingException; import org.spongepowered.configurate.util.CheckedConsumer; @@ -74,6 +75,15 @@ public void serialize(final Type type, final @Nullable T obj, final Configuratio } } + @Override + public @Nullable T emptyValue(final Type specificType, final ConfigurationOptions options) { + try { + return this.createNew(0, getElementType(specificType)); + } catch (final ObjectMappingException ex) { + return null; + } + } + /** * Given the type of container, provide the expected type of an element. If * the element type is not available, an exception must be thrown. diff --git a/core/src/main/java/org/spongepowered/configurate/serialize/ConfigurationNodeSerializer.java b/core/src/main/java/org/spongepowered/configurate/serialize/ConfigurationNodeSerializer.java index 4b575d2c9..4769a1d84 100644 --- a/core/src/main/java/org/spongepowered/configurate/serialize/ConfigurationNodeSerializer.java +++ b/core/src/main/java/org/spongepowered/configurate/serialize/ConfigurationNodeSerializer.java @@ -18,7 +18,9 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.BasicConfigurationNode; import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.ConfigurationOptions; import java.lang.reflect.Type; @@ -43,4 +45,9 @@ public void serialize(final @NonNull Type type, final @Nullable ConfigurationNod node.setValue(obj); } + @Override + public @Nullable ConfigurationNode emptyValue(final Type specificType, final ConfigurationOptions options) { + return BasicConfigurationNode.root(options); + } + } diff --git a/core/src/main/java/org/spongepowered/configurate/serialize/MapSerializer.java b/core/src/main/java/org/spongepowered/configurate/serialize/MapSerializer.java index 9d274d466..a6d4c3f3d 100644 --- a/core/src/main/java/org/spongepowered/configurate/serialize/MapSerializer.java +++ b/core/src/main/java/org/spongepowered/configurate/serialize/MapSerializer.java @@ -22,6 +22,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.spongepowered.configurate.BasicConfigurationNode; import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.ConfigurationOptions; import org.spongepowered.configurate.objectmapping.ObjectMappingException; import java.lang.reflect.ParameterizedType; @@ -111,4 +112,9 @@ public void serialize(final Type type, final @Nullable Map obj, final Conf } } + @Override + public Map emptyValue(final Type specificType, final ConfigurationOptions options) { + return new LinkedHashMap<>(); + } + } diff --git a/core/src/main/java/org/spongepowered/configurate/serialize/TypeSerializer.java b/core/src/main/java/org/spongepowered/configurate/serialize/TypeSerializer.java index 71a37163e..8cb32ff18 100644 --- a/core/src/main/java/org/spongepowered/configurate/serialize/TypeSerializer.java +++ b/core/src/main/java/org/spongepowered/configurate/serialize/TypeSerializer.java @@ -18,6 +18,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.ConfigurationOptions; import org.spongepowered.configurate.objectmapping.ObjectMappingException; import org.spongepowered.configurate.util.CheckedFunction; @@ -93,4 +94,18 @@ static ScalarSerializer of(Class type, */ void serialize(Type type, @Nullable T obj, ConfigurationNode node) throws ObjectMappingException; + /** + * Create an empty value of the appropriate type. + * + *

This method is for the most part designed to create empty collection + * types, though it may be useful for scalars in limited cases.

+ * + * @param specificType specific subtype to create an empty value of + * @param options options used from the loading node + * @return new empty value + */ + default @Nullable T emptyValue(final Type specificType, ConfigurationOptions options) { + return null; + } + } diff --git a/core/src/test/java/org/spongepowered/configurate/AbstractConfigurationNodeTest.java b/core/src/test/java/org/spongepowered/configurate/AbstractConfigurationNodeTest.java index d25d1c46f..b285b9cdc 100644 --- a/core/src/test/java/org/spongepowered/configurate/AbstractConfigurationNodeTest.java +++ b/core/src/test/java/org/spongepowered/configurate/AbstractConfigurationNodeTest.java @@ -29,6 +29,7 @@ import com.google.common.collect.ImmutableMap; import io.leangen.geantyref.TypeToken; import org.junit.jupiter.api.Test; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.ObjectMappingException; import org.spongepowered.configurate.transformation.NodePath; import org.spongepowered.configurate.util.UnmodifiableCollections; @@ -36,9 +37,11 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; import java.util.stream.IntStream; @@ -399,7 +402,32 @@ public void testCollectToList() throws ObjectMappingException { .collect(BasicConfigurationNode.factory().toListCollector(Integer.class)); assertEquals(ImmutableList.of(1, 2, 3, 4, 8), target.getList(TypeToken.get(Integer.class))); + } + + @ConfigSerializable + static class Empty { + String ignoreMe = "hello"; + + @Override + public boolean equals(final Object that) { + return that instanceof Empty + && Objects.equals(this.ignoreMe, ((Empty) that).ignoreMe); + } + + @Override + public int hashCode() { + return 31 * Objects.hashCode(this.ignoreMe); + } + } + + @Test + void testImplicitInitialization() throws ObjectMappingException { + final BasicConfigurationNode node = BasicConfigurationNode.root(ConfigurationOptions.defaults().withImplicitInitialization(true)); + assertNull(node.getValue()); + assertEquals(Collections.emptyList(), node.getValue(new TypeToken>() {})); + assertEquals(Collections.emptyMap(), node.getValue(new TypeToken>() {})); + assertEquals(new Empty(), node.getValue(Empty.class)); } } diff --git a/core/src/test/java/org/spongepowered/configurate/objectmapping/DefaultsTest.java b/core/src/test/java/org/spongepowered/configurate/objectmapping/DefaultsTest.java new file mode 100644 index 000000000..a1f820daa --- /dev/null +++ b/core/src/test/java/org/spongepowered/configurate/objectmapping/DefaultsTest.java @@ -0,0 +1,76 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.objectmapping; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.spongepowered.configurate.BasicConfigurationNode; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.ConfigurationOptions; + +import java.util.Collections; +import java.util.List; + +/** + * Tests for application of defaults + */ +public class DefaultsTest { + + public static final ConfigurationOptions IMPLICIT_OPTS = ConfigurationOptions.defaults() + .withImplicitInitialization(true); + + @ConfigSerializable + static class ImplicitDefaultsOnly { + List myStrings; + AnotherThing funTimes; + int[] items; + } + + @ConfigSerializable + static class AnotherThing { + + } + + @Test + void testFieldsInitialized() throws ObjectMappingException { + final ImplicitDefaultsOnly instance = ObjectMapper.factory().get(ImplicitDefaultsOnly.class).load(BasicConfigurationNode.root(IMPLICIT_OPTS)); + assertEquals(Collections.emptyList(), instance.myStrings); + assertNotNull(instance.funTimes); + assertNotNull(instance.items); + assertEquals(0, instance.items.length); + } + + @Test + void testImplicitDefaultsSaved() throws ObjectMappingException { + final BasicConfigurationNode node = BasicConfigurationNode.root(IMPLICIT_OPTS.withShouldCopyDefaults(true)); + node.getValue(ImplicitDefaultsOnly.class); + + assertPresentAndEmpty(node.getNode("my-strings")); + assertPresentAndEmpty(node.getNode("fun-times")); + assertPresentAndEmpty(node.getNode("items")); + } + + private void assertPresentAndEmpty(final ConfigurationNode node) { + assertFalse(node.isVirtual()); + assertTrue(node.isEmpty()); + } + +} diff --git a/core/src/test/java14/org/spongepowered/configurate/objectmapping/RecordDiscovererTest.java b/core/src/test/java14/org/spongepowered/configurate/objectmapping/RecordDiscovererTest.java index c576b0481..78eae87cf 100644 --- a/core/src/test/java14/org/spongepowered/configurate/objectmapping/RecordDiscovererTest.java +++ b/core/src/test/java14/org/spongepowered/configurate/objectmapping/RecordDiscovererTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import org.checkerframework.checker.nullness.qual.Nullable; import org.junit.jupiter.api.Test; import org.spongepowered.configurate.BasicConfigurationNode; import org.spongepowered.configurate.CommentedConfigurationNode; @@ -81,4 +82,37 @@ final var record = new AnnotatedRecord(new TestRecord("nested", 0xFACE), assertEquals("The most url", target.getNode("fetch-loc").getComment()); } + @ConfigSerializable + record Empty(@Nullable String value) { + + @SuppressWarnings("checkstyle:RequireThis") // TODO remove when https://github.com/checkstyle/checkstyle/issues/8873 is resolved + public Empty { + if (value == null) { + value = ""; + } + } + + } + + @ConfigSerializable + record ImplicitlyFillable(Empty something, Set somethingElse) { + + @SuppressWarnings("checkstyle:RequireThis") // TODO remove when https://github.com/checkstyle/checkstyle/issues/8873 is resolved + public ImplicitlyFillable { + somethingElse = Set.copyOf(somethingElse); + } + + } + + @Test + void testImplicitDefaultsLoaded() throws ObjectMappingException { + final var filled = + ObjectMapper.factory().get(ImplicitlyFillable.class) + .load(BasicConfigurationNode.root(ConfigurationOptions.defaults() + .withImplicitInitialization(true))); + + assertEquals(new Empty(""), filled.something()); + assertEquals(Set.of(), filled.somethingElse()); + } + } diff --git a/etc/checkstyle/checkstyle.xml b/etc/checkstyle/checkstyle.xml index e617f1b1a..42959d283 100644 --- a/etc/checkstyle/checkstyle.xml +++ b/etc/checkstyle/checkstyle.xml @@ -72,6 +72,7 @@ + @@ -99,6 +100,7 @@ + diff --git a/extra/kotlin/src/main/kotlin/org/spongepowered/configurate/kotlin/ObjectMapping.kt b/extra/kotlin/src/main/kotlin/org/spongepowered/configurate/kotlin/ObjectMapping.kt index 006609cc8..2c15e9be5 100644 --- a/extra/kotlin/src/main/kotlin/org/spongepowered/configurate/kotlin/ObjectMapping.kt +++ b/extra/kotlin/src/main/kotlin/org/spongepowered/configurate/kotlin/ObjectMapping.kt @@ -42,10 +42,21 @@ import kotlin.reflect.jvm.javaMethod private val dataClassMapperFactory = ObjectMapper.factoryBuilder().addDiscoverer(DataClassFieldDiscoverer).build() +/** + * Get an object mapper factory with standard capabilities and settings, except + * for the added ability to interpret data clasess with a [dataClassFieldDiscoverer]. + */ fun objectMapperFactory(): Factory { return dataClassMapperFactory } +/** + * Get a field discoverer that can determine field information from data classes. + */ +fun dataClassFieldDiscoverer(): FieldDiscoverer<*> { + return DataClassFieldDiscoverer +} + /** * Get an object mapper for the type [T] using the default object mapper factory */ @@ -90,11 +101,11 @@ annotation class Fancy(val value: String, val message: String = "") * * See [KT-39369](https://youtrack.jetbrains.com/issue/KT-39369) for details. */ -object DataClassFieldDiscoverer : FieldDiscoverer> { +private object DataClassFieldDiscoverer : FieldDiscoverer> { override fun discover( target: AnnotatedType, - collector: FieldDiscoverer.FieldCollector, V> - ): FieldDiscoverer.InstanceFactory>? { + collector: FieldDiscoverer.FieldCollector, V> + ): FieldDiscoverer.InstanceFactory>? { val klass = erase(target.type).kotlin if (!klass.isData) { return null @@ -114,17 +125,25 @@ object DataClassFieldDiscoverer : FieldDiscoverer> { param.name, resolvedType, combinedAnnotations(param.type.javaElement, param.javaElement, field.javaField), // type, backing field, etc - { intermediate, arg -> intermediate[param] = arg }, + // deserializer + { intermediate, arg, implicitProvider -> + if (arg != null) { + intermediate[param] = arg + } else if (!param.isOptional) { + intermediate[param] = implicitProvider.get() + } + }, + // serializer { (field as KProperty1).get(it) } ) } - return object : FieldDiscoverer.InstanceFactory> { - override fun begin(): MutableMap { + return object : FieldDiscoverer.InstanceFactory> { + override fun begin(): MutableMap { return mutableMapOf() } - override fun complete(intermediate: MutableMap): Any { + override fun complete(intermediate: MutableMap): Any { return constructor.callBy(intermediate) } diff --git a/extra/kotlin/src/test/kotlin/org/spongepowered/configurate/kotlin/ObjectMappingTest.kt b/extra/kotlin/src/test/kotlin/org/spongepowered/configurate/kotlin/ObjectMappingTest.kt index 5819999b3..932acca14 100644 --- a/extra/kotlin/src/test/kotlin/org/spongepowered/configurate/kotlin/ObjectMappingTest.kt +++ b/extra/kotlin/src/test/kotlin/org/spongepowered/configurate/kotlin/ObjectMappingTest.kt @@ -21,6 +21,8 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.spongepowered.configurate.BasicConfigurationNode import org.spongepowered.configurate.CommentedConfigurationNode +import org.spongepowered.configurate.ConfigurationOptions +import org.spongepowered.configurate.objectmapping.ConfigSerializable import org.spongepowered.configurate.objectmapping.ObjectMappingException import org.spongepowered.configurate.objectmapping.meta.Comment import org.spongepowered.configurate.objectmapping.meta.Matches @@ -79,4 +81,26 @@ class ObjectMappingTest { ) } } + + // can't be local to the function: https://youtrack.jetbrains.com/issue/KT-42440 + @ConfigSerializable + data class Empty(val empty: String?) + + @ConfigSerializable + data class ImplicitTest(val test: Set, val help: Map, val empty: Empty) + + @Test + fun `collections are initialized implicitly`() { + val node = CommentedConfigurationNode.root( + ConfigurationOptions.defaults() + .withImplicitInitialization(true) + .withSerializers { it.registerAnnotatedObjects(objectMapperFactory()) } + ) + + val tester = objectMapper().load(node) + + assertEquals(setOf(), tester.test) + assertEquals(mapOf(), tester.help) + assertEquals(null, tester.empty.empty) + } }