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.jackson; 018 019import com.fasterxml.jackson.core.JsonFactory; 020import com.fasterxml.jackson.core.JsonFactoryBuilder; 021import com.fasterxml.jackson.core.JsonGenerator; 022import com.fasterxml.jackson.core.JsonLocation; 023import com.fasterxml.jackson.core.JsonParser; 024import com.fasterxml.jackson.core.JsonToken; 025import com.fasterxml.jackson.core.exc.StreamReadException; 026import com.fasterxml.jackson.core.json.JsonReadFeature; 027import net.kyori.option.Option; 028import net.kyori.option.OptionSchema; 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.util.UnmodifiableCollections; 040 041import java.io.BufferedReader; 042import java.io.IOException; 043import java.io.Writer; 044import java.util.Collections; 045import java.util.List; 046import java.util.Map; 047import java.util.Set; 048 049/** 050 * A loader for JSON-formatted configurations, using the jackson library for 051 * parsing and generation. 052 * 053 * @since 4.0.0 054 */ 055public final class JacksonConfigurationLoader extends AbstractConfigurationLoader<BasicConfigurationNode> { 056 057 private static final Set<Class<?>> NATIVE_TYPES = UnmodifiableCollections.toSet(Map.class, List.class, Double.class, Float.class, 058 Long.class, Integer.class, Boolean.class, String.class, byte[].class); 059 060 /** 061 * Creates a new {@link JacksonConfigurationLoader} builder. 062 * 063 * @return a new builder 064 * @since 4.0.0 065 */ 066 public static Builder builder() { 067 return new Builder(); 068 } 069 070 /** 071 * Builds a {@link JacksonConfigurationLoader}. 072 * 073 * <p>This builder supports the following options:</p> 074 * <ul> 075 * <li>{@link #INDENT}</li> 076 * <li>{@link #FIELD_VALUE_SEPARATOR}</li> 077 * </ul> 078 * 079 * @since 4.0.0 080 */ 081 public static final class Builder extends AbstractConfigurationLoader.Builder<Builder, JacksonConfigurationLoader> { 082 083 private static final OptionSchema.Mutable UNSAFE_SCHEMA = OptionSchema.childSchema(AbstractConfigurationLoader.Builder.SCHEMA); 084 085 /** 086 * A schema of options available to configure the Jackson loader. 087 * 088 * @since 4.2.0 089 */ 090 public static final OptionSchema SCHEMA = UNSAFE_SCHEMA.frozenView(); 091 092 /** 093 * Set the indentation to when emitting json. 094 * 095 * @see #indent(int) 096 * @since 4.2.0 097 */ 098 public static final Option<Integer> INDENT = UNSAFE_SCHEMA.intOption("jackson:indent", 2); 099 100 /** 101 * Set the field-value separator style to be used when emitting json. 102 * 103 * @see #fieldValueSeparatorStyle(FieldValueSeparatorStyle) 104 * @since 4.2.0 105 */ 106 public static final Option<FieldValueSeparatorStyle> FIELD_VALUE_SEPARATOR = UNSAFE_SCHEMA.enumOption( 107 "jackson:field-value-separator", 108 FieldValueSeparatorStyle.class, 109 FieldValueSeparatorStyle.SPACE_AFTER 110 ); 111 112 private final JsonFactoryBuilder factory = new JsonFactoryBuilder(); 113 114 Builder() { 115 this.factory.enable(JsonReadFeature.ALLOW_JAVA_COMMENTS) 116 .enable(JsonReadFeature.ALLOW_YAML_COMMENTS) 117 .enable(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER) 118 .enable(JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES) 119 .enable(JsonReadFeature.ALLOW_SINGLE_QUOTES) 120 .enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS) 121 .enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS); 122 } 123 124 @Override 125 protected OptionSchema optionSchema() { 126 return SCHEMA; 127 } 128 129 /** 130 * Gets the {@link JsonFactory} used to configure the implementation. 131 * 132 * @return the json factory 133 * @since 4.0.0 134 */ 135 public JsonFactoryBuilder factoryBuilder() { 136 return this.factory; 137 } 138 139 /** 140 * Sets the level of indentation the resultant loader should use. 141 * 142 * @param indent the indent level 143 * @return this builder (for chaining) 144 * @since 4.0.0 145 */ 146 public Builder indent(final int indent) { 147 this.optionStateBuilder().value(INDENT, indent); 148 return this; 149 } 150 151 /** 152 * Gets the level of indentation to be used by the resultant loader. 153 * 154 * @return the indent level 155 * @since 4.0.0 156 */ 157 public int indent() { 158 return this.optionState().value(INDENT); 159 } 160 161 /** 162 * Sets the field value separator style the resultant loader should use. 163 * 164 * @param style the style 165 * @return this builder (for chaining) 166 * @since 4.0.0 167 */ 168 public Builder fieldValueSeparatorStyle(final FieldValueSeparatorStyle style) { 169 this.optionStateBuilder().value(FIELD_VALUE_SEPARATOR, style); 170 return this; 171 } 172 173 /** 174 * Gets the field value separator style to be used by the built loader. 175 * 176 * @return the style 177 * @since 4.0.0 178 */ 179 public FieldValueSeparatorStyle fieldValueSeparatorStyle() { 180 return this.optionState().value(FIELD_VALUE_SEPARATOR); 181 } 182 183 @Override 184 public JacksonConfigurationLoader build() { 185 defaultOptions(o -> o.nativeTypes(NATIVE_TYPES)); 186 return new JacksonConfigurationLoader(this); 187 } 188 } 189 190 private final JsonFactory factory; 191 private final int indent; 192 private final FieldValueSeparatorStyle fieldValueSeparatorStyle; 193 194 private JacksonConfigurationLoader(final Builder builder) { 195 super(builder, new CommentHandler[]{CommentHandlers.DOUBLE_SLASH, CommentHandlers.SLASH_BLOCK, CommentHandlers.HASH}); 196 this.factory = builder.factoryBuilder().build(); 197 this.factory.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE); 198 this.indent = builder.optionState().value(Builder.INDENT); 199 this.fieldValueSeparatorStyle = builder.optionState().value(Builder.FIELD_VALUE_SEPARATOR); 200 } 201 202 private static final int MAX_CTX_LENGTH = 80; 203 204 @Override 205 protected void loadInternal(final BasicConfigurationNode node, final BufferedReader reader) throws ParsingException { 206 try (JsonParser parser = this.factory.createParser(reader)) { 207 parser.nextToken(); 208 parseValue(parser, node); 209 } catch (final StreamReadException ex) { 210 throw newException(node, ex.getLocation(), ex.getRequestPayloadAsString(), ex.getMessage(), ex.getCause()); 211 } catch (final IOException ex) { 212 throw ParsingException.wrap(node, ex); 213 } 214 } 215 216 private static void parseValue(final JsonParser parser, final ConfigurationNode node) throws IOException { 217 try { 218 final JsonToken token = parser.getCurrentToken(); 219 switch (token) { 220 case START_OBJECT: 221 parseObject(parser, node); 222 break; 223 case START_ARRAY: 224 parseArray(parser, node); 225 break; 226 case VALUE_NUMBER_FLOAT: 227 final double doubleVal = parser.getDoubleValue(); 228 if ((float) doubleVal != doubleVal) { 229 node.raw(parser.getDoubleValue()); 230 } else { 231 node.raw(parser.getFloatValue()); 232 } 233 break; 234 case VALUE_NUMBER_INT: 235 final long longVal = parser.getLongValue(); 236 if ((int) longVal != longVal) { 237 node.raw(parser.getLongValue()); 238 } else { 239 node.raw(parser.getIntValue()); 240 } 241 break; 242 case VALUE_STRING: 243 node.raw(parser.getText()); 244 break; 245 case VALUE_TRUE: 246 case VALUE_FALSE: 247 node.raw(parser.getBooleanValue()); 248 break; 249 case VALUE_NULL: // Ignored values 250 case FIELD_NAME: 251 break; 252 default: 253 final JsonLocation loc = JacksonCompat.currentTokenLocation(parser); 254 throw new ParsingException(node, loc.getLineNr(), loc.getColumnNr(), parser.getText(), "Unsupported token type: " + token, null); 255 } 256 } catch (final StreamReadException ex) { 257 throw newException(node, ex.getLocation(), ex.getRequestPayloadAsString(), ex.getMessage(), ex.getCause()); 258 } 259 } 260 261 private static void parseArray(final JsonParser parser, final ConfigurationNode node) throws IOException { 262 boolean written = false; 263 JsonToken token; 264 while ((token = parser.nextToken()) != null) { 265 if (token == JsonToken.END_ARRAY) { // ensure the type is preserved 266 if (!written) { 267 node.raw(Collections.emptyList()); 268 } 269 return; 270 } else { 271 parseValue(parser, node.appendListNode()); 272 written = true; 273 } 274 } 275 throw newException(node, JacksonCompat.currentLocation(parser), null, "Reached end of stream with unclosed array!", null); 276 } 277 278 private static void parseObject(final JsonParser parser, final ConfigurationNode node) throws IOException { 279 boolean written = false; 280 JsonToken token; 281 while ((token = parser.nextToken()) != null) { 282 if (token == JsonToken.END_OBJECT) { // ensure the type is preserved 283 if (!written) { 284 node.raw(Collections.emptyMap()); 285 } 286 return; 287 } else { 288 parseValue(parser, node.node(JacksonCompat.currentName(parser))); 289 written = true; 290 } 291 } 292 throw newException(node, JacksonCompat.currentLocation(parser), null, "Reached end of stream with unclosed object!", null); 293 } 294 295 @Override 296 protected void saveInternal(final ConfigurationNode node, final Writer writer) throws ConfigurateException { 297 try (JsonGenerator generator = this.factory.createGenerator(writer)) { 298 generator.setPrettyPrinter(new ConfiguratePrettyPrinter(this.indent, this.fieldValueSeparatorStyle)); 299 node.visit(JacksonVisitor.INSTANCE.get(), generator); 300 writer.write(SYSTEM_LINE_SEPARATOR); // Jackson doesn't add a newline at the end of files by default 301 } catch (final IOException ex) { 302 throw ConfigurateException.wrap(node, ex); 303 } 304 } 305 306 @Override 307 public BasicConfigurationNode createNode(final @NonNull ConfigurationOptions options) { 308 return BasicConfigurationNode.root(options.nativeTypes(NATIVE_TYPES)); 309 } 310 311 private static ParsingException newException(final ConfigurationNode node, 312 final JsonLocation position, 313 final @Nullable String content, 314 final @Nullable String message, 315 final @Nullable Throwable cause) { 316 @Nullable String context = content == null ? null : content.substring((int) position.getCharOffset()); 317 if (context != null) { 318 int nextLine = context.indexOf('\n'); 319 if (nextLine == -1) { 320 nextLine = context.length(); 321 } else if (context.charAt(nextLine - 1) == '\r') { 322 nextLine--; 323 } 324 325 if (nextLine > MAX_CTX_LENGTH) { 326 context = context.substring(0, MAX_CTX_LENGTH) + "..."; 327 } else { 328 context = context.substring(0, nextLine); 329 } 330 } 331 // no newline: set to length 332 // too long: truncate 333 // otherwise: trim to position of next newline 334 return new ParsingException(node, position.getLineNr(), position.getColumnNr(), context, message, cause); 335 } 336 337}