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>&lt;prefix&gt;.yaml.node-style</dt>
103     *     <dd>Equivalent to {@link #nodeStyle(NodeStyle)}</dd>
104     *     <dt>&lt;prefix&gt;.yaml.comments-enabled</dt>
105     *     <dd>Equivalent to {@link #commentsEnabled(boolean)}</dd>
106     *     <dt>&lt;prefix&gt;.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}