TheElectronWill / night-config

Powerful java configuration library for toml, yaml, hocon, json and in-memory configurations. Serialization/deserialization framework.
GNU Lesser General Public License v3.0
242 stars 28 forks source link

Create a deep copy of a configuration? #132

Open brainbytes42 opened 1 year ago

brainbytes42 commented 1 year ago

hi again,

is there any (simple) way to create a deep copy of a configuration? Then one could compare the original and copied version, if one was changed later on? (equals seems to work properly for this case, comparing values and recursively including sub-configurations.)

This is slightly related to #118, as I was evaluating workarounds to this and to the question, if the config has been changed (to then manually / optionally save the config with user interaction). (Not only for autosave, but also for changes in config, a listener would be nice.)

But it seems, that in a copy only the 'simple' values are copied; sub-configurations remain linked and will be changed in both places:

Config config = Config.inMemory();
config.set("foo", "bar");
Config sub = config.createSubConfig();
sub.set("hello", "world");
config.set("sub", sub);
System.out.println("config = " + config);

System.out.println("--- COPY + CHANGE ---");

Config copy = Config.copy(config);
copy.set("foo", "bar-changed");
copy.<Config>get("sub")
    .set("hello", "world-changed");
System.out.println("config = " + config + "     <--  EXPECTED hello='world', BUT IS 'world-changed'! (no real deep copy!)");
System.out.println("copy = " + copy);

Maybe (if this behaviour isn't considered a bug), add Config.deepCopy(UnmodifiableConfig)?

TheElectronWill commented 1 year ago

Hi!

I've created #133 to track your feature request about modification tracking. Config#copy() is intended to be a shallow copy, in a similar way to clone(). A deepCopy() or deepClone() would be useful, though. Let's track this here.

For now, you can implement it yourself by calling copy, iterating on the entries and recursively copying sub-configurations, it shouldn't be too long to implement.

TheElectronWill commented 8 months ago

I'm still wondering what the right interface is. One possibility is to add these to Config

    /**
     * Puts a deep copy of the given configuration to the root of this config.
     * <p>
     * This methods calls {@code deepCopy} on every sub-configuration,
     * manually copies lists of known type (including {@code ArrayList}),
     * calls {@code clone()} on {@code Cloneable} values, and simply
     * copy the reference of other values.
     * <p>
     * For more control over the deep copy, in particular if you have
     * put nested mutable objects into the config, use {@link #putDeepCopy(Function)}.
     */
    void putDeepCopy(UnmodifiableConfig configToCopy);

    /**
     * Puts a deep copy of the given configuration to the root of this config.
     * <p>
     * This methods calls {@code deepCopy} on every sub-configuration
     * and uses the {@code valueCopier} for other values (including lists).
     *
     * @param valueCopier a function that creates a deep copy of any object
     */
    void putDeepCopy(UnmodifiableConfig configToCopy, Function<Object, Object> valueCopier);

But given that many options could be good to have, it may be better to have a dedicated class DeepConfigCopier.

brainbytes42 commented 8 months ago

The Signature doesn't fit the JavaDoc's @return...

(And I'd prefer the described functionality in @return - so that I just get a new Config-Object from an existing one, without too much hassle...)

TheElectronWill commented 8 months ago

Ah that's right, I miscopied some code. The design choice here is: how do you choose the type of the new config. For simple configs that maps a HashMap and aren't thread-safe it's trivial, but for ConcurrentConfigs it's not (for instance, a subConfig of a SynchronizedConfig is not the same as a new SynchronizedConfig).

Thus, doing new MyNewConfig().putDeepCopy(config); works well. An alternative would be config.deepCopyTo(MyNewConfig::new);.

brainbytes42 commented 8 months ago

Ah, I see - that makes sense...

From a usability / "discoverability" view, I'd prefer the latter, as I'd then come from an existing object and have all my methods there, with all their documentation / examples. Seems more fluent to me... But maybe that's a personal preference... :-)