/*
* The MIT License (MIT)
*
* Copyright (c) 2017. Diorite (by Bartłomiej Mazur (aka GotoFinal))
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.diorite.config.impl;
import javax.annotation.Nullable;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.function.BiConsumer;
import java.util.function.Function;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.diorite.commons.arrays.DioriteArrayUtils;
import org.diorite.commons.reflections.DioriteReflectionUtils;
import org.diorite.commons.reflections.MethodInvoker;
import org.diorite.config.Config;
import org.diorite.config.ConfigPropertyTemplate;
import org.diorite.config.ConfigPropertyValue;
import org.diorite.config.ConfigTemplate;
import org.diorite.config.annotations.AsList;
import org.diorite.config.annotations.BooleanFormat;
import org.diorite.config.annotations.CollectionType;
import org.diorite.config.annotations.Comment;
import org.diorite.config.annotations.CustomKey;
import org.diorite.config.annotations.Formatted;
import org.diorite.config.annotations.HexNumber;
import org.diorite.config.annotations.MapTypes;
import org.diorite.config.annotations.Mapped;
import org.diorite.config.annotations.PaddedNumber;
import org.diorite.config.annotations.PropertyType;
import org.diorite.config.annotations.Unmodifiable;
import org.diorite.config.serialization.DeserializationData;
import org.diorite.config.serialization.SerializationData;
import org.diorite.config.serialization.comments.DocumentComments;
import org.diorite.config.serialization.snakeyaml.YamlCollectionCreator;
@SuppressWarnings({"unchecked", "rawtypes"})
public class ConfigPropertyTemplateImpl<T> implements ConfigPropertyTemplate<T>
{
private final ConfigTemplate<?> template;
private final Class<T> rawType;
private final Type genericType;
private final String name;
private Function<Config, T> defaultValueSupplier;
private final AnnotatedElement annotatedElement;
@Nullable
private BiConsumer<SerializationData, ConfigPropertyValue> serializeFunc;
@Nullable
private BiConsumer<DeserializationData, ConfigPropertyValue> deserializeFunc;
@Nullable
private MethodInvoker toKeyMapper;
@Nullable
private MethodInvoker toStringMapper;
private boolean returnUnmodifiableCollections;
public ConfigPropertyTemplateImpl(ConfigTemplate<?> template, Class<T> rawType, Type genericType, String name, Function<Config, T> defaultValueSupplier,
AnnotatedElement annotatedElement)
{
this.template = template;
this.rawType = rawType;
this.genericType = genericType;
this.name = name;
this.defaultValueSupplier = defaultValueSupplier;
this.annotatedElement = annotatedElement;
}
public void init()
{
Comment comment = this.annotatedElement.getAnnotation(Comment.class);
String key;
if (this.annotatedElement.isAnnotationPresent(CustomKey.class))
{
key = this.annotatedElement.getAnnotation(CustomKey.class).value();
}
else if ((comment != null) && ! comment.name().isEmpty())
{
key = comment.name();
}
else
{
key = this.name;
}
this.initSerializeFunc(key);
if (comment != null)
{
DocumentComments comments = this.template.getComments();
comments.setComment(key, comment.value());
}
this.returnUnmodifiableCollections = this.annotatedElement.isAnnotationPresent(Unmodifiable.class);
}
public void setToKeyMapper(@Nullable MethodInvoker toKeyMapper)
{
this.toKeyMapper = toKeyMapper;
if (toKeyMapper != null)
{
toKeyMapper.ensureAccessible();
}
}
public void setToStringMapper(@Nullable MethodInvoker toStringMapper)
{
this.toStringMapper = toStringMapper;
if (toStringMapper != null)
{
toStringMapper.ensureAccessible();
}
}
private void initSerializeFunc(String key)
{
String format = null;
if (this.annotatedElement.isAnnotationPresent(Formatted.class))
{
format = this.annotatedElement.getAnnotation(Formatted.class).value();
}
if ((this.rawType == boolean.class) || (this.rawType == Boolean.class))
{
this.initForBoolean(key, format);
return;
}
if (Number.class.isAssignableFrom(DioriteReflectionUtils.getWrapperClass(this.rawType)))
{
this.initForNumber(key, format);
return;
}
if (Collection.class.isAssignableFrom(this.rawType))
{
this.initForCollection(key);
return;
}
if (! Config.class.isAssignableFrom(this.rawType) && Map.class.isAssignableFrom(this.rawType))
{
this.initForMap(key);
return;
}
Class type;
if (this.annotatedElement.isAnnotationPresent(PropertyType.class))
{
type = this.annotatedElement.getAnnotation(PropertyType.class).annotationType();
}
else
{
type = this.rawType;
}
this.serializeFunc = (data, val) -> data.add(key, val.getPropertyValue(), type);
this.deserializeFunc = (data, val) -> val.setPropertyValue(data.get(key, type, val.getDefault()));
}
private void initForMap(String key)
{
AsList asList = this.annotatedElement.getAnnotation(AsList.class);
Class<?>[] mapType = this.getMapType(asList);
Class keyType = mapType[0];
Class valueType = mapType[1];
if (asList == null)
{
if (this.toStringMapper == null)
{
if ((keyType == null) || (this.toKeyMapper != null))
{
if (this.toKeyMapper == null)
{
throw new IllegalStateException("Missing toKeyMapper/toStringMapper/keyType for: '" + this.name + "' (" + this.genericType + ") in: " +
this.template.getConfigType());
}
this.serializeFunc = (data, val) -> data.addMap(key, ((Map) val.getPropertyValue()), valueType);
this.deserializeFunc = (data, val) ->
{
Object collection = YamlCollectionCreator.createCollection(this.rawType, 10);
data.getMap(key, s -> this.toKeyMapper.invoke(val.getDeclaringConfig(), s), valueType);
val.setPropertyValue(collection);
};
}
else
{
this.serializeFunc = (data, val) -> data.addMap(key, ((Map) val.getPropertyValue()), keyType, valueType);
this.deserializeFunc = (data, val) ->
{
Object collection = YamlCollectionCreator.createCollection(this.rawType, 10);
data.getMap(key, keyType, valueType);
val.setPropertyValue(collection);
};
}
}
else
{
if (this.toKeyMapper == null)
{
throw new IllegalStateException("Missing toKeyMapper for: '" + this.name + "' (" + this.genericType + ") in: " +
this.template.getConfigType());
}
this.serializeFunc = (data, val) -> data.addMap(key, ((Map) val.getPropertyValue()), valueType,
s -> String.valueOf(this.toStringMapper.invoke(val.getDeclaringConfig(), s)));
this.deserializeFunc = (data, val) ->
{
Object collection = YamlCollectionCreator.createCollection(this.rawType, 10);
data.getMap(key, s -> this.toKeyMapper.invoke(val.getDeclaringConfig(), s), valueType);
val.setPropertyValue(collection);
};
}
}
else
{
String keyProperty = asList.keyProperty();
if (keyProperty.isEmpty() || (this.toStringMapper != null))
{
if (this.toStringMapper == null)
{
throw new IllegalStateException("Missing toStringMapper for: '" + this.name + "' (" + this.genericType + ") in: " +
this.template.getConfigType());
}
this.serializeFunc = (data, val) -> data.addMapAsList(key, ((Map) val.getPropertyValue()), valueType);
this.deserializeFunc = (data, val) ->
{
Object collection = YamlCollectionCreator.createCollection(this.rawType, 10);
data.getAsMap(key, valueType, s -> String.valueOf(this.toStringMapper.invoke(val.getDeclaringConfig(), s)), (Map) collection);
val.setPropertyValue(collection);
};
}
else
{
this.serializeFunc = (data, val) -> data.addMapAsListWithKeys(key, ((Map) val.getPropertyValue()), valueType, keyProperty);
this.deserializeFunc = (data, val) ->
{
Object collection = YamlCollectionCreator.createCollection(this.rawType, 10);
data.getAsMapWithKeys(key, keyType, valueType, keyProperty, (Map) collection);
val.setPropertyValue(collection);
};
}
}
}
private void initForCollection(String key)
{
Mapped mapped = this.annotatedElement.getAnnotation(Mapped.class);
Class collectionType = this.getCollectionType(mapped);
if (mapped == null)
{
this.serializeFunc = (data, val) -> data.addCollection(key, (Collection) val.getPropertyValue(), collectionType);
this.deserializeFunc = (data, val) ->
{
Object collection = YamlCollectionCreator.createCollection(this.rawType, 10);
data.getAsCollection(key, collectionType, (Collection) collection);
val.setPropertyValue(collection);
};
}
else
{
if (this.toStringMapper == null)
{
throw new IllegalStateException("Missing toStringMapper for: '" + this.name + "' (" + this.genericType + ") in: " +
this.template.getConfigType());
}
this.serializeFunc = (data, val) -> data.addMappedList(key, collectionType, (Collection) val.getPropertyValue(),
o -> String.valueOf(this.toStringMapper.invoke(val.getDeclaringConfig(), o)));
this.deserializeFunc = (data, val) ->
{
Object collection = YamlCollectionCreator.createCollection(this.rawType, 10);
data.getAsCollection(key, collectionType, (Collection) collection);
val.setPropertyValue(collection);
};
}
}
private Class<?>[] getMapType(@Nullable AsList asList)
{
Class<?>[] result = new Class[2];
if (asList != null)
{
result[0] = (asList.keyType() == Object.class) ? null : asList.keyType();
result[1] = (asList.valueType() == Object.class) ? null : asList.valueType();
}
MapTypes mapTypes = this.annotatedElement.getAnnotation(MapTypes.class);
if (mapTypes != null)
{
result[0] = mapTypes.keyType();
result[1] = mapTypes.valueType();
}
if (this.genericType instanceof ParameterizedType)
{
if (result[0] == null)
{
Type keyType = ((ParameterizedType) this.genericType).getActualTypeArguments()[0];
if (keyType instanceof WildcardType)
{
Type[] upperBounds = ((WildcardType) keyType).getUpperBounds();
keyType = (upperBounds.length == 0) ? null : upperBounds[0];
}
result[0] = (keyType instanceof Class) ? (Class<?>) keyType : null;
}
if (result[1] == null)
{
Type valueType = ((ParameterizedType) this.genericType).getActualTypeArguments()[1];
if (valueType instanceof WildcardType)
{
Type[] upperBounds = ((WildcardType) valueType).getUpperBounds();
valueType = (upperBounds.length == 0) ? null : upperBounds[0];
}
result[1] = (valueType instanceof Class) ? (Class<?>) valueType : null;
}
}
if ((result[1] == null)) // key can be null
{
throw new IllegalStateException("Can't read generic type of map '" + this.name + "' (" + this.genericType + ") in: " +
this.template.getConfigType());
}
return result;
}
private Class<?> getCollectionType(@Nullable Mapped mapped)
{
Class<?> collectionType = null;
if (this.annotatedElement.isAnnotationPresent(CollectionType.class))
{
collectionType = this.annotatedElement.getAnnotation(CollectionType.class).value();
}
else if ((mapped != null) && (mapped.type() != Object.class))
{
collectionType = mapped.type();
}
else if (this.genericType instanceof ParameterizedType)
{
Type type = ((ParameterizedType) this.genericType).getActualTypeArguments()[0];
if (type instanceof WildcardType)
{
Type[] upperBounds = ((WildcardType) type).getUpperBounds();
type = (upperBounds.length == 0) ? null : upperBounds[0];
}
collectionType = (type instanceof Class) ? (Class<?>) type : null;
}
if (collectionType == null)
{
throw new IllegalStateException("Can't read generic type of collection '" + this.name + "' (" + this.genericType + ") in: " +
this.template.getConfigType());
}
return collectionType;
}
private void initForNumber(String key, @Nullable String format)
{
Class<?> primitive = DioriteReflectionUtils.getPrimitive(this.rawType);
boolean hex = this.annotatedElement.isAnnotationPresent(HexNumber.class);
int padding;
if (this.annotatedElement.isAnnotationPresent(PaddedNumber.class))
{
padding = this.annotatedElement.getAnnotation(PaddedNumber.class).value();
}
else
{
padding = 0;
}
if (hex)
{
this.serializeFunc = (data, val) -> data.addHexNumber(key, (Number) val.getPropertyValue(), padding);
this.deserializeFunc = (data, val) -> val.setPropertyValue(data.getAsHexNumber(key, (Class) this.rawType, (Number) val.getDefault()));
}
else if ((padding == 0) && (format != null))
{
this.serializeFunc = (data, val) -> data.addFormatted(key, format, val.getPropertyValue());
this.deserializeFunc = (data, val) -> val.setPropertyValue(data.get(key, (Class) this.rawType, val.getDefault()));
}
else
{
this.serializeFunc = (data, val) -> data.addNumber(key, (Number) val.getPropertyValue(), padding);
this.deserializeFunc = (data, val) -> val.setPropertyValue(data.get(key, (Class) this.rawType, val.getDefault()));
}
}
private void initForBoolean(String key, @Nullable String format)
{
String trueValue;
String falseValue;
String[] trueValues;
String[] falseValues;
if (this.annotatedElement.isAnnotationPresent(BooleanFormat.class))
{
BooleanFormat annotation = this.annotatedElement.getAnnotation(BooleanFormat.class);
trueValues = annotation.trueValues();
falseValues = annotation.falseValues();
trueValue = (trueValues.length > 0) ? trueValues[0] : "true";
falseValue = (falseValues.length > 0) ? falseValues[0] : "false";
}
else if (format != null)
{
this.serializeFunc = (data, val) -> data.addFormatted(key, format, val.getPropertyValue());
this.deserializeFunc = (data, val) -> val.setPropertyValue(data.getAsBoolean(key, (boolean) val.getDefault()));
return;
}
else
{
trueValue = "true";
falseValue = "false";
trueValues = DioriteArrayUtils.EMPTY_STRINGS;
falseValues = DioriteArrayUtils.EMPTY_STRINGS;
}
this.serializeFunc = (data, val) -> data.addBoolean(key, (Boolean) val.getPropertyValue(), trueValue, falseValue);
this.deserializeFunc = (data, val) ->
{
data.addTrueValues(trueValues);
data.addFalseValues(falseValues);
val.setPropertyValue(data.getAsBoolean(key, (boolean) val.getDefault()));
};
}
@Override
public Class<T> getRawType()
{
return this.rawType;
}
@Override
public Type getGenericType()
{
return this.genericType;
}
@Override
public String getName()
{
return this.name;
}
@SuppressWarnings("unchecked")
@Override
public T getDefault(Config config)
{
T def = this.defaultValueSupplier.apply(config);
if ((def == null) && this.rawType.isPrimitive())
{
return (T) this.getPrimitiveDefault();
}
return def;
}
@Override
public void set(ConfigPropertyValue<T> propertyValue, @Nullable T value)
{
propertyValue.setRawValue(value);
}
@Nullable
@Override
public T get(ConfigPropertyValue<T> propertyValue)
{
T rawValue = propertyValue.getRawValue();
if (rawValue == null)
{
return null;
}
if (this.returnUnmodifiableCollections)
{
if (rawValue instanceof Collection)
{
if (rawValue instanceof Set)
{
if (rawValue instanceof NavigableSet)
{
return (T) Collections.unmodifiableNavigableSet((NavigableSet<?>) rawValue);
}
if (rawValue instanceof SortedSet)
{
return (T) Collections.unmodifiableSortedSet((SortedSet<?>) rawValue);
}
return (T) Collections.unmodifiableSet((Set<?>) rawValue);
}
if (rawValue instanceof List)
{
return (T) Collections.unmodifiableList((List<?>) rawValue);
}
return (T) Collections.unmodifiableCollection((Collection<?>) rawValue);
}
else if (rawValue instanceof Map)
{
if (rawValue instanceof NavigableMap)
{
return (T) Collections.unmodifiableNavigableMap((NavigableMap<?, ?>) rawValue);
}
if (rawValue instanceof SortedMap)
{
return (T) Collections.unmodifiableSortedMap((SortedMap<?, ?>) rawValue);
}
return (T) Collections.unmodifiableMap((Map<?, ?>) rawValue);
}
}
return rawValue;
}
@Override
public void serialize(SerializationData data, ConfigPropertyValue<T> value)
{
Validate.notNull(this.serializeFunc);
this.serializeFunc.accept(data, value);
}
@Override
public void deserialize(DeserializationData data, ConfigPropertyValue<T> value)
{
Validate.notNull(this.deserializeFunc);
this.deserializeFunc.accept(data, value);
}
private Object getPrimitiveDefault()
{
Class<T> rawType = this.rawType;
if (rawType == boolean.class)
{
return false;
}
if (rawType == byte.class)
{
return (byte) 0;
}
if (rawType == short.class)
{
return (short) 0;
}
if (rawType == char.class)
{
return '\0';
}
if (rawType == int.class)
{
return 0;
}
if (rawType == long.class)
{
return 0L;
}
if (rawType == float.class)
{
return 0.0F;
}
if (rawType == double.class)
{
return 0.0;
}
throw new InternalError("Unknown primitive type:" + rawType);
}
public void setDefaultValueSupplier(Function<Config, T> defaultValueSupplier)
{
this.defaultValueSupplier = defaultValueSupplier;
}
@Override
public String toString()
{
return new ToStringBuilder(this).appendSuper(super.toString()).append("rawType", this.rawType).append("genericType", this.genericType)
.append("name", this.name).toString();
}
}