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