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.loader.AbstractConfigurationLoader;
024import org.spongepowered.configurate.loader.CommentHandler;
025import org.spongepowered.configurate.loader.CommentHandlers;
026import org.spongepowered.configurate.loader.LoaderOptionSource;
027import org.spongepowered.configurate.util.UnmodifiableCollections;
028import org.yaml.snakeyaml.DumperOptions;
029import org.yaml.snakeyaml.LoaderOptions;
030import org.yaml.snakeyaml.Yaml;
031import org.yaml.snakeyaml.constructor.Constructor;
032import org.yaml.snakeyaml.representer.Representer;
033
034import java.io.BufferedReader;
035import java.io.Writer;
036import java.math.BigInteger;
037import java.sql.Timestamp;
038import java.util.Date;
039import java.util.Set;
040
041/**
042 * A loader for YAML-formatted configurations, using the SnakeYAML library for
043 * parsing and generation.
044 *
045 * @since 4.0.0
046 */
047public final class YamlConfigurationLoader extends AbstractConfigurationLoader<CommentedConfigurationNode> {
048
049    /**
050     * YAML native types from <a href="https://yaml.org/type/">YAML 1.1 Global tags</a>.
051     *
052     * <p>using SnakeYaml representation: https://bitbucket.org/snakeyaml/snakeyaml/wiki/Documentation#markdown-header-yaml-tags-and-java-types
053     */
054    private static final Set<Class<?>> NATIVE_TYPES = UnmodifiableCollections.toSet(
055            Boolean.class, Integer.class, Long.class, BigInteger.class, Double.class, // numeric
056            byte[].class, String.class, Date.class, java.sql.Date.class, Timestamp.class); // complex types
057
058    /**
059     * Creates a new {@link YamlConfigurationLoader} 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 YamlConfigurationLoader}.
070     *
071     * <p>This builder supports the following options:</p>
072     * <dl>
073     *     <dt>&lt;prefix&gt;.yaml.node-style</dt>
074     *     <dd>Equivalent to {@link #nodeStyle(NodeStyle)}</dd>
075     * </dl>
076     *
077     * @since 4.0.0
078     */
079    public static final class Builder extends AbstractConfigurationLoader.Builder<Builder, YamlConfigurationLoader> {
080        private final DumperOptions options = new DumperOptions();
081        private @Nullable NodeStyle style;
082
083        Builder() {
084            this.indent(4);
085            this.defaultOptions(o -> o.nativeTypes(NATIVE_TYPES));
086            this.from(DEFAULT_OPTIONS_SOURCE);
087        }
088
089        @Override
090        protected void populate(final LoaderOptionSource options) {
091            final @Nullable NodeStyle declared = options.getEnum(NodeStyle.class, "yaml", "node-style");
092            if (declared != null) {
093                this.style = declared;
094            }
095        }
096
097        /**
098         * Sets the level of indentation the resultant loader should use.
099         *
100         * @param indent the indent level
101         * @return this builder (for chaining)
102         * @since 4.0.0
103         */
104        public Builder indent(final int indent) {
105            this.options.setIndent(indent);
106            return this;
107        }
108
109        /**
110         * Gets the level of indentation to be used by the resultant loader.
111         *
112         * @return the indent level
113         * @since 4.0.0
114         */
115        public int indent() {
116            return this.options.getIndent();
117        }
118
119        /**
120         * Sets the node style the built loader should use.
121         *
122         * <dl><dt>Flow</dt>
123         * <dd>the compact, json-like representation.<br>
124         * Example: <code>
125         *     {value: [list, of, elements], another: value}
126         * </code></dd>
127         *
128         * <dt>Block</dt>
129         * <dd>expanded, traditional YAML<br>
130         * Example: <code>
131         *     value:
132         *     - list
133         *     - of
134         *     - elements
135         *     another: value
136         * </code></dd>
137         * </dl>
138         *
139         * <p>A {@code null} value will tell the loader to pick a value
140         * automatically based on the contents of each non-scalar node.</p>
141         *
142         * @param style the node style to use
143         * @return this builder (for chaining)
144         * @since 4.0.0
145         */
146        public Builder nodeStyle(final @Nullable NodeStyle style) {
147            this.style = style;
148            return this;
149        }
150
151        /**
152         * Gets the node style to be used by the resultant loader.
153         *
154         * @return the node style
155         * @since 4.0.0
156         */
157        public @Nullable NodeStyle nodeStyle() {
158            return this.style;
159        }
160
161        @Override
162        public YamlConfigurationLoader build() {
163            return new YamlConfigurationLoader(this);
164        }
165    }
166
167    private final ThreadLocal<Yaml> yaml;
168
169    private YamlConfigurationLoader(final Builder builder) {
170        super(builder, new CommentHandler[] {CommentHandlers.HASH});
171        final LoaderOptions loaderOpts = new LoaderOptions()
172            .setAcceptTabs(true)
173            .setProcessComments(false);
174        loaderOpts.setCodePointLimit(Integer.MAX_VALUE);
175
176        final DumperOptions opts = builder.options;
177        opts.setDefaultFlowStyle(NodeStyle.asSnakeYaml(builder.style));
178        this.yaml = ThreadLocal.withInitial(() -> new Yaml(new Constructor(loaderOpts), new Representer(opts), opts, loaderOpts));
179    }
180
181    @Override
182    protected void loadInternal(final CommentedConfigurationNode node, final BufferedReader reader) {
183        node.raw(this.yaml.get().load(reader));
184    }
185
186    @Override
187    protected void saveInternal(final ConfigurationNode node, final Writer writer) {
188        this.yaml.get().dump(node.raw(), writer);
189    }
190
191    @Override
192    public CommentedConfigurationNode createNode(final ConfigurationOptions options) {
193        return CommentedConfigurationNode.root(options);
194    }
195
196}