Migration from CUBA Platform

Overview

Jmix framework is a direct successor of CUBA platform. It offers the same rapid application development approach but with modernized infrastructure. You can think of Jmix as a new major version of CUBA, so if your CUBA project is in active development, consider upgrading to Jmix. To facilitate this process, we provide a compatibility module with a set of CUBA APIs and features that were changed or removed in Jmix. Also, Jmix Studio contains an advanced procedure for converting a CUBA project into Jmix one.

In this chapter, we describe the main differences between Jmix and CUBA and give you detailed instructions on how to migrate your CUBA project to Jmix.

Differences from CUBA

Project Structure

A CUBA project consists of at least three modules: global, core, web. In Jmix, project has a single module by default.

In a CUBA project, source files of all types (Java, Kotlin, XML, properties) are mixed in the same directories under the single source root (one per module). Jmix project follows the standard Maven directory layout when there are separate roots for files of different types:

  • src/main - sources of production code

  • src/test - test sources

  • src/main/java, src/test/java - for Java classes

  • src/main/kotlin, src/test/kotlin - for Kotlin files

  • src/main/resources, src/test/resources - for XML and property files.

Deployment Structure

A CUBA application is built into two separate web applications: core and web. Even if you use a "single WAR" or "uber JAR" deployment, these artifacts actually contain the core and web classes isolated in different classloaders and running in separate Spring contexts.

A Jmix application is a single web application built as an executable JAR or as a WAR using standard Spring Boot Gradle deployment tasks.

Application Properties

Due to the separate deployment of CUBA modules, a CUBA project properties are set in two files: app.properties for the core module and web-app.properties for the web module. These files are located in the base package of the corresponding module.

In Jmix, all properties are defined in a single application.properties file located in the src/main/resources root.

Spring Configuration

In CUBA, Spring container configuration is defined in spring.xml for the core module and web-spring.xml for the web module.

Jmix uses only Java-based container configuration. Spring Boot automatically scans all classes in the base package of the application (where the class annotated with @SpringBootApplcation is located) and packages below it for Spring annotations.

Data Model

CUBA data model entities have to extend one of the base classes (StandardEntity, BaseUuidEntity, BaseGenericIdEntity, etc.) and optionally implement some interfaces (Versioned, Creatable, Updatable, SoftDelete).

Jmix doesn’t force you to inherit your entities from the framework’s base classes. Instead, you should just annotate your class with @JmixEntity and that’s it - the framework will enhance the class' bytecode to implement the features previously implemented by the CUBA entity base classes. Optional behavior (see traits) is also declared with annotations like @CreatedBy, @CreatedDate, etc. Of course, you can create your own base classes with required characteristics on the project level. The jmix-cuba compatibility module provides a set of base classes equivalent to the CUBA ones, so you don’t have to rewrite your entities when migrating to Jmix.

CUBA JPA entities have to be registered in the persistence.xml file located in the base package of the global module. In Jmix, the persistence.xml file is created automatically at build time by the Jmix Gradle plugin. The plugin scans the classpath and writes all @JmixEntity-annotated classes into <base-package>/persistence.xml inside the application JAR/WAR file.

CUBA non-persistent entities and custom datatypes are registered in the metadata.xml file. There is no such file in Jmix:

  • DTO entities are recognized just by the @JmixEntity annotation.

  • Custom datatype classes are annotated with @DatatypeDef which makes them Spring beans and allow the framework to find and register them at startup.

DataManager

The io.jmix.core.DataManager bean in Jmix core is very similar to the TransactionalDataManager in CUBA: it joins an existing transaction and has the save() method instead of commit(). But it has a crucial difference in handling security permissions: in Jmix, io.jmix.core.DataManager checks resource and row-level policies by default, and there is separate io.jmix.core.UnconstrainedDataManager that bypasses all access control rules. See details in the Authorization section.

The jmix-cuba compatibility module provides the com.haulmont.cuba.core.global.DataManager bean that has the same interface and behavior as in CUBA with respect to security: it checks only row-level policies and has the secure() method which returns a DataManager implementation that checks also the resource policies.

Transactions

Unlike CUBA, Jmix doesn’t provide any specific interfaces for transaction management. You should use either declarative transaction demarcation with the @Transactional annotation on bean methods, or programmatic management with Spring’s TransactionTemplate class.

The jmix-cuba compatibility module provides the com.haulmont.cuba.core.Persistence and com.haulmont.cuba.core.Transaction interfaces that save you from rewriting your business logic working with CUBA APIs.

EntityManager

In Jmix, code working with JPA should use the standard JPA EntityManager, Query and TypedQuery interfaces. The EntityManager is obtained using injecton with @PersistenceContext annotation, for example:

@PersistenceContext
private EntityManager entityManager;

With the jmix-cuba compatibility module, you can keep using com.haulmont.cuba.core.EntityManager obtained through com.haulmont.cuba.core.Persistence.

EntityPersistingEvent

The CUBA’s EntityPersistingEvent is superseded by EntitySavingEvent which is sent both on persisting new entities and updating existing ones. The old EntityPersistingEvent is provided by the jmix-cuba compatibility module and is sent only when saving new instances as it was in CUBA.

EntitySavingEvent and EntityPersistingEvent are sent only when saving entities using DataManager. If you persist entities using EntityManager, EntityPersistingEvent is not sent, regardless of the presence of the jmix-cuba module.

Fetch Plans and Lazy Loading

Jmix supports partial loading of entities by using fetch plans, which are the same as views in CUBA. In addition, Jmix also supports lazy loading of references for JPA entities loaded using DataManager, so use of fetch plans is optional and should be considered as a performance optimization. See details in the Fetching Data section.

The jmix-cuba module provides the com.haulmont.cuba.core.global.View, ViewBuilder and ViewRepository classes for backward compatibility.

Security

Jmix resource roles and resource policies are very similar to CUBA roles and permissions. The main difference is how they are defined in design time: CUBA roles use classes, Jmix roles use interfaces.

Jmix row-level roles have the same purpose as CUBA access group constraints, but there are significant differences:

  • Jmix row-level roles are stored in a plain list instead of a hierarchy;

  • a user can have any number of row-level roles;

  • there is no equivalent for predefined session attributes of access groups.

The Studio migration procedure converts CUBA design-time roles into Jmix resource roles automatically. Access groups and constraints have to be converted manually, see Changed APIs section for details.

The migration procedure will preserve your list of users in the database, but all runtime security configuration (roles, policies, role assignments) will have to be done from scratch.

Session Attributes

If you need to share some values across multiple requests from the same connected user, use the SessionData bean. It has methods for reading and writing named values stored in the current user session.

You can inject the SessionData bean into UI screens directly:

public class MyScreen extends Screen {

    @Autowired
    private SessionData sessionData;

In a singleton bean, use SessionData through org.springframework.beans.factory.ObjectProvider:

@Service
public class MyService {

    @Autowired
    private ObjectProvider<SessionData> sessionDataProvider;

    public void saveSessionValue(String value) {
        sessionDataProvider.getObject().setAttribute("my-attribute", value);
    }

When handling UI requests, the shared values are stored in the HTTP session.

If you want to share session attributes between REST API requests authenticated with the same token, add the following dependency to your build.gradle:

implementation 'io.jmix.sessions:jmix-sessions-starter'

For backward compatibility, the jmix-cuba module provides the com.haulmont.cuba.security.global.UserSession class that delegates its getAttribute() / setAttribute() methods to SessionData.

Features Removed in Jmix

Below is a list of CUBA features that were removed in Jmix without replacement.

  • Attribute access control on the DataManager level. Entity attribute permissions are now considered only when displaying data in UI components and returning entities via REST API endpoints. See Data Access Checks.

  • State-based entity attribute access control with SetupAttributeAccessHandler and SetupAttributeAccessHandler.

  • Screen component permissions.

  • Session attributes defined in the Access Groups.

  • ClusterManagerAPI interface and its implementation.

  • Editor screen opening history and @TrackEditScreenHistory annotation.

  • Support for Microsoft SQL Server 2005 with net.sourceforge.jtds.jdbc.Driver.

Changed APIs

Below is a list of changed APIs that are not converted by the Studio automatic migration and have no compatibility wrappers in jmix-cuba module. Use this information when fixing your code for compilation.

Access groups and constraints

Convert the annotated class to an interface. The interface methods should return void and are used merely for grouping annotations. See details in the Row-level Roles section.

  • com.haulmont.cuba.security.app.group.annotation.AccessGroupio.jmix.security.role.annotation.RowLevelRole

  • com.haulmont.cuba.security.app.group.annotation.JpqlConstraintio.jmix.security.role.annotation.JpqlRowLevelPolicy

  • com.haulmont.cuba.security.app.group.annotation.Constraintio.jmix.security.role.annotation.PredicateRowLevelPolicy.

Security configuration entities

Below are rough equivalents of entities used to configure security at runtime:

  • com.haulmont.cuba.security.entity.Roleio.jmix.securitydata.entity.ResourceRoleEntity

  • com.haulmont.cuba.security.entity.Groupio.jmix.securitydata.entity.RowLevelRoleEntity

  • com.haulmont.cuba.security.entity.UserRoleio.jmix.securitydata.entity.RoleAssignmentEntity

  • com.haulmont.cuba.security.entity.Permissionio.jmix.securitydata.entity.ResourcePolicyEntity

  • com.haulmont.cuba.security.entity.Constraintio.jmix.securitydata.entity.RowLevelPolicyEntity

Multitenancy

After running the automatic migration procedure, follow the steps below.

  1. Add the StandardTenantEntity to your project:

    package com.company.app.entity; // replace with your base package
    
    import com.haulmont.cuba.core.entity.StandardEntity;
    import io.jmix.core.annotation.TenantId;
    import io.jmix.core.metamodel.annotation.JmixEntity;
    
    import javax.persistence.Column;
    import javax.persistence.MappedSuperclass;
    
    @MappedSuperclass
    @JmixEntity
    public abstract class StandardTenantEntity extends StandardEntity {
    
        private static final long serialVersionUID = -1215037188627831268L;
    
        @TenantId
        @Column(name = "TENANT_ID")
        protected String tenantId;
    
        public void setTenantId(String tenantId) {
            this.tenantId = tenantId;
        }
    
        public String getTenantId() {
            return tenantId;
        }
    }

    In all entities extended from the CUBA StandardTenantEntity, replace the import of com.haulmont.addon.sdbmt.entity.StandardTenantEntity to the import of your own StandardTenantEntity.

  2. In the User entity, implement the AcceptsTenant interface and add the tenant attribute annotated with @TenantId and mapped to the SYS_TENANT_ID column:

    public class User implements JmixUserDetails, HasTimeZone, AcceptsTenant {
        // ...
    
        @TenantId
        @Column(name = "SYS_TENANT_ID")
        private String tenant;
    
        public String getTenant() {
            return tenant;
        }
    
        public void setTenant(String tenant) {
            this.tenant = tenant;
        }
    
        @Override
        public String getTenantId() {
            return tenant;
        }
    }
  3. Add tenant attribute to the user browse and edit screens as described in items 3, 4, 5 of the Multitenancy / Configuring Users section.

  4. Rename CUBASDBMT_TENANT table to MTEN_TENANT using the following Liquibase changeset (it’s needed only in Jmix 1.1.0, because jmix-cuba module in Jmix 1.1.1+ contains this changeset):

    <changeSet id="10" author="me">
        <preConditions onFail="MARK_RAN">
            <tableExists tableName="CUBASDBMT_TENANT"/>
        </preConditions>
    
        <renameTable oldTableName="CUBASDBMT_TENANT" newTableName="MTEN_TENANT"/>
    </changeSet>

Reports

  • com.haulmont.reports.app.service.ReportService, com.haulmont.reports.gui.ReportGuiManagerio.jmix.reports.runner.ReportRunner

Entity snapshots

  • com.haulmont.cuba.core.app.EntitySnapshotServiceio.jmix.audit.snapshot.EntitySnapshotManager

  • com.haulmont.cuba.gui.app.core.entitydiff.EntityDiffViewerio.jmix.auditui.screen.snapshot.SnapshotDiffViewer

  • <frame id="diffFrame" src="/com/haulmont/cuba/gui/app/core/entitydiff/diff-view.xml"/><fragment id="diffFrame" screen="snapshotDiff"/>

Email sending

  • com.haulmont.cuba.core.app.EmailServiceio.jmix.email.Emailer

  • com.haulmont.cuba.core.global.EmailInfoBuilder#setCaptionio.jmix.email.EmailInfoBuilder#setSubject

How To Migrate

Jmix Studio provides an automatic procedure for converting a CUBA project into Jmix one. It creates a new project with a standard Jmix template and then copies the source code from your CUBA project into the new structure inside the new Jmix project. While copying, Studio makes a lot of changes in the source files: replaces packages and known framework classes, converts screen XML descriptors to the new schema, configures your database connections, adds dependencies to the new add-ons. After the migration procedure completes, you should fix the remaining problems manually.

The migration procedure keeps your CUBA project untouched, so it’s safe to run the procedure on any working copy of a project.

There are the following limitations of the automatic migration:

  • Projects using HSQLDB as a main data store may have an invalid connection string. We recommend switching your project to a different database before migration.

  • Test classes are not copied to the Jmix project.

In Jmix Studio v.1.1.4 and below, the migration procedure may fail if your IntelliJ IDEA contains Kotlin plugin of a version newer than 1.5.10. In such a case, downgrade Kotlin plugin to 1.5.10 or below.

In Jmix Studio v.1.1.5 and above, the migration does not have a dependency on Kotlin plugin.

Main Migration

Follow the steps below to run the automatic migration procedure.

  1. Open your CUBA project in Jmix Studio.

  2. Wait until the project is imported and fully indexed. Watch the IDE progress bar and wait until it stops displaying new messages.

  3. You should see a notification about migrating to Jmix in the bottom right corner. Click Migrate or select File → New → Jmix project from this CUBA project in the main menu of the IDE.

    The notification could not appear if the project was opened and imported to the IDE before. In this case, click Reload All Gradle Projects button in the Gradle tool window.

  4. Studio starts the New Jmix project wizard.

  5. Select the latest Jmix version (at least 1.1.0) and the project JDK used in your CUBA project. Click Next.

  6. On the next step of the wizard, enter the new Jmix project name and location. Click Finish.

  7. Studio creates a new project with the specified Jmix template and starts the migration process. You will see a notification about it in the bottom right corner of the IDE.

    When the migration is finished, Studio creates the MigrationResult.md file and opens it in the editor window. The file describes what has been done automatically and recommendations on what should be done manually.

  8. Add required dependencies to the build.gradle file. The migration procedure adds only the known Jmix counterparts of the CUBA add-ons.

  9. Your next goal is to compile the project. Click Build → Build Project in the IDE main menu.

    See compilation errors in the build output panel and fix your code to comply with the new API. Use the information from the Changed API section above.

  10. After successful compilation, check the main database connection in the Data Stores section of the Jmix tool window.

    Jmix Studio will modify the database schema and run some updates automatically. Never use production databases at the development stage!
  11. To update an existing CUBA database to be compatible with the new Jmix application, do the following:

    1. Ensure that application.properties file contains the line:

      jmix.liquibase.contexts = cuba
    2. Click Update in the context menu of the Main Data Store item. Studio will run Liquibase changelogs that come with jmix-cuba module. If the process is finished successfully, your database is compatible with Jmix modules included in the project.

  12. Now you can run the application using the Jmix Application run/debug configuration.

    By default, it first checks the database schema and generates a Liquibase changelog if it differs from the application data model. Review the generated changelog carefully and remove from it all potentially dangerous instructions like drop and alter. You can use Remove and Ignore command in the Changelog Preview window to remove a selected instruction. Then your choice will be remembered in the jmix-studio.xml file of your project, and when you run the application next time, the ignored instructions will not be generated.

  13. To create a new empty database for your application, do the following:

    1. Change Liquibase context in application.properties:

      jmix.liquibase.contexts = migrated
    2. Replace all appearances of the users table name in resources/<base-package>/liquibase/changelog/010-init-user.xml to SEC_USER. For example: <createTable tableName="APP_USER"><createTable tableName="SEC_USER">, etc.

    3. Click Recreate in the context menu of the Main Data Store item. Studio will drop/create the database and run Liquibase changelogs from all Jmix modules.

    4. Run the application using the Jmix Application run/debug configuration. Studio will generate a Liquibase changelog for entities in your data model. Alternatively, you can create a changelog file manually and add all SQL statements from the CUBA project create-db.sql files using Liquibase sql instructions.

File Storage

Local file storage structure in Jmix is the same as in CUBA. You can just move all files from the work/filestorage directory of your CUBA application to the Jmix file storage directory which is {user.dir}/.jmix/work/filestorage by default and can be changed by the jmix.localfs.storageDir property.

Make sure that in screen descriptors, upload fields working with FileDescriptor attributes are defined as cuba:cubaUpload.

Jmix CUBA compatibility module of version 1.1.2 and below has an issue with the _instance_name fetch plan (called _minimal in CUBA). To work around it, specify _base or _local fetch plan instead of _instance_name (or _minimal) for attributes of the FileDescriptor type in screens.

WebDAV

This section describes how to migrate code and data related to the WebDAV add-on.

  1. Add the premium repository and add-on dependencies to your build.gradle:

    repositories {
        // ...
        maven {
            url = 'https://global.repo.jmix.io/repository/premium'
            credentials {
                username = rootProject['premiumRepoUser']
                password = rootProject['premiumRepoPass']
            }
        }
    }
    
    dependencies {
        implementation 'io.jmix.webdav:jmix-webdav-starter'
        implementation 'io.jmix.webdav:jmix-webdav-ui-starter'
        implementation 'io.jmix.webdav:jmix-webdav-rest-starter'
        // ...

    Refresh the project using Load Gradle Changes popup at the top right corner of the edit window or using Reload All Gradle Projects action of the Gradle tool window.

  2. Replace CUBA WebDAV packages with Jmix ones throughout your codebase:

    • com.haulmont.webdav.entity.io.jmix.webdav.entity.

    • com.haulmont.webdav.annotation.io.jmix.webdav.annotation.

    • com.haulmont.webdav.components.io.jmix.webdavui.component.

  3. Fix WebDAV UI components declaration in your screen XML descriptors.

    • Replace webdav schema URI : xmlns:webdav="http://schemas.haulmont.com/webdav/ui-component.xsdxmlns:webdav="http://jmix.io/schema/webdav/ui

    • Replace component XML elements:

      • document-linkdocumentLink

      • document-version-linkdocumentVersionLink

      • webdav-document-uploadwebdavDocumentUpload

  4. Jmix WebDAV add-on works only with attributes of WebdavDocument type, so if you have FileDescriptor attributes annotated with @WebdavSupport, you should change the attribute type and migrate data stored in the corresponding column. Let’s consider this process on an example.

    Suppose you have the following entity with a FileDescriptor attribute supporting WebDAV:

    @JmixEntity
    @Table(name = "DEMO_DOC")
    @Entity(name = "demo_Doc")
    public class Doc extends StandardEntity {
    
        @WebdavSupport
        @OneToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "FILE_ID")
        private FileDescriptor file;

    First, replace FileDescriptor type with WebdavDocument:

    @JmixEntity
    @Table(name = "DEMO_DOC")
    @Entity(name = "demo_Doc")
    public class Doc extends StandardEntity {
    
        @WebdavSupport
        @OneToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "FILE_ID")
        private WebdavDocument file;

    @WebdavSupport annotation is not required in this case, but it can be used to disable versioning.

    If there are WebdavDocumentLink components created for this attribute, replace withFileDescriptor() invocations with withWebdavDocument().

    Next you need to create a Liquibase changelog updating data of the FILE_ID column. Create an XML file (choose an appropriate name, for example 020-migrate-webdav.xml) in the src/main/resources/<base-package>/liquibase/changelog directory with the following content:

    <?xml version="1.0" encoding="UTF-8"?>
    <databaseChangeLog
            xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
                          http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd"
            context="cuba">
    
        <changeSet id="1" author="demo">
            <dropForeignKeyConstraint baseTableName="DEMO_DOC"
                                      constraintName="FK_DEMO_DOC_ON_FILE"/>
            <update tableName="DEMO_DOC">
                <column name="FILE_ID" valueComputed="(select wd.id
    from webdav_webdav_document_version wdv, webdav_webdav_document wd
    where wdv.file_descriptor_id = FILE_ID and wdv.webdav_document_id = wd.id)"/>
            </update>
            <addForeignKeyConstraint baseColumnNames="FILE_ID" baseTableName="DEMO_DOC"
                                     constraintName="FK_DEMO_DOC_ON_FILE" referencedColumnNames="ID"
                                     referencedTableName="WEBDAV_WEBDAV_DOCUMENT"/>
        </changeSet>
    
    </databaseChangeLog>

    In general, you should create such changelogs for each FileDescriptor attribute which you have turned into WebdavDocument. The changelogs should match the following pattern:

    <changeSet id="{NUM}" author="sample">
        <dropForeignKeyConstraint baseTableName="{ENTITY_TABLE_NAME}"
                                  constraintName="{FK_FOR_DOCUMENT}"/>
        <update tableName="{ENTITY_TABLE_NAME}">
            <column name="{DOCUMENT_COLUMN_NAME}" valueComputed="(select wd.id
    from webdav_webdav_document_version wdv, webdav_webdav_document wd
    where wdv.file_descriptor_id = {DOCUMENT_COLUMN_NAME} and wdv.webdav_document_id = wd.id)"/>
        </update>
        <addForeignKeyConstraint baseColumnNames="{DOCUMENT_COLUMN_NAME}"
                    baseTableName="{ENTITY_TABLE_NAME}"
                    constraintName="{FK_FOR_DOCUMENT}" referencedColumnNames="ID"
                    referencedTableName="WEBDAV_WEBDAV_DOCUMENT"/>
    </changeSet>

    where

    • {NUM} - number of the changelog in the file.

    • {ENTITY_TABLE_NAME} - entity table name.

    • {FK_FOR_DOCUMENT} - foreign key for referenced FileDescriptor.

    • {DOCUMENT_COLUMN_NAME} - name of the FileDescriptor column.

    Click Update in the context menu of the Main Data Store item. Studio will run existing Liquibase changelogs.

    When you start the application, Studio will generate Liquibase changelogs for the difference between the database schema and your data model. Remove the instruction to drop the FILE_DESCRIPTOR_ID.WEBDAV_WEBDAV_DOCUMENT_VERSION column from the changelog (use Remove and Ignore command in the Changelog Preview window):

    <dropColumn columnName="FILE_DESCRIPTOR_ID"
                tableName="WEBDAV_WEBDAV_DOCUMENT_VERSION"/>

    Keep this column until you complete the migration.

    Start the application, go to Administration → JMX Console and open the jmix.cuba:type=MigrationHelper MBean. Execute the convertCubaFileDescriptorsForWebdav() operation.

  5. Set up HTTPS for your application. See the Configuring HTTPS guide for how to do it with a self-signed certificate.

  6. Move your local file storage content as described above.

Frontend

If your project has a frontend module created with CUBA React client, you can migrate it to Jmix as follows:

  1. Copy public, src directories and all files from the root of modules/front directory of your CUBA project into front directory of your new Jmix project.

  2. See Jmix Frontend UI → Migration from CUBA guide for further instructions.