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 views, 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:
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
:
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 views of this entity to display the extended attributes. Below we consider an example of overriding the list view of the Department
entity replaced by ExtDepartment
as described in the previous section.
To extend and override a view provided by an add-on, select Override an existing view template in the Studio view creation wizard. Studio will create new XML descriptor and controller files. The XML descriptor will contain the extends
attribute referring to the base view descriptor:
<view xmlns="http://jmix.io/schema/flowui/view"
title="msg://com.company.sample.base.view.department/departmentListView.title"
messagesGroup="com.company.sample.base.view.department"
extends="com/company/sample/base/view/department/department-list-view.xml">
After that, you can add components to display the extended attributes:
<view xmlns="http://jmix.io/schema/flowui/view"
title="msg://com.company.sample.base.view.department/departmentListView.title"
messagesGroup="com.company.sample.base.view.department"
extends="com/company/sample/base/view/department/department-list-view.xml">
<layout>
<dataGrid id="departmentsDataGrid">
<columns>
<column property="description"/>
<column property="manager"/>
</columns>
</dataGrid>
</layout>
</view>
You don’t have to repeat all elements and attributes of the base view, 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 view to avoid the possible "N+1 queries" problem:
<view xmlns="http://jmix.io/schema/flowui/view"
title="msg://com.company.sample.base.view.department/departmentListView.title"
messagesGroup="com.company.sample.base.view.department"
extends="com/company/sample/base/view/department/department-list-view.xml">
<data>
<collection id="departmentsDc"
class="com.company.sample.ext.entity.ExtDepartment">
<fetchPlan extends="_base">
<property name="manager" fetchPlan="_base"/>
</fetchPlan>
</collection>
</data>
<layout>
<dataGrid id="departmentsDataGrid">
<columns>
<column property="description"/>
<column property="manager"/>
</columns>
</dataGrid>
</layout>
</view>
The controller of the extended view will be inherited from the base view class:
@Route(value = "departments", layout = MainView.class)
@ViewController("base_Department.list")
@ViewDescriptor("ext-department-list-view.xml")
public class ExtDepartmentListView extends DepartmentListView {
Note that the |
You can override public and protected methods of the base controller to extend the view logic if needed.
Rules of XML Descriptor Extension
Extension of XML descriptors doesn’t take into account the semantics of the view and works purely on the XML level. It merges two XML files according to the following rules:
-
If the extending descriptor has a certain element, the corresponding element will be searched for in the parent descriptor using the following algorithm:
-
If the overriding element has the
id
attribute, the corresponding element with the sameid
will be searched for. Some elements are analyzed also for other attributes that serve as unique identifiers instead ofid
:-
For
button
element:action
attribute. -
For
column
element:property
andkey
attributes. -
For
property
element:name
attribute.
-
-
If the search is successful, the found element is overridden.
-
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.
-
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.
-
-
The text for the overridden or added element is copied from the extending element.
-
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.
-
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:
-
Define an additional namespace in the extending descriptor:
xmlns:ext="http://jmix.io/schema/flowui/view-ext"
. -
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:
-
Create its subclass in the application project:
public class ExtDepartmentService extends DepartmentService { @Override public void sayHello() { super.sayHello(); System.out.println("Hello from ext"); } }
-
Define a bean with the
@Primary
annotation in the main application class or in any@Configuration
class:@SpringBootApplication public class ExtApplication implements AppShellConfigurator { @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();
}