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.hocon;
018
019import com.typesafe.config.Config;
020import com.typesafe.config.ConfigException;
021import com.typesafe.config.ConfigFactory;
022import com.typesafe.config.ConfigList;
023import com.typesafe.config.ConfigObject;
024import com.typesafe.config.ConfigOrigin;
025import com.typesafe.config.ConfigOriginFactory;
026import com.typesafe.config.ConfigRenderOptions;
027import com.typesafe.config.ConfigValue;
028import com.typesafe.config.ConfigValueFactory;
029import org.checkerframework.checker.nullness.qual.Nullable;
030import org.spongepowered.configurate.CommentedConfigurationNode;
031import org.spongepowered.configurate.CommentedConfigurationNodeIntermediary;
032import org.spongepowered.configurate.ConfigurateException;
033import org.spongepowered.configurate.ConfigurationNode;
034import org.spongepowered.configurate.ConfigurationOptions;
035import org.spongepowered.configurate.loader.AbstractConfigurationLoader;
036import org.spongepowered.configurate.loader.CommentHandler;
037import org.spongepowered.configurate.loader.CommentHandlers;
038import org.spongepowered.configurate.loader.ParsingException;
039import org.spongepowered.configurate.util.UnmodifiableCollections;
040
041import java.io.BufferedReader;
042import java.io.IOException;
043import java.io.Writer;
044import java.lang.reflect.Constructor;
045import java.lang.reflect.InvocationTargetException;
046import java.util.ArrayList;
047import java.util.Arrays;
048import java.util.Collections;
049import java.util.List;
050import java.util.Map;
051import java.util.Set;
052import java.util.stream.Collectors;
053
054/**
055 * A loader for HOCON (Hodor)-formatted configurations, using the
056 * <a href="https://github.com/lightbend/config">lightbend config</a> library
057 * for parsing and generation.
058 *
059 * @since 4.0.0
060 */
061public final class HoconConfigurationLoader extends AbstractConfigurationLoader<CommentedConfigurationNode> {
062
063    private static final Set<Class<?>> NATIVE_TYPES = UnmodifiableCollections.toSet(
064            Double.class, Long.class, Integer.class, Boolean.class, String.class, Number.class);
065
066    /**
067     * The default render options used by configurate.
068     */
069    private static final ConfigRenderOptions DEFAULT_RENDER_OPTIONS = ConfigRenderOptions.defaults()
070            .setOriginComments(false)
071            .setJson(false);
072
073    /**
074     * An instance of {@link ConfigOrigin} for configurate.
075     */
076    private static final ConfigOrigin CONFIGURATE_ORIGIN = ConfigOriginFactory.newSimple("configurate-hocon");
077
078    /**
079     * Creates a new {@link HoconConfigurationLoader} builder.
080     *
081     * @return a new builder
082     * @since 4.0.0
083     */
084    public static Builder builder() {
085        return new Builder();
086    }
087
088    /**
089     * Builds a {@link HoconConfigurationLoader}.
090     *
091     * @since 4.0.0
092     */
093    public static final class Builder extends AbstractConfigurationLoader.Builder<Builder, HoconConfigurationLoader> {
094        private ConfigRenderOptions render = DEFAULT_RENDER_OPTIONS;
095
096        Builder() {
097        }
098
099        /**
100         * Set whether output from this loader will be pretty-printed or not.
101         *
102         * <p>Output will always print with a fixed indent of 4 spaces per
103         * level. This is a limitation of the underlying library, so it may
104         * become customizable at some point in the future.</p>
105         *
106         * @param prettyPrinting whether to pretty-print
107         * @return this builder
108         * @since 4.0.0
109         */
110        public Builder prettyPrinting(final boolean prettyPrinting) {
111            this.render = this.render.setFormatted(prettyPrinting);
112            return this;
113        }
114
115        /**
116         * Set whether comments should be emitted.
117         *
118         * <p>Comments will always be loaded from files and
119         * stored in memory.</p>
120         *
121         * @param emitComments whether to emit comments
122         * @return this builder
123         * @since 4.0.0
124         */
125        public Builder emitComments(final boolean emitComments) {
126            this.render = this.render.setComments(emitComments);
127            return this;
128        }
129
130        /**
131         * Set whether output generated by this loader should be
132         * json-compatible.
133         *
134         * <p>Whatever format input is received in, this will output
135         * JSON. To be fully spec-compliant, comment output must also
136         * be disabled.</p>
137         *
138         * @param jsonCompatible to emit json-format output
139         * @return this builder
140         * @since 4.0.0
141         */
142        public Builder emitJsonCompatible(final boolean jsonCompatible) {
143            this.render = this.render.setJson(jsonCompatible);
144            return this;
145        }
146
147        @Override
148        public HoconConfigurationLoader build() {
149            defaultOptions(o -> o.nativeTypes(NATIVE_TYPES));
150            return new HoconConfigurationLoader(this);
151        }
152    }
153
154    private final ConfigRenderOptions render;
155
156    private HoconConfigurationLoader(final Builder build) {
157        super(build, new CommentHandler[] {CommentHandlers.HASH, CommentHandlers.DOUBLE_SLASH});
158        this.render = build.render;
159    }
160
161    @Override
162    protected void loadInternal(final CommentedConfigurationNode node, final BufferedReader reader) throws ParsingException {
163        Config hoconConfig;
164        try {
165            hoconConfig = ConfigFactory.parseReader(reader);
166            hoconConfig = hoconConfig.resolve();
167        } catch (final ConfigException ex) {
168            throw new ParsingException(node, ex.origin().lineNumber(), 0, ex.origin().description(), null, ex);
169        }
170
171        for (Map.Entry<String, ConfigValue> ent : hoconConfig.root().entrySet()) {
172            readConfigValue(ent.getValue(), node.node(ent.getKey()));
173        }
174    }
175
176    private static void readConfigValue(final ConfigValue value, final CommentedConfigurationNode node) {
177        if (!value.origin().comments().isEmpty()) {
178            node.comment(value.origin().comments().stream()
179                    .map(input -> input.replace("\r", ""))
180                    .collect(Collectors.joining("\n")));
181        }
182
183        switch (value.valueType()) {
184            case OBJECT:
185                final ConfigObject object = (ConfigObject) value;
186                if (object.isEmpty()) {
187                    node.raw(Collections.emptyMap());
188                } else {
189                    for (Map.Entry<String, ConfigValue> ent : object.entrySet()) {
190                        readConfigValue(ent.getValue(), node.node(ent.getKey()));
191                    }
192                }
193                break;
194            case LIST:
195                final ConfigList list = (ConfigList) value;
196                if (list.isEmpty()) {
197                    node.raw(Collections.emptyList());
198                } else {
199                    for (int i = 0; i < list.size(); ++i) {
200                        readConfigValue(list.get(i), node.node(i));
201                    }
202                }
203                break;
204            case NULL:
205                return;
206            default:
207                node.raw(value.unwrapped());
208                break;
209        }
210    }
211
212    @Override
213    protected void saveInternal(final ConfigurationNode node, final Writer writer) throws ConfigurateException {
214        try {
215            if (!node.isMap()) {
216                if (node.virtual() || node.raw() == null) {
217                    writer.write(SYSTEM_LINE_SEPARATOR);
218                    return;
219                } else {
220                    throw new ConfigurateException(node, "HOCON can only write nodes that are in map format!");
221                }
222            }
223            final ConfigValue value = fromValue(node);
224            final String renderedValue = value.render(this.render);
225            writer.write(renderedValue);
226        } catch (final IOException io) {
227            throw new ConfigurateException(node, io);
228        }
229    }
230
231    private static ConfigValue fromValue(final ConfigurationNode node) {
232        ConfigValue ret;
233        if (node.isMap()) {
234            final Map<String, ConfigValue> children = node.options().mapFactory().create();
235            for (Map.Entry<Object, ? extends ConfigurationNode> ent : node.childrenMap().entrySet()) {
236                children.put(String.valueOf(ent.getKey()), fromValue(ent.getValue()));
237            }
238            ret = newConfigObject(children);
239        } else if (node.isList()) {
240            final List<ConfigValue> children = new ArrayList<>();
241            for (ConfigurationNode ent : node.childrenList()) {
242                children.add(fromValue(ent));
243            }
244            ret = newConfigList(children);
245
246        } else {
247            ret = ConfigValueFactory.fromAnyRef(node.rawScalar(), CONFIGURATE_ORIGIN.description());
248        }
249        if (node instanceof CommentedConfigurationNodeIntermediary<?>) {
250            final CommentedConfigurationNodeIntermediary<?> commentedNode = (CommentedConfigurationNodeIntermediary<?>) node;
251            final @Nullable String origComment = commentedNode.comment();
252            if (origComment != null) {
253                ret = ret.withOrigin(ret.origin().withComments(Arrays.asList(CONFIGURATE_LINE_PATTERN.split(origComment))));
254            }
255        }
256        return ret;
257    }
258
259    static ConfigValue newConfigObject(final Map<String, ConfigValue> vals) {
260        try {
261            return CONFIG_OBJECT_CONSTRUCTOR.newInstance(CONFIGURATE_ORIGIN, vals);
262        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
263            throw new RuntimeException(e); // rethrow
264        }
265
266    }
267
268    static ConfigValue newConfigList(final List<ConfigValue> vals) {
269        try {
270            return CONFIG_LIST_CONSTRUCTOR.newInstance(CONFIGURATE_ORIGIN, vals);
271        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
272            throw new RuntimeException(e); // rethrow
273        }
274    }
275
276    @Override
277    public CommentedConfigurationNode createNode(final ConfigurationOptions options) {
278        return CommentedConfigurationNode.root(options.nativeTypes(NATIVE_TYPES));
279    }
280
281    // -- Comment handling -- this might have to be updated as the hocon dep changes
282    // (But tests should detect this breakage)
283    private static final Constructor<? extends ConfigValue> CONFIG_OBJECT_CONSTRUCTOR;
284    private static final Constructor<? extends ConfigValue> CONFIG_LIST_CONSTRUCTOR;
285
286    static {
287        final Class<? extends ConfigValue> objectClass;
288        final Class<? extends ConfigValue> listClass;
289        try {
290            objectClass = Class.forName("com.typesafe.config.impl.SimpleConfigObject").asSubclass(ConfigValue.class);
291            listClass = Class.forName("com.typesafe.config.impl.SimpleConfigList").asSubclass(ConfigValue.class);
292        } catch (final ClassNotFoundException e) {
293            throw new ExceptionInInitializerError(e);
294        }
295
296        try {
297            CONFIG_OBJECT_CONSTRUCTOR = objectClass.getDeclaredConstructor(ConfigOrigin.class, Map.class);
298            CONFIG_OBJECT_CONSTRUCTOR.setAccessible(true);
299            CONFIG_LIST_CONSTRUCTOR = listClass.getDeclaredConstructor(ConfigOrigin.class, List.class);
300            CONFIG_LIST_CONSTRUCTOR.setAccessible(true);
301        } catch (final NoSuchMethodException e) {
302            throw new ExceptionInInitializerError(e);
303        }
304    }
305
306}