001/*
002 * Configurate
003 * Copyright (C) zml and Configurate contributors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package ninja.leaping.configurate.objectmapping.serialize;
018
019import com.google.common.reflect.TypeToken;
020import ninja.leaping.configurate.ConfigurationNode;
021import ninja.leaping.configurate.objectmapping.ObjectMappingException;
022import org.checkerframework.checker.nullness.qual.Nullable;
023
024import java.util.List;
025import java.util.function.Predicate;
026
027/**
028 * Serialize a value that can be represented as a scalar value within a node. Implementations must be able
029 * to serialize when one of the accepted types is a {@link String}, and may support any other types as desired.
030 * <p>
031 * When serializing to a node, null values will be passed through directly. If the type serialized by this serializer is
032 * one of the native types of the backing node, it will be written directly to the node without any transformation.
033 * <p>
034 * Any serialized value must be deserializable by the same serializer..
035 *
036 * @param <T> The object type to serialize
037 */
038public abstract class ScalarSerializer<T> implements TypeSerializer<T> {
039    private final TypeToken<T> type;
040
041    protected ScalarSerializer(TypeToken<T> type) {
042        this.type = type.wrap();
043    }
044
045    protected ScalarSerializer(Class<T> type) {
046        if (type.getTypeParameters().length > 0) {
047            throw new IllegalArgumentException("Provided type " + type + " has type parameters but was not provided as a TypeToken!");
048        }
049        this.type = TypeToken.of(type);
050    }
051
052    /**
053     * Get the general type token applicable for this serializer. This token may be parameterized.
054     *
055     * @return The type token for this serializer
056     */
057    public final TypeToken<T> type() {
058        return this.type;
059    }
060
061    @Override
062    public final @Nullable T deserialize(TypeToken<?> type, ConfigurationNode node) throws ObjectMappingException {
063        ConfigurationNode deserializeFrom = node;
064        if (node.isList()) {
065            List<? extends ConfigurationNode> children = node.getChildrenList();
066            if (children.size() == 1) {
067                deserializeFrom = children.get(0);
068            }
069        }
070
071        if (deserializeFrom.isList() || deserializeFrom.isMap()) {
072            throw new ObjectMappingException("Value must be provided as a scalar!");
073        }
074
075        @Nullable Object value = deserializeFrom.getValue();
076        if (value == null) {
077            return null;
078        }
079
080        type = type.wrap(); // every primitive type should be boxed (cuz generics!)
081        @Nullable T possible = cast(value);
082        if (possible != null) {
083            return possible;
084        }
085
086        return deserialize(type, value);
087    }
088
089    @Override
090    public final void serialize(TypeToken<?> type, @Nullable T obj, ConfigurationNode node) {
091        if (obj == null) {
092            node.setValue(null);
093            return;
094        }
095
096        if (node.getOptions().acceptsType(obj.getClass())) {
097            node.setValue(obj);
098            return;
099        }
100
101        node.setValue(serialize(obj, node.getOptions()::acceptsType));
102    }
103
104    /**
105     * Given an object of unknown type, attempt to convert it into the given type.
106     *
107     * @param type The specific type of the type's usage
108     * @param obj  The object to convert
109     * @return A converted object
110     * @throws ObjectMappingException If the object could not be converted for any reason
111     */
112    public abstract T deserialize(TypeToken<?> type, Object obj) throws ObjectMappingException;
113
114    /**
115     * @param item          The value to serialize
116     * @param typeSupported A predicate to allow choosing which types are supported
117     * @return A serialized form of this object
118     */
119    public abstract Object serialize(T item, Predicate<Class<?>> typeSupported);
120
121    /**
122     * Attempt to deserialize the provided object using an unspecialized type. This may fail on more complicated
123     * deserialization processes
124     *
125     * @param value The object to deserialize.
126     * @return The deserialized object, if possible
127     * @throws ObjectMappingException If unable to coerce the value to the requested type
128     */
129    public final T deserialize(Object value) throws ObjectMappingException {
130        @Nullable T possible = cast(value);
131        if (possible != null) {
132            return possible;
133        }
134
135        return this.deserialize(this.type(), value);
136    }
137
138    @SuppressWarnings("unchecked")
139    private @Nullable T cast(Object value) {
140        Class<?> rawType = this.type().getRawType();
141        if (rawType.isInstance(value)) {
142            return (T) value;
143        }
144        return null;
145    }
146
147    /**
148     * Attempt to deserialize the provided object, but rather than throwing an exception when a parse error occurs,
149     * return null instead.
150     *
151     * @param obj The object to try to deserialize
152     * @return An instance of the appropriate type, or null
153     * @see #deserialize(Object)
154     */
155    public final @Nullable T tryDeserialize(@Nullable Object obj) {
156        if (obj == null) {
157            return null;
158        }
159
160        try {
161            return deserialize(obj);
162        } catch (ObjectMappingException ex) {
163            return null;
164        }
165    }
166
167    /**
168     * Serialize the item to a {@link String}, in a representation that can be interpreted by this serializer again
169     *
170     * @param item The item to serialize
171     * @return The serialized form of the item
172     */
173    public final String serializeToString(T item) {
174        if (item instanceof CharSequence) {
175            return item.toString();
176        }
177        // Otherwise, use the serializer
178        return (String) serialize(item, clazz -> clazz.isAssignableFrom(String.class));
179    }
180
181}