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}