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 com.typesafe.config.impl.ConfigNodeComment; 030import net.kyori.option.Option; 031import net.kyori.option.OptionSchema; 032import net.kyori.option.OptionState; 033import org.checkerframework.checker.nullness.qual.Nullable; 034import org.spongepowered.configurate.CommentedConfigurationNode; 035import org.spongepowered.configurate.CommentedConfigurationNodeIntermediary; 036import org.spongepowered.configurate.ConfigurateException; 037import org.spongepowered.configurate.ConfigurationNode; 038import org.spongepowered.configurate.ConfigurationOptions; 039import org.spongepowered.configurate.loader.AbstractConfigurationLoader; 040import org.spongepowered.configurate.loader.CommentHandler; 041import org.spongepowered.configurate.loader.CommentHandlers; 042import org.spongepowered.configurate.loader.ParsingException; 043import org.spongepowered.configurate.util.UnmodifiableCollections; 044 045import java.io.BufferedReader; 046import java.io.IOException; 047import java.io.Writer; 048import java.lang.reflect.Constructor; 049import java.lang.reflect.InvocationTargetException; 050import java.util.ArrayList; 051import java.util.Collections; 052import java.util.List; 053import java.util.Map; 054import java.util.Set; 055import java.util.stream.Collectors; 056 057/** 058 * A loader for HOCON (Hodor)-formatted configurations, using the 059 * <a href="https://github.com/lightbend/config">lightbend config</a> library 060 * for parsing and generation. 061 * 062 * @since 4.0.0 063 */ 064public final class HoconConfigurationLoader extends AbstractConfigurationLoader<CommentedConfigurationNode> { 065 066 private static final Set<Class<?>> NATIVE_TYPES = UnmodifiableCollections.toSet( 067 Double.class, Long.class, Integer.class, Boolean.class, String.class, Number.class); 068 069 /** 070 * The default render options used by configurate. 071 */ 072 private static final ConfigRenderOptions DEFAULT_RENDER_OPTIONS = ConfigRenderOptions.defaults() 073 .setOriginComments(false) 074 .setJson(false); 075 076 /** 077 * An instance of {@link ConfigOrigin} for configurate. 078 */ 079 private static final ConfigOrigin CONFIGURATE_ORIGIN = ConfigOriginFactory.newSimple("configurate-hocon"); 080 081 /** 082 * Creates a new {@link HoconConfigurationLoader} builder. 083 * 084 * @return a new builder 085 * @since 4.0.0 086 */ 087 public static Builder builder() { 088 return new Builder(); 089 } 090 091 /** 092 * Builds a {@link HoconConfigurationLoader}. 093 * 094 * <p>This builder supports the following options:</p> 095 * <ul> 096 * <li>{@link #HEADER_MODE}</li> 097 * <li>{@link #PRETTY_PRINTING}</li> 098 * <li>{@link #INDENT}</li> 099 * <li>{@link #COMMENTS}</li> 100 * <li>{@link #JSON_COMPATIBLE}</li> 101 * </ul> 102 * 103 * @since 4.0.0 104 */ 105 public static final class Builder extends AbstractConfigurationLoader.Builder<Builder, HoconConfigurationLoader> { 106 private static final OptionSchema.Mutable UNSAFE_SCHEMA = OptionSchema.childSchema(AbstractConfigurationLoader.Builder.SCHEMA); 107 108 /** 109 * A schema of options available to configure the Hocon loader. 110 * 111 * @since 4.2.0 112 */ 113 public static final OptionSchema SCHEMA = UNSAFE_SCHEMA.frozenView(); 114 115 /** 116 * Set whether output from this loader will be pretty-printed or not. 117 * 118 * @see #prettyPrinting(boolean) 119 * @since 4.2.0 120 */ 121 public static final Option<Boolean> PRETTY_PRINTING = 122 UNSAFE_SCHEMA.booleanOption("hocon:pretty-printing", DEFAULT_RENDER_OPTIONS.getFormatted()); 123 124 /** 125 * Set the amount of spaces to indent with when {@link #prettyPrinting(boolean)} is on. 126 * 127 * @see #indent(int) 128 * @since 4.2.0 129 */ 130 public static final Option<Integer> INDENT = UNSAFE_SCHEMA.intOption("hocon:indent", DEFAULT_RENDER_OPTIONS.getIndent()); 131 132 /** 133 * Set whether comments will be emitted. 134 * 135 * @see #emitComments(boolean) 136 * @since 4.2.0 137 */ 138 public static final Option<Boolean> COMMENTS = UNSAFE_SCHEMA.booleanOption("hocon:emit-comments", DEFAULT_RENDER_OPTIONS.getComments()); 139 140 /** 141 * Set whether JSON compatible output mode will be used. 142 * 143 * @see #emitJsonCompatible(boolean) 144 * @since 4.2.0 145 */ 146 public static final Option<Boolean> JSON_COMPATIBLE = UNSAFE_SCHEMA.booleanOption("hocon:json-compatible", DEFAULT_RENDER_OPTIONS.getJson()); 147 148 @Override 149 protected OptionSchema optionSchema() { 150 return SCHEMA; 151 } 152 153 /** 154 * Set whether output from this loader will be pretty-printed or not. 155 * 156 * @param prettyPrinting whether to pretty-print 157 * @return this builder 158 * @since 4.0.0 159 */ 160 public Builder prettyPrinting(final boolean prettyPrinting) { 161 this.optionStateBuilder().value(PRETTY_PRINTING, prettyPrinting); 162 return this; 163 } 164 165 /** 166 * Set the amount of spaces to indent with when 167 * {@link #prettyPrinting(boolean)} is on. 168 * 169 * <p>Defaults to 4.</p> 170 * 171 * @param indent indent level 172 * @return this builder 173 * @since 4.2.0 174 */ 175 public Builder indent(final int indent) { 176 this.optionStateBuilder().value(INDENT, indent); 177 return this; 178 } 179 180 /** 181 * Set whether comments should be emitted. 182 * 183 * <p>Comments will always be loaded from files and 184 * stored in memory.</p> 185 * 186 * @param emitComments whether to emit comments 187 * @return this builder 188 * @since 4.0.0 189 */ 190 public Builder emitComments(final boolean emitComments) { 191 this.optionStateBuilder().value(COMMENTS, emitComments); 192 return this; 193 } 194 195 /** 196 * Set whether output generated by this loader should be 197 * json-compatible. 198 * 199 * <p>Whatever format input is received in, this will output 200 * JSON. To be fully spec-compliant, comment output must also 201 * be disabled.</p> 202 * 203 * @param jsonCompatible to emit json-format output 204 * @return this builder 205 * @since 4.0.0 206 */ 207 public Builder emitJsonCompatible(final boolean jsonCompatible) { 208 this.optionStateBuilder().value(JSON_COMPATIBLE, jsonCompatible); 209 return this; 210 } 211 212 ConfigRenderOptions renderOptions() { 213 final OptionState opt = this.optionState(); 214 return DEFAULT_RENDER_OPTIONS 215 .setFormatted(opt.value(PRETTY_PRINTING)) 216 .setIndent(opt.value(INDENT)) 217 .setComments(opt.value(COMMENTS)) 218 .setJson(opt.value(JSON_COMPATIBLE)); 219 } 220 221 @Override 222 public HoconConfigurationLoader build() { 223 defaultOptions(o -> o.nativeTypes(NATIVE_TYPES)); 224 return new HoconConfigurationLoader(this); 225 } 226 } 227 228 private final ConfigRenderOptions render; 229 230 private HoconConfigurationLoader(final Builder build) { 231 super(build, new CommentHandler[] {CommentHandlers.HASH, CommentHandlers.DOUBLE_SLASH}); 232 this.render = build.renderOptions(); 233 } 234 235 @Override 236 protected void checkCanWrite(final ConfigurationNode node) throws ConfigurateException { 237 if (!node.isMap() && !node.virtual() && node.raw() != null) { 238 throw new ConfigurateException(node, "HOCON can only write nodes that are in map format!"); 239 } 240 } 241 242 @Override 243 protected void loadInternal(final CommentedConfigurationNode node, final BufferedReader reader) throws ParsingException { 244 Config hoconConfig; 245 try { 246 hoconConfig = ConfigFactory.parseReader(reader); 247 hoconConfig = hoconConfig.resolve(); 248 } catch (final ConfigException ex) { 249 throw new ParsingException(node, ex.origin().lineNumber(), 0, ex.origin().description(), null, ex); 250 } 251 252 for (Map.Entry<String, ConfigValue> ent : hoconConfig.root().entrySet()) { 253 readConfigValue(ent.getValue(), node.node(ent.getKey())); 254 } 255 } 256 257 private static void readConfigValue(final ConfigValue value, final CommentedConfigurationNode node) { 258 if (!value.origin().comments().isEmpty()) { 259 node.comment(value.origin().comments().stream() 260 .map(input -> { 261 final String lineStripped = input.commentText().replace("\r", ""); 262 if (!lineStripped.isEmpty() && lineStripped.charAt(0) == ' ') { 263 return lineStripped.substring(1); 264 } else { 265 return lineStripped; 266 } 267 }) 268 .collect(Collectors.joining("\n"))); 269 } 270 271 switch (value.valueType()) { 272 case OBJECT: 273 final ConfigObject object = (ConfigObject) value; 274 if (object.isEmpty()) { 275 node.raw(Collections.emptyMap()); 276 } else { 277 for (Map.Entry<String, ConfigValue> ent : object.entrySet()) { 278 readConfigValue(ent.getValue(), node.node(ent.getKey())); 279 } 280 } 281 break; 282 case LIST: 283 final ConfigList list = (ConfigList) value; 284 if (list.isEmpty()) { 285 node.raw(Collections.emptyList()); 286 } else { 287 for (int i = 0; i < list.size(); ++i) { 288 readConfigValue(list.get(i), node.node(i)); 289 } 290 } 291 break; 292 case NULL: 293 return; 294 default: 295 node.raw(value.unwrapped()); 296 break; 297 } 298 } 299 300 @Override 301 protected void saveInternal(final ConfigurationNode node, final Writer writer) throws ConfigurateException { 302 try { 303 if (!node.isMap() && (node.virtual() || node.raw() == null)) { 304 writer.write(SYSTEM_LINE_SEPARATOR); 305 return; 306 } 307 final ConfigValue value = fromValue(node); 308 final String renderedValue = value.render(this.render); 309 writer.write(renderedValue); 310 } catch (final IOException io) { 311 throw new ConfigurateException(node, io); 312 } 313 } 314 315 private static ConfigValue fromValue(final ConfigurationNode node) { 316 ConfigValue ret; 317 if (node.isMap()) { 318 final Map<String, ConfigValue> children = node.options().mapFactory().create(); 319 for (Map.Entry<Object, ? extends ConfigurationNode> ent : node.childrenMap().entrySet()) { 320 children.put(String.valueOf(ent.getKey()), fromValue(ent.getValue())); 321 } 322 ret = newConfigObject(children); 323 } else if (node.isList()) { 324 final List<ConfigValue> children = new ArrayList<>(); 325 for (ConfigurationNode ent : node.childrenList()) { 326 children.add(fromValue(ent)); 327 } 328 ret = newConfigList(children); 329 330 } else { 331 ret = ConfigValueFactory.fromAnyRef(node.rawScalar(), CONFIGURATE_ORIGIN.description()); 332 } 333 if (node instanceof CommentedConfigurationNodeIntermediary<?>) { 334 final CommentedConfigurationNodeIntermediary<?> commentedNode = (CommentedConfigurationNodeIntermediary<?>) node; 335 final @Nullable String origComment = commentedNode.comment(); 336 if (origComment != null) { 337 final List<ConfigNodeComment> nodes = new ArrayList<>(); 338 for (final String line : CONFIGURATE_LINE_PATTERN.split(origComment, -1)) { 339 if (line.length() != 0 && line.charAt(0) == '#') { 340 // allow lines that are only the comment character, for box drawing 341 nodes.add(ConfigNodeComment.hashComment(line)); 342 } else { 343 nodes.add(ConfigNodeComment.hashComment(' ' + line)); 344 } 345 } 346 ret = ret.withOrigin(ret.origin().withComments(nodes)); 347 } 348 } 349 return ret; 350 } 351 352 static ConfigValue newConfigObject(final Map<String, ConfigValue> vals) { 353 try { 354 return CONFIG_OBJECT_CONSTRUCTOR.newInstance(CONFIGURATE_ORIGIN, vals); 355 } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { 356 throw new RuntimeException(e); // rethrow 357 } 358 359 } 360 361 static ConfigValue newConfigList(final List<ConfigValue> vals) { 362 try { 363 return CONFIG_LIST_CONSTRUCTOR.newInstance(CONFIGURATE_ORIGIN, vals); 364 } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { 365 throw new RuntimeException(e); // rethrow 366 } 367 } 368 369 @Override 370 public CommentedConfigurationNode createNode(final ConfigurationOptions options) { 371 return CommentedConfigurationNode.root(options.nativeTypes(NATIVE_TYPES)); 372 } 373 374 // -- Comment handling -- this might have to be updated as the hocon dep changes 375 // (But tests should detect this breakage) 376 private static final Constructor<? extends ConfigValue> CONFIG_OBJECT_CONSTRUCTOR; 377 private static final Constructor<? extends ConfigValue> CONFIG_LIST_CONSTRUCTOR; 378 379 static { 380 final Class<? extends ConfigValue> objectClass; 381 final Class<? extends ConfigValue> listClass; 382 try { 383 objectClass = Class.forName("com.typesafe.config.impl.SimpleConfigObject").asSubclass(ConfigValue.class); 384 listClass = Class.forName("com.typesafe.config.impl.SimpleConfigList").asSubclass(ConfigValue.class); 385 } catch (final ClassNotFoundException e) { 386 throw new ExceptionInInitializerError(e); 387 } 388 389 try { 390 CONFIG_OBJECT_CONSTRUCTOR = objectClass.getDeclaredConstructor(ConfigOrigin.class, Map.class); 391 CONFIG_OBJECT_CONSTRUCTOR.setAccessible(true); 392 CONFIG_LIST_CONSTRUCTOR = listClass.getDeclaredConstructor(ConfigOrigin.class, List.class); 393 CONFIG_LIST_CONSTRUCTOR.setAccessible(true); 394 } catch (final NoSuchMethodException e) { 395 throw new ExceptionInInitializerError(e); 396 } 397 } 398 399}