From 360d66321f24415e054f4453679ab38cae1ecfa5 Mon Sep 17 00:00:00 2001 From: "David M. Lloyd" Date: Mon, 11 May 2020 10:09:30 -0500 Subject: [PATCH] Configuration mapper --- mapper/pom.xml | 48 + .../smallrye/config/mapper/CompareWith.java | 23 + .../smallrye/config/mapper/ConfigMapping.java | 631 +++++++++++ .../config/mapper/ConfigurationInterface.java | 988 ++++++++++++++++++ .../config/mapper/ConfigurationObject.java | 8 + .../ConfigurationValidationException.java | 41 + .../smallrye/config/mapper/ConvertWith.java | 24 + .../io/smallrye/config/mapper/Default.java | 17 + .../mapper/DefaultValuesConfigSource.java | 30 + .../io/smallrye/config/mapper/KeyMap.java | 220 ++++ .../config/mapper/MappingContext.java | 195 ++++ .../io/smallrye/config/mapper/Matching.java | 22 + .../smallrye/config/mapper/MaxExclusive.java | 22 + .../smallrye/config/mapper/MaxInclusive.java | 22 + .../smallrye/config/mapper/MinExclusive.java | 22 + .../smallrye/config/mapper/MinInclusive.java | 22 + .../smallrye/config/mapper/NameIterator.java | 348 ++++++ .../io/smallrye/config/mapper/WithName.java | 22 + .../config/mapper/WithParentName.java | 16 + .../smallrye/config/mapper/MapperTests.java | 50 + pom.xml | 8 + 21 files changed, 2779 insertions(+) create mode 100644 mapper/pom.xml create mode 100644 mapper/src/main/java/io/smallrye/config/mapper/CompareWith.java create mode 100644 mapper/src/main/java/io/smallrye/config/mapper/ConfigMapping.java create mode 100644 mapper/src/main/java/io/smallrye/config/mapper/ConfigurationInterface.java create mode 100644 mapper/src/main/java/io/smallrye/config/mapper/ConfigurationObject.java create mode 100644 mapper/src/main/java/io/smallrye/config/mapper/ConfigurationValidationException.java create mode 100644 mapper/src/main/java/io/smallrye/config/mapper/ConvertWith.java create mode 100644 mapper/src/main/java/io/smallrye/config/mapper/Default.java create mode 100644 mapper/src/main/java/io/smallrye/config/mapper/DefaultValuesConfigSource.java create mode 100644 mapper/src/main/java/io/smallrye/config/mapper/KeyMap.java create mode 100644 mapper/src/main/java/io/smallrye/config/mapper/MappingContext.java create mode 100644 mapper/src/main/java/io/smallrye/config/mapper/Matching.java create mode 100644 mapper/src/main/java/io/smallrye/config/mapper/MaxExclusive.java create mode 100644 mapper/src/main/java/io/smallrye/config/mapper/MaxInclusive.java create mode 100644 mapper/src/main/java/io/smallrye/config/mapper/MinExclusive.java create mode 100644 mapper/src/main/java/io/smallrye/config/mapper/MinInclusive.java create mode 100644 mapper/src/main/java/io/smallrye/config/mapper/NameIterator.java create mode 100644 mapper/src/main/java/io/smallrye/config/mapper/WithName.java create mode 100644 mapper/src/main/java/io/smallrye/config/mapper/WithParentName.java create mode 100644 mapper/src/test/java/io/smallrye/config/mapper/MapperTests.java diff --git a/mapper/pom.xml b/mapper/pom.xml new file mode 100644 index 000000000..ce0e358ab --- /dev/null +++ b/mapper/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + + io.smallrye.config + smallrye-config-parent + 1.8.1-SNAPSHOT + + + smallrye-config-mapper + + SmallRye: Configuration Mapper + A configuration-to-object mapping utility + + + + org.ow2.asm + asm + + + io.smallrye.config + smallrye-config + + + io.smallrye.common + smallrye-common-constraint + + + junit + junit + test + + + + + + + maven-surefire-plugin + + false + + + + + diff --git a/mapper/src/main/java/io/smallrye/config/mapper/CompareWith.java b/mapper/src/main/java/io/smallrye/config/mapper/CompareWith.java new file mode 100644 index 000000000..f9466c0c7 --- /dev/null +++ b/mapper/src/main/java/io/smallrye/config/mapper/CompareWith.java @@ -0,0 +1,23 @@ +package io.smallrye.config.mapper; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Comparator; + +/** + * Specify the comparator to use to compare the annotated type for range. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE_USE) +public @interface CompareWith { + /** + * The comparator class to use. + * + * @return the comparator class + */ + Class> value(); +} diff --git a/mapper/src/main/java/io/smallrye/config/mapper/ConfigMapping.java b/mapper/src/main/java/io/smallrye/config/mapper/ConfigMapping.java new file mode 100644 index 000000000..15354ec1e --- /dev/null +++ b/mapper/src/main/java/io/smallrye/config/mapper/ConfigMapping.java @@ -0,0 +1,631 @@ +package io.smallrye.config.mapper; + +import static io.smallrye.config.mapper.ConfigurationInterface.GroupProperty; +import static io.smallrye.config.mapper.ConfigurationInterface.LeafProperty; +import static io.smallrye.config.mapper.ConfigurationInterface.MapProperty; +import static io.smallrye.config.mapper.ConfigurationInterface.MayBeOptionalProperty; +import static io.smallrye.config.mapper.ConfigurationInterface.PrimitiveProperty; +import static io.smallrye.config.mapper.ConfigurationInterface.Property; +import static io.smallrye.config.mapper.ConfigurationInterface.getConfigurationInterface; + +import java.lang.reflect.Method; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; + +import org.eclipse.microprofile.config.spi.Converter; + +import io.smallrye.common.constraint.Assert; +import io.smallrye.common.function.Functions; +import io.smallrye.config.ConfigValue; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; + +/** + * + */ +public final class ConfigMapping { + /** + * The do-nothing action is used when the matched property is eager. + */ + private static final BiConsumer DO_NOTHING = Functions.discardingBiConsumer(); + + private static final KeyMap> IGNORE_EVERYTHING; + + static { + final KeyMap> map = new KeyMap<>(); + map.putAny(map); + map.putRootValue(DO_NOTHING); + IGNORE_EVERYTHING = map; + } + + private final Map> roots; + private final KeyMap> matchActions; + private final KeyMap defaultValues; + + ConfigMapping(final Builder builder) { + roots = new HashMap<>(builder.roots); + final ArrayDeque currentPath = new ArrayDeque<>(); + KeyMap> matchActions = new KeyMap<>(); + KeyMap defaultValues = new KeyMap<>(); + for (Map.Entry> entry : roots.entrySet()) { + NameIterator rootNi = new NameIterator(entry.getKey()); + while (rootNi.hasNext()) { + currentPath.add(rootNi.getNextSegment()); + rootNi.next(); + } + List roots = entry.getValue(); + for (ConfigurationInterface root : roots) { + // construct the lazy match actions for each group + BiFunction ef = (mc, ni) -> mc + .getRoot(root.getInterfaceType(), entry.getKey()); + processEagerGroup(currentPath, matchActions, defaultValues, root, ef); + } + currentPath.clear(); + } + for (String[] ignoredPath : builder.ignored) { + int len = ignoredPath.length; + KeyMap> found; + if (ignoredPath[len - 1].equals("**")) { + found = matchActions.findOrAdd(ignoredPath, 0, len - 1); + found.putRootValue(DO_NOTHING); + found.putAny(IGNORE_EVERYTHING); + } else { + found = matchActions.findOrAdd(ignoredPath); + found.putRootValue(DO_NOTHING); + } + } + this.matchActions = matchActions; + this.defaultValues = defaultValues; + } + + static String skewer(Method method) { + return skewer(method.getName()); + } + + static String skewer(String camelHumps) { + return skewer(camelHumps, 0, camelHumps.length(), new StringBuilder()); + } + + static String skewer(String camelHumps, int start, int end, StringBuilder b) { + assert !camelHumps.isEmpty() : "Method seems to have an empty name"; + int cp = camelHumps.codePointAt(start); + b.appendCodePoint(Character.toLowerCase(cp)); + start += Character.charCount(cp); + if (start == end) { + // a lonely character at the end of the string + return b.toString(); + } + if (Character.isUpperCase(cp)) { + // all-uppercase words need one code point of lookahead + int nextCp = camelHumps.codePointAt(start); + if (Character.isUpperCase(nextCp)) { + // it's some kind of `WORD` + for (;;) { + b.appendCodePoint(Character.toLowerCase(cp)); + start += Character.charCount(cp); + cp = nextCp; + if (start == end) { + return b.toString(); + } + nextCp = camelHumps.codePointAt(start); + // combine non-letters in with this name + if (Character.isLowerCase(nextCp)) { + b.append('-'); + return skewer(camelHumps, start, end, b); + } + } + // unreachable + } else { + // it was the start of a `Word`; continue until we hit the end or an uppercase. + b.appendCodePoint(nextCp); + start += Character.charCount(nextCp); + for (;;) { + if (start == end) { + return b.toString(); + } + cp = camelHumps.codePointAt(start); + // combine non-letters in with this name + if (Character.isUpperCase(cp)) { + b.append('-'); + return skewer(camelHumps, start, end, b); + } + b.appendCodePoint(cp); + start += Character.charCount(cp); + } + // unreachable + } + // unreachable + } else { + // it's some kind of `word` + for (;;) { + cp = camelHumps.codePointAt(start); + // combine non-letters in with this name + if (Character.isUpperCase(cp)) { + b.append('-'); + return skewer(camelHumps, start, end, b); + } + b.appendCodePoint(cp); + start += Character.charCount(cp); + if (start == end) { + return b.toString(); + } + } + // unreachable + } + // unreachable + } + + static final class ConsumeOneAndThen implements BiConsumer { + private final BiConsumer delegate; + + ConsumeOneAndThen(final BiConsumer delegate) { + this.delegate = delegate; + } + + public void accept(final MappingContext context, final NameIterator nameIterator) { + nameIterator.previous(); + delegate.accept(context, nameIterator); + } + } + + static final class ConsumeOneAndThenFn implements BiFunction { + private final BiFunction delegate; + + ConsumeOneAndThenFn(final BiFunction delegate) { + this.delegate = delegate; + } + + public T apply(final MappingContext context, final NameIterator nameIterator) { + nameIterator.previous(); + return delegate.apply(context, nameIterator); + } + } + + private void processEagerGroup(final ArrayDeque currentPath, + final KeyMap> matchActions, final KeyMap defaultValues, + final ConfigurationInterface group, + final BiFunction getEnclosingFunction) { + Class type = group.getInterfaceType(); + int pc = group.getPropertyCount(); + HashSet usedProperties = new HashSet<>(); + for (int i = 0; i < pc; i++) { + Property property = group.getProperty(i); + String memberName = property.getMethod().getName(); + if (usedProperties.add(memberName)) { + // process by property type + if (property.isOptional()) { + // switch to lazy mode + MayBeOptionalProperty nestedProperty = property.asOptional().getNestedProperty(); + if (nestedProperty.isGroup()) { + GroupProperty nestedGroup = nestedProperty.asGroup(); + BiConsumer matchAction = (mc, ni) -> { + // on match, always create the outermost group, which recursively creates inner groups + getOrCreateEnclosingGroupInGroup(mc, ni, getEnclosingFunction, group, nestedGroup); + }; + if (!nestedGroup.isParentPropertyName()) { + if (nestedGroup.hasPropertyName()) { + currentPath.addLast(nestedGroup.getPropertyName()); + } else { + currentPath.addLast(skewer(property.getMethod().getName())); + } + matchAction = new ConsumeOneAndThen(matchAction); + } + BiFunction ef = (mc, ni) -> { + ConfigurationObject outer = getEnclosingFunction.apply(mc, ni); + // eagerly populated groups will always exist + return (ConfigurationObject) mc.getEnclosedField(type, memberName, outer); + }; + processLazyGroupInGroup(matchActions, nestedGroup, ef, matchAction, new HashSet<>()); + } else if (nestedProperty.isLeaf()) { + LeafProperty leafProperty = nestedProperty.asLeaf(); + if (leafProperty.hasDefaultValue()) { + KeyMap base = defaultValues.findOrAdd(currentPath); + if (!leafProperty.isParentPropertyName()) { + if (leafProperty.hasPropertyName()) { + base = base.findOrAdd(leafProperty.getPropertyName()); + } else { + base = base.findOrAdd(skewer(leafProperty.getMethod())); + } + } + base.putRootValue(leafProperty.getDefaultValue()); + } + KeyMap> actions = matchActions.findOrAdd(currentPath); + if (!leafProperty.isParentPropertyName()) { + if (leafProperty.hasPropertyName()) { + actions = actions.findOrAdd(leafProperty.getPropertyName()); + } else { + actions = actions.findOrAdd(skewer(leafProperty.getMethod())); + } + } + actions.putRootValue(DO_NOTHING); + } + } else if (property.isGroup()) { + processEagerGroup(currentPath, matchActions, defaultValues, property.asGroup().getGroupType(), (mc, + ni) -> getOrCreateEnclosingGroupInGroup(mc, ni, getEnclosingFunction, group, property.asGroup())); + } else if (property.isPrimitive()) { + // already processed eagerly + PrimitiveProperty primitiveProperty = property.asPrimitive(); + if (primitiveProperty.hasDefaultValue()) { + KeyMap base = defaultValues.findOrAdd(currentPath); + if (!primitiveProperty.isParentPropertyName()) { + if (primitiveProperty.hasPropertyName()) { + base = base.findOrAdd(primitiveProperty.getPropertyName()); + } else { + base = base.findOrAdd(skewer(primitiveProperty.getMethod())); + } + } + base.putRootValue(primitiveProperty.getDefaultValue()); + } + KeyMap> actions = matchActions.findOrAdd(currentPath); + if (!primitiveProperty.isParentPropertyName()) { + if (primitiveProperty.hasPropertyName()) { + actions = actions.findOrAdd(primitiveProperty.getPropertyName()); + } else { + actions = actions.findOrAdd(skewer(primitiveProperty.getMethod())); + } + } + actions.putRootValue(DO_NOTHING); + } else if (property.isLeaf()) { + // already processed eagerly + LeafProperty leafProperty = property.asLeaf(); + if (leafProperty.hasDefaultValue()) { + KeyMap base = defaultValues.findOrAdd(currentPath); + if (!leafProperty.isParentPropertyName()) { + if (leafProperty.hasPropertyName()) { + base = base.findOrAdd(leafProperty.getPropertyName()); + } else { + base = base.findOrAdd(skewer(leafProperty.getMethod())); + } + } + base.putRootValue(leafProperty.getDefaultValue()); + } + KeyMap> actions = matchActions.findOrAdd(currentPath); + if (!leafProperty.isParentPropertyName()) { + if (leafProperty.hasPropertyName()) { + actions = actions.findOrAdd(leafProperty.getPropertyName()); + } else { + actions = actions.findOrAdd(skewer(leafProperty.getMethod())); + } + } + actions.putRootValue(DO_NOTHING); + } else if (property.isMap()) { + KeyMap> actualMap; + if (property.hasPropertyName()) { + actualMap = matchActions.findOrAdd(property.getPropertyName()); + } else { + actualMap = matchActions; + } + // the enclosure of the map is this group + processLazyMapInGroup(actualMap, property.asMap(), getEnclosingFunction, group); + } + } + } + int sc = group.getSuperTypeCount(); + for (int i = 0; i < sc; i++) { + processEagerGroup(currentPath, matchActions, defaultValues, group.getSuperType(i), getEnclosingFunction); + } + } + + private void processLazyGroupInGroup(final KeyMap> matchActions, + final GroupProperty groupProperty, + BiFunction getEnclosingFunction, + BiConsumer matchAction, HashSet usedProperties) { + ConfigurationInterface group = groupProperty.getGroupType(); + int pc = group.getPropertyCount(); + for (int i = 0; i < pc; i++) { + Property property = group.getProperty(i); + if (usedProperties.add(property.getMethod().getName())) { + boolean optional = property.isOptional(); + if (property.isGroup() || optional && property.asOptional().getNestedProperty().isGroup()) { + GroupProperty asGroup = optional ? property.asOptional().getNestedProperty().asGroup() : property.asGroup(); + KeyMap> actualMap; + final BiFunction nestedEnclosingFunction = (mc, + ni) -> getOrCreateEnclosingGroupInGroup(mc, ni, getEnclosingFunction, group, asGroup); + BiConsumer nestedMatchAction = optional + ? (mc, ni) -> getOrCreateEnclosingGroupInGroup(mc, ni, getEnclosingFunction, group, asGroup) + : matchAction; + if (property.isParentPropertyName()) { + actualMap = matchActions; + } else { + String propertyName = property.hasPropertyName() ? property.getPropertyName() + : skewer(property.getMethod()); + actualMap = matchActions.findOrAdd(propertyName); + BiConsumer old = nestedMatchAction; + nestedMatchAction = (mc, ni) -> { + ni.previous(); + old.accept(mc, ni); + }; + } + processLazyGroupInGroup(actualMap, asGroup, nestedEnclosingFunction, nestedMatchAction, usedProperties); + } else if (property.isLeaf() || property.isPrimitive() + || optional && property.asOptional().getNestedProperty().isLeaf()) { + KeyMap> keyMap; + BiConsumer actualAction; + if (!property.isParentPropertyName()) { + String propertyName = property.hasPropertyName() ? property.getPropertyName() + : skewer(property.getMethod()); + keyMap = matchActions.findOrAdd(propertyName); + actualAction = new ConsumeOneAndThen(matchAction); + } else { + keyMap = matchActions; + actualAction = matchAction; + } + keyMap.putRootValue(actualAction); + } else if (property.isMap()) { + KeyMap> actualMap; + if (property.isParentPropertyName()) { + actualMap = matchActions; + } else { + if (property.hasPropertyName()) { + actualMap = matchActions.findOrAdd(property.getPropertyName()); + } else { + actualMap = matchActions.findOrAdd(skewer(property.getMethod())); + } + } + processLazyMapInGroup(actualMap, property.asMap(), getEnclosingFunction, group); + } + } + } + int sc = group.getSuperTypeCount(); + for (int i = 0; i < sc; i++) { + processLazyGroupInGroup(matchActions, groupProperty, getEnclosingFunction, matchAction, usedProperties); + } + } + + private void processLazyMapInGroup(final KeyMap> matchActions, + final MapProperty property, BiFunction getEnclosingGroup, + ConfigurationInterface enclosingGroup) { + BiFunction> getEnclosingMap = (mc, ni) -> getOrCreateEnclosingMapInGroup(mc, ni, + getEnclosingGroup, enclosingGroup, property); + processLazyMap(matchActions, property, getEnclosingMap, enclosingGroup); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private void processLazyMap(final KeyMap> matchActions, final MapProperty property, + BiFunction> getEnclosingMap, ConfigurationInterface enclosingGroup) { + Property valueProperty = property.getValueProperty(); + Class> keyConvertWith = property.getKeyConvertWith(); + Class keyRawType = property.getKeyRawType(); + + if (valueProperty.isLeaf()) { + LeafProperty leafProperty = valueProperty.asLeaf(); + Class> valConvertWith = leafProperty.getConvertWith(); + Class valueRawType = leafProperty.getValueRawType(); + + matchActions.getOrCreateAny().putRootValue((mc, ni) -> { + StringBuilder sb = mc.getStringBuilder(); + sb.setLength(0); + ni.previous(); + sb.append(ni.getAllPreviousSegments()); + String configKey = sb.toString(); + Map map = getEnclosingMap.apply(mc, ni); + ni.next(); + String rawMapKey = ni.getPreviousSegment(); + Converter keyConv; + SmallRyeConfig config = mc.getConfig(); + if (keyConvertWith != null) { + keyConv = mc.getConverterInstance(keyConvertWith); + } else { + keyConv = config.getConverter(keyRawType); + } + Object key = keyConv.convert(rawMapKey); + Converter valueConv; + if (valConvertWith != null) { + valueConv = mc.getConverterInstance(valConvertWith); + } else { + valueConv = config.getConverter(valueRawType); + } + ((Map) map).put(key, config.getValue(configKey, valueConv)); + }); + } else if (valueProperty.isMap()) { + processLazyMap(matchActions.getOrCreateAny(), valueProperty.asMap(), (mc, ni) -> { + ni.previous(); + Map enclosingMap = getEnclosingMap.apply(mc, ni); + ni.next(); + String rawMapKey = ni.getPreviousSegment(); + Converter keyConv; + SmallRyeConfig config = mc.getConfig(); + if (keyConvertWith != null) { + keyConv = mc.getConverterInstance(keyConvertWith); + } else { + keyConv = config.getConverter(keyRawType); + } + Object key = keyConv.convert(rawMapKey); + return (Map) ((Map) enclosingMap).computeIfAbsent(key, x -> new HashMap<>()); + }, enclosingGroup); + } else { + assert valueProperty.isGroup(); + final BiFunction ef = (mc, + ni) -> getOrCreateEnclosingGroupInMap(mc, ni, getEnclosingMap, property, enclosingGroup); + final BiConsumer matchAction = ef::apply; + processLazyGroupInGroup(matchActions.getOrCreateAny(), valueProperty.asGroup(), ef, matchAction, new HashSet<>()); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + static ConfigurationObject getOrCreateEnclosingGroupInMap(MappingContext context, NameIterator ni, + BiFunction> getEnclosingMap, MapProperty enclosingMap, + ConfigurationInterface enclosingGroup) { + ni.previous(); + Map ourEnclosing = getEnclosingMap.apply(context, ni); + String mapKey = ni.getNextSegment(); + Converter keyConverter = context.getKeyConverter(enclosingGroup.getInterfaceType(), + enclosingMap.getMethod().getName(), enclosingMap.getLevels() - 1); + ConfigurationObject val = (ConfigurationObject) ourEnclosing.get(mapKey); + if (val == null) { + StringBuilder sb = context.getStringBuilder(); + sb.replace(0, sb.length(), ni.getAllPreviousSegments()); + ((Map) ourEnclosing).put(mapKey, + val = (ConfigurationObject) context.constructGroup(enclosingGroup.getInterfaceType())); + } + ni.next(); + return val; + } + + static ConfigurationObject getOrCreateEnclosingGroupInGroup(MappingContext context, NameIterator ni, + BiFunction getEnclosingGroup, + ConfigurationInterface enclosingGroup, GroupProperty enclosedGroup) { + boolean consumeName = !enclosedGroup.isParentPropertyName(); + if (consumeName) + ni.previous(); + ConfigurationObject ourEnclosing = getEnclosingGroup.apply(context, ni); + Class enclosingType = enclosingGroup.getInterfaceType(); + String methodName = enclosedGroup.getMethod().getName(); + ConfigurationObject val = (ConfigurationObject) context.getEnclosedField(enclosingType, methodName, ourEnclosing); + if (val == null) { + // it must be an optional group + val = (ConfigurationObject) context.constructGroup(enclosedGroup.getGroupType().getInterfaceType()); + context.registerEnclosedField(enclosingType, methodName, ourEnclosing, val); + } + if (consumeName) + ni.next(); + return val; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + static Map getOrCreateEnclosingMapInMap(MappingContext context, NameIterator ni, + BiFunction> getEnclosingMap, ConfigurationInterface enclosingGroup, + MapProperty property) { + ni.previous(); + Map ourEnclosing = getEnclosingMap.apply(context, ni); + String mapKey = ni.getNextSegment(); + Converter keyConverter = context.getKeyConverter(enclosingGroup.getInterfaceType(), property.getMethod().getName(), + property.getLevels() - 1); + Object realKey = keyConverter.convert(mapKey); + Map map = (Map) ourEnclosing.get(realKey); + if (map == null) { + map = new HashMap<>(); + ((Map) ourEnclosing).put(realKey, map); + } + ni.next(); + return map; + } + + static Map getOrCreateEnclosingMapInGroup(MappingContext context, NameIterator ni, + BiFunction getEnclosingGroup, + ConfigurationInterface enclosingGroup, MapProperty property) { + boolean consumeName = !property.isParentPropertyName(); + if (consumeName) + ni.previous(); + ConfigurationObject ourEnclosing = getEnclosingGroup.apply(context, ni); + Class enclosingType = enclosingGroup.getInterfaceType(); + String methodName = property.getMethod().getName(); + Map val = (Map) context.getEnclosedField(enclosingType, methodName, ourEnclosing); + if (val == null) { + // map is not yet constructed + val = new HashMap<>(); + context.registerEnclosedField(enclosingType, methodName, ourEnclosing, val); + } + if (consumeName) + ni.next(); + return val; + } + + public static Builder builder() { + return new Builder(); + } + + public B registerDefaultValues(B builder) { + Assert.checkNotNullParam("builder", builder); + builder.withSources(new DefaultValuesConfigSource(defaultValues)); + return builder; + } + + public Result mapConfiguration(SmallRyeConfig config) throws ConfigurationValidationException { + Assert.checkNotNullParam("config", config); + final MappingContext context = new MappingContext(config); + final Map, Object>> rootsMap = new HashMap<>(); + // eagerly populate roots + for (Map.Entry> entry : roots.entrySet()) { + String path = entry.getKey(); + List roots = entry.getValue(); + for (ConfigurationInterface root : roots) { + StringBuilder sb = context.getStringBuilder(); + sb.replace(0, sb.length(), path); + Class type = root.getInterfaceType(); + Object group = context.constructGroup(type); + rootsMap.computeIfAbsent(path, x -> new IdentityHashMap<>(roots.size())).put(type, group); + } + } + // lazily sweep + for (String name : config.getPropertyNames()) { + // may be null + ConfigValue configValue = config.getConfigValue(name); + NameIterator ni = new NameIterator(name); + BiConsumer action = matchActions.findRootValue(ni); + if (action != null) { + // ni is positioned at the end of the string + action.accept(context, ni); + } else if (configValue != null) { + context.unknownConfigElement(configValue); + } + } + ArrayList problems = context.getProblems(); + if (!problems.isEmpty()) { + throw new ConfigurationValidationException( + problems.toArray(ConfigurationValidationException.Problem.NO_PROBLEMS)); + } + context.fillInOptionals(); + return new Result(rootsMap); + } + + public static final class Builder { + SmallRyeConfig config; + final Map> roots = new HashMap<>(); + final List ignored = new ArrayList<>(); + + Builder() { + } + + public SmallRyeConfig getConfig() { + return config; + } + + public Builder setConfig(final SmallRyeConfig config) { + this.config = config; + return this; + } + + public Builder addRoot(String path, Class type) { + Assert.checkNotNullParam("type", type); + return addRoot(path, getConfigurationInterface(type)); + } + + public Builder addRoot(String path, ConfigurationInterface info) { + Assert.checkNotNullParam("path", path); + Assert.checkNotNullParam("info", info); + roots.computeIfAbsent(path, k -> new ArrayList<>(4)).add(info); + return this; + } + + public Builder addIgnored(String... patternSegments) { + Assert.checkNotNullParam("patternSegments", patternSegments); + ignored.add(patternSegments); + return this; + } + + public ConfigMapping build() { + return new ConfigMapping(this); + } + } + + public static final class Result { + private final Map, Object>> rootsMap; + + Result(final Map, Object>> rootsMap) { + this.rootsMap = rootsMap; + } + + public T getConfigRoot(String path, Class type) { + return type.cast(rootsMap.getOrDefault(path, Collections.emptyMap()).get(type)); + } + } +} diff --git a/mapper/src/main/java/io/smallrye/config/mapper/ConfigurationInterface.java b/mapper/src/main/java/io/smallrye/config/mapper/ConfigurationInterface.java new file mode 100644 index 000000000..2293f2594 --- /dev/null +++ b/mapper/src/main/java/io/smallrye/config/mapper/ConfigurationInterface.java @@ -0,0 +1,988 @@ +package io.smallrye.config.mapper; + +import static io.smallrye.config.mapper.ConfigMapping.skewer; +import static org.objectweb.asm.Type.getDescriptor; +import static org.objectweb.asm.Type.getInternalName; +import static org.objectweb.asm.Type.getType; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.eclipse.microprofile.config.spi.Converter; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import io.smallrye.common.constraint.Assert; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.inject.InjectionMessages; +import sun.misc.Unsafe; + +/** + * Information about a configuration interface. + */ +public final class ConfigurationInterface { + static final ConfigurationInterface[] NO_TYPES = new ConfigurationInterface[0]; + static final Property[] NO_PROPERTIES = new Property[0]; + static final ClassValue cv = new ClassValue() { + protected ConfigurationInterface computeValue(final Class type) { + return createConfigurationInterface(type); + } + }; + static final Unsafe unsafe; + + static { + unsafe = AccessController.doPrivileged(new PrivilegedAction() { + public Unsafe run() { + try { + final Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + return (Unsafe) field.get(null); + } catch (IllegalAccessException e) { + throw new IllegalAccessError(e.getMessage()); + } catch (NoSuchFieldException e) { + throw new NoSuchFieldError(e.getMessage()); + } + } + }); + } + + private final Class interfaceType; + private final ConfigurationInterface[] superTypes; + private final Property[] properties; + private final Class implClass; + private final Constructor constructor; + private final Map propertiesByName; + + ConfigurationInterface(final Class interfaceType, final ConfigurationInterface[] superTypes, + final Property[] properties) { + this.interfaceType = interfaceType; + this.superTypes = superTypes; + this.properties = properties; + implClass = createConfigurationObjectClass().asSubclass(ConfigurationObject.class); + try { + constructor = implClass.getDeclaredConstructor(MappingContext.class); + } catch (NoSuchMethodException e) { + throw new NoSuchMethodError(e.getMessage()); + } + final Map propertiesByName = new HashMap<>(); + for (Property property : properties) { + propertiesByName.put(property.getMethod().getName(), property); + } + this.propertiesByName = propertiesByName; + } + + /** + * Get the configuration interface information for the given interface class. This information is cached. + * + * @param interfaceType the interface type (must not be {@code null}) + * @return the configuration interface, or {@code null} if the type does not appear to be a configuration interface + */ + public static ConfigurationInterface getConfigurationInterface(Class interfaceType) { + Assert.checkNotNullParam("interfaceType", interfaceType); + return cv.get(interfaceType); + } + + /** + * Get the configuration interface type. + * + * @return the configuration interface type + */ + public Class getInterfaceType() { + return interfaceType; + } + + /** + * Get the number of supertypes which define configuration properties. Implemented interfaces which do not + * define any configuration properties and whose supertypes in turn do not define any configuration properties + * are not counted. + * + * @return the number of supertypes + */ + public int getSuperTypeCount() { + return superTypes.length; + } + + /** + * Get the supertype at the given index, which must be greater than or equal to zero and less than the value returned + * by {@link #getSuperTypeCount()}. + * + * @param index the index + * @return the supertype definition + * @throws IndexOutOfBoundsException if {@code index} is invalid + */ + public ConfigurationInterface getSuperType(int index) throws IndexOutOfBoundsException { + if (index < 0 || index >= superTypes.length) + throw new IndexOutOfBoundsException(); + return superTypes[index]; + } + + /** + * Get the number of properties defined on this type (excluding supertypes). + * + * @return the number of properties + */ + public int getPropertyCount() { + return properties.length; + } + + /** + * Get the property definition at the given index, which must be greater than or equal to zero and less than the + * value returned by {@link #getPropertyCount()}. + * + * @param index the index + * @return the property definition + * @throws IndexOutOfBoundsException if {@code index} is invalid + */ + public Property getProperty(int index) throws IndexOutOfBoundsException { + if (index < 0 || index >= properties.length) + throw new IndexOutOfBoundsException(); + return properties[index]; + } + + public Property getProperty(final String name) { + return propertiesByName.get(name); + } + + Constructor getConstructor() { + return constructor; + } + + public static abstract class Property { + private final Method method; + private final String propertyName; + + Property(final Method method, final String propertyName) { + this.method = method; + this.propertyName = propertyName; + } + + public Method getMethod() { + return method; + } + + public String getPropertyName() { + return Assert.checkNotEmptyParam("propertyName", Assert.checkNotNullParam("propertyName", propertyName)); + } + + public boolean hasPropertyName() { + return propertyName != null; + } + + public boolean isParentPropertyName() { + return hasPropertyName() && propertyName.isEmpty(); + } + + public boolean isPrimitive() { + return false; + } + + public boolean isOptional() { + return false; + } + + public boolean isGroup() { + return false; + } + + public boolean isLeaf() { + return false; + } + + public boolean isMap() { + return false; + } + + public boolean isMayBeOptional() { + return false; + } + + public PrimitiveProperty asPrimitive() { + throw new ClassCastException(); + } + + public OptionalProperty asOptional() { + throw new ClassCastException(); + } + + public GroupProperty asGroup() { + throw new ClassCastException(); + } + + public LeafProperty asLeaf() { + throw new ClassCastException(); + } + + public MapProperty asMap() { + throw new ClassCastException(); + } + + public MayBeOptionalProperty asMayBeOptional() { + throw new ClassCastException(); + } + } + + public static abstract class MayBeOptionalProperty extends Property { + MayBeOptionalProperty(final Method method, final String propertyName) { + super(method, propertyName); + } + + @Override + public boolean isMayBeOptional() { + return true; + } + + @Override + public MayBeOptionalProperty asMayBeOptional() { + return this; + } + } + + public static final class PrimitiveProperty extends Property { + private static final Map, Class> boxTypes; + private static final Map, String> unboxMethodName; + private static final Map, String> unboxMethodDesc; + + static { + Map, Class> map = new HashMap<>(); + map.put(byte.class, Byte.class); + map.put(short.class, Short.class); + map.put(int.class, Integer.class); + map.put(long.class, Long.class); + + map.put(float.class, Float.class); + map.put(double.class, Double.class); + + map.put(boolean.class, Boolean.class); + + map.put(char.class, Character.class); + boxTypes = map; + Map, String> nameMap = new HashMap<>(); + nameMap.put(byte.class, "byteValue"); + nameMap.put(short.class, "shortValue"); + nameMap.put(int.class, "intValue"); + nameMap.put(long.class, "longValue"); + + nameMap.put(float.class, "floatValue"); + nameMap.put(double.class, "doubleValue"); + + nameMap.put(boolean.class, "booleanValue"); + + nameMap.put(char.class, "charValue"); + unboxMethodName = nameMap; + nameMap = new HashMap<>(); + nameMap.put(byte.class, "()B"); + nameMap.put(short.class, "()S"); + nameMap.put(int.class, "()I"); + nameMap.put(long.class, "()J"); + + nameMap.put(float.class, "()F"); + nameMap.put(double.class, "()D"); + + nameMap.put(boolean.class, "()Z"); + + nameMap.put(char.class, "()C"); + unboxMethodDesc = nameMap; + nameMap = new HashMap<>(); + nameMap.put(byte.class, "B"); + nameMap.put(short.class, "S"); + nameMap.put(int.class, "I"); + nameMap.put(long.class, "J"); + + nameMap.put(float.class, "F"); + nameMap.put(double.class, "D"); + + nameMap.put(boolean.class, "Z"); + + nameMap.put(char.class, "C"); + } + + private final Class primitiveType; + private final Class> convertWith; + private final String defaultValue; + + PrimitiveProperty(final Method method, final String propertyName, final Class primitiveType, + final Class> convertWith, final String defaultValue) { + super(method, propertyName); + this.primitiveType = primitiveType; + this.convertWith = convertWith; + this.defaultValue = defaultValue; + } + + public Class getPrimitiveType() { + return primitiveType; + } + + public Class getBoxType() { + return boxTypes.get(primitiveType); + } + + public Class> getConvertWith() { + return Assert.checkNotNullParam("convertWith", convertWith); + } + + public boolean hasConvertWith() { + return convertWith != null; + } + + public String getDefaultValue() { + return Assert.checkNotNullParam("defaultValue", defaultValue); + } + + public boolean hasDefaultValue() { + return defaultValue != null; + } + + @Override + public boolean isPrimitive() { + return true; + } + + @Override + public PrimitiveProperty asPrimitive() { + return this; + } + + String getUnboxMethodName() { + return unboxMethodName.get(primitiveType); + } + + String getUnboxMethodDescriptor() { + return unboxMethodDesc.get(primitiveType); + } + + int getReturnInstruction() { + if (primitiveType == float.class) { + return Opcodes.FRETURN; + } else if (primitiveType == double.class) { + return Opcodes.DRETURN; + } else if (primitiveType == long.class) { + return Opcodes.LRETURN; + } else { + return Opcodes.IRETURN; + } + } + } + + public static final class OptionalProperty extends Property { + private final MayBeOptionalProperty nestedProperty; + + OptionalProperty(final Method method, final String propertyName, final MayBeOptionalProperty nestedProperty) { + super(method, propertyName); + this.nestedProperty = nestedProperty; + } + + @Override + public boolean isOptional() { + return true; + } + + @Override + public OptionalProperty asOptional() { + return this; + } + + public MayBeOptionalProperty getNestedProperty() { + return nestedProperty; + } + } + + public static final class GroupProperty extends MayBeOptionalProperty { + private final ConfigurationInterface groupType; + + GroupProperty(final Method method, final String propertyName, final ConfigurationInterface groupType) { + super(method, propertyName); + this.groupType = groupType; + } + + public ConfigurationInterface getGroupType() { + return groupType; + } + + @Override + public boolean isGroup() { + return true; + } + + @Override + public GroupProperty asGroup() { + return this; + } + } + + public static final class LeafProperty extends MayBeOptionalProperty { + private final Type valueType; + private final Class> convertWith; + private final Class rawType; + private final String defaultValue; + + LeafProperty(final Method method, final String propertyName, final Type valueType, + final Class> convertWith, final String defaultValue) { + super(method, propertyName); + this.valueType = valueType; + this.convertWith = convertWith; + rawType = rawTypeOf(valueType); + this.defaultValue = defaultValue; + } + + public Type getValueType() { + return valueType; + } + + public Class> getConvertWith() { + return Assert.checkNotNullParam("convertWith", convertWith); + } + + public boolean hasConvertWith() { + return convertWith != null; + } + + public String getDefaultValue() { + return Assert.checkNotNullParam("defaultValue", defaultValue); + } + + public boolean hasDefaultValue() { + return defaultValue != null; + } + + public Class getValueRawType() { + return rawType; + } + + @Override + public boolean isLeaf() { + return true; + } + + @Override + public LeafProperty asLeaf() { + return this; + } + } + + public static final class MapProperty extends Property { + private final Type keyType; + private final Class> keyConvertWith; + private final Property valueProperty; + + MapProperty(final Method method, final String propertyName, final Type keyType, + final Class> keyConvertWith, final Property valueProperty) { + super(method, propertyName); + this.keyType = keyType; + this.keyConvertWith = keyConvertWith; + this.valueProperty = valueProperty; + } + + public Type getKeyType() { + return keyType; + } + + public Class getKeyRawType() { + return rawTypeOf(keyType); + } + + public Class> getKeyConvertWith() { + return Assert.checkNotNullParam("keyConvertWith", keyConvertWith); + } + + public boolean hasKeyConvertWith() { + return keyConvertWith != null; + } + + public Property getValueProperty() { + return valueProperty; + } + + @Override + public boolean isMap() { + return true; + } + + @Override + public MapProperty asMap() { + return this; + } + + public int getLevels() { + if (valueProperty.isMap()) { + return valueProperty.asMap().getLevels() + 1; + } else { + return 1; + } + } + } + + static ConfigurationInterface createConfigurationInterface(Class interfaceType) { + if (!interfaceType.isInterface() || interfaceType.getTypeParameters().length != 0) { + return null; + } + // first, find any supertypes + ConfigurationInterface[] superTypes = getSuperTypes(interfaceType.getInterfaces(), 0, 0); + // now find any properties + Property[] properties = getProperties(interfaceType.getDeclaredMethods(), 0, 0); + // is it anything? + if (superTypes.length == 0 && properties.length == 0) { + // no + return null; + } else { + // it is a proper configuration interface + return new ConfigurationInterface(interfaceType, superTypes, properties); + } + } + + private static final String I_CLASS = getInternalName(Class.class); + private static final String I_COLLECTIONS = getInternalName(Collections.class); + private static final String I_CONFIGURATION_OBJECT = getInternalName(ConfigurationObject.class); + private static final String I_CONVERTER = getInternalName(Converter.class); + private static final String I_MAP = getInternalName(Map.class); + private static final String I_MAPPING_CONTEXT = getInternalName(MappingContext.class); + private static final String I_OBJECT = getInternalName(Object.class); + private static final String I_OPTIONAL = getInternalName(Optional.class); + private static final String I_RUNTIME_EXCEPTION = getInternalName(RuntimeException.class); + private static final String I_SMALLRYE_CONFIG = getInternalName(SmallRyeConfig.class); + private static final String I_STRING_BUILDER = getInternalName(StringBuilder.class); + private static final String I_STRING = getInternalName(String.class); + + private static final int V_THIS = 0; + private static final int V_MAPPING_CONTEXT = 1; + private static final int V_STRING_BUILDER = 2; + private static final int V_LENGTH = 3; + + private void addProperties(ClassVisitor cv, final String className, MethodVisitor ctor, MethodVisitor fio, + Set visited) { + for (Property property : properties) { + Method method = property.getMethod(); + String memberName = method.getName(); + if (!visited.add(memberName)) { + // duplicated property + continue; + } + // the field + String fieldType = getInternalName(method.getReturnType()); + String fieldDesc = getDescriptor(method.getReturnType()); + cv.visitField(Opcodes.ACC_PRIVATE, memberName, fieldDesc, null, null); + + // now process the property + final Property realProperty; + final boolean optional = property.isOptional(); + if (optional) { + realProperty = property.asOptional().getNestedProperty(); + } else { + assert property.isMayBeOptional(); + realProperty = property.asMayBeOptional(); + } + + // now handle each possible type + if (property.isMap()) { + // stack: - + ctor.visitMethodInsn(Opcodes.INVOKESTATIC, I_COLLECTIONS, "emptyMap", "()L" + I_MAP + ';', false); + // stack: map + ctor.visitVarInsn(Opcodes.ALOAD, V_THIS); + // stack: map this + ctor.visitInsn(Opcodes.SWAP); + // stack: this map + ctor.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); + // stack: - + } else if (property.isGroup()) { + // stack: - + boolean restoreLength = appendPropertyName(ctor, property, memberName); + ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); + // stack: ctxt + ctor.visitLdcInsn(getType(realProperty.asGroup().getGroupType().getInterfaceType())); + // stack: ctxt clazz + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "constructGroup", + "(L" + I_CLASS + ";)L" + I_OBJECT + ';', false); + // stack: nested + ctor.visitVarInsn(Opcodes.ALOAD, V_THIS); + // stack: nested this + ctor.visitInsn(Opcodes.SWAP); + // stack: this nested + ctor.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); + // stack: - + if (restoreLength) { + restoreLength(ctor); + } + } else if (property.isLeaf() || property.isPrimitive() || property.isOptional() && property.isLeaf()) { + // stack: - + boolean restoreLength = appendPropertyName(ctor, property, memberName); + ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getConfig", "()L" + I_SMALLRYE_CONFIG + ';', + false); + // stack: config + ctor.visitVarInsn(Opcodes.ALOAD, V_STRING_BUILDER); + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_STRING_BUILDER, "toString", "()L" + I_STRING + ';', false); + // stack: config key + // get the converter to use + ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); + // public Converter getValueConverter(Class enclosingType, String field) { + ctor.visitLdcInsn(getType(getInterfaceType())); + ctor.visitLdcInsn(memberName); + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getValueConverter", + "(L" + I_CLASS + ";L" + I_STRING + ";)L" + I_CONVERTER + ';', false); + // stack: config key converter + Label _try = new Label(); + Label _catch = new Label(); + Label _continue = new Label(); + ctor.visitLabel(_try); + if (property.isOptional()) { + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_SMALLRYE_CONFIG, "getOptionalValue", + "(L" + I_STRING + ";L" + I_CONVERTER + ";)L" + I_OPTIONAL + ';', false); + } else { + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_SMALLRYE_CONFIG, "getValue", + "(L" + I_STRING + ";L" + I_CONVERTER + ";)L" + I_OBJECT + ';', false); + } + // stack: value + if (property.isPrimitive()) { + PrimitiveProperty prim = property.asPrimitive(); + // unbox it + // stack: box + String boxType = getInternalName(prim.getBoxType()); + ctor.visitTypeInsn(Opcodes.CHECKCAST, boxType); + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, boxType, prim.getUnboxMethodName(), + prim.getUnboxMethodDescriptor(), false); + // stack: value + } else if (!property.isOptional()) { + assert property.isLeaf(); + ctor.visitTypeInsn(Opcodes.CHECKCAST, fieldType); + } + // stack: value + ctor.visitVarInsn(Opcodes.ALOAD, V_THIS); + // stack: value this + ctor.visitInsn(Opcodes.SWAP); + // stack: this value + ctor.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); + // stack: - + ctor.visitJumpInsn(Opcodes.GOTO, _continue); + ctor.visitLabel(_catch); + // stack: exception + ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); + // stack: exception ctxt + ctor.visitInsn(Opcodes.SWAP); + // stack: ctxt exception + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "reportProblem", + "(L" + I_RUNTIME_EXCEPTION + ";)V", false); + // stack: - + ctor.visitLabel(_continue); + if (restoreLength) { + restoreLength(ctor); + } + // add the try/catch + ctor.visitTryCatchBlock(_try, _catch, _catch, I_RUNTIME_EXCEPTION); + } else if (property.isOptional()) { + // stack: - + ctor.visitMethodInsn(Opcodes.INVOKESTATIC, I_OPTIONAL, "empty", "()L" + I_OPTIONAL + ";", false); + // stack: empty + ctor.visitVarInsn(Opcodes.ALOAD, V_THIS); + // stack: empty this + ctor.visitInsn(Opcodes.SWAP); + // stack: this empty + ctor.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); + + // also generate a sweep-up stub + fio.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); + // stack: ctxt + fio.visitLdcInsn(getType(interfaceType)); + // stack: ctxt iface + fio.visitLdcInsn(memberName); + // stack: ctxt iface name + fio.visitVarInsn(Opcodes.ALOAD, V_THIS); + // stack: ctxt iface name this + fio.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getEnclosedField", + "(L" + I_CLASS + ";L" + I_STRING + ";L" + I_OBJECT + ";)L" + I_OBJECT + ';', false); + // stack: obj? + fio.visitInsn(Opcodes.DUP); + Label _continue = new Label(); + // stack: obj? obj? + fio.visitJumpInsn(Opcodes.IFNULL, _continue); + // stack: obj + fio.visitMethodInsn(Opcodes.INVOKESTATIC, I_OPTIONAL, "of", "(L" + I_OBJECT + ";)L" + I_OPTIONAL + ';', false); + // stack: opt + fio.visitVarInsn(Opcodes.ALOAD, V_THIS); + // stack: opt this + fio.visitInsn(Opcodes.SWAP); + // stack: this opt + fio.visitFieldInsn(Opcodes.PUTFIELD, className, memberName, fieldDesc); + fio.visitLabel(_continue); + } + + // the accessor method implementation + MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, memberName, "()" + fieldDesc, null, null); + // stack: - + mv.visitVarInsn(Opcodes.ALOAD, V_THIS); + // stack: this + mv.visitFieldInsn(Opcodes.GETFIELD, className, memberName, fieldDesc); + // stack: obj + if (property.isPrimitive()) { + mv.visitInsn(property.asPrimitive().getReturnInstruction()); + } else { + mv.visitInsn(Opcodes.ARETURN); + } + mv.visitEnd(); + mv.visitMaxs(0, 0); + // end loop + } + // subtype overrides supertype + for (ConfigurationInterface superType : superTypes) { + superType.addProperties(cv, className, ctor, fio, visited); + } + } + + private boolean appendPropertyName(final MethodVisitor ctor, final Property property, final String memberName) { + boolean restoreLength = false; + if (!property.isParentPropertyName()) { + restoreLength = true; + // stack: - + ctor.visitVarInsn(Opcodes.ALOAD, V_STRING_BUILDER); + // stack: sb + ctor.visitInsn(Opcodes.DUP); + // stack: sb sb + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_STRING_BUILDER, "length", "()I", false); + // stack: sb len + ctor.visitVarInsn(Opcodes.ISTORE, V_LENGTH); + // stack: sb + ctor.visitLdcInsn(Character.valueOf('.')); + // stack: sb '.' + ctor.visitInsn(Opcodes.I2C); + // stack: sb '.' + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_STRING_BUILDER, "append", "(C)L" + I_STRING_BUILDER + ';', false); + // stack: sb + if (property.hasPropertyName()) { + ctor.visitLdcInsn(property.getPropertyName()); + // stack: sb name + } else { + ctor.visitLdcInsn(skewer(memberName)); + // stack: sb name + } + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_STRING_BUILDER, "append", + "(L" + I_STRING + ";)L" + I_STRING_BUILDER + ';', false); + // stack: sb + ctor.visitInsn(Opcodes.POP); + } + return restoreLength; + } + + private void restoreLength(final MethodVisitor ctor) { + ctor.visitVarInsn(Opcodes.ALOAD, V_STRING_BUILDER); + ctor.visitVarInsn(Opcodes.ILOAD, V_LENGTH); + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_STRING_BUILDER, "setLength", "(I)V", false); + } + + Class createConfigurationObjectClass() { + ClassWriter cv = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + String internalName = getInternalName(interfaceType); + String className = internalName + "__ConfigImpl"; // ignored by the VM, probably? + cv.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL, className, null, I_OBJECT, + new String[] { I_CONFIGURATION_OBJECT, internalName }); + MethodVisitor ctor = cv.visitMethod(Opcodes.ACC_PUBLIC, "", "(L" + I_MAPPING_CONTEXT + ";)V", null, null); + Label ctorStart = new Label(); + Label ctorEnd = new Label(); + ctor.visitLabel(ctorStart); + // stack: - + ctor.visitVarInsn(Opcodes.ALOAD, V_THIS); + // stack: this + ctor.visitMethodInsn(Opcodes.INVOKESPECIAL, I_OBJECT, "", "()V", false); + // stack: - + ctor.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); + // stack: ctxt + ctor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getStringBuilder", "()L" + I_STRING_BUILDER + ';', + false); + // stack: sb + ctor.visitVarInsn(Opcodes.ASTORE, V_STRING_BUILDER); + // stack: - + MethodVisitor fio = cv.visitMethod(Opcodes.ACC_PUBLIC, "fillInOptionals", "(L" + I_MAPPING_CONTEXT + ";)V", null, null); + Label fioStart = new Label(); + Label fioEnd = new Label(); + fio.visitLabel(fioStart); + // stack: - + fio.visitVarInsn(Opcodes.ALOAD, V_MAPPING_CONTEXT); + // stack: ctxt + fio.visitMethodInsn(Opcodes.INVOKEVIRTUAL, I_MAPPING_CONTEXT, "getStringBuilder", "()L" + I_STRING_BUILDER + ';', + false); + // stack: sb + fio.visitVarInsn(Opcodes.ASTORE, V_STRING_BUILDER); + // stack: - + addProperties(cv, className, ctor, fio, new HashSet<>()); + // stack: - + fio.visitInsn(Opcodes.RETURN); + fio.visitLabel(fioEnd); + fio.visitLocalVariable("mc", 'L' + I_MAPPING_CONTEXT + ';', null, fioStart, fioEnd, V_MAPPING_CONTEXT); + fio.visitEnd(); + fio.visitMaxs(0, 0); + // stack: - + ctor.visitInsn(Opcodes.RETURN); + ctor.visitLabel(ctorEnd); + ctor.visitLocalVariable("mc", 'L' + I_MAPPING_CONTEXT + ';', null, ctorStart, ctorEnd, V_MAPPING_CONTEXT); + ctor.visitLocalVariable("sb", 'L' + I_STRING_BUILDER + ';', null, ctorStart, ctorEnd, V_STRING_BUILDER); + ctor.visitLocalVariable("len", "I", null, ctorStart, ctorEnd, V_LENGTH); + ctor.visitEnd(); + ctor.visitMaxs(0, 0); + cv.visitEnd(); + + // todo: MR JAR/JDKSpecific + return unsafe.defineAnonymousClass(interfaceType, cv.toByteArray(), null); + } + + private static ConfigurationInterface[] getSuperTypes(Class[] interfaces, int si, int ti) { + if (si == interfaces.length) { + if (ti == 0) { + return NO_TYPES; + } else { + return new ConfigurationInterface[ti]; + } + } + Class item = interfaces[si]; + ConfigurationInterface ci = getConfigurationInterface(item); + if (ci != null) { + ConfigurationInterface[] array = getSuperTypes(interfaces, si + 1, ti + 1); + array[ti] = ci; + return array; + } else { + return getSuperTypes(interfaces, si + 1, ti); + } + } + + private static Property[] getProperties(Method[] methods, int si, int ti) { + if (si == methods.length) { + if (ti == 0) { + return NO_PROPERTIES; + } else { + return new Property[ti]; + } + } + Method method = methods[si]; + int mods = method.getModifiers(); + if (!Modifier.isPublic(mods) || Modifier.isStatic(mods) || !Modifier.isAbstract(mods)) { + return getProperties(methods, si + 1, ti); + } + if (method.getParameterCount() > 0) { + throw new IllegalArgumentException("Configuration methods cannot accept parameters"); + } + if (method.getReturnType() == void.class) { + throw new IllegalArgumentException("Void config methods are not allowed"); + } + Property p = getPropertyDef(method, method.getGenericReturnType()); + Property[] array = getProperties(methods, si + 1, ti + 1); + array[ti] = p; + return array; + } + + private static Property getPropertyDef(Method method, Type type) { + // now figure out what kind it is + Class> convertWith = getConvertWith(type); + String propertyName = getPropertyName(method); + Class rawType = rawTypeOf(type); + if (rawType.isPrimitive()) { + // primitive! + Default annotation = method.getAnnotation(Default.class); + return new PrimitiveProperty(method, propertyName, rawType, convertWith, + annotation == null ? null : annotation.value()); + } + if (convertWith == null) { + if (rawType == Optional.class) { + // optional is special: it can contain a leaf or a group, but not a map (unless it has @ConvertWith) + Property nested = getPropertyDef(method, typeOfParameter(type, 0)); + if (nested.isMayBeOptional()) { + return new OptionalProperty(method, propertyName, nested.asMayBeOptional()); + } + throw new IllegalArgumentException("Property type " + type + " cannot be optional"); + } + if (rawType == Map.class) { + // it's a map... + Type keyType = typeOfParameter(type, 0); + Class> keyConvertWith = getConvertWith(keyType); + Type valueType = typeOfParameter(type, 1); + return new MapProperty(method, propertyName, keyType, keyConvertWith, getPropertyDef(method, valueType)); + } + ConfigurationInterface configurationInterface = getConfigurationInterface(rawType); + if (configurationInterface != null) { + // it's a group + return new GroupProperty(method, propertyName, configurationInterface); + } + // fall out (leaf) + } + // otherwise it's a leaf + Default annotation = method.getAnnotation(Default.class); + return new LeafProperty(method, propertyName, type, convertWith, annotation == null ? null : annotation.value()); + } + + private static Class> getConvertWith(final Type type) { + if (type instanceof AnnotatedType) { + ConvertWith annotation = ((AnnotatedType) type).getAnnotation(ConvertWith.class); + if (annotation != null) { + return annotation.value(); + } else { + return null; + } + } else { + return null; + } + } + + private static String getPropertyName(final AnnotatedElement element) { + boolean useParent = element.getAnnotation(WithParentName.class) != null; + WithName annotation = element.getAnnotation(WithName.class); + if (annotation != null) { + if (useParent) { + throw new IllegalArgumentException("Cannot specify both @ParentConfigName and @ConfigName"); + } + String name = annotation.value(); + if (!name.isEmpty()) { + // already interned, effectively + return name; + } + // else invalid name + throw new IllegalArgumentException("Property name is empty"); + } else if (useParent) { + return ""; + } else { + return null; + } + } + + private static Type typeOfParameter(final Type type, final int index) { + if (type instanceof ParameterizedType) { + return ((ParameterizedType) type).getActualTypeArguments()[index]; + } else { + return null; + } + } + + static Class rawTypeOf(final Type type) { + if (type instanceof Class) { + return (Class) type; + } else if (type instanceof ParameterizedType) { + return rawTypeOf(((ParameterizedType) type).getRawType()); + } else if (type instanceof GenericArrayType) { + return Array.newInstance(rawTypeOf(((GenericArrayType) type).getGenericComponentType()), 0).getClass(); + } else if (type instanceof WildcardType) { + Type[] upperBounds = ((WildcardType) type).getUpperBounds(); + if (upperBounds != null) { + return rawTypeOf(upperBounds[0]); + } else { + return Object.class; + } + } else { + throw InjectionMessages.msg.noRawType(type); + } + } + +} diff --git a/mapper/src/main/java/io/smallrye/config/mapper/ConfigurationObject.java b/mapper/src/main/java/io/smallrye/config/mapper/ConfigurationObject.java new file mode 100644 index 000000000..a0c26db49 --- /dev/null +++ b/mapper/src/main/java/io/smallrye/config/mapper/ConfigurationObject.java @@ -0,0 +1,8 @@ +package io.smallrye.config.mapper; + +/** + * An interface implemented internally by configuration object implementations. + */ +public interface ConfigurationObject { + void fillInOptionals(MappingContext context); +} diff --git a/mapper/src/main/java/io/smallrye/config/mapper/ConfigurationValidationException.java b/mapper/src/main/java/io/smallrye/config/mapper/ConfigurationValidationException.java new file mode 100644 index 000000000..b4884123a --- /dev/null +++ b/mapper/src/main/java/io/smallrye/config/mapper/ConfigurationValidationException.java @@ -0,0 +1,41 @@ +package io.smallrye.config.mapper; + +/** + * + */ +public class ConfigurationValidationException extends Exception { + private static final long serialVersionUID = -2637730579475070264L; + + private final Problem[] problems; + + /** + * Constructs a new {@code ConfigurationValidationException} instance. + * + * @param problems the reported problems + */ + public ConfigurationValidationException(final Problem[] problems) { + this.problems = problems; + } + + public int getProblemCount() { + return problems.length; + } + + public Problem getProblem(int index) { + return problems[index]; + } + + public static final class Problem { + public static final Problem[] NO_PROBLEMS = new Problem[0]; + + private final String message; + + public Problem(final String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + } +} diff --git a/mapper/src/main/java/io/smallrye/config/mapper/ConvertWith.java b/mapper/src/main/java/io/smallrye/config/mapper/ConvertWith.java new file mode 100644 index 000000000..25be97a84 --- /dev/null +++ b/mapper/src/main/java/io/smallrye/config/mapper/ConvertWith.java @@ -0,0 +1,24 @@ +package io.smallrye.config.mapper; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.eclipse.microprofile.config.spi.Converter; + +/** + * Specify the converter to use to convert the annotated type. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE_USE) +public @interface ConvertWith { + /** + * The converter class to use. + * + * @return the converter class + */ + Class> value(); +} diff --git a/mapper/src/main/java/io/smallrye/config/mapper/Default.java b/mapper/src/main/java/io/smallrye/config/mapper/Default.java new file mode 100644 index 000000000..e81418d9c --- /dev/null +++ b/mapper/src/main/java/io/smallrye/config/mapper/Default.java @@ -0,0 +1,17 @@ +package io.smallrye.config.mapper; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specify the default value of a property. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Default { + String value(); +} diff --git a/mapper/src/main/java/io/smallrye/config/mapper/DefaultValuesConfigSource.java b/mapper/src/main/java/io/smallrye/config/mapper/DefaultValuesConfigSource.java new file mode 100644 index 000000000..ccf46f69b --- /dev/null +++ b/mapper/src/main/java/io/smallrye/config/mapper/DefaultValuesConfigSource.java @@ -0,0 +1,30 @@ +package io.smallrye.config.mapper; + +import java.util.Collections; +import java.util.Map; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +final class DefaultValuesConfigSource implements ConfigSource { + private final KeyMap defaultValues; + + DefaultValuesConfigSource(final KeyMap defaultValues) { + this.defaultValues = defaultValues; + } + + public Map getProperties() { + return Collections.emptyMap(); + } + + public String getValue(final String s) { + return defaultValues.findRootValue(s); + } + + public String getName() { + return "Default values"; + } + + public int getOrdinal() { + return Integer.MIN_VALUE; + } +} diff --git a/mapper/src/main/java/io/smallrye/config/mapper/KeyMap.java b/mapper/src/main/java/io/smallrye/config/mapper/KeyMap.java new file mode 100644 index 000000000..a210ba148 --- /dev/null +++ b/mapper/src/main/java/io/smallrye/config/mapper/KeyMap.java @@ -0,0 +1,220 @@ +package io.smallrye.config.mapper; + +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +import io.smallrye.common.function.Functions; + +/** + * A multi-level key map. + */ +public final class KeyMap extends HashMap> { + private static final Object NO_VALUE = new Object(); + + private static final long serialVersionUID = 3584966224369608557L; + + private KeyMap any; + @SuppressWarnings("unchecked") + private V rootValue = (V) NO_VALUE; + + public KeyMap(final int initialCapacity, final float loadFactor) { + super(initialCapacity, loadFactor); + } + + public KeyMap(final int initialCapacity) { + super(initialCapacity); + } + + public KeyMap() { + } + + public KeyMap get(final String key, int offs, int len) { + return get(key.substring(offs, offs + len)); + } + + public KeyMap getAny() { + return any; + } + + public KeyMap getOrCreateAny() { + KeyMap any = this.any; + if (any == null) { + any = this.any = new KeyMap<>(); + } + return any; + } + + public KeyMap putAny(KeyMap any) { + KeyMap oldAny = this.any; + this.any = any; + return oldAny; + } + + public KeyMap putAnyIfAbsent(KeyMap any) { + KeyMap oldAny = this.any; + if (oldAny == null) { + this.any = any; + return null; + } else { + return oldAny; + } + } + + public boolean hasRootValue() { + return rootValue != NO_VALUE; + } + + public V getRootValue() { + return getRootValueOrDefault(null); + } + + public V getRootValueOrDefault(final V defaultVal) { + V rootValue = this.rootValue; + return rootValue == NO_VALUE ? defaultVal : rootValue; + } + + public V getOrComputeRootValue(final Supplier supplier) { + V rootValue = this.rootValue; + if (rootValue == NO_VALUE) { + this.rootValue = rootValue = supplier.get(); + } + return rootValue; + } + + @SuppressWarnings("unchecked") + public V removeRootValue() { + V rootValue = this.rootValue; + if (rootValue != NO_VALUE) { + this.rootValue = (V) NO_VALUE; + } + return rootValue; + } + + public V putRootValue(final V rootValue) { + V old = this.rootValue; + this.rootValue = rootValue; + return old == NO_VALUE ? null : old; + } + + public KeyMap find(final String path) { + return find(new NameIterator(path)); + } + + public KeyMap find(final NameIterator ni) { + if (!ni.hasNext()) { + return this; + } + String seg = ni.getNextSegment(); + ni.next(); + try { + KeyMap next = getOrDefault(seg, any); + return next == null ? null : next.find(ni); + } finally { + ni.previous(); + } + } + + public KeyMap find(final Iterator iter) { + if (!iter.hasNext()) { + return this; + } + String seg = iter.next(); + KeyMap next = seg.equals("*") ? any : getOrDefault(seg, any); + return next == null ? null : next.find(iter); + } + + public KeyMap find(final Iterable i) { + return find(i.iterator()); + } + + public KeyMap findOrAdd(final String path) { + return findOrAdd(new NameIterator(path)); + } + + public KeyMap findOrAdd(final NameIterator ni) { + if (!ni.hasNext()) { + return this; + } + String seg = ni.getNextSegment(); + ni.next(); + try { + return computeIfAbsent(seg, k -> new KeyMap<>()).findOrAdd(ni); + } finally { + ni.previous(); + } + } + + public KeyMap findOrAdd(final Iterator iter) { + if (!iter.hasNext()) { + return this; + } + String seg = iter.next(); + KeyMap next = seg.equals("*") ? getOrCreateAny() : computeIfAbsent(seg, k -> new KeyMap<>()); + return next.findOrAdd(iter); + } + + public KeyMap findOrAdd(final Iterable i) { + return findOrAdd(i.iterator()); + } + + public KeyMap findOrAdd(final String... keys) { + return findOrAdd(keys, 0, keys.length); + } + + public KeyMap findOrAdd(final String[] keys, int off, int len) { + String seg = keys[off]; + KeyMap next = seg.equals("*") ? getOrCreateAny() : computeIfAbsent(seg, k -> new KeyMap<>()); + return next.findOrAdd(keys, off + 1, len - 1); + } + + public V findRootValue(final String path) { + return findRootValue(new NameIterator(path)); + } + + public V findRootValue(final NameIterator ni) { + KeyMap result = find(ni); + return result == null ? null : result.getRootValue(); + } + + public boolean hasRootValue(final String path) { + return hasRootValue(new NameIterator(path)); + } + + public boolean hasRootValue(final NameIterator ni) { + KeyMap result = find(ni); + return result != null && result.hasRootValue(); + } + + public KeyMap map(P param, BiFunction conversion) { + return map(param, conversion, new IdentityHashMap<>()); + } + + public KeyMap map(Function conversion) { + return map(conversion, Functions.functionBiFunction()); + } + + KeyMap map(P param, BiFunction conversion, IdentityHashMap, KeyMap> cached) { + if (cached.containsKey(this)) { + return cached.get(this); + } + KeyMap newMap = new KeyMap<>(size()); + cached.put(this, newMap); + Set>> entries = entrySet(); + for (Entry> entry : entries) { + newMap.put(entry.getKey(), entry.getValue().map(param, conversion, cached)); + } + KeyMap any = getAny(); + if (any != null) { + newMap.putAny(any.map(param, conversion, cached)); + } + if (hasRootValue()) { + newMap.putRootValue(conversion.apply(param, getRootValue())); + } + return newMap; + } +} diff --git a/mapper/src/main/java/io/smallrye/config/mapper/MappingContext.java b/mapper/src/main/java/io/smallrye/config/mapper/MappingContext.java new file mode 100644 index 000000000..b72d1eb35 --- /dev/null +++ b/mapper/src/main/java/io/smallrye/config/mapper/MappingContext.java @@ -0,0 +1,195 @@ +package io.smallrye.config.mapper; + +import static io.smallrye.config.mapper.ConfigurationInterface.LeafProperty; +import static io.smallrye.config.mapper.ConfigurationInterface.MapProperty; +import static io.smallrye.config.mapper.ConfigurationInterface.PrimitiveProperty; +import static io.smallrye.config.mapper.ConfigurationInterface.Property; +import static io.smallrye.config.mapper.ConfigurationInterface.getConfigurationInterface; +import static io.smallrye.config.mapper.ConfigurationValidationException.Problem; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import org.eclipse.microprofile.config.spi.Converter; + +import io.smallrye.config.ConfigValue; +import io.smallrye.config.SmallRyeConfig; + +/** + * A mapping context. This is used by generated classes during configuration mapping, and is released once the configuration + * mapping has completed. + */ +public final class MappingContext { + + private final Map, Map>> enclosedThings = new IdentityHashMap<>(); + private final Map, Map> roots = new IdentityHashMap<>(); + private final Map, Map>> convertersByTypeAndField = new IdentityHashMap<>(); + private final List, Map>>> keyConvertersByDegreeTypeAndField = new ArrayList<>(); + private final Map, Converter> converterInstances = new IdentityHashMap<>(); + private final List allInstances = new ArrayList<>(); + private final SmallRyeConfig config; + private final StringBuilder stringBuilder = new StringBuilder(); + private final ArrayList problems = new ArrayList<>(); + + MappingContext(final SmallRyeConfig config) { + this.config = config; + } + + public ConfigurationObject getRoot(Class rootType, String rootPath) { + return roots.getOrDefault(rootType, Collections.emptyMap()).get(rootPath); + } + + public void registerRoot(Class rootType, String rootPath, ConfigurationObject root) { + roots.computeIfAbsent(rootType, x -> new HashMap<>()).put(rootPath, root); + } + + public Object getEnclosedField(Class enclosingType, String key, Object enclosingObject) { + return enclosedThings + .getOrDefault(enclosingType, Collections.emptyMap()) + .getOrDefault(key, Collections.emptyMap()) + .get(enclosingObject); + } + + public void registerEnclosedField(Class enclosingType, String key, Object enclosingObject, Object value) { + enclosedThings + .computeIfAbsent(enclosingType, x -> new HashMap<>()) + .computeIfAbsent(key, x -> new IdentityHashMap<>()) + .put(enclosingObject, value); + } + + public T constructGroup(Class interfaceType) { + Constructor constructor = getConfigurationInterface(interfaceType).getConstructor(); + ConfigurationObject instance; + try { + instance = constructor.newInstance(this); + } catch (InstantiationException e) { + throw new InstantiationError(e.getMessage()); + } catch (IllegalAccessException e) { + throw new IllegalAccessError(e.getMessage()); + } catch (InvocationTargetException e) { + try { + throw e.getCause(); + } catch (RuntimeException | Error e2) { + throw e2; + } catch (Throwable t) { + throw new UndeclaredThrowableException(t); + } + } + allInstances.add(instance); + return interfaceType.cast(instance); + } + + @SuppressWarnings("unchecked") + public Converter getValueConverter(Class enclosingType, String field) { + return (Converter) convertersByTypeAndField + .computeIfAbsent(enclosingType, x -> new HashMap<>()) + .computeIfAbsent(field, x -> { + ConfigurationInterface ci = getConfigurationInterface(enclosingType); + Property property = ci.getProperty(field); + boolean optional = property.isOptional(); + if (property.isLeaf() || optional && property.asOptional().getNestedProperty().isLeaf()) { + LeafProperty leafProperty = optional ? property.asOptional().getNestedProperty().asLeaf() + : property.asLeaf(); + if (leafProperty.hasConvertWith()) { + Class> convertWith = leafProperty.getConvertWith(); + // todo: generics + return getConverterInstance(convertWith); + } else { + return config.getConverter(leafProperty.getValueRawType()); + } + } else if (property.isPrimitive()) { + PrimitiveProperty primitiveProperty = property.asPrimitive(); + if (primitiveProperty.hasConvertWith()) { + return getConverterInstance(primitiveProperty.getConvertWith()); + } else { + return config.getConverter(primitiveProperty.getBoxType()); + } + } else { + throw new IllegalStateException(); + } + }); + } + + @SuppressWarnings("unchecked") + public Converter getKeyConverter(Class enclosingType, String field, int degree) { + List, Map>>> list = this.keyConvertersByDegreeTypeAndField; + while (list.size() <= degree) { + list.add(new IdentityHashMap<>()); + } + Map, Map>> map = list.get(degree); + return (Converter) map + .computeIfAbsent(enclosingType, x -> new HashMap<>()) + .computeIfAbsent(field, x -> { + ConfigurationInterface ci = getConfigurationInterface(enclosingType); + MapProperty property = ci.getProperty(field).asMap(); + while (degree + 1 > property.getLevels()) { + property = property.getValueProperty().asMap(); + } + if (property.hasKeyConvertWith()) { + return getConverterInstance(property.getKeyConvertWith()); + } else { + return config.getConverter(property.getKeyRawType()); + } + }); + } + + @SuppressWarnings("unchecked") + public Converter getConverterInstance(Class> converterType) { + return (Converter) converterInstances.computeIfAbsent(converterType, t -> { + try { + return (Converter) t.getConstructor().newInstance(); + } catch (InstantiationException e) { + throw new InstantiationError(e.getMessage()); + } catch (IllegalAccessException e) { + throw new IllegalAccessError(e.getMessage()); + } catch (InvocationTargetException e) { + try { + throw e.getCause(); + } catch (RuntimeException | Error e2) { + throw e2; + } catch (Throwable t2) { + throw new UndeclaredThrowableException(t2); + } + } catch (NoSuchMethodException e) { + throw new NoSuchMethodError(e.getMessage()); + } + }); + } + + public NoSuchElementException noSuchElement(Class type) { + return new NoSuchElementException("A required configuration group of type " + type.getName() + " was not provided"); + } + + public void unknownConfigElement(final ConfigValue configValue) { + } + + void fillInOptionals() { + for (ConfigurationObject instance : allInstances) { + instance.fillInOptionals(this); + } + } + + public SmallRyeConfig getConfig() { + return config; + } + + public StringBuilder getStringBuilder() { + return stringBuilder; + } + + public void reportProblem(RuntimeException problem) { + problems.add(new Problem(problem.toString())); + } + + ArrayList getProblems() { + return problems; + } +} diff --git a/mapper/src/main/java/io/smallrye/config/mapper/Matching.java b/mapper/src/main/java/io/smallrye/config/mapper/Matching.java new file mode 100644 index 000000000..5d749b4d6 --- /dev/null +++ b/mapper/src/main/java/io/smallrye/config/mapper/Matching.java @@ -0,0 +1,22 @@ +package io.smallrye.config.mapper; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specify the regular expression pattern that must be matched for this property to be considered valid. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE_USE) +public @interface Matching { + /** + * The pattern. + * + * @return the pattern + */ + String value(); +} diff --git a/mapper/src/main/java/io/smallrye/config/mapper/MaxExclusive.java b/mapper/src/main/java/io/smallrye/config/mapper/MaxExclusive.java new file mode 100644 index 000000000..7c76db856 --- /dev/null +++ b/mapper/src/main/java/io/smallrye/config/mapper/MaxExclusive.java @@ -0,0 +1,22 @@ +package io.smallrye.config.mapper; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specify the maximum value (exclusive) for this property. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE_USE) +public @interface MaxExclusive { + /** + * The value. + * + * @return the value + */ + String value(); +} diff --git a/mapper/src/main/java/io/smallrye/config/mapper/MaxInclusive.java b/mapper/src/main/java/io/smallrye/config/mapper/MaxInclusive.java new file mode 100644 index 000000000..9d332eb49 --- /dev/null +++ b/mapper/src/main/java/io/smallrye/config/mapper/MaxInclusive.java @@ -0,0 +1,22 @@ +package io.smallrye.config.mapper; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specify the maximum value (inclusive) for this property. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE_USE) +public @interface MaxInclusive { + /** + * The value. + * + * @return the value + */ + String value(); +} diff --git a/mapper/src/main/java/io/smallrye/config/mapper/MinExclusive.java b/mapper/src/main/java/io/smallrye/config/mapper/MinExclusive.java new file mode 100644 index 000000000..204b85561 --- /dev/null +++ b/mapper/src/main/java/io/smallrye/config/mapper/MinExclusive.java @@ -0,0 +1,22 @@ +package io.smallrye.config.mapper; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specify the minimum value (exclusive) for this property. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE_USE) +public @interface MinExclusive { + /** + * The value. + * + * @return the value + */ + String value(); +} diff --git a/mapper/src/main/java/io/smallrye/config/mapper/MinInclusive.java b/mapper/src/main/java/io/smallrye/config/mapper/MinInclusive.java new file mode 100644 index 000000000..dea5905ec --- /dev/null +++ b/mapper/src/main/java/io/smallrye/config/mapper/MinInclusive.java @@ -0,0 +1,22 @@ +package io.smallrye.config.mapper; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specify the minimum value (inclusive) for this property. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE_USE) +public @interface MinInclusive { + /** + * The value. + * + * @return the value + */ + String value(); +} diff --git a/mapper/src/main/java/io/smallrye/config/mapper/NameIterator.java b/mapper/src/main/java/io/smallrye/config/mapper/NameIterator.java new file mode 100644 index 000000000..0238d58d6 --- /dev/null +++ b/mapper/src/main/java/io/smallrye/config/mapper/NameIterator.java @@ -0,0 +1,348 @@ +package io.smallrye.config.mapper; + +import java.util.NoSuchElementException; + +import io.smallrye.common.constraint.Assert; + +/** + * An iterator for property name strings. + */ +public final class NameIterator { + /** + * Configuration key maximum allowed length. + */ + public static final int MAX_LENGTH = 2048; + private static final int POS_MASK = 0x0FFF; + private static final int POS_BITS = 12; + private static final int SE_SHIFT = 32 - POS_BITS; + + private final String name; + private int pos; + + public NameIterator(final String name) { + this(name, false); + } + + public NameIterator(final String name, final boolean startAtEnd) { + this(name, startAtEnd ? name.length() : -1); + } + + public NameIterator(final String name, final int pos) { + Assert.checkNotNullParam("name", name); + if (name.length() > MAX_LENGTH) + throw new IllegalArgumentException("Name is too long"); + Assert.checkMinimumParameter("pos", -1, pos); + Assert.checkMaximumParameter("pos", name.length(), pos); + if (pos != -1 && pos != name.length() && name.charAt(pos) != '.') + throw new IllegalArgumentException("Position is not located at a delimiter"); + this.name = name; + this.pos = pos; + } + + public void goToEnd() { + this.pos = name.length(); + } + + public void goToStart() { + this.pos = -1; + } + + /** + * Get the cursor position. It will be {@code -1} if the cursor is at the beginning of the string, or {@code name.length()} + * if it is at the end. + * + * @return the cursor position + */ + public int getPosition() { + return pos; + } + + /* + * next-iteration DFA + * ## on EOI + * I → ## on '.' + * I → Q ## on '"' + * Q → I ## on '"' + * Q → QBS ## on '\' + * QBS → Q ## on any + * I → BS ## on '\' + * BS → I ## on any + */ + private static final int FS_INITIAL = 0; + private static final int FS_QUOTE = 1; + private static final int FS_BACKSLASH = 2; + private static final int FS_QUOTE_BACKSLASH = 3; + + /* + * Iteration cookie format + * + * Bit: 14...12 11 ... 0 + * ┌───────┬────────────┐ + * │ state │ position │ + * │ │ (signed) │ + * └───────┴────────────┘ + */ + + /** + * Create a new iteration cookie at the current position. + * + * @return the new cookie + */ + private int initIteration() { + return this.pos & POS_MASK; + } + + private int cookieOf(int state, int pos) { + return state << POS_BITS | pos & POS_MASK; + } + + private int getPosition(int cookie) { + return (cookie & POS_MASK) << SE_SHIFT >> SE_SHIFT; + } + + private int getState(int cookie) { + return cookie >> POS_BITS; + } + + /** + * Move to the next position. + * + * @param cookie the original cookie value + * @return the new cookie value + */ + private int nextPos(int cookie) { + int pos = getPosition(cookie); + if (isEndOfString(cookie)) { + throw new NoSuchElementException(); + } + int state = getState(cookie); + int ch; + for (;;) { + pos++; + if (pos == name.length()) { + return cookieOf(state, pos); + } + ch = name.charAt(pos); + if (state == FS_INITIAL) { + if (ch == '.') { + return cookieOf(state, pos); + } else if (ch == '"') { + state = FS_QUOTE; + } else if (ch == '\\') { + state = FS_BACKSLASH; + } else { + return cookieOf(state, pos); + } + } else if (state == FS_QUOTE) { + if (ch == '"') { + state = FS_INITIAL; + } else if (ch == '\\') { + state = FS_QUOTE_BACKSLASH; + } else { + return cookieOf(state, pos); + } + } else if (state == FS_BACKSLASH) { + state = FS_INITIAL; + return cookieOf(state, pos); + } else { + assert state == FS_QUOTE_BACKSLASH; + state = FS_QUOTE; + return cookieOf(state, pos); + } + } + } + + private int prevPos(int cookie) { + int pos = getPosition(cookie); + if (isStartOfString(cookie)) { + throw new NoSuchElementException(); + } + int state = getState(cookie); + int ch; + for (;;) { + pos--; + if (pos == -1) { + return cookieOf(state, pos); + } + ch = name.charAt(pos); + if (state == FS_INITIAL) { + if (pos >= 1 && name.charAt(pos - 1) == '\\') { + // always accept as-is + return cookieOf(state, pos); + } else if (ch == '.') { + return cookieOf(state, pos); + } else if (ch == '"') { + state = FS_QUOTE; + } else if (ch == '\\') { + // skip + } else { + // regular char + return cookieOf(state, pos); + } + } else if (state == FS_QUOTE) { + if (pos >= 1 && name.charAt(pos - 1) == '\\') { + // always accept as-is + return cookieOf(state, pos); + } else if (ch == '"') { + state = FS_INITIAL; + } else if (ch == '\\') { + // skip + } else { + return cookieOf(state, pos); + } + } else { + throw Assert.unreachableCode(); + } + } + } + + private boolean isSegmentDelimiter(int cookie) { + return isStartOfString(cookie) || isEndOfString(cookie) || getState(cookie) == FS_INITIAL && charAt(cookie) == '.'; + } + + private boolean isEndOfString(int cookie) { + return getPosition(cookie) == name.length(); + } + + private boolean isStartOfString(int cookie) { + return getPosition(cookie) == -1; + } + + private int charAt(int cookie) { + return name.charAt(getPosition(cookie)); + } + + public int getPreviousStart() { + int cookie = initIteration(); + do { + cookie = prevPos(cookie); + } while (!isSegmentDelimiter(cookie)); + return getPosition(cookie) + 1; + } + + public int getNextEnd() { + int cookie = initIteration(); + do { + cookie = nextPos(cookie); + } while (!isSegmentDelimiter(cookie)); + return getPosition(cookie); + } + + public boolean nextSegmentEquals(String other) { + return nextSegmentEquals(other, 0, other.length()); + } + + public boolean nextSegmentEquals(String other, int offs, int len) { + int cookie = initIteration(); + int strPos = 0; + for (;;) { + cookie = nextPos(cookie); + if (isSegmentDelimiter(cookie)) { + return strPos == len; + } + if (strPos == len) { + return false; + } + if (other.charAt(offs + strPos) != charAt(cookie)) { + return false; + } + strPos++; + } + } + + public String getNextSegment() { + final StringBuilder b = new StringBuilder(); + int cookie = initIteration(); + for (;;) { + cookie = nextPos(cookie); + if (isSegmentDelimiter(cookie)) { + return b.toString(); + } + b.append((char) charAt(cookie)); + } + } + + public boolean previousSegmentEquals(String other) { + return previousSegmentEquals(other, 0, other.length()); + } + + public boolean previousSegmentEquals(final String other, final int offs, final int len) { + int cookie = initIteration(); + int strPos = len; + for (;;) { + strPos--; + cookie = prevPos(cookie); + if (isSegmentDelimiter(cookie)) { + return strPos == -1; + } + if (strPos == -1) { + return false; + } + if (other.charAt(offs + strPos) != charAt(cookie)) { + return false; + } + } + } + + public String getPreviousSegment() { + final StringBuilder b = new StringBuilder(); + int cookie = initIteration(); + for (;;) { + cookie = prevPos(cookie); + if (isSegmentDelimiter(cookie)) { + return b.reverse().toString(); + } + b.append((char) charAt(cookie)); + } + } + + public String getAllPreviousSegments() { + final int pos = getPosition(); + if (pos == -1) { + return ""; + } + return name.substring(0, pos); + } + + public String getAllPreviousSegmentsWith(String suffix) { + final int pos = getPosition(); + if (pos == -1) { + return suffix; + } + return name.substring(0, pos) + "." + suffix; + } + + public boolean hasNext() { + return pos < name.length(); + } + + public boolean hasPrevious() { + return pos > -1; + } + + public void next() { + pos = getNextEnd(); + } + + public void previous() { + pos = getPreviousStart() - 1; + } + + public String getName() { + return name; + } + + public String toString() { + if (pos == -1) { + return "*" + name; + } else if (pos == name.length()) { + return name + "*"; + } else { + return name.substring(0, pos) + '*' + name.substring(pos + 1); + } + } + + public void appendTo(final StringBuilder sb) { + sb.append(getAllPreviousSegments()); + } +} diff --git a/mapper/src/main/java/io/smallrye/config/mapper/WithName.java b/mapper/src/main/java/io/smallrye/config/mapper/WithName.java new file mode 100644 index 000000000..82d265a6d --- /dev/null +++ b/mapper/src/main/java/io/smallrye/config/mapper/WithName.java @@ -0,0 +1,22 @@ +package io.smallrye.config.mapper; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The name of the configuration property or group. + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface WithName { + /** + * The name of the property or group. Must not be empty. + * + * @return the name + */ + String value(); +} diff --git a/mapper/src/main/java/io/smallrye/config/mapper/WithParentName.java b/mapper/src/main/java/io/smallrye/config/mapper/WithParentName.java new file mode 100644 index 000000000..40e855f28 --- /dev/null +++ b/mapper/src/main/java/io/smallrye/config/mapper/WithParentName.java @@ -0,0 +1,16 @@ +package io.smallrye.config.mapper; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Use the parent's configuration name. + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface WithParentName { +} diff --git a/mapper/src/test/java/io/smallrye/config/mapper/MapperTests.java b/mapper/src/test/java/io/smallrye/config/mapper/MapperTests.java new file mode 100644 index 000000000..4478689f6 --- /dev/null +++ b/mapper/src/test/java/io/smallrye/config/mapper/MapperTests.java @@ -0,0 +1,50 @@ +package io.smallrye.config.mapper; + +import static org.junit.Assert.assertEquals; + +import java.util.Collections; + +import org.junit.Test; + +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.common.MapBackedConfigSource; + +/** + * + */ +public class MapperTests { + + public interface Basic { + String helloWorld(); + + @Default("this is the default") + String helloWorldWithDefault(); + } + + @Test + public void testBasic() throws ConfigurationValidationException { + ConfigMapping.Builder mb = ConfigMapping.builder(); + mb.addRoot("test", Basic.class); + ConfigMapping mapping = mb.build(); + SmallRyeConfigBuilder cb = new SmallRyeConfigBuilder(); + mapping.registerDefaultValues(cb); + SmallRyeConfig config = cb.build(); + try { + mapping.mapConfiguration(config); + } catch (ConfigurationValidationException e) { + assertEquals(1, e.getProblemCount()); + } + + cb = new SmallRyeConfigBuilder(); + //noinspection serial + cb.withSources(new MapBackedConfigSource("source", Collections.singletonMap("test.hello-world", "here I am!")) { + }); + mapping.registerDefaultValues(cb); + config = cb.build(); + ConfigMapping.Result result = mapping.mapConfiguration(config); + Basic basic = result.getConfigRoot("test", Basic.class); + assertEquals("here I am!", basic.helloWorld()); + assertEquals("this is the default", basic.helloWorldWithDefault()); + } +} diff --git a/pom.xml b/pom.xml index a37dfb0ae..ea575c2eb 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,7 @@ http://smallrye.io + 7.0 1.4 1.0.2 @@ -66,6 +67,7 @@ common implementation + mapper sources/hocon sources/file-system sources/yaml @@ -114,6 +116,12 @@ + + org.ow2.asm + asm + ${version.asm} + + io.smallrye.common