private static <T> void copy(final T source,
                                 final T dest,
                                 final T vanillaObject,
                                 final UpdateAction updateAction,
                                 final CopyOption... copyOptions) {
        Objects.requireNonNull(source);
        Objects.requireNonNull(dest);

        LOGGER.trace("copy called for {}", source.getClass().getSimpleName());
        try {
            final Map<String, Prop> properties = PropertyUtil.getProperties(source);
            for (Prop prop : properties.values()) {
                LOGGER.trace("Examining prop {} of type {}",
                        prop.getName(), prop.getValueClass().getSimpleName());

                final Object sourcePropValue = prop.getGetter().invoke(source);
                final Object destPropValue = prop.getGetter().invoke(dest);

                final Object defaultPropValue = vanillaObject != null
                        ? prop.getGetter().invoke(vanillaObject)
                        : null;

                // TODO this doesn't work for props that are collections of stroom classes,
                //  e.g. List<ForwardDestination>
//                if (AbstractConfig.class.isAssignableFrom(prop.getValueClass())) {
                if (prop.getValueClass().getName().startsWith("stroom")
                        && !StroomDuration.class.equals(prop.getValueClass())) {
                    // property is another stroom pojo
                    if (sourcePropValue == null && destPropValue == null) {
                        // nothing to do
                    } else if (sourcePropValue == null && destPropValue != null) {
//                            LOGGER.info("Updating config value of {} from [{}] to [{}]",
//                                    prop.getName(), destPropValue, sourcePropValue);
//                            prop.getSetter().invoke(dest, sourcePropValue);
                        updateValue(dest,
                                prop,
                                sourcePropValue,
                                destPropValue,
                                defaultPropValue,
                                updateAction,
                                copyOptions);
                    } else if (destPropValue == null) {
                        // Create a new object to copy into
                        final Object newInstance = prop.getValueClass().getConstructor().newInstance();

                        // no destination instance so just use the new instance
                        prop.getSetter().invoke(dest, newInstance);
                        copy(sourcePropValue, newInstance, defaultPropValue, updateAction, copyOptions);
                    } else {
                        // Recurse for deep copy.
                        copy(sourcePropValue, destPropValue, defaultPropValue, updateAction, copyOptions);
                    }
                } else {
                    // prop is a primitive or a non-stroom class, so update it if it is different
                    if ((sourcePropValue == null && destPropValue != null) ||
                            (sourcePropValue != null && !sourcePropValue.equals(destPropValue))) {
                        updateValue(dest,
                                prop,
                                sourcePropValue,
                                destPropValue,
                                defaultPropValue,
                                updateAction,
                                copyOptions);
                    }
                }
            }
        } catch (InvocationTargetException
                | IllegalAccessException
                | NoSuchMethodException
                | InstantiationException e) {
            throw new RuntimeException(LogUtil.message("Error copying fields from [{}] to [{}]", source, dest), e);
        }
    }
Deep copy the field values of stroom POJO object trees from source into dest.
Relies on the public getters/setters so private fields won't be copied.
NOTE: Only deep copies objects in packages starting with 'stroom'
Only non default source values will be copied, ie. the value in source
is equal to the value in vanillaObject.
stroom-util/src/main/java/stroom/util/config/FieldMapper.java