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