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, 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}