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.LoaderOptionSource; 039import org.spongepowered.configurate.loader.ParsingException; 040import org.spongepowered.configurate.serialize.TypeSerializerCollection; 041import org.spongepowered.configurate.util.Strings; 042import org.spongepowered.configurate.util.UnmodifiableCollections; 043 044import java.io.BufferedReader; 045import java.io.IOException; 046import java.io.Writer; 047import java.util.Collections; 048import java.util.Set; 049 050/** 051 * A loader for JSON-formatted configurations, using the GSON library for 052 * parsing and generation. 053 * 054 * @since 4.0.0 055 */ 056public final class GsonConfigurationLoader extends AbstractConfigurationLoader<BasicConfigurationNode> { 057 058 private static final Set<Class<?>> NATIVE_TYPES = UnmodifiableCollections.toSet( 059 Double.class, Float.class, Long.class, Integer.class, Boolean.class, String.class); 060 private static final TypeSerializerCollection GSON_SERIALIZERS = TypeSerializerCollection.defaults().childBuilder() 061 .register(JsonElement.class, JsonElementSerializer.INSTANCE) 062 .build(); 063 064 // visible for tests 065 static final ConfigurationOptions DEFAULT_OPTIONS = ConfigurationOptions.defaults() 066 .nativeTypes(NATIVE_TYPES) 067 .serializers(GSON_SERIALIZERS); 068 069 /** 070 * Creates a new {@link GsonConfigurationLoader} builder. 071 * 072 * @return a new builder 073 * @since 4.0.0 074 */ 075 public static @NonNull 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 * <p>This builder supports the following options:</p> 099 * <dl> 100 * <dt><prefix>.gson.lenient</dt> 101 * <dd>Equivalent to {@link #lenient(boolean)}</dd> 102 * <dt><prefix>.gson.indent</dt> 103 * <dd>Equivalent to {@link #indent(int)}</dd> 104 * </dl> 105 * 106 * @since 4.0.0 107 */ 108 public static final class Builder extends AbstractConfigurationLoader.Builder<Builder, GsonConfigurationLoader> { 109 private boolean lenient = true; 110 private int indent = 2; 111 112 Builder() { 113 this.defaultOptions(DEFAULT_OPTIONS); 114 this.from(DEFAULT_OPTIONS_SOURCE); 115 } 116 117 @Override 118 protected void populate(final LoaderOptionSource options) { 119 this.indent = options.getInt(this.indent, "gson", "indent"); 120 this.lenient = options.getBoolean(this.lenient, "gson", "lenient"); 121 } 122 123 /** 124 * Sets the level of indentation the resultant loader should use. 125 * 126 * @param indent the indent level 127 * @return this builder (for chaining) 128 * @since 4.0.0 129 */ 130 public @NonNull Builder indent(final int indent) { 131 this.indent = indent; 132 return this; 133 } 134 135 /** 136 * Gets the level of indentation to be used by the resultant loader. 137 * 138 * @return the indent level 139 * @since 4.0.0 140 */ 141 public int indent() { 142 return this.indent; 143 } 144 145 /** 146 * Sets if the resultant loader should parse leniently. 147 * 148 * @param lenient whether the parser should parse leniently 149 * @return this builder (for chaining) 150 * @see JsonReader#setLenient(boolean) 151 * @since 4.0.0 152 */ 153 public @NonNull Builder lenient(final boolean lenient) { 154 this.lenient = lenient; 155 return this; 156 } 157 158 /** 159 * Gets if the resultant loader should parse leniently. 160 * 161 * @return whether the parser should parse leniently 162 * @since 4.0.0 163 */ 164 public boolean lenient() { 165 return this.lenient; 166 } 167 168 @Override 169 public @NonNull GsonConfigurationLoader build() { 170 this.defaultOptions(o -> o.nativeTypes(NATIVE_TYPES)); 171 return new GsonConfigurationLoader(this); 172 } 173 } 174 175 private final boolean lenient; 176 private final String indent; 177 178 GsonConfigurationLoader(final Builder builder) { 179 super(builder, new CommentHandler[] {CommentHandlers.DOUBLE_SLASH, CommentHandlers.SLASH_BLOCK, CommentHandlers.HASH}); 180 this.lenient = builder.lenient(); 181 this.indent = Strings.repeat(" ", builder.indent()); 182 } 183 184 @Override 185 protected void checkCanWrite(final ConfigurationNode node) throws ConfigurateException { 186 if (!this.lenient && !node.isMap()) { 187 throw new ConfigurateException(node, "Non-lenient json generators must have children of map type"); 188 } 189 } 190 191 @Override 192 protected void loadInternal(final BasicConfigurationNode node, final BufferedReader reader) throws ParsingException { 193 try { 194 reader.mark(1); 195 if (reader.read() == -1) { 196 return; 197 } 198 reader.reset(); 199 } catch (final IOException ex) { 200 throw new ParsingException(node, 0, 0, null, "peeking file size", ex); 201 } 202 203 try (JsonReader parser = new JsonReader(reader)) { 204 parser.setLenient(this.lenient); 205 this.parseValue(parser, node); 206 } catch (final IOException ex) { 207 throw ParsingException.wrap(node, ex); 208 } 209 } 210 211 private void parseValue(final JsonReader parser, final BasicConfigurationNode node) throws ParsingException { 212 final JsonToken token; 213 try { 214 token = parser.peek(); 215 } catch (final IOException ex) { 216 throw this.newException(parser, node, ex.getMessage(), ex); 217 } 218 219 try { 220 switch (token) { 221 case BEGIN_OBJECT: 222 this.parseObject(parser, node); 223 break; 224 case BEGIN_ARRAY: 225 this.parseArray(parser, node); 226 break; 227 case NUMBER: 228 node.raw(this.readNumber(parser)); 229 break; 230 case STRING: 231 node.raw(parser.nextString()); 232 break; 233 case BOOLEAN: 234 node.raw(parser.nextBoolean()); 235 break; 236 case NULL: // Ignored values 237 parser.nextNull(); 238 node.raw(null); 239 break; 240 case NAME: 241 break; 242 default: 243 throw this.newException(parser, node, "Unsupported token type: " + token, null); 244 } 245 } catch (final JsonParseException | MalformedJsonException ex) { 246 throw this.newException(parser, node, ex.getMessage(), ex.getCause()); 247 } catch (final ParsingException ex) { 248 ex.initPath(node::path); 249 throw ex; 250 } catch (final IOException ex) { 251 throw this.newException(parser, node, "An underlying exception occurred", ex); 252 } 253 } 254 255 private ParsingException newException(final JsonReader reader, final ConfigurationNode node, final @Nullable String message, 256 final @Nullable Throwable cause) { 257 return new ParsingException(node, JsonReaderAccess.lineNumber(reader), JsonReaderAccess.column(reader), null, message, cause); 258 } 259 260 private Number readNumber(final JsonReader reader) throws IOException { 261 final String number = reader.nextString(); 262 if (number.contains(".")) { 263 return Double.parseDouble(number); 264 } 265 final long nextLong = Long.parseLong(number); 266 final int nextInt = (int) nextLong; 267 if (nextInt == nextLong) { 268 return nextInt; 269 } 270 return nextLong; 271 } 272 273 private void parseArray(final JsonReader parser, final BasicConfigurationNode node) throws IOException { 274 parser.beginArray(); 275 276 boolean written = false; 277 @Nullable JsonToken token; 278 while ((token = parser.peek()) != null) { 279 if (token == JsonToken.END_ARRAY) { 280 parser.endArray(); 281 // ensure the type is preserved 282 if (!written) { 283 node.raw(Collections.emptyList()); 284 } 285 return; 286 } else { 287 this.parseValue(parser, node.appendListNode()); 288 written = true; 289 } 290 } 291 throw this.newException(parser, node, "Reached end of stream with unclosed array!", null); 292 } 293 294 private void parseObject(final JsonReader parser, final BasicConfigurationNode node) throws ParsingException, IOException { 295 parser.beginObject(); 296 297 boolean written = false; 298 @Nullable JsonToken token; 299 while ((token = parser.peek()) != null) { 300 switch (token) { 301 case END_OBJECT: 302 case END_DOCUMENT: 303 parser.endObject(); 304 // ensure the type is preserved 305 if (!written) { 306 node.raw(Collections.emptyMap()); 307 } 308 return; 309 case NAME: 310 this.parseValue(parser, node.node(parser.nextName())); 311 written = true; 312 break; 313 default: 314 throw new JsonParseException("Received improper object value " + token); 315 } 316 } 317 throw new JsonParseException("Reached end of stream with unclosed object!"); 318 } 319 320 @Override 321 protected void saveInternal(final ConfigurationNode node, final Writer writer) throws ConfigurateException { 322 try { 323 try (JsonWriter generator = new JsonWriter(writer)) { 324 generator.setIndent(this.indent); 325 generator.setLenient(this.lenient); 326 node.visit(GsonVisitor.INSTANCE.get(), generator); 327 writer.write(SYSTEM_LINE_SEPARATOR); // Jackson doesn't add a newline at the end of files by default 328 } 329 } catch (final IOException ex) { 330 throw ConfigurateException.wrap(node, ex); 331 } 332 } 333 334 @Override 335 public BasicConfigurationNode createNode(final ConfigurationOptions options) { 336 return BasicConfigurationNode.root(options.nativeTypes(NATIVE_TYPES)); 337 } 338 339}