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>&lt;prefix&gt;.yaml.node-style</dt>
092     *     <dd>Equivalent to {@link #nodeStyle(NodeStyle)}</dd>
093     *     <dt>&lt;prefix&gt;.yaml.comments-enabled</dt>
094     *     <dd>Equivalent to {@link #commentsEnabled(boolean)}</dd>
095     *     <dt>&lt;prefix&gt;.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        node.from(this.yaml.get().load(reader));
273    }
274
275    @Override
276    protected void saveInternal(final ConfigurationNode node, final Writer writer) {
277        this.yaml.get().dump(node, writer);
278    }
279
280    @Override
281    public CommentedConfigurationNode createNode(final ConfigurationOptions options) {
282        return CommentedConfigurationNode.root(options);
283    }
284
285}