Lifecycle of objects in Registry/Convertor APIs
In order to correctly use Registry and Convertor APIs the
lifecycle of objects managed by these APIs must be explained. There is
several design decisions which were made and which will be described in
this document. They could be summarized as:
- bindings are singletons
-
stateless convertors
- merged context does not create new instance
-
no autosave
The behaviour has changed in several aspects compared to existing
Filesystems/Datasystems APIs. The motivation for these changes were
simplification of APIs and optimization for the most common
usecases.
Bindings are singletons
The Registry API treats objects it creates as singletons. Once the
object is created and as
long as some non-Registry code is holding a reference to the object,
subsequent calls will return the same object. Current implementation weakly references all created objects.
Stateless convertors
The Convertor API is stateless. Convertor is factory which does not
need any state to produce instance from persited data. Persisted data
should contain everything necessary to recreate the instance.
This is different from core/settings convertors which passes state to
their convertor and allow to create different instances from one
persisted data.
Merged context does not create new instance
Merged context does not create new instances. It delegates object
creation to its delegating contexts. That means the bindings are treated
as singletons on merged context too.
This is different from how MultiFilesystem works. The MultiFilesystem
which merged set of filesystems creates new MultiFileObject for each
merged file and for this file object the Datasystems creates new
DataObject which produce new instace through InstanceCookie. In this
case it make sense to influence object creation and produce difference
instace for regular FileObject and for MultiFileObject. In merged
context there is only one object and it should be the same without
regard whether it was created from the regular Context or from the
Merged Context.
No autosave
Modified object retrieved from Registry is not autosaved. The modified
object must be bound to Registry to be persisted otherwise the
modification is lost after application reload.
This is different from InstanceDataObject which saves modified instances automatically.
Illustrative examples
Several illustrative examples are provided here which demonstrate incorrect usage of APIs or suggest solution for some usecases.
Example 1: Shared object with a state
This example demonstrates that special care must be taken during design
of API of objects which will be shared by multiple clients in Registry.
As was said above the Registry treats objects as singletons. That means
that multiple foreign clients can retrieve the same instance and
depending on the API of that object they can simultaneously mutate the
state of the object what can results in unexpected random failures. Let's
demonstrate it on example:
Example of wrong usage:
Imagine following public API class:
class Compiler {
// configure compilation classpath
public void setClasspath(String classpath);
// compile set of files according to configured classpath
public void compile(Set files);
}
The design of the class expects that first the class is instantiated,
then the classpath is set and then the files can be compiled. If
instance of this class is stored in Registry and two clients want to
use it concurrently to compile a file it will occasionally not work correctly. They share the
same instance and they are changing the state of the object (the
classpath) each other. The result is random failures during the
compilation of files because of incorrect classpath.
Fixed example:
The above example must be solved by registering compiler factory class
to Registry instead. The modified classes could look like:
class Compiler {
// compile set of files
public void compile(Set files);
}
class CompilerFactory {
// create compiler for the given classpath
public Compiler createCompiler(String classpath);
}
This design expects that CompilerFactory creates private instance of
Compiler for the given classpath and this Compiler instance can be then
used for compilation of set of files. Only instances of CompilerFactory
would be allowed to be registered in Registry. Any client can retrieve
the (singleton) instance of CompilerFactory and use it whatever way
without clashing with other
clients, because the product (the Compiler) is created per client and
nobody else has access to it.
Note:
It is important to emphasize that this is relevant only in case the
Compiler class is public API. If the Compiler class from first wrong
example was private API which no foreign module could access then the
example is perfectly OK if the module owner is aware of the
singletonness of the instance.
Example 2: Merged context usages
It must be kept in mind that behaviour of Merged context is different
then MultiFilesystem behavoiur. The Merged context does not create new
instances but delegates calls to its delegating contexts.
Imagine following case. Editor defines registry context per each
MIMEtype and modules can register to these contexts actions which
should be visible in editor popup menu. The
editor popup menu is generated for edited file by going from the most
specific MIMEtype to the less specific one and by merging all found
actions. For
example for Ant script opened in editor the MIMEtype contexts searched
for the actions are: text/x-ant+xml, text/xml and text/plain. Merged
context can be used to do the merging of these contexts. The
instantiation of an action through the merged context will be delegated
to the underlaying context on which the action was found.
That means it is irrelevant whether the action is created directly
from context owning the action or from merged context - the result will
be the same action. That's also reason why the convertors are
stateless, because the context through which the action is created
should not influence it.
If there are cases where this behaviour is not desired the usecases
must be provided. Generally it can be workarounded by creating always
new instance of Registry SPI Context and merging these instead of the
default one. But reasons for that should be well thought.
Example 3: Context based instances
There are cases where instance is defined as path to some other context
and is created from the content of that context. Such a definition of
instance allows foreign modules to register their instances into the
folder. The potential problem here is with addressing - how to resolve
the context path to real Context instance when there is multiple
Registry instances.
For Default Registry this is not issue. Any absolute context name can be easily resolved to Default Registry.
For any non-default registry the situation is different. The convertors
are
stateless - they do not know the context (the registry instance) in
which they operate and so they are not able to resolve context path.
Nevertheless the real compelling usecases where this is needed must be
provided if this issue needs to be resolved generally. Till now the
only usecase is Looks API. And after the talk with Hrebejk (Looks API
owner) even this case can be resolved differently. The represented
object
used by Looks should know project it belongs to and so according to
this
information it should be able to find correct project registry.
Generally, whenever this is a problem the context path must be resolved outside of Registry/Convertor API.
Imagine following theoretical case. The JMenu instance bound in
registry is defined as context path to context which contains
individual actions. The persisted instance looks like:
<menu xmlns=".../menu_ns">
<context>/some/context/with/menu/items</context>
</menu>
If this instance is defined on Default Registry then convertor registered for this namespace can directly
resolve the path to context from Default Registry and create the
instance of the JMenu.
If this registration should be used on non-default registry then
convertor cannot resolve the path and cannot create the JMenu. Instead
of that the instance of interface like this should be returned:
public interface MenuDescriptor {
JMenu create(Context c);
}
The instance keeps the context path as String and postpones
resolving to create(Context) method. Client who knows the Context from
which the object was read will call create(Context) factory method to
get the JMenu instance.
If several compelling usecases are provided the solution could be
generalized and supported directly in Registry API. For example there
can be in API of Registry interface:
public interface ContextSensitiveInstance {
Object create(Context c);
}
which must be implemented by all object which are resolving context
path. The Registry API would check for the presence of this interface
directly in the method api.Context.getObject(String, Object) and do the
translation of the object automatically in the Registry API by
calling the ContextSensitiveInstance.create(Context) method with proper context. The API client would get the translated object.
Links