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 org.checkerframework.checker.nullness.qual.Nullable; 020import org.spongepowered.configurate.CommentedConfigurationNode; 021import org.spongepowered.configurate.ConfigurationNode; 022import org.spongepowered.configurate.ConfigurationOptions; 023import org.spongepowered.configurate.RepresentationHint; 024import org.spongepowered.configurate.loader.AbstractConfigurationLoader; 025import org.spongepowered.configurate.loader.CommentHandler; 026import org.spongepowered.configurate.loader.CommentHandlers; 027import org.spongepowered.configurate.loader.LoaderOptionSource; 028import org.spongepowered.configurate.util.UnmodifiableCollections; 029import org.yaml.snakeyaml.DumperOptions; 030import org.yaml.snakeyaml.LoaderOptions; 031import org.yaml.snakeyaml.Yaml; 032 033import java.io.BufferedReader; 034import java.io.Writer; 035import java.math.BigInteger; 036import java.sql.Timestamp; 037import java.util.Date; 038import java.util.Set; 039 040/** 041 * A loader for YAML-formatted configurations, using the SnakeYAML library for 042 * parsing and generation. 043 * 044 * @since 4.0.0 045 */ 046public final class YamlConfigurationLoader extends AbstractConfigurationLoader<CommentedConfigurationNode> { 047 048 /** 049 * YAML native types from <a href="https://yaml.org/type/">YAML 1.1 Global tags</a>. 050 * 051 * <p>using SnakeYaml representation: https://bitbucket.org/snakeyaml/snakeyaml/wiki/Documentation#markdown-header-yaml-tags-and-java-types 052 */ 053 private static final Set<Class<?>> NATIVE_TYPES = UnmodifiableCollections.toSet( 054 Boolean.class, Integer.class, Long.class, BigInteger.class, Double.class, // numeric 055 byte[].class, String.class, Date.class, java.sql.Date.class, Timestamp.class); // complex types 056 057 /** 058 * The YAML scalar style this node should attempt to use. 059 * 060 * <p>If the chosen scalar style would produce syntactically invalid YAML, a 061 * valid one will replace it.</p> 062 * 063 * @since 4.2.0 064 */ 065 public static final RepresentationHint<ScalarStyle> SCALAR_STYLE = RepresentationHint.of("configurate:yaml/scalarstyle", ScalarStyle.class); 066 067 /** 068 * The YAML node style to use for collection nodes. A {@code null} value 069 * will instruct the emitter to fall back to the 070 * {@link Builder#nodeStyle()} setting. 071 * 072 * @since 4.2.0 073 */ 074 public static final RepresentationHint<NodeStyle> NODE_STYLE = RepresentationHint.of("configurate:yaml/nodestyle", NodeStyle.class); 075 076 /** 077 * Creates a new {@link YamlConfigurationLoader} builder. 078 * 079 * @return a new builder 080 * @since 4.0.0 081 */ 082 public static Builder builder() { 083 return new Builder(); 084 } 085 086 /** 087 * Builds a {@link YamlConfigurationLoader}. 088 * 089 * <p>This builder supports the following options:</p> 090 * <dl> 091 * <dt><prefix>.yaml.node-style</dt> 092 * <dd>Equivalent to {@link #nodeStyle(NodeStyle)}</dd> 093 * <dt><prefix>.yaml.comments-enabled</dt> 094 * <dd>Equivalent to {@link #commentsEnabled(boolean)}</dd> 095 * <dt><prefix>.yaml.line-length</dt> 096 * <dd>Equivalent to {@link #lineLength(int)}</dd> 097 * </dl> 098 * 099 * @since 4.0.0 100 */ 101 public static final class Builder extends AbstractConfigurationLoader.Builder<Builder, YamlConfigurationLoader> { 102 private final DumperOptions options = new DumperOptions(); 103 private @Nullable NodeStyle style; 104 private boolean enableComments; 105 private int lineLength; 106 107 Builder() { 108 this.indent(4); 109 this.defaultOptions(o -> o.nativeTypes(NATIVE_TYPES)); 110 this.from(DEFAULT_OPTIONS_SOURCE); 111 } 112 113 @Override 114 protected void populate(final LoaderOptionSource options) { 115 final @Nullable NodeStyle declared = options.getEnum(NodeStyle.class, "yaml", "node-style"); 116 if (declared != null) { 117 this.style = declared; 118 } 119 this.enableComments = options.getBoolean(true, "yaml", "comments-enabled"); 120 this.lineLength = options.getInt(150, "yaml", "line-length"); 121 } 122 123 /** 124 * Sets the level of indentation the resultant loader should use. 125 * 126 * @param indent the indent level 127 * @return this builder (for chaining) 128 * @since 4.0.0 129 */ 130 public Builder indent(final int indent) { 131 this.options.setIndent(indent); 132 return this; 133 } 134 135 /** 136 * Gets the level of indentation to be used by the resultant loader. 137 * 138 * @return the indent level 139 * @since 4.0.0 140 */ 141 public int indent() { 142 return this.options.getIndent(); 143 } 144 145 /** 146 * Sets the node style the built loader should use. 147 * 148 * <dl><dt>Flow</dt> 149 * <dd>the compact, json-like representation.<br> 150 * Example: <code> 151 * {value: [list, of, elements], another: value} 152 * </code></dd> 153 * 154 * <dt>Block</dt> 155 * <dd>expanded, traditional YAML<br> 156 * Example: <code> 157 * value: 158 * - list 159 * - of 160 * - elements 161 * another: value 162 * </code></dd> 163 * </dl> 164 * 165 * <p>A {@code null} value will tell the loader to pick a value 166 * automatically based on the contents of each non-scalar node.</p> 167 * 168 * @param style the node style to use 169 * @return this builder (for chaining) 170 * @since 4.0.0 171 */ 172 public Builder nodeStyle(final @Nullable NodeStyle style) { 173 this.style = style; 174 return this; 175 } 176 177 /** 178 * Gets the node style to be used by the resultant loader. 179 * 180 * @return the node style 181 * @since 4.0.0 182 */ 183 public @Nullable NodeStyle nodeStyle() { 184 return this.style; 185 } 186 187 /** 188 * Set whether comment handling is enabled on this loader. 189 * 190 * <p>When comment handling is enabled, comments will be read from files 191 * and written back to files where possible.</p> 192 * 193 * <p>The default value is {@code true}</p> 194 * 195 * @param enableComments whether comment handling should be enabled 196 * @return this builder (for chaining) 197 * @since 4.2.0 198 */ 199 public Builder commentsEnabled(final boolean enableComments) { 200 this.enableComments = enableComments; 201 return this; 202 } 203 204 /** 205 * Get whether comment handling is enabled. 206 * 207 * @return whether comment handling is enabled 208 * @see #commentsEnabled(boolean) for details on comment handling 209 * @since 4.2.0 210 */ 211 public boolean commentsEnabled() { 212 return this.enableComments; 213 } 214 215 /** 216 * Set the maximum length of a configuration line. 217 * 218 * <p>The default value is {@code 150}</p> 219 * 220 * @param lineLength the maximum length of a configuration line 221 * @return this builder (for chaining) 222 * @since 4.2.0 223 */ 224 public Builder lineLength(final int lineLength) { 225 this.lineLength = lineLength; 226 return this; 227 } 228 229 /** 230 * Get the maximum length of a configuration line. 231 * 232 * @return the maximum length of a configuration line 233 * @see #lineLength(int) for details on the line length 234 * @since 4.2.0 235 */ 236 public int lineLength() { 237 return this.lineLength; 238 } 239 240 @Override 241 public YamlConfigurationLoader build() { 242 return new YamlConfigurationLoader(this); 243 } 244 } 245 246 private final ThreadLocal<YamlConstructor> constructor; 247 private final ThreadLocal<Yaml> yaml; 248 249 private YamlConfigurationLoader(final Builder builder) { 250 super(builder, new CommentHandler[] {CommentHandlers.HASH}); 251 final LoaderOptions loaderOpts = new LoaderOptions() 252 .setAcceptTabs(true) 253 .setProcessComments(builder.commentsEnabled()); 254 loaderOpts.setCodePointLimit(Integer.MAX_VALUE); 255 256 final DumperOptions opts = builder.options; 257 opts.setDefaultFlowStyle(NodeStyle.asSnakeYaml(builder.style)); 258 opts.setProcessComments(builder.commentsEnabled()); 259 opts.setWidth(builder.lineLength()); 260 opts.setIndicatorIndent(builder.indent()); 261 opts.setIndentWithIndicator(true); 262 // the constructor needs ConfigurationOptions, which is only available when called (loadInternal) 263 this.constructor = ThreadLocal.withInitial(() -> new YamlConstructor(loaderOpts)); 264 this.yaml = ThreadLocal.withInitial(() -> new Yaml(this.constructor.get(), new YamlRepresenter(true, opts), opts, loaderOpts)); 265 } 266 267 @Override 268 protected void loadInternal(final CommentedConfigurationNode node, final BufferedReader reader) { 269 // the constructor needs ConfigurationOptions for the to be created nodes 270 // and since it's a thread-local, this won't cause any issues 271 this.constructor.get().options = node.options(); 272 273 @Nullable CommentedConfigurationNode loaded = this.yaml.get().load(reader); 274 // when a file exists but is empty (or if the file only exists of comments), the first event will be StreamEnd. 275 // getSingleNode will return null, getSingleData uses the Constructor of Tag Null, which just returns null. 276 // So we have to map null to an empty root node. 277 if (loaded == null) { 278 loaded = CommentedConfigurationNode.root(node.options()); 279 } 280 node.from(loaded); 281 } 282 283 @Override 284 protected void saveInternal(final ConfigurationNode node, final Writer writer) { 285 this.yaml.get().dump(node, writer); 286 } 287 288 @Override 289 public CommentedConfigurationNode createNode(final ConfigurationOptions options) { 290 return CommentedConfigurationNode.root(options); 291 } 292 293}