Extending Functionality

Functionality of framework subsystems and add-ons can be extended and modified by the application or by other add-ons located lower in the hierarchy.

For declaratively created elements, such as data model entities and XML layout of UI screens, Jmix offers its own extension mechanism. Business logic defined by Spring beans can be extended using standard Java and Spring features.

Extending Data Model

Let’s consider an example of extending the data model of an add-on.

Suppose we have the following entities defined in the base add-on:

Diagram

Their source code:

@JmixEntity
@Table(name = "BASE_DEPARTMENT")
@Entity(name = "base_Department")
public class Department {

    @JmixGeneratedValue
    @Column(name = "ID", nullable = false)
    @Id
    private UUID id;

    @InstanceName
    @Column(name = "NAME")
    private String name;

    // getters and setters
@JmixEntity
@Table(name = "BASE_EMPLOYEE", indexes = {
        @Index(name = "IDX_BASE_EMPLOYEE_DEPARTMENT", columnList = "DEPARTMENT_ID")
})
@Entity(name = "base_Employee")
public class Employee {

    @JmixGeneratedValue
    @Column(name = "ID", nullable = false)
    @Id
    private UUID id;

    @Column(name = "FIRST_NAME")
    private String firstName;

    @InstanceName
    @Column(name = "LAST_NAME")
    private String lastName;

    @JoinColumn(name = "DEPARTMENT_ID")
    @ManyToOne(fetch = FetchType.LAZY)
    private Department department;

    // getters and setters

In the ext application which uses the base add-on, we need to add the description and manager attributes to the Department entity. Obviously, we cannot modify the source code of the add-on, so we need to define another entity in the application and make other entities reference it instead of Department:

Diagram

The extended entity source code:

@JmixEntity
@Entity
@ReplaceEntity(Department.class) (1)
public class ExtDepartment extends Department { (2)

    @InstanceName
    @Column(name = "DESCRIPTION")
    private String description; (3)

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "MANAGER_ID")
    private User manager; (3)

    // getters and setters
1 The @ReplaceEntity annotation indicates that this entity must completely replace the parent entity, specified in the annotation value. This annotation is added by Studio if you select the Replace parent checkbox in the entity designer.
2 A standard inheritance of JPA entities. In this case, the base class doesn’t specify the JPA inheritance strategy, so the extension attributes will be stored in the same BASE_DEPARTMENT table.
3 The attributes added in the extension.

After introducing the ExtDepartment entity annotated with @ReplaceEntity in the application project, it will be returned everywhere in data access code instead of the Department entity. For example, you can safely cast references to the ExtDepartment class:

Employee employee = dataManager.load(Employee.class).id(employeeId).one();

ExtDepartment department = (ExtDepartment) employee.getDepartment();
String description = department.getDescription();

Also, the ExtDepartment entity meta-class will be returned by the metadata API for both ExtDepartment and Department Java classes. The original entity meta-class can be obtained using the ExtendedEntities bean.

Extending UI

If you replace an entity of an add-on by an extended version, most probably you will also need to override UI screens of this entity to display the extended attributes. Below we consider an example of overriding the browse screen of the Department entity replaced by ExtDepartment as described in the previous section.

To extend and override a screen provided by an add-on, select Override an existing screen template in the Studio screen creation wizard. Studio will create new XML descriptor and controller files. The XML descriptor will contain the extends attribute referring to the base screen descriptor:

<window xmlns="http://jmix.io/schema/ui/window"
        caption="msg://modularity.sample.base.screen.department/departmentBrowse.caption"
        extends="modularity/sample/base/screen/department/department-browse.xml">

After that, you can add components to display the extended attributes:

<window xmlns="http://jmix.io/schema/ui/window"
        caption="msg://modularity.sample.base.screen.department/departmentBrowse.caption"
        extends="modularity/sample/base/screen/department/department-browse.xml">
    <layout>
        <groupTable id="departmentsTable">
            <columns>
                <column id="description"/>
                <column id="manager"/>
            </columns>
        </groupTable>
    </layout>
</window>

You don’t have to repeat all elements and attributes of the base screen, only the changed part is required. The resulting XML will be merged from the base and extended descriptors - see more about it below.

In our case, one of extended attributes (manager) is a reference to another entity. This reference will be loaded on demand due to the automatic lazy loading, but you may want to include the reference to the fetch plan of the screen to avoid the possible "N+1 queries" problem:

<window xmlns="http://jmix.io/schema/ui/window"
        caption="msg://modularity.sample.base.screen.department/departmentBrowse.caption"
        extends="modularity/sample/base/screen/department/department-browse.xml">
    <data>
        <collection id="departmentsDc"
                    class="modularity.sample.ext.entity.ExtDepartment">
            <fetchPlan extends="_base">
                <property name="manager" fetchPlan="_base"/>
            </fetchPlan>
        </collection>
    </data>
    <layout>
        <groupTable id="departmentsTable">
            <columns>
                <column id="description"/>
                <column id="manager"/>
            </columns>
        </groupTable>
    </layout>
</window>

The controller of the extended screen will be inherited from the base controller class:

@UiController("base_Department.browse")
@UiDescriptor("ext-department-browse.xml")
public class ExtDepartmentBrowse extends DepartmentBrowse {

Note that the @UiController annotation has the same value as in the base screen. This is important because we actually need to override the base screen, meaning that everywhere in the system the extended screen will be used instead of the base one, as is done for replaced entities.

You can override public and protected methods of the base controller to extend the screen logic if needed.

Rules of XML Descriptor Extension

Extension of XML descriptors doesn’t take into account the semantics of the screen and works purely on the XML level. It merges two XML files according to the following rules:

  1. If the extending descriptor has a certain element, the corresponding element will be searched for in the parent descriptor using the following algorithm:

    1. If the overriding element has the id attribute, the corresponding element with the same id will be searched for.

    2. If the search is successful, the found element is overridden.

    3. Otherwise, the framework determines how many elements with the provided path and name are contained in the parent descriptor. If there is only one element, it is overridden.

    4. If the search yields no result and there is either zero or more than one element with the given path and name in the parent descriptor, a new element is added.

  2. The text for the overridden or added element is copied from the extending element.

  3. All attributes from the extending element are copied to the overridden or added element. If attribute names match, the value is taken from the extending element.

  4. By default, the new element is added to the end of the list of adjacent elements. In order to add a new element to the beginning or with an arbitrary index, do the following:

    1. Define an additional namespace in the extending descriptor: xmlns:ext="http://jmix.io/schema/ui/window-ext".

    2. Add the ext:index attribute with a desired index, for example: ext:index="0" to the extending element.

Overriding Spring Beans

All Jmix subsystems use Spring beans by their type and never by bean name. Therefore, beans can be overridden just by providing alternative implementations with the same or extended type. We recommend following this convention in your own add-ons and applications.

To override a Spring bean defined in an add-on, create its subclass (or implement the same interface) and declare a bean of this new type in a Java configuration, adding the @Primary annotation to the new bean.

For example, suppose you have the following bean in the base add-on:

@Component("base_DepartmentService")
public class DepartmentService {

    public void sayHello() {
        System.out.println("Hello from base");
    }
}

You can override it in an application as follows:

  1. Create its subclass in the application project:

    public class ExtDepartmentService extends DepartmentService {
    
        @Override
        public void sayHello() {
            super.sayHello();
            System.out.println("Hello from ext");
        }
    }
  2. Define a bean with the @Primary annotation in the main application class or in any @Configuration class:

    @SpringBootApplication
    public class ExtApplication {
    
        @Primary
        @Bean
        ExtDepartmentService extDepartmentService() {
            return new ExtDepartmentService();
        }

After that, Spring container will always return ExtDepartmentService instead of DepartmentService, so any call to sayHello() method even from the base add-on will print both "Hello from base" and "Hello from ext" messages. Of course, you are free not to call super() in your overriding methods and hence completely replace the inherited behavior.

In a rare situation when you need to override a bean which already has a subclass marked as @Primary, you can use the jmix.core.exclude-beans application property to remove other primary beans from the container.

Modules API

The JmixModules bean allows you to get information about modules used in your application: the list of all modules, the last module in the list (which is normally the application), a module descriptor by module id. The getPropertyValues() method returns the list of values defined for a property by each module.

The JmixModulesAwareBeanSelector bean is designed for selecting an effective implementation of some interface from a given list. It returns a bean belonging to the lowest module in the hierarchy. For example, if you know that multiple add-ons define their own implementations of the AmountCalculator interface, and you want to use the one defined in the lowest module of the hierarchy, you can do it as follows:

@Autowired
ApplicationContext applicationContext;
@Autowired
JmixModulesAwareBeanSelector beanSelector;

BigDecimal calculate() {
    Map<String, AmountCalculator> calculators = applicationContext.getBeansOfType(AmountCalculator.class);
    AmountCalculator calculator = beanSelector.selectFrom(calculators.values());
    return calculator.calculate();
}