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