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 ninja.leaping.configurate.hocon;
018
019import com.google.common.base.Joiner;
020import com.google.common.collect.ImmutableSet;
021import com.typesafe.config.Config;
022import com.typesafe.config.ConfigFactory;
023import com.typesafe.config.ConfigList;
024import com.typesafe.config.ConfigObject;
025import com.typesafe.config.ConfigOrigin;
026import com.typesafe.config.ConfigOriginFactory;
027import com.typesafe.config.ConfigParseOptions;
028import com.typesafe.config.ConfigRenderOptions;
029import com.typesafe.config.ConfigValue;
030import com.typesafe.config.ConfigValueFactory;
031import ninja.leaping.configurate.ConfigurationNode;
032import ninja.leaping.configurate.ConfigurationOptions;
033import ninja.leaping.configurate.commented.CommentedConfigurationNode;
034import ninja.leaping.configurate.loader.AbstractConfigurationLoader;
035import ninja.leaping.configurate.loader.CommentHandler;
036import ninja.leaping.configurate.loader.CommentHandlers;
037import org.checkerframework.checker.nullness.qual.NonNull;
038
039import java.io.BufferedReader;
040import java.io.IOException;
041import java.io.Writer;
042import java.lang.reflect.Constructor;
043import java.lang.reflect.InvocationTargetException;
044import java.util.ArrayList;
045import java.util.Collections;
046import java.util.List;
047import java.util.Map;
048import java.util.regex.Pattern;
049
050/**
051 * A loader for HOCON (Hodor)-formatted configurations, using the typesafe config library for
052 * parsing and generation.
053 */
054public class HoconConfigurationLoader extends AbstractConfigurationLoader<CommentedConfigurationNode> {
055
056    /**
057     * The pattern used to match newlines.
058     */
059    public static final Pattern CRLF_MATCH = Pattern.compile("\r?");
060
061    /**
062     * The default render options used by configurate.
063     */
064    private static final ConfigRenderOptions DEFAULT_RENDER_OPTIONS = ConfigRenderOptions.defaults()
065            .setOriginComments(false)
066            .setJson(false);
067
068    /**
069     * An instance of {@link ConfigOrigin} for configurate.
070     */
071    private static final ConfigOrigin CONFIGURATE_ORIGIN = ConfigOriginFactory.newSimple("configurate-hocon");
072
073    /**
074     * Gets the default {@link ConfigRenderOptions} used by configurate.
075     *
076     * @return The default render options
077     */
078    public static ConfigRenderOptions defaultRenderOptions() {
079        return DEFAULT_RENDER_OPTIONS;
080    }
081
082    /**
083     * Gets the default {@link ConfigParseOptions} used by configurate.
084     *
085     * @return The default parse options
086     */
087    public static ConfigParseOptions defaultParseOptions() {
088        return ConfigParseOptions.defaults();
089    }
090
091    /**
092     * Creates a new {@link HoconConfigurationLoader} builder.
093     *
094     * @return A new builder
095     */
096    @NonNull
097    public static Builder builder() {
098        return new Builder();
099    }
100
101    /**
102     * Builds a {@link HoconConfigurationLoader}.
103     */
104    public static class Builder extends AbstractConfigurationLoader.Builder<Builder> {
105        private ConfigRenderOptions render = defaultRenderOptions();
106        private ConfigParseOptions parse = defaultParseOptions();
107
108        protected Builder() {
109        }
110
111        /**
112         * Sets the {@link ConfigRenderOptions} the resultant loader should use.
113         *
114         * @param options The render options
115         * @return This builder (for chaining)
116         */
117        @NonNull
118        public Builder setRenderOptions(@NonNull ConfigRenderOptions options) {
119            this.render = options;
120            return this;
121        }
122
123        /**
124         * Gets the {@link ConfigRenderOptions} to be used by the resultant loader.
125         *
126         * @return The render options
127         */
128        @NonNull
129        public ConfigRenderOptions getRenderOptions() {
130            return render;
131        }
132
133        /**
134         * Sets the {@link ConfigParseOptions} the resultant loader should use.
135         *
136         * @param options The parse options
137         * @return This builder (for chaining)
138         */
139        @NonNull
140        public Builder setParseOptions(ConfigParseOptions options) {
141            this.parse = options;
142            return this;
143        }
144
145        /**
146         * Gets the {@link ConfigRenderOptions} to be used by the resultant loader.
147         *
148         * @return The render options
149         */
150        @NonNull
151        public ConfigParseOptions getParseOptions() {
152            return parse;
153        }
154
155        @NonNull
156        @Override
157        public HoconConfigurationLoader build() {
158            return new HoconConfigurationLoader(this);
159        }
160    }
161
162    private final ConfigRenderOptions render;
163    private final ConfigParseOptions parse;
164
165    private HoconConfigurationLoader(Builder build) {
166        super(build, new CommentHandler[] {CommentHandlers.HASH, CommentHandlers.DOUBLE_SLASH});
167        this.render = build.getRenderOptions();
168        this.parse = build.getParseOptions();
169    }
170
171    @Override
172    public void loadInternal(CommentedConfigurationNode node, BufferedReader reader) throws IOException {
173        Config hoconConfig = ConfigFactory.parseReader(reader, parse);
174        hoconConfig = hoconConfig.resolve();
175        for (Map.Entry<String, ConfigValue> ent : hoconConfig.root().entrySet()) {
176            readConfigValue(ent.getValue(), node.getNode(ent.getKey()));
177        }
178    }
179
180    private static void readConfigValue(ConfigValue value, CommentedConfigurationNode node) {
181        if (!value.origin().comments().isEmpty()) {
182            node.setComment(CRLF_MATCH.matcher(Joiner.on('\n').join(value.origin().comments())).replaceAll(""));
183        }
184        switch (value.valueType()) {
185            case OBJECT:
186                if (((ConfigObject) value).isEmpty()) {
187                    node.setValue(Collections.emptyMap());
188                } else {
189                    for (Map.Entry<String, ConfigValue> ent : ((ConfigObject) value).entrySet()) {
190                        readConfigValue(ent.getValue(), node.getNode(ent.getKey()));
191                    }
192                }
193                break;
194            case LIST:
195                List<ConfigValue> values = (ConfigList) value;
196                if (values.isEmpty()) {
197                    node.setValue(Collections.emptyList());
198                } else {
199                    for (int i = 0; i < values.size(); ++i) {
200                        readConfigValue(values.get(i), node.getNode(i));
201                    }
202                }
203                break;
204            case NULL:
205                return;
206            default:
207                node.setValue(value.unwrapped());
208        }
209    }
210
211    @Override
212    protected void saveInternal(ConfigurationNode node, Writer writer) throws IOException {
213        if (!node.isMap()) {
214            if (node.getValue() == null) {
215                writer.write(SYSTEM_LINE_SEPARATOR);
216                return;
217            } else {
218                throw new IOException("HOCON cannot write nodes not in map format!");
219            }
220        }
221        final ConfigValue value = fromValue(node);
222        final String renderedValue = value.render(render);
223        writer.write(renderedValue);
224    }
225
226    private static ConfigValue fromValue(ConfigurationNode node) {
227        ConfigValue ret;
228        if (node.isMap()) {
229            Map<String, ConfigValue> children = node.getOptions().getMapFactory().create();
230            for (Map.Entry<Object, ? extends ConfigurationNode> ent : node.getChildrenMap().entrySet()) {
231                children.put(String.valueOf(ent.getKey()), fromValue(ent.getValue()));
232            }
233            ret = newConfigObject(children);
234        } else if (node.isList()) {
235            List<ConfigValue> children = new ArrayList<>();
236            for (ConfigurationNode ent : node.getChildrenList()) {
237                children.add(fromValue(ent));
238            }
239            ret = newConfigList(children);
240
241        } else {
242            ret = ConfigValueFactory.fromAnyRef(node.getValue(), CONFIGURATE_ORIGIN.description());
243        }
244        if (node instanceof CommentedConfigurationNode) {
245            CommentedConfigurationNode commentedNode = ((CommentedConfigurationNode) node);
246            final ConfigValue finalRet = ret;
247            ret = commentedNode.getComment().map(comment -> finalRet.withOrigin(finalRet.origin().withComments(LINE_SPLITTER.splitToList(comment)))).orElse(ret);
248        }
249        return ret;
250    }
251
252    static ConfigValue newConfigObject(Map<String, ConfigValue> vals) {
253        try {
254            return CONFIG_OBJECT_CONSTRUCTOR.newInstance(CONFIGURATE_ORIGIN, vals);
255        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
256            throw new RuntimeException(e); // rethrow
257        }
258
259    }
260
261    static ConfigValue newConfigList(List<ConfigValue> vals) {
262        try {
263            return CONFIG_LIST_CONSTRUCTOR.newInstance(CONFIGURATE_ORIGIN, vals);
264        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
265            throw new RuntimeException(e); // rethrow
266        }
267    }
268
269    @NonNull
270    @Override
271    public CommentedConfigurationNode createEmptyNode(@NonNull ConfigurationOptions options) {
272        options = options.withNativeTypes(ImmutableSet.of(Map.class, List.class, Double.class,
273                Long.class, Integer.class, Boolean.class, String.class, Number.class));
274        return CommentedConfigurationNode.root(options);
275    }
276
277    // -- Comment handling -- this might have to be updated as the hocon dep changes (But tests should detect this
278    // breakage
279    private static final Constructor<? extends ConfigValue> CONFIG_OBJECT_CONSTRUCTOR;
280    private static final Constructor<? extends ConfigValue> CONFIG_LIST_CONSTRUCTOR;
281    static {
282        Class<? extends ConfigValue> objectClass, listClass;
283        try {
284            objectClass = Class.forName("com.typesafe.config.impl.SimpleConfigObject").asSubclass(ConfigValue.class);
285            listClass = Class.forName("com.typesafe.config.impl.SimpleConfigList").asSubclass(ConfigValue.class);
286        } catch (ClassNotFoundException e) {
287            throw new ExceptionInInitializerError(e);
288        }
289
290        try {
291            CONFIG_OBJECT_CONSTRUCTOR = objectClass.getDeclaredConstructor(ConfigOrigin.class, Map.class);
292            CONFIG_OBJECT_CONSTRUCTOR.setAccessible(true);
293            CONFIG_LIST_CONSTRUCTOR = listClass.getDeclaredConstructor(ConfigOrigin.class, List.class);
294            CONFIG_LIST_CONSTRUCTOR.setAccessible(true);
295        } catch (NoSuchMethodException e) {
296            throw new ExceptionInInitializerError(e);
297        }
298    }
299}