Skip to main content

Data Access Layer

This document covers the client-side data access layer in the SureClinical Desktop application: the endpoint interface hierarchy, the EntityDataSource singleton cache, the ServiceProvider service registry, authentication clients, and helper services that sit between the UI and the remote Nuxeo backend.


Overview

The data layer is split across two modules:

  • suredms-desktop-client-data — pure interfaces and entity types; no implementation or I/O
  • suredms-desktop-client — concrete implementations including EntityDataSource, DocQueueService, and the dependency-injection system
  • suredms-desktop-client-connector — Nuxeo-specific implementations (NuxeoEndPoints, DocumentUpdater, DocumentProxy, SureConnector)

The central abstraction is IEndPoints, which is obtained via the EndPoints factory. All entity data flows through this interface and is aggregated into the EntityDataSource singleton.


Endpoint Interface Hierarchy

All endpoint interfaces live in suredms-desktop-client-data under com.sureclinical.suredms.endpoints.

IEndPoints

The root interface for a connection context:

public interface IEndPoints {
IAuth getAuthSvc() throws RemoteException;
IDocument getDocSvc() throws RemoteException;
EndPointsType getEndPointsType();
void close();
}

EndPoints.getCurrentEndPoints() returns the currently active IEndPoints implementation, which is one of:

  • NuxeoEndPoints — live Nuxeo Automation API connection
  • XMLEndPoints — XML-backed offline store (archive file mode)
  • XmlDemoEndPoints — XML-backed demo/test stub (used in UI integration tests)

IAuth

Authentication service interface:

MethodDescription
getUserInfo()Returns the currently authenticated User
login(username, password)Performs a basic login; returns a session token
login(username, password, extendedLoginInfo)Login with MFA token and/or shared login record
isOnline()Returns true if the connection is live (not XML-backed)
getAuthToken(user, password)Returns a pre-computed auth token for signing

IDocument

Document and entity data service. Extends IDocFileProvider.

Read methods:

MethodReturn type
getArchiveList()Collection<Archive>
getDocList()Collection<Document>
getCategoryList()Collection<Category>
getContentTypeList()Collection<ContentType>
getPersonList()Collection<Person>
getPersonRoleList()Collection<PersonRole>
getOrganizationList()Collection<Organization>
getOrganizationRoleList()Collection<OrganizationRole>
getPropDefsList()Collection<DataPropertyDef>
getAnnotationDefsList()Collection<AnnotationDef>
getDocQItems()Collection<DocQItem>
getDocDiscrepancyTypes()Collection<DocDiscrepancyType>

Control methods:

MethodDescription
resetData(hardReset, internalEvent)Clears the internal entity cache; data reloads on next getXXX call
prefetch(archives)Pre-warms data for a specific set of archives
setFetchMode(FetchMode)Switches between ASYNCHRONOUS and SYNCHRONOUS data loading
addListener(EndPointListener)Registers for onDataUpdated(boolean) callbacks
removeListener(EndPointListener)Deregisters a listener
getIDocument(archive)Returns the IDocument scoped to a single archive
getSignatureInformation(archive)Returns List<X509Certificate> for a signed archive
getCoverSheet(archive)Returns the PDf cover sheet blob and hash

EndPointsType

Enum describing the nature of the active connection:

ValueTitleOnline
EP_REMOTEOnlineYes
EP_LOCALOfflineNo
EP_DEMODemo ModeYes

Each value carries an EndPointParameters instance holding baseURL, username, password, and rootFolder.

FetchMode

Two-value enum:

ValueDescription
ASYNCHRONOUSData is fetched in a background thread
SYNCHRONOUSData is fetched on the calling thread

EndPointListener

public interface EndPointListener {
void onDataUpdated(boolean internalEvent);
}

internalEvent = true means the update was triggered programmatically (e.g., after a metadata save) rather than by a background refresh.

TimedReloadService

public interface TimedReloadService {
void reloadData();
}

Implemented by the background polling component that periodically refreshes entity data from the server.


EntityDataSource — Central Entity Cache

Located in: suredms-desktop-client/src/main/java/com/sureclinical/suredms/dao/EntityDataSource.java

EntityDataSource is the application-wide entity cache. It aggregates all entity collections from IDocument into in-memory maps and lists, and provides a consistent read/write API to the UI layer.

Initialization

EntityDataSource uses double-checked locking for lazy singleton initialization:

public static EntityDataSource getInstance() {
if (instance != null) {
return instance;
}
synchronized (EntityDataSource.class) {
if (instance == null) {
// Callable runs on the calling thread; initialises from IDocument
Callable<EntityDataSource> callable = () -> {
IDocument documentService = EndPoints.getCurrentEndPoints().getDocSvc();
Collection<Archive> archives = documentService.getArchiveList();
Collection<Category> categories = documentService.getCategoryList();
Collection<Document> documents = documentService.getDocList();
Collection<PersonRole> personRoles = documentService.getPersonRoleList();
// ... all other collections ...
return new EntityDataSource(archives, categories, documents, ...);
};
}
}
}

On login, EndPoints initialises the IDocument implementation and EntityDataSource.getInstance() is called from the EDT to load all entity data. Archive list filtered by platform flag (URE-3918).

Internal structure

FieldTypeDescription
allAvailableArchivesList<Archive>All archives visible to the current user
archivesMap<Long, EntityData>Per-archive entity data, keyed by archive ID
entitiesMap<String, BaseEntity>All entities by Nuxeo UUID
reportsList<Document>Report documents
uniqueOrganizationsCacheList<Organization> (nullable)Cached unique org list; invalidated on refresh
userManagerDataCacheUserManagerData (nullable)Cached user management data
REPORT_DOCUMENT_IDSMap<String, List<String>> (concurrent)Report document IDs per archive

Implemented interfaces

InterfacePurpose
DocumentProviderProvides Document lookup by ID/UUID
BaseDataSourceProvides per-archive lists of orgs, persons, roles
EntityAndUserProviderCombines entity and user lookup

BaseDataSource interface

public interface BaseDataSource {
List<Archive> getModifiableArchives();
List<Archive> getWritableArchives();
List<OrganizationRole> getOrganizationRoles(Archive archive);
List<Organization> getOrganizations(Archive archive);
List<Person> getPersons(Archive archive);
List<PersonRole> getPersonRoles(Archive archive);
}

EntityDataSource implements this by filtering the archive-scoped entity maps.

StaticDataSource

StaticDataSource is a plain ArrayList-backed implementation of BaseDataSource. It is used in import wizards and test scenarios where entity data is supplied programmatically before any archive is open:

StaticDataSource ds = new StaticDataSource();
ds.setArchives(archiveList);
ds.setOrganizations(organizationList);
// ... etc.

StaticDataSourceBuilder constructs a StaticDataSource from the current EntityDataSource state for a specific archive.

Change listeners

EntityDataSource notifies UI components of data changes through two listener sets (both backed by weak references to prevent memory leaks):

Listener typeWhen notified
EntityDataUpdatedListenerAny entity refresh (legacy V1 interface)
EntityDataUpdatedListenerV2Any entity refresh (V2 with granular event details)
DocumentContentChangeListenerWhen an individual document's content (binary) changes

An internal EntityDataSourceUpdateListener implements EndPointListener and bridges IDocument.onDataUpdated() into the listener notification chain.

Observer Chain (XGAP-01)

The full data update chain from Nuxeo to the UI is:

NuxeoDocument (updaterThread, 60 s cycle)
└── IDocument.onDataUpdated(internalEvent)
└── EntityDataSourceUpdateListener.onDataUpdated()
└── EntityDataSource.notifyListeners(internalEvent)
└── SwingUtilities.invokeLater(...)
├── EntityDataUpdatedListener.handleEntityDataUpdate()
└── EntityDataUpdatedListenerV2.handleEntityDataUpdate(internalEvent)

Registration API:

// Register (weak reference — listener must stay alive via strong reference elsewhere)
EntityDataSource.addDataUpdatedListener(EntityDataUpdatedListener listener);
EntityDataSource.addDataUpdatedListener(EntityDataUpdatedListenerV2 listener);

// Deregister (optional — GC will clean up if listener is collected)
EntityDataSource.removeDataUpdatedListener(listener);

Known implementors of EntityDataUpdatedListenerV2:

  • BookmarkModel — refreshes bookmark list
  • UserVisibilityHelper — recomputes user-visible archives
  • MilestoneTrackerDialog — refreshes milestone status
  • TaskBar — updates task notification count
  • DocNavMetadataSelector — refreshes metadata field options

All UI-level listeners receive the event on the Swing EDT (via invokeLater), so it is safe to update UI components directly in handleEntityDataUpdate().

Triggering a manual refresh:

EntityDataSource.close() nulls the singleton and clears listener sets. The next getInstance() call reloads all entity data from the current endpoint. This is called on logout.


Temp Data and Visibility Cache

The tempdata package (suredms-desktop-client) provides fast in-memory caches that avoid repetitive Nuxeo queries:

ClassPurpose
ArchiveVisibilityCacheCaches user/role/group data and name dictionaries for the archive access rule editor
ArchiveAclRuleEditorHelperCRUD helper for archive-level ACL rules (desktop-side blinding rules)
UserVisibilityHelperHelper for computing user-specific archive visibility
DocNavTreeModel2Cached document navigator tree model (used in Injector for @Inject fields)

ArchiveVisibilityCache (XGAP-02)

Source: SC/suredms-desktop-client/src/main/java/com/sureclinical/suredms/tempdata/ArchiveVisibilityCache.java

Static in-memory cache for the archive access rule editor UI. Holds:

FieldTypeContents
USERSList<User>All users (for rule subject picker)
ALL_ROLESList<UserRole>All role definitions
USER_GROUPSList<ArchiveAclRuleGroup>All user group definitions
NAME_DICTIONARYMap<String, String>Login → "FirstName LastName (login)" display strings
ENTITY_DICTIONARYMap<String, String>Entity ID → display label

API:

MethodDescription
reloadVisibilityCache()Rebuilds all five caches from UserManager and ArchiveVisibilityHelper; synchronized
clear()Empties all caches; synchronized
getDisplayNameFromDictionary(objectId)Lazy-loads from EntityDataSource.getEntityById() if not yet cached
getDisplayNameForUserGroup(userGroupId)Returns display name for one user group
getDisplayNameForUserGroups(Collection<String>)Comma-joined display names for multiple groups
getUserGroupsById(List<String>)Returns List<ArchiveAclRuleGroup> for matching IDs
showOrganizationRolesForVisibility()Delegates to GlobalPreferenceManager.VISIBILITY_SHOW_ORGANIZATION_ROLES
showSystemRolesForVisibility()Delegates to GlobalPreferenceManager.VISIBILITY_SHOW_SYSTEM_ROLES

ArchiveAclRuleEditorHelper (XGAP-02)

Source: SC/suredms-desktop-client/src/main/java/com/sureclinical/suredms/tempdata/ArchiveAclRuleEditorHelper.java

Concrete helper for the desktop-side archive ACL rule editor (equivalent to the web client's Blinding Rules feature). Takes ArchiveVisibilityHelper globalVisibility and EntityDataSource dataSource in its constructor.

MethodDescription
static addEntitiesToRule(ArchiveAclRule, List<TaxonEntity>)Adds entities (folders, content types) to an existing rule; delegates to ArchiveAclHelper
addContentAndSaveChanges(Multimap<Archive, TaxonEntity>, ArchiveAclRuleGroup)Finds or creates one ArchiveAclRule per archive, adds entities, and calls globalVisibility.updateRules()
removeContentAndSaveChanges(TaxonEntity, ArchiveAclRuleGroup)Removes entity from existing rules; deletes rules with no remaining folder restrictions via globalVisibility.deleteRules()

Located in: suredms-desktop-client-connector/src/main/java/com/sureclinical/suredms/endpoints/DocumentProxy.java

DocumentProxy implements IDocument and EndPointListener. It holds a list of IDocument instances and merges their entity collections into a single view. When any child IDocument fires onDataUpdated(), DocumentProxy calls refill() to re-merge all sources.

Internally it maintains the same entity lists as EntityDataSource but without the singleton semantics — it is used by EndPointsProxy to aggregate multiple connection endpoints.

EndpointController

public interface EndpointController {
void setUpdaterThreadPaused(boolean paused);
void prefetchArchive(Archive archive) throws RemoteException;
void loadUpdates(NuxeoClient client);
}

Implemented by the background thread that polls Nuxeo for changes. Exposed via DocumentUpdater in the connector module.


Authentication Clients

BasicAuthAutomationClient

Located at: suredms-desktop-client-connector/.../nuxeo/client/BasicAuthAutomationClient.java

Extends HttpAutomationClient. Creates a SureConnector wrapping the shared CloseableHttpClient. HTTP Basic Auth header is added by the underlying Nuxeo client library.

FormAuthAutomationClient

Located at: suredms-desktop-client-connector/.../nuxeo/client/FormAuthAutomationClient.java

Extends BasicAuthAutomationClient. Overrides connect() to POST credentials to {url}isAlive before loading the operation registry:

List<NameValuePair> pairs = new ArrayList<>();
pairs.add(new BasicNameValuePair("user_name", username));
pairs.add(new BasicNameValuePair("user_password", password));
// Optional: MFA token
pairs.add(new BasicNameValuePair(AuthConstants.MFA_TOKEN_KEY, extendedLoginInfo.getMfaToken()));
// Optional: shared login access token
pairs.add(new BasicNameValuePair(AuthConstants.ACCESS_TOKEN, extendedLoginInfo.getSharedLoginRecord().getToken()));

HttpPost post = new HttpPost(url + "isAlive");
post.setEntity(new UrlEncodedFormEntity(pairs, Consts.UTF_8));

An existing cookie in the CookieStore skips the login step (SSO/shared-session semantics).

SureConnector

Located at: suredms-desktop-client-connector/.../nuxeo/client/SureConnector.java

Extends ConnectorImpl. Caches OperationRegistry and LoginInfo objects in two static ConcurrentHashMap caches, keyed by {URL} and {username}:{URL} respectively. This avoids re-fetching the operation registry on every session open.

NuxeoClientBuilder

A factory for standalone (server-side / integration test) NuxeoClient construction:

Static methodSource of credentials
initFromProperties()System properties: server.hostname, server.key, server.salt
initFromProperties(hostname)Explicit hostname + system properties for key/salt
initFromArguments(args)args[0] URL, args[1] key, args[2] salt
init(url, username, password)Explicit credentials

Password is derived via PkAuth.getPassword(key, salt). Retries up to 5 times on connection failure before re-throwing the last exception.


DocQueueService — Document Queue

Located at: suredms-desktop-client/src/main/java/com/sureclinical/suredms/docq/DocQueueService.java

DocQueueService is a singleton that manages the document queue (DocQItem list) used by the Acquire Queue UI. It listens to EndPoints.getCurrentEndPoints().getDocSvc() for data updates, which re-syncs the queue from the server.

State

FieldTypeDescription
itemsList<DocQItem>Current queue items
listenersSet<DocQListener>UI observers (WeakSet)
autoArchivebooleanIf true, queued documents are archived automatically

Events

DocQListener callbackWhen fired
onDocumentsAdded(items)New items added to the queue
onDocumentsRemoved(items)Items removed from the queue
onDocumentsArchived(items)Items successfully moved to archive

Barcode integration

DocQueueService uses BarcodeFactory and BarcodeWriter to generate barcode cover sheets for queue items. The BarcodeFactory is resolved at startup and determines which barcode type (QR, Code 128, etc.) is appropriate for the current archive configuration.


ServiceProvider — Service Registry

Located at: suredms-common/src/main/java/com/sureclinical/suredms/services/ServiceProvider.java

ServiceProvider is a thin wrapper around Java ServiceLoader. All services in the desktop application implement the IService marker interface. Services are loaded via SPI (META-INF/services files), cached by class, and sorted by priority.

API

// All implementations, sorted by priority descending
List<S> services = ServiceProvider.getServices(FeatureManager.class);

// Highest-priority implementation
S service = ServiceProvider.getService(FeatureManager.class);

// Client-side only (filters by ServiceType.ST_CLIENT)
S service = ServiceProvider.getClientService(FeatureManager.class);

// Server-side only (filters by ServiceType.ST_SERVER)
S service = ServiceProvider.getServerService(FeatureManager.class);

IService

public interface IService {
int getServicePriority();
ServiceType getServiceType(); // ST_CLIENT or ST_SERVER
}

All services return a priority integer. Higher values win when getService() selects a single implementation from a registered set.

Caching

Service class lists are cached in SERVICE_CLASSES (a ConcurrentHashMap<Class<?>, List<Class<?>>>) after the first ServiceLoader scan. Instances are re-created on each call to getService() via Class.newInstance() — services are not singletons at the ServiceProvider level (individual services may themselves be singletons).

For tests, SERVICE_CLASSES_FOR_TESTS allows injecting mock implementations alongside real ones.


FeatureManager — Feature Flag System

Located at: suredms-desktop-client-connector/src/main/java/com/sureclinical/suredms/services/features/

Interface

public interface FeatureManager extends IService {
boolean isFeatureEnabled(String featureKey);
boolean isFeatureEnabled(String featureKey, User user);
boolean isFeatureEnabled(String featureKey, String role);
boolean isImageViewEnabled();
FeatureSnapshotHelper getActiveConfiguration();
FeatureSnapshot getCurrentConfiguration();
void applySnapshot(FeatureSnapshot snapshot);
List<FeatureSnapshot> getSnapshots();
FeatureSnapshot loadSnapshotDetails(FeatureSnapshot snapshot);
void restoreSnapshot(long id);
void createSnapshot(String comment);
}

Implementations

ClassWhen used
NuxeoFeatureManagerEndPointsType.EP_REMOTE (online mode) — queries Nuxeo for feature configuration
SaveFeatureManagerEP_LOCAL or EP_DEMO — loads from local saved state

FeatureManagerImpl selects between these at construction time based on EndPoints.getCurrentEndPoints().getEndPointsType().

Snapshot mechanism

FeatureSnapshot is a named, timestamped snapshot of the active feature flag set. Snapshots can be created, listed, loaded in detail, and restored to roll back feature configuration changes.


JobManager — Background Job Queue

Located at: suredms-desktop-client-connector/src/main/java/com/sureclinical/suredms/services/job/JobManager.java

JobManager is a singleton thread-pool-based background job manager. It uses a PriorityBlockingQueue<Job> and a fixed pool of PoolWorker threads named SureDMS.PoolWorker{n}.

Job lifecycle

  1. Job is submitted to the queue.
  2. A free PoolWorker picks it up, fires EventType.STARTED.
  3. job.run() executes; errors are stored in job.getErrors().
  4. On success, fires EventType.FINISHED. On error, fires EventType.ERROR.

JobManagerListener

Registered via event subscription on DesktopClient. Fires JobManagerEvent with EventType.STARTED, FINISHED, or ERROR for progress and error reporting.

UiThreadPool

A companion to JobManager that submits Runnable tasks to the Swing EDT via SwingUtilities.invokeLater(). Used when a background job needs to update Swing components after completion.


Temp Data and Visibility Cache

The tempdata package (suredms-desktop-client) provides fast in-memory caches that avoid repetitive Nuxeo queries:

ClassPurpose
ArchiveVisibilityCacheCaches which users can see which archives (used in EntityDataSource filtering logic)
UserVisibilityHelperHelper for computing user-specific archive visibility
DocNavTreeModel2Cached document navigator tree model (used in Injector for @Inject fields)