Skip to content

Commit

Permalink
core: Allow enabling implicit field initialization.
Browse files Browse the repository at this point in the history
This means that for collection and record types,
an empty value will be returned instead of null.

Fixes #156
  • Loading branch information
zml2008 committed Oct 3, 2020
1 parent 244247f commit 06943b7
Show file tree
Hide file tree
Showing 17 changed files with 363 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,17 @@ static <V> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ static class Lazy {
// avoid initialization cycles

static final ConfigurationOptions DEFAULTS = new AutoValue_ConfigurationOptions(MapFactories.insertionOrdered(), null,
TypeSerializerCollection.defaults(), null, false);
TypeSerializerCollection.defaults(), null, false, false);

}

Expand Down Expand Up @@ -87,7 +87,8 @@ public ConfigurationOptions withMapFactory(final MapFactory mapFactory) {
if (this.getMapFactory() == mapFactory) {
return this;
}
return new AutoValue_ConfigurationOptions(mapFactory, getHeader(), getSerializers(), getNativeTypes(), shouldCopyDefaults());
return new AutoValue_ConfigurationOptions(mapFactory, getHeader(), getSerializers(), getNativeTypes(),
shouldCopyDefaults(), isImplicitInitialization());
}

/**
Expand All @@ -108,7 +109,8 @@ public ConfigurationOptions withHeader(final @Nullable String header) {
if (Objects.equals(this.getHeader(), header)) {
return this;
}
return new AutoValue_ConfigurationOptions(getMapFactory(), header, getSerializers(), getNativeTypes(), shouldCopyDefaults());
return new AutoValue_ConfigurationOptions(getMapFactory(), header, getSerializers(), getNativeTypes(),
shouldCopyDefaults(), isImplicitInitialization());
}

/**
Expand All @@ -130,7 +132,8 @@ public ConfigurationOptions withSerializers(final TypeSerializerCollection seria
if (this.getSerializers().equals(serializers)) {
return this;
}
return new AutoValue_ConfigurationOptions(getMapFactory(), getHeader(), serializers, getNativeTypes(), shouldCopyDefaults());
return new AutoValue_ConfigurationOptions(getMapFactory(), getHeader(), serializers, getNativeTypes(),
shouldCopyDefaults(), isImplicitInitialization());
}

/**
Expand Down Expand Up @@ -208,7 +211,7 @@ public ConfigurationOptions withNativeTypes(final @Nullable Set<Class<?>> native
return this;
}
return new AutoValue_ConfigurationOptions(getMapFactory(), getHeader(), getSerializers(),
nativeTypes == null ? null : UnmodifiableCollections.copyOf(nativeTypes), shouldCopyDefaults());
nativeTypes == null ? null : UnmodifiableCollections.copyOf(nativeTypes), shouldCopyDefaults(), isImplicitInitialization());
}

/**
Expand All @@ -220,8 +223,9 @@ public ConfigurationOptions withNativeTypes(final @Nullable Set<Class<?>> native
public abstract boolean shouldCopyDefaults();

/**
* 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
* @param shouldCopyDefaults whether to copy defaults
Expand All @@ -232,7 +236,38 @@ public ConfigurationOptions withShouldCopyDefaults(final boolean shouldCopyDefau
return this;
}

return new AutoValue_ConfigurationOptions(getMapFactory(), getHeader(), getSerializers(), getNativeTypes(), shouldCopyDefaults);
return new AutoValue_ConfigurationOptions(getMapFactory(), getHeader(), getSerializers(), getNativeTypes(),
shouldCopyDefaults, isImplicitInitialization());
}

/**
* Get whether values should be implicitly initialized.
*
* <p>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.</p>
*
* <p>This option is disabled by default</p>
*
* @return if implicit initialization is enabled.
*/
public abstract boolean isImplicitInitialization();

/**
* 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;
}

return new AutoValue_ConfigurationOptions(getMapFactory(), getHeader(), getSerializers(), getNativeTypes(),
shouldCopyDefaults(), implicitInitialization);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -55,8 +55,8 @@ public abstract class FieldData<I, O> {
* @return new field data
*/
static <I, O> FieldData<I, O> of(final String name, final AnnotatedType resolvedFieldType,
final List<Constraint<?>> constraints, final List<Processor<?>> processors, final BiConsumer<I, Object> deserializer,
final CheckedFunction<O, @Nullable Object, Exception> serializer, final NodeResolver resolver) {
final List<Constraint<?>> constraints, final List<Processor<?>> processors,
final Deserializer<I> deserializer, final CheckedFunction<O, @Nullable Object, Exception> serializer, final NodeResolver resolver) {
return new AutoValue_FieldData<>(name, resolvedFieldType,
UnmodifiableCollections.copyOf(constraints),
UnmodifiableCollections.copyOf(processors),
Expand Down Expand Up @@ -87,7 +87,7 @@ static <I, O> FieldData<I, O> of(final String name, final AnnotatedType resolved

abstract List<Processor<?>> processors();

abstract BiConsumer<I, Object> deserializer();
abstract Deserializer<I> deserializer();

abstract CheckedFunction<O, @Nullable Object, Exception> serializer();

Expand Down Expand Up @@ -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 <I> intermediate data type
*/
@FunctionalInterface
public interface Deserializer<I> {

/**
* 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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.AnnotatedType;
import java.util.function.BiConsumer;
import java.util.function.Supplier;

/**
Expand Down Expand Up @@ -155,7 +154,7 @@ interface FieldCollector<I, V> {
* @param serializer a function to extract a value from a completed
* object instance.
*/
void accept(String name, AnnotatedType type, AnnotatedElement enclosing, BiConsumer<I, Object> deserializer,
void accept(String name, AnnotatedType type, AnnotatedElement enclosing, FieldData.Deserializer<I> deserializer,
CheckedFunction<V, Object, Exception> serializer);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,17 @@ public Map<Field, Object> begin() {
public void complete(final Object instance, final Map<Field, Object> intermediate) throws ObjectMappingException {
for (Map.Entry<Field, Object> 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);
}
Expand Down Expand Up @@ -124,12 +134,24 @@ private void collectFields(final AnnotatedType clazz, final FieldCollector<Map<F
field.setAccessible(true);
final AnnotatedType fieldType = getFieldType(field, clazz);
fieldMaker.accept(field.getName(), fieldType, Typing.combinedAnnotations(fieldType, field),
(intermediate, val) -> {
(intermediate, val, implicitProvider) -> {
if (val != null) {
intermediate.put(field, val);
} else {
intermediate.put(field, new ImplicitProvider(implicitProvider));
}
}, field::get);
}
}

static class ImplicitProvider {

final Supplier<Object> provider;

ImplicitProvider(final Supplier<Object> provider) {
this.provider = provider;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}.
Expand Down Expand Up @@ -149,7 +150,7 @@ private ObjectMapper<?> computeMapper(final Type type) throws ObjectMappingExcep
*/
@SuppressWarnings({"unchecked", "rawtypes"})
private <I, O> void makeData(final List<FieldData<I, O>> fields, final String name, final AnnotatedType type,
final AnnotatedElement container, final BiConsumer<I, Object> deserializer, final CheckedFunction<O, Object, Exception> serializer) {
final AnnotatedElement container, final FieldData.Deserializer<I> deserializer, final CheckedFunction<O, Object, Exception> serializer) {
@Nullable NodeResolver resolver = null;
for (NodeResolver.Factory factory : this.resolverFactories) {
final @Nullable NodeResolver next = factory.make(name, container);
Expand Down Expand Up @@ -247,6 +248,16 @@ public void serialize(final Type type, final @Nullable Object obj, final Configu
((ObjectMapper<Object>) 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

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;

class ObjectMapperImpl<I, V> implements ObjectMapper<V> {

Expand Down Expand Up @@ -62,15 +63,25 @@ final V load0(final ConfigurationNode source, final CheckedFunction<I, V, Object
final @Nullable Object newVal = node.isVirtual() ? null : serial.deserialize(field.resolvedType().getType(), node);
field.validate(newVal);

if (newVal == null) {
// set up an implicit initializer
// only the instance factory has knowledge of the underlying data type,
// so we have to pass both implicit and explicit options along to it.
final Supplier<@Nullable Object> 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().shouldCopyDefaults()) {
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;
Expand All @@ -84,9 +95,8 @@ final V load0(final ConfigurationNode source, final CheckedFunction<I, V, Object
throw failure;
}


final V complete = completer.apply(intermediate);
if (source.getOptions().shouldCopyDefaults() && unseenFields != null) {
if (unseenFields != null) {
for (FieldData<I, V> field : unseenFields) {
saveSingle(field, complete, source);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -127,15 +133,17 @@ 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) {
throw new ObjectMappingException(e);
}
}

@Override public boolean canCreateInstances() {
@Override
public boolean canCreateInstances() {
return true;
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 06943b7

Please sign in to comment.