Dependency Injection
The SureClinical Desktop Client uses a lightweight, reflection-based dependency injection framework implemented in the com.sureclinical.suredms.inject package within suredms-desktop-client. It is not backed by a container such as Spring or Guice — injection is performed manually by calling Injector.inject(this) from a constructor or factory method.
Package Overview
All classes are in:
suredms-desktop-client/src/main/java/com/sureclinical/suredms/inject/
| Class / interface | Role |
|---|---|
Injectable | Marker interface; any class that can receive injected values |
DefaultInjectable | Base class that calls Injector.inject(this) from its constructor |
Injector | Static utility that performs the actual injection via reflection |
InjectableResources | Constants for well-known injectable resource keys |
@Inject | Field annotation: injects a service or well-known value |
@Component | Field annotation: creates and injects a Swing UI component |
Injectable and DefaultInjectable
Injectable is an empty marker interface:
public interface Injectable {
}
Any class that wants to participate in injection must either implement Injectable or extend DefaultInjectable.
DefaultInjectable is the preferred base class for view panels and form controllers:
public class DefaultInjectable implements Injectable {
public DefaultInjectable() {
Injector.inject(this);
}
}
Classes extending DefaultInjectable receive injection automatically when their constructor runs — no separate inject() call is needed.
Injector
Injector is a static utility class (private constructor). Its single public method is:
public static void inject(Injectable injectable)
How it works
- Starts at
injectable.getClass(). - Walks the class hierarchy using
currentClass.getSuperclass()until reachingObject,JComponent, orJPanel. - For each class in the hierarchy, iterates over
getDeclaredFields(). - For each field annotated with
@Inject, resolves a value from the injection context and sets it via reflection. - For each field annotated with
@Component, creates a Swing component instance and sets it via reflection.
@Inject resolution
Fields annotated with @Inject are resolved by type:
| Field type | Resolved value |
|---|---|
IService subtype | ServiceProvider.getService(fieldType) |
EntityDataSource | EntityDataSource.getInstance() |
User | EndPoints.getCurrentEndPoints().getAuthSvc().getUserInfo() |
DocNavTreeModel2 | DesktopClient.getInstance().getDocNavTreeModel() |
ResourceMap | Application.getInstance().getContext().getResourceMap(injectable.getClass()) |
String with property = "username" | EndPoints.getCurrentEndPoints().getAuthSvc().getUserInfo().getUsername() |
If resolution fails (value is null), a WARN log entry is emitted but no exception is thrown — the field is left at its default (usually null).
@Component resolution
Fields annotated with @Component are resolved by type and annotation attributes:
| Field type | values attribute | Created component |
|---|---|---|
JComboBox / JTypedComboBox | "uniqueOrganizationNames" | JTypedComboBox populated from entityDataSource.getUniqueOrganizationNames() |
JComboBox / JTypedComboBox | "uniqueOrganizationRoleNames" | JTypedComboBox of org role names |
JComboBox / JTypedComboBox | "uniquePersonRoleNames" | JTypedComboBox of person role names |
JComboBox / JTypedComboBox | "countryCodes" | JTypedComboBox from LocaleUtils.getCountryCodes() |
JLabel | resource attribute | Created via JComponentFactory.label(resource) |
| Other Swing types | — | Created via JComponentFactory using the resource attribute for localised text |
After creation, attributes from @Component are applied:
editable=false→jcomponent.setEnabled(false)name→jcomponent.setName(name)fontBold=true→jcomponent.setFont(font.deriveFont(Font.BOLD))fontSize > 0→jcomponent.setFont(font.deriveFont((float) fontSize))
Annotations
@Inject
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
String property() default "";
}
The property attribute is used to qualify which property of the resolved object to inject (e.g., property = "username" resolves to the current user's username string). For most service types, property is empty and the type alone determines the injected value.
@Component
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
String resource() default ""; // BSAF ResourceMap key for label text
String values() default ""; // Predefined value list key
boolean editable() default false;
boolean enabled() default true;
String name() default ""; // JComponent.setName()
int fontSize() default -1;
boolean fontBold() default false;
}
InjectableResources
Constants for the values attribute of @Component and the property attribute of @Inject:
| Constant | Value | Used for |
|---|---|---|
C_UNIQUE_ORG_NAMES | "uniqueOrganizationNames" | @Component(values=...) on a combo box |
C_UNIQUE_ORG_ROLE_NAMES | "uniqueOrganizationRoleNames" | @Component(values=...) on a combo box |
C_UNIQUE_PERSON_ROLE_NAMES | "uniquePersonRoleNames" | @Component(values=...) on a combo box |
C_COUNTRY_CODES | "countryCodes" | @Component(values=...) on a combo box |
P_USERNAME | "username" | @Inject(property=...) on a String field |
Usage Pattern
In a view panel
public class TeamMemberPanel extends DefaultInjectable {
@Inject
private EntityDataSource entityDataSource;
@Inject
private FeatureManager featureManager;
@Component(values = "uniqueOrganizationNames")
private JTypedComboBox<String> organizationCombo;
@Component(resource = "TeamMemberPanel.nameLabel")
private JLabel nameLabel;
public TeamMemberPanel() {
super(); // triggers Injector.inject(this)
// entityDataSource, featureManager, organizationCombo, nameLabel are already populated
}
}
Manual injection
Classes that cannot extend DefaultInjectable (e.g., those already extending another class) call injection explicitly:
public class MyDialog extends JDialog implements Injectable {
@Inject
private EntityDataSource entityDataSource;
public MyDialog() {
Injector.inject(this);
}
}
Relationship to ServiceProvider
Injector resolves IService-typed fields via ServiceProvider.getService(). This means any IService implementation registered via SPI (META-INF/services) can be injected into any Injectable class without hard-coding a class reference.
Example: a field of type FeatureManager is resolved by:
Injectordetects the field type isFeatureManager extends IService.- Calls
ServiceProvider.getService(FeatureManager.class). ServiceProviderloads allFeatureManagerimplementations viaServiceLoader.- Returns the highest-priority instance (either
NuxeoFeatureManagerorSaveFeatureManager).
This design keeps UI components decoupled from implementation choice and allows the same panel class to work in online, offline, and test modes.
Limitations
- No scope management: all services are re-created by
ServiceProvideron each injection. Services that need singleton semantics manage their own state (e.g.,JobManager.getInstance()). - No circular dependency detection: circular injection will cause a
StackOverflowError. - No conditional injection: there is no
@Optionalequivalent. If a service is missing, aWARNis logged and the field stays null. - EDT safety:
Injector.inject()may be called from the EDT (when initiated from a constructor in response to a UI action) or from a background thread (during wizard initialisation). The caller is responsible for EDT safety of the injected components.