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}