Lucy users can store the state of the application in a so-called workspace. Later, they can return the application to the state in which they left it by re-loading the workspace. They do not have to re-set their GUI preferences, nor do they have to re-load their maps. The GUI layout is restored as it was in the previous session, maps are opened in their previous positioning, and additional data is re-loaded as map layers.
This article starts by explaining how the Lucy state information is saved in the workspace. Next, it discusses a number of guidelines for guaranteeing workspace support for your own developments. For common Lucy development work, you will be able to rely on Lucy API utilities for workspace support. More extensive development might require an additional effort on your part though.
Workspace support is an optional feature.
If you decide not to use workspaces, you can simply remove all workspace-related UI elements from Lucy.
The class Javadoc of the |
The workspace saving and loading mechanism
As an object-oriented application, a Java application consists of a complex web of objects that are related to each other. One object either owns an object, contains another object, or stores a reference to another object. Such a web of objects forms an object graph.
The basic principle behind the Lucy workspace mechanism is storing and restoring the Java object graph that makes up the application. That includes storing information about the relations between the different Java object instances and information about the state of each instance. During workspace loading, the encoded information is used to restore the Java object graph.
For the Lucy workspace framework, the Java object graph contains two types of objects: permanent objects that remain present in the Java object graph during workspace swapping, and objects that are stored with their properties before being destroyed, and newly created with the right properties when a workspace is restored:
-
Permanent objects Certain object types essential to the Lucy framework are not removed and recreated when workspaces are loaded. Those object instances remain in the Java object graph, and their state is altered to match the state that was encoded in the workspace. Typical examples of such object instances are the Lucy back-end
ILcyLucyEnv
and the Lucy add-ons. -
Stored objects When a workspace is saved, a limited number of object types is stored in the workspace. Those types are: models, layers, views, maps, application panes, and a few other, minor types. The saved object state consists of object properties and references to other objects that needs to be re-created. Other instances, which form the bulk of the Java object graph, are not stored. They are re-created implicitly via stored objects.
The difference between the stored objects and the permanent objects is that when the workspace is loaded, all stored objects are destroyed and removed from the Java object graph. New instances are created for restoring the state that was encoded in the workspace. The references between the different Java object instances are restored as well.
It is very important that the Java object graph is correctly restored. For example, in Figure 1, “Bolded objects below the line are re-created based on object references. Grayed out objects and connectors are not stored when a workspace is saved.” Layer1 and Layer2 share the same model. When the workspace is restored with new instances of the layers and the models, it is essential that those new layer instances refer to the same model, so that both layers are updated when you edit the data. Giving each layer its own model instance would be plainly wrong: once you edit one model instance, the other becomes outdated.
The figure also illustrates that only part of the Java object graph is saved. For example, the toolbar from Map2 is not encoded in the workspace. It is re-created during workspace loading when Map2 is restored.
The main API classes
The main API classes work closely together. This section illustrates the different interactions between these classes, and then discusses their role and responsibilities in more detail.
Workspace saving and loading code flow
This section explains the steps taken by the Lucy workspace framework to encode a workspace, and then decode the workspace again.
Workspace encoding
When a Lucy user saves a workspace, a call to ALcyWorkspaceCodec.encodeWorkspace
is issued. The workspace encoding call results in these interactions:
When the TLcyWorkspaceManager
calls ALcyWorkspaceCodec.encodeWorkspace
:
-
The
ALcyWorkspaceCodec
iterates over allALcyWorkspaceCodecDelegate
instances, and calls theirencode
method -
The
ALcyWorkspaceCodecDelegate
instances store properties that describe state of permanent objects. -
The
ALcyWorkspaceCodecDelegate
also determines if it needs to store references to stored objects. In the Figure 1, “Bolded objects below the line are re-created based on object references. Grayed out objects and connectors are not stored when a workspace is saved.” example, theALcyWorkspaceCodecDelegate
stores the state of the map manager. It will store references to Map1 and Map2.The storing of a reference to a map, or any other stored object, is a process in two steps:
-
Because the map is a stored object, a new map instance will be created during workspace decoding. This means that the workspace should contain enough information to re-create a map during decoding with the same state as the map which was originally encoded.
The
encode
method of aALcyWorkspaceObjectCodec
takes care of the creation and storage of the map information: it can store the necessary information about a stored object, to be able to re-create a new instance with matching state afterwards. -
It is possible that multiple
ALcyWorkspaceCodecDelegate
instances want to store a reference to the same map. If the information to restore the map is already available in the workspace, it should not be added a second time.More importantly, the workspace mechanism must ensure that both
ALcyWorkspaceCodecDelegate
instances will decode the same map Java object when they decode the reference during workspace decoding.This is the role of the
ALcyWorkspaceCodec.encodeReference
method: it keeps track of which stored objects have already been stored. If the object has not been stored yet, theALcyWorkspaceCodec
selects the properALcyWorkspaceObjectCodec
and asks it to store the necessary information into the workspace. TheALcyWorkspaceCodec
keeps a reference to the location of the information in the workspace, and returns that reference to theALcyWorkspaceCodecDelegate
. If the object was already stored, theALcyWorkspaceCodec
looks up the reference, and returns that to theALcyWorkspaceCodecDelegate
.It is that reference that the
ALcyWorkspaceCodecDelegate
will save into the workspace.
-
-
As explained previously, the
ALcyWorkspaceObjectCodec
stores all the information it needs to re-create a stored object. It is possible however that a stored object has references to other stored objects.Returning to the example of Figure 1, “Bolded objects below the line are re-created based on object references. Grayed out objects and connectors are not stored when a workspace is saved.”, the object codec for the map stores a reference to the view. The object codec of the view will have to store references to each of the layers in the view, while the object codec for the layer needs to store a reference to the model.
They do that by using exactly the same
ALcyWorkspaceCodec.encodeReference
method as theALcyWorkspaceCodecDelegate
.
Workspace decoding
During workspace decoding, the information that was stored in the workspace by the ALcyWorkspaceCodecDelegate
instances is passed back to those codec delegates by the ALcyWorkspaceCodec
.
The information allows the ALcyWorkspaceCodecDelegate
instances to restore the state of the permanent objects.
If the ALcyWorkspaceCodecDelegate
needs to decode a reference to a stored object, it must call the
ALcyWorkspaceCodec.decodeReference
method.
Similar to the check in the reference encoding step, the ALcyWorkspaceCodec.decodeReference
method checks whether that reference has already been decoded.
If the reference has not been decoded yet, the workspace codec will use the reference to retrieve all the information for
the stored object from the workspace, and ask the ALcyWorkspaceObjectCodec
to create a new instance based on that information.
It will then return that instance to the ALcyWorkspaceCodecDelegate
.
If the reference has already been decoded, the ALcyWorkspaceCodec.decodeReference
method will return the already decoded
object.
Note that this mechanism ensures that the same reference will be decoded if multiple ALcyWorkspaceCodecDelegate
instances have stored a reference to the same object. They have all encoded the same reference. Therefore, they get exactly
the same object back during the decoding step.
Keep in mind that the ALcyWorkspaceObjectCodec
instances can also save references to other stored objects.
During decoding, they use the same ALcyWorkspaceCodec.decodeReference
method to decode those other stored objects.
To pass stored information back to the correct codec, the ALcyWorkspaceCodec
relies on the unique identifier (UID) of the ALcyWorkspaceCodecDelegate
and ALcyWorkspaceObjectCodec
instances.
This means that a UID can never be changed once it has been chosen. The UIDs of the ALcyWorkspaceCodecDelegate
instances added by Lucy itself all start with com.luciad
, for example com.luciad.lucy.addons.genericmap.TLcyCombinedMapManager.codecDelegate
.
Role of the ALcyWorkspaceCodec
The ALcyWorkspaceCodec
is responsible for encoding and decoding the complete workspace.
It keeps track of the object graph, by managing the object references.
Whenever an ALcyWorkspaceCodecDelegate
or an ALcyWorkspaceObjectCodec
needs a reference to a stored object, the codec must explicitly ask the ALcyWorkspaceCodec
to create the reference.
Referring to the example of The workspace saving and loading mechanism, when a layer is encoded, it needs to store a reference to its model.
It is important that the framework keeps track of those references, as multiple layers might refer to the same model.
During workspace encoding, it is the role of the ALcyWorkspaceCodec
to ensure that it returns the same reference each time one of the codecs asks for a reference to object X.
This applies to workspace decoding as well: each time one of the codecs asks to decode a reference Y, the ALcyWorkspaceCodec
must ensure that the same object instance, matching that reference, is returned.
Role of the ALcyWorkspaceCodecDelegate
As shown in Figure 2, “Workspace encoding sequence diagram”, the ALcyWorkspaceCodec
iterates over the ALcyWorkspaceCodecDelegate
instances to save the workspace.
An ALcyWorkspaceCodecDelegate
is responsible for saving the state of a permanent object, and storing references to each of the stored objects. It will
need to reset the state of the permanent object during workspace decoding.
As such, the ALcyWorkspaceCodecDelegate
instances determine which part of the Java object graph will be saved into the workspace, and identify which stored objects
must be encoded in the workspace.
For the actual encoding of the stored objects, they simply ask the ALcyWorkspaceCodec
to store a reference to the stored object. The ALcyWorkspaceCodec
delegates the encoding of the stored object to the ALcyWorkspaceObjectCodec
instances.
Role of the ALcyWorkspaceObjectCodec
The ALcyWorkspaceObjectcodec
is responsible for encoding the state of a stored object during workspace encoding. While it is encoding objects, an object
codec can request and store extra object references.
During workspace loading, the codec must create new object instances, and reset the state of those new instances to match the state that was encoded in the workspace.
Example: saving the state of a Lucy map manager
Let us illustrate this with a part of the example of Figure 1, “Bolded objects below the line are re-created based on object references. Grayed out objects and connectors are
not stored when a workspace is saved.”.
To save the state of the map manager, we register an ALcyWorkspaceCodecDelegate
.
This ALcyWorkspaceCodecDelegate
will store a reference to each of its maps, Map1 and Map2, in its encode
method.
To do so, the codec delegate asks the ALcyWorkspaceCodec
to encode a reference to those maps, because the map is a stored object that is re-created each time a workspace is loaded.
public void encode(ALcyWorkspaceCodec aWSCodec, OutputStream aOut) throws IOException { TLcyLspMapManager mm = fLucyEnv.getService( TLcyLspMapManager.class ); ArrayList<String> tokens = new ArrayList<>(); for ( int i = 0; i < mm.getMapComponentCount(); i++ ) { ILcyGenericMapComponent<ILspView, ILspLayer> map = mm.getMapComponent(i); //ask the ALcyWorkspaceCodec for a reference for the map, and store that reference String mapToken = aWSCodec.encodeReference(map); tokens.add(mapToken); } // Store tokens to aOut }
If the ALcyWorkspaceCodec
has already stored the map, it just returns the reference. If the map was not stored yet, the workspace codec searches for
the ALcyWorkspaceObjectCodec
which indicates it can save the map,
and lets that ALcyWorkspaceObjectCodec
encode the state of the map.
The ALcyWorkspaceCodec
keeps track of the encoded information and couples it to a reference, so that it can return the same reference the next time
an object asks to encode the map.
The ALcyWorkspaceObjectCodec
for the map will, among other things, save a reference to the view
public void encodeObject(ALcyWorkspaceCodec aWSCodec, Object aObject, Object aParent, OutputStream aOut) throws IOException { ILcyGenericMapComponent<ILspView, ILspLayer> map = (ILcyGenericMapComponent<ILspView, ILspLayer>) aObject; // cast is OK as canEncodeObject verified aObject //ask the ALcyWorkspaceCodec for a reference for the view, and store that reference String token = aWSCodec.encodeReference(map.getMainView()); //Write token to aOut }
In turn, the ALcyWorkspaceObjectCodec
for the view will save references to the layers, and so on.
The saving sequence continues until an ALcyWorkspaceObjectCodec
no longer needs to store references to other stored objects.
See Example of an object codec for an example of a full-blown ALcyWorkspaceObjectCodec
.
The TLcyWorkspaceManager
The TLcyWorkspaceManager
has multiple responsibilities:
-
It is the place where all available
ALcyWorkspaceCodecDelegate
andALcyWorkspaceObjectCodec
instances must be registered. -
It provides the API to encode or decode a workspace programmatically.
-
It allows you to attach listeners that are informed when workspace decoding and encoding starts or ends.
Destroying stored objects before workspace decoding
Before a workspace is decoded, the stored objects in the current Java object graph must be destroyed and removed from the
graph.
The ALcyWorkspaceCodec
does not take care of this, nor do the ALcyWorkspaceCodecDelegate
instances.
If you need to destroy stored objects, attach a listener to the TLcyWorkspaceManager
, which performs such a clean-up when
workspace decoding starts.
In the example of the map manager from Figure 1, “Bolded objects below the line are re-created based on object references. Grayed out objects and connectors are
not stored when a workspace is saved.”, the existing maps must be closed and removed when a new workspace is loaded. Therefore, the map add-on will not only register
an ALcyWorkspaceCodecDelegate
to encode and decode the state of the map manager, but it will also add a listener to the workspace manager to clean up any
existing maps when workspace decoding starts:
@Override public void workspaceStatusChanged(TLcyWorkspaceManagerEvent aEvent) { if (aEvent.getID() == TLcyWorkspaceManagerEvent.STARTING_WORKSPACE_DECODING) { //close all the existing maps before decoding a workspace closeExistingMaps(); } }
Threading principles for workspace management
The following threading principles apply to workspace encoding and decoding:
-
You can decode and encode a workspace on any thread. Typically, you would use a worker thread for that, but you can also use the Event Dispatch thread (EDT), also known as the Swing thread. The benefit of using a worker thread is that you can then use the EDT to to show a progress indication to the user.
-
All operations that touch the UI, views, layers, or models, must happen on the EDT. One exception is model reading, which can also happen on a worker thread. This means that you will frequently use
TLcdAWTUtil.invokeAndWait
to defer parts of the codec implementations from the worker thread to the EDT. -
Workspace management is a sequential process: workspace saving and loading operations must never occur concurrently.
More concretely, this means that all operations in the
encode
anddecode
methods of theALcyWorkspaceCodecDelegate
instances andALcyWorkspaceObjectCodec
instances must happen on the thread from which the method was called.The only exceptions to this rule are operations on the EDT: they are allowed if you use a
TLcdAWTUtil.invokeAndWait
call.
The principles in the list comply with the basic LuciadLightspeed threading guidelines. For more information, see Threads and locks.
All changes to the user interface, such as setting the active state of a checkbox, or adding a menu to the user interface, must happen on the EDT. If you do not respect that rule, and add a menu item to a menu bar on the workspace thread, for instance, you can experience seemingly random errors. Such an error may involve menu items not being added to menu bars for no apparent reason.
If you see those kinds of errors during workspace decoding, there is a reasonable chance that a threading issue is the cause.
Use the method TLcdAWTUtil.invokeAndWait
when you alter the user interface during workspace decoding. The method TLcdAWTUtil.invokeAndWait
queues the event on the Event Dispatch Thread and blocks the calling thread until the event is handled.
The Lucy debug add-on has a setting that can help you detect threading errors. To activate the Lucy
debug add-on, start the LucyDebug
script file. Go to the Debug menu, and select Check Swing Thread Violations. You can read more about the debug add-on in the class javadoc of the TLcyDebugAddOn
.
The class Javadoc also illustrates how you enable this add-on from inside your IDE.
Providing workspace support for your own add-ons
For basic Lucy development, such as adding a new data format, storing some preferences, or adding your own application pane to create a UI panel, you can make use of high-level utility classes in the Lucy API. Those utility classes will help you provide workspace support, for example:
-
ALcyGeneralFormat
for storing models -
ALcyLspStyleFormat
for storing layers of a custom data format -
TLcyPreferencesTool
for storing preferences -
ALcyApplicationPaneTool
for adding application panes
For more information about saving and restoring preferences, see Saving and restoring of general settings and preferences.
If you are developing add-ons that go beyond such basic functionality, you do need to take additional steps to ensure that their state is saved in a workspace. In practice, you typically only have to set up workspace management if you are working with GXY layers. In GXY layers there is not much API standardization for map painting and styling. This makes GXY layers less suitable for the automated persistence of properties. Real-time data layers, which integrate with the Lucy previewer, also offer little standardization. In Lightspeed layers, on the other hand, painting and styling are tightly coupled to a styling API, and properties are typically persisted automatically.
When do you need to encode your objects?
You may need to register your own object codecs with the workspace manager. If you developed an add-on that is responsible
for creating an object, such as a model, layer, view, map, or application pane, you also need to provide an ALcyWorkspaceObjectCodec
for that object. In addition, you may also have to reference certain other objects.
You can often manage without registering explicit codecs, and re-use existing codecs instead. Suppose your UI contains a combo
box and you want to make the currently selected value persistent. For such a case, you can simply store the value with the
preferences at all times, and get workspace support for free. |
Each add-on has its own internal state which it needs to encode. For instance, to save its state, the TLcyLspMapAddOn
registers a Lightspeed map manager TLcyLspMapManager
. Among other things, the map manager encodes the existing Lightspeed map component objects in the workspace object graph.
The map components in turn encode the layers existing in those map components. This means that it needs to encode references
to layers, which in turn means that there should be ALcyWorkspaceObjectCodec
instances that can encode the state of these layers. For example:
-
The object codec for a map is provided by the map add-on.
-
The object codec for a particular type of model is provided by the same add-on as the one that registers the respective model decoder.
This means that you may need to provide object codecs that can encode and restore the state of the objects created by your custom add-ons. Aside from that, you may also have to reference certain other objects. Keep in mind that for the storage of many object values, it is often sufficient to re-use existing codecs instead of explicitly registering new codecs. Some of the pre-defined Lucy add-ons already provide object codecs for those objects.
Carefully consider before you decide to register |
Encoding and decoding data paths
In Lucy workspace management it is possible to encode paths, to data files for instance. This is useful because it means that you do not have to save the actual data to the workspace file. Saving all the data could result in enormous workspace files. Instead, you can save the path to the data and later re-load the data from that path. The way you save these paths has an impact on the portability of your workspace.
You can use ALcyWorkspaceCodecDelegate
instances and ALcyWorkspaceObjectCodec
instances to let the workspace codec encode a data path relative to the location of the workspace file. The implementation
of ALcyWorkspaceCodec
determines in what way these paths are encoded. The default Lucy implementation TLcyFileWorkspaceCodec
can encode paths using relative paths or absolute paths, with the following methods.
-
setUseRelativePaths
: specifies whether theTLcyFileWorkspaceCodec
encodes relative paths or not. By default, this property is set totrue
, and the codec only encodes paths as relative paths if they reside within the directory where the workspace is saved. -
setAllowUpRelativePaths
: if this property is set to true, the codec will try to encode all paths as relative paths, even when these paths do not point to a location within the directory where the workspace file is saved. By default, this property is set tofalse
.
How to create workspaces that can be copied or moved
Using relative paths, you can create workspaces that you can move to another position or to another computer. To do so, save the workspace file in the same folder as the data files, or one or more folder levels above. Now you can copy the workspace file as well as the data files to another location, as long as you respect their relative file structure.
Saving and restoring of general settings and preferences
General settings consist of simple properties in the form of String
instances, numbers, and so on. For instance, the default front-end of Lucy can encode the relative position of the split
panes. In general, Lucy already allows those kinds of settings to be stored as Lucy preferences, and automatically provides
workspace support for stored preferences.
To store a general setting or preference, simply use one of the support tools in the Lucy API for properties and preferences.
TLcyPreferencesTool.getWorkspacePreferences
, for example, is available for most add-ons. If you want to store a setting bound to a certain map instance, you can use
ILcyLspMapComponent.getProperties
.
Packaging a workspace in a JAR file
The TLcyWorkspaceAddon
loads a default workspace when Lucy starts, but you may have set up a customized workspace for your Lucy application. When
you deploy your application, using Java Web Start for instance, you might find it useful to package your custom workspace
in a JAR file, and deploy it together with the application. When users start the application, you can let the application
load your custom workspace automatically.
The TLcyFileWorkspaceCodec
is able to read workspace files that are located within JAR files. To create a workspace in a jar file:
-
Save the workspace using relative paths.
-
Ensure that all data files referenced in the workspace file are located within the directory where you save the workspace file.
-
Put the directory where you have saved the workspace file in a JAR file and put this JAR file in the classpath, for example, by putting it in the
lib
folder. -
Specify the file name of the workspace file in the preference file
config/lucy/workspace/workspace_addon.cfg
.
The TLcyFileWorkspaceCodec
will now load the workspace in the JAR file as the initial workspace.
Ensuring backward compatibility
Objects store their data as a bunch of key-value pairs, TLcyStringProperties
for example. This is simple and allows for evolution. For example, if a newer version of Lucy needs to store an additional
property, it can simply add the property to the workspace. If Lucy decodes old workspaces in which that property did not exist
yet, it will use a default property value. Old versions of Lucy reading a newer workspace file simply ignore the unknown property.
Writing robust workspace codecs
It is always possible that something goes wrong during workspace decoding.
A typical example is the decoding of an ILcdModel
.
Suppose that only the source name of the model was stored during workspace encoding.
In normal circumstances, that is sufficient to restore the model during workspace decoding: the codec retrieves the
stored source name, and uses an ILcdModelDecoder
to restore the model.
However, by the time the workspace gets decoded, the source file may no longer be available on the file system.
As a consequence, the codec cannot restore the model.
The codec can deal with such a problem in two ways:
-
Throw an exception: when the model decoder cannot find the source file, it throws an exception. The codec can opt to just let this exception roll upwards.
A codec exception signals to the workspace mechanism to abort workspace loading, because the loading mechanism entered an invalid state from which it could not recover. Typically, this is not what you want.
-
Log a warning, and return
null
: when the model decoder cannot find the source file, it throws an exception. The codec can catch that exception, and opt to log a warning throughALcyWorkspaceCodec.getLogListener
. Because the model could not be decoded, the codec returnsnull
.The main benefit of this approach is that workspace decoding continues, although not all data will be restored. To inform the user, all the warning messages logged to the log listener are presented to the user once workspace loading has finished.
In most cases, this is the preferred approach.
Note that this also means that each time your codec asks the ALcyWorkspaceCodec
to decode a reference using the
ALcyWorkspaceCodec.decodeReference
method, it should handle the situation in which the reference could not be decoded and null
is returned.
For an illustration, consider an ALcyWorkspaceObjectCodec
for an ILcdLayer
.
That object codec stores a reference to a model.
During decoding, the model could not be restored, which means that the layer cannot be restored either.
@Override public Object createObject(ALcyWorkspaceCodec aWSCodec, Object aParent, InputStream aIn) throws IOException{ //... String modelReference = ...; // Restore the model from the stored reference ILcdModel model = (ILcdModel)aWSCodec.decodeReference(modelReference); // The model is potentially null if (model != null){ return createLayer( model ); } else { // Do not log a message. // If the model could not be decoded, the model codec probably already logged a message return null; } }
Example of an object codec
This is the example code of a full-blown object codec for a model. It stores the model by storing the source name, and re-creates it from the stored source name.
public class MyModelCodec extends ALcyWorkspaceObjectCodec { private final ILcdModelDecoder fModelDecoder; @Override public String getUID() { return "UniqueStringForThisCodec"; } @Override public boolean canEncodeObject(ALcyWorkspaceCodec aWSCodec, Object aObject, Object aParent) { return (aObject instanceof ILcdModel) && (((ILcdModel) aObject).getModelDescriptor() instanceof MyModelDescriptor); } @Override public void encodeObject(ALcyWorkspaceCodec aWSCodec, Object aObject, Object aParent, OutputStream aOut) throws IOException { // Cast is OK as canEncodeObject verified aObject ILcdModel model = (ILcdModel) aObject; // Retrieve the source name from the model String sourceName; try (TLcdLockUtil.Lock ignore = TLcdLockUtil.readLock(model)) { sourceName = model.getModelDescriptor().getSourceName(); } // Makes the file name relative to the workspace file String encodedPath = aWSCodec.encodePath(sourceName); // Store to stream TLcyStringProperties props = new TLcyStringProperties(); props.put("sourceName", encodedPath); new TLcyStringPropertiesCodec().encode(props, aOut); } @Override public Object createObject(ALcyWorkspaceCodec aWSCodec, Object aParent, InputStream aIn) throws IOException { // Read from stream ALcyProperties props = new TLcyStringPropertiesCodec().decode(aIn); String encodedPath = props.getString("sourceName", null); String sourceName = aWSCodec.decodePath(encodedPath); //Create the model by decoding it from the source. //If this fails, we only log a warning and return null. The alternative is to let the exception roll upwards. //That is a valid alternative, but has a different meaning: abort loading the workspace, we can't recover. //Logging a warning means workspace decoding can continue. try { return fModelDecoder.decode(sourceName); } catch (IOException e) { aWSCodec.getLogListener().warn("Failed to data from " + sourceName); return null; } } @Override public void decodeObject(ALcyWorkspaceCodec aWSCodec, Object aObject, Object aParent, InputStream aIn) throws IOException { // Nothing else to do, object is already fully initialized } }