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}