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.JsonParseException; 020import com.google.gson.stream.JsonReader; 021import com.google.gson.stream.JsonToken; 022import com.google.gson.stream.JsonWriter; 023import com.google.gson.stream.MalformedJsonException; 024import org.checkerframework.checker.nullness.qual.NonNull; 025import org.checkerframework.checker.nullness.qual.Nullable; 026import org.spongepowered.configurate.BasicConfigurationNode; 027import org.spongepowered.configurate.ConfigurateException; 028import org.spongepowered.configurate.ConfigurationNode; 029import org.spongepowered.configurate.ConfigurationOptions; 030import org.spongepowered.configurate.loader.AbstractConfigurationLoader; 031import org.spongepowered.configurate.loader.CommentHandler; 032import org.spongepowered.configurate.loader.CommentHandlers; 033import org.spongepowered.configurate.loader.ParsingException; 034import org.spongepowered.configurate.util.Strings; 035import org.spongepowered.configurate.util.UnmodifiableCollections; 036 037import java.io.BufferedReader; 038import java.io.IOException; 039import java.io.Writer; 040import java.util.Collections; 041import java.util.Set; 042 043/** 044 * A loader for JSON-formatted configurations, using the GSON library for 045 * parsing and generation. 046 * 047 * @since 4.0.0 048 */ 049public final class GsonConfigurationLoader extends AbstractConfigurationLoader<BasicConfigurationNode> { 050 051 private static final Set<Class<?>> NATIVE_TYPES = UnmodifiableCollections.toSet( 052 Double.class, Float.class, Long.class, Integer.class, Boolean.class, String.class); 053 054 /** 055 * Creates a new {@link GsonConfigurationLoader} builder. 056 * 057 * @return a new builder 058 * @since 4.0.0 059 */ 060 @NonNull 061 public static Builder builder() { 062 return new Builder(); 063 } 064 065 /** 066 * Builds a {@link GsonConfigurationLoader}. 067 * 068 * @since 4.0.0 069 */ 070 public static final class Builder extends AbstractConfigurationLoader.Builder<Builder, GsonConfigurationLoader> { 071 private boolean lenient = true; 072 private int indent = 2; 073 074 Builder() { } 075 076 /** 077 * Sets the level of indentation the resultant loader should use. 078 * 079 * @param indent the indent level 080 * @return this builder (for chaining) 081 * @since 4.0.0 082 */ 083 @NonNull 084 public Builder indent(final int indent) { 085 this.indent = indent; 086 return this; 087 } 088 089 /** 090 * Gets the level of indentation to be used by the resultant loader. 091 * 092 * @return the indent level 093 * @since 4.0.0 094 */ 095 public int indent() { 096 return this.indent; 097 } 098 099 /** 100 * Sets if the resultant loader should parse leniently. 101 * 102 * @see JsonReader#setLenient(boolean) 103 * @param lenient whether the parser should parse leniently 104 * @return this builder (for chaining) 105 * @since 4.0.0 106 */ 107 @NonNull 108 public Builder lenient(final boolean lenient) { 109 this.lenient = lenient; 110 return this; 111 } 112 113 /** 114 * Gets if the resultant loader should parse leniently. 115 * 116 * @return whether the parser should parse leniently 117 * @since 4.0.0 118 */ 119 public boolean lenient() { 120 return this.lenient; 121 } 122 123 @Override 124 public @NonNull GsonConfigurationLoader build() { 125 this.defaultOptions(o -> o.nativeTypes(NATIVE_TYPES)); 126 return new GsonConfigurationLoader(this); 127 } 128 } 129 130 private final boolean lenient; 131 private final String indent; 132 133 private GsonConfigurationLoader(final Builder builder) { 134 super(builder, new CommentHandler[] {CommentHandlers.DOUBLE_SLASH, CommentHandlers.SLASH_BLOCK, CommentHandlers.HASH}); 135 this.lenient = builder.lenient(); 136 this.indent = Strings.repeat(" ", builder.indent()); 137 } 138 139 @Override 140 protected void loadInternal(final BasicConfigurationNode node, final BufferedReader reader) throws ParsingException { 141 try { 142 reader.mark(1); 143 if (reader.read() == -1) { 144 return; 145 } 146 reader.reset(); 147 } catch (final IOException ex) { 148 throw new ParsingException(node, 0, 0, null, "peeking file size", ex); 149 } 150 151 try (JsonReader parser = new JsonReader(reader)) { 152 parser.setLenient(this.lenient); 153 parseValue(parser, node); 154 } catch (final IOException ex) { 155 throw ParsingException.wrap(node, ex); 156 } 157 } 158 159 private void parseValue(final JsonReader parser, final BasicConfigurationNode node) throws ParsingException { 160 final JsonToken token; 161 try { 162 token = parser.peek(); 163 } catch (final IOException ex) { 164 throw newException(parser, node, ex.getMessage(), ex); 165 } 166 167 try { 168 switch (token) { 169 case BEGIN_OBJECT: 170 parseObject(parser, node); 171 break; 172 case BEGIN_ARRAY: 173 parseArray(parser, node); 174 break; 175 case NUMBER: 176 node.raw(readNumber(parser)); 177 break; 178 case STRING: 179 node.raw(parser.nextString()); 180 break; 181 case BOOLEAN: 182 node.raw(parser.nextBoolean()); 183 break; 184 case NULL: // Ignored values 185 parser.nextNull(); 186 node.raw(null); 187 break; 188 case NAME: 189 break; 190 default: 191 throw newException(parser, node, "Unsupported token type: " + token, null); 192 } 193 } catch (final JsonParseException | MalformedJsonException ex) { 194 throw newException(parser, node, ex.getMessage(), ex.getCause()); 195 } catch (final ParsingException ex) { 196 ex.initPath(node::path); 197 throw ex; 198 } catch (final IOException ex) { 199 throw newException(parser, node, "An underlying exception occurred", ex); 200 } 201 } 202 203 private ParsingException newException(final JsonReader reader, final ConfigurationNode node, final @Nullable String message, 204 final @Nullable Throwable cause) { 205 return new ParsingException(node, JsonReaderAccess.lineNumber(reader), JsonReaderAccess.column(reader), null, message, cause); 206 } 207 208 private Number readNumber(final JsonReader reader) throws IOException { 209 final String number = reader.nextString(); 210 if (number.contains(".")) { 211 return Double.parseDouble(number); 212 } 213 final long nextLong = Long.parseLong(number); 214 final int nextInt = (int) nextLong; 215 if (nextInt == nextLong) { 216 return nextInt; 217 } 218 return nextLong; 219 } 220 221 private void parseArray(final JsonReader parser, final BasicConfigurationNode node) throws IOException { 222 parser.beginArray(); 223 224 boolean written = false; 225 @Nullable JsonToken token; 226 while ((token = parser.peek()) != null) { 227 if (token == JsonToken.END_ARRAY) { 228 parser.endArray(); 229 // ensure the type is preserved 230 if (!written) { 231 node.raw(Collections.emptyList()); 232 } 233 return; 234 } else { 235 parseValue(parser, node.appendListNode()); 236 written = true; 237 } 238 } 239 throw newException(parser, node, "Reached end of stream with unclosed array!", null); 240 } 241 242 private void parseObject(final JsonReader parser, final BasicConfigurationNode node) throws ParsingException, IOException { 243 parser.beginObject(); 244 245 boolean written = false; 246 @Nullable JsonToken token; 247 while ((token = parser.peek()) != null) { 248 switch (token) { 249 case END_OBJECT: 250 case END_DOCUMENT: 251 parser.endObject(); 252 // ensure the type is preserved 253 if (!written) { 254 node.raw(Collections.emptyMap()); 255 } 256 return; 257 case NAME: 258 parseValue(parser, node.node(parser.nextName())); 259 written = true; 260 break; 261 default: 262 throw new JsonParseException("Received improper object value " + token); 263 } 264 } 265 throw new JsonParseException("Reached end of stream with unclosed object!"); 266 } 267 268 @Override 269 protected void saveInternal(final ConfigurationNode node, final Writer writer) throws ConfigurateException { 270 if (!this.lenient && !node.isMap()) { 271 throw new ConfigurateException(node, "Non-lenient json generators must have children of map type"); 272 } 273 try { 274 try (JsonWriter generator = new JsonWriter(writer)) { 275 generator.setIndent(this.indent); 276 generator.setLenient(this.lenient); 277 node.visit(GsonVisitor.INSTANCE.get(), generator); 278 writer.write(SYSTEM_LINE_SEPARATOR); // Jackson doesn't add a newline at the end of files by default 279 } 280 } catch (final IOException ex) { 281 throw ConfigurateException.wrap(node, ex); 282 } 283 } 284 285 @Override 286 public BasicConfigurationNode createNode(final ConfigurationOptions options) { 287 return BasicConfigurationNode.root(options.nativeTypes(NATIVE_TYPES)); 288 } 289 290}