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.

Table of Contents

1 Understanding Views and Their Types
2 Opening and Closing Views
3 Thread Safety
3.1 Using Critical Sections
3.2 Using a Delegable Lock
4 Understanding the CDO File System
4.1 The Root Resource
4.2 Resource Folders
4.3 Model Resources
4.4 Binary Resources
4.5 Text Resources
5 Resource Sets and Their Usage
6 Navigating Models
7 Waiting For Updates
8 Querying Resources
9 Querying Model Objects
10 Querying Cross References
11 Custom Queries
12 Units
13 View Events
14 View Options
15 View Properties

1  Understanding Views and Their Types

A view binds one client-side CDOView and its objects to a session, a resource set, and a branch point. Use a read-only view for navigation, queries, or a stable historical target; use a CDOTransaction when the operation must stage local edits and commit them. A normal head view can receive remote invalidations as repository commits arrive, so it is not a frozen snapshot. Audit or historical views read an explicit branch/time point and are read-only. Objects are view-scoped: reopen by persistent ID in another view instead of carrying model objects across view lifetimes.

OpenReadOnlyView.java      
CDOView view = session.openView();
try
{
  System.out.println("Opened view with ID: " + view.getViewID());
}
finally
{
  view.close();
}

shows a short-lived read; the transaction example

ModifyAndCommit.java      
CDOTransaction transaction = session.openTransaction();

try
{
  CDOObject object = transaction.getObject(objectID);
  if (!object.eClass().getEAllStructuralFeatures().contains(feature))
  {
    throw new IllegalArgumentException("Feature does not belong to the object's EClass");
  }

  object.eSet(feature, value);
  transaction.commit();
}
catch (CommitException | RuntimeException ex)
{
  transaction.rollback();
  throw ex;
}
finally
{
  transaction.close();
}

shows an editable view and its commit boundary.

2  Opening and Closing Views

Open views from a session with CDOSession.openView() for the current branch head, or choose an explicit branch point through the session's view-opener overloads. Open a transaction when the operation must modify data; a historical view is read-only. The session tracks its open views, their resource sets, caches, listeners, and remote view state, so close each view in a finally block or try-with-resources pattern when the work ends. Closing a view releases those resources and ends the validity of its model-object context. It does not close the session, which may create later views. Avoid sharing a single long-lived view across unrelated operations just to avoid reopening it; view lifetime should match the consistency and object-identity scope the application needs.

OpenReadOnlyView.java      
CDOView view = session.openView();
try
{
  System.out.println("Opened view with ID: " + view.getViewID());
}
finally
{
  view.close();
}

and

ModifyAndCommit.java      
CDOTransaction transaction = session.openTransaction();

try
{
  CDOObject object = transaction.getObject(objectID);
  if (!object.eClass().getEAllStructuralFeatures().contains(feature))
  {
    throw new IllegalArgumentException("Feature does not belong to the object's EClass");
  }

  object.eSet(feature, value);
  transaction.commit();
}
catch (CommitException | RuntimeException ex)
{
  transaction.rollback();
  throw ex;
}
finally
{
  transaction.close();
}

shows the transaction cleanup pattern.

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().

Each individual access to a view or one of its objects is synchronized with the view's internal work. This does not make a sequence of separate calls atomic: an invalidation may occur between calls. Use CDOView.sync() around the smallest multi-call operation that must observe one coherent state. Keep that critical section short; do not wait for network work, block on another thread, or call arbitrary application callbacks while holding it.

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:

CriticalSectionWithCallable.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:

A view's critical section uses a real reentrant lock; the current default is a non-fair reentrant lock. A lock supplied with CDOUtil.setNextViewLock(Lock) or the session's delegable-lock option is used when configured. Synchronizing directly on the view object is unsupported. By default, CDO detects this when a view lock is next entered and throws UnsupportedOperationException, directing the caller to CDOView.sync(). Set -Dorg.eclipse.emf.cdo.view.DISABLE_INTRINSIC_MONITOR_CHECK=true to disable that safety check.

Deprecated monitor and lock methods fail fast by default. Set -Dorg.eclipse.emf.cdo.view.ENABLE_LEGACY_LOCKING_API=true to re-enable their best-effort behavior. The compatibility monitor returned by getViewMonitor() coordinates only callers that synchronize on that returned object; it does not coordinate by itself with CDO's internal view lock. The deprecated lock methods use the real view lock when enabled.

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 = null;
try
{
  view = session.openView();
  CriticalSection sync = view.sync();

  if (!(sync instanceof LockedCriticalSection) || ((LockedCriticalSection)sync).getLock() != customLock)
  {
    throw new IllegalStateException("The configured lock was not installed");
  }
}
finally
{
  if (view != null)
  {
    view.close();
  }

  CDOUtil.setNextViewLock(null);
}

A DelegableReentrantLock is useful when a framework synchronously hands work to another thread that must access the same view, such as an SWT UI callback. It solves that specific lock-ownership handoff; it is not a reason to hold the critical section across arbitrary blocking work. Register the relevant delegate detector and enable the session option or install the lock before opening the view.

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().request().getRevision(rootResourceID, mainBranch.getHead());
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

The CDO resource hierarchy is a repository-side virtual file system. A CDOResourceFolder is a persistent model object that contains resource nodes; those nodes may be folders, model resources, binary resources, or text resources. Use folders when resource paths need meaningful grouping and use the node APIs to enumerate children without loading every model's contents. Folder creation is a transaction change and becomes visible to other views only after commit.

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

A CDOResource is an EMF resource backed by a CDO view. Its root objects and resource metadata participate in the view's model and notification behavior; creating a resource or changing its contents requires a CDOTransaction. Loading it in a read-only view gives access to repository state through that view's ResourceSet. Adding a resource to an ordinary EMF ResourceSet does not persist it in CDO; use the CDO transaction's resource-creation APIs and commit the transaction to publish the change.

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[] { 0, 1, 2, 3, 4, 5 });
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

A view is associated with a ResourceSet; resources loaded through it contain objects owned by that view. Keep the resource set with the view that created it and close the view when the application is done. Do not move CDO resources or their objects to another resource set and assume their view context changes with them. A ResourceSet can also contain ordinary EMF resources, but their persistence and lifecycle remain the responsibility of their own resource implementation. See Integrating with EMF and Other Frameworks for URI-based loading and resource factories.

6  Navigating Models

Use ordinary EMF navigation for a known, bounded part of the graph. References may be proxies, so reading one can trigger additional loading. A broad walk can therefore cause many round trips and retain many objects. When the desired result is defined by a type or predicate over repository contents, prefer a repository query and load only the returned objects. Choose deliberately between traversal and query; neither is always cheaper.

7  Waiting For Updates

Passive updates advance or invalidate a live head view according to its session update mode. An application that needs to wait for such updates can use the view/session update-waiting API; a fixed-time historical view does not move forward. Waiting should happen outside a view critical section, because update processing may need that same synchronization boundary. Use view or session events for notification rather than polling model values.

8  Querying Resources

Queries are created from the view and executed by the repository. Use a query when selecting candidate resources is cheaper than loading and traversing a broad resource tree; result limits and asynchronous consumption are covered in Queries and Large-Scale Model Access.

9  Querying Model Objects

A query runs against the view's repository coordinate and returns objects associated with that view. Select a language supported by both the client and server, bind parameters instead of building expressions from untrusted strings, and close asynchronous result iterators. For a transaction, query options determine whether local dirty state participates; this does not turn a repository query into arbitrary in-memory graph evaluation. OCL is an optional integration, not a universally available language.

10  Querying Cross References

queryXRefs asks the repository for objects whose selected source references point at one or more target objects. This is useful when reverse navigation is not already loaded locally. Restrict the source reference set where possible, and use the asynchronous iterator for large results so the whole result list need not be retained at once. The iterator is a resource and must be closed.

11  Custom Queries

A custom query language is only usable when the server has a matching server query handler registered and the client supplies the language identifier and parameters that handler expects. It is not enough to install a parser on the client. Handler registration and result delivery are covered in Query Handlers.

12  Units

A unit is a repository-defined subtree that the view can treat as a bounded working set. Units can help an application load and manage a coherent part of a large model, but they do not make unrelated object access free or replace transaction boundaries. Unit membership and lifecycle must follow the repository's unit-manager support; close units and views according to the APIs that opened them.

13  View Events

View events report lifecycle and target/update changes for that view. Register listeners only for the period in which the application needs them and remove them when the owning component is disposed. Event callbacks may run on a CDO-managed thread; capture the data needed by the UI and dispatch UI work to its thread instead of blocking the callback. See Notifications and Event Handling for event categories and threading guidance.

14  View Options

View options control view-local behavior such as invalidation, object-cache references, adapters, and locking or notification details. Configure options before relying on the resulting policy, and distinguish them from session options that apply to all views opened by that session. A view option cannot enable a repository feature that the server/store does not provide.

15  View Properties

View properties are an application-owned key/value area associated with a view. They are useful for associating client-side metadata with that view; they are not persisted model features, are not shared with the repository, and should not be used to smuggle objects between views. Remove values that retain large application objects when they are no longer needed.