Working with Views

 

Author: Eike Stepper

This chapter covers view management, resource handling, querying, transactions, and related options in CDO client applications. Views are central to accessing and interacting with model data in a CDO repository. Understanding how to use views effectively is key to building responsive and scalable applications.

1  Understanding Views and Their Types

CDO provides several types of views, including read-only views and transactional views. Read-only views allow safe navigation of repository data without risk of modification, while transactional views enable changes and commits. This section explains the differences, use cases, and lifecycle of each view type.

2  Opening and Closing Views

Learn how to open views to access repository data and close them to release resources. Proper management of view lifecycles helps prevent memory leaks and ensures efficient resource usage.

3  Thread Safety

Views in CDO are inherently thread-safe, but this guarantee applies only to individual method calls. When performing multiple operations that need to be atomic or consistent, developers must use a CriticalSection to synchronize access to the view. A CDO view provides its critical section via the CDOView.sync().

Thread safety of CDO views is absolutely essential, even in single-threaded applications. The reason is that CDO views are accessed by background threads for tasks such as asynchronous updates, notifications, and event handling. If a view were not thread-safe, these background operations could lead to data corruption, inconsistent states, and unpredictable behavior in the application. By ensuring that views are thread-safe, CDO allows developers to build robust applications that can safely handle concurrent operations without risking data integrity.

This section discusses best practices for managing concurrent access, synchronizing operations, and avoiding race conditions when working with views in multi-threaded environments.

3.1  Using Critical Sections

To ensure thread-safe access to a CDO view when performing multiple operations, use the view's critical section object. It is returned by the CDOView.sync() method. The critical section provides methods to execute code blocks safely, such as CriticalSection.run(Runnable) and CriticalSection.call(Callable).

Here is an example of using a critical section with a callable to access multiple objects in a view atomically:

CrititicalSectionWithCallable.java      
CriticalSection sync = view.sync();

MyResult result = sync.call(() -> {
  // Access the view and its objects safely here.
  CDOObject object1 = view.getObject(id1);
  CDOObject object2 = view.getObject(id2);
  CDOObject object3 = view.getObject(id3);

  // Return a result object.
  return new MyResult();
});

The CriticalSection interface provides the following methods:

By default the critical section of a view uses the monitor lock of that view to synchronize. If you need a different locking strategy you can override this by calling CDOUtil.setNextViewLock(Lock) before opening the view.

Here's an example of setting a custom lock for the next view to be opened:

CustomLockForNextView.java      
Lock customLock = new ReentrantLock();
CDOUtil.setNextViewLock(customLock);

CDOView view = session.openView();
CriticalSection sync = view.sync();

if (sync instanceof LockedCriticalSection)
{
  Lock lock = ((LockedCriticalSection)sync).getLock();
  assert lock == customLock;
}
else
{
  throw new IllegalStateException();
}

The following chapter describes how to use a special kind of lock, a DelegableReentrantLock, that allows to delegate the lock ownership to a different thread.

3.2  Using a Delegable Lock

As an alternative to the default locking strategy of a view's critical section, you can use a DelegableReentrantLock, which allows to delegate the lock ownership to a different thread. This is useful in scenarios where you need to hold the lock while waiting for an asynchronous operation to complete in a different thread.

A typical example is the Display.syncExec() method in SWT/JFace UI applications. With the default locking strategy this can lead to deadlocks:

  1. Thread A (not the UI thread) holds the view lock and calls Display.syncExec() to execute some code in the UI thread.
  2. The Runnable passed to syncExec() is scheduled for execution in the UI thread. Thread A waits for the Runnable to complete.
  3. The UI thread executes the Runnable, which tries to access the view or an object of the view. This requires the view lock, which is already held by thread A.
  4. Deadlock: Thread A waits for the Runnable to complete and the UI thread waits for the view lock to be released.

Here is an example that illustrates this scenario:

DeadlockExample.java      
CDOObject object = view.getObject(id);
object.eAdapters().add(new AdapterImpl()
{
  @Override
  public void notifyChanged(Notification msg)
  {
    // This code is executed in a non-UI thread and holds the view lock.

    // The following call to Display.syncExec() will execute the Runnable in the UI thread
    // and make the current thread wait for it to complete. During that time the view lock
    // is still held by the current thread.
    Display.getDefault().syncExec(() -> {
      // This code is executed in the UI thread.
      // It tries to access the view while the view lock is held by the non-UI thread.
      // The result is a deadlock.
      CDOResource resource = view.getResource("/my/resource");
    });
  }
});

Note that, in this scenario, Thread A is holding the view lock while waiting for the Runnable to complete. This is kind of an anti-pattern, because it blocks other threads from accessing the view for an indeterminate amount of time. In addition, it is not necessary to hold the view lock while waiting for the Runnable to complete, because Thread A can not access the view in that time.

A DelegableReentrantLock can be used to avoid the deadlock. It uses so called lock delegation to temporarily transfer the ownership of the lock to a different thread. In the scenario described above, Thread A can delegate the lock ownership to the UI thread while waiting for the Runnable to complete. When the Runnable completes, the lock ownership is transferred back to Thread A. This way, the UI thread can access the view while executing the Runnable and no deadlock occurs.

DelegableReentrantLock uses DelegateDetectors to determine whether the current thread is allowed to delegate the lock ownership to a different thread. A DelegateDetector can be registered with the lock by calling DelegableReentrantLock.addDelegateDetector(DelegateDetector). The org.eclipse.net4j.util.ui plugin provides a DisplayDelegateDetector for the SWT/JFace UI thread that detects calls to Display.syncExec().

There are two ways to use a DelegableReentrantLock with a CDO view:

  1. Set the lock as the next view lock by calling CDOUtil.setNextViewLock(Lock) before opening the view. This way, the view will use the lock for its critical section. Here's an example:

    IndividualViewLock.java      
    CDOUtil.setNextViewLock(new DelegableReentrantLock());

    CDOView view = session.openView();
    CriticalSection sync = view.sync();

    // Acquire the view lock.
    sync.run(() -> {
      // This code is executed in a non-UI thread and holds the view lock.

      // The following call to Display.syncExec() will execute the Runnable in the UI thread
      Display.getDefault().syncExec(() -> {
        // This code is executed in the UI thread.
        // It can access the view because the lock ownership has been delegated to the UI thread.
        CDOResource resource = view.getResource("/my/resource");
        System.out.println("Resource: " + resource.getURI());
      });
    });

  2. Set delegableViewLockEnabled to true on the session. This way, all views opened from the session will use a DelegableReentrantLock for their critical section. The lock is created automatically and configured with all DelegateDetectors that are registered. Example:

    SetDelegableViewLockEnabled.java      
    session.options().setDelegableViewLockEnabled(true);

    CDOView view = session.openView();
    CriticalSection sync = view.sync();

    // Acquire the view lock.
    sync.run(() -> {
      // This code is executed in a non-UI thread and holds the view lock.

      // The following call to Display.syncExec() will execute the Runnable in the UI thread
      Display.getDefault().syncExec(() -> {
        // This code is executed in the UI thread.
        // It can access the view because the lock ownership has been delegated to the UI thread.
        CDOResource resource = view.getResource("/my/resource");
        System.out.println("Resource: " + resource.getURI());
      });
    });

4  Understanding the CDO File System

The CDO repository exposes a virtual file system for organizing model resources. This section describes the structure and usage of the file system, including root resources, folders, and different resource types.

4.1  The Root Resource

The root resource is the entry point to the CDO file system. It contains all top-level folders and resources, providing a hierarchical view of the repository's contents.

Each CDO view or transaction can provide the root resource. Here is an example of how to access and list the contents of the root resource:

GetRootResource.java      
// Each CDO view or transaction can provide the root resource.
CDOResource rootResource = view.getRootResource();

for (EObject content : rootResource.getContents())
{
  if (content instanceof CDOResourceFolder)
  {
    CDOResourceFolder folder = (CDOResourceFolder)content;
    System.out.println("Folder: " + folder.getName());
  }
  else if (content instanceof CDOResource)
  {
    CDOResource resource = (CDOResource)content;
    System.out.println("Model Resource: " + resource.getName());
  }
  else if (content instanceof CDOBinaryResource)
  {
    CDOBinaryResource binary = (CDOBinaryResource)content;
    System.out.println("Binary File: " + binary.getName());
  }
  else if (content instanceof CDOTextResource)
  {
    CDOTextResource text = (CDOTextResource)content;
    System.out.println("Text File: " + text.getName());
  }
}

When you don't have a view or transaction available, you can ask a session for the root resource's ID as follows:

GetRootResourceID.java      
CDOID rootResourceID = session.getRepositoryInfo().getRootResourceID();
CDOBranch mainBranch = session.getBranchManager().getMainBranch();

CDORevision rootResourceRevision = session.getRevisionManager(). //
    getRevision(rootResourceID, mainBranch.getHead(), //
        CDORevision.UNCHUNKED, CDORevision.DEPTH_NONE, true);
System.out.println("Root Resource Revision: " + rootResourceRevision);

The root resource of a repository is created automatically when the repository is initialized for the first time. It can not be deleted, but its contents can be modified like any other resource.

4.2  Resource Folders

Resource folders organize model resources into logical groups. Learn how to create, navigate, and manage folders to structure your repository effectively.

For creating resource folders you need a CDOTransaction. Here is an example that illustrates how to create folders and subfolders:

CreateFolder.java      
// Create a new folder at the specified path directly in the transaction.
CDOResourceFolder folder = transaction.createResourceFolder("/my/new/folder");
System.out.println("Created folder: " + folder.getPath()); // Outputs: /my/new/folder

CDOResourceFolder parent = folder.getFolder();
System.out.println("Parent folder: " + parent.getPath()); // Outputs: /my/new

CDOResourceFolder parent2 = parent.getFolder();
System.out.println("Parent parent folder: " + parent2.getPath()); // Outputs: /my

// Create a subfolder within the newly created folder.
CDOResourceFolder subfolder = folder.addResourceFolder("subfolder");
System.out.println("Created subfolder: " + subfolder.getPath()); // Outputs: /my/new/folder/subfolder

// None of the above changes are visible in other views/transactions.
// They become visible only after committing the transaction.
transaction.commit();

Listing subnodes (folders and resources) within a folder does not require a transaction and can be done as follows:

ListSubNodes.java      
// List all nodes (folders and resources) within the folder.
for (CDOResourceNode node : folder.getNodes())
{
  System.out.println("Node: " + node.getName());
}

// Access a specific subnode by name.
CDOResourceNode subnode = folder.getNode("subfolder"); // May return null.
if (subnode instanceof CDOResourceFolder)
{
  CDOResourceFolder subfolder = (CDOResourceFolder)subnode;
  System.out.println("Subfolder path: " + subfolder.getPath());
}

Note that resource folders are model objects and therefore support EMF features such as adapters, notifications, and so on.

4.3  Model Resources

Model resources store EMF model data in the repository. This section explains how to load, save, and query model resources, and how they relate to EMF ResourceSet.

For creating model resources you need a CDOTransaction. Here is an example that illustrates how to create model resources:

CreateResource.java      
// Create a new folder at the specified path directly in the transaction.
CDOResource resource = transaction.createResource("/my/new/resource");
System.out.println("Created resource: " + resource.getPath()); // Outputs: /my/new/resource

// Your method to create the root object.
EObject rootObject = createContentTree();

// Add the root object to the resource's contents.
resource.getContents().add(rootObject);

// CDO resources support multiple root objects.
resource.getContents().add(EcoreUtil.copy(rootObject));

// None of the above changes are visible in other views/transactions.
// They become visible only after committing the transaction.
transaction.commit();

Listing root objects and contained objects within a model resource does not require a transaction and can be done as follows:

GetContents.java      
// List root objects in the resource.
for (EObject rootObject : resource.getContents())
{
  System.out.println("Root object: " + rootObject);
}

// Iterate over all contained objects in the resource. Normal EMF model object operation.
TreeIterator<EObject> allContents = resource.eAllContents();
allContents.forEachRemaining(eObject -> System.out.println("Contained object: " + eObject));

Note that model resources are model objects and therefore support EMF features such as adapters, notifications, and so on.

4.4  Binary Resources

Binary resources allow storage of non-model data, such as images or files, within the CDO repository. They are based on CDO's special data type CDOBlob, which supports efficient handling of large binary objects. Large object support is described in detail in the chapter Large Objects.

For creating binary resources you need a CDOTransaction. Here is an example that illustrates how to create binary resources:

CreateBinaryFile.java      
// Create a new binary file at the specified path directly in the transaction.
CDOBinaryResource binary = transaction.createBinaryResource("/my/new/file");
System.out.println("Created binary file: " + binary.getPath()); // Outputs: /my/new/file

CDOBlob blob = transaction.getSession().newBlob(new byte[] { 01234});
binary.setContents(blob);

// None of the above changes are visible in other views/transactions.
// They become visible only after committing the transaction.
transaction.commit();

Getting the binary contents of a binary resource does not require a transaction and can be done as follows:

GetContents.java      
// Get the binary contents.
CDOBlob blob = binary.getContents();

// Print some information about the binary contents.
System.out.println("Binary contents ID: " + blob.getID());
System.out.println("Binary contents size: " + blob.getSize());

// For demonstration purposes, copy the binary contents to System.out.
blob.copyTo(System.out);

try (InputStream stream = blob.getContents())
{
  // Or do something else with the InputStream...
}

Note that binary resources are model objects and therefore support EMF features such as adapters, notifications, and so on.

4.5  Text Resources

Text resources store textual data, such as configuration files or documentation, in the repository. They are based on CDO's special data type CDOClob, which supports efficient handling of large text objects. Large object support is described in detail in the chapter Large Objects.

For creating text resources you need a CDOTransaction. Here is an example that illustrates how to create text resources:

CreateTextFile.java      
// Create a new text file at the specified path directly in the transaction.
CDOTextResource text = transaction.createTextResource("/my/new/text");
System.out.println("Created text file: " + text.getPath()); // Outputs: /my/new/file

CDOClob clob = transaction.getSession().newClob("Hello, CDO Text Resource!");
text.setContents(clob);

// None of the above changes are visible in other views/transactions.
// They become visible only after committing the transaction.
transaction.commit();

Getting the text contents of a text resource does not require a transaction and can be done as follows:

GetContents.java      
// Get the text contents.
CDOClob clob = text.getContents();

// Print some information about the binary contents.
System.out.println("Text contents ID: " + clob.getID());
System.out.println("Text contents size: " + clob.getSize());

// For demonstration purposes, copy the binary contents to System.out.
clob.copyTo(new OutputStreamWriter(System.out));

try (Reader reader = clob.getContents())
{
  // Or do something else with the Reader...
}

Note that text resources are model objects and therefore support EMF features such as adapters, notifications, and so on.

5  Resource Sets and Their Usage

Resource sets are collections of resources managed together. Learn how to use EMF ResourceSet with CDO, manage resource lifecycles, and optimize performance for large models.

6  Navigating Models

This section provides techniques for traversing and querying model objects within views, including use of EMF APIs and CDO-specific features for efficient navigation.

7  Waiting For Updates

Learn how to synchronize your client with repository changes, block for updates, and react to notifications to keep your application state current.

8  Querying Resources

CDO supports querying resources using various criteria. This section explains how to construct and execute queries to locate resources and model objects efficiently.

9  Querying Model Objects

Learn how to query model objects using CDO's query APIs, including support for OCL, custom queries, and cross-references.

10  Querying Cross References

Cross references allow navigation between related model objects. This section covers techniques for querying and resolving cross references in CDO models.

11  Custom Queries

Extend CDO's querying capabilities with custom query implementations. Learn how to define, register, and execute custom queries for advanced use cases.

12  Units

Units are disjunct subtrees of model objects that can be managed independently. This section explains how to create, open, and close units, and how they can improve performance and consistency in your application.

13  View Events

Views emit events for changes, updates, and errors. This section explains how to listen for and handle view events to build responsive applications.

14  View Options

Configure view options to customize behavior, such as passive updates, notification handling, and more.

15  View Properties

Access and modify view properties to store custom metadata and configuration values.