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