package org.wildfly.swarm.container.runtime; import java.lang.invoke.LambdaMetafactory; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.stream.Collectors; import org.jboss.logging.Logger; import org.wildfly.swarm.bootstrap.performance.Performance; import org.wildfly.swarm.config.runtime.Keyed; import org.wildfly.swarm.config.runtime.SubresourceInfo; import org.wildfly.swarm.internal.SwarmConfigMessages; import org.wildfly.swarm.spi.api.Defaultable; import org.wildfly.swarm.spi.api.Fraction; import org.wildfly.swarm.spi.api.annotations.Configurable; import org.wildfly.swarm.spi.api.annotations.ConfigurableAlias; import org.wildfly.swarm.spi.api.config.ConfigKey; import org.wildfly.swarm.spi.api.config.ConfigView; import org.wildfly.swarm.spi.api.config.Converter; import org.wildfly.swarm.spi.api.config.Resolver; import org.wildfly.swarm.spi.api.config.SimpleKey; /** * @author Bob McWhirter */ public class ConfigurableManager implements AutoCloseable { private static final String SUBRESOURCES = "subresources"; private static final String ACCEPT = "accept"; private static final Set<String> BLACKLISTED_FIELDS = new HashSet<String>() {{ add("pcs"); add("key"); add(SUBRESOURCES); }}; private static final Set<Class<?>> BLACKLISTED_CLASSES = new HashSet<Class<?>>() {{ add(List.class); add(Map.class); add(Properties.class); }}; private static final Set<Class<?>> CONFIGURABLE_VALUE_TYPES = new HashSet<Class<?>>() {{ add(Boolean.class); add(Boolean.TYPE); add(Short.class); add(Short.TYPE); add(Integer.class); add(Integer.TYPE); add(Long.class); add(Long.TYPE); add(Float.class); add(Float.TYPE); add(String.class); add(List.class); add(Map.class); add(Properties.class); add(Defaultable.class); }}; private static Logger LOG = Logger.getLogger("org.wildfly.swarm.config"); private final List<ConfigurableHandle> configurables = new ArrayList<>(); private final List<Object> deferred = new ArrayList<>(); private final ConfigView configView; public ConfigurableManager(ConfigView configView) { this.configView = configView; } public ConfigView configView() { return this.configView; } public List<ConfigurableHandle> configurables() { return this.configurables; } @SuppressWarnings("unchecked") protected <T> void configure(ConfigurableHandle configurable) throws Exception { try (AutoCloseable handle = Performance.accumulate("ConfigurableManager#configure")) { Resolver<?> resolver = this.configView.resolve(configurable.key()); Class<?> resolvedType = configurable.type(); boolean isList = false; boolean isMap = false; boolean isProperties = false; if (resolvedType.isEnum()) { resolver = resolver.as((Class<Enum>) resolvedType, converter((Class<Enum>) resolvedType)); } else if (List.class.isAssignableFrom(resolvedType)) { isList = true; resolver = listResolver((Resolver<String>) resolver, configurable.key()); } else if (Map.class.isAssignableFrom(resolvedType)) { isMap = true; resolver = mapResolver((Resolver<String>) resolver, configurable.key()); } else if (Properties.class.isAssignableFrom(resolvedType)) { isProperties = true; resolver = propertiesResolver((Resolver<String>) resolver, configurable.key()); } else { resolver = resolver.as(resolvedType); } if (isList || isMap || isProperties || resolver.hasValue()) { Object resolvedValue = resolver.getValue(); if (isList && ((List) resolvedValue).isEmpty()) { // ignore } else if (isMap && ((Map) resolvedValue).isEmpty()) { // ignore } else if (isProperties && ((Properties) resolvedValue).isEmpty()) { // also ignore } else { configurable.set(resolvedType.cast(resolvedValue)); } } } } private <ENUMTYPE extends Enum<ENUMTYPE>> Converter<ENUMTYPE> converter(Class<ENUMTYPE> enumType) { return (str) -> { try { return Enum.valueOf(enumType, str.toUpperCase().replace('-', '_')); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Invalid value '" + str + "'; should be one of: " + String.join(",", Arrays.stream(enumType.getEnumConstants()).map((constant) -> constant.toString()).collect(Collectors.toList()))); } }; } private Resolver<List> listResolver(Resolver<String> resolver, ConfigKey key) { return resolver.withDefault("").as(List.class, listConverter(key)); } private Converter<List> listConverter(ConfigKey key) { return (ignored) -> { return this.configView.simpleSubkeys(key).stream() .map((subKey) -> { return this.configView.resolve(key.append(subKey)).getValue(); }) .collect(Collectors.toList()); }; } private Resolver<Map> mapResolver(Resolver<String> resolver, ConfigKey key) { return resolver.withDefault("").as(Map.class, mapConverter(key)); } private Converter<Map> mapConverter(ConfigKey key) { return (ignored) -> { Map<String,Object> map = new HashMap<>(); Set<SimpleKey> subKeys = this.configView.simpleSubkeys(key); for (SimpleKey subKey : subKeys) { map.put(subKey.name(), this.configView.resolve(key.append(subKey)).getValue()); } return map; }; } private Resolver<Properties> propertiesResolver(Resolver<String> resolver, ConfigKey key) { return resolver.withDefault("").as(Properties.class, propertiesConverter(key)); } private Converter<Properties> propertiesConverter(ConfigKey key) { return (ignored) -> { Properties props = new Properties(); Set<SimpleKey> subKeys = this.configView.simpleSubkeys(key); for (SimpleKey subKey : subKeys) { props.setProperty(subKey.name(), this.configView.resolve(key.append(subKey)).getValue()); } return props; }; } public void rescan() throws Exception { for (Object each : this.deferred) { scanInternal(each); } } public void scan(Object instance) throws Exception { try (AutoCloseable handle = Performance.accumulate("ConfigurableManager#scan")) { this.deferred.add(instance); scanInternal(instance); } } private void scanInternal(Object instance) throws Exception { if (instance instanceof Fraction) { scanFraction((Fraction) instance); } else { scan(null, instance, false); } } protected void scanFraction(Fraction fraction) throws Exception { ConfigKey prefix = nameFor(fraction); scan(prefix, fraction, true); } protected SimpleKey getKey(Object object) throws Exception { if (object instanceof Keyed) { return new SimpleKey(((Keyed) object).getKey()); } Method getKey = findGetKeyMethod(object); if (getKey != null) { Object key = getKey.invoke(object); if (key != null) { return new SimpleKey(key.toString()); } } return null; } protected Method findGetKeyMethod(Object object) { Method[] methods = object.getClass().getMethods(); for (Method method : methods) { if (!Modifier.isPublic(method.getModifiers())) { continue; } if (Modifier.isStatic(method.getModifiers())) { continue; } if (!method.getName().equals("getKey")) { continue; } if (method.getParameterCount() != 0) { continue; } return method; } return null; } protected ConfigKey nameFor(Fraction fraction) throws Exception { Configurable anno = fraction.getClass().getAnnotation(Configurable.class); if (anno != null) { return ConfigKey.parse(anno.value()); } SimpleKey key = getKey(fraction); if (key == null) { key = new SimpleKey(fraction.getClass().getSimpleName().replace("Fraction", "").toLowerCase()); } return ConfigKey.of("swarm").append(key); } protected void scan(ConfigKey prefix, Object instance, boolean isFraction) throws Exception { scan(prefix, instance, instance.getClass(), isFraction); if (isFraction) { scanSubresources(prefix, instance); } } protected void scan(ConfigKey prefix, Object instance, Class<?> curClass, boolean isFraction) throws Exception { if (curClass == null || curClass == Object.class || isBlacklisted(curClass)) { return; } Field[] fields = curClass.getDeclaredFields(); for (Field field : fields) { if (!Modifier.isStatic(field.getModifiers())) { if (isBlacklisted(field)) { continue; } if (isFraction || field.getAnnotation(Configurable.class) != null) { if (isConfigurableType(field.getType())) { ConfigKey name = nameFor(prefix, field); if (!seen(name)) { ConfigurableHandle configurable = new ObjectBackedConfigurableHandle(name, instance, field); this.configurables.add(configurable); configure(configurable); } // Process @ConfigurableAlias if (field.getAnnotation(ConfigurableAlias.class) != null) { name = nameForAlias(prefix, field); if (!seen(name)) { ConfigurableHandle configurable = new ObjectBackedConfigurableHandle(name, instance, field); this.configurables.add(configurable); configure(configurable); } } } } } } scan(prefix, instance, curClass.getSuperclass(), isFraction); } private boolean seen(ConfigKey name) { return this.configurables.stream().anyMatch(e -> e.key().equals(name)); } private boolean isConfigurableType(Class<?> type) { return type.isEnum() || CONFIGURABLE_VALUE_TYPES.contains(type); } private boolean isBlacklisted(Class<?> cls) { return BLACKLISTED_CLASSES.stream().anyMatch((e) -> { if (e.isInterface()) { for (Class<?> each : cls.getInterfaces()) { if (each == e) { return true; } } return false; } else { return e == cls; } }); } private boolean isBlacklisted(Field field) { if (BLACKLISTED_FIELDS.stream().anyMatch((e) -> e.equals(field.getName()))) { return true; } return isBlacklisted(field.getType()); } protected ConfigKey nameFor(ConfigKey prefix, Field field) { Configurable anno = field.getAnnotation(Configurable.class); if (anno != null) { if (!anno.value().equals("")) { return ConfigKey.parse(anno.value()); } if (!anno.simpleName().equals("")) { return prefix.append(ConfigKey.parse(anno.simpleName())); } } return prefix.append(nameFor(field)); } protected ConfigKey nameForAlias(ConfigKey prefix, Field field) { ConfigurableAlias annoAlias = field.getAnnotation(ConfigurableAlias.class); if (annoAlias != null) { if (!annoAlias.value().equals("")) { return ConfigKey.parse(annoAlias.value()); } } return prefix.append(nameFor(field)); } protected ConfigKey nameFor(Field field) { StringBuilder str = new StringBuilder(); char[] chars = field.getName().toCharArray(); for (char c : chars) { if (Character.isUpperCase(c)) { str.append("-"); } str.append(Character.toLowerCase(c)); } return ConfigKey.of(str.toString()); } protected void scanSubresources(ConfigKey prefix, Object instance) throws Exception { Method method = getSubresourcesMethod(instance); if (method == null) { return; } Object subresources = method.invoke(instance); Field[] fields = subresources.getClass().getDeclaredFields(); for (Field field : fields) { if (field.getAnnotation(SubresourceInfo.class) == null && List.class.isAssignableFrom(field.getType())) { continue; } field.setAccessible(true); Object value = field.get(subresources); ConfigKey subPrefix = prefix.append(nameFor(field)); if (value != null && value instanceof List) { int index = 0; Set<SimpleKey> seenKeys = new HashSet<>(); for (Object each : ((List) value)) { SimpleKey key = getKey(each); ConfigKey itemPrefix = null; if (key != null) { seenKeys.add(key); itemPrefix = subPrefix.append(key); } else { itemPrefix = subPrefix.append("" + index); } scan(itemPrefix, each, true); ++index; } Set<SimpleKey> keysWithConfiguration = this.configView.simpleSubkeys(subPrefix); keysWithConfiguration.removeAll(seenKeys); if (!keysWithConfiguration.isEmpty()) { Method factoryMethod = getKeyedFactoryMethod(instance, field); if (factoryMethod != null) { for (SimpleKey key : keysWithConfiguration) { ConfigKey itemPrefix = subPrefix.append(key); Object lambda = createLambda(itemPrefix, factoryMethod); if (lambda != null) { factoryMethod.invoke(instance, key.name(), lambda); } } } } } else { // Singleton resources, without key if (value == null) { // If doesn't exist, only create it if there's some // configuration keys that imply we want it. if (this.configView.hasKeyOrSubkeys(subPrefix)) { Method factoryMethod = getNonKeyedFactoryMethod(instance, field); if (factoryMethod != null) { Object lambda = createLambda(subPrefix, factoryMethod); if (lambda != null) { factoryMethod.invoke(instance, lambda); } } } } else { scan(subPrefix, value, true); } } } } protected Object createLambda(ConfigKey itemPrefix, Method factoryMethod) { MethodHandles.Lookup lookup = MethodHandles.lookup(); // The consumer is the last parameter Class<?> consumerType = factoryMethod.getParameterTypes()[factoryMethod.getParameterCount() - 1]; try { Method acceptMethod = null; for (Method method : consumerType.getMethods()) { if (method.getName().equals(ACCEPT)) { acceptMethod = method; } } if (acceptMethod == null) { return null; } MethodHandle target = lookup.findVirtual(ConfigurableManager.class, "subresourceAdded", MethodType.methodType(void.class, ConfigKey.class, Object.class)); MethodType samType = MethodType.methodType(void.class, acceptMethod.getParameterTypes()[0]); MethodHandle mh = LambdaMetafactory.metafactory( lookup, ACCEPT, MethodType.methodType(consumerType, ConfigurableManager.class, ConfigKey.class), samType, target, samType) .getTarget(); return mh.invoke(this, itemPrefix); } catch (Throwable t) { throw new RuntimeException(t); } } public void subresourceAdded(ConfigKey itemPrefix, Object object) throws Exception { scan(itemPrefix, object, true); } protected Method getKeyedFactoryMethod(Object instance, Field field) { SubresourceInfo anno = field.getAnnotation(SubresourceInfo.class); if (anno != null) { String name = anno.value(); Method[] methods = instance.getClass().getMethods(); for (Method method : methods) { if (!method.getName().equals(name)) { continue; } if (!Modifier.isPublic(method.getModifiers())) { continue; } if (Modifier.isStatic(method.getModifiers())) { continue; } if (method.getParameterCount() != 2) { continue; } if (method.getParameterTypes()[0] != String.class) { continue; } if (method.getParameterTypes()[1].getAnnotation(FunctionalInterface.class) == null) { continue; } boolean acceptMethodFound = false; for (Method paramMethod : method.getParameterTypes()[1].getMethods()) { if (paramMethod.getName().equals(ACCEPT)) { acceptMethodFound = true; break; } } if (!acceptMethodFound) { continue; } return method; } } return null; } protected Method getNonKeyedFactoryMethod(Object instance, Field field) { String name = field.getName(); Method[] methods = instance.getClass().getMethods(); for (Method method : methods) { if (!method.getName().equals(name)) { continue; } if (!Modifier.isPublic(method.getModifiers())) { continue; } if (Modifier.isStatic(method.getModifiers())) { continue; } if (method.getParameterCount() != 1) { continue; } if (method.getParameterTypes()[0].getAnnotation(FunctionalInterface.class) == null) { continue; } boolean acceptMethodFound = false; for (Method paramMethod : method.getParameterTypes()[0].getMethods()) { if (paramMethod.getName().equals(ACCEPT)) { acceptMethodFound = true; break; } } if (!acceptMethodFound) { continue; } return method; } return null; } protected Method getSubresourcesMethod(Object instance) { Method[] methods = instance.getClass().getMethods(); for (Method method : methods) { if (Modifier.isStatic(method.getModifiers())) { continue; } if (!method.getName().equals(SUBRESOURCES)) { continue; } if (method.getParameterCount() != 0) { continue; } return method; } return null; } public void log() { // just for a while boolean verbose = true; int longestKey = 0; for (ConfigurableHandle each : this.configurables) { if (each.key().name().length() > longestKey) { longestKey = each.key().name().length(); } } StringBuilder str = new StringBuilder(); List<ConfigurableHandle> sorted = this.configurables .stream() .sorted((l, r) -> l.key().name().compareTo(r.key().name())) .collect(Collectors.toList()); boolean first = true; for (ConfigurableHandle each : sorted) { try { String name = each.key().name(); Object value = each.currentValue(); if (value != null || verbose) { String printedValue = "(unset)"; if (value != null) { if (name.toLowerCase().contains("password")) { printedValue = "<redacted>"; } else { printedValue = value.toString(); } } if (!first) { str.append("\n"); } str.append(String.format(" %-" + longestKey + "s = %s", name, printedValue)); first = false; } } catch (Exception e) { SwarmConfigMessages.MESSAGES.errorResolvingConfigurableValue(each.key().name(), e); } } SwarmConfigMessages.MESSAGES.configuration(str.toString()); } public void close() { this.configurables.clear(); this.deferred.clear(); } }