The previous use of Java's default serialization dumped
all implementation-detail class names and fields into the serialization,
making it basically impossible to improve the implementation.
Two strategies here:
- prohibit serialization of unresolved configs, which are
the location of a lot of implementation detail
- delegate all serialization to an Externalizable
SerializedConfigValue class, which serializes
using fields that have lengths. Unknown fields
can thus be skipped and we can write code to
support obsolete fields, and so on.
As a side effect, this makes the serialization far more compact
because we don't need the Java per-class header noise, and we
jump through some hoops to avoid writing out duplicate ConfigOrigin
information. It still isn't super-compact compared to something
like protobuf but it's a lot less insane.
This should have been the API, rather than adding a loader
parameter to ConfigFactory methods.
By adding to the options, the class loader is inherited by
any included files or urls. Previously, it was only inherited
by included classpath resources, but including classpath resources
from non-classpath resources would lose track of the class loader.
The methods that take a ClassLoader are now convenience API that
just adds that passed-in loader to your ConfigParseOptions.
path : [ /bin ]
path : ${path} [ /usr/bin ]
This added very few lines of code or bytecode!
It's just a natural extension of the existing
string concatenation.
But it did add a fair few lines of specification
and tests.
Only one subclass ever used this, SimpleConfigObject
So now we have withFallbacksIgnored that asserts they are
already ignored, for the other classes, and has a special
SimpleConfigObject implementation.
The basic idea in this patch is to unify on the "replacer"
(modifying the tree in which we lookup substitutions)
mechanism for detecting cycles.
Drop the "traverse" method, instead keeping a trace of expressions
we've passed through for nice error messages.
There is now only one internal checked exception possible,
NotPossibleToResolve which is thrown when a cycle is detected;
this checked exception is always caught by ConfigReference
so cannot "escape" from the library. The exception exists
because we want to get the traceString debug info out
of the spot that detected the cycle, if we didn't want that
debug info we could just return null as usual for undefined.
As part of simplifying this (which should also simplify the
spec), resolutions which require double-traverse of the same
reference are no longer supported:
a=1, b=${a}, a=${b}
Also, cycles now always throw UnresolvedSubstitution rather
than BadValue. This was needed for consistency since
conceptually a single a=${a} is going to "look back" earlier
in the merge stack, discover there is no earlier value of a,
and fail; it should be the same exception as a=${a},a={b:1},
and in both cases referring to these cycles via ${?} should
hide the exception.
This makes the code a good bit simpler to reason about, the old
ConfigSubstitution really mixed two things into one class.
Unfortunately old ConfigSubstitution is hanging around as a compat
shim so we can deserialize stuff from the old version of the library.
The old version will not be able to deserialize unresolved configs
from this new version.
In retrospect it might have been better to forbid serialization
of unresolved configs and only support serializing resolved configs.
The MemoKey stuff here was leftover from some earlier version of
the code, and was no longer used (the restrictToChild was always
null in the MemoKey).