Upgrading Commerce Manager
Upgrading Commerce Manager
Changes to the underlying technology and structure of Commerce Manager from Elastic Path Commerce 6.17 to Elastic Path Commerce 7.0 necessitate a detailed understanding of these changes when upgrading an existing Elastic Path project to 7.0.
Understanding Commerce Manager 7.0
Prior to Elastic Path 7.0, Commerce Manager was a desktop client application. As of Elastic Path 7.0, Commerce Manager is a web application built with Eclipse RAP. This allows for the preservation of most business logic and code between Elastic Path Commerce Manager 6.17 and 7.0. However, it produces the following architecture-level changes:
- Commerce Manager no longer uses an HTTP bridge to communicate with the Commerce Manager Server webapp and Elastic Path Commerce services and databases. Instead, this responsibility is directly integrated in the Commerce Manager 7.0 web application.
- Quartz jobs were moved from Commerce Manager Server webapp to a new Batch Server webapp.
- Commerce Manager is now multi-user and can be scaled horizontally behind a load-balancer with sticky sessions.
- FTP is no longer required to send and receive files to the Commerce Manager webapp.
These architecture changes necessitate the following the following code-level changes, as well as an overall UI update. These are examined in detail in the following sections.
Threading
Commerce Manager 7.0 has two types of threads: UI threads and worker threads. The UI Thread is responsible for UI updates, and provides the user's session. Worker threads (or background threads) are used for all non-UI tasks. This includes service calls, database access, business logic and the spawning of additional background jobs. Worker threads cannot update the UI or access session or RWT instance variables, causing an InvalidThreadException if attempted.
In general, each user session has a UI thread, and utilizes worker threads from a Thread Pool.
Multi-User Application Session Isolation
Commerce Manager 7.0 is a multi-user application: many users can access the same application instance. This means that session isolation – the ability for classes to share data between each other – must be preserved. A session cannot update another session's data or UI elements. As such the use of Singleton classes is dangerous and highly discouraged.
Scopes
As previously stated, Commerce Manager now exists outside of a single user's session. Certain services and beans may require scoping for a single user's session, or to the entire application context. For more information, see the Eclipse RAP Scope Documentation.
User Interface
Commerce Manager no longer features a File menu or status bar. If your functionality was accessed via these menus, you will need to add items to Commerce Manager's main toolbar to accommodate.
Entry Points
As a web application, Commerce Manager requires a distinction between the idea of application startup and an application entry point. The startup of an application versus a user's login are different processes, and Eclipse RAP applications require one or more entry points. RAP creates a separate instance of an entry point for every UI session.
ServerPush Asynchronous Notifications
Eclipse RAP applications use a long poll approach to asynchronously push updates from the server to the UI, as a web server cannot send an update to the browser without a request. Eclipse RAP calls this approach a server push: the browser sends a request to the web server and returns data only when it becomes available. When a browser receives a response from the long poll, it will send another long poll request, allowing more server data to be sent. For more information, see Eclipse RAP's Server Push Documentation.
Below is an example of instantiating a server push session:
private final ServerPushSession pushSession = new ServerPushSession(); ... private void startTimedTableRefresh() { final Display disp = Display.getCurrent(); final TimerTask task = new TimerTask() { @Override public void run() { disp.syncExec(() -> refreshViewerInput()); } }; timer.scheduleAtFixedRate(task, 0, SearchIndexesView.PERIOD); pushSession.start(); } ... @Override public void dispose() { timer.cancel(); pushSession.stop(); super.dispose(); }
Migrating Plugins From a Legacy Project
Fundamentally, your migration of legacy Commerce Manager extensions will take the format of an extended plugin in Commerce Manager 7.0. Read Extending Commerce Manager in addition to this guide for a full understanding of the extension model.
General Migration Strategy
- Ensure the plugin works in 6.17.
- The plugin should work in 6.17 before being upgraded to 7.0.
- Move the plugin to the commerce-extensions project.
- Extensions to out of the box Commerce Manager should live in the commerce-extensions project. This allows them to depend on the ext-core project correctly.
- Change the MANIFEST.MF file dependencies to RAP.
- Replace the RCP dependencies in the META-INF/MANIFEST.MF file with RAP dependencies.
- Change the NLS implementation to RAP.
- If your plugin needs to work in a multi-user/locale environment, there need to be localization changes. If there is only a single locale being used, this is not necessary.
- Remove Singletons.
- Singletons cause InvalidThreadAccess Exceptions. In order to mitigate this, replace singleton variables with session-scoped variables, using the CmSingletonUtil.
- Fix unit tests.
- Existing unit tests may require access to session variables, as well as the Display object. In order to do this, include the RAP TestContext as part of the test setup.
- Ensure the plugin is deployed with the rest of Commerce Manager webapp.
- New plugins need to be included in the feature.xml and config.ini files in the extension webapp project. Otherwise, the new plugin will not start up.
- Startup Commerce Manager and ensure there are no Display issues.
- There may be calls to update the UI from non-UI threads. These need to be delegated to the UI thread by using the Display object.
Configuration File Changes
Adding to commerce-extension's feature.xml
You must include your new extension plugin in commerce-extensions/cm/ext-cm-webapp/feature.xml as follows, where PLUGIN_ID is the ID of your plugin:
<plugin id="PLUGIN_ID" download-size="0" install-size="0" version="0.0.0" unpack="false"/commerce-legacy/>
Your plugin ID can be found in your plugin's MANIFEST.MF file as the Bundle-SymbolicName, or in its pom.xml file as the artifactId.
Removing from Out of the Box feature.xml
If you are migrating a plugin from out of the box Commerce Manager to the extensions project, then you will need to remove the plugin from the out of the box Commerce Manager's feature.xml file in commerce-manager-client/com.elasticpath.cmclient.platform.feature/.
Adding New Plugin Dependencies
If your plugin depends on a bundle that is not registered in either out of the box or commerce-extensions' feature.xml , you need to add it as a dependency to commerce-extensions' feature.xml:
To do this, add a new plugin entry with the required Bundle ID. Ensure that you have the correct version or version range, as per Eclipse OSGi's Version Range Documentation.
Updating MANIFEST.MF
The plugin being migrated must depend on the Eclipse RAP libraries instead of the Eclipse RCP libraries.
Edit your plugin's META-INF/MANIFEST.MF file with the following changes:
Your project should now compile when running mvn clean install from commerce-extensions. If this does not succeed, you may have to replace/remove other dependencies in the MANIFEST.MF file, or change specific Java imports to the RAP version.
Updating plugin.xml
All plugins must wait until a user session exists before running any display related code. Any calls in the AbstractEpUIPlugin.start() method that should run on the display thread need to be moved to an EarlyStartup class.
Early Startup classes should be registered in commerce-extensions' plugin.xml file using the org.eclipse.ui.startup extension point as follows:
<extension point="org.eclipse.ui.startup"> <startup class="com.elasticpath.cmclient.PACKAGE.PLUGIN_NAME"/commerce-legacy/> </extension>
Code Changes
Plugin.java
All plugins must wait until a user session exists before running any display related code. Any calls in the AbstractEpUIPlugin.start() method that should be run on the display thread will need to be deferred until after the user workbench is created.
To do this, register with CorePlugin using registerPreStartupCallback() or registerPostStartupCallback():
@Override public void start(final BundleContext context) throws Exception { super.start(context); //If change set is disabled or then remove change set toolbar from the CoolBar CorePlugin.registerPostStartupCallback(new HideActionSetRunnable(changeSetHideCondition, ACTION_SET_ID)); }
NLS Language Localization
NLS (localized) strings need to be converted from a single-user/single-locale to multiple-user/multiple-locale.
Prior to 7.0, strings were localized when the bundle was loaded, and were kept as constants of the Message class. As of 7.0, the localization strings are kept as instance fields of the Message class. An instance of the Message class must be created for each user, which is handled via the LocalizedMessagePostProcessor class.
Each Message class will need to implement a static Message.get() method, to expose the call to LocalizedMessagePostProcessor, which returns a specific instance that is localized to the users' locale.
All calls to the static Message class's static variables have to be converted to use the field variables.
For example, a new TermsAndConditionsMessages class could have the following method:
public String TermsAndConditionsDisplayMessage; public static TermsAndConditionsMessages get() { return LocalizedMessagePostProcessor.getUTF8Encoded(TermsAndConditionsMessages.BUNDLE_NAME, TermsAndConditionsMessages.class); }
A class uses it in the following manner:
layoutComposite.addLabelBold(TermsAndConditionsMessages.get().TermsAndConditionsDisplayMessage, data);
The PluginResource.properties file does not change for migrating to 7.0. However, one will be required for each locale, with the specific locale as a suffix.
For instance, the TermsAndConditions plugin has the files:
Beans
Replace all calls of Application.getInstance().getBean() with ServiceLocator.getService(). For example:
Pre-7.0:
private final ChangeSetService changeSetService = Application.getInstance().getBean(ContextIdNames.CHANGESET_SERVICE);
7.0:
private final ChangeSetService changeSetService = ServiceLocator.getService(ContextIdNames.CHANGESET_SERVICE);
Singletons
Singletons are instances of a class that are shared across the entire JVM. They are only instantiated once, and all threads that use the class will use the same instance. This makes it easy to share data between threads.
However, this is a very bad design pattern when in a multiple-user application, as each user needs their own instance of specific classes.
The pre 7.0 CM client used Singletons throughout the code base. Any plugin that is to run in 7.0 CM must not contain singletons that are shared between users, otherwise, the users will encounter Threading issues.
Every plugin must convert their singletons into session instances, which are shared within a session, but not between different sessions.
- Identify any Singletons, and remove the singleton instance variable.
- Convert the getInstance() method to CmSingletonUtil.getSessionInstance().
For example, the `AdminUsersEventService` was converted from the Following pre-7.0:
public final class AdminUsersEventService { private static AdminUsersEventService instance = new AdminUsersEventService(); /** * Private constructor following the singleton pattern. */ private AdminUsersEventService() { super(); } /** * Gets a singleton instance of AdminUsersEventService. * * @return singleton instance of AdminUsersEventService */ public static AdminUsersEventService getInstance() { if (AdminUsersEventService.instance == null) { AdminUsersEventService.instance = new AdminUsersEventService(); } return AdminUsersEventService.instance; } }
To the following:
public final class AdminUsersEventService { /** * Private constructor following the session-singleton pattern. */ private AdminUsersEventService() { super(); } /** * Gets a session instance of AdminUsersEventService. * * @return session instance of AdminUsersEventService. */ public static AdminUsersEventService getInstance() { return CmSingletonUtil.getSessionInstance(AdminUsersEventService.class); } }
Image Registry
If your plugin has an ImageRegistry, you need to ensure that it is Thread Safe. To do this, add a static initializer to the ImageRegistry class, and initialize all the images in the static block. Ensure that the getImage() method does not update any of the existing images.
Unit/Integration Tests
If a unit test is using any of the new RAP functionality, such as instance field message strings, or calling the Display thread, they require an Eclipse RAP TestContext in order to pass.
To do this, ensure that there are no static initialization of fields, other than the TestContext, moving the field initialization to the setUp() method annotated with @Before. Next, include the TestContext as a field in the Unit test, as follows:
@Rule public TestContext context = new TestContext();
and ensure that the TestContext is part of the plugins Require-Bundle section with the following entry:
org.eclipse.rap.rwt.testfixture;resolution:=optional
Test IDs
Ensure all Widgets, such as Buttons, Combos, TextFields, MenuItems are created through EpControlFactory so that test ids will be added to those widgets.