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 org.spongepowered.configurate.gson;
018
019import com.google.gson.JsonArray;
020import com.google.gson.JsonElement;
021import com.google.gson.JsonNull;
022import com.google.gson.JsonObject;
023import com.google.gson.JsonParseException;
024import com.google.gson.JsonPrimitive;
025import com.google.gson.stream.JsonReader;
026import com.google.gson.stream.JsonToken;
027import com.google.gson.stream.JsonWriter;
028import com.google.gson.stream.MalformedJsonException;
029import net.kyori.option.Option;
030import net.kyori.option.OptionSchema;
031import org.checkerframework.checker.nullness.qual.NonNull;
032import org.checkerframework.checker.nullness.qual.Nullable;
033import org.spongepowered.configurate.BasicConfigurationNode;
034import org.spongepowered.configurate.ConfigurateException;
035import org.spongepowered.configurate.ConfigurationNode;
036import org.spongepowered.configurate.ConfigurationOptions;
037import org.spongepowered.configurate.loader.AbstractConfigurationLoader;
038import org.spongepowered.configurate.loader.CommentHandler;
039import org.spongepowered.configurate.loader.CommentHandlers;
040import org.spongepowered.configurate.loader.ParsingException;
041import org.spongepowered.configurate.serialize.TypeSerializerCollection;
042import org.spongepowered.configurate.util.Strings;
043import org.spongepowered.configurate.util.UnmodifiableCollections;
044
045import java.io.BufferedReader;
046import java.io.IOException;
047import java.io.Writer;
048import java.util.Collections;
049import java.util.Set;
050
051/**
052 * A loader for JSON-formatted configurations, using the GSON library for
053 * parsing and generation.
054 *
055 * @since 4.0.0
056 */
057public final class GsonConfigurationLoader extends AbstractConfigurationLoader<BasicConfigurationNode> {
058
059    private static final Set<Class<?>> NATIVE_TYPES = UnmodifiableCollections.toSet(
060            Double.class, Float.class, Long.class, Integer.class, Boolean.class, String.class);
061    private static final TypeSerializerCollection GSON_SERIALIZERS = TypeSerializerCollection.defaults().childBuilder()
062            .register(JsonElement.class, JsonElementSerializer.INSTANCE)
063            .build();
064
065    // visible for tests
066    static final ConfigurationOptions DEFAULT_OPTIONS = ConfigurationOptions.defaults()
067            .nativeTypes(NATIVE_TYPES)
068            .serializers(GSON_SERIALIZERS);
069
070    /**
071     * Creates a new {@link GsonConfigurationLoader} builder.
072     *
073     * @return a new builder
074     * @since 4.0.0
075     */
076    public static @NonNull Builder builder() {
077        return new Builder();
078    }
079
080    /**
081     * Get a {@link TypeSerializerCollection} for handling Gson types.
082     *
083     * <p>Currently, this serializer can handle:</p>
084     * <ul>
085     *     <li>{@link JsonElement} and its subtypes: {@link JsonArray}, {@link JsonObject},
086     *          {@link JsonPrimitive}, and {@link JsonNull}</li>
087     * </ul>
088     *
089     * @return gson type serializers
090     * @since 4.1.0
091     */
092    public static TypeSerializerCollection gsonSerializers() {
093        return GSON_SERIALIZERS;
094    }
095
096    /**
097     * Builds a {@link GsonConfigurationLoader}.
098     *
099     * <p>This builder supports the following options:</p>
100     * <ul>
101     *     <li>{@link #INDENT}</li>
102     *     <li>{@link #LENIENT}</li>
103     * </ul>
104     *
105     * @since 4.0.0
106     */
107    public static final class Builder extends AbstractConfigurationLoader.Builder<Builder, GsonConfigurationLoader> {
108
109        private static final OptionSchema.Mutable UNSAFE_SCHEMA = OptionSchema.childSchema(AbstractConfigurationLoader.Builder.SCHEMA);
110
111        /**
112         * A schema of options available on the Gson loader.
113         *
114         * @since 4.2.0
115         */
116        public static final OptionSchema SCHEMA = UNSAFE_SCHEMA.frozenView();
117
118        /**
119         * The level of indentation to be used by the resulting loader.
120         *
121         * @since 4.2.0
122         */
123        public static final Option<Integer> INDENT = UNSAFE_SCHEMA.intOption("gson:indent", 2);
124
125        /**
126         * If the resultant loader should parse leniently.
127         *
128         * @since 4.2.0
129         */
130        public static final Option<Boolean> LENIENT = UNSAFE_SCHEMA.booleanOption("gson:lenient", true);
131
132        Builder() {
133            this.defaultOptions(DEFAULT_OPTIONS);
134        }
135
136        @Override
137        protected OptionSchema optionSchema() {
138            return SCHEMA;
139        }
140
141        /**
142         * Sets the level of indentation the resultant loader should use.
143         *
144         * @param indent the indent level
145         * @return this builder (for chaining)
146         * @since 4.0.0
147         */
148        public @NonNull Builder indent(final int indent) {
149            this.optionStateBuilder().value(INDENT, indent);
150            return this;
151        }
152
153        /**
154         * Gets the level of indentation to be used by the resultant loader.
155         *
156         * @return the indent level
157         * @since 4.0.0
158         */
159        public int indent() {
160            return this.optionState().value(INDENT);
161        }
162
163        /**
164         * Sets if the resultant loader should parse leniently.
165         *
166         * @param lenient whether the parser should parse leniently
167         * @return this builder (for chaining)
168         * @see JsonReader#setLenient(boolean)
169         * @since 4.0.0
170         */
171        public @NonNull Builder lenient(final boolean lenient) {
172            this.optionStateBuilder().value(LENIENT, lenient);
173            return this;
174        }
175
176        /**
177         * Gets if the resultant loader should parse leniently.
178         *
179         * @return whether the parser should parse leniently
180         * @since 4.0.0
181         */
182        public boolean lenient() {
183            return this.optionState().value(LENIENT);
184        }
185
186        @Override
187        public @NonNull GsonConfigurationLoader build() {
188            this.defaultOptions(o -> o.nativeTypes(NATIVE_TYPES));
189            return new GsonConfigurationLoader(this);
190        }
191    }
192
193    private final boolean lenient;
194    private final String indent;
195
196    GsonConfigurationLoader(final Builder builder) {
197        super(builder, new CommentHandler[] {CommentHandlers.DOUBLE_SLASH, CommentHandlers.SLASH_BLOCK, CommentHandlers.HASH});
198        this.lenient = builder.lenient();
199        this.indent = Strings.repeat(" ", builder.indent());
200    }
201
202    @Override
203    protected void checkCanWrite(final ConfigurationNode node) throws ConfigurateException {
204        if (!this.lenient && !node.isMap()) {
205            throw new ConfigurateException(node, "Non-lenient json generators must have children of map type");
206        }
207    }
208
209    @Override
210    protected void loadInternal(final BasicConfigurationNode node, final BufferedReader reader) throws ParsingException {
211        try {
212            reader.mark(1);
213            if (reader.read() == -1) {
214                return;
215            }
216            reader.reset();
217        } catch (final IOException ex) {
218            throw new ParsingException(node, 0, 0, null, "peeking file size", ex);
219        }
220
221        try (JsonReader parser = new JsonReader(reader)) {
222            parser.setLenient(this.lenient);
223            this.parseValue(parser, node);
224        } catch (final IOException ex) {
225            throw ParsingException.wrap(node, ex);
226        }
227    }
228
229    private void parseValue(final JsonReader parser, final BasicConfigurationNode node) throws ParsingException {
230        final JsonToken token;
231        try {
232            token = parser.peek();
233        } catch (final IOException ex) {
234            throw this.newException(parser, node, ex.getMessage(), ex);
235        }
236
237        try {
238            switch (token) {
239                case BEGIN_OBJECT:
240                    this.parseObject(parser, node);
241                    break;
242                case BEGIN_ARRAY:
243                    this.parseArray(parser, node);
244                    break;
245                case NUMBER:
246                    node.raw(this.readNumber(parser));
247                    break;
248                case STRING:
249                    node.raw(parser.nextString());
250                    break;
251                case BOOLEAN:
252                    node.raw(parser.nextBoolean());
253                    break;
254                case NULL: // Ignored values
255                    parser.nextNull();
256                    node.raw(null);
257                    break;
258                case NAME:
259                    break;
260                default:
261                    throw this.newException(parser, node, "Unsupported token type: " + token, null);
262            }
263        } catch (final JsonParseException | MalformedJsonException ex) {
264            throw this.newException(parser, node, ex.getMessage(), ex.getCause());
265        } catch (final ParsingException ex) {
266            ex.initPath(node::path);
267            throw ex;
268        } catch (final IOException ex) {
269            throw this.newException(parser, node, "An underlying exception occurred", ex);
270        }
271    }
272
273    private ParsingException newException(final JsonReader reader, final ConfigurationNode node, final @Nullable String message,
274            final @Nullable Throwable cause) {
275        return new ParsingException(node, JsonReaderAccess.lineNumber(reader), JsonReaderAccess.column(reader), null, message, cause);
276    }
277
278    private Number readNumber(final JsonReader reader) throws IOException {
279        final String number = reader.nextString();
280        if (number.contains(".")) {
281            return Double.parseDouble(number);
282        }
283        final long nextLong = Long.parseLong(number);
284        final int nextInt = (int) nextLong;
285        if (nextInt == nextLong) {
286            return nextInt;
287        }
288        return nextLong;
289    }
290
291    private void parseArray(final JsonReader parser, final BasicConfigurationNode node) throws IOException {
292        parser.beginArray();
293
294        boolean written = false;
295        @Nullable JsonToken token;
296        while ((token = parser.peek()) != null) {
297            if (token == JsonToken.END_ARRAY) {
298                parser.endArray();
299                // ensure the type is preserved
300                if (!written) {
301                    node.raw(Collections.emptyList());
302                }
303                return;
304            } else {
305                this.parseValue(parser, node.appendListNode());
306                written = true;
307            }
308        }
309        throw this.newException(parser, node, "Reached end of stream with unclosed array!", null);
310    }
311
312    private void parseObject(final JsonReader parser, final BasicConfigurationNode node) throws ParsingException, IOException {
313        parser.beginObject();
314
315        boolean written = false;
316        @Nullable JsonToken token;
317        while ((token = parser.peek()) != null) {
318            switch (token) {
319                case END_OBJECT:
320                case END_DOCUMENT:
321                    parser.endObject();
322                    // ensure the type is preserved
323                    if (!written) {
324                        node.raw(Collections.emptyMap());
325                    }
326                    return;
327                case NAME:
328                    this.parseValue(parser, node.node(parser.nextName()));
329                    written = true;
330                    break;
331                default:
332                    throw new JsonParseException("Received improper object value " + token);
333            }
334        }
335        throw new JsonParseException("Reached end of stream with unclosed object!");
336    }
337
338    @Override
339    protected void saveInternal(final ConfigurationNode node, final Writer writer) throws ConfigurateException {
340        try {
341            try (JsonWriter generator = new JsonWriter(writer)) {
342                generator.setIndent(this.indent);
343                generator.setLenient(this.lenient);
344                node.visit(GsonVisitor.INSTANCE.get(), generator);
345                writer.write(SYSTEM_LINE_SEPARATOR); // Jackson doesn't add a newline at the end of files by default
346            }
347        } catch (final IOException ex) {
348            throw ConfigurateException.wrap(node, ex);
349        }
350    }
351
352    @Override
353    public BasicConfigurationNode createNode(final ConfigurationOptions options) {
354        return BasicConfigurationNode.root(options.nativeTypes(NATIVE_TYPES));
355    }
356
357}