private static <T extends AbstractConfig> T mutateBranch(
            final T config,
            final Map<PropertyPath, ObjectInfo<? extends AbstractConfig>> objectInfoMap,
            final Map<PropertyPath, Object> replacementValueMap) {

        final ObjectInfo<T> objectInfo = (ObjectInfo<T>) Objects.requireNonNull(
                objectInfoMap.get(config.getBasePath()));

        final boolean hasBranchReplacement = replacementValueMap.containsKey(config.getBasePath());

        final T mutatedConfig;
        if (hasBranchReplacement) {
            final Object replacementValue = replacementValueMap.get(config.getBasePath());
            if (replacementValue != null) {
                mutatedConfig = (T) replacementValue;
            } else {
                mutatedConfig = null;
            }
        } else {
            final Map<String, Object> valueMap = new HashMap<>();

            // No replacement for the whole branch so check all its props
            AtomicBoolean haveAnyPropsChanged = new AtomicBoolean(false);
            objectInfo.getPropertyMap().forEach((propName, prop) -> {
                final PropertyPath propPath = config.getBasePath().merge(propName);
                final Class<?> valueClass = prop.getValueClass();
                LOGGER.trace(() -> LogUtil.message("prop: {}, class: {}", prop, prop.getValueClass()));
                final boolean hasValueReplacement = replacementValueMap.containsKey(propPath);

                if (hasValueReplacement) {
                    final Object replacementValue = replacementValueMap.get(propPath);
                    valueMap.put(propName, replacementValue);
                    haveAnyPropsChanged.set(true);
                    // Remove this entry so when we get to the end we know if we have found them all
                    replacementValueMap.remove(propPath);
                } else {
                    final Object existingPropValue = prop.getValueFromConfigObject();

                    if (existingPropValue != null
                        && AbstractConfig.class.isAssignableFrom(valueClass)) {
                        // Branch so recurse
                        Object newPropValue = mutateBranch((AbstractConfig) existingPropValue,
                                objectInfoMap,
                                replacementValueMap);
                        if (!Objects.equals(existingPropValue, newPropValue) && !haveAnyPropsChanged.get()) {
                            haveAnyPropsChanged.set(true);
                        }
                        valueMap.put(propName, newPropValue);
                    } else {
                        // Unchanged leaf
                        valueMap.put(propName, existingPropValue);
                    }
                }
            });

            if (haveAnyPropsChanged.get()) {
                // Create a replacement for this branch
                mutatedConfig = objectInfo.createInstance(valueMap::get);
                // Copy the basePath over in case it has been decorated with paths
                mutatedConfig.setBasePath(config.getBasePath());
            } else {
                // no props have change on this branch so just return what we had
                mutatedConfig = config;
            }
        }
        return mutatedConfig;
    }
Populates objectInfoMap with an {@link ObjectInfo} object for each sub-class of {@link AbstractConfig}
that is not marked as {@link stroom.util.shared.NotInjectableConfig}. It walks the object tree from
the passed config object downwards. path is used as the property path for the root config

@param config The root config
stroom-util/src/main/java/stroom/util/config/AbstractConfigUtil.java