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 checkCanWrite(final ConfigurationNode node) throws ConfigurateException {
163        if (!node.isMap() && !node.virtual() && node.raw() != null) {
164            throw new ConfigurateException(node, "HOCON can only write nodes that are in map format!");
165        }
166    }
167
168    @Override
169    protected void loadInternal(final CommentedConfigurationNode node, final BufferedReader reader) throws ParsingException {
170        Config hoconConfig;
171        try {
172            hoconConfig = ConfigFactory.parseReader(reader);
173            hoconConfig = hoconConfig.resolve();
174        } catch (final ConfigException ex) {
175            throw new ParsingException(node, ex.origin().lineNumber(), 0, ex.origin().description(), null, ex);
176        }
177
178        for (Map.Entry<String, ConfigValue> ent : hoconConfig.root().entrySet()) {
179            readConfigValue(ent.getValue(), node.node(ent.getKey()));
180        }
181    }
182
183    private static void readConfigValue(final ConfigValue value, final CommentedConfigurationNode node) {
184        if (!value.origin().comments().isEmpty()) {
185            node.comment(value.origin().comments().stream()
186                .map(input -> {
187                    final String lineStripped = input.replace("\r", "");
188                    if (!lineStripped.isEmpty() && lineStripped.charAt(0) == ' ') {
189                        return lineStripped.substring(1);
190                    } else {
191                        return lineStripped;
192                    }
193                })
194                .collect(Collectors.joining("\n")));
195        }
196
197        switch (value.valueType()) {
198            case OBJECT:
199                final ConfigObject object = (ConfigObject) value;
200                if (object.isEmpty()) {
201                    node.raw(Collections.emptyMap());
202                } else {
203                    for (Map.Entry<String, ConfigValue> ent : object.entrySet()) {
204                        readConfigValue(ent.getValue(), node.node(ent.getKey()));
205                    }
206                }
207                break;
208            case LIST:
209                final ConfigList list = (ConfigList) value;
210                if (list.isEmpty()) {
211                    node.raw(Collections.emptyList());
212                } else {
213                    for (int i = 0; i < list.size(); ++i) {
214                        readConfigValue(list.get(i), node.node(i));
215                    }
216                }
217                break;
218            case NULL:
219                return;
220            default:
221                node.raw(value.unwrapped());
222                break;
223        }
224    }
225
226    @Override
227    protected void saveInternal(final ConfigurationNode node, final Writer writer) throws ConfigurateException {
228        try {
229            if (!node.isMap() && (node.virtual() || node.raw() == null)) {
230                writer.write(SYSTEM_LINE_SEPARATOR);
231                return;
232            }
233            final ConfigValue value = fromValue(node);
234            final String renderedValue = value.render(this.render);
235            writer.write(renderedValue);
236        } catch (final IOException io) {
237            throw new ConfigurateException(node, io);
238        }
239    }
240
241    private static ConfigValue fromValue(final ConfigurationNode node) {
242        ConfigValue ret;
243        if (node.isMap()) {
244            final Map<String, ConfigValue> children = node.options().mapFactory().create();
245            for (Map.Entry<Object, ? extends ConfigurationNode> ent : node.childrenMap().entrySet()) {
246                children.put(String.valueOf(ent.getKey()), fromValue(ent.getValue()));
247            }
248            ret = newConfigObject(children);
249        } else if (node.isList()) {
250            final List<ConfigValue> children = new ArrayList<>();
251            for (ConfigurationNode ent : node.childrenList()) {
252                children.add(fromValue(ent));
253            }
254            ret = newConfigList(children);
255
256        } else {
257            ret = ConfigValueFactory.fromAnyRef(node.rawScalar(), CONFIGURATE_ORIGIN.description());
258        }
259        if (node instanceof CommentedConfigurationNodeIntermediary<?>) {
260            final CommentedConfigurationNodeIntermediary<?> commentedNode = (CommentedConfigurationNodeIntermediary<?>) node;
261            final @Nullable String origComment = commentedNode.comment();
262            if (origComment != null) {
263                ret = ret.withOrigin(ret.origin().withComments(Arrays.asList(CONFIGURATE_LINE_PATTERN.split(origComment))));
264            }
265        }
266        return ret;
267    }
268
269    static ConfigValue newConfigObject(final Map<String, ConfigValue> vals) {
270        try {
271            return CONFIG_OBJECT_CONSTRUCTOR.newInstance(CONFIGURATE_ORIGIN, vals);
272        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
273            throw new RuntimeException(e); // rethrow
274        }
275
276    }
277
278    static ConfigValue newConfigList(final List<ConfigValue> vals) {
279        try {
280            return CONFIG_LIST_CONSTRUCTOR.newInstance(CONFIGURATE_ORIGIN, vals);
281        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
282            throw new RuntimeException(e); // rethrow
283        }
284    }
285
286    @Override
287    public CommentedConfigurationNode createNode(final ConfigurationOptions options) {
288        return CommentedConfigurationNode.root(options.nativeTypes(NATIVE_TYPES));
289    }
290
291    // -- Comment handling -- this might have to be updated as the hocon dep changes
292    // (But tests should detect this breakage)
293    private static final Constructor<? extends ConfigValue> CONFIG_OBJECT_CONSTRUCTOR;
294    private static final Constructor<? extends ConfigValue> CONFIG_LIST_CONSTRUCTOR;
295
296    static {
297        final Class<? extends ConfigValue> objectClass;
298        final Class<? extends ConfigValue> listClass;
299        try {
300            objectClass = Class.forName("com.typesafe.config.impl.SimpleConfigObject").asSubclass(ConfigValue.class);
301            listClass = Class.forName("com.typesafe.config.impl.SimpleConfigList").asSubclass(ConfigValue.class);
302        } catch (final ClassNotFoundException e) {
303            throw new ExceptionInInitializerError(e);
304        }
305
306        try {
307            CONFIG_OBJECT_CONSTRUCTOR = objectClass.getDeclaredConstructor(ConfigOrigin.class, Map.class);
308            CONFIG_OBJECT_CONSTRUCTOR.setAccessible(true);
309            CONFIG_LIST_CONSTRUCTOR = listClass.getDeclaredConstructor(ConfigOrigin.class, List.class);
310            CONFIG_LIST_CONSTRUCTOR.setAccessible(true);
311        } catch (final NoSuchMethodException e) {
312            throw new ExceptionInInitializerError(e);
313        }
314    }
315
316}