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.gson;
018
019import com.google.common.base.Strings;
020import com.google.common.collect.ImmutableSet;
021import com.google.gson.JsonParseException;
022import com.google.gson.stream.JsonReader;
023import com.google.gson.stream.JsonToken;
024import com.google.gson.stream.JsonWriter;
025import ninja.leaping.configurate.ConfigurationNode;
026import ninja.leaping.configurate.ConfigurationOptions;
027import ninja.leaping.configurate.loader.AbstractConfigurationLoader;
028import ninja.leaping.configurate.loader.CommentHandler;
029import ninja.leaping.configurate.loader.CommentHandlers;
030import org.checkerframework.checker.nullness.qual.NonNull;
031import org.checkerframework.checker.nullness.qual.Nullable;
032
033import java.io.BufferedReader;
034import java.io.IOException;
035import java.io.Writer;
036import java.util.List;
037import java.util.Map;
038
039/**
040 * A loader for JSON-formatted configurations, using the GSON library for parsing and generation.
041 */
042public class GsonConfigurationLoader extends AbstractConfigurationLoader<ConfigurationNode> {
043
044    /**
045     * Creates a new {@link GsonConfigurationLoader} builder.
046     *
047     * @return A new builder
048     */
049    @NonNull
050    public static Builder builder() {
051        return new Builder();
052    }
053
054    /**
055     * Builds a {@link GsonConfigurationLoader}.
056     */
057    public static class Builder extends AbstractConfigurationLoader.Builder<Builder> {
058        private boolean lenient = true;
059        private int indent = 2;
060
061        protected Builder() {
062        }
063
064        /**
065         * Sets the level of indentation the resultant loader should use.
066         *
067         * @param indent The indent level
068         * @return This builder (for chaining)
069         */
070        @NonNull
071        public Builder setIndent(int indent) {
072            this.indent = indent;
073            return this;
074        }
075
076        /**
077         * Gets the level of indentation to be used by the resultant loader.
078         *
079         * @return The indent level
080         */
081        public int getIndent() {
082            return this.indent;
083        }
084
085        /**
086         * Sets if the resultant loader should parse leniently.
087         *
088         * @see JsonReader#setLenient(boolean)
089         * @param lenient Whether the parser should parse leniently
090         * @return This builder (for chaining)
091         */
092        @NonNull
093        public Builder setLenient(boolean lenient) {
094            this.lenient = lenient;
095            return this;
096        }
097
098        /**
099         * Gets if the resultant loader should parse leniently.
100         *
101         * @return Whether the parser should parse leniently
102         */
103        public boolean isLenient() {
104            return this.lenient;
105        }
106
107        @NonNull
108        @Override
109        public GsonConfigurationLoader build() {
110            return new GsonConfigurationLoader(this);
111        }
112    }
113
114    private final boolean lenient;
115    private final String indent;
116
117    private GsonConfigurationLoader(Builder builder) {
118        super(builder, new CommentHandler[] {CommentHandlers.DOUBLE_SLASH, CommentHandlers.SLASH_BLOCK, CommentHandlers.HASH});
119        this.lenient = builder.isLenient();
120        this.indent = Strings.repeat(" ", builder.getIndent());
121    }
122
123    @Override
124    protected void loadInternal(ConfigurationNode node, BufferedReader reader) throws IOException {
125        reader.mark(1);
126        if (reader.read() == -1) {
127            return;
128        }
129        reader.reset();
130        try (JsonReader parser = new JsonReader(reader)) {
131            parser.setLenient(lenient);
132            parseValue(parser, node);
133        }
134    }
135
136    private void parseValue(JsonReader parser, ConfigurationNode node) throws IOException {
137        JsonToken token = parser.peek();
138        switch (token) {
139            case BEGIN_OBJECT:
140                parseObject(parser, node);
141                break;
142            case BEGIN_ARRAY:
143                parseArray(parser, node);
144                break;
145            case NUMBER:
146                node.setValue(readNumber(parser));
147                break;
148            case STRING:
149                node.setValue(parser.nextString());
150                break;
151            case BOOLEAN:
152                node.setValue(parser.nextBoolean());
153                break;
154            case NULL: // Ignored values
155                parser.nextNull();
156                node.setValue(null);
157                break;
158            case NAME:
159                break;
160            default:
161                throw new IOException("Unsupported token type: " + token);
162        }
163    }
164
165    private Number readNumber(JsonReader reader) throws IOException {
166        String number = reader.nextString();
167        if (number.contains(".")) {
168            return Double.parseDouble(number);
169        }
170        long nextLong = Long.parseLong(number);
171        int nextInt = (int) nextLong;
172        if (nextInt == nextLong) {
173            return nextInt;
174        }
175        return nextLong;
176    }
177
178    private void parseArray(JsonReader parser, ConfigurationNode node) throws IOException {
179        parser.beginArray();
180
181        boolean written = false;
182        @Nullable JsonToken token;
183        while ((token = parser.peek()) != null) {
184            switch (token) {
185                case END_ARRAY:
186                    parser.endArray();
187                    return;
188                default:
189                    parseValue(parser, node.appendListNode());
190            }
191        }
192        throw new JsonParseException("Reached end of stream with unclosed array at!");
193
194    }
195
196    private void parseObject(JsonReader parser, ConfigurationNode node) throws IOException {
197        parser.beginObject();
198        boolean written = false;
199        @Nullable JsonToken token;
200        while ((token = parser.peek()) != null) {
201            switch (token) {
202                case END_OBJECT:
203                case END_DOCUMENT:
204                    parser.endObject();
205                    return;
206                case NAME:
207                    parseValue(parser, node.getNode(parser.nextName()));
208                    break;
209                default:
210                    throw new JsonParseException("Received improper object value " + token);
211            }
212        }
213        throw new JsonParseException("Reached end of stream with unclosed object!");
214    }
215
216    @Override
217    public void saveInternal(ConfigurationNode node, Writer writer) throws IOException {
218        if (!lenient && !node.isMap()) {
219            throw new IOException("Non-lenient json generators must have children of map type");
220        }
221        try (JsonWriter generator = new JsonWriter(writer)) {
222            generator.setIndent(indent);
223            generator.setLenient(lenient);
224            node.visit(GsonVisitor.INSTANCE, generator);
225            writer.write(SYSTEM_LINE_SEPARATOR); // Jackson doesn't add a newline at the end of files by default
226        }
227    }
228
229    @Override
230    public ConfigurationNode createEmptyNode(@NonNull ConfigurationOptions options) {
231        options = options.withNativeTypes(ImmutableSet.of(Map.class, List.class, Double.class, Float.class,
232                Long.class, Integer.class, Boolean.class, String.class));
233        return ConfigurationNode.root(options);
234    }
235}