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.json;
018
019import com.fasterxml.jackson.core.JsonFactory;
020import com.fasterxml.jackson.core.JsonGenerator;
021import com.fasterxml.jackson.core.JsonParseException;
022import com.fasterxml.jackson.core.JsonParser;
023import com.fasterxml.jackson.core.JsonToken;
024import com.google.common.collect.ImmutableSet;
025import ninja.leaping.configurate.ConfigurationNode;
026import ninja.leaping.configurate.ConfigurationOptions;
027import ninja.leaping.configurate.commented.CommentedConfigurationNode;
028import ninja.leaping.configurate.loader.AbstractConfigurationLoader;
029import ninja.leaping.configurate.loader.CommentHandler;
030import ninja.leaping.configurate.loader.CommentHandlers;
031import org.checkerframework.checker.nullness.qual.NonNull;
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 jackson library for parsing and generation.
041 */
042public class JSONConfigurationLoader extends AbstractConfigurationLoader<ConfigurationNode> {
043
044    /**
045     * Creates a new {@link JSONConfigurationLoader} 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 JSONConfigurationLoader}.
056     */
057    public static class Builder extends AbstractConfigurationLoader.Builder<Builder> {
058        private final JsonFactory factory = new JsonFactory();
059        private int indent = 2;
060        private FieldValueSeparatorStyle fieldValueSeparatorStyle = FieldValueSeparatorStyle.SPACE_AFTER;
061
062        protected Builder() {
063            factory.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
064            factory.enable(JsonParser.Feature.ALLOW_COMMENTS);
065            factory.enable(JsonParser.Feature.ALLOW_YAML_COMMENTS);
066            factory.enable(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER);
067            factory.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES);
068            factory.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES);
069            factory.enable(JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS);
070            factory.enable(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS);
071        }
072
073        /**
074         * Gets the {@link JsonFactory} used to configure the implementation.
075         *
076         * @return The json factory
077         */
078        @NonNull
079        public JsonFactory getFactory() {
080            return this.factory;
081        }
082
083        /**
084         * Sets the level of indentation the resultant loader should use.
085         *
086         * @param indent The indent level
087         * @return This builder (for chaining)
088         */
089        @NonNull
090        public Builder setIndent(int indent) {
091            this.indent = indent;
092            return this;
093        }
094
095        /**
096         * Gets the level of indentation to be used by the resultant loader.
097         *
098         * @return The indent level
099         */
100        public int getIndent() {
101            return this.indent;
102        }
103
104        /**
105         * Sets the field value separator style the resultant loader should use.
106         *
107         * @param style The style
108         * @return  This builder (for chaining)
109         */
110        @NonNull
111        public Builder setFieldValueSeparatorStyle(@NonNull FieldValueSeparatorStyle style) {
112            this.fieldValueSeparatorStyle = style;
113            return this;
114        }
115
116        /**
117         * Gets the field value separator style to be used by the resultant loader.
118         *
119         * @return The style
120         */
121        @NonNull
122        public FieldValueSeparatorStyle getFieldValueSeparatorStyle() {
123            return fieldValueSeparatorStyle;
124        }
125
126        @NonNull
127        @Override
128        public JSONConfigurationLoader build() {
129            return new JSONConfigurationLoader(this);
130        }
131    }
132
133    private final JsonFactory factory;
134    private final int indent;
135    private final FieldValueSeparatorStyle fieldValueSeparatorStyle;
136
137    private JSONConfigurationLoader(Builder builder) {
138        super(builder, new CommentHandler[]{CommentHandlers.DOUBLE_SLASH, CommentHandlers.SLASH_BLOCK, CommentHandlers.HASH});
139        this.factory = builder.getFactory();
140        this.indent = builder.getIndent();
141        this.fieldValueSeparatorStyle = builder.getFieldValueSeparatorStyle();
142    }
143
144    @Override
145    protected void loadInternal(ConfigurationNode node, BufferedReader reader) throws IOException {
146        try (JsonParser parser = factory.createParser(reader)) {
147            parser.nextToken();
148            parseValue(parser, node);
149        }
150    }
151
152    private static void parseValue(JsonParser parser, ConfigurationNode node) throws IOException {
153        JsonToken token = parser.getCurrentToken();
154        switch (token) {
155            case START_OBJECT:
156                parseObject(parser, node);
157                break;
158            case START_ARRAY:
159                parseArray(parser, node);
160                break;
161            case VALUE_NUMBER_FLOAT:
162                double doubleVal = parser.getDoubleValue();
163                if ((float)doubleVal != doubleVal) {
164                    node.setValue(parser.getDoubleValue());
165                } else {
166                    node.setValue(parser.getFloatValue());
167                }
168                break;
169            case VALUE_NUMBER_INT:
170                long longVal = parser.getLongValue();
171                if ((int)longVal != longVal) {
172                    node.setValue(parser.getLongValue());
173                } else {
174                    node.setValue(parser.getIntValue());
175                }
176                break;
177            case VALUE_STRING:
178                node.setValue(parser.getText());
179                break;
180            case VALUE_TRUE:
181            case VALUE_FALSE:
182                node.setValue(parser.getBooleanValue());
183                break;
184            case VALUE_NULL: // Ignored values
185            case FIELD_NAME:
186                break;
187            default:
188                throw new IOException("Unsupported token type: " + token + " (at " + parser.getTokenLocation() + ")");
189        }
190    }
191
192    private static void parseArray(JsonParser parser, ConfigurationNode node) throws IOException {
193        JsonToken token;
194        while ((token = parser.nextToken()) != null) {
195            switch (token) {
196                case END_ARRAY:
197                    return;
198                default:
199                    parseValue(parser, node.appendListNode());
200            }
201        }
202        throw new JsonParseException(parser, "Reached end of stream with unclosed array!", parser.getCurrentLocation());
203    }
204
205    private static void parseObject(JsonParser parser, ConfigurationNode node) throws IOException {
206        JsonToken token;
207        while ((token = parser.nextToken()) != null) {
208            switch (token) {
209                case END_OBJECT:
210                    return;
211                default:
212                    parseValue(parser, node.getNode(parser.getCurrentName()));
213            }
214        }
215        throw new JsonParseException(parser, "Reached end of stream with unclosed array!", parser.getCurrentLocation());
216    }
217
218    @Override
219    public void saveInternal(ConfigurationNode node, Writer writer) throws IOException {
220        try (JsonGenerator generator = factory.createGenerator(writer)) {
221            generator.setPrettyPrinter(new ConfiguratePrettyPrinter(indent, fieldValueSeparatorStyle));
222            node.visit(JacksonVisitor.INSTANCE.get(), generator);
223            writer.write(SYSTEM_LINE_SEPARATOR); // Jackson doesn't add a newline at the end of files by default
224        }
225    }
226
227    @NonNull
228    @Override
229    public CommentedConfigurationNode createEmptyNode(@NonNull ConfigurationOptions options) {
230        options = options.withNativeTypes(ImmutableSet.of(Map.class, List.class, Double.class, Float.class,
231                Long.class, Integer.class, Boolean.class, String.class, byte[].class));
232        return CommentedConfigurationNode.root(options);
233    }
234}