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).
this is part of a plan to remove SubstitutionResolver (moving the
memoized hash to ResolveContext) and to move the "replacements"
functionality to ResolveSource. Should be clearer.
I think this was a relic of some earlier idea about how to do
things. It was used in one place that could just use the
public NotResolved exception, since it was converted to that
later anyhow.
To avoid error-message regression, we still catch NotResolved
and change it into a NotResolved with a different message in
certain cases.
Also clean up the wording of NotResolved in various places.
Probably NotResolved could just have a zero-parameters constructor.
With this patch, you can write:
path="a🅱️c"
path=${path}":d"
The semantics are somewhat complicated to specify and
implement, but hopefully not complicated to use for
any reasonable usage that makes any sense.
This patch is technically backward incompatible,
illustrated by a change to existing unit tests,
but the changed behavior is in a bizarre corner case:
cyclical self-references where the cycle could be broken
by only partially resolving an object.
This corner case just threw an exception in the version
of this lib included with Akka/Play 2.0, but in commit
a59e31f744 we tried to handle the case. So the behavior
changes since that commit.
The partial resolution case now has the same consistent
defined semantics we use for all self-reference
resolution, that is it "looks back." In the previous
code it would "look forward."
This makes it easier to mess with the parameters needed without
changing every resolveSubstitutions() all over the place.
Really the SubstitutionResolver and ResolveContext should maybe
be merged, but keeping this patch more incremental.
This is probably not different from the depth counter
in practical situations, but it does guarantee 100%
that we are only detecting true cycles. Since we want
true cycles to have different semantics (in a following
patch), I feel better about this implementation.