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}