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}