Skip to main content

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 / interfaceRole
InjectableMarker interface; any class that can receive injected values
DefaultInjectableBase class that calls Injector.inject(this) from its constructor
InjectorStatic utility that performs the actual injection via reflection
InjectableResourcesConstants for well-known injectable resource keys
@InjectField annotation: injects a service or well-known value
@ComponentField 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

  1. Starts at injectable.getClass().
  2. Walks the class hierarchy using currentClass.getSuperclass() until reaching Object, JComponent, or JPanel.
  3. For each class in the hierarchy, iterates over getDeclaredFields().
  4. For each field annotated with @Inject, resolves a value from the injection context and sets it via reflection.
  5. 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 typeResolved value
IService subtypeServiceProvider.getService(fieldType)
EntityDataSourceEntityDataSource.getInstance()
UserEndPoints.getCurrentEndPoints().getAuthSvc().getUserInfo()
DocNavTreeModel2DesktopClient.getInstance().getDocNavTreeModel()
ResourceMapApplication.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 typevalues attributeCreated 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()
JLabelresource attributeCreated via JComponentFactory.label(resource)
Other Swing typesCreated via JComponentFactory using the resource attribute for localised text

After creation, attributes from @Component are applied:

  • editable=falsejcomponent.setEnabled(false)
  • namejcomponent.setName(name)
  • fontBold=truejcomponent.setFont(font.deriveFont(Font.BOLD))
  • fontSize > 0jcomponent.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:

ConstantValueUsed 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:

  1. Injector detects the field type is FeatureManager extends IService.
  2. Calls ServiceProvider.getService(FeatureManager.class).
  3. ServiceProvider loads all FeatureManager implementations via ServiceLoader.
  4. Returns the highest-priority instance (either NuxeoFeatureManager or SaveFeatureManager).

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 ServiceProvider on 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 @Optional equivalent. If a service is missing, a WARN is 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.