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