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 TLcyWorkspaceAddon explains how to achieve this.

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.

workspaceobjectstorage
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.

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:

workspaceEncoding
Figure 2. Workspace encoding sequence diagram

When the TLcyWorkspaceManager calls ALcyWorkspaceCodec.encodeWorkspace:

  1. The ALcyWorkspaceCodec iterates over all ALcyWorkspaceCodecDelegate instances, and calls their encode method

  2. The ALcyWorkspaceCodecDelegate instances store properties that describe state of permanent objects.

  3. 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, the ALcyWorkspaceCodecDelegate 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:

    1. 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 a ALcyWorkspaceObjectCodec 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.

    2. 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, the ALcyWorkspaceCodec selects the proper ALcyWorkspaceObjectCodec and asks it to store the necessary information into the workspace. The ALcyWorkspaceCodec keeps a reference to the location of the information in the workspace, and returns that reference to the ALcyWorkspaceCodecDelegate. If the object was already stored, the ALcyWorkspaceCodec looks up the reference, and returns that to the ALcyWorkspaceCodecDelegate.

      It is that reference that the ALcyWorkspaceCodecDelegate will save into the workspace.

  4. 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 the ALcyWorkspaceCodecDelegate.

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

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.

Program: A codec delegate encodes a workspace with the ALcyWorkspaceCodec
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

Program: An object codec saves a reference to a 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 and ALcyWorkspaceObjectCodec 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:

Program: Cleaning up maps when a workspace is decoded
  @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 and decode methods of the ALcyWorkspaceCodecDelegate instances and ALcyWorkspaceObjectCodec 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:

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. TLcyPreferencesTool.getWorkspacePreferences is available for most add-ons, for instance, or ILcyLspMapComponent.getProperties if the setting is bound to a certain map instance.

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 ALcyWorkspaceObjectCodec instances for new object types other than maps, views, layers, models or application panes. If you add object types that are not typically part of the workspace object graph, and especially if those objects are not part of the API, it is very likely you are encoding implementation details that should not be encoded in the first place.

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 the TLcyFileWorkspaceCodec encodes relative paths or not. By default, this property is set to true, 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 to false.

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 through ALcyWorkspaceCodec.getLogListener. Because the model could not be decoded, the codec returns null.

    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.

Program: A robust layer codec that can handle a missing model.
@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.

Program: Object codec example
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
  }
}