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/Osuredms-desktop-client— concrete implementations includingEntityDataSource,DocQueueService, and the dependency-injection systemsuredms-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 connectionXMLEndPoints— XML-backed offline store (archive file mode)XmlDemoEndPoints— XML-backed demo/test stub (used in UI integration tests)
IAuth
Authentication service interface:
| Method | Description |
|---|---|
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:
| Method | Return 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:
| Method | Description |
|---|---|
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:
| Value | Title | Online |
|---|---|---|
EP_REMOTE | Online | Yes |
EP_LOCAL | Offline | No |
EP_DEMO | Demo Mode | Yes |
Each value carries an EndPointParameters instance holding baseURL, username, password, and rootFolder.
FetchMode
Two-value enum:
| Value | Description |
|---|---|
ASYNCHRONOUS | Data is fetched in a background thread |
SYNCHRONOUS | Data 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
| Field | Type | Description |
|---|---|---|
allAvailableArchives | List<Archive> | All archives visible to the current user |
archives | Map<Long, EntityData> | Per-archive entity data, keyed by archive ID |
entities | Map<String, BaseEntity> | All entities by Nuxeo UUID |
reports | List<Document> | Report documents |
uniqueOrganizationsCache | List<Organization> (nullable) | Cached unique org list; invalidated on refresh |
userManagerDataCache | UserManagerData (nullable) | Cached user management data |
REPORT_DOCUMENT_IDS | Map<String, List<String>> (concurrent) | Report document IDs per archive |
Implemented interfaces
| Interface | Purpose |
|---|---|
DocumentProvider | Provides Document lookup by ID/UUID |
BaseDataSource | Provides per-archive lists of orgs, persons, roles |
EntityAndUserProvider | Combines 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 type | When notified |
|---|---|
EntityDataUpdatedListener | Any entity refresh (legacy V1 interface) |
EntityDataUpdatedListenerV2 | Any entity refresh (V2 with granular event details) |
DocumentContentChangeListener | When 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 listUserVisibilityHelper— recomputes user-visible archivesMilestoneTrackerDialog— refreshes milestone statusTaskBar— updates task notification countDocNavMetadataSelector— 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:
| Class | Purpose |
|---|---|
ArchiveVisibilityCache | Caches user/role/group data and name dictionaries for the archive access rule editor |
ArchiveAclRuleEditorHelper | CRUD helper for archive-level ACL rules (desktop-side blinding rules) |
UserVisibilityHelper | Helper for computing user-specific archive visibility |
DocNavTreeModel2 | Cached document navigator tree model (used in Injector for @Inject fields) |
ArchiveVisibilityCache (XGAP-02)
Static in-memory cache for the archive access rule editor UI. Holds:
| Field | Type | Contents |
|---|---|---|
USERS | List<User> | All users (for rule subject picker) |
ALL_ROLES | List<UserRole> | All role definitions |
USER_GROUPS | List<ArchiveAclRuleGroup> | All user group definitions |
NAME_DICTIONARY | Map<String, String> | Login → "FirstName LastName (login)" display strings |
ENTITY_DICTIONARY | Map<String, String> | Entity ID → display label |
API:
| Method | Description |
|---|---|
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)
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.
| Method | Description |
|---|---|
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 method | Source 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
| Field | Type | Description |
|---|---|---|
items | List<DocQItem> | Current queue items |
listeners | Set<DocQListener> | UI observers (WeakSet) |
autoArchive | boolean | If true, queued documents are archived automatically |
Events
DocQListener callback | When 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
| Class | When used |
|---|---|
NuxeoFeatureManager | EndPointsType.EP_REMOTE (online mode) — queries Nuxeo for feature configuration |
SaveFeatureManager | EP_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
- Job is submitted to the queue.
- A free
PoolWorkerpicks it up, firesEventType.STARTED. job.run()executes; errors are stored injob.getErrors().- On success, fires
EventType.FINISHED. On error, firesEventType.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:
| Class | Purpose |
|---|---|
ArchiveVisibilityCache | Caches which users can see which archives (used in EntityDataSource filtering logic) |
UserVisibilityHelper | Helper for computing user-specific archive visibility |
DocNavTreeModel2 | Cached document navigator tree model (used in Injector for @Inject fields) |