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.yaml; 018 019import net.kyori.option.Option; 020import net.kyori.option.OptionSchema; 021import org.checkerframework.checker.nullness.qual.Nullable; 022import org.spongepowered.configurate.CommentedConfigurationNode; 023import org.spongepowered.configurate.ConfigurationNode; 024import org.spongepowered.configurate.ConfigurationOptions; 025import org.spongepowered.configurate.RepresentationHint; 026import org.spongepowered.configurate.loader.AbstractConfigurationLoader; 027import org.spongepowered.configurate.loader.CommentHandler; 028import org.spongepowered.configurate.loader.CommentHandlers; 029import org.spongepowered.configurate.util.UnmodifiableCollections; 030import org.yaml.snakeyaml.DumperOptions; 031import org.yaml.snakeyaml.LoaderOptions; 032import org.yaml.snakeyaml.Yaml; 033import org.yaml.snakeyaml.constructor.Constructor; 034 035import java.io.BufferedReader; 036import java.io.Writer; 037import java.math.BigInteger; 038import java.sql.Timestamp; 039import java.util.Date; 040import java.util.Set; 041 042/** 043 * A loader for YAML-formatted configurations, using the SnakeYAML library for 044 * parsing and generation. 045 * 046 * @since 4.0.0 047 */ 048public final class YamlConfigurationLoader extends AbstractConfigurationLoader<CommentedConfigurationNode> { 049 050 /** 051 * YAML native types from <a href="https://yaml.org/type/">YAML 1.1 Global tags</a>. 052 * 053 * <p>using SnakeYaml representation: https://bitbucket.org/snakeyaml/snakeyaml/wiki/Documentation#markdown-header-yaml-tags-and-java-types 054 */ 055 private static final Set<Class<?>> NATIVE_TYPES = UnmodifiableCollections.toSet( 056 Boolean.class, Integer.class, Long.class, BigInteger.class, Double.class, // numeric 057 byte[].class, String.class, Date.class, java.sql.Date.class, Timestamp.class); // complex types 058 059 /** 060 * The YAML scalar style this node should attempt to use. 061 * 062 * <p>If the chosen scalar style would produce syntactically invalid YAML, a 063 * valid one will replace it.</p> 064 * 065 * @since 4.3.0 066 */ 067 public static final RepresentationHint<ScalarStyle> SCALAR_STYLE = RepresentationHint.of("configurate:yaml/scalar_style", ScalarStyle.class); 068 069 /** 070 * The YAML node style to use for collection nodes. A {@code null} value 071 * will instruct the emitter to fall back to the 072 * {@link Builder#nodeStyle()} setting. 073 * 074 * @since 4.3.0 075 */ 076 public static final RepresentationHint<NodeStyle> NODE_STYLE = RepresentationHint.of("configurate:yaml/node_style", NodeStyle.class); 077 078 /** 079 * The amount of blank lines that a specific node should have. 080 * Absent is to inherit the {@link BlankLineStyle} of the document. 081 * 082 * @since 4.3.0 083 */ 084 public static final RepresentationHint<Integer> BLANK_LINE_STYLE_OVERRIDE 085 = RepresentationHint.of("configurate:yaml/blank_line_style_override", Integer.class); 086 087 /** 088 * Creates a new {@link YamlConfigurationLoader} builder. 089 * 090 * @return a new builder 091 * @since 4.0.0 092 */ 093 public static Builder builder() { 094 return new Builder(); 095 } 096 097 /** 098 * Builds a {@link YamlConfigurationLoader}. 099 * 100 * <p>This builder supports the following options:</p> 101 * <dl> 102 * <dt><prefix>.yaml.node-style</dt> 103 * <dd>Equivalent to {@link #nodeStyle(NodeStyle)}</dd> 104 * <dt><prefix>.yaml.comments-enabled</dt> 105 * <dd>Equivalent to {@link #commentsEnabled(boolean)}</dd> 106 * <dt><prefix>.yaml.line-length</dt> 107 * <dd>Equivalent to {@link #lineLength(int)}</dd> 108 * </dl> 109 * 110 * @since 4.0.0 111 */ 112 public static final class Builder extends AbstractConfigurationLoader.Builder<Builder, YamlConfigurationLoader> { 113 114 private static final OptionSchema.Mutable UNSAFE_SCHEMA = OptionSchema.childSchema(AbstractConfigurationLoader.Builder.SCHEMA); 115 116 /** 117 * A schema of options available to configure the YAML loader. 118 * 119 * @since 4.2.0 120 */ 121 public static final OptionSchema SCHEMA = UNSAFE_SCHEMA.frozenView(); 122 123 /** 124 * The collection node style to use globally when emitting with 125 * this loader. 126 * 127 * @see #nodeStyle(NodeStyle) 128 * @since 4.2.0 129 */ 130 public static final Option<NodeStyle> NODE_STYLE = UNSAFE_SCHEMA.enumOption("yaml:node_style", NodeStyle.class, null); 131 132 /** 133 * The indent size (in spaces) to use for documents emitted by 134 * the created loader. 135 * 136 * @see #indent(int) 137 * @since 4.2.0 138 */ 139 public static final Option<Integer> INDENT = UNSAFE_SCHEMA.intOption("yaml:indent", 4); 140 141 /** 142 * Whether comments are being read and written. 143 * 144 * @see #commentsEnabled(boolean) 145 * @since 4.3.0 146 */ 147 public static final Option<Boolean> COMMENTS_ENABLED = UNSAFE_SCHEMA.booleanOption("yaml:comments_enabled", true); 148 149 /** 150 * The line length before the scalar representation is split into multiple lines. 151 * 152 * @see #lineLength(int) 153 * @since 4.3.0 154 */ 155 public static final Option<Integer> LINE_LENGTH = UNSAFE_SCHEMA.intOption("yaml:line_length", 150); 156 157 /** 158 * The blank line style for writing YAML documents. 159 * 160 * @see #blankLineStyle(BlankLineStyle) 161 * @since 4.3.0 162 */ 163 public static final Option<BlankLineStyle> BLANK_LINE_STYLE = UNSAFE_SCHEMA.enumOption("yaml:blank_line_style", BlankLineStyle.class, null); 164 165 private final DumperOptions options = new DumperOptions(); 166 167 Builder() { 168 this.defaultOptions(o -> o.nativeTypes(NATIVE_TYPES)); 169 } 170 171 @Override 172 protected OptionSchema optionSchema() { 173 return SCHEMA; 174 } 175 176 /** 177 * Sets the level of indentation the resultant loader should use. 178 * 179 * @param indent the indent level 180 * @return this builder (for chaining) 181 * @since 4.0.0 182 */ 183 public Builder indent(final int indent) { 184 this.optionStateBuilder().value(INDENT, indent); 185 return this; 186 } 187 188 /** 189 * Gets the level of indentation to be used by the resultant loader. 190 * 191 * @return the indent level 192 * @since 4.0.0 193 */ 194 public int indent() { 195 return this.optionState().value(INDENT); 196 } 197 198 /** 199 * Sets the node style the built loader should use. 200 * 201 * <dl><dt>Flow</dt> 202 * <dd>the compact, json-like representation.<br> 203 * Example: <code> 204 * {value: [list, of, elements], another: value} 205 * </code></dd> 206 * 207 * <dt>Block</dt> 208 * <dd>expanded, traditional YAML<br> 209 * Example: <code> 210 * value: 211 * - list 212 * - of 213 * - elements 214 * another: value 215 * </code></dd> 216 * </dl> 217 * 218 * <p>A {@code null} value will tell the loader to pick a value 219 * automatically based on the contents of each non-scalar node.</p> 220 * 221 * @param style the node style to use 222 * @return this builder (for chaining) 223 * @since 4.0.0 224 */ 225 public Builder nodeStyle(final @Nullable NodeStyle style) { 226 this.optionStateBuilder().value(NODE_STYLE, style); 227 return this; 228 } 229 230 /** 231 * Gets the node style to be used by the resultant loader. 232 * 233 * @return the node style 234 * @since 4.0.0 235 */ 236 public @Nullable NodeStyle nodeStyle() { 237 return this.optionState().value(NODE_STYLE); 238 } 239 240 /** 241 * Sets the blank line style handling for all written files. 242 * 243 * @param style the style to apply 244 * @return this builder 245 * @see BlankLineStyle 246 * @since 4.3.0 247 */ 248 public Builder blankLineStyle(final @Nullable BlankLineStyle style) { 249 this.optionStateBuilder().value(BLANK_LINE_STYLE, style); 250 return this; 251 } 252 253 /** 254 * Gets the explicitly applied current blank line styling. 255 * This doesn't return the implicit fallback used when not setting it explicitly. 256 * 257 * @return the blank line style 258 * @since 4.3.0 259 */ 260 public @Nullable BlankLineStyle blankLineStyle() { 261 return this.optionState().value(BLANK_LINE_STYLE); 262 } 263 264 /** 265 * Set whether comment handling is enabled on this loader. 266 * 267 * <p>When comment handling is enabled, comments will be read from files 268 * and written back to files where possible.</p> 269 * 270 * <p>The default value is {@code true}</p> 271 * 272 * @param enableComments whether comment handling should be enabled 273 * @return this builder (for chaining) 274 * @since 4.3.0 275 */ 276 public Builder commentsEnabled(final boolean enableComments) { 277 this.optionStateBuilder().value(COMMENTS_ENABLED, enableComments); 278 return this; 279 } 280 281 /** 282 * Get whether comment handling is enabled. 283 * 284 * @return whether comment handling is enabled 285 * @see #commentsEnabled(boolean) for details on comment handling 286 * @since 4.3.0 287 */ 288 public boolean commentsEnabled() { 289 return this.optionState().value(COMMENTS_ENABLED); 290 } 291 292 /** 293 * Set the maximum length of a configuration line. 294 * 295 * <p>The default value is {@code 150}</p> 296 * 297 * @param lineLength the maximum length of a configuration line 298 * @return this builder (for chaining) 299 * @since 4.3.0 300 */ 301 public Builder lineLength(final int lineLength) { 302 this.optionStateBuilder().value(LINE_LENGTH, lineLength); 303 return this; 304 } 305 306 /** 307 * Get the maximum length of a configuration line. 308 * 309 * @return the maximum length of a configuration line 310 * @see #lineLength(int) for details on the line length 311 * @since 4.3.0 312 */ 313 public int lineLength() { 314 return this.optionState().value(LINE_LENGTH); 315 } 316 317 @Override 318 public YamlConfigurationLoader build() { 319 return new YamlConfigurationLoader(this); 320 } 321 } 322 323 private final LoaderOptions loaderOpts; 324 private final DumperOptions dumperOpts; 325 private final BlankLineStyle blankLineStyle; 326 327 private YamlConfigurationLoader(final Builder builder) { 328 super(builder, new CommentHandler[] {CommentHandlers.HASH}); 329 this.loaderOpts = new LoaderOptions() 330 .setAcceptTabs(true) 331 .setProcessComments(builder.commentsEnabled()); 332 this.loaderOpts.setCodePointLimit(Integer.MAX_VALUE); 333 334 this.dumperOpts = builder.options; 335 this.dumperOpts.setDefaultFlowStyle(NodeStyle.asSnakeYaml(builder.optionState().value(Builder.NODE_STYLE))); 336 this.dumperOpts.setIndent(builder.optionState().value(Builder.INDENT)); 337 this.dumperOpts.setProcessComments(builder.optionState().value(Builder.COMMENTS_ENABLED)); 338 this.dumperOpts.setWidth(builder.optionState().value(Builder.LINE_LENGTH)); 339 this.dumperOpts.setIndicatorIndent(builder.optionState().value(Builder.INDENT)); 340 this.dumperOpts.setIndentWithIndicator(true); 341 342 final @Nullable BlankLineStyle configuredBlankLineStyle = builder.optionState().value(Builder.BLANK_LINE_STYLE); 343 this.blankLineStyle = configuredBlankLineStyle != null ? configuredBlankLineStyle : BlankLineStyle.NONE; 344 } 345 346 @Override 347 protected void loadInternal(final CommentedConfigurationNode node, final BufferedReader reader) { 348 final Constructor constructor = new YamlConstructor(this.loaderOpts, node.options(), this.blankLineStyle); 349 final Yaml yaml = new Yaml(constructor, new YamlRepresenter(this.blankLineStyle, true, this.dumperOpts), this.dumperOpts, this.loaderOpts); 350 351 @Nullable CommentedConfigurationNode loaded = yaml.load(reader); 352 // when a file exists but is empty (or if the file only exists of comments), the first event will be StreamEnd. 353 // getSingleNode will return null, getSingleData uses the Constructor of Tag Null, which just returns null. 354 // So we have to map null to an empty root node. 355 if (loaded == null) { 356 loaded = CommentedConfigurationNode.root(node.options()); 357 } 358 node.from(loaded); 359 } 360 361 @Override 362 protected void saveInternal(final ConfigurationNode node, final Writer writer) { 363 final Constructor constructor = new YamlConstructor(this.loaderOpts, node.options(), this.blankLineStyle); 364 final Yaml yaml = new Yaml(constructor, new YamlRepresenter(this.blankLineStyle, true, this.dumperOpts), this.dumperOpts, this.loaderOpts); 365 yaml.dump(node, writer); 366 } 367 368 @Override 369 public CommentedConfigurationNode createNode(final ConfigurationOptions options) { 370 return CommentedConfigurationNode.root(options); 371 } 372 373}